mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-10-24 04:33:17 +00:00
d52131e0d1c3452d60d0a59262dbbcd419bd2332
5889 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
1896f1f2ba |
Stop the Restarts and Lifecycle headers clipping at tablet widths (#2555)
Two column headers were clipped at tablet widths.
**Docker containers "Restarts".** The header clipped to "RES…" between
600 and 800px, and widening the column clipped "Memory" instead.
- Below a 720px table the restart count now lives in the row drawer, as
it already did on phones.
- So the crash-loop warning stays visible on narrow screens, a running
container that has restarted more than five times shows "7 restarts" in
amber in its State cell there.
- Sorting by State uses that text.
**Kubernetes Configuration "Lifecycle / trust".** The header clipped to
"LIFECYCLE / …" through the tablet band.
- The shared responsive header label gains `compactInBand`, which keeps
the short label it already shows on phones ("Status") through that band.
- Other headers are unchanged.
Verification:
- Header sweep from 390 to 1920px against main: no clipped header at any
width, and no column clips more cells than before.
- Rebased onto the Docker drawer health change (#2534) and re-verified.
- Sol's two findings are fixed: the lost crash-loop signal, and State
sorting.
- Tests and guardrails added, receipt included.
|
||
|
|
f279dc0b07 |
Resolve masked webhook values per key and keep responses masked
The webhook editor sends back the ***REDACTED*** values the list returns for every header, custom field and signing secret the user did not retype. Three paths mishandled them: - Update restored the whole saved header or custom-field map whenever any value was masked, silently dropping a header or field added in the same edit. - Update echoed the restored saved custom fields, so a Pushover token and user key came back in the response even though the list masks them, in breach of the api-contracts webhook response clause. Create echoed the request, and could store a masked value verbatim. - The editor's Test button posts the form, so testing an edit after a reload sent the literal mask as the token or header. Resolve each masked value to the saved value of the same key on update and on a form test that carries a saved webhook's id, keep values typed in the same request, and drop masks with no saved value. Build list, create and update responses from one masked shape. Normalization now drops blank headers, so clearing a header removes it instead of sending an empty Content-Type that generic receivers cannot parse. |
||
|
|
417cd7f675 | Merge remote-tracking branch 'origin/main' into fix/tablet-header-clips | ||
|
|
bbccd624b2 |
Size Alert History columns to content so messages read (#2537)
## Problem
The desktop Alerts → History table gave every visible column an equal
share: 16.7%, 14.3% or 11.1%, depending on how many columns the card can
show. So Message, the column that says what happened, got the same
100–160px as Timestamp:
- **Message:** at most 21 of 106 messages read in full at any width
("Kubernetes Clust…", "Docker container…"). Users had to open each row
to see what fired.
- **Status:** "acknowledged" (98px) was cut at a 1024px viewport, where
Status got 100px.
## Change
Each `<col>` now reads its width from a per-column custom property
(`--alert-history-<column>-width`), which the table sets at each density
in `index.css`. The existing visibility classes and density breakpoints
are unchanged, plus a new density at an 80rem card. Shares are sized for
the narrowest card at each density:
- **Content-sized:** Timestamp, Severity, Duration, Status (fits
"acknowledged"), Type and Node get what their content needs.
- **Protected:** Resource and Node keep at least their old share, so
they don't truncate more.
- **Actions:** the row buttons stay within two lines, and fit on one
line from an 80rem card.
- **Message:** takes the rest.
The table width still compensates for the collapsed tracks (it's the sum
of all nine track widths), so the visible columns fill the card exactly.
`App.architecture.test.ts` checks that arithmetic at every density, and
`Alerts.helpers.test.ts` pins the per-column tracks.
## Verification
Mock-mode private stack on main
|
||
|
|
5b335fdfed |
Stop the Restarts and Lifecycle headers clipping at tablet widths
Docker containers clipped its Restarts header to "RES..." between 600
and 800px, and widening the column clipped Memory instead. Below a 720px
table the count now waits in the row drawer, as it already did on
phones, and a running container that restarted more than five times
says so in its State cell there ("7 restarts"), so the crash-loop
warning stays visible on narrow screens; State sorts by that same text.
Kubernetes Configuration clipped "Lifecycle / trust" to "LIFECYCLE /
..." through the tablet band. The shared responsive header label gains
compactInBand, which keeps the compact label it already shows on phones
("Status") through that band; other headers are unchanged.
|
||
|
|
9615e46d25 |
Register agent command channels under the identity Pulse resolves (#2550)
## Problem An agent's command channel could be refused for good after a restart whenever the ID it presented differed from the host ID Pulse resolved for its reports. This applied to every install path. - The first command-channel registration bound the install token to whatever ID the agent presented, and a binding never moves. - The agent registered before its first report, under a configured `--agent-id`, or under an ID Pulse had forked for a cloned machine ID or kept from an earlier enrollment. - Pulse resolved its reports to another host ID and acknowledged that as `agentId`, and the agent persisted it. - The config gate judges the resolved ID, so it switched the operator's commands off. - The next restart registered under the persisted ID: "Agent token mismatch: token is not bound to the registering agent", indefinitely. This was the remaining gap called out in #2542. ## Why not move the binding at report ingest The obvious fix is to rebind the token to the resolved ID when a report arrives. It isn't safe. Identity resolution matches a report onto a host's continuity record by machine ID without checking the token, and a token-machine binding then keeps that resolution stable. A holder of one host's token could send one report that resolves onto another host's retained record, then a second claiming the bound ID, and move the token's command identity across hosts. A forged report and a legitimately stale one look identical, so this change moves no binding from report contents. ## Fix **Server (`internal/api/agent_ingest.go`)** - Report acknowledgements name `commandAgentId`, the resolved host ID, only when the token's binding already holds it, or when a registration presenting it would take the first bind, backfill, legacy migration, hostname rebind or one-time deploy repair it would get anyway. - The credential must be one command admission takes (`agent:exec`, at most one organization, legacy or no runtime role), and the host must be held under the reporting token, so mock acks name nothing. - The ID is judged for the hostname the report presents. - Naming grants nothing that registration wouldn't. The config gate takes the same decision. **Agent (`internal/hostagent`)** - The command channel stays staged until Pulse acknowledges a report, then registers under the named identity. - When Pulse names none (an older Pulse, or a token already bound to the presented ID such as an existing `--agent-id` install), it registers under the agent's own ID as before. - Only a real ack negotiates the identity: `success` set, a host named, and a report that presents this process's agent ID and hostname. An unreadable or non-ack body, a replayed buffered report from an earlier run, or an observer ack neither names nor releases the channel. - A 403 (the token lacks report scope, so Pulse can never answer) releases the channel under the agent's own ID, keeping today's behaviour for exec-only tokens. - A client Pulse has admitted is never rebuilt, because that would reopen its receipt store and mark in-flight operations interrupted. A client Pulse never admitted is retired and rebuilt under the named identity, behind a compare-and-swap so it can't be admitted afterwards. - A client a concurrent disable has already cleared can no longer take the run slot. ## Tests - **Agent (`internal/hostagent/command_client_test.go`):** - registers under the acknowledged identity, and waits for an accepted report; - keeps its own ID when none is named, and keeps the acknowledged ID through a later ack that names none and through a disable/enable; - ignores unreadable and non-ack bodies; - releases on 403; - leaves an admitted running client in place, and moves a never-admitted one; - ignores observer acks and replayed buffered reports; - lets no superseded client take the run slot; - CAS exclusivity, plus a real websocket handshake test. - **Server:** - `TestHostAgentCommandChannelBindsTheIdentityPulseAcknowledges`: end to end through the router. A configured-ID install goes through report, registration, restart, and the gate keeping commands on. - `TestContract_UnifiedAgentReportAckNamesCommandIdentityOnlyWhenTheBindingAdmitsIt`: the field is present only when admitted, and absent for bound-elsewhere, no-exec, collector and two-org tokens. - Security cases in `security_regression_test.go`: - an established binding never moves; - a hostname-bound deploy token backfills the resolved ID; - the deploy placeholder repair lands on the resolved ID; - an agent reconfigured after #2547 recorded its ID is named that ID; - mock acks name nothing. - **Fail without the fix:** the behavioural agent and server tests were run against the previous `agent.go` and `agent_ingest.go` and fail there, apart from the compatibility and invariant checks (own ID kept, established binding never moves), which pass either way by design. The tests for the newest guards use new seams, so the mutation checks below cover them instead. - **Mutation checks:** each guard was dropped in turn (real-ack check, current-identity check, never-admitted retirement, handshake admission, admitted-client protection, binding decision, role check, org check, token-held check), and each mutation fails at least one test. - **Locally:** the full `internal/hostagent` and `cmd/pulse-agent` suites pass; the new agent tests pass under `-race`; the governance guards and audits pass. ## Not in this change - Tokens already bound to a diverged ID are not recovered automatically, for the reason above. Re-running the install command issues a fresh token, which binds the resolved identity. - If Pulse later changes a host's resolved ID (a rename heal), it is named only where the existing binding rules already allow that ID. |
||
|
|
8b4df6e1fa |
Use the shared link and button primitives in two remaining spots (#2549)
This clears the last shared-template audit findings. Combined with #2546, the audit then passes. - **Backup export and import warnings:** the "migration guide" links were raw `target="_blank"` anchors. They now use `ExternalTextLink`. Its new `inlineInherit` variant keeps the warning's own amber or yellow colour and underline, so a link inside a tinted callout sentence doesn't switch to link blue. - **External watchdog:** the Refresh and Show/Hide buttons now use the shared outline `Button` instead of a copied border shell. On desktop they look and measure the same as on main (26px and 38px). On phones they become 44px touch targets. - `BackupTransferDialogs.tsx` is now a registered required consumer of `ExternalTextLink`, and the guardrail pins the new variant. Verification: - Checked live on a mock stack against main, in light and dark mode at 1440px and at 390px. - The links keep their callout colour, stay inline and open in a new tab with noopener. - The receipt is included. |
||
|
|
b12af35847 |
Paint the backgrounds the dead bg-base class dropped (#2548)
`bg-base` has rendered nothing since the `base` colour alias was removed (it also coloured the `text-base` font-size utility). Tailwind 4 ignores unknown colour classes without warning, so about 85 uses were transparent. On the page background that looked the same. Inside cards it did not: - command, JSON and XML blocks lost their wells (External agents, Agent Doctor, installer, update guide, Ollama, SAML preview, setup wizard); - the Patrol mode segmented control showed its selected option as a white chip on a white track; - the branding header preview vanished into its card; - the Dead Man's Switch "Not configured" pill read as plain text; - the alert bulk-selection bar showed white count text on a see-through pill. What changed: - New `@utility bg-page` (the page backdrop, deliberately not a theme colour) for wells inside surface cards. - `bg-surface` for blocks on a surface-alt body (resource drawer JSON, correlation and policy cards) and for the floating bulk bar; `bg-surface-alt` for the muted Card tone; `hover:bg-surface-hover` for drawer buttons whose hover did nothing. - The class is dropped from page shells that sit directly on `<body>`, which already paints the page colour. - Command text that assumed a dark well (`green-400`, `blue-400`, `emerald-400`, `slate-300`) gets light-mode shades so it stays readable. - `App.architecture.test.ts` fails if a runtime class uses `bg-base` again or a theme colour named `base` comes back. Verified on a private mock stack before and after, in light and dark, at 1440px and 390px; details are in the browser receipt. A few surfaces can't be reached with mock data (the drawer change history and its coloured buttons, Quick Security Setup, the profile-suggestion JSON, the Patrol tool-call trace). Their new tokens follow the same container rules, and the drawer button hover was measured with the exact classes in the live drawer. |
||
|
|
f77c8b2f3d |
Register agent command channels under the identity Pulse resolves
A token binds the first agent ID its command channel presents and never moves. The agent registered that channel before its first report, under its configured --agent-id, or an ID Pulse had forked for a cloned machine ID or kept from an earlier enrollment. Pulse meanwhile resolved its reports to another host ID and acknowledged it, and the agent persisted that ID. The config gate, which judges the resolved ID, then turned the operator's commands off, and the next restart under the persisted ID was refused for good. Report acknowledgements now name commandAgentId: the resolved host ID, only when the token's binding already holds it or a registration presenting it would take the first bind, backfill, legacy migration, hostname rebind or deploy repair it would get anyway. The credential must also be one command admission takes, and the host must be held under the reporting token. Naming grants nothing that registration would not. An established binding to another ID is never moved from report contents, because resolution can map a report onto another host's continuity record by machine ID, so a forged report and a stale one look the same. The config gate takes the same decision. The agent keeps its channel staged until a report is acknowledged and registers under the named identity, or under its own ID when none is named, which is what an older Pulse or an already-bound token sees. An unreadable ack keeps it waiting, and a 403 releases it under its own ID because Pulse can never answer that token. A running client is never rebuilt, since that would reopen its receipt store under in-flight operations. A superseded client can no longer take the run slot. Tokens already bound to a diverged ID stay as they are; re-running the install command issues a fresh token that binds the resolved identity. |
||
|
|
d961e0b18f |
Let agents without a command channel reduce their authority (#2547)
## Problem The safe-profile migration's authority reduction (`POST /api/agents/collector/reduce-authority`) needs the agent's ID on its token: `bound_agent_id` must be set and equal the ID the installer sends. Until now only a command-channel registration wrote that ID. Deploy enrollment (since rcourtman/Pulse#2542), Proxmox auto-registration and pre-v6.1.1 installs all leave a token bound to the node's hostname with no agent ID. The ID is filled in only at the agent's first command registration. An agent whose token lacks `agent:exec`, or that never connects the command channel, never gets an ID on its token. The reduction then refuses it with "Collector credential binding mismatch" for as long as the agent runs. Before #2542, deploy tokens instead carried an invented `agent-<hostname>`, which failed the same check. ## Fix The report path now records the agent's identity on such a token, under the same rules as command binding: - **When it binds.** After `/api/agents/agent/report` resolves a report, a token with a `bound_hostname` and no `bound_agent_id` records the server-resolved host ID. This happens only when the report presented that same ID. In the same run that is the ID the agent's command registration presents. It is also the `agentId` the response acknowledges, which the agent persists and the installer sends to the reduction. - **Same decision as the command channel.** `agentbinding.EvaluateReportedIdentity` records only where `Evaluate` would first-bind, legacy-migrate or backfill that ID for the reported hostname. It also requires: - a token bound to at most one organization, whose command-session organization matches the report's; - a collector, legacy full-trust or empty runtime role; - a token that has not expired. - **Only a host Pulse actually holds.** The resolved host must be one the monitor holds under that token (`host.TokenID`). Mock mode's discarded, unresolved acknowledgements therefore never qualify, however mock mode is toggled. - **Judged on the report's own hostname.** That is the hostname the agent's command registration presents, and it matters because a report rejected by the ordering watermark is answered with the stored host. - **Nothing else changes.** The path writes `bound_agent_id`, `bound_at` and the binding version. The binding version makes the ID immutable to every later registration. Scopes, runtime role, command policy and `bound_hostname` are never touched. A bound ID never moves and a deploy placeholder is never repaired. A recorded identity grants no execution authority: command admission still needs `agent:exec` and the same ID. - **Same store and lock as command admission.** The write goes to the router's canonical token store and persistence, under the token lock command admission holds, against the live record. A failed save restores the previous metadata and the next report retries. - **Cheap screen first.** The handler checks the request's token snapshot before taking the exclusive token lock, so already-bound or ineligible tokens cost nothing per report. Contracts updated: agent-lifecycle, security-privacy, api-contracts and storage-recovery. ## Behaviour change: the first recorded identity is final As with a command registration, the first identity recorded is final. Take an agent that reported under its machine ID while it had no command channel, for example with commands off. If it is later restarted under a different `--agent-id`, its command registration under the new ID is refused on that token. A report replayed from an earlier run's buffer has the same effect. This is exactly what happens today when that first run's command channel did connect. `TestReportedIdentityIsFinalForALaterReconfiguredAgent` pins the behaviour. Command-channel identity convergence lifts it: that follow-up to #2542 has the agent register under the identity Pulse names in the report ack, and it builds on this decision. ## Tests - **`TestContractDeployEnrolledCollectorReducesItsAuthorityAfterItsFirstReport`** (`internal/api/contract_test.go`) replays enrollment without commands, the first report and the reduction, through the real router. It checks that the token's scopes and runtime role are unchanged and that command admission is still refused. It fails without the fix: the reduction returns 403. - **Boundary tests in `internal/api/security_regression_test.go`:** - `TestReportedIdentityMatchesCommandRegistrationAndNeverMoves`: the recorded ID matches the agent's own command registration; neither a second identity's registration nor another machine's reports moves it; an ID the command channel bound first survives a report that resolves elsewhere. - `TestReportedIdentityRecordsNothingWhenTheResolvedIdentityIsNotThePresentedOne`: an agent with a configured `--agent-id` records nothing, and its command registration is still admitted. - `TestReportedIdentityIsFinalForALaterReconfiguredAgent`: pins the trade-off above. - `TestReportedIdentityJudgesAReplayedReportByItsOwnHostname`: a report the ordering watermark rejects is answered with the stored host, and is judged on its own hostname rather than the stored one. - `TestReportedIdentityRestoresTheTokenWhenTheSaveFails`, `TestReportedIdentityRequiresTheTokensOwnOrganization` and `TestReportedIdentityIgnoresAnExpiredToken`. - `TestReportHandlerOffersOnlyQualifyingLiveReportsToTheRecorder`: the handler's snapshot screen and mock mode. - **`TestApplyHostReportStampsTheTokenOnlyOnHostsItHolds`** (`internal/monitoring/monitor_host_agents_test.go`) pins the monitor properties the handler relies on: - an applied report carries its token's ID; - a watermark-rejected report returns the stored host and hostname; - mock mode's discard acknowledgement carries no token. - **`TestEvaluateReportedIdentityRecordsOnlyWhatCommandRegistrationWouldBind`** (`internal/api/agentbinding/policy_test.go`) is the policy table: the deploy, auto-registered, legacy and equivalent-hostname cases, plus every refusal, including a versionless token already bound to an ID. - **Mutation checks.** Each of these fails at least one named test: - dropping the wiring; - allowing presented ≠ resolved; - letting a bound ID move; - allowing multiple organizations; - allowing any runtime role; - skipping `Evaluate`; - not stamping the binding version; - not restoring on a failed save; - dropping the organization or expiry check; - dropping the held-host check; - judging by the stored host's hostname; - dropping the snapshot screen. Reviewed adversarially in three passes: 1. The first pass found two issues: the overstated "same ID as command registration" claim, now scoped to the same run and paired with the final-identity trade-off; and a mock-mode toggle race, replaced by the held-host check. 2. The second pass found that replayed reports were judged on the stored hostname, now judged on the report's own. 3. The third pass found nothing further. Governance: - `canonical_completion_guard.py` passes in CI mode on this commit. - `registry_audit.py --check`, `contract_audit.py --check` and `status_audit.py --check` pass. - All governance `*_test.py` suites from `canonical-governance.yml` and `go test ./internal/repoctl` pass. ## Not in this change - **Presented ≠ resolved.** When the agent presents an ID other than the one Pulse resolves (a configured agent ID, a continuity fork), nothing is recorded. Reconciling that disagreement belongs to the command-channel identity convergence follow-up. - **Tokens bound to no hostname.** Generic host install tokens without `agent:exec` still get their binding only from command-channel first use, so their reduction still has no ID to match. - **Historical deploy placeholders.** Tokens still bound to `agent-<hostname>` are repaired only by command registration, so a placeholder token without `agent:exec` still cannot reduce. - **Builds on #2542**, which has merged; this branch adds one commit on top of it. |
||
|
|
77bdc77b35 |
Move three forms onto shared controls and draw the select arrow (#2546)
This clears the shared template audit (`node
scripts/shared-template-audit.mjs`) for the forms that still built their
own controls. Three findings remain, in BackupTransferDialogs.tsx and
AlertDeadManDestinationSection.tsx. Those two files are part of an open
`bg-base` change and will follow after it lands.
**Moved onto shared controls:**
- **Alerts › Thresholds › CPU averaging:** the ten per-platform selects
now use `FormSelect`, with each profile name as a visible label.
- **Discovery availability proposal:** the Interval and Observation
location fields use `FormSelect`, and the check-name field next to them
uses the shared field classes, so the three match.
- **Availability check › Request body:** the field uses `FormTextarea`.
Its size note is now the field's accessible description. With
`sm:min-h-24` it also gets the 96px height its class asked for on
desktop; on main it rendered at 54px.
**Select arrow.** `FormSelect`'s default chrome set `appearance-none`
and drew no arrow of its own, so every default select in Settings read
as a text field. That includes the existing Preset, Target type and
Probe selects in the availability dialog. A new `form-select-arrow`
utility draws a caret in the padding the class already reserved, using
the muted text colour. In forced-colors mode it hands the native arrow
back. Filter-bar selects use their own class and are unchanged.
**Registry fixes:**
- `AlertEscalationSection` is removed from the `form-select-shell`
required consumers. It has used checkboxes since
|
||
|
|
df41170313 |
Send built-in webhook template headers on real alerts (#2545)
Fixes #2540. A Telegram webhook passed **Test** while every real alert was rejected with `Bad Request: message text is empty`. **Cause.** The two paths built their headers differently: - The Test path always overlaid the service template's headers (`Content-Type: application/json`). - Real deliveries sent the stored header copy that the UI seeded when the webhook was created. An edit can leave that copy blank, masked or replaced. For example, the edit form shows the stored value as `***REDACTED***`, and clearing it saves an empty `Content-Type`. Telegram's HTTP reader parses body arguments only for JSON, form-urlencoded or multipart content types, and `sendMessage` checks `text` before `chat_id`. So a JSON body without a JSON Content-Type produces exactly that error. **Fix.** When a built-in service renders its own body (no custom template), the template's static headers describe that body: - `prepareWebhookDeliveryContext` applies them on every real firing and recovery, replacing stored values under any spelling of the header name. Unrelated stored headers stay. - The Test path uses the same `withWebhookTemplateHeaders` helper, so a passing Test predicts real delivery. - Custom templates and generic webhooks keep their stored headers. ntfy recoveries still build their own plain-text request. **Tests** (`internal/notifications/webhook_template_headers_test.go`): - A real queued Telegram alert, with blank, masked, replaced, lowercase and duplicate stored Content-Type values, now sends JSON with `text`. Before the fix it sent `""`, `***REDACTED***` or `text/plain`. - For every built-in service, the Test, grouped and resolved requests carry identical template headers and keep unrelated stored headers. - A custom template keeps its stored Content-Type. `go test ./internal/notifications` and `./internal/api/alerting` pass. |
||
|
|
d043c09341 |
Show a Docker container's health check in its drawer (#2534)
Follow-up to #2526. The containers table now says Unhealthy or Exited (139), but the row's drawer still disagreed with it. A running container whose health check was failing read **Observed state: Online**, and a crashed one read **Offline**. The drawer never mentioned the health check, and it could show uptime that a stopped container had left behind. For a Docker container, the drawer's Runtime context now uses the same presentation as the table (`dockerContainerStatePresentation.ts`): - **Observed state** is Docker's own state, such as Running, or Exited (137) in red. - **Health check** is a new row for a running container: Failing (red), Starting (amber) or Passing. - **Uptime** covers only the current run. A stopped container shows neither a stale health check nor leftover uptime. Other resources are unchanged. Verification: - I opened the drawers at 1440px and on a 390px phone, using edited API data: one container with a failing health check, healthy containers, and a crashed container carrying a stale unhealthy status and leftover uptime. I compared each against main. - Tests cover the helpers and both drawer cases, and the receipt is included. |
||
|
|
eaaea2214e |
Use the shared link and button primitives in two remaining spots
The shared template audit still flagged hand-built controls in two places: the backup export and import warnings opened the migration guide through raw new-tab anchors, and the external watchdog section copied the outline button shell for Refresh and Show/Hide. The guide links now compose ExternalTextLink. Its new inlineInherit variant keeps the warning's own colour and underline, so a link inside a tinted callout sentence does not switch to link blue. The watchdog buttons compose Button (outline, xs and settingsAction), which keeps their desktop look and gives them a 44px touch target on phones. |
||
|
|
b4ea3761e8 |
Keep paired PR benchmarks inside their time budget (#2544)
The non-required **Benchmarks** check has failed on every non-superseded PR since about 15:00 UTC on 5 October. Examples are #2521, #2520, #2524, #2525, #2527, #2530 and #2532, each ending with `Paired base and candidate benchmarks (pull requests)` timed out after 25 minutes. The paired step runs 10 base and 10 candidate samples, so each second added per sample costs 20 seconds of step time. ### What made it slower Paired step duration across PR runs, with per-package seconds per sample taken from the `ok` lines in the job logs: | Period | Paired step | `internal/unifiedresources` | `internal/monitoring` | |---|---|---|---| | 24 Sep – 5 Oct 08:00 | 16–18 min | 7.9 s | 4.5 s | | after #2486 (08:11) | 20.5–20.9 min | 16.2 s | 5.0 s | | after #2518 (15:01) | killed at 25 min | 38.2 s | 9.9 s | - **#2518** added `BenchmarkRegistryProjectionSnapshot`. Its `cold` mode rebuilt the registry between `b.StopTimer` and `b.StartTimer` on every iteration. Go picks `b.N` from timed work only, so the empty-registry cases ran hundreds of thousands of untimed ingests. Per sample, `/0/cold/point-targets` took 10.3 s and `/0/cold/bulk-targets` 5.9 s at a 100 ms benchtime. That is the cause of the timeouts. - **#2486** added 46 owner-index and metadata sub-benchmarks, about +8 s per sample. That is legitimate coverage. #2518's `BenchmarkBroadcastMetricsSnapshot` and #2527 added about +6 s more. - `-count`, `-benchtime` and the package list did not change. ### Fix 1. **Root cause.** `cold` now invalidates the registry's derived state (canonical metadata, typed views, source-target index) instead of rebuilding the registry. The refresh never reads its own output, so this repeats a fresh ingest's first read: at 1000 VMs, 40011 vs 40014 allocs/op and about 5.09 MB/op, the same as before. Locally the 24-case family now runs in 4.9 s; before, its 6 cold cases alone took 33.8 s. 2. **Scope.** Paired PR runs now sample only packages whose in-module dependency closure (`go list -deps -test`) holds a changed file. A `_test.go` file selects only its own package. Changes to `go.mod`/`go.sum`, dirty or non-Git trees, or an unreadable graph select every package. Applied to today's failures: #2521, #2524 and #2525 sample nothing; #2530 samples `internal/api`; #2532 samples api and monitoring; #2520 samples 5 packages; #2527 samples 7. The metadata records `selection=` and `packages=`. 3. **Budget.** A module or shared-model change still samples nearly everything, about 24 minutes including the ~2.7 min warm-up. The step timeout is now a measured 30 minutes (job stays at 35). `PULSE_BENCH_BUDGET_SECONDS=1680` makes the script project its total after each round. Once the projection reaches 28 minutes it stops and prints each package's mean seconds per sample, which replaces an unexplained timeout. `TestPairedBenchmarksStopInsideTheirBudget` keeps budget < step timeout < job timeout. 4. **Store-backed chart benchmarks** (second commit). Go calls a benchmark function again for each `b.N` it tries. `HandleWorkloadCharts_StoreBacked` and `HandleWorkloadsSummaryCharts_StoreBacked` each seeded 84,000 points into a fresh SQLite store (about 0.6 s) to time a request of 0.2 ms or less, two or three times per sample. In CI that was 3.5 s and 3.6 s of internal/api's 11.5 s per sample. The timed loop now runs in a `range=4h` sub-benchmark, so the parent seeds once per run. Data, request and bytes/allocs per op are unchanged, and locally the pair falls from about 6 s to 1–1.5 s. The names gain `/range=4h`, so this PR's comparison reports them unmatched. Unpaired branch runs still sample every package. With the cold fix, their 15-minute step (13–14.2 min today) regains about 3 minutes of headroom. ### Verification - `scripts/tests/test-ci-benchmarks.sh` covers no-op, own-test-only, shared-dependency, unreadable-graph, dirty-tree, module-file, budget-exceeded, in-budget and invalid-budget cases. Five mutations of the selection and budget logic are each caught. - On the real package graph, selection maps `internal/models` to 7 packages, `internal/monitoring` to api+monitoring, an `internal/api` embed to api, and frontend or docs paths to none. - End to end with real Go on two clean clones (`origin/main` vs this change), only `./internal/unifiedresources/` was sampled. Its per-sample time went from 53.9 s to 26.7 s on a loaded Mac. - `go test ./scripts/installtests -run 'TestPairedBenchmarksStopInsideTheirBudget|TestBenchmarkQualificationRetainsProvenance'`, plus the script-reference and doc-drift suites. This PR's own Benchmarks run should sample only `internal/unifiedresources` and `internal/api`, because both changes are those packages' own test files. |
||
|
|
bf7f53d8b0 |
Paint the backgrounds the dead bg-base class dropped
Tailwind 4 emits nothing for a colour it does not know, and the `base` colour alias was removed on purpose because it also coloured the `text-base` font-size utility. Every remaining `bg-base` (about 85 uses) therefore rendered transparent. On the page background that looked the same, but command blocks, previews and status pills inside surface cards lost their wells, the segmented filter track hid its selected option, and the alert bulk-selection bar showed white text on a see-through pill. Each use now gets what it was meant to have, checked live in light and dark: - a new `bg-page` utility (the page backdrop, not a theme colour) for wells inside a surface card: command and JSON blocks, the branding header preview, setup wizard wells, the segmented track; - `bg-surface` where the block sits on a surface-alt body (resource drawer JSON, correlation and policy cards) or floats (the bulk-selection bar), `bg-surface-alt` for the muted Card tone, and `hover:bg-surface-hover` for drawer buttons whose hover did nothing; - no class at all on page shells that sit directly on body. Terminal-coloured command text that assumed a dark well gets light-mode shades so it stays readable on the restored light well. An architecture guard fails if a runtime class uses `bg-base` again or a theme colour named `base` returns. Contract-Neutral: Restores intended backgrounds for a Tailwind class that generated no CSS; no API, data or behaviour contract changes beyond the frontend-primitives note on the new bg-page utility. |
||
|
|
8d77c0e277 |
Explain why the Container update risk switch is locked (#2541)
Patrol settings showed the Container update risk switch dimmed on installs without ai_alerts and gave no reason, so a free install could not tell a plan limit from a broken setting. The card now renders the canonical Patrol availability presentation. patrolAutonomyAvailability.ts keeps one precedence and the only action labels, while Patrol mode and container update risk each supply their lock state and copy. Links appear only when the session allows upgrade prompts. The explanation is tied to the switch with aria-describedby, because the shared Toggle does not render its locked/lockedMessage props. The second commit makes hidden commercial surfaces win over the runtime lock, and the Patrol header and banners now pass the session's real presentation policy instead of a constant. Demo and white-label sessions on the community runtime previously saw "Pulse Pro runtime required" on the card and "Install the Pulse Pro runtime" inside the header's mode disclosure; they now get the neutral lock. Sessions that show commercial surfaces keep the runtime prompt and download link unchanged. |
||
|
|
e0647e860f |
Release v6.5.0 (#2536)
Release preparation for `v6.5.0`. After this merges, the release workflow runs at this exact head `38c981a397c7a37f469b619200b5035bfc60b328`. |
||
|
|
4742159bb3 |
Let agents without a command channel reduce their authority
Deploy enrollment, Proxmox auto-registration and pre-v6.1.1 installs leave an agent's token bound to the node's hostname and to no agent ID until the agent registers a command channel. An agent whose token lacks agent:exec, or that never connects that channel, never gets an ID on its token, and the safe-profile migration's authority reduction, which matches that ID, refuses it for as long as the agent runs. A report the monitor resolved under such a token now records the server-resolved host ID on it when the report presented that same ID, which in the same run is the ID the agent's command registration presents. The shared binding policy must first-bind, migrate or backfill that ID for the reported hostname, the token must run command sessions in the report's organization, and the binding version makes the ID immutable. Scopes, runtime role and command policy are untouched, so a recorded identity grants nothing; command registration still needs agent:exec and the same ID. When the presented and resolved IDs differ, nothing is recorded and the command channel decides as before. As with a command registration, the first identity recorded is final, so an agent later restarted under a different --agent-id is refused under that ID on the same token. |
||
|
|
25d299346d |
Keep deployed agents' command channel across restarts (#2542)
## Problem Agents installed through the one-step cluster deploy lose their command channel for good after their first restart. `POST /api/agents/agent/enroll` bound the runtime token to an invented `agent-<hostname>` and returned that as the agent's ID. The agent used it until Pulse acknowledged its first report with the host's real, machine-derived ID, which the agent persists. On the next start the agent registered under that real ID. The token was by then a version-2 binding to `agent-<hostname>`, so admission refused it with "Agent token mismatch: token is not bound to the registering agent", every few minutes, indefinitely. Seen live on a v6.5.0-rc.1 server with a deployed Proxmox node: reports flowed, but the node could run no commands or actions. ## Fix - **Enrollment leaves identity to the agent.** The runtime token is bound to the node's hostname and the current binding version, marked `deploy_identity_binding=agent`, and the response no longer carries `agentId`. The first command registration backfills the agent's own ID through the existing hostname-bound backfill path, and the config gate takes the same decision. - **One-time repair for tokens already issued.** A token enrollment bound to `agent-<hostname>` (it has `deploy_job_id` and no identity marker) may move once to the registering agent's ID: - The hostname must match exactly (case-insensitive), not by short-name equivalence. - The move persists `deploy_identity_binding=repaired` in the same save, and a failed save restores the previous metadata. - It applies whatever the token's binding version. - Any other identity or hostname is refused rather than handed to the legacy hostname migration. - A marked token never moves again, including after a hostname rebind recreates the `agent-<hostname>` shape. ## Tests - **`TestHandleEnroll_RuntimeTokenBindsTheAgentsOwnIdentityOnFirstCommandRegistration`** replays the field sequence: the identity enrollment handed out, then the acknowledged identity after a restart. It fails on the old code at the "registration after restart" step. - **`TestDeployPlaceholderTokenMovesToTheRegisteringAgentOnce`** covers: - the repair on the exact hostname only; - the persisted marker after a reload; - the repair, rebind to host b, repair-again attack; - config-gate parity. - **`TestVersionlessDeployPlaceholderCannotBeMovedTwice`** covers the versionless variant of that attack. - **`TestCurrentDeployTokenNeverMovesOnceBound`** checks that a token issued by current enrollment never moves once bound. - **Policy unit tests:** `TestEvaluateRepairsDeployPlaceholderBindingOnce`, `TestEvaluateVersionlessDeployPlaceholderOnlyMovesThroughTheRepair` and `TestEvaluateCurrentDeployTokenBackfillsTheAgentIdentity`. - **`TestContractDeployEnrollResponseCarriesNoAgentIdentity`** pins the enroll response keys. - **Mutation checks:** removing the marker check, loosening the hostname match, not persisting the marker, or letting placeholders fall through to the legacy branch each fails at least one of these tests. ## Not in this change Both of these exist for every install path and are not made worse here: - The first command registration binds whatever ID the agent presents. If that differs from the identity Pulse later resolves for its reports (a configured agent ID, or a host continuity fork), the next restart still mismatches. - Tokens that never connect the command channel keep an empty `bound_agent_id`, so collector-authority reduction cannot match them. Before this change they had a non-empty but wrong `agent-<hostname>`, which failed the same way. |
||
|
|
ca381121df |
Keep agent-reported disks out of cluster member state (#2543)
An agent on a Proxmox cluster node reports SMART data for the node's
disks, and the registry stamps each disk with its node's Proxmox
identity so it stays discoverable in the Proxmox workspace. The
`/api/connections` member projection accepted any resource carrying that
identity, so each disk merged into its node's member row. Its health
became the node's state and its model name became a host alias.
A disk in warning (60C or more, wear, errors) made an actively reporting
node show Stale; a disk with unknown health made it show Pending. Since
6.4.5 merges members by freshness, the row flips whenever the agent
report and the Proxmox poll swap places, several times a minute on a
customer's three-node 6.4.5 cluster. Standalone nodes are unaffected
because only cluster nodes carry the cluster-member flag.
Only node resources now compose cluster members. The disk keeps its
ownership facet, risk and storage evidence.
`TestContract_ConnectionSystemMembersIgnoreOwnedPhysicalDisks` runs a
cluster node, its linked agent and a 63C NVMe disk through the real
registry. On current main it reproduces the customer's payload exactly
(aliases include `wd_black sn7100 4tb`, state `stale`); with the fix the
member stays active.
Refs #2062, which reports the same Pending/Stale symptom on a 3-node
cluster and was expected to be fixed by
|
||
|
|
9c0b0262df |
Keep Actions target names whole on narrow cards (#2539)
## Problem
Each Actions card's second line shows the target name, its type and the
time. The name and type were sibling flex items that shrank together. So
on a phone the line read "analytics-ba… · Virtual ma…" and "backup-coor…
· App cont…", which hid which resource the action touches. That's what
the operator needs to see before opening the review. From 360 to 430px
every target name was cut; from 480px up everything fit.
## Change
In `src/pages/Actions.tsx`, the name and the type now sit in their own
inline flex row:
- **Name:** never shrinks and is capped at the row's width (`shrink-0
max-w-full truncate`).
- **Type:** wrapped together with its separator, takes whatever room is
left (`min-w-0 flex-1 truncate`).
The time keeps its place. A first attempt with a large shrink factor
still gave the name about 0.08px of the shortfall, which was enough to
draw an ellipsis at every pixel density. The two-level row hands the
name no shortfall at all.
## Verification
Mock-mode private stack on main
|
||
|
|
f7e61446bd |
Move three forms onto shared controls and draw the select arrow
The shared template audit listed Settings, Alerts and Discovery forms still building their own controls. The Thresholds CPU averaging selects and the Discovery availability proposal now compose FormSelect (the proposal's name field takes the shared field classes beside them), and an availability check's request body composes FormTextarea, which links its help text and, with sm:min-h-24, finally gets the height its class asked for on desktop. FormSelect's default chrome set appearance-none and drew no arrow, so every default select in Settings read as a text field. A form-select-arrow utility draws the caret in the padding the class already reserved, and hands the native arrow back in forced-colors mode. Two registry entries were wrong: AlertEscalationSection has used checkboxes instead of a select since its destinations became per-escalation, and the inline detail row rule matched a test file; test files are now excluded from it. |
||
|
|
06cdaa577a |
Send built-in webhook template headers on real alerts
A Telegram webhook passed Test while every real alert was rejected with "message text is empty" (#2540). The Test path always overlaid the service template's headers, but real deliveries sent the stored header copy that the UI seeded at creation. An edit can leave that copy blank, masked or replaced. Telegram reads arguments from a body only when it is labelled as JSON or form data, and checks text before chat_id, so a JSON body without a JSON Content-Type produces exactly that error. When a built-in service renders its own body (no custom template), the template's static headers describe that body. Apply them on every real firing and recovery through prepareWebhookDeliveryContext, replace stored values under any spelling of the name, and route the Test path through the same helper so Test predicts real delivery. Custom templates and generic webhooks keep their stored headers. |
||
|
|
b027bb0e8e |
Keep paired PR benchmarks inside their time budget
Since 15:00 UTC on 5 October every paired benchmark run that was not superseded has hit its 25-minute step timeout. Each step runs ten base and ten candidate samples, so per-sample growth is multiplied by twenty. BenchmarkRegistryProjectionSnapshot (#2518) rebuilt a registry behind StopTimer on every cold iteration. Go sizes b.N from timed work only, so the empty-registry cases ran hundreds of thousands of untimed ingests and took 16 s of a 100 ms benchtime. With #2486's new owner-index and metadata benchmarks, internal/unifiedresources went from 8 s to 38 s per sample. Cold now invalidates the registry's derived state instead. The refresh never reads its own output, so this repeats a fresh ingest's first read with the same allocations, and the family drops from 22 s to about 5 s per sample. Paired runs also measured every package even when both trees compiled identical code, as on frontend-only changes. They now sample only packages whose in-module dependency closure the change reaches. Module files, dirty or non-Git trees and an unreadable graph still select everything. A module or shared-model change still samples nearly every package, about 24 minutes with warm-up. The step therefore gets a measured 30 minutes, and the script stops at a 28-minute projected budget with per-package costs instead of meeting a bare timeout. |
||
|
|
4ce78685e8 |
Keep Pro runtime wording out of hidden-commercial sessions
Demo and white-label sessions hide commercial surfaces, but a licensed install on the community runtime still saw "Pulse Pro runtime required" on the Container update risk card and, inside the Patrol header's mode disclosure, "Install the Pulse Pro runtime" beside a disabled paid-mode picker. The shared availability presentation checked the runtime lock before the hidden-commercial policy, and the Patrol header passed a constant true for that policy, so the order could not be fixed without breaking the header's runtime prompt for everyone else. Hidden commercial surfaces now win over the runtime lock, and the Patrol header and banners pass the session's real policy. Sessions that show commercial surfaces keep the runtime prompt and download link unchanged. |
||
|
|
16c81784b9 |
Keep agent-reported disks out of cluster member state
An agent on a Proxmox cluster node reports SMART data for the node's disks, and the registry stamps each disk with its node's Proxmox identity so it stays discoverable in the Proxmox workspace. The /api/connections member projection accepted any resource carrying that identity, so each disk merged into its node's member row, adding its model name to the aliases and its health to the node state. A disk with a risk warning (for example 60C or more) therefore made an actively reporting node show Stale. Since members are merged by freshness, the row flipped every time the agent report and the Proxmox poll swapped places, several times a minute on a customer's three-node cluster running 6.4.5. Only node resources now compose cluster members. The disk keeps its ownership facet, risk and storage evidence unchanged. |
||
|
|
d01ad47524 |
Keep deployed agents' command channel across restarts
Deploy enrollment bound the runtime token to an invented agent-<hostname> and handed that ID to the agent. Once Pulse acknowledged the agent's reports with its machine-derived ID, the next restart registered under that ID and the command channel refused it for good. Enrollment now binds the token to the node's hostname only and lets the first command registration bind the agent's own ID. Tokens already issued with the placeholder move once, on the exact same hostname, to the registering agent's ID, and record that they were repaired so they never move again. |
||
|
|
357b1a505e |
Explain why the Container update risk switch is locked
Patrol settings showed the Container update risk switch dimmed on installs without ai_alerts and gave no reason, so a free install could not tell a plan limit from a broken setting. The card now renders the canonical Patrol availability presentation. patrolAutonomyAvailability.ts keeps one precedence (runtime lock, then hidden commercial surfaces, then plan lock) and the only action labels, while Patrol mode and container update risk each supply their lock state and copy. Links appear only when the session allows upgrade prompts, and installs that hide commercial surfaces get a neutral line with no plan or Pro wording. The explanation is tied to the switch with aria-describedby, because the shared Toggle does not render its locked/lockedMessage props. |
||
|
|
4819afb082 |
Keep Actions target names whole on narrow cards
Each Actions card's second line shows the target name, its type and the time. Name and type were sibling flex items that shrank together, so on a phone the line read "analytics-ba... · Virtual ma..." and hid which resource the action touches, which is what the operator needs before opening the review. From 360 to 430px every target name was cut. The name and the type now sit in their own row where the name keeps its width (capped at the line) and the type, with its separator, takes whatever is left. Names read in full at every width from 360px (the box now equals the text width at 1x, 2x and 3x pixel ratio), a name longer than the line still truncates with the time visible, and nothing changes from 480px up. |
||
|
|
38c981a397 |
Prepare v6.5.0
Promote v6.5.0-rc.1 to stable after its 24-hour soak. Only release metadata changes. Contract-Neutral: Release-only v6.5.0 version, release notes and distribution metadata; no public install or runtime contract changes. Change-source: pulse-maintainer |
||
|
|
4efb95faba |
Size Alert History columns to content so messages read
The desktop alert history table gave every visible column an equal share (16.7%, 14.3% or 11.1% by density), so Message, the column that says what happened, got the same 100-160px as Timestamp. At most 21 of 106 messages read in full at any width, and "acknowledged" was cut at a 1024px viewport where Status got 100px. Each track now reads its width from a per-column custom property that the table sets at each density. Timestamp, severity, duration, status, type and node get what their content needs, Resource and Node keep at least their old share, the row actions stay within two lines (one line from an 80rem card), and Message takes the rest. The table width still compensates for collapsed tracks, so the visible ones fill the card; App.architecture.test.ts checks that arithmetic at every density. Measured on a mock stack from 768 to 1920px: fully readable messages rise at every width except the 600px card at a 1024px viewport (for example 15 to 66 at 1440 and 21 to 87 at 1920), every pill fits, Resource and Node truncate exactly as often as before, no header clips, and rows halve to 39px from 1760px. |
||
|
|
531a34f5cb |
Keep a disabled toggle's track visible and true to its state
A disabled toggle switched its track to bg-base, which names no theme colour (the base alias was removed because it collided with text-base), so the track rendered transparent. Every disabled toggle showed only a faint knob: a plan-locked setting such as Patrol's Container update risk was nearly invisible, and for a read-only session a setting that was on looked the same as one that was off. A disabled toggle now keeps its on or off track colour and dims, and the knob relies on the track's opacity instead of dimming again. |
||
|
|
17633ecfee |
Show a Docker container's health check in its drawer
The containers table now says Unhealthy or Exited (139), but the row's drawer still read "Observed state: Online" for a running container whose health check was failing, "Offline" for a crashed one, never mentioned the health check, and could show uptime a stopped container had left behind. For a Docker container the drawer's Runtime context now uses the same presentation as the table: Observed state is Docker's own state (Running, Exited (137)), the health check of a running container gets a row of its own (Failing, Starting, Passing), and Uptime is the current run, so a stopped container shows neither a stale health check nor leftover uptime. Other resources are unchanged. |
||
|
|
bd95e64bf2 |
Reconcile batch-start upstream with reviewed local source
Preserve the exact reviewed Patrol guidance and VM Discovery safety commits while incorporating upstream commits captured before coordination. No source conflict resolution or product changes are added by this integration. Change-source: pulse-maintainer |
||
|
|
690d0ef31e |
Give organization deletion time for a busy tenant loop to stop (#2529)
## Problem
|
||
|
|
53ec5fae93 |
Integrate reviewed safe Patrol rule-removal guidance
Preserve the exact documentation and regression-test candidate; native browser follow-through and the missing UI remain owned separately. Change-source: pulse-maintainer |
||
|
|
377aea8ad5 |
Integrate reviewed canonical VM Discovery safety repair
Preserve the exact Web candidate and parent-bound browser proof; backend admission and native recovery remain separate work. Change-source: pulse-maintainer |
||
|
|
ac643e2bde |
Flag an outdated agent once on the Infrastructure page (#2531)
Each outdated Pulse Agent on Settings › Infrastructure was flagged twice: an amber **Agent update** badge, plus a second-line **Version behind** note. The band also counted every outdated agent as needing attention. On a mock estate it read "11 systems needs attention", and all 11 were version lag. - **Version lag is maintenance.** Version behind and Update available are now maintenance signals. The badge, which opens Agent Doctor, carries them, so they never become the row's problem line and don't count toward needs attention. The Manage drawer still lists every fleet signal. - **Real update problems still show.** A failed update and auto-update switched off remain row problems, since each needs a different action. - **Problems can't hide behind the cap.** A row's problem is now picked from all its visible signals before the three-highlight cap, and problems rank ahead of maintenance. A second attached agent's failed update can no longer fall behind another agent's version lag. - **The band** says how many agent updates are available, in a neutral tone. "Systems need" and "hosts have" now agree in number. - **Column widths.** The Actions column is wide only when a row shows an install button beside Manage, and Health gets 27% of the table. The Health cell now keeps the status, the badge and the last-seen time on one line. On main, the last-seen time was cut to "18s a…". Verification: - At 1280, 1440 and 1920px the table has 16 rows and no clipped cells. Main has 27 rows (including 11 problem lines), with 11 and 6 clipped cells at 1280 and 1440px. - The phone card layout shows no Version behind lines. - Re-measured after rebasing onto the shared Table change in #2524. The receipt is included. |
||
|
|
574b4dd350 |
Name mock history seeding as the observed slow tenant stop
The deletion-budget note and its test said only that a loop still
starting up can be slow to stop. The CI failures have a specific
cause: in mock mode Monitor.Start runs startMockMetricsSampler
synchronously before its select loop, and prepareMockMetricsHistory
seeds seven days of fixture history without a context, so the loop
saw its cancelled context only once seeding finished ("Mock metrics
history sampler started" and "monitoring loop stopped" log in the same
second in runs 37329726585 and 37331626618). Say so in the contract
and the test comment.
|
||
|
|
065cd2bf37 |
Show Docker container health and uptime in the containers table (#2526)
The Docker containers table now shows whether each container is healthy and how long it has been running. - **State names the problem.** It shows Unhealthy (red) or Starting (amber) for a running container's health check, Exited (139) (red) for a crash, Restarting, and a muted Exited or Paused for a container stopped on purpose, with the full sentence on hover. Before, an unhealthy container read `running` and only its dot was red. The column also disappeared whenever every container was running, so a fleet with one failing health check showed no State column at all. The column now appears whenever any row has one of these states. - **Stale health is ignored.** Docker keeps the last health status on a stopped container (#1724), which made a stopped container's dot read unhealthy. Health now counts only for a running container. A container with no reported Docker state is treated as running when its status is online. - **Uptime column.** From a 900px table, an Uptime column shows the current run, so `0m` or `5m` marks a container that just restarted. v5 had this column. Stopped containers show a dash, and narrower rows keep uptime in the drawer. - **Room for the new columns:** - A single-host table, which is always flat, drops the Host column that repeated one name on every row. - The Ports cell drops the all-interfaces address and the duplicate IPv6 entry, so it reads `23210->8080/tcp`. A specific bind address such as `127.0.0.1` stays, and the hover text, drawer and search keep the full form. - Column weights moved from CPU, memory, ports and networks. - New model tests hold the State words (86px) and the Uptime header (57px) at each layout's narrowest table, alongside the existing Update-control width test. Verification: - Measured every column at nine widths from 390px to 1920px against main, which now includes #2521. Nothing new clips except one more truncated container name at 1000px. Ports overflow per cell drops from 175px to 123px at 1440px. "Exited (139)" fits on a 390px phone. - Checked unhealthy, starting and stale-health states, the single-host view, dark mode and the phone drawer, using edited API data. The receipt is included. |
||
|
|
74a195daaf |
Flag an outdated agent once on the Infrastructure page
Every outdated Pulse Agent row showed an amber Agent update badge and,
on a second line, Version behind, and the band counted each of them as
a system that needs attention ("11 systems needs attention"), although
an agent waiting for an update is maintenance rather than an incident.
Version behind and Update available are now maintenance signals: the
badge, which opens Agent Doctor, carries them, so they never become the
row's problem line and do not count toward needs attention. The band
says how many agent updates are available in a neutral tone instead. A
failed update and auto-update switched off stay problems, since each
needs a different action, and a row's problem is picked before the
three-highlight cap so a second agent's failure cannot hide behind
another agent's version lag.
The band's counts now agree in number, and the Actions column is wide
only when a row shows an install button beside Manage, so the Health
cell keeps the badge and the last-seen time on one line.
|
||
|
|
de84b0e311 | Merge remote-tracking branch 'origin/main' into fix/tenant-deletion-shutdown-wait | ||
|
|
2218ab6c18 |
Keep the Proxmox guest Backup badge whole at laptop widths (#2525)
## Problem
The Proxmox guest table's Backup badge doesn't truncate, but its column
was too narrow for it.
- **Badge widths, with the shield:** Running 73px, Unknown 79px, None
58px, month ages ("12mo") 61px.
- **Column room:** the compact layout gave Backup a weight of 5, which
left a 33–58px content box from 1000 to 1513px. The wide layout gave it
72px.
- **Result:** Running and Unknown were cut inside their pill at every
width up to 1600px, and None and month ages up to about 1500px.
Running is the badge that says a backup is in progress.
## Change
In `guestRowModel.tsx`:
| Layout | Backup | Taken from | Why that column can give |
|---|---|---|---|
| Compact | weight 5 → 10 | Type 7 → 5.4 | It still fits the VM/LXC
badge and the sorted "TYPE▲" header (about 51px). |
| Compact | | CPU 13 → 9.6 | Its bar only shows a percentage and core
count. |
| Wide | 72px → 96px | CPU 140px → 116px | Same reason. |
Name, ID, Uptime, Net I/O, Disk I/O and the memory and disk bars keep
their widths, and their used/total figures. Views without a Backup
column (pods, Docker) give them slightly more, so two
`GuestRow.test.tsx` width expectations change. The peer whose claim
covers that file agreed to the edit; its own PR, #2524, edits a distant
hunk.
## Verification
Mock-mode private stack on main
|
||
|
|
51dbf1485a |
Keep a text-selection drag from toggling table rows (#2524)
Dragging across a pod or container name to copy it ends with a click on
the row the drag stayed inside. Every platform table ran its row action
on that click, so the detail drawer opened or closed and shifted the
layout under the selection.
**Change**
- `TableRow` (`components/shared/Table.tsx`) owns a per-row
`createRowTextSelectionGuard()`. It records where the row was pressed. A
click whose pointer moved more than 3px since that press, while text
inside the row is selected, skips the row action.
- A press released in place still runs the action on the first click,
even over text that is already selected. Chromium clears that selection
only after the click, so a plain selection check alone would need a
second click.
- The 37 `drawer.toggle` tables, the Proxmox tables built on
`getPlatformResourceDetailRowInteractionProps`, and `PulseDataGrid`
inherit the guard with no call-site changes. The custom `GuestRow`
(Workloads) and `StoragePoolRow` shells wire the same guard.
- The disclosure chevron and its keyboard activation handle their own
events and are unchanged. A double-click is two stationary clicks, so it
behaves as before.
**Verification**
- New cases in `Table.test.tsx`, `PulseDataGrid.test.tsx`,
`GuestRow.test.tsx`, `Storage.test.tsx` and
`PlatformResourceDetailTableRow.test.tsx` cover drags, in-place clicks
over a selection, selections outside the row, bound handlers, and the
chevron plus Enter while text stays selected. All six fail with the
guard disabled.
- `src/features src/hooks src/__tests__
src/components/{shared,Workloads,Storage,Infrastructure}`: 580 files and
10,012 tests pass.
- Ran Playwright with real mouse drags on a private mock stack at
1440x900 and 768x1024 across Kubernetes pods, Docker containers, Proxmox
hosts, guests and storage pools, plus 390x844 touch taps.
- The drag reproduces the toggle on the parent commit and leaves state
unchanged on this branch.
- A plain click on the selected name opens the row first time.
- Receipt: `frontend-modern/browser-verification/4f08442c2617c48d.json`.
|
||
|
|
94e10d65d8 |
Give organization deletion time for a busy tenant loop to stop
|
||
|
|
ee618fb993 |
Guard canonical VM Discovery during guest-read deferrals
Use the existing guest safety signals in the canonical resource drawer, including typed and legacy PVE facets. Keep saved Discovery evidence readable and the backup precaution visible without admitting a manual rescan while paused. Contract-Neutral: performance-and-scalability; the workload helper input is narrowed without changing its decisions, metrics, render path or performance contract. Change-source: pulse-maintainer |
||
|
|
310c7e6e51 |
Document safe removal of mistakenly created Patrol rules
Explain the missing v6.4.5 undo control and the existing scoped API. Verify exact selection, confirmation, browser session and CSRF, failure handling and readback without copying credentials or deleting unrelated rules. Contract-Neutral: Documentation and regression-proof coverage of existing Patrol suppression APIs only; no runtime, route, authority or subsystem contract changes. Change-source: pulse-maintainer |
||
|
|
d836208f16 |
Exchange the bootstrap token before any agent module starts (#2520)
## Problem An agent installed by the cluster deploy flow starts with `--enroll` and a one-time bootstrap token. Only the host module exchanged that token for the runtime token. Everything else `main.go` builds at startup kept the bootstrap token for the whole first process lifetime: - the remote config client (initial fetch and the one-minute refresh) - the updater - the Docker / Podman and Kubernetes modules and their primary report targets The bootstrap token carries only the enrollment scope and the server removes it on exchange, so each of those failed with 401 until the service restarted. On a freshly deployed Proxmox node this showed up as a remote config 401 every minute. Those modules also kept the pre-enrollment agent ID instead of the server-assigned one. ## Fix `main.go` now enrolls once, before it builds any authenticated client: - `hostagent.Enroll` is a standalone entry point over the existing enrollment loop. Payload, headers, retry and backoff, status handling, and `runtime.token` / `agent-id` persistence are unchanged. - It runs after self-test and agent-ID preparation and before the first remote config fetch. Every module is then built from the runtime token and the canonical agent ID. - The ID is persisted to the configured agent-ID file, and the host module's own enrollment is switched off so there is no second exchange. - A persisted runtime token is reused without a request or host discovery. A `403` keeps the configured token as a non-bootstrap credential. A permanent rejection stops startup before any module is built. - Windows is gated off and keeps its previous behaviour. Startup there runs before the service control dispatcher, and an enrollment retry could outlast its 30-second deadline. No Windows installer passes `--enroll`. - Enrollment and reporting now derive hostname, OS and architecture through one `resolveHostIdentity`, so the runtime token is bound to the hostname the agent reports under. ## Tests - `TestRunEnrollsBeforeBuildingAuthenticatedModules`: enrollment happens before any request to Pulse. Remote config, the updater, and the host, Docker and Kubernetes configs and their targets all carry the runtime token and canonical ID, and the agent-ID file is written. With the new step disabled, this test fails. - `TestRunStopsBeforeModulesWhenEnrollmentIsRejected`, `TestRunWithoutEnrollDoesNotExchangeTokens` and `TestRunLeavesEnrollmentToTheHostModuleWhenGatedOff`. - `TestEnroll*` cases for `Enroll`: payload and identity, the monitoring-only command ceiling, reuse of a persisted token (including when host discovery fails), the 403 keep, the 409 rejection, and a missing token. - `TestEnrollPresentsTheIdentityNewReports` checks that enrollment presents the hostname, OS and architecture that `New` reports, with Proxmox detection and the architecture fallback. - `go test ./cmd/pulse-agent ./internal/hostagent ./internal/remoteconfig` passes. `go vet` is clean for linux, windows and freebsd. ## Not in this change - The health listener still starts after enrollment, so `/readyz` is unreachable while enrollment retries on network errors. The installer only warns in that case. With the health listener enabled, its registration check now always sees the runtime token. With it disabled, the process-only check can still race enrollment. - Distinguishing a deploy binding mismatch from a non-bootstrap token, which both return 403. - Two older durability gaps that this change doesn't make worse. If writing a custom `--agent-id-file` fails, the next start has no fallback to the canonical ID saved in the state directory. If persisting `runtime.token` fails, enrollment still succeeds, and the next restart is left with the spent bootstrap token. |
||
|
|
8439dad9e5 |
Show Docker container health and uptime in the containers table
The State column printed Docker's raw state, so a running container whose health check failed read "running" and only its dot was red, and the column hid entirely when every container was running, unhealthy or not. v5 showed "running (healthy)" and an Uptime column; v6 had lost both. State now says what docker ps would flag, from the same verdict as the row dot: Unhealthy and Starting for a running container's health check, Exited (139) for a crash, Restarting, and muted Exited or Paused for a container stopped on purpose, with the full sentence on hover. The column appears whenever a row has one of those states. A health status only counts for a running container: Docker keeps the last one on a stopped container (#1724), which made a stopped container's dot read unhealthy. From a 900px table an Uptime column shows the current run, which marks a container that just restarted; narrower rows keep it in the drawer. A single-host table drops the Host column that repeated one name on every row, and the Ports cell drops the all-interfaces address and the duplicate IPv6 entry, so the widths for State and Uptime come from columns that had room or truncated anyway. |