Commit Graph

6441 Commits

Author SHA1 Message Date
pulse-triage[bot] ec4ee724de Reset obsolete guest metadata scheduling on invalidation
The broad guest lifecycle proof caught a recovery regression when a known unavailable agent deliberately invalidated its cache. Clear that key’s obsolete refresh/backoff deadline without clearing active fetch ownership, independent installations or Proxmox uncertainty. Retain immediate available-poll recovery and early-failure backoff as distinct paths.

Contract-Neutral: Preserve existing recovery on deliberate guest metadata cache invalidation; no guest lock, uncertainty, command, public API, configuration or duration changes.
Change-source: pulse-maintainer
2026-10-07 19:47:50 +01:00
pulse-triage[bot] 9b77a4b6c6 Keep guest metadata fetch ownership through completion
Honour cold-cache retry backoff and retain a per-key in-flight reservation until enrichment returns, rather than treating a timed hold as completion. Avoid duplicate command sequences and stale safety-state writers while preserving existing lock, uncertainty, observation and refresh policies. Cover blocked real HTTP reads, early deferral, slot cancellation and ordinary recovery.

Contract-Neutral: Restore the existing per-key metadata reservation and retry scheduling semantics; no public API, configuration, guest command, lock, uncertainty fence, observation or numeric duration changes.
Change-source: pulse-maintainer
2026-10-07 19:39:32 +01:00
pulse-triage[bot] 1992c681c2 Preserve reviewed monitoring maintenance with current Patrol and alert defaults
Additively incorporate upstream 843daf7ae3 without removing accepted source. Resolve the overlapping AI documentation assertion by preserving its whitespace-normalized compatibility check together with upstream dead-Patrol guidance. The focused documentation policy tests pass; browser and final-source validation dependencies remain explicit.

Change-source: pulse-maintainer
2026-10-07 19:11:35 +01:00
rcourtman 843daf7ae3 Remove dead Patrol assessment and save-failure handoffs (#2682)
Stacked on #2674 (branch `refactor/drop-patrol-context-summary`); this
branch contains that commit.

## What

Removes the two Patrol-to-Assistant handoff builders in
`frontend-modern/src/features/patrol/patrolInvestigationContextModel.ts`
that no production code calls:

- `buildPatrolAssessmentAssistantHandoff`: its caller, the summary
card's Discuss handler, went in eac7dfe9ef (2026-05-15).
- `buildPatrolConfigurationFailureHandoff`: its last caller went in
27ba85ddc6 (2026-06-23).

With them go `PatrolInvestigationContextSummary`, their input and output
types, and the private helpers only they used. The model drops from
2,326 to 1,257 lines.

## Why it is safe

- A TypeScript compiler export graph rooted at `src/index.tsx` shows no
reachable importer of either builder, and `git grep` finds only their
own tests.
- Production builds of the parent tree and this tree are byte-identical
across all 187 dist files (`shasum`). `check-bundle-size.mjs` passes.
- The Assistant drawer still restores saved `patrol_assessment` and
`patrol_configuration_failure` sessions. That code
(`components/AI/Chat/index.tsx`, `workflowStarters.ts`,
`internal/ai/chat/session.go`) is unchanged.

## Tests

- The removed builders' cases are gone.
- Two cases covered branches the live finding handoff still uses, so
they now go through the live builders: a typed pending action stays
approval-bound without a legacy approval id, and the resource node
qualifier plus long-line truncation.
- v8 coverage of the surviving model code matches the baseline from
before the removal. The one exception is a defensive guard the live
caller cannot reach.

## Contracts and docs

Eight contracts changed: `patrol-intelligence`, `ai-runtime`,
`api-contracts`, `frontend-primitives`, `security-privacy`,
`storage-recovery`, `agent-lifecycle` and `unified-resources`.

- No contract describes either handoff as a live producer any more.
Statements about restoring saved sessions of those kinds stay.
- Some clauses described a rendered Patrol summary card, assessment
strip or readout, score chip, header trust line, or inline save-failure
surface. They are now conditional or say what renders instead.
- The run-history handoff clauses now say the browser sends the briefing
and safe run metadata, and `internal/ai/patrol_assistant_handoff.go`
rebuilds the model context from the run ID.
- `docs/AI.md` and its public mirror pointed users at a legacy
operational score in a collapsed supporting-context section. Neither
exists any more. The section now points at **Activity > Finding options
and history > History**.
- `ai_runtime_docs_policy_test.py` is the registered proof for
`docs/AI.md`. It now pins that location and rejects the retired claim.
It also had a stale pin that has failed on main since 2789a15fe5; that
pin now matches the current Patrol mode API wording.

Prose about the remaining test-only summary presenters in
`utils/patrolSummaryPresentation.ts` is left for a separate follow-up
that removes those presenters.

## Browser verification

Receipt `frontend-modern/browser-verification/6e6cbc66d62863d1.json`.
Checked on an embedded production build on a private demo mock stack, at
1440x900 and 390x844:

- Inbox "Explain with Assistant" opens the drawer with the Pulse Patrol
attention briefing.
- The run-history "Discuss" button opens the drawer with the "Patrol run
attached" briefing.
2026-10-07 18:52:17 +01:00
rcourtman 2eef42c512 Keep Off alert defaults off after saving (#2684)
## Summary

Switching a metric Off in a **Global Defaults** row (Alerts >
Thresholds) did not stick. After Save and a reload the metric was back
On at its factory threshold and kept alerting. This affected every
section whose factory threshold is positive: Machines CPU, memory, disk
and disk temperature, Proxmox node temperature, PBS, Docker, Kubernetes,
TrueNAS, vSphere and storage.

The editors stage `-1` for Off, and the save payload passed it through
as `{trigger: -1}`. Backend config normalization reads a negative
default trigger as unset and restores the factory value. Guest and node
CPU, memory and disk escaped only because nothing normalizes them.

- `buildAlertsConfigurationPayload` now writes every Off default as
`{trigger: 0, clear: 0}`, the value the engine and normalization both
read as off. Per-resource overrides are serialized separately and keep
`-1`, which the override path stores as written. A `-1` already stored
on a guest or node default becomes `0` on the next save.
- The Global Defaults **Custom** badge now treats `-1` and `0` as the
same Off value. Without that, a fresh install showed Custom on
Kubernetes, TrueNAS Systems and vSphere, and VMs & Containers turned
Custom after any save, though nothing had changed. An unset factory
value stays distinct from Off: an unset TrueNAS disk temperature follows
Disk temperature by type.
- A row's custom-threshold marker still compares raw values. It drives
the row's revert control and override removal, so a stored `-1` override
must stay visible against an Off default. A test pins that.
- The alerts contract no longer claims Off is written as `-1`
everywhere.

## Verification

- New payload test: every section's Off default is written as `{0, 0}`
and reads back Off. It fails on `main` with `guestDefaults.cpu: expected
{ trigger: -1 }`.
- New backend test: a `{0, 0}` default stays off through `UpdateConfig`
in every section, and a host at 99% raises no CPU, memory or disk alert
when the agent defaults are off. Both fail when the defaults are written
as `-1`.
- Live on a private mock stack. On `main`, Proxmox CPU and Temp switched
Off saved as `{-1}` and `{80, 75}`, and after reload Temp read On. With
the fix:
- Machines CPU at 1% raised 8 alerts. Switching it Off stored `{0, 0}`
and resolved all 8.
  - Proxmox Temp Off resolved the node temperature alert.
  - Nothing re-fired over two minutes.
- Values stayed Off after a backend restart and a reload. Machines,
Proxmox, Kubernetes, TrueNAS and vSphere were each checked at desktop or
phone width.
- Custom badge on a fresh install: no Proxmox, Kubernetes, TrueNAS or
vSphere group reads Custom, and after an unrelated save only the group
actually changed does.
2026-10-07 18:47:09 +01:00
pulse-triage[bot] 98b92689a3 Integrate reviewed bounded agent log guidance
Preserve the exact reviewed Product Intelligence commit and retained documentation proof. Existing agent help uses passive bounded log reads and keeps failed partial output private.

Change-source: pulse-maintainer
2026-10-07 18:45:56 +01:00
pulse-triage[bot] 65bfcda7dd Integrate reviewed guest OS-info suppression repair
Preserve the exact reviewed Core commit and completed source controls. Retain completed safety suppression on later version deferral without renewing partial observations or changing retry and backup admission.

Change-source: pulse-maintainer
2026-10-07 18:45:54 +01:00
pulse-triage[bot] 615e113469 Integrate reviewed Proxmox replication help
Preserve the three reviewed Web commits and their exact source proof. Existing help separates classification from stale inventory without disruptive diagnostics.

Change-source: pulse-maintainer
2026-10-07 18:45:52 +01:00
pulse-triage[bot] ba90322535 Integrate reviewed stable release metadata admission
Bind unattended update selection to complete release objects with explicit stable maturity, while preserving signature, edition, consent and recovery gates.

Change-source: pulse-maintainer
2026-10-07 18:30:10 +01:00
pulse-triage[bot] d793f32a30 Preserve reviewed maintenance with current summary cleanup
Additively incorporate the batch-start upstream export cleanup without changing accepted commit identities. Retain monitoring, recovery and help integrations for validation and protected publication.

Change-source: pulse-maintainer
2026-10-07 18:06:41 +01:00
pulse-triage[bot] 4733e816e2 Bound passive agent log collection across help entry points
Replace indefinite journal followers, a failure-masking search and diagnostic debug/restart advice with one shared deadline-bounded reader. Preserve failed reads and both streams, identify the affected agent host and keep raw errors private. Exercise copied commands with missing-tool, partial-failure, timeout and termination controls; no native recovery claim.

Contract-Neutral: Existing agent-help recipes and executable documentation controls only; agent, installation, collection, command-channel, permission and notification runtime contracts are unchanged.
Change-source: pulse-maintainer
2026-10-07 18:06:37 +01:00
pulse-triage[bot] 7cea42a4c1 Retain guest OS-info suppression across later command deferral
Preserve completed OS-info failure or reset state when a subsequent version read pauses. Keep partial identity and observation age unchanged, honour retry backoff, and cover cold/retained caches and both HTTP poll entry points without native guest or backup changes.

Contract-Neutral: Restore existing OS-info negative-cache semantics across version deferral; no public API, configuration, admission, pause or observation contract changes. Dedicated source tests and contract clarification accompany the fix.
Change-source: pulse-maintainer
2026-10-07 18:05:31 +01:00
rcourtman dace9295c6 Remove exports orphaned by the summary component deletions (#2671)
Stacked on #2662 (its commits are ancestors of this branch, so this PR
shows them until #2662 merges).

The May cleanup deleted InfrastructureSummary, StorageSummary, the
shared summary table and the SummaryMetricCard, InteractiveSparkline,
DensityMap and SummaryPanel primitives, but left the helpers only they
called. This removes them:

- `getMetricsChartKeyCandidatesFromResource` (utils/agentResources.ts)
- `getNormalizedIdentityLookupVariants` and
`getAgentLikeIdentityAliases` (utils/resourceIdentity.ts)
- `getPBSStatusIndicator` (utils/status.ts)
- `useSummaryContextualFocusState`
(components/shared/contextualFocus.ts), plus
`filterSummarySeriesByGroupScope`, `resolveSummaryCardInteractionState`
and `SummaryCardInteractionState` that only it used
- the deprecated `ChartsAPI.getInfrastructureCharts` delegate; its
branch tests now exercise `getInfrastructureSummaryCharts`

Proof of deadness: a TypeScript import/export graph from `src/index.tsx`
finds no production importer of any removed symbol, and a repo-wide grep
finds no remaining reference. The production build differs from its
parent only in the shared chunk that carries ChartsAPI, which loses the
delegate's static method (99 bytes); after normalising chunk hashes, SRI
hashes and minified identifiers every other file is identical. Tests:
`src/utils`, `src/components/shared`, `src/api`, `src/features`,
`src/hooks`, `src/__tests__`, Workloads, Storage and Infrastructure
suites pass (only the known Mac-only `agentInstallCommand` failures).

Contracts: frontend-primitives, storage-recovery and
performance-and-scalability still described the deleted summary cards,
sparkline, density map, summary panel, sticky summary section and Jump
to row button as live owners, and credited contextualFocus.ts with the
removed hook's series filtering. Those clauses now name the live owners
(summaryCardInteraction.ts behind summaryTableFocus.ts, the storage
summary cache behind the pool Growth column, TooltipPortal for the mini
sparkline and backup activity chart tooltips), become conditional on a
summary strip returning, or go. api-contracts and unified-resources
record the removed chart client and chart-key helper.

Browser pass (embedded production build, demo mock stack, 1440x900 and
390x844): Proxmox node-to-guests reveal, guest row hover emphasis and
inline detail reveal, storage Growth column, pool row hover and inline
detail, pool-group hover preview, pin and Escape clear. Receipt:
`frontend-modern/browser-verification/232e5c23490f8cd4.json`.
2026-10-07 18:02:21 +01:00
pulse-triage[bot] df046b6f88 Integrate reviewed passive notification report guidance
Merge exact PI candidate d0a4f48bb2. Retain optional Test-versus-delivery and message-identity context without new sends, queue mutation or destination disclosure. Exact source proof and adverse controls were reviewed; no runtime or notification-delivery acceptance is claimed.

Change-source: pulse-maintainer
2026-10-07 17:58:55 +01:00
pulse-triage[bot] d0a4f48bb2 Keep notification report evidence passive and distinct
Clarify Test versus ordinary delivery and message identity in the existing optional evidence fields. Preserve original queue context without sending, replaying or exposing destination secrets; pin both form schemas and the mirrored triage guide.

Contract-Neutral: Report guidance and its static checks only; no runtime, notification delivery or intake automation change.
Change-source: pulse-maintainer
2026-10-07 17:44:15 +01:00
pulse-triage[bot] 01b910fcda Bind unattended updates to complete stable release objects
Replace line-oriented release admission with object-bound JSON maturity checks. Refuse missing flags, mixed sibling fields, incomplete payloads and redirect-only discovery; bound both API reads without ambient curl configuration. Preserve highest-stable selection, confirmed latest fallback and all existing installation/rollback gates.

Change-source: pulse-maintainer
2026-10-07 17:42:05 +01:00
pulse-triage[bot] ef222a7c7b Reclaim expired guest-command admission before capacity deferral
Count only new endpoint/VMID slots and reclaim expired idle entries when a whole alias group otherwise exceeds the unchanged 4096-entry bound. Keep busy work, uncertain completion, backup locks and single-attempt commands protected. Cover all six HTTP readers, alias reuse and fail-closed full capacity; no native recovery or reported-cause claim.

Change-source: pulse-maintainer
2026-10-07 17:41:02 +01:00
pulse-triage[bot] cb74547e15 Explain safe Proxmox replication disagreement checks
Separate job outcome classification from stale or absent inventory, point to existing native and expanded-row evidence, and keep disruptive diagnostics and broad permissions out of the help path.

Contract-Neutral: Existing mirrored help and rendered guidance checks only; replication runtime, API and authority contracts are unchanged.
Change-source: pulse-maintainer
2026-10-07 17:40:05 +01:00
pulse-triage[bot] d73614cc57 Integrate reviewed cross-installation Proxmox help
Preserve the exact Web candidate, mirrored guidance and rendered-document proof without treating documentation as native recovery.

Change-source: pulse-maintainer
2026-10-07 17:28:34 +01:00
pulse-triage[bot] 7fc545fb9f Integrate reviewed updater edition and audit ownership repairs
Retain the exact Delivery candidate and completed installer race evidence without claiming installed update acceptance.

Change-source: pulse-maintainer
2026-10-07 17:28:32 +01:00
pulse-triage[bot] 62068fce3f Integrate the upstream low-alert threshold repair
Preserve the batch-start upstream frontier alongside reviewed local maintenance.

Change-source: pulse-maintainer
2026-10-07 17:28:23 +01:00
pulse-triage[bot] f1745e47f4 Integrate reviewed privacy-safe cross-installation issue intake
Preserve the exact reviewed product-intelligence commit and its proof. Existing intake fields and mirrored triage guidance keep repeated identities distinguishable without private names or setup changes; no runtime or new product surface changes.

Change-source: pulse-maintainer
2026-10-07 17:23:14 +01:00
rcourtman 7eb32f3181 Keep a low alert default's clear below its trigger (#2678)
## Problem

Setting an alert global default to a positive trigger of 5 or less, for
example Machines CPU at 1% in Alerts > Thresholds, stored
`agentDefaults.cpu` as `{trigger: 1, clear: 75}`, a clear far above its
trigger.

The thresholds page sends `clear = max(0, trigger - 5)`, which is 0 for
those triggers. `NormalizeAgentDefaults` derived `trigger - 5`, found it
non-positive, and fell back to the factory clear (75/80/85).
`ValidateHysteresisThresholds` never checked agent CPU, memory or disk,
so the pair survived. PBS CPU/memory, node temperature and
`internal/config/persistence.go` had the same fallback. On the save
path, validation repaired PBS and node temperature, but
`LoadAlertConfig` still turned a stored node temperature clear of 0 into
75; at startup `UpdateConfig` repaired that in memory.

Per-type `DiskFillByType` / `DiskTempByType` entries were worse: any
clear ≤0 reset the whole pair. A SATA disk temperature trigger of 4 from
the thresholds page was stored as 55, and an API-written fill entry of
`{88, 0}` became `{90, 85}`.

## What a user saw

- **Firing and resolving did not change for these pairs.**
`buildCanonicalMetricSpec` drops a clear at or above the trigger, and
the reducer then clears at the trigger. That held both before and after
this change. There was no flapping and no alert that never re-armed.
- `/api/alerts/config` and `alerts.json` served the factory clear above
the trigger.
- Firing agent alerts carried it as `metadata.clearThreshold`: 75 on a
1% CPU alert, and 85 on a disk alert still firing at 81.8%.
- A per-type disk trigger of 1–5 was silently replaced by the type
default.
- Machines metric bars: `resolveThreshold` in `metricThresholds.ts`
clamps the warning level to `min(clear, trigger)`, so a 1–5% agent
trigger had no warning band. Readings below the trigger, including 0,
now show amber instead of green, which matches guest, node, Docker and
Kubernetes bars at the same setting. This one is read from the code; I
did not exercise it in a browser.

## Fix

- PBS, node temperature, agent and per-type disk defaults now use
`normalizeThresholdPointer`, the normalizer Kubernetes, TrueNAS and
vSphere already use. A missing clear sits five points under the trigger,
floored at 0. A clear at or above the trigger is repaired. Zero still
means off for global defaults, and nil or negative still takes the
factory default. For the canonical per-type keys (the types the page
edits), a non-positive trigger still resets to the type default, because
the page cannot represent a per-type Off; other per-type keys are stored
as written, as before.
- `internal/config/persistence.go` drops its duplicate
agent/node-temperature and storage helpers and calls the same normalizer
through a new `NormalizeHysteresisThreshold`. A config saved before this
fix loads repaired.
- Agent CPU/memory/disk/disk temperature are added to
`ValidateHysteresisThresholds`, as every other family already is.
- One deliberate behaviour change for invalid pairs above the margin: an
API-written `{50, 80}` is repaired to `{50, 45}`, so it now has a real
recovery band where it used to clear at the trigger. This matches what
`EnsureValidHysteresis` already does for every other family.
- An alerts contract clause records the rule.

## Verification

- `internal/alerts/config/normalize_low_trigger_clear_test.go` runs
every default family and both per-type maps through the `UpdateConfig`
normalizer chain.
- `TestUpdateConfigKeepsLowTriggerClearBelowTrigger` pins the saved
config and a firing alert's `clearThreshold`.
- `persistence_test.go` checks the written `alerts.json` itself as well
as load repair.
- All new tests fail against the previous normalizers. Coverage cases
that pinned the old fallback (including one labelled "suspected source
bug") are updated.
- `go test ./internal/alerts/... ./internal/config/ ./internal/ai/`
passes.
- Live, on private mock stacks (baseline binary vs this branch): I saved
the payload the thresholds page sends, then read back
`/api/alerts/config`.
- Baseline: agent `{1,75}`, `{3,80}`, `{5,85}`; `diskTempByType.sata`
`{55,50}` for a 4°C trigger; firing agent alerts reported
`clearThreshold` 75/80/85.
- This branch: every pair `{t, 0}`, `diskTempByType.sata` `{4, 0}`, and
alerts reported 1/3/5.
- After a restart, this branch also repaired a hand-written
`alerts.json` holding `{1,75}` and node temperature `{2,75}`.
2026-10-07 17:22:55 +01:00
pulse-triage[bot] 819e5de1b4 Preserve reviewed maintenance with the published Patrol context cleanup
Additively compose exact upstream 5c55197900 imported by the bounded refresh receipt. Preserve every reviewed commit and the existing pending proposal; final-content browser coverage and exact composed-source validation remain required before publication.

Change-source: pulse-maintainer
2026-10-07 17:17:59 +01:00
rcourtman 5c55197900 Remove test-only Patrol context summary helpers (#2674)
Removes two exports from
`frontend-modern/src/features/patrol/patrolInvestigationContextModel.ts`
that only tests still called, and corrects the contract prose that
described the removed Patrol supporting-context surface as live.

## Why

`buildPatrolInvestigationContextSummary` (with its input type) and
`selectPatrolSupportingRecentChanges` lost their last production caller
in 27ba85ddc6 (2026-06-23). That commit removed the Patrol page's
supporting-context block and its `Details` toggle for recent changes,
learned correlations and policy coverage. Since then, only their own
tests and a raw-source guard referenced them.

## Evidence

- A TypeScript-compiler import graph rooted at `src/index.tsx` on
current main shows no reachable importer of either export. `git grep`
finds only the tests and the guard.
- Before/after production `vite build`: byte-identical across all 187
dist files, because Rollup had already tree-shaken the code.
`check-bundle-size.mjs` passes and `.bundlesize.json` is unchanged.
- Browser pass on an embedded build of this tree (demo-mode mock stack,
1440x900 and 390x844). None of these show recent-change, correlation,
policy-coverage or `Details` context UI: Patrol Inbox, Protection,
Activity, Finding options and history, the finding review panel, the run
findings snapshot and the selected run. The attention ("Explain with
Assistant") and run ("Discuss") handoffs from this module still open the
Assistant drawer with their briefings. Receipt:
`frontend-modern/browser-verification/eaf59cf8f93521c4.json`.
- `tsc --noEmit`, eslint and prettier pass. Vitest passes over
`src/features`, `src/utils`, `src/components/AI`,
`src/components/patrol`, `src/stores` and `src/pages`. The only failure
is `agentInstallCommand.test.ts`, which fails on the Mac regardless of
this change. `contract_audit.py`, `registry_audit.py` and
`status_audit.py` all pass.

## Guard

The raw-source guard in `frontendResourceTypeBoundaries.test.ts` pinned
the removed builder. It now does three things:
- It pins the export of
`buildPatrolAssistantFindingHandoffFromUnifiedFinding`, which the Patrol
hook imports.
- It checks the hook's import declaration with one regex, instead of the
symbol and the module path as separate strings. Moving the import to
another module now fails, and a single-line import still passes.
- The `policy-covered resource` copy pin is now a negative pin.

## Contracts

The patrol-intelligence, frontend-primitives, api-contracts and
ai-runtime clauses that described the removed surface now match the
code:
- Patrol renders no recent changes, correlations, posture or summary
card, and reads the AI summary only for overall health.
- There is no `Details` context affordance. One rule that still allowed
it contradicted an existing prohibition.
- The shared change and correlation cards carry no resource-link
default.
- Rules for a returning summary or recent-change normalization are now
conditional.

## Left for other PRs, already in progress

- The equally unreachable Patrol assessment and save-failure Assistant
handoff builders, their prose, and the retired summary-card and
assessment-strip clauses.
- The Patrol refresh path's unread `/api/ai/intelligence/correlations`
fetch, plus the two api-contracts clauses about it and the matching
unified-resources.md prose.
2026-10-07 17:14:56 +01:00
pulse-triage[bot] 81271e4b4c Preserve reviewed monitoring maintenance with current upstream
Compose the exact current upstream shared-clock, last-known guest readings and platform-alert ownership repairs without rewriting the reviewed batch. Source proofs, final-content browser evidence and hosted checks remain separate publication prerequisites.

Change-source: pulse-maintainer
2026-10-07 17:09:16 +01:00
rcourtman 22b8b995ca Read the shared wall clock on Alerts overview cards (#2673)
The overview hook kept its own minute signal and handed its value to
each card as "now". A card that mounted between ticks measured from up
to a minute earlier: an alert raised 70 seconds ago read "this minute",
a reading already past the 10-minute stale cut-off led the card as the
live one, a just-raised alert stayed out of Triggered (24h) until the
next tick, and the stale flip and 24h drop landed up to a minute late.

The card now reads useRelativeTimeNow for its started age and the live
reading's stale cut-off, and the hook reads it for the 24h count, so
they measure from the wall clock and move with the shared 30-second
tick. The minute interval stays only for the delivery diagnoses
refresh, a server read that should not double its rate.
2026-10-07 16:56:58 +01:00
rcourtman 2d6a2937fc Keep platform metric alerts through Proxmox node cleanup (#2672)
`CleanupAlertsForNodes` runs on every Proxmox node poll
(`monitor_pve_storage.go`) and on every mock alert tick. It removes
alerts whose `Node` is not a current Proxmox node or PBS instance, and
keeps the types `shouldPreserveAlertOutsideNodeCleanup` lists.

Kubernetes, TrueNAS and vSphere metric alerts carry a cluster or
platform host as `Node`, and a display label (`TrueNAS Disk`,
`Kubernetes Pod`, `vSphere VM`) in `metadata.resourceType`. The preserve
list only had the bare words `truenas`, `kubernetes`, `k8s` and
`vmware`, so none of these labels matched. In an estate with Proxmox
plus one of those platforms:

- every Proxmox poll deleted these alerts without resolving them;
- their reducer incident stayed firing, so the next unified evaluation
re-created each one and handed it to the alert callback again as a new
alert.

## Change

`shouldPreserveAlertOutsideNodeCleanup` keeps an alert that carries
`metadata.platformType` (set on every unified Kubernetes/TrueNAS/vSphere
metric alert), or whose label maps through `CanonicalAlertResourceType`
to one of those platform types. The label path covers alerts persisted
before `platformType` existed. A guest alert whose Proxmox node left the
estate is still removed.

## Evidence

- Unit probe on main: a TrueNAS disk temperature alert, a Kubernetes pod
CPU alert and a vSphere VM CPU alert were all deleted by
`CleanupAlertsForNodes({"pve1": true})`. Over three
cleanup-then-evaluate cycles the TrueNAS alert went to the alert
callback 3 times; with this change, 0 times and it stays active.
- Private mock stack: TrueNAS disk temperature alerts kept vanishing
between mock alert ticks, while the disks kept their readings in
`/api/resources`. With this change they stay open.

## Tests

`TestCleanupAlertsForNodesKeepsPlatformMetricAlerts` (in
`internal/alerts/alerts_test.go`) raises a TrueNAS disk, a TrueNAS pool,
a Kubernetes pod and a vSphere VM alert, then runs the cleanup three
ways: as raised, with only `platformType` (label unrecognised), and with
only the label (`platformType` removed).
`TestCleanupAlertsForNodesStillRemovesMissingProxmoxNodeAlerts` keeps
the removal of a guest alert on a removed Proxmox node. Each preserve
condition was mutation-checked: removing either one fails the first
test.
2026-10-07 16:56:25 +01:00
pulse-triage[bot] e4e3e8d655 Keep captured audit findings past early watchdog cleanup
The full installer race suite exposed report deletion despite the subshell-counter guard. Bind EXIT cleanup to the original process using the direct child shell parent PID, including before the inherited counter is initialised. Retain strict and unchanged-graph verdicts, exact arguments and parent cleanup. Align the stale channel-copy assertion with the existing non-stable message; executable consent controls remain unchanged.

Change-source: pulse-maintainer
2026-10-07 16:53:29 +01:00
rcourtman e30b44ea8f Read a silent in-guest agent's guest drawer readings as last known (#2669)
A Pulse agent linked into a VM keeps its last report on the VM resource
after it stops reporting: the registry copies the agent's whole facet,
`agent.stale` included, onto the guest. The guest drawer never read that
flag. A quiet agent's Guest RAID kept its live colours and "Rebuild: 42%
· 118M/sec", its network rows and version read as current, and nothing
said the agent had gone.

## Change

- `useWorkloads` maps `agent.stale` onto the workload as `agentStale`,
and sets `disksFromAgent` when Proxmox reported no guest filesystems and
the agent's own `disks` filled in. Filesystem rows are the only agent
reading with a second source.
- When the agent is stale, the guest drawer:
- passes `HOST_AGENT_STOPPED_REPORTING_REASON` to `RaidCard`
(`lastKnownReason`, #2652);
  - puts a status line over muted Network rows;
  - marks the Pulse Agent row "· stopped reporting";
- reads agent-supplied filesystem rows through the existing `last-known`
disk read state.
- Agent-supplied filesystem rows now follow the agent's freshness
instead of the Proxmox guest-read reason. Before, a VM with a **live**
linked agent but no qemu-guest-agent showed the agent's filesystems as
"Usage unavailable · ?/328 GB" with advice to install qemu-guest-agent.
A stopped guest still reads those rows as unavailable, whether its
status or the Proxmox `vm-stopped` reason says so. Proxmox's own guest
filesystems keep their read state.

## Verification

- Private mock stack built from `main` plus this change. Two host agents
were held past their reporting lease and two left live, then each was
linked into a VM through `POST /api/resources/{vm}/link`. The two silent
VMs serve `agent.stale: true` with the agent's raid, disks and
interfaces; the two live ones carry no stale flag.
- Playwright at 1440 and 390:
  - silent VMs read as described above;
  - the live VM with agent-only filesystems shows usage with bars;
  - the live control is unchanged;
  - no horizontal overflow.
- New tests in `GuestDrawer.test.tsx` cover:
  - silent agent;
  - live agent with Proxmox agent-not-running;
  - Proxmox filesystems under a silent agent;
  - stopped guest, with and without the reason;
  - stale flip in both directions on an open drawer.
The new tests fail against the old drawer. `useWorkloads.test.ts`
asserts source selection with distinct mountpoints.
- Frontend suites over Workloads, hooks, utils, shared, platformPage and
Infrastructure pass, apart from the Mac-only PTY tests in
`agentInstallCommand.test.ts`.
- WorkloadsSurface grows by 0.22 kB gzip (64.85 kB against a ~71.9 kB
cap), and the chunk set is unchanged.

## Not covered

- The workload table's disk cell and the drawer History still read the
aggregate disk metric with the Proxmox reason.
- The agent's OS, its addresses in the identity rows, and the lazily
loaded Physical Disks card's health and wearout carry no stale cue yet.
2026-10-07 16:46:23 +01:00
pulse-triage[bot] 453509905a Explain safe cross-installation Proxmox identity comparisons
Keep wrong-node errors, backup attribution and lost agent-backed Docker monitoring separate. Provide passive existing-view comparisons and native fallbacks without renaming hosts, re-enrolling agents or recreating the incident. Bind the help entry points and privacy limits to rendered documentation checks.

Contract-Neutral: Existing help maintenance only; no runtime, identity or provider contract changes.
Change-source: pulse-maintainer
2026-10-07 16:44:12 +01:00
pulse-triage[bot] 2dbee5639a Stop unidentified editions entering community auto-updates
Require a successful bounded executable identity probe before release discovery. Keep Pro and unknown runtime editions untouched even when VERSION offers a usable community version, and do not defer a broken primary binary to a legacy one. Exercise the real shell classifier and main boundary with adverse controls and a TERM-resistant deadline.

Change-source: pulse-maintainer
2026-10-07 16:39:25 +01:00
pulse-triage[bot] 7a91214811 Preserve cross-installation relationships in privacy-safe reports
Repeated identity reports affect node errors, backup status and agent-backed Docker monitoring. Let reporters describe shared and independent targets using consistent aliases, without disclosing private identities or changing their setup. Preserve every affected surface during triage.

Contract-Neutral: Clarifies existing issue intake and triage guidance; no runtime, automation or feature contract changes.
Change-source: pulse-maintainer
2026-10-07 16:38:46 +01:00
pulse-triage[bot] e3965e4e14 Merge reviewed bounded TrueNAS diagnostic help
Retain exact Web candidate 20261007T151838Z-web-product and tested documentation/renderer content. Reuse existing fail-closed log readers; preserve canonical Core and Delivery changes without claiming native recovery.

Change-source: pulse-maintainer
2026-10-07 16:37:29 +01:00
pulse-triage[bot] 6ab3c9a41b Integrate reviewed bounded Docker module transport recovery
Preserve exact Delivery commits 697608448a and 745c1f9ce5. Same graph and endpoints; only the observed transport-only error can retry. Refusals, integrity failures, unknown output, timeouts and signals remain terminal.

Change-source: pulse-maintainer
2026-10-07 16:25:56 +01:00
pulse-triage[bot] e07d00154c Integrate reviewed local filesystem directory-boundary repair
Preserve exact Core candidate 0ef2bfa7f1 alongside batch-start main. Restore independent local capacity and alert readings while retaining system mounts, explicit exclusions and guest safety.

Change-source: pulse-maintainer
2026-10-07 16:25:47 +01:00
pulse-triage[bot] 26db2344de Route TrueNAS diagnostics through bounded fail-closed log readers
Remove the record-limited but indefinitely blocking duplicate commands. Use the existing unfiltered Pulse readers, preserve NAS-specific safety guidance, and exercise the linked source and rendered help against failures, real deadlines, TERM resistance and missing timeout.

Contract-Neutral: Documentation and diagnostic-recipe verification only; no runtime, API, permission or subsystem behaviour changes.
Change-source: pulse-maintainer
2026-10-07 16:21:52 +01:00
rcourtman 9bf72eb407 Merge remote-tracking branch 'origin/main' into fix/global-default-off 2026-10-07 16:08:21 +01:00
rcourtman 564ea41a6e Keep Off alert defaults off after saving
Switching a Global Defaults metric Off on the thresholds page staged -1,
and the save payload passed it through as trigger -1. Alert config
normalization reads a negative default trigger as unset and restores the
factory threshold, so every Off default with a positive factory value came
back On after save and reload: agent CPU, memory, disk and disk
temperature, Proxmox node temperature, PBS, Docker, Kubernetes, TrueNAS,
vSphere and storage. Guest and node CPU, memory and disk escaped only
because nothing normalizes them.

The payload builder now writes every Off default as trigger 0, which the
engine and normalization both read as off. Overrides keep -1, which the
override path stores as written. A -1 already stored on a guest or node
default becomes 0 on the next save.

With -1 and 0 both live Off values, the Global Defaults Custom badge
compares Off values as equal, so the factory -1 I/O defaults no longer
read Custom against a saved 0 and tell users they changed something they
had not. A row's custom marker keeps comparing raw values: it drives the
row's revert control and override removal, so a stored -1 override must
stay visible against an Off default.

Contract-Neutral: Global Defaults Off now persists; the Off wire value and Custom badge rule are recorded in the alerts contract, and frontend-primitives gains no rule
2026-10-07 16:05:38 +01:00
pulse-triage[bot] 2c02eba64b Preserve monitoring repairs with current demo and disk identity safeguards
Additively incorporate published demo-write admission, disk-replacement identity and Proxmox sensor guidance. Resolve the storage-recovery contract append conflict by retaining both the silent-source History rule and the demo-write rule unchanged.

Change-source: pulse-maintainer
2026-10-07 16:05:00 +01:00
rcourtman 7432995fe8 Stop websocket upgrade headers exempting demo-mode writes (#2666)
`DemoModeMiddleware` let any request carrying `Upgrade: websocket`
through, whatever its method or path. On a `DEMO_MODE` instance a
`POST`/`PUT`/`PATCH`/`DELETE` with that header reached its handler, and
the read-only guard did nothing.

## How exposed the public demo was

- The public demo (`demo.pulserelay.pro`, v6.5.0) runs `DEMO_MODE=true`
with `PULSE_AUTH_USER=demo`, and its login page signs visitors in as
demo/demo. No proxy auth is configured. Caddy is a plain `reverse_proxy`
with no method filtering, and it forwards `Connection: Upgrade` and
`Upgrade: websocket` unchanged.
- That session is the configured admin.
`sessionUserHasSettingsAdminPrivileges` matches `cfg.AuthUser`, so
`RequireAdmin` accepts it, and session requests skip token scope checks.
Nothing in the handlers checks demo mode on its own, so this middleware
was the only write guard.
- Anyone using the public credentials could therefore make any admin
write by adding the two headers. That covers switching the demo out of
mock mode (`POST /api/system/mock-mode`), changing system settings,
creating organisations, importing config, changing the password and
adding notification targets. A CSRF token was needed, but the attacker
gets one from their own login.
- On the old code, the new contract test reproduces this end to end. A
demo/demo session's mock-mode POST with upgrade headers returned 200 and
turned mock mode off.
- I found no sign it was used. Over the last 30 days the demo journal
shows every "mock mode disabled" line paired with a process restart (new
PID). Config file mtimes match deploys. 5,782 ordinary writes were
blocked over the same period. Caddy keeps no access log for the demo, so
this is the absence of evidence, not proof.

## Change

The exemption is deleted rather than narrowed. RFC 6455 handshakes are
`GET` requests, gorilla's `Upgrader` refuses any other method, and `GET`
already passes the read-only branch above it. A narrowed `GET`-only
exemption would never run. `/ws` and `/api/agent/ws` keep connecting,
and the request method alone decides.

## Tests

- `TestContract_DemoModeUpgradeHeadersDoNotExemptWrites` (registered
api-contracts proof) runs against the full router. It signs in as the
configured demo admin and checks that the session passes `RequireAdmin`
for a read. A mock-mode POST carrying `Connection: Upgrade` and
`Upgrade: websocket` now gets the demo 403 and leaves mock mode on, and
a real `/ws` handshake from the same session still upgrades.
- `demo_middleware_test.go`: the table row that asserted the bypass now
expects 403. POST/PUT/PATCH/DELETE with handshake headers to mock-mode,
settings, orgs, `/ws` and `/api/agent/ws` all get 403. Real gorilla
handshakes to `/ws` and `/api/agent/ws` through the middleware still
upgrade and echo.
- Negative control: run against the old middleware via `go test
-overlay`, the new contract test and the upgrade-header test fail, and
the handshake test passes on both versions.

## Not changed here

- `auth.go` `isWebSocketUpgrade` and `configapi`
`explicitAPITokenFromRequest` still accept a `?token=` query credential
on any request with `Upgrade: websocket`. A valid, scoped token is still
required, so no access is gained, but the credential can travel in a
URL. The `ErrorHandler` skip in `middleware.go` only bypasses recovery
and metrics.
- Older and separate:
`/api/ai/sessions/{id}/{abort,summarize,fork,undo,redo}` change state on
`GET`, which the demo guard admits as reads. Tracked separately.
2026-10-07 15:49:33 +01:00
pulse-triage[bot] 745c1f9ce5 Keep a kill backstop on the bounded module download
A TERM-only timeout relies on the child exiting. Keep a fixed five-second KILL backstop, preserve every timeout as a failure and bind the shell/Go command proofs to the actual bounded invocation.

Contract-Neutral: Internal build timeout containment; source graph, endpoints, checksum trust and artifact acceptance are unchanged.
Change-source: pulse-maintainer
2026-10-07 15:46:01 +01:00
rcourtman cb5ffc2efe Remove dead Patrol assessment and save-failure handoffs
buildPatrolAssessmentAssistantHandoff lost its last caller when the Patrol
summary card's Discuss handler was removed in eac7dfe9ef (2026-05-15), and
buildPatrolConfigurationFailureHandoff lost its last caller in 27ba85ddc6
(2026-06-23). Since then only their own tests called them. A TypeScript
compiler import graph rooted at src/index.tsx confirms no reachable file
imports either one. Removing them also drops PatrolInvestigationContextSummary,
their input and output types, and the private assessment, recent-change,
correlation and save-failure helpers that only they used (found with
noUnusedLocals until it reported nothing).

Rollup had already tree-shaken all of it: production builds of the parent
tree and of this tree are byte-identical across all 187 dist files, and the
bundle-size check passes. The presentation helpers the dead code imported
keep their live callers.

Their test cases go with them. Two cases covered branches the live finding
handoff still uses, so they now run through
buildPatrolAssistantFindingHandoffActions and buildPatrolAssistantFindingHandoff:
a typed pending action without a legacy approval id stays approval-bound,
and the finding context qualifies its resource with the record's node and
truncates long lines. v8 coverage of the surviving model code matches the
pre-removal baseline except one defensive guard the live caller cannot
reach. The raw-source guards pinned none of the removed code.

The Assistant drawer still restores saved patrol_assessment and
patrol_configuration_failure sessions, so that compatibility path stays.

The contracts described both handoffs as live producers, and described a
Patrol summary card, assessment strip, assessment readout, score chip and
header trust line that the page no longer renders. patrol-intelligence,
ai-runtime, api-contracts, frontend-primitives, security-privacy,
storage-recovery, agent-lifecycle and unified-resources now say what the
code does: those handoffs survive only as saved sessions,
the drawer, session list and API keep restoring them, rules for a returning
summary card, strip or readout are conditional, and handoff_actions and
adjacent-boundary clauses name the finding and attention handoffs that send
them today. The run-history handoff clauses also claimed the browser sends a
[Patrol Run Context] block and scoped resources; it sends the briefing and
run identity, and internal/ai/patrol_assistant_handoff.go rebuilds the rest
from the run ID.

docs/AI.md (and its public mirror) told users the legacy operational score
sits in a collapsed supporting-context section. That section and the score
are gone; the section now points at Activity > Finding options and history,
where check history lives. ai_runtime_docs_policy_test.py, its registered
proof, now pins that location and rejects the retired claim. The same file
had failed on main since 2789a15fe5 reworded the Patrol mode API sentence to
"The API retains `autonomy_level` for compatibility" without updating its
pin; the pin now matches that wording.
2026-10-07 15:45:49 +01:00
pulse-triage[bot] c39cb074f7 Integrate reviewed private-file Unix agent setup help
Change-source: pulse-maintainer
2026-10-07 15:40:12 +01:00
pulse-triage[bot] 17a79564a8 Integrate reviewed Patrol summary help
Change-source: pulse-maintainer
2026-10-07 15:40:10 +01:00
pulse-triage[bot] bb39f89771 Compose batch-start availability repairs with retained mock fixture cleanup
Keep the published process-wide tenant switch while using the retained removal lifecycle for both agent and Docker fixture hosts in every ended epoch. Preserve both lifecycle regression tests; no candidate source is re-parented.

Change-source: pulse-maintainer
2026-10-07 15:40:08 +01:00
rcourtman 75263ea589 Refuse a seeded disk slot mapping when the disk was replaced (#2667)
`ingestRecord` reuses an existing source-key mapping before any identity
matching. For physical disks it checked only the type and parent scope.
Proxmox keys a disk by its slot (`ProxmoxPhysicalDiskSourceID`, e.g.
`pve1-node1--dev-sdb`), so in a registry seeded from persisted unified
resources, a disk swapped into the slot mapped straight back to the
previous disk's canonical resource. Where an agent report had been
merged in, `mergeInto` also kept the old disk's serial, WWN, temperature
and FAILED health. This was found while reviewing #2658, and its
contract note said the seeded mapping was not covered.

**Change.** A Proxmox disk observation now refuses the mapping when the
two disks carry hardware identities naming different disks
(`diskinventory.HardwareIdentityConflict` from #2658):
- Only fields reported on both sides count. A missing serial or WWN,
including Proxmox's literal `unknown`, keeps the mapping.
- Differing serials do not count for SAS, because Proxmox may report the
SAS address there (#1595).
- They also do not count when the mapped disk was merged with an agent
report and Proxmox did not read the drive's own serial. This is the same
rule monitoring's `hostAgentSMARTSerialComparable` applies: NVMe on both
sides, or agent SATA under Proxmox vendor `ATA`, and never a Proxmox
`usb` disk.
- The refused key falls through to the linked-disk join, `findMatch` and
`chooseNewID`, but none of them may merge it back into the refused disk.
An ID landing on it is replaced with the key's source-specific ID.

**Reachability.** No current caller replays Proxmox disk records over a
seeded mapping. The monitor rebuilds a fresh registry each poll. The
resource API, host-continuity and Patrol registries seeded from unified
resources do not replay Proxmox disk records. This closes the registry
contract rather than a reported symptom.

**Proof.** `TestRegistrySeededSlotMappingRefusesReplacedDisk` (in
`internal/monitoring/issue1595_collection_trust_test.go`) builds the
previous disk through `IngestSnapshot` with fixtures shaped like
`monitor_pve`'s output, rehydrates with `IngestResources(List())`, polls
Proxmox, then rehydrates a second time.
- 5 replacement cases: SATA with and without a new WWN, SAS, a
USB-enclosure swap ingested Proxmox-first, and a disk Proxmox alone
reports.
- 8 same-disk controls: re-ingested, no serial, a SAS address serial,
SAS without a WWN, a USB bridge serial, a bridge serial under vendor
`ATA`, a SCSI designator, and a SCSI designator from an agent without
collection status.

Negative controls via `go test -overlay` each fail only their intended
cases: the whole fix removed, the refused-disk exclusion removed, serial
comparability always true or always false, the Proxmox-only branch
removed, a collection-status provenance rule instead, and the USB veto
removed. The combined tree of main, #2660 and this change also passes
`internal/unifiedresources` and the monitoring disk tests.

**Residuals (in the unified-resources contract).**
- Drives that share a serial they do not own, such as a USB bridge's,
still read as one disk, as they do in identity matching.
- The refused disk keeps its Proxmox facet until a rebuild, so the
slot's PVE alert reference resolves as ambiguous rather than to either
disk.
2026-10-07 15:38:50 +01:00
rcourtman 080ace2e97 Show the outdated sensor setup notice on every Proxmox tab (#2668)
Since the per-tab Proxmox queries landed, only the Storage tab loads
`physical_disk` rows. So the outdated sensor setup notice, which judged
nodes from the active tab's disk rows, never showed on Overview.

The registry now derives the verdict on each Proxmox node
(`proxmox.sensorSetupOutdated`, an explicit true or false on every node)
right after child counts resolve each disk's canonical parent. A node is
flagged when its temperature payload is available and in the legacy
`sensors -j` format and a SATA/SAS disk under it has no reading
collected now (`diskinventory.TemperatureCollected`). The page lists
flagged nodes from the rows Overview, Storage, Replication and Backups
already load, so no tab hydrates disks for it.

Rule fixes that came with the move:
- Proxmox's disk inventory types SATA/SAS disks as `hdd`/`ssd`, so the
old `sata`/`sas`-only check never matched a node without a host agent.
- A retained reading no longer counts as current (carried over from
#2659's frontend rule).
- The verdict is sent as explicit false, because the browser's
field-by-field facet merge would otherwise keep an earlier true after
the condition clears.

Stacked on #2659 (shares its `sensorSetup.ts` edit, which this
replaces).

Verified on a private mock stack at 1440px and 390px:
- Overview, Storage, Replication and Backups show the notice for
legacy-format nodes with an `hdd` disk without a reading, a retained
SATA reading, and a SATA disk without a reading.
- Overview makes no `physical_disk` request.
- With `main`'s frontend swapped in, Overview showed nothing and Storage
named one node of three.
- A node whose disk starts reporting drops out on the next websocket
update without a reload.
2026-10-07 15:38:27 +01:00
pulse-triage[bot] 697608448a Recover bounded HTTP2 module proxy failures in Docker builds
Keep the same pinned Go graph and endpoint. Retry only the observed transport-only diagnostic, at most three attempts; stop on refusals, integrity errors, unknown output, timeouts and signals. Execute the actual Docker download command against controlled outcomes and retain its original-base rejection.

Contract-Neutral: Build-only transport recovery; source graph, endpoints, checksum trust and artifact acceptance are unchanged.
Change-source: pulse-maintainer
2026-10-07 15:38:23 +01:00
pulse-triage[bot] 0ef2bfa7f1 Keep local disk mounts beside system directories visible
Match automatic system-mount exclusions at directory boundaries rather than textual prefixes. Local /snapshots, /runtime and /var/lib/docker-data volumes retain capacity and usage through agent collection, both report paths and QGA aggregation; genuine system and remote/container mounts remain filtered. Regression controls include host usage alert onset and recovery.

Contract-Neutral: Restore existing filesystem eligibility without changing schemas, identities, explicit operator patterns or guest-command safety policy.
Change-source: pulse-maintainer
2026-10-07 15:36:53 +01:00