Merge origin/main into fix/saml-acs-request-binding

The SAML stack this branch builds on appends its boundary notes to
storage-recovery.md at the anchor main used for the linked-agent stale
flag note. Both sets of notes are kept, the SAML ones first.

Contract-Neutral: Merge of origin/main into the SAML stack: main's changes were governed when they landed, and the only resolution keeps both sets of storage-recovery boundary notes
This commit is contained in:
rcourtman
2026-10-08 00:13:11 +01:00
137 changed files with 5918 additions and 2835 deletions
+3 -1
View File
@@ -891,7 +891,9 @@ For an existing incident whose notifications should be paused without clearing
it, use the snooze and unsnooze endpoints below.
- `GET /api/alerts/config`
- `PUT /api/alerts/config`
- `PUT /api/alerts/config` — replaces only the top-level keys the body
carries; a key left out keeps its stored value, and the merged config is
normalized as before. The body must be a JSON object.
- `GET /api/alerts/deadman/config` — returns only whether an external watchdog
is configured; `pingUrl` is `***REDACTED***` when present and never returns
the credential-bearing URL
@@ -248,6 +248,18 @@ Without comparable identity (a SAS, SCSI or USB replacement with no WWN on one
side) the row still matches. Report admission and the SMART wire contract are
unchanged; the regression is
`TestHostAgentSMARTRowForSwappedOutDiskDoesNotLendItsIdentity`.
A PVE disk that a still-reporting linked Agent's SMART row matches is marked
`AgentSMARTReported` (internal poll evidence on `models.PhysicalDisk`, never
serialized); disks the poller builds from that report when the Proxmox disk
query fails go through the same merge, and an Agent whose lease lapsed owns
none. The Agent then owns the disk's temperature alert
through `CheckHost`, and the PVE disk temperature check closes its own as
moved to the Agent, so one disk never alerts twice. Report admission, the SMART
wire contract and Agent authority are unchanged;
`TestMergeHostAgentSMARTIntoDisksMarksDisksTheAgentReports` in
`internal/monitoring/physical_disk_roundtrip_test.go` and
`TestPhysicalDiskAgentSMARTReportedStaysInternal` in
`internal/models/deepcopy_test.go` pin the marker.
Assistant historical metric wiring uses the current monitor's retained store
and registry metrics coordinates. Historical reads do not alter enrollment,
@@ -3678,6 +3690,14 @@ Agent` secondary handoff against the live setup wizard instead of relying
## Current State
### Alert settings saves do not touch agent lifecycle
`PUT /api/alerts/config` in `internal/api/alerting/alerts.go` now keeps stored
values for the top-level keys a client leaves out. Agent alert defaults
(`agentDefaults`) follow the same rule as every other key; enrollment,
reporting leases, commands and agent removal are unchanged.
`internal/api/alerting/alerts_test.go` pins the handler.
### VM guest execution admission (backup precaution)
`AgentRegisterPayload.guest_exec_guard_version` is optional; version 1 on a
@@ -4712,6 +4732,12 @@ agent-lifecycle operations. If those routes are called directly, the API must
fail them as unsupported rather than presenting file diffs or reverts as
agent command rollback, enrollment repair, update rollback, or fleet-control
authority.
Assistant session mutations (`/abort`, `/summarize`, `/fork`, `/undo`,
`/redo`, `/steer` under `/api/ai/sessions/{id}`) accept only `POST`; any other
method that reaches those handlers gets `405` before touching session state.
That method guard
is AI-runtime transport hardening in `internal/api/ai_handler.go`; it adds no
agent command, enrollment, update, or fleet-control path.
That same shared dependency now also assumes hosted cloud handoff authorizes
tenant org access before browser lifecycle continues. Lifecycle-adjacent opens
into hosted workspaces may depend on `internal/api/cloud_handoff_handlers.go`,
@@ -8974,3 +9000,11 @@ like any other write instead of always reaching its handler. Agent command
websocket handshakes to `/api/agent/ws` are HTTP/1.1 `GET` requests, the only
transport its gorilla upgrader serves, and still connect on a demo instance.
No agent registration, enrolment, install, update or removal path changed.
### Demo mode hides the pprof routes
`internal/api/demo_mode_operations.go` changed only so the demo guard on a
`DEMO_MODE` instance answers `/debug/pprof` and every path below it with `404`
for every method. No agent route lives under `/debug/`, so agent registration,
enrolment, install, update, removal, report ingest and `/api/agent/ws`
handshakes are unchanged on a demo instance.
@@ -3556,6 +3556,12 @@ query...`, and `Reading storage...` before streamed tool arguments are
answer (nothing streaming, no error block, no pending approval or
question, and a user prompt available to re-send) because the undo
boundary only operates on the last durable turn.
Session mutations stay POST-only at the handler: abort, summarize, fork,
undo, redo, and steer refuse every other method with `405` and
`Allow: POST` before resolving the Assistant service. A `GET` or `HEAD`
that the demo-mode read-only guard or the CSRF check admits as a read
therefore cannot cancel a running turn, compact or rewind a transcript,
or persist a fork.
The empty Assistant drawer may surface recent non-empty sessions as direct
resume actions using the backend session list already owned by the drawer;
it must not create a parallel recent-chat store or product-authored prompt
@@ -6723,6 +6729,11 @@ seed prompt correlations now flow through the shared AI intelligence facade
first, so the detector remains an implementation detail behind one canonical
correlation access path instead of being routed directly by handlers or prompt
builders.
The browser does not read the global learned-correlation list:
`frontend-modern/src/api/ai.ts` carries no client for
`/api/ai/intelligence/correlations`, and neither the shared AI intelligence
store nor the Patrol page loads it, because no page renders it. The resource
drawer reads per-resource correlations from the resource-intelligence payload.
AI-facing policy metadata must also be cloned through the shared unified-
resource policy helper so chat and tools consumers do not maintain their own
policy copy logic. Chat mention prefetch now calls that shared helper directly
@@ -1114,6 +1114,43 @@ history and affirmative recovery. This is source-fixture lifecycle proof, not
an installed registry/destination result or a diagnosis of a reporter's daily
resolve/reopen cycle.
### A settings save keeps the alert settings it did not send
`PUT /api/alerts/config` applies the body to the stored configuration through
`Manager.ApplyConfigUpdate` and `ApplyAlertConfigUpdate` in
`internal/alerts/config/update.go`: a top-level key the client sent replaces
that setting whole, a key it left out keeps the stored value, and the result
is normalized as any update is. The thresholds page has no control for
flapping detection (`flappingEnabled` and its window, threshold and cooldown),
alert TTL cleanup (`maxAlertAgeDays`, `maxAcknowledgedAgeDays`,
`autoAcknowledgeAfterHours`) or `customRules`, so it never sends them. The
handler used to decode the body into a zero `AlertConfig`, so every thresholds
save turned flapping detection and TTL cleanup off, reset the flapping tuning
to its defaults, dropped custom rules, and persisted that. The page's
`flapping`, `aggregation` and `ioNormalization` payload objects are not
`AlertConfig` fields and never enabled anything. A stored config keeps what it
holds, including values an earlier save zeroed; nothing rewrites them.
The body is decoded on its own, so sent keys mean exactly what the decoder
makes of them, duplicates and case-folded keys included. Unsent fields are
copied from a JSON round trip of the stored config, so the result shares no
maps or slices with it. `ApplyConfigUpdate` reads, merges and applies under
the manager lock, so two concurrent partial saves cannot revert each other in
memory, and returns a JSON-cloned snapshot so that persistence, which
normalizes the config it is handed in place, never writes into the live
config's maps.
`internal/alerts/config/update_test.go` covers unsent, explicit-off,
case-folded, duplicate-key, malformed and full-body updates, a sent object
replacing its stored value whole (a sent `truenasDiskDefaults: {}` does not
inherit the stored temperature) and the no-sharing rule.
`TestUpdateConfigKeepsSettingsAThresholdsSaveDidNotSend` and
`TestApplyConfigUpdateKeepsConcurrentPartialUpdates` in
`config_validation_test.go` run saves through the manager, and
`TestUpdateAlertConfig_KeepsStoredValuesForUnsentKeys` in
`internal/api/alerting/alerts_test.go` drives the handler against a real
manager.
### Confirmed empty storage is recovery evidence
Static storage capacity evaluation must admit a zero usage observation when
@@ -1473,7 +1510,8 @@ That same guest-threshold owner also governs guest-derived lifecycle and
posture alerts. Snapshot age, backup age, powered-off state, and
configuration-change reevaluation must all construct a canonical lightweight
guest snapshot and route threshold resolution through the shared
guest-defaults → filter-driven custom rules → guest-override chain.
guest-defaults → filter-driven custom rules → guest-override → `pulse-relaxed`
chain.
That canonical guest context must preserve the live guest name and tags for
snapshot and backup posture evaluation. Ignored prefixes, `pulse-no-alerts`,
configured ignored tags, and required-tag filtering must resolve through the
@@ -1483,6 +1521,27 @@ operator's suppression policy.
Passing `nil` guest context or resolving only overrides/defaults is forbidden
because it silently bypasses custom guest rules and makes guest lifecycle
alerting diverge from running-guest metric truth.
The `pulse-relaxed` step lives in `effectiveAlertPolicyNoLock`, not in
`CheckGuest`, so polling and config-save reevaluation judge a relaxed guest's
alert by one threshold. Reevaluation rebuilds the guest from alert metadata,
so `guestSnapshotFromAlert` reads back the `tags` the evaluator recorded
(`[]string` live, `[]interface{}` after a restart). Without them a save judged
the alert by unrelaxed thresholds, resolved one raised under an unset
threshold's relaxed floor with a recovery notification, and the next poll
raised it again. Those tags must stay current: `metricOptionsWithTags` drops
the key from an open alert once its resource has no tags, or a removed
`pulse-relaxed` would keep judging the alert by the relaxed trigger. The tag
lifts CPU, memory and disk triggers to a 95/92/95 floor and gives an unset
threshold that floor, but an Off threshold (trigger <= 0) stays Off: relaxing
a guest must never raise an alert its config turned off. A filesystem's own
override still replaces the relaxed guest disk threshold for that filesystem,
on save as while polling. Proof:
`TestConfigSaveJudgesRelaxedGuestAlertByRelaxedThresholds`,
`TestConfigSaveKeepsRelaxedGuestFilesystemOverride`,
`TestPulseRelaxedKeepsOffGuestThresholdOff` and the Off case of
`TestApplyRelaxedGuestThresholds` in `internal/alerts/alerts_test.go`, and
`TestCheckGuestDropsRemovedTagsFromOpenAlerts` in
`internal/alerts/unified_eval_test.go`.
That same guest-alert owner also has to retire per-disk guest alerts when the
guest stops, disk alerting is disabled, or the reported disk set changes.
Canonical guest disk identity is only valid while the guest still exposes that
@@ -1851,14 +1910,84 @@ A disk is hot from its trigger. Alerts and Patrol findings stay open until the
reading falls to the clear value, or under the trigger when there is no band
below it. The Physical Disks verdict and the TrueNAS Health cell judge only the
current reading against the trigger. PDF reports colour disk temperatures by
the same thresholds. Judges that still differ: TrueNAS disk temperature alerts
use `TrueNASDiskDefaults.Temperature` (a flat 55/50), and the TrueNAS disk
drawer tones the reading from a fixed 55C.
the same thresholds.
`TestDiskTemperatureThresholdMatchesCheckHostPolicy` in
`internal/alerts/threshold_resolution_shared_test.go` pins per-type resolution, a raised
NVMe trigger that `CheckHost` also honours, the copy, the disabled default and
the nil manager.
TrueNAS disk temperature alerts follow the policy too:
`trueNASDiskTemperatureDefaultNoLock`
(`internal/alerts/truenas_disk_temperature.go`) is the type-default tier of
`effectiveAlertPolicyNoLock` for `truenas-disk`, so an unset
`TrueNASDiskDefaults.Temperature` resolves per disk type (the
`alertPolicyQuery.DiskType` the unified input carries from
`PhysicalDisk.DiskType`). A TrueNAS-wide value the user saved under TrueNAS
Disks replaces it for every TrueNAS disk, as a host Disk Temp override does for
an agent's disks, 0 meaning off, and a per-disk override beats both. The alert
records `diskType` so a config save re-judges it per type; one without it is
held to the lowest per-type trigger. A retained reading
(`diskinventory.TemperatureCollected` false) is no evidence and raises nothing.
The factory value is unset. `normalizeTrueNASDiskTemperature` drops a stored
55/50 only from a config without `TrueNASDiskTemperatureByType`, because the
thresholds page wrote that flat factory value back on every save; the page
writes the marker, so a 55 typed later is kept. Every config `GetConfig`
returns carries the marker, so a client that reads and writes back keeps it.
Without the marker (an old saved file, or a page loaded before the upgrade)
55/50 is read as that unchosen factory value; a client building a config from
scratch with an explicit TrueNAS-wide 55 has to send the marker. The TrueNAS
Disks Global Defaults cell reads `By type` while unset
(`globalDefaultFallbacks` on `ResourceTable`,
`resolveAlertResourceGlobalDefaultCell`), or Off when the agent Disk Temp
default switches the policy off, and switching it on then stages an explicit
TrueNAS-wide 55 because unset would stay off. Each disk row inherits its type's
trigger from the unsaved editor state (`resolveTrueNASDiskTemperatureDefault`).
The TrueNAS disk drawer tones a current reading from the same per-type trigger
(`getDiskTemperatureThresholds` passed into `buildTrueNASDetailSections`), so it
no longer judges heat from a fixed 55C. Judges that still differ: the TrueNAS
storage table and drawer do not apply a TrueNAS-wide value or per-disk
override.
`TestTrueNASDiskTemperatureAlertsFollowDiskTemperaturePolicy` and
`TestTrueNASDiskTemperatureAlertIgnoresRetainedReading` in
`internal/alerts/unified_eval_test.go` pin the tiers and retained readings;
`TestTrueNASDiskTemperatureAlertsReevaluatePerDiskType` and
`TestTrueNASDiskTemperatureAlertWithoutDiskTypeHeldAtLowestTrigger` in
`internal/alerts/alerts_test.go` pin re-judging on a config save;
`TestNormalizeTrueNASDiskTemperatureKeepsOnlyChosenValues` pins the
saved-config migration.
Proxmox physical disks alert on temperature under the same policy.
`CheckProxmoxDiskTemperature` (`internal/alerts/proxmox_disk_temperature.go`)
raises a `diskTemperature` metric alert under the disk's PVE alert reference
(`ProxmoxPhysicalDiskAlertResourceID`, resource type `proxmox-disk`), judged by
`proxmoxDiskTemperatureThresholdNoLock`: the disk type's `DiskTempByType` entry
under the agent Disk Temp default, which switches it off for every type. It
carries the disk's serial, WWN and `diskType` like the health and wearout
alerts, so `ProxmoxPhysicalDiskAlertIdentifiers` lists its
`<reference>::metric-threshold:diskTemperature` identifier for history
ownership, and `reevaluateActiveAlertsLocked` re-judges it per disk type (the
lowest per-type trigger when the type was never recorded) instead of falling
through to guest thresholds that have no disk temperature. A disk a
still-reporting linked agent lists in its SMART report is the agent's: its
alert closes with the `moved_to_agent` resolution and `CheckHost` raises the
agent's own. The handover happens on the next disk poll, so for that one
interval an agent newly linked to a disk with an open PVE alert can hold its
own alert beside it. A lapsed agent owns nothing, so the node's own readings
keep alerting while its agent is silent. An
excluded disk, or a policy switched off, closes the alert; a poll without a
current reading holds it. Like the health and wearout alerts, a disk that
leaves the inventory keeps its alert until the 24-hour stale sweep. The
`proxmox-disk` alert delay defaults to the factory 5 seconds, so a warning
waits out the same 300-second noisy-gauge stability window agent and TrueNAS
disk temperature warnings do, and a critical reading bypasses it.
`TestProxmoxDiskTemperatureAlertsFollowDiskTemperaturePolicy`,
`TestProxmoxDiskTemperatureAlertOwnershipAndHolds`,
`TestProxmoxDiskTemperatureWarningWaitsOutStabilityWindow`,
`TestProxmoxDiskTemperatureAlertsReevaluatePerDiskType` and
`TestCheckDiskHealthAlertsCarryHistoryOwnershipIdentity` in
`internal/alerts/alerts_test.go` pin the policy, ownership, holds, re-judging
and identifiers.
### Agent disk temperature alerts clear when their disk leaves the report
`CheckHost` also clears a disk temperature alert, and drops its pending
@@ -3180,6 +3309,76 @@ ownership is `internal/alerts/alert_identity_migration_test.go`,
`internal/monitoring/monitor_alert_override_migration_test.go`, and
`frontend-modern/src/features/alerts/__tests__/alertsConfigurationModel.snapshot.test.ts`.
### Alert config snapshots are owned copies
`Manager.GetConfig()` returns `AlertConfig.Clone()`, a typed deep copy that
shares no map, slice or pointer with the live config (custom rule filter
values are copied as decoded JSON values). Callers encode, persist
and edit that snapshot outside `m.mu`: the GET config handler, alert
activation, the identity migration, backup evaluation and diagnostics. It used
to be a shallow copy, so `SaveAlertConfig` normalizing it in place, or
`UpdateConfig` re-normalizing the overrides of a re-applied snapshot, wrote the
live maps while evaluation or another encoder read them, and Go aborted Pulse
with a concurrent map write. `SaveAlertConfig` also clones before normalizing,
because a config just handed to `UpdateConfig` is the live config. Persistence
keeps an explicit `0` type-level delay as no delay on save and load, matching
`NormalizeTimeThresholds`. It used to store and load it as the 5-second factory
delay, which the noisy-gauge rule stretches to 300 seconds for memory and
temperature, and the shared map write was what made the running manager agree.
Pulse saves never wrote a `0`, so only a newly sent or hand-edited `0` changes
meaning. `internal/alerts/config/clone_test.go`
fills every field and fails when a new reference field is not copied.
Regression ownership is `internal/alerts/resolved_lock_discipline_test.go` and
`internal/config/persistence_alert_ownership_test.go`.
### Alert evaluation owns the thresholds it normalizes
Host, guest and Docker container evaluation copy what they read from
`m.config` while holding `m.mu`, and only then fill in defaults.
`CheckHost` and `CheckGuest` clone a per-disk
override's `Disk` threshold before `ensureHysteresisThreshold` sets a missing
`Clear` to the trigger minus 5. They used to normalize the live override's
pointer after unlocking, so a `GetConfig()` clone under the read lock raced
the write, and the saved override changed to whatever the last poll filled in.
The live override now keeps the clear it was saved with, and the config API
returns that value. Docker container evaluation resolves its thresholds from
cloned `DockerDefaults` plus the override under the same lock, and reads the
restart-loop and memory-limit settings under the lock. It used to point into
`m.config.DockerDefaults` with no lock while `UpdateConfig` replaced
`m.config`. The test-only `getThresholdForMetricFromConfig`, which normalized
in place, is gone. `internal/alerts/alerts_test.go`
`TestAlertEvaluationLeavesTheLiveConfigToTheLock` runs host, guest and Docker
evaluation against `GetConfig()` and `UpdateConfig(GetConfig())` under
`-race`.
### Config-save reevaluation judges a filesystem alert by its own threshold
`UpdateConfig` re-judges every active alert, and a filesystem usage alert's
threshold resolves through the same per-filesystem helper its evaluator uses.
`hostDiskUsageThresholdNoLock` resolves an agent filesystem
(`agent:<host>/disk:<label>`): its own override, then the `DiskFillByType`
threshold for a hardware type inferred from the device while the host's disk
alerting is on, then the host's disk threshold. `guestDiskUsageThresholdNoLock`
resolves a guest filesystem (`<guestID>-disk-<key>`): its `guest-disk:`
override, then the guest's disk threshold. `CheckHost` and `CheckGuest` call
them under the read lock, and `resolveHostAlertThresholdsNoLock` and
`resolveGuestAlertThresholdsNoLock` call them for reevaluation. Both return an
override or per-type threshold as an owned copy. A guest filesystem alert
resolves its guest's threshold, override and identity-based custom rules by the
canonical `instance:node:vmid` guest ID that `parseGuestAlertIdentity` reads
from its resource ID. Custom rules that filter on live metrics still see no
readings there, as for every reevaluated guest alert. Reevaluation used to
judge agent filesystems by the host threshold, and guest filesystems without
their `guest-disk:` override or the guest's own override, because the
filesystem's resource ID names no guest. Any settings save resolved an alert a
per-disk threshold of 70 raised at 80 under a default of 90, notifying
recovery, and the next poll raised it again as a new alert.
`internal/alerts/threshold_resolution_shared_test.go`
`TestConfigSaveJudgesFilesystemAlertsByTheirOwnThreshold` covers an agent
filesystem override, an NVMe fill threshold, a guest filesystem override and a
guest override: an unchanged save must keep the alert, and a save lifting the
trigger above the reading must resolve it.
### Versioned alert-intent policy
The alerts runtime owns one versioned alert-intent document and its durable
@@ -2090,6 +2090,20 @@ payload shape change when the portal presents compact client rows.
index; clients must treat the endpoint's `accepted:true` as inbox
receipt, not delivery. Browser clients must use the shared
`AIChatAPI.steerSession(sessionId, { prompt, clientMessageId })` helper.
Every Assistant session mutation sub-resource under
`/api/ai/sessions/{id}` (`/abort`, `/summarize`, `/fork`, `/undo`,
`/redo`, `/steer`) accepts only `POST`. Any other method that reaches the
handler returns `405` with `Allow: POST` through
`requireAssistantSessionMutationMethod` in `internal/api/ai_handler.go`,
before the session service is resolved (earlier layers still answer
first: `Router.ServeHTTP` returns `200` for `OPTIONS` preflight, and
auth, scope, CSRF, and demo-mode guards keep their own refusals). The
session router dispatches these sub-resources by path alone, and the
demo-mode read-only guard and the CSRF check both treat `GET` and `HEAD`
as reads, so the method contract belongs to the mutating handler rather
than to its callers. `GET /api/ai/sessions/{id}/messages` stays the read
path, and the unsupported `/diff`, `/revert`, and `/unrevert` handlers
keep answering `501` for every method that reaches them.
OpenCode-style file diff/revert session routes are deliberately not part
of Pulse's supported Assistant session contract: Pulse sessions do not own
local code-file edits, and infrastructure mutations must be reviewed
@@ -3192,7 +3206,7 @@ a new API state machine, queue contract, or verification-accounting field.
regressions keep the current findings empty state out of green all-clear copy
even when the active finding count is zero and the latest summary score is
otherwise healthy
and the Patrol Current work assessment behavior, so the same governed AI summary contract decides whether the workspace leads with verified health, issues detected, coverage incomplete, or another attention state instead of letting count-only page fragments emit a stale `No issues found` conclusion
and the Patrol Current work empty-state behavior, so once runtime state (running, blocked, disabled, unavailable) and stale coverage have taken precedence, the governed AI summary's overall health, together with run history, decides whether the current-findings empty state reads `No current issues`, `Check needed`, or `Patrol needs review` instead of letting count-only page fragments emit a stale `No issues found` conclusion
and the Patrol Current work copy boundary, so the API-owned health,
finding, run, and control facts remain semantic inputs while browser-visible
empty and descriptive text stays operator-facing: what will appear there,
@@ -3250,8 +3264,8 @@ a new API state machine, queue contract, or verification-accounting field.
without adding a new API payload or page-level proof surface; those process details
remain available in expanded context, selected run records, Assistant
handoff context, or API diagnostics when they are actually needed
and the Patrol verification summary derived from run history, so the page also states whether recent Patrol evidence came from a successful full patrol or only from scoped/erroring runs instead of leaving verification scope implicit
and the same-day activity-mix explanation derived from that governed run history, so when a recent full patrol is followed by alert-triggered or anomaly-triggered scoped work the verification surface can explain the mix directly instead of reconstructing it from page-local timing heuristics
and the Patrol verification posture derived from run history (`getPatrolVerificationPresentation`), which today only withholds the current-findings empty state's all-clear when the latest completed full patrol ended with errors or only targeted or follow-up runs completed; the page shows no aggregate verification-posture summary (expanded run-history entries still show each run's own scope and coverage), and any surface that adds one must take it from that run-history posture rather than page-local heuristics
and the same-day activity-mix label that posture computes from the same governed run history, which no surface renders today; one that does must explain a recent full patrol followed by alert-triggered or anomaly-triggered scoped work from that label instead of reconstructing the mix from page-local timing heuristics
and the Patrol status recency split, so `last_patrol_at` remains reserved for completed full Patrol sweeps while scoped runs and verification checks advance `last_activity_at` without claiming a fresh full-estate verification pass
and the monitor-context Patrol coverage posture boundary, so Proxmox
overview and other monitor-first launch pages must not consume API-owned
@@ -3304,7 +3318,7 @@ a new API state machine, queue contract, or verification-accounting field.
and the `/api/ai/intelligence/changes` route plus `internal/api/contract_test.go`, so the canonical recent-changes endpoint stays on the same intelligence facade and contract snapshot instead of bypassing the shared timeline source
and the canonical policy-posture snapshot derived from unified resources, so sensitivity, routing, and redaction counts stay owned by the same AI summary contract instead of being reconstructed as a page-local governance rollup
and the resource-intelligence payload carried by the drawer AI card, so the resource-detail surface stays on one canonical intelligence contract instead of introducing a separate detail endpoint
and the learned-correlation payload loaded into the shared AI intelligence store, so the Patrol intelligence page and the AI summary page consume the same governed correlation slice instead of each page fetching its own copy
and the global learned-correlation route `/api/ai/intelligence/correlations`, which has no frontend client today: no page renders that list, so the shared AI intelligence store neither loads nor holds it and the resource drawer reads per-resource correlations from the resource-intelligence payload; a surface that later renders the global list must add one store-owned load instead of a page-local fetch
and the shared dashboard-load bundle inside `frontend-modern/src/stores/aiIntelligence.ts`, so the page orchestration stays on the store-owned bundle instead of enumerating the AI fetches inline
and the Patrol page refresh lifecycle in `frontend-modern/src/features/patrol/usePatrolIntelligenceState.ts`, so slow or stalled secondary reads from that shared dashboard-load bundle may continue resolving in the background while the operator-facing Patrol refresh control remains generation-aware, timeout-bounded, and reusable once Patrol findings and status are already visible
and the Patrol header support drawer in `frontend-modern/src/features/patrol/PatrolIntelligenceHeader.tsx`, so API-owned Patrol status and trigger facts can feed a secondary Schedule & model surface without turning provider model, schedule, trigger tuning, or background-only runtime-policy pauses into the primary Patrol control decision
@@ -3325,6 +3339,7 @@ a new API state machine, queue contract, or verification-accounting field.
action artifact so the Assistant runtime can refresh finding and action posture
from IDs and safe summaries instead of relying on pasted chat text
and the dedicated `frontend-modern/src/stores/aiIntelligenceSummaryModel.ts` owner, so recent-change counts and governed policy-posture fallbacks normalize once at the shared store boundary instead of as Patrol-hook-local payload repair
and the Patrol findings list the state hook reads from `aiIntelligenceStore.patrolFindings`, so `usePatrolIntelligenceState.ts` does not re-derive page-level severity tallies from it: the hook's unread `summaryStats` tally has been removed, the hook derives no client-side severity tally from that list today (the backend's per-severity `findings_count` reaches it only inside the AI summary payload it passes through), and the Activity tab's `This week` card takes its open-by-severity counts from `GET /api/ai/patrol/digest` instead of recounting that list
and the shared `frontend-modern/src/components/Infrastructure/ResourceCorrelationSummary.tsx` card, so learned correlations and correlation context stay rendered through one governed frontend card instead of separate page-local list loops
and the same shared correlation card's ordering and truncation rule, so callers pass raw correlations instead of encoding their own top-N sort behavior
and the shared `frontend-modern/src/components/Infrastructure/ResourceChangeSummary.tsx` and `frontend-modern/src/components/Infrastructure/ResourceCorrelationSummary.tsx` cards' optional `buildResourceHref` input, so a surface that needs resource-filter links passes them into the shared card instead of rebuilding infrastructure URLs inline; the cards carry no link default since the cross-resource drilldown links were retired, and render resource labels as plain text
@@ -3458,9 +3473,9 @@ a new API state machine, queue contract, or verification-accounting field.
17. Keep Patrol intelligence summary transport semantics single-voiced: the canonical overall-health payload and Patrol run-history payload together must support one primary assessment plus one explicit verification explanation, and frontend consumers must not need to derive a second compact assessment or verification verdict row from the same payloads beneath the primary assessment strip.
That same transport split supports Patrol assessment and action metadata
for any consumer without adding another API field (the Patrol page renders
no assessment strip today): the frontend summary
contract derives compact state from existing overall-health, run-history,
active finding, runtime, and pending-approval facts, so the API remains the
no assessment strip today): a consumer that needs compact state derives
it from existing overall-health, run-history, active finding, runtime, and
pending-approval facts (no shared frontend presenter does so today), so the API remains the
source of facts while the configured LLM owns next-step reasoning. Those
action references map back to existing API-backed Patrol controls and
approval/finding filters; summary transport must not become a new execution
@@ -4172,17 +4187,19 @@ the authoritative analysis outcome.
plumbing.
The same page-header recency line also reads
`PatrolRecencyPresentation.resourcesCheckedLabel` — derived by
`getPatrolRecencyPresentation` from
`PatrolRunRecord.resources_checked` and the latest completed run outcome —
`getPatrolRecencyPresentation` from the latest completed run's
`PatrolRunRecord.resources_checked` —
so the operator sees coverage alongside recency without needing a
new API surface. The label stays optional when coverage is zero, says
verified only for successful full patrols, and uses neutral checked wording
for errored full patrols or scoped activity.
The Patrol Current work empty-state copy uses that same run-history-backed
coverage truth when reconciling AI summary coverage factors: a successful
full run with non-zero `resources_checked` suppresses stale
coverage-incomplete wording, while scoped, missing, zero-coverage, or errored
runs still allow the coverage caveat to surface.
new API surface. The label stays optional when coverage is zero and reads
`checked N resources` for any completed run, whatever its type or outcome;
it never says verified.
The Patrol current-findings empty state does not reconcile AI summary
coverage factors against run history: outside runtime and stale-coverage
states, any `coverage` factor in `overall_health` withholds the all-clear,
even after a successful full run
with non-zero `resources_checked`, and run history adds its own caveat when
the latest completed full run ended with errors or only targeted or
follow-up runs completed.
and the Assistant finding-context request contract, so `/api/ai/chat`
payloads carrying `finding_id` may hydrate a structured investigation
summary from the unified finding, but raw action commands must stay
@@ -4308,10 +4325,10 @@ the authoritative analysis outcome.
control level and paid Patrol settings currently allowed by runtime
entitlements.
11. Treat Patrol summary supporting metrics as readouts, not reinterpretations: when frontend consumers derive cards such as active findings, criticals, warnings, or fixes from the canonical payloads, those cards must stay numeric and must not synthesize new assessment labels like `Issues detected` or verification labels like `Partial verification` beneath the primary summary contract
12. Treat active Patrol runtime transport as compatible with factual activity surfaces: when the runtime is currently running, frontend consumers may surface in-progress activity context, but they must not replace the activity strip with a second assessment verdict derived from runtime state alone
13. Treat Patrol recency as a singular transport-driven fact: once header metadata, verification copy, or the findings footer already present the governed Patrol timing context, frontend summary consumers must not derive an extra timing pill from the same payloads inside any primary summary card
12. Treat active Patrol runtime transport as compatible with factual activity surfaces: when the runtime is currently running, frontend consumers may surface in-progress activity context, but they must not add a second assessment verdict derived from runtime state alone
13. Treat Patrol recency as a singular transport-driven fact: the page-header recency line presents the governed Patrol timing context, so frontend summary consumers must not derive an extra timing pill from the same payloads inside any primary summary card that returns
14. Treat Patrol findings counts as a singular supporting surface as well: when the summary shell already exposes count cards for active findings, warnings, criticals, and fixes, the primary assessment card must not repeat those same payload-derived counts as secondary badges
15. Treat Patrol schedule and recency as header-owned metadata on the main Patrol page: findings empty-state consumers should not receive or restate `next_patrol_at`, `last_patrol_at`, `last_activity_at`, or interval timing once those transport fields are already presented by the primary header and verification shell
15. Treat Patrol schedule and recency as header-owned metadata on the main Patrol page: findings empty-state consumers should not receive or restate `next_patrol_at`, `last_patrol_at`, `last_activity_at`, or interval timing once those transport fields are already presented by the page header
16. Keep recovery payload filters canonical across `/api/recovery/rollups`, `/api/recovery/points`, `/api/recovery/series`, and `/api/recovery/facets`: when `internal/api/recovery_handlers.go` adds a governed recovery filter or display field such as provider-neutral `itemType`, the same normalized transport must land across all four endpoints and the contract tests must pin both outbound payload shape and accepted query aliases in the same slice
17. Keep recovery platform-query vocabulary canonical across that same `/api/recovery/*` surface: operator-facing transport must emit `platform` as the canonical query field, accepted legacy `provider` aliases must remain compatibility-only input, and `internal/api/contract_test.go` must pin that fallback behavior in the same slice as any handler change
18. Keep recovery payload platform vocabulary canonical across that same `/api/recovery/*` surface: point payloads must expose `platform`, rollup payloads must expose `platforms`, and any compatibility `provider` / `providers` aliases must remain secondary fallback fields rather than replacing the shared response model
@@ -4846,6 +4863,10 @@ transition and evidence times) from the frontend-primitives shared
relative-time clock. That is presentation over the same `GET
/api/ai/patrol/status`, run history and `GET /api/ai/patrol/attention/{id}`
payloads: no field, request or polling cadence changes.
The attention detail's suppression `expiresAt` and a finding's `remind_at` and
`snoozed_until` from `GET /api/ai/patrol/findings` are future times, so the
page presents them as countdowns from the same clock ("in 1d") instead of
ages. That is presentation only: no field, request or polling cadence changes.
### Resource-list facets preserve scoped navigation evidence
@@ -7270,12 +7291,14 @@ introducing VMware-only mention payloads or provider-local inventory reads
under `/api/vmware/*`. Runtime-specific container/app mentions remain shared
unified-resource mentions as well; VMware network inventory does not create a
provider-local mention family.
That same `/api/ai/chat` payload boundary owns per-request execution-mode
overrides. Dashboard Pulse Brief and other scoped handoffs may include
`autonomous_mode:false` on the chat request to force approval-required command
execution for that exchange, but the transport must treat the field as a
request override only and must not mutate the user's persistent AI control
setting.
That same `/api/ai/chat` payload boundary owns request-local execution mode,
and the server sets it. `ChatRequest` in `internal/api/ai_handler.go` has no
`autonomous_mode` field, so the `autonomous_mode:false` that scoped handoffs
such as an explain-this-issue request still send is dropped at decode, and the
handler passes approval-required mode into the chat service for every
exchange; an Autonomous control level runs as Controlled for that request.
That clamp is request-local and must not mutate the user's persistent AI
control setting.
That same chat transport boundary owns new-session anchoring. When the request
omits `session_id`, the handler may generate and stream a session ID
immediately so the browser can anchor the visible turn, but that generated ID is
@@ -7329,7 +7352,7 @@ not serialize the model-only `handoff_context`, runtime failure detail, action
preflight/result bodies, remediation descriptions, raw commands, or approval
command payloads, and it must not preserve Patrol-authored next-step
recommendation fields from legacy handoffs.
Patrol finding handoffs are stricter than ordinary chat requests: when a request
Patrol finding handoffs keep that clamp on their own path: when a request
carries a non-empty `finding_id` or resolves to model-only Patrol briefing,
resource, or action context, `internal/api/ai_handler.go` must clamp the
request-local autonomous mode to false even if the caller supplied
@@ -7449,11 +7472,13 @@ The same AI resource-intelligence payload now also carries dependency and
dependent correlation arrays plus correlation evidence, so the drawer can render
canonical correlation context from the shared AI contract instead of inferring it
from the relationship facet payload alone.
The same AI frontend client now also loads `/api/ai/intelligence/correlations`
through the shared `frontend-modern/src/stores/aiIntelligence.ts` store for
the Patrol intelligence page and the AI summary page, so the
learned-correlation list is governed by the same API contract that backs the
resource drawer's correlation evidence instead of being fetched as page-local state.
The frontend does not load `/api/ai/intelligence/correlations`. No page renders
the global learned-correlation list, so `frontend-modern/src/api/ai.ts` carries
no client for it, and neither the dashboard load bundle in
`frontend-modern/src/stores/aiIntelligence.ts` nor the Patrol refresh path
fetches it. The resource drawer's correlation evidence comes from the
resource-intelligence payload above. The route stays an API surface behind the
`ai:execute` scope.
That correlations route now reads through the canonical AI intelligence
facade first, so the handler and its payload keep the detector behind one
shared access layer instead of routing directly to Patrol-local correlation
@@ -7784,6 +7809,23 @@ read-only demo account. That same hidden read-side boundary includes `GET` and
`HEAD` reads for `/api/admin/users` and manual discovery at `/api/discover`;
public demo mode may block writes generically, but it must not reveal that
admin-user inventory or manual-discovery read routes exist.
The same policy hides the Go runtime profiling family for every method: on a
`DEMO_MODE` instance the demo guard answers `/debug/pprof`, `/debug/pprof/`
and every path below it with `404`, ahead of route authentication.
`registerDebugRoutes` gates those handlers on `RequireAdmin` plus a
`settings:read` scope that session logins skip, so a demo that signs visitors
in as the configured admin would otherwise hand every visitor goroutine and
heap dumps, the process command line, and CPU profiles, execution traces and
forced garbage collection that cost the whole process. `PULSE_PPROF_DISABLED`
still removes the routes on any instance.
`TestPublicDemoAdminOperationsPolicyHidesEveryDebugRoute` fails when a literal
route the parsed router files register under `/debug/`, with or without a
method prefix, escapes that policy for any common method.
`TestContract_DemoModeHidesPprofFromDemoAdminSession` in
`internal/api/contract_test.go` signs in as that admin through the full
router, proves the same session still reads `/debug/pprof/cmdline` below the
demo guard, and gets `404` through it for eight pprof paths with `GET`,
`HEAD`, `OPTIONS` and `POST`.
That generic write block keys on the request method alone. The demo
middleware admits `GET`, `HEAD` and `OPTIONS` plus the login, logout, AI
execute and OIDC login/callback allowlist; HTTP/1.1 websocket handshakes for
@@ -10424,6 +10466,22 @@ boundaries. Alert evaluation after that save remains alerts-owned and must use
the persisted trigger/recovery pair plus a valid derived critical threshold;
the API must not invent a TrueNAS-specific threshold sidecar.
`PUT /api/alerts/config` replaces only the top-level keys the body carries
and keeps the stored value of every key it leaves out, so a client that does
not know a setting cannot turn it off by leaving it out: the thresholds page
has no flapping-detection or alert-TTL controls, and an older browser bundle
or API script predates later fields. A sent key replaces its stored value
whole, so `"overrides": {}` removes every override rather than merging, and
the merged config then goes through the same normalization as any update (for
example, activation state is never cleared). The read, merge and apply run
under the alert manager's lock, so two concurrent partial saves cannot revert
each other in memory; the handler runs one save at a time and persists a
snapshot of the applied config, so successful saves through this endpoint
reach `alerts.json` in the order they were applied. A body that is not a JSON object, `null` included, is a 400 and changes
nothing. `TestUpdateAlertConfig_KeepsStoredValuesForUnsentKeys`
and `TestUpdateAlertConfig_RejectsNonObjectBody` in
`internal/api/alerting/alerts_test.go` prove the transport.
### Alert intent and UDP availability transport
`GET /api/alerts/intent-policies` and
@@ -294,6 +294,14 @@ expose only a raw reason. A same-VM fresh observation withdraws the notice and
updates values without remounting the row. Permission, disabled-agent and actual
unavailable cases retain their own explanations.
When a linked Pulse agent's filesystems fill in for a VM with no Proxmox guest
filesystems (`disksFromAgent`), the reading follows that agent instead of the
Proxmox reason: current while it reports, last known once it is stale, and
unavailable once the guest is stopped. `getWorkloadGuestDiskRead` in
`workloadGuestPresentation.ts` owns that rule for the workload row's disk cell,
the guest drawer's filesystem rows and its History, so the three cannot
disagree about one guest; `workloadGuestPresentation.test.ts` pins it.
Verification: `workloadGuestPresentation.test.ts`, the mounted `GuestRow`,
`StackedDiskBar` and `GuestDrawerOverview.filesystems` regressions,
and `browser-tests/guest-disk-deferral.cjs` cover the fixed reasons, retained/no
@@ -2116,6 +2124,18 @@ the same record; the table renders defaults once, in both desktop table and
narrow card layouts, and per-section metric columns must resolve to metric
keys the shared column normalizer produces so the defaults editor reads and
writes the same record keys the section persists.
A global default that another setting decides while it is unset passes a
`globalDefaultFallbacks` entry for that metric (label, title, whether the other
setting is itself off, and the value switching on stages when it is). The
TrueNAS Disks temperature uses it to read `By type`, following Disk
temperature by type, with `features/alerts/thresholds/trueNASDiskTemperature.ts`
giving each disk row its type's trigger from the unsaved editor state.
`resolveAlertResourceGlobalDefaultCell` decides the cell for both layouts:
unset follows the fallback, switching on from Off returns to unset unless the
fallback is off, and an unset default with no fallback stays Off as the engine
reads it. `ResourceTable.tsx` hands its props to the shared table state through
getters, so the Custom badge and reset control follow defaults that load after
the table mounts.
Platform sub-routes that add native provider inventory must stay on the shared
platform page and table primitives. The vSphere Networks surface routes through
`/vmware/networks`, the shared platform tab model, the command palette
@@ -4498,12 +4518,12 @@ The recency line beside the header actions also renders coverage
alongside time when the canonical `getPatrolRecencyPresentation` helper
returns `resourcesCheckedLabel` from the latest completed run. Render code
must gate on `<Show when={recency().resourcesCheckedLabel}>` (truthy) so
zero-coverage runs do not surface a misleading coverage phrase, failed or
scoped runs use neutral checked wording, and only successful full patrols read
as verified. Any Patrol assessment shell must pass the same run-history facts
into `getPatrolAssessmentPresentation` so assessment coverage caveats do not
contradict the header's verified full-run coverage state; the page renders no
assessment shell today.
zero-coverage runs do not surface a misleading coverage phrase. The label
reads `checked N resources` for every completed run with a positive count
(full, targeted, follow-up, or errored) and never says `verified`. The page
renders no assessment shell today; if one returns, its coverage caveats must
come from the same run-history facts so they do not contradict the coverage
the header line shows.
The same header row may surface `Trigger status` when
`getPatrolTriggerStatusSummary` returns a runtime-relevant value from the
Patrol status payload. That text is page-owned operational metadata inside the
@@ -4900,6 +4920,16 @@ or not fully verified.
The same empty-state helper must consume Patrol trust-history evidence so a
historical regression reads as history review context, not as a current issue
and not as a healthy all-clear.
`frontend-modern/scripts/canonical-platform-audit.mjs` carries no Patrol
summary-card rule. Its `canonical-patrol/no-local-summary-card-presentation`
regex matched only text shaped like the deleted summary card's tint ternaries
on `summaryStats().criticalFindings`, `warningFindings` and `fixedCount`. The
Patrol state hook's `summaryStats` accessor has had no reader since that card
was deleted, so the accessor and the rule are both gone.
`frontend-modern/src/utils/patrolSummaryPresentation.ts` is off the audit
allowlist too: it now holds only the recency and verification presenters,
which no audit rule needs to exempt, so the audit scans it like any other
module.
The Patrol page renders no summary shell today. If one returns, the same
hierarchy applies inside it: once its primary assessment strip states Patrol's
current risk and verification basis,
@@ -4915,10 +4945,10 @@ That same summary shell should also keep the shared Pulse surface neutral:
severity belongs in compact accents, inline readouts, and badges rather than
turning the whole assessment into a tinted warning banner, nested card, or
hero-style block that breaks the surrounding operator workflow.
That same summary-shell rule also applies to timing metadata: if the header,
verification card, or findings footer already presents the governed Patrol
activity timestamp, the summary chip row must not add another recency badge
that competes with those owned timing surfaces.
That same summary-shell rule also applies to timing metadata: the header
recency line already presents the governed Patrol activity timestamp, so a
returning summary chip row must not add another recency badge that competes
with it.
The same default-readout rule applies to collapsed Patrol issue rows:
`MetadataBadge` may carry severity, recurrence, and active decision/work states,
but the default Patrol page must not render raw lifecycle or investigation
@@ -5118,10 +5148,10 @@ KPI, problem-resource, or card shells. Workload-table and guest-row fallback
copy that lives under `frontend-modern/src/components/Workloads/` must keep
using `frontend-modern/src/utils/workloadEmptyStatePresentation.ts` and
`frontend-modern/src/utils/workloadGuestPresentation.ts`. New route-level empty
states, tone mapping, or compact issue copy must extend the shared
`emptyStatePresentation`, `semanticTonePresentation`, and
`problemResourcePresentation` helpers instead of reviving deleted
dashboard-only KPI, metric, storage, recovery, or trend presentation helpers.
states or tone mapping must extend the shared `emptyStatePresentation` and
`semanticTonePresentation` helpers instead of reviving deleted dashboard-only
KPI, metric, storage, recovery, problem-resource, or trend presentation
helpers.
That shell must also stay passive with respect to data ownership: future
overview trend cards may render summary-range controls and operator-facing
empty or error copy only after they have a governed owner, and they must not
@@ -5245,7 +5275,19 @@ too. Every read of the clock returns the wall clock; the 30-second tick only
tells readers to re-read, so a cell that mounts between ticks never measures
from a stale time and a timestamp from the last few seconds never reads as a
future time.
The rule has one deliberate exception. The age of a latest reading (last used,
Future times have their own formatter. `formatRelativeTime` is past-only: it
reads a time ahead of `now` as "just now" (compact) or "0s ago", which is right
for clock skew on something already observed and wrong for an expiry, a
reminder or a schedule.
Those go through `formatTimeUntil` (`frontend-modern/src/utils/format.ts`),
which counts down ("in 3h", "in 1d"), rounds to the nearest unit so a duration
just chosen reads as chosen, and returns `dueText` (default "now") once the
time arrives. A countdown target does not change while its surface stays open,
so the countdown passes `now` from the shared clock, as the Patrol suppression
expiry and the Patrol findings reminder and snooze lines do. The replication
Next sync column and the Patrol header's next-check `CountdownTimer` keep their
own minute- and second-precision countdowns.
The relative-age rule has one deliberate exception. The age of a latest reading (last used,
last seen, last success, last checked) on data the surface reads once and does
not re-read stays the age at read time, because a moving age over a snapshot
that never refreshes claims the reading stopped when it may not have. Such a
@@ -5268,8 +5310,8 @@ than hardcoded gray palettes; non-gray typed tones may retain their state color
vocabulary so success, warning, danger, info, and platform-adjacent metadata do
not collapse into visually identical chips.
Patrol run-history labels follow this state-badge boundary:
Patrol may derive the status label and typed variant in
`patrolRunPresentation.ts` or `patrolSummaryPresentation.ts`, but
Patrol derives the status label and typed variant in
`patrolRunPresentation.ts`, but
`RunHistoryEntry.tsx` must render visible state badges through
`StatusIndicatorBadge` rather than `runStatus.badgeClass` or a local span.
The shared segmented selector now follows that same owner split.
@@ -5760,7 +5802,15 @@ the surface-alt detail row shell locally. The content shell must clip
horizontal paint below the large breakpoint without becoming a scroll
container, reset the parent table's `whitespace-nowrap` inheritance, and allow
its descendants to shrink, then restore visible overflow for the static
desktop layout. Long operator-state copy must wrap inside the shared row border
desktop layout. Below that breakpoint the default content shell is capped at
the table scroll shell's content-box inline size (`max-w-[100cqi]`; the shell
is a size container), never a viewport estimate of the page chrome, so a phone
drawer does not leave an empty strip beside its content (the retired
`100vw-3.5rem` cap left 27px at 390px) and is never wider than the table's
visible area. Callers that pass their own `contentClass` own their width.
Fixed-layout cells clip overflow, so the sticky offset is inert and horizontal
scrolling still moves the drawer with its cell.
Long operator-state copy must wrap inside the shared row border
instead of painting beneath adjacent controls or disappearing at the clip edge.
When focused detail content is removed, `InlineDetailTableRow` restores focus
to its current `aria-controls` disclosure with `preventScroll`; live refresh,
@@ -6400,20 +6450,6 @@ rollups, points, or facets payloads arrive, it must keep that selected
platform present in the option set so the shared `LabeledFilterSelect` shows
the owned value immediately instead of flashing back to `All Platforms` until
recovery data warms.
`frontend-modern/src/utils/problemResourcePresentation.ts` now also belongs to
that same dashboard overview boundary so the problem-resource severity contract
stays shared with `ProblemResourcesTable.tsx` instead of floating as an
unowned helper.
Problem-resource table readability belongs to that same owner. Repeated rows
may collapse only when they share the same governed display label, resource
type, and problem signal; the header count and Pulse Brief counts must continue
to represent the underlying affected resources, and grouped links must route to
the broad owning surface rather than inventing a synthetic resource target.
Problem Resources and Pulse Brief wording must not amplify generic
status-shaped names such as `storage (offline)` into first-viewport prose or
grouped-row sublabels; when the resource name is only a type plus status, the
surface should summarize the type-level issue in operator language instead of
repeating raw backend-shaped labels.
The retired dashboard action queue must not be reintroduced as a compact
Patrol or infrastructure issue panel. Patrol-owned runtime findings remain
governed by `frontend-modern/src/utils/aiFindingPresentation.ts` and their
@@ -6766,25 +6802,28 @@ same-day scoped follow-up work, that summary shell should also carry a compact
activity-mix explanation rather than forcing operators to infer why Patrol
looked busy from a second competing status band.
That explanation belongs on the verification surface itself when operators are
reconciling `Recently verified` copy against same-day scoped Patrol bursts; the
reconciling `Recently checked` copy against same-day scoped Patrol bursts; the
supporting activity context may complement the readout, but it is not
sufficient as the only explanation path.
That same shell rule also owns Patrol recency labels. Shared Patrol header and
status-shell surfaces must keep `Last full patrol` tied only to the full-sweep
transport fact and use `Last activity` for scoped or verification work instead
of collapsing both timestamps back into a generic `Last run` label. Coverage
phrases on those recency surfaces must come from the Patrol recency presenter
instead of hardcoding verified wording in the shell.
That same run-history ownership applies to assessment caveats: Patrol summary
shells should not present `Recent coverage is incomplete` when the shared
recency/verification helpers already prove a successful full patrol with
non-zero resource coverage.
Patrol recency labels and coverage phrases come from the Patrol recency
presenter (`getPatrolRecencyPresentation`), never from hardcoded shell copy.
The header labels the latest completed run of any type `Last check`; without
run history it uses `Last check` for the `last_patrol_at` full-sweep transport
fact and `Last activity` when `last_activity_at` is newer, and it never
collapses them into a generic `Last run` label. Its coverage phrase reads
`checked N resources` and never says `verified`.
If a summary shell returns, it should not present `Recent coverage is
incomplete` when run history shows a successful full patrol with non-zero
resource coverage.
That same Patrol shell ownership includes refresh affordance state:
`frontend-modern/src/features/patrol/usePatrolIntelligenceState.ts` must keep
operator refresh controls generation-aware, timeout-bounded, and separate from
background polling state, so a slow supporting intelligence read cannot make the
shared Patrol header Refresh Patrol action spin indefinitely or stay disabled
while Patrol findings and status remain visible.
Neither the Patrol load and poll path (the store's `loadDashboardData` bundle)
nor the Retry path's background supporting reads fetch the global
learned-correlation list, which no Patrol surface shows.
That same Patrol shell should make scoped trigger policy legible without
another navigation step. `frontend-modern/src/features/patrol/PatrolIntelligenceHeader.tsx`
should keep actionable scoped-trigger state legible without promoting
@@ -1825,6 +1825,29 @@ boundary around it:
matches node names only, so it relies on fixture node names being unique
across instances, which they are.
`checkPhysicalDiskAlerts` also hands each disk's temperature to
`CheckProxmoxDiskTemperature`, so a Proxmox disk that only the node's sensors
report alerts under the disk temperature policy, as its Running Hot verdict
already says it should. It passes the reading only when this poll collected it
(`collectedPhysicalDiskTemperature`, `diskinventory.TemperatureCollected`), so
a retained last-known value holds the alert rather than judging it, and it
passes `AgentSMARTReported`, which `mergeHostAgentSMARTIntoDisks` sets on a
disk a still-reporting linked agent lists in its SMART report (disks built
from that report when the Proxmox query fails go through the same merge). A
disk the agent lists is the agent's: `CheckHost` raises its temperature
alert, and the PVE disk check closes its own as moved to the agent, so no
disk alerts twice. Once the agent's lease lapses (`ExpireHostTelemetry` marks
the host offline) its retained rows still enrich the disk, but ownership
returns to the PVE check, which judges the node's own current readings; an
agent newly linked to a disk with an open PVE alert takes it over on the next
disk poll. An excluded device closes its temperature
alert with the health and wearout ones. Fixture disks carry no agent SMART
merge, so on a mock estate every hot disk on an online node alerts.
`TestMergeHostAgentSMARTIntoDisksMarksDisksTheAgentReports` and
`TestCheckPhysicalDiskAlertsRaisesProxmoxDiskTemperatureAlerts` in
`internal/monitoring/physical_disk_roundtrip_test.go` pin the marker,
the collected reading, agent ownership and exclusion.
Switching mock mode fences the alert evaluations that read mode-dependent data
(`mockModeFence`, `internal/monitoring/mock_mode_fence.go`). `GetState`, the
fixture graph, the unified read view, the recovery rollups and the connection
@@ -4491,6 +4514,13 @@ disk usage when the last VM snapshot is still recent guest-agent truth rather
than an already carried-forward fallback. That keeps transient guest-agent or
status-call failures from regressing a VM back to misleading allocated-disk
data while still avoiding indefinite replay of stale disk summaries.
The previous VM that `previousVMFromView` builds from the unified read state
carries disk usage only when it is Proxmox's own reading. When a manually
linked Pulse agent's disk filled in because Proxmox had no guest filesystems
(`VMView.DiskFromLinkedAgent`), carrying it would relabel the agent's value as
a Proxmox `prev-` read, and with Proxmox outranking the agent on guests that
copy would then freeze over the agent's live disk.
`TestPreviousVMFromViewKeepsLinkedAgentDiskOutOfProxmoxCarry` pins both owners.
That compatibility boundary also applies to historical snapshot labels that may
still exist in tests, live in-memory state, or pre-canonical diagnostic paths:
legacy aliases such as `rrd-available`, `rrd-data`, `node-status-available`,
@@ -827,11 +827,12 @@ clear`, `Found N new issues`, `Fixed N issues`, `N issues still open`, or
and raw tool traces are forensic context and must stay secondary to what
Patrol did.
2. Keep Patrol-specific copy and badge logic inside the governed Patrol presentation helpers instead of page-local branches
Patrol assessment copy must not present an all-clear health prediction while
active Patrol findings or Patrol runtime issues are still present. The
canonical summary helper owns that conflict resolution so the visible
assessment title, description, and compact metrics all speak from the same
current findings state. Patrol-owned runtime issues must stay
Patrol renders no assessment copy today. If it returns, it must not present
an all-clear health prediction while active Patrol findings or Patrol
runtime issues are still present, and one shared presenter must own that
conflict resolution so the visible assessment title, description, and
compact metrics all speak from the same current findings state.
Patrol-owned runtime issues must stay
distinct from infrastructure findings in assessment copy rather than being
described as infrastructure warning findings about Patrol itself.
Historical Patrol trust regressions must also suppress a green all-clear in
@@ -842,13 +843,24 @@ clear`, `Found N new issues`, `Fixed N issues`, `N issues still open`, or
tolerance and otherwise allows two configured Patrol intervals before
warning, so deliberately slower schedules are not mislabeled while an old
default-schedule result cannot remain green indefinitely.
Assessment coverage caveats must also reconcile against current run-history
proof: a stale coverage factor or prediction must not claim recent coverage
is incomplete when the latest completed full Patrol run successfully checked
real resources.
Recency coverage copy must also come from the shared presentation helper:
use verified wording only for successful full patrols, and use neutral
checked wording for failed full patrols or scoped activity.
No assessment readout renders today, so nothing reconciles a coverage
caveat against run history: once runtime states (running, blocked,
disabled, unavailable) and stale coverage have taken precedence, the
current-findings empty state withholds the all-clear (`Check needed`)
whenever the overall-health summary carries any
`coverage` factor, including the backend `Recent Patrol errors` factor that
can outlast a successful full Patrol run until three consecutive clean full
runs lead the coverage window or the erroring runs leave it
(`summarizeRecentPatrolCoverage` in `internal/ai/intelligence.go`), and
whenever the latest completed full run ended with errors or only targeted or
follow-up runs completed. If an assessment readout returns, its coverage
caveat must reconcile against current run-history proof: a stale coverage
factor or prediction must not claim recent coverage is incomplete when the
latest completed full Patrol run successfully checked real resources.
Recency coverage copy comes from the shared presentation helper
(`getPatrolRecencyPresentation`): `checked N resources` for the latest
completed run with a positive count, whatever its type or error state. It
never says `verified`.
3. Update this contract whenever a new Patrol-specific page, store, helper, or presentation component becomes canonical runtime surface area
4. Keep retired hosted-model and trial-like Patrol acquisition copy out of the
normal self-hosted GA app. Patrol may parse legacy transport fields, but
@@ -1496,15 +1508,29 @@ provider/settings affordances.
The feature surface now also keeps the same shell/runtime split internally:
`frontend-modern/src/features/patrol/PatrolIntelligenceSurface.tsx` owns feature
composition, the Patrol-owned section files under
`frontend-modern/src/features/patrol/` own the header, banner, summary, and
workspace render surfaces, and
`frontend-modern/src/features/patrol/usePatrolIntelligenceState.ts` owns Patrol
state, transport, polling, and effect lifecycle. The shell and section surfaces
must not re-accumulate Patrol API calls, timer orchestration, or store refresh
semantics, and `frontend-modern/src/stores/aiIntelligenceSummaryModel.ts` now
`frontend-modern/src/features/patrol/` own the header, banner, workspace, and
tab-panel render surfaces (the Inbox attention list, Protection objectives, and
the Activity tab's `This week` digest and recent work), and
`frontend-modern/src/features/patrol/usePatrolIntelligenceState.ts` owns the
page's shared Patrol state, transport, polling, and effect lifecycle. The shell
and the header, banner, and workspace surfaces must not re-accumulate those
Patrol API calls, timer orchestration, or store refresh semantics; the
attention list, objectives, recent work, and `This week` panels each own their
own typed read and refresh instead, and `frontend-modern/src/stores/aiIntelligenceSummaryModel.ts` now
owns the canonical summary normalization so Patrol consumers inherit one
governed recent-change and policy-posture snapshot instead of reintroducing
hook-local fallback logic.
The hook no longer returns the summary card's severity and fix tallies. Its
`summaryStats` accessor (critical, warning, and total active findings plus
fixed outcomes, counted client-side over the Patrol findings list) lost its
only reader when the Patrol summary card was deleted and has been removed,
together with the `canonical-patrol/no-local-summary-card-presentation` rule in
`frontend-modern/scripts/canonical-platform-audit.mjs`, whose regex matched only
text shaped like the deleted card's tint ternaries on
`summaryStats().criticalFindings`, `warningFindings` and `fixedCount`. The open-issue severity counts the page shows today come from
the Activity tab's `This week` card, which reads them from
`GET /api/ai/patrol/digest` (see that card's section below); a count surface
that returns must not revive a hook-local tally of the findings list.
That same Patrol hook boundary now consumes shared AI settings/model truth
through `frontend-modern/src/stores/aiRuntimeState.ts` instead of mounting its
own `/api/settings/ai` or `/api/ai/models` reads. Patrol-specific state still
@@ -1571,12 +1597,20 @@ compatible `handoff_metadata.kind=patrol_configuration_failure` plus only the
runtime-failure boolean needed for drawer/session presentation, so they restore
as a Patrol control issue without carrying raw provider, credential, command,
or retry payloads into the browser.
Successful provider-model saves that return
`patrol_readiness.status=not_ready` are still Patrol control issues, not silent
successes: the Patrol popover must keep the saved provider/model visible,
render a `Patrol control needs attention` inline state with the returned
readiness cause and summary; any Assistant handoff from that state must
describe a Patrol control issue rather than a save failure.
Successful Pulse Intelligence settings saves whose returned
`patrol_readiness.status` is `not_ready` or `warning` are still Patrol control
issues, not silent successes: Settings > Provider & Models, Patrol, Assistant
and Service context keep the saved non-secret values in the form and raise a
warning
toast (`<page> settings saved, but Patrol is not ready` or `... is degraded`)
with the readiness summary, provider and model instead of a success toast
(`getAISettingsPatrolReadinessSaveMessage` in
`frontend-modern/src/components/Settings/useAISettingsState.ts`). A Patrol
on/off toggle save whose response reports `not_ready` raises the fixed warning
`Patrol setting was saved, but Patrol is not ready to run.` in either
direction, because readiness is evaluated independently of `patrol_enabled`.
Patrol renders no inline control-issue state or Assistant handoff for either
case.
A separate browser proof in
`tests/integration/tests/78-monitor-first-patrol-workbench.spec.ts` keeps the
authenticated launch workbench monitor-first. When the runtime has monitored
@@ -1675,55 +1709,49 @@ must not show Pro trial CTAs, upgrade links, paid helper copy, or a plan-lock
upsell banner in the default Patrol workflow unless hosted mode, an explicit
commercial handoff, or an active entitlement makes those actions relevant.
That degraded empty-state copy must also interpret the finding state rather
than simply replaying the primary assessment sentence verbatim: when coverage
than replaying an `overall_health.prediction` sentence verbatim: when coverage
is incomplete, the findings panel should tell the operator to run Patrol to
complete coverage before trusting the all-clear. The page must not duplicate
the assessment prediction as a second independent status surface above current
issues.
Secondary metric strips must not render `No issues found` when the same
governed overall-health summary says coverage is incomplete or health still
requires attention; compact assessment derivation belongs to the shared
`frontend-modern/src/utils/patrolSummaryPresentation.ts` helper for Assistant,
history, and explicit context consumers rather than the default Patrol page.
Any explicit context assessment shell must also stay inside the shared Pulse
card language. It should use the same neutral `bg-surface` page-card base as
adjacent Pulse surfaces, carrying severity through compact header accents, icon
treatments, and badges instead of tinting an entire full-width assessment panel
as a one-off warning banner.
That supporting score chip must also avoid overstating infrastructure truth.
When the current state is dominated by incomplete coverage or Patrol-owned
runtime failures, the chip should read as an `Assessment` grade rather than an
`Health` grade; `Health` belongs to verified healthy infrastructure states.
That same helper also owns explicit assessment explanations. Assessment
consumers must not pair an `Issues detected` headline with a raw coverage-only
`overall_health.prediction` sentence from a separate source; when active
findings and incomplete verification are both true, the assessment should
describe both in one canonical message.
That same assessment contract must also distinguish Patrol-owned runtime
findings from infrastructure findings. When the only active Patrol findings are
synthetic Patrol service/runtime conditions such as the `ai-service`
provider-credit failure, the top assessment should read as a Patrol runtime
issue rather than implying infrastructure issues were detected across the
estate. When there is exactly one active Patrol runtime finding, that same
assessment copy should name the concrete runtime failure, such as
`Provider billing or quota issue`, instead of reducing the state to a generic count of
runtime findings.
That primary assessment must also expose a single visible recommended next
step derived from the same Patrol summary contract, not a page-local helper.
`frontend-modern/src/utils/patrolSummaryPresentation.ts` owns that decision
from pending governed approvals, Patrol runtime issues, active infrastructure
findings, verification posture, and the current assessment tone. Pending
approvals take priority over general triage, coverage-incomplete states must
ask for full verification before any all-clear claim, runtime impairment must
point the operator back to restoring Patrol visibility, and verified healthy
states should fall back to continued scheduled monitoring rather than
inviting generic Assistant chat.
When that recommendation has an immediate governed operator path, the same
helper must also declare the bounded action kind. The summary card may map
those action kinds only to existing Patrol controls: run a full Patrol,
review approval-scoped findings, review active findings, or open Patrol
provider settings. It must not
invent page-local remediation, approval, or execution authority from
No shared presenter derives a compact assessment, assessment shell, score
chip, assessment explanation, or recommended next step today. The helpers that
did (`getPatrolAssessmentPresentation`, `getPatrolAssessmentShellPresentation`,
`getPatrolScoreChipLabel`, `getPatrolCompactAssessmentLabel`,
`getPatrolSummaryMetricState`, `getPatrolAssessmentAction` and their siblings)
have had no production caller since the Patrol summary card was deleted
(several lost theirs months earlier) and are gone;
`frontend-modern/src/utils/patrolSummaryPresentation.ts` now owns only the
header recency line (`getPatrolRecencyPresentation`) and the run-history
verification posture the current-findings empty state reads
(`getPatrolVerificationPresentation`). If a summary card, metric strip, or
assessment readout returns, it must derive its state from one shared
Patrol-owned presenter rather than page-local logic, and keep these rules.
A secondary metric strip must not render `No issues found` while the governed
overall-health summary says coverage is incomplete or health still requires
attention. An assessment shell stays inside the shared Pulse card language:
the neutral `bg-surface` page-card base, with severity carried by compact
header accents, icon treatments, and badges rather than a tinted full-width
warning banner. A supporting score chip reads as an `Assessment` grade rather
than a `Health` grade when incomplete coverage or Patrol-owned runtime failures
dominate; `Health` belongs to verified healthy infrastructure states. An
`Issues detected` headline is never paired with a raw coverage-only
`overall_health.prediction` sentence from a separate source; active findings
and incomplete verification are described together in one message. When the
only active Patrol findings are synthetic Patrol service/runtime conditions
such as the `ai-service` provider-credit failure, the readout names a Patrol
runtime issue rather than implying infrastructure issues across the estate,
and with exactly one such finding it names the concrete failure, such as
`Provider billing or quota issue`. Any recommended next step comes from that
same presenter: pending approvals take priority over general triage,
coverage-incomplete states ask for a full check before any all-clear, runtime
impairment points the operator back to restoring Patrol visibility, and
healthy states fall back to continued scheduled monitoring rather than
inviting generic Assistant chat. When that recommendation has an immediate
governed operator path, the presenter declares a bounded action kind that maps
only to existing Patrol controls (run a full Patrol, review approval-scoped
findings, review active findings, or open Patrol provider settings), never to
page-local remediation, approval, or execution authority invented from
recommendation text.
If a summary card returns, that same runtime-owned assessment must expose the
fix path directly. When the primary Patrol issue is a Patrol runtime/provider
@@ -1801,8 +1829,8 @@ dismiss, resolve, or suppress controls for them. The correct operator path is
to fix Patrol provider configuration in Pulse Intelligence > Provider & Models
settings and rerun Patrol, optionally adding context notes, rather than hiding
the runtime issue.
That same runtime-versus-infrastructure split must carry through the summary
metrics strip as well. When Patrol-owned runtime issues are active, the
If a summary metrics strip returns, that same runtime-versus-infrastructure
split must carry through it. When Patrol-owned runtime issues are active, the
supporting metrics must stop counting them under generic infrastructure
`Warnings` or `Active findings`; the strip should break out `Runtime issues`
separately and reserve infrastructure finding counts for actual estate issues.
@@ -1810,16 +1838,18 @@ The findings list must respect that same trust priority. When Patrol-owned
runtime issues share a severity tier with ordinary infrastructure findings, the
runtime issue should sort first within that tier so Patrol blindness is not
buried under same-severity estate warnings.
The summary recency chip must follow the same governed scope distinction. When
the latest completed activity was only a scoped run, the summary should label
that timestamp as `Last activity` instead of `Last patrol`; `Last full patrol`
belongs only to the most recent completed full Patrol run.
That same distinction is transport-backed. `last_patrol_at` names the last
completed full Patrol sweep, while `last_activity_at` may advance on scoped
work or fix-verification checks without claiming a new full verification pass.
That same recency contract also applies to the header metadata row. The top
header must not revert to a generic `Last:` timestamp when the rest of Patrol
is explicitly distinguishing activity from full verification recency.
Patrol renders no summary recency chip today; the page-header recency line is
the only Patrol recency surface, and `getPatrolRecencyPresentation` labels it.
When run history holds a completed run, the line reads `Last check` for the
most recent one of any type: full, targeted, or follow-up. Otherwise it falls
back to the status transport: `Last check` with `last_patrol_at` when that is
the only timestamp, when it is at least as recent as `last_activity_at`, or when
only the activity timestamp fails to parse; otherwise `Last activity` with
`last_activity_at`. The transport keeps the
distinction: `last_patrol_at` names the last completed full Patrol sweep,
while `last_activity_at` may advance on scoped work or fix-verification checks
without claiming a new full verification pass. The header must not revert to
a generic `Last:` timestamp.
If Patrol renders a summary surface again, it must also avoid reintroducing a
second compact assessment or verification layer beneath the primary card. Supporting metric strips
belong to counts and outcomes such as active findings, critical findings,
@@ -1828,8 +1858,8 @@ verification labels in a second row that competes with the primary governed
assessment and verification copy above.
The same supporting-chip rule applies to timing: such a primary summary card
may show health and active-finding support, but it should not add another recency
pill once header metadata, verification, and findings footer already carry the
governed activity/verification timestamps.
pill, because the header recency line already carries the governed activity
timestamp.
That same Patrol-owned run-history surface must keep platform-backed system
counts canonical too. When the backend distinguishes API-backed TrueNAS
systems from unified-agent hosts in Patrol run history, the run-history chips
@@ -1844,16 +1874,20 @@ split also applies to findings counts: the primary assessment card may keep
health as supporting context, but active
findings, warning counts, and critical counts belong to the supporting metric
strip rather than being repeated as duplicate badges inside the primary card.
That same summary surface must also explain what Patrol actually checked
without turning coverage mechanics into the default product language. Recent
run history should drive a visible check summary that tells the operator
whether Patrol recently completed a clean broad check, only ran targeted alert
or anomaly-triggered checks, or ended its most recent broad check with errors,
so the page does not leave trust and coverage as implicit background knowledge.
When same-day run history shows both a recent broad check and a burst of
targeted or follow-up activity, that same surface should expose the recent
Patrol renders no check summary today. `getPatrolVerificationPresentation`
still classifies recent run history (a clean full check, a full check that
ended with errors, only targeted or follow-up checks, or no completed check)
and computes a same-day activity-mix label, but its only reader is the
current-findings empty state, which uses just its warning tone to withhold the
all-clear; the header recency line carries the visible coverage phrase. If a
check summary returns with that summary surface, it must explain what Patrol
actually checked without turning coverage mechanics into the default product
language: whether Patrol recently completed a clean broad check, only ran
targeted alert or anomaly-triggered checks, or ended its most recent broad
check with errors. When same-day run history shows both a recent broad check
and a burst of targeted or follow-up activity, that surface should expose the
activity mix in check language instead of asking operators to reconcile a
`Recently verified` headline with a busy Patrol strip elsewhere on the page.
`Recently checked` headline with a busy Patrol strip elsewhere on the page.
Fix-verification checks belong to that same explanation layer as follow-up
checks, not as evidence of a fresh full-estate sweep.
The same hierarchy keeps correlations, recent changes, and policy posture off
@@ -1880,8 +1914,9 @@ activity facts belong in run history, selected finding details, or explicit
secondary context, not as a second Patrol verdict label.
That same activity explanation should handle noisy Patrol behavior concretely.
When same-day history shows a mix of full sweeps, verification checks, and
scoped alert- or anomaly-triggered patrols, the verification/activity surface
should expose a compact breakdown of those run categories instead of leaving
scoped alert- or anomaly-triggered patrols, a returning verification/activity
surface should expose a compact breakdown of those run categories (Patrol
renders none today) instead of leaving
operators to infer why Patrol looked busy from an undifferentiated run count
alone.
The same operational layer should also surface scoped-trigger state directly.
@@ -2154,8 +2189,10 @@ requester identity as safe metadata, without copying the approval command
payload into Assistant.
That same store now owns the Patrol dashboard load bundle as well, so the
page refresh path stays aligned on a single orchestrated AI bundle instead of
repeating the individual summary, findings, approval, and correlation fetches
inline.
repeating the individual summary, findings, circuit-breaker, and approval
fetches inline. Neither that bundle nor the Patrol refresh path fetches the
global learned-correlation list: the Patrol page renders no correlations, so
the store holds no copy of that list.
The shared
`frontend-modern/src/components/Infrastructure/ResourceChangeSummary.tsx` and
`frontend-modern/src/components/Infrastructure/ResourceCorrelationSummary.tsx`
@@ -2326,10 +2363,10 @@ focused on product title, recency, and route controls instead of repeating
the same trust counters above the workspace tabs.
The same page-header recency line must also surface the coverage
signal from the most recent completed run via
`PatrolRecencyPresentation.resourcesChecked`, populated by
`getPatrolRecencyPresentation` from
`PatrolRunRecord.resources_checked`. The render reads "Last full
patrol: 3m ago — checked 47 resources" so operators see both
`PatrolRecencyPresentation.resourcesCheckedLabel` (raw count in
`resourcesChecked`), populated by `getPatrolRecencyPresentation` from
`PatrolRunRecord.resources_checked`. The render reads "Last check:
3m ago · checked 47 resources" so operators see both
temporal recency and coverage in one line. The field stays optional
(omitted when zero) so a degenerate run that completed without
checking any resources does not render a misleading "checked 0
@@ -2655,6 +2692,36 @@ clock while it stays open. Its `Last seen` and flapping window are latest
readings of a record that may have moved since, so they stay the ages at
read time, and the queue rows, summary, recent work, run history and digest
re-derive on their own 30- or 60-second re-reads.
Future times in Patrol count down instead of reading as ages, because the
past-only `formatRelativeTime` reads each of them as "just now" for their whole
duration. The attention detail's suppression line counts down to the expiry
with the exact time on hover and names the state expiry restores in the
early-end control's words, from the same
`getAttentionSuppressionEndPresentation` split on
`operationalRecord.acknowledgement`: an open issue reads `Returns to active
attention in 1d.`, one marked reviewed reads `Suppression ends in 1d. It stays
reviewed.` (expiry restores Acknowledged, which is not active attention), and a
passed expiry reads `Suppression has ended.`
(`getAttentionSuppressionExpiryLabel`). The findings list reads `Reminding in
6d` for a will_fix_later reminder and `snoozed, returns in 3h` for a snooze,
both through `formatTimeUntil` on the shared clock with the exact time on
hover. A reminder reads `Reminder overdue` once the panel's overdue id list
holds it. That list is sampled on the shared clock and is what the Overdue
commitments chip and filter count, so the label, chip and filter agree: a row
mounted or re-read between the deadline and the tick that adds it reads
`Reminder due`, and a row already on screen keeps its last countdown until that
tick. The list only notifies when its ids change, so a clock tick that moves no
deadline past now does not rebuild the open Overdue rows. The hourly reminder
sweep then brings the finding back whether or not it still trips. An ended
snooze reads `snooze ended` until the next findings re-read drops the snoozed
status: `GET /api/ai/patrol/findings` sends no status, so the frontend
`aiIntelligence` store (`normalizeFindingStatus`) derives `snoozed` on each
read only while `snoozed_until` is ahead, and the finding then shows as Active
unless it has since resolved or been dismissed.
`getFindingReminderPresentation` and `getFindingSnoozePresentation` in
`aiFindingPresentation.ts` own the findings copy.
`PatrolAttentionWorkbench.test.tsx`, `FindingsPanel.links.test.tsx` and
`FindingsPanel.test.ts` pin it.
### This week card answers what Patrol did for the customer
@@ -2712,9 +2712,26 @@ unavailable once the guest is stopped, whether its status or the Proxmox
`vm-stopped` reason says so, and Proxmox's own guest filesystems keep their
Proxmox read state. The two flags ride the existing row mapping and
add no query or poll; `GuestDrawer.test.tsx` and `useWorkloads.test.ts` pin
both sources. The workload table's disk cell and the drawer History still read
the aggregate disk metric and the Proxmox reason, and the agent's OS, addresses
and physical disk health carry no stale cue yet.
both sources. The workload table's disk cell (`useGuestRowState.ts`,
`GuestRow.tsx`) and the drawer History (`guestDrawerModel.ts`) judge the same
guest through the drawer rows' rule, `getWorkloadGuestDiskRead` in
`workloadGuestPresentation.ts`: a silent agent's bars carry the Last known
label, a live agent's bars stay current under Proxmox `agent-not-running`, and
History reads the aggregate disk metric as current, last known or unavailable
the same way. The rule covers the aggregate because Proxmox records no guest
disk usage without guest filesystems, so a manually linked guest then keeps the
agent's disk metric, and the poller no longer carries that agent reading
forward as Proxmox's own `prev-` read (see the monitoring contract).
`TestResourceRegistry_ManualGuestAgentLinkDiskMetricFollowsProxmoxGuestFilesystems`
pins that merge; `GuestRow.test.tsx` and `GuestDrawer.test.tsx` pin the cell and
History. On the websocket path (`resourceStateAdapters.ts`) a native Proxmox VM
facet's new guest-read outcome drops filesystems it no longer reports, a native
agent facet without `disks` drops the agent's, and the agent facet always
carries `stale`, so the rows switch source and lose the Last known label when
the agent resumes, without a reload. A stale Proxmox source
can still lose the aggregate to a live agent while its own filesystem rows
remain; such a guest stays on the Proxmox rule. The agent's OS, addresses and
physical disk health carry no stale cue yet.
Guest, node, and Docker-host drawer headers follow the same frontend-primitives dependency
boundary for collapse: Workloads owns which inline row is selected and the
close handler, while
@@ -1413,6 +1413,12 @@ state only. A restored prompt or restored message count may help an operator
continue a protected-item investigation, but it must not become backup coverage
evidence, recovery freshness, restore entitlement, storage-owner identity,
approval policy, or a provider-local recovery command handoff.
The POST-only guard on Assistant session mutations (`/abort`,
`/summarize`, `/fork`, `/undo`, `/redo`, `/steer`; `405` with `Allow: POST`
for any other method that reaches the handler) is transport hardening in
`internal/api/ai_handler.go`.
It opens no storage, backup, or recovery read or write path, and a rejected
request never reaches session state.
Approved Assistant tool execution through `internal/api/router_routes_ai_relay.go`
is also adjacent API/AI action plumbing for storage/recovery consumers.
`AssistantToolExecutor` / `ApprovedAssistantToolExecutor` may execute an already
@@ -3002,6 +3008,15 @@ recovery evidence. Unremembered overflow deliveries remain auditable with
`reason=capacity`; cache expiry, overflow, or restart is not a successful
storage operation or a reason to delete historical events.
### Alert settings saves keep unsent keys without a new storage path
`PUT /api/alerts/config` now applies only the top-level keys the body carries
and keeps stored values for the rest, so a thresholds save no longer turns off
alert TTL cleanup. It still writes `alerts.json` through the existing
`SaveAlertConfig` path, opens no storage or recovery route, and leaves backup
and snapshot alert settings under the same replace-what-was-sent rule as every
other key. `internal/api/alerting/alerts_test.go` pins the handler.
### Container diagnostics shares private bootstrap transport (1 October 2026)
Recovery-adjacent diagnostics now reuse the canonical complete installer
@@ -6905,6 +6920,15 @@ expires after ten minutes. Nothing is persisted, so a restart during the IdP
round trip refuses the returning Response and the user signs in again. No
storage, retention, backup, migration or recovery path is added or moved.
### A linked agent's stale flag and guest disk owner open no storage path
`internal/unifiedresources/types.go` now always sends `AgentData.Stale`, so a
resumed agent clears its stopped-reporting mark in browsers that merge agent
facets field by field, and `VMView.DiskFromLinkedAgent` keeps the poller from
carrying a linked agent's guest disk forward as a Proxmox read. Both describe
which source's reading is current; no backup, retention, migration or recovery
path is added or moved.
### Demo write guard ignores websocket upgrade headers
`internal/api/demo_middleware.go` no longer exempts every request carrying
@@ -6915,3 +6939,11 @@ allowlist it gets the generic demo `403`, or `404` on a hidden route, and that
includes config import at `/api/config/import`. HTTP/1.1 websocket handshakes
are `GET` requests and still connect. No storage, retention, backup, migration
or recovery path is added or moved.
### Demo mode hides the pprof routes
`internal/api/demo_mode_operations.go` adds the Go runtime profiling family
(`/debug/pprof` and every path below it, every method) to the public-demo
hidden routes, so the demo guard on a `DEMO_MODE` instance answers it with
`404`. That removes a diagnostic read surface on demo instances only; no
storage, retention, backup, migration or recovery path is added or moved.
@@ -2303,6 +2303,16 @@ evidence of what the array was doing when the agent went quiet, but without
live status colour. Where a retained rebuild percentage is shown, it reads
"Rebuild was at N%" with no speed or progress bar, and a member that was not
healthy names its state in its badge.
A Pulse agent manually linked into a guest carries the same signal there.
`AgentData.Stale` is always on the wire, because browsers merge agent facets
field by field (`resourceStateAdapters.ts`) and would keep a resumed agent's
omitted false as true; `TestAgentDataAlwaysSendsStale` pins it. That always
present `stale` also marks a native agent facet, so one without `disks` has
withdrawn them, and a native Proxmox VM facet's new guest-read outcome likewise
drops `disks` it no longer reports; either way the other source's filesystems
take over without a reload (`resourceStateAdapters.test.ts`). `VMView.DiskFromLinkedAgent` names the agent
as the owner of a guest's selected disk metric, which the monitoring poller
reads before it carries a Proxmox disk reading forward.
For SMART disk
temperatures on rows that still render, the provenance travels on
`agent.sensors.smart[].collection`: the Machines temperature cell
@@ -4614,26 +4624,29 @@ reconstructing a separate type-token summary in the emitter.
The same AI resource-intelligence payload now also carries canonical
correlation evidence from the shared detector, so the drawer can show learned
edge patterns alongside the dependency relationships without rebuilding correlation
reasoning from raw events. The Patrol intelligence page now also renders that
correlation evidence through the shared
reasoning from raw events. The drawer renders that correlation evidence
through the shared
`frontend-modern/src/components/Infrastructure/ResourceCorrelationSummary.tsx`
card, so the same learned-edge list stays governed by one frontend surface
instead of separate page-local implementations. That shared card also owns
card, and is that card's only caller today; the Patrol page renders no learned
correlations and the frontend does not load the global correlation list. Any later surface
that shows learned edges must reuse that card instead of a page-local
implementation. That shared card also owns
the first-class relationship-map surface for canonical `resource.relationships`,
the correlation ordering, and the truncation rule, so callers pass raw
relationships and correlation lists instead of encoding their own sort or
top-N behavior.
Canonical parent edges now also originate in this subsystem: `ParentID` is
folded into the facet relationship set through
`ResourceRelationshipsWithCanonicalParent` before any drawer or Patrol
consumer renders a relationship map, so pages do not rederive parent topology
`ResourceRelationshipsWithCanonicalParent` before any consumer renders a
relationship map, so pages do not rederive parent topology
from raw resource fields or invent relationship-map fallbacks locally.
The same surfaces now also render recent changes through the shared
The drawer renders recent changes through the shared
`frontend-modern/src/components/Infrastructure/ResourceChangeSummary.tsx`
card, so canonical timeline wording and ordering stay governed by one
frontend feed instead of separate page-local loops. Callers may suppress
resource-change metadata badges only for compact operator-context surfaces such
as Patrol's supporting context; the shared card still owns headline/reason
resource-change metadata badges only for compact operator-context surfaces;
no caller does today, and the Patrol page renders no recent changes. The
shared card still owns headline/reason
dedupe so prefixed backend reasons do not render as duplicated visible copy.
Assistant finding handoffs are part of that same timeline contract: when the AI
runtime needs recent changes for product-originated handoff resources, it should
@@ -5577,12 +5590,14 @@ wording stays aligned with the drawer's recent-change cards and timeline.
Timeline cards in that drawer surface change metadata when it is present, so
the history view preserves the richer provenance already carried by the
unified-resource model instead of flattening those fields away.
The same Infrastructure resource-only links now also default through the
shared `frontend-modern/src/components/Infrastructure/ResourceChangeSummary.tsx`
The shared `frontend-modern/src/components/Infrastructure/ResourceChangeSummary.tsx`
and `frontend-modern/src/components/Infrastructure/ResourceCorrelationSummary.tsx`
cards from the Patrol page, resource drawer, and problem-resource dashboard
panels, so canonical resource-filter path construction stays owned by the
shared summary cards rather than being duplicated per surface.
cards keep resource links behind an optional `buildResourceHref` input with no
default, following the 2026-05-16 cross-resource drilldown retirement above.
The resource drawer passes none, so its change and correlation labels render
as plain text; a surface that needs resource links must pass a platform-route
builder into the shared card rather than rebuilding resource-filter paths per
surface.
Platform tables supply the resource-label resolver to the resource drawer
through `PlatformResourceDetailTableRow`. `createPlatformResourceLabelResolver(...)`
in `frontend-modern/src/features/platformPage/PlatformResourceDetailTableRow.tsx`
@@ -6073,7 +6088,12 @@ the reference and is never retried. An owned row records its alert's
reference as `alert_resource_id`. A read by the reference returns the rows
journaled under it plus the rows owned away from it, matched through the
indexed alert identifier (`ProxmoxPhysicalDiskAlertIdentifiers`) and that
recorded reference, and every count uses the same predicate. The Alerts
recorded reference, and every count uses the same predicate. Those
identifiers cover the health and wearout specs and the disk temperature
metric spec (`<reference>::metric-threshold:diskTemperature`), since PVE disks
also raise temperature alerts under the same reference
(`TestProxmoxDiskTemperatureAlertRowsFollowRecordedHardwareIdentity` in
`internal/unifiedresources/store_test.go`). The Alerts
history Resource action therefore still lists every occurrence raised under
the path, across the disks that held it, and alert-centric reads keep their
occurrences (see the AI runtime contract's incident-history queries). Rows
@@ -6229,6 +6249,13 @@ presentation. `frontend-modern/src/components/Infrastructure/`
placement, signal, and snapshot context through the canonical resource drawer
and debug/source sections rather than introducing a VMware-only detail route,
drawer tab, or provider-local investigation shell.
The TrueNAS physical-disk drawer judges a current temperature reading by the
disk temperature policy, like the TrueNAS storage table:
`useResourceDetailDrawerDerivedState.ts` passes the alerts store's
`getDiskTemperatureThresholds` into `buildTrueNASDetailSections`, and the
Temperature row takes a warning tone only when `isPhysicalDiskRunningHot`
says the reading reached its disk type's alert trigger. A retained reading
keeps its muted last-known row, and without a resolver heat is not judged.
That same infrastructure consumer boundary also owns source selection
continuity. Settings infrastructure panels and platform/runtime pages must
keep canonical sources such as `truenas` and `availability` present in their