Commit Graph

5934 Commits

Author SHA1 Message Date
pulse-triage[bot] c851f46ef8 Keep VM disk FAQ out of unsafe installation assumptions
A disk dash does not diagnose an absent or stopped agent. Keep setup specific to the supported guest OS, defer live changes during backup or guest incidents, and require independent recovery rather than treating a retained reading or an OK backup as current evidence. Link the existing guidance and cover its rendered destinations and shipped mirror.

Contract-Neutral: Documentation and rendered-help coverage only; no runtime, data, API or permission contract changes.
Change-source: pulse-maintainer
2026-10-06 02:49:30 +01:00
pulse-triage[bot] 9d7a414497 Integrate reviewed filesystem-first guest collection and admission state
Change-source: pulse-maintainer
2026-10-06 02:15:52 +01:00
pulse-triage[bot] ae5fe7a339 Integrate reviewed retained-notification recovery guidance
Preserve the exact reviewed PI commit. Clarify saved destination settings and safe Telegram recovery in existing help, with connected queue and documentation coverage.

Change-source: pulse-maintainer
2026-10-06 02:02:29 +01:00
pulse-triage[bot] aae30e2baa Explain safe recovery of retained notification failures
A destination edit and a passing Test do not update the settings stored in an old queued delivery. Make the existing recovery help distinguish those paths, explain the passive Telegram header check, and keep the shipped guides in sync. Connect a real rejected HTTP delivery through edit, Test, restart, retained retry and new alert, including current disable, removal and pause controls.

Contract-Neutral: Documents and verifies existing destination-snapshot and retry semantics; no runtime, API, schema or delivery-policy change.
Change-source: pulse-maintainer
2026-10-06 01:30:57 +01:00
pulse-triage[bot] 737b6c27e1 Integrate reviewed backup-safe guest disk guidance
Change-source: pulse-maintainer
2026-10-06 01:30:05 +01:00
pulse-triage[bot] 96abf2a9bc Incorporate batch-start upstream agent identity repair
Change-source: pulse-maintainer
2026-10-06 01:30:01 +01:00
pulse-triage[bot] 29024c8227 Retain the reviewed guidance and installer attestation correction
Compose the exact reviewed Delivery correction and its retained coordinator integration without rewriting accepted history. Fresh combined-stylesheet proof remains with the existing Web handoff.

Change-source: pulse-maintainer
2026-10-06 01:16:59 +01:00
rcourtman 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.
2026-10-06 01:12:54 +01:00
pulse-triage[bot] 2d311a5ff0 Reconcile reviewed maintenance with current published main
Change-source: pulse-maintainer
2026-10-06 01:08:48 +01:00
pulse-triage[bot] bfdb1f1a47 Preserve current guest admission through repeated canonical reads
The connected QGA control exposed that mergeProxmoxData kept the previous filesystem reason and guest-agent status. Native guest facets now replace only those admission fields; partial non-guest facets leave them alone. Add repeated read and JSON transitions, and seed the no-status control with actual bounded QEMU evidence and retained metadata.

Change-source: pulse-maintainer
2026-10-06 01:05:01 +01:00
rcourtman 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.
2026-10-06 00:53:39 +01:00
pulse-triage[bot] 0db135d7cc Keep missing guest disk guidance out of unsafe setup assumptions
Explain that the shared Proxmox unavailable status is not an installation diagnosis. Keep setup behind backup recovery and supported guest-OS checks in the existing shipped guide; pin the rendered safety/setup fragments and preserve every covered filesystem and prior-active restoration requirements.

Contract-Neutral: Clarifies existing collection limits and backup-safe setup without changing collection, runtime UI, actions or permissions.
Change-source: pulse-maintainer
2026-10-06 00:49:01 +01:00
pulse-triage[bot] 9714004424 Integrate reviewed guidance guards and installer input attestation
Preserve maintained token safety guidance and bind every compiled installer test through the real source-manifest reader. Retain exact specialist commit identity and its source-ol4k9q3h proof.

Change-source: pulse-maintainer
2026-10-06 00:45:57 +01:00
pulse-triage[bot] 95d0ba6334 Reconcile reviewed maintenance with batch-start upstream fixes
Incorporate the supplied upstream frontier without rewriting reviewed history. Later upstream reconciliation belongs to the publication gate.

Change-source: pulse-maintainer
2026-10-06 00:45:53 +01:00
rcourtman 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.
2026-10-06 00:44:10 +01:00
rcourtman 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.
2026-10-06 00:36:41 +01:00
rcourtman 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.
2026-10-06 00:35:23 +01:00
pulse-triage[bot] 9fb5bfc9e3 Integrate reviewed frontend dependency refresh and formatter proof
Change-source: pulse-maintainer
2026-10-06 00:31:41 +01:00
pulse-triage[bot] 357710d554 Reconcile reviewed maintenance with the supplied upstream frontier
Change-source: pulse-maintainer
2026-10-06 00:31:39 +01:00
pulse-triage[bot] 7b5cdfc5c3 Read guest filesystems before optional QGA enrichment
Prevent optional Linux memory and metadata failures from starving supported filesystem usage. Carry shared admission deferral independently of a completed disk sample. Preserve backup locks, uncertain completion, no replay and original observation ages; bind both poll paths and truthful History to connected controls. This is source repair, not a native discussion #2538 cause or installed recovery.

Change-source: pulse-maintainer
2026-10-06 00:30:02 +01:00
pulse-triage[bot] 5c1779e6af Bind maintained guidance and all compiled installer inputs
Retain the newer private-token and prompt-revocation guidance rather than restoring obsolete copy to satisfy stale assertions. Bind all nine added installer test sources in the rootful manifest and exercise their real reader hashes; the complete compiled-source coverage guard remains intact. No workflow, release policy or runtime authority changes.

Change-source: pulse-maintainer
2026-10-06 00:20:57 +01:00
rcourtman 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 e0baecc418.
- The inline detail row rule now excludes test files. Its only finding
was `ProxmoxBackupsTable.test.tsx`.

Verification:
- Checked live on a mock stack, before and after, in light and dark mode
at 1440px and 390px.
- The Discovery card was rendered through route interception.
- Forced-colors mode was emulated.
- 11,393 tests pass, and the receipt is included.
2026-10-06 00:20:46 +01:00
rcourtman 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.
2026-10-06 00:19:35 +01:00
rcourtman 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.
2026-10-06 00:17:11 +01:00
rcourtman 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.
2026-10-06 00:08:07 +01:00
pulse-triage[bot] 28c64e3808 Reconcile reviewed monitoring maintenance with published benchmark repair
Preserve the exact reviewed tip and incorporate current origin/main additively before combined-source validation.

Change-source: pulse-maintainer
2026-10-06 00:05:38 +01:00
pulse-triage[bot] d6d80dc533 Bind formatter compatibility proof to the real Node implementation
The exact prepared graph full suite found that Vite's browser resolution selected Prettier standalone without the Markdown parser. Use native Node package loading in this Node-only tooling test, preserving the original dollar-variable spacing assertion, version pin, resolver and advisory floors. Retain source-16gubdgw's failed full suite and validate the corrected source without changing runtime code or manifests.

Change-source: pulse-maintainer
2026-10-06 00:05:13 +01:00
pulse-triage[bot] f2dc413d70 Integrate reviewed action-runner receipt fixture readiness repair
Accept Delivery's exact fixture-only commit with retained source-75ax6j3d affected races, vet, installer and build evidence. Observe actual activation and a query round trip before dispatch; preserve cancellation, mutation, replay and timeout assertions. Original publisher failure remains adverse and is not diagnosed as noise.

Change-source: pulse-maintainer
2026-10-06 00:01:40 +01:00
pulse-triage[bot] 922c3c36b9 Reconcile reviewed maintenance with the supplied upstream frontier
Preserve exact reviewed source and browser receipts while incorporating the published frontier available at the start of this coordination batch.

Change-source: pulse-maintainer
2026-10-06 00:01:32 +01:00
rcourtman 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.
2026-10-06 00:00:56 +01:00
rcourtman 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.
2026-10-05 23:58:46 +01:00
rcourtman 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.
2026-10-05 23:57:17 +01:00
pulse-triage[bot] 33aea9890f Integrate reviewed truthful TrueNAS pool capacity observations
Change-source: pulse-maintainer
2026-10-05 23:49:36 +01:00
pulse-triage[bot] 993528f870 Reconcile reviewed maintenance with supplied upstream frontier
Change-source: pulse-maintainer
2026-10-05 23:49:25 +01:00
pulse-triage[bot] f1c360b648 Compose the prepared frontend patch group with current repairs
Retain the existing #2352 candidate and exact prepared manifests alongside live cluster, monitoring, accessibility and Go repairs. No upstream proposal mutation or installation is performed; exact-graph validation follows.

Change-source: pulse-maintainer
2026-10-05 23:47:29 +01:00
pulse-triage[bot] e0647e860f Release v6.5.0 (#2536)
Release preparation for `v6.5.0`. After this merges, the release
workflow runs at this exact head
`38c981a397c7a37f469b619200b5035bfc60b328`.
2026-10-05 22:45:32 +00:00
rcourtman 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.
2026-10-05 23:38:18 +01:00
rcourtman 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.
2026-10-05 23:33:44 +01:00
pulse-triage[bot] 242be284c3 Integrate reviewed live cluster connection confirmation
Require a current same-node agent connection before presenting cluster installation success. Retain the exact reviewed commits and parent-bound Chromium/WebKit proof; the retained full frontend receipt covers the unchanged implementation without claiming native installation or recovery.

Change-source: pulse-maintainer
2026-10-05 23:31:08 +01:00
pulse-triage[bot] be8ab26e07 Retain reviewed maintenance with current published monitoring repairs
Additively incorporate exact newly observed upstream main without rewriting accepted commits. Combined-source publication remains deferred pending executor proof and validation of these additional published repairs.

Change-source: pulse-maintainer
2026-10-05 23:21:11 +01:00
rcourtman 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 54c3cdc061 in 6.4.5.
2026-10-05 23:14:31 +01:00
rcourtman 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 1dcf9418aa, three open demo actions,
before against after at 360, 390, 430, 480, 640, 768, 1024 and 1440px:

| Viewport | `analytics-batch-01` (main → after) | `backup-coordinator`
(main → after) |
|---|---|---|
| 360 | 61 → 107 of 107px | 67 → 112 of 112px |
| 390 | 77 → 107 | 84 → 112 |
| 430 | 98 → 107 | 107 → 112 |

- **Exact fit:** the name's box equals its text width at 1×, 2× and 3×
pixel ratio. The type with its separator gets the rest ("Virtual
machine" 21, 51 and 91px at 360, 390 and 430px; whole from 480px).
Nothing changes from 480px up.
- **Long names:** a name longer than the line, injected in the browser,
truncates inside it at 360 and 390px with the time fully visible.
- **Interactions:** the review opens from a card and closes with Escape
at 390 and 1024px, and the History tab works. No console errors.
- **Tests:** the new
`features/actions/__tests__/Actions.metaLine.test.tsx` pins the
structure and fails against main's source. 25 test files across actions,
pages, routing and the resource-actions API pass.

The browser receipt is
`frontend-modern/browser-verification/2294fd5a84fdc31a.json`.
2026-10-05 23:12:54 +01:00
pulse-triage[bot] 1f5e451578 Make cancellation receipt proof wait for an activated runner
Registration is visible before credential activation and the command reader. Wait for each new client’s actual activated health write and a receipt-query round trip, retaining three-second deadlines and exact durable replay assertions. A controlled failed-first activation proves that registered alone is insufficient. Install idempotent runner and dispatch cleanup before assertions so early failure cannot leave the fixture alive.

Contract-Neutral: Proof-fixture readiness and cleanup only; production admission, cancellation, receipt replay, timeouts and mutation authority are unchanged.
Change-source: pulse-maintainer
2026-10-05 23:08:55 +01:00
pulse-triage[bot] 4acbb02da0 Reconcile reviewed maintenance with published toggle rendering
Preserve every accepted integration while incorporating the exact current upstream disabled-toggle repair. Validate the combined source before publication.

Change-source: pulse-maintainer
2026-10-05 23:04:42 +01:00
rcourtman 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.
2026-10-05 23:01:46 +01:00
pulse-triage[bot] c5cee05f87 Keep incomplete TrueNAS pool readings out of usage and History
Validate existing raw pool byte readings without converting absence to zero or discarding unrelated snapshot inventory. Preserve exact RPC integers and zero-valued empty/full pools, keep sources coherent, and withhold incomplete host subtotals. Connect TLS collection and scoped ingestion to memory and persistent History controls; this does not add an unsupported CORE capacity method.

Change-source: pulse-maintainer
2026-10-05 22:57:32 +01:00
pulse-triage[bot] 3fe60c8732 Integrate reviewed aligned Go patch graph
Retain exact Delivery commits and current main repairs. Reviewed source-1dqqprfr verifies offline codec and Kubernetes races, runtime checks, vet and real server/agent builds with unchanged manifests. No frontend content is resolved or changed by this integration.

Change-source: pulse-maintainer
2026-10-05 22:55:45 +01:00
rcourtman 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.
2026-10-05 22:46:47 +01:00
rcourtman 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.
2026-10-05 22:39:51 +01:00
pulse-triage[bot] 8be86746a9 Confirm live cluster agent connections instead of saved links
A saved node-agent association can outlive its exec connection. Keep installation outcomes pending until the current candidate response confirms a connected agent on the exact node and cluster, and name the badge Connected to Pulse rather than Reporting. Preserve bounded waiting, focus continuity and the existing installer fallback.

Change-source: pulse-maintainer
2026-10-05 22:39:37 +01:00