mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-10-24 04:33:17 +00:00
Merge branch 'refactor/drop-unread-patrol-state-members' into fix/drop-unread-circuit-breaker-read
This commit is contained in:
+10
-5
@@ -836,9 +836,12 @@ delivery for all resources.
|
||||
- `GET /api/admin/reports/catalog` (admin, `settings:read`)
|
||||
- Returns the canonical reporting catalog for the settings surface, including locked-state teaser copy, enabled-surface guidance copy, performance-report options, canonical single-report filename subject, canonical fallback filename date style, and the nested VM inventory export definition.
|
||||
- Metadata route: readable without the `advanced_reporting` feature so locked admin surfaces can render the same reporting definition before upsell.
|
||||
- `GET /api/admin/reports/generate` (admin, `settings:read`)
|
||||
- Query params: `format` (pdf/csv, default `pdf`), `resourceType`, `resourceId`, `metricType` (optional), `start`/`end` (RFC3339, optional; defaults to last 24h), `title` (optional)
|
||||
- `POST /api/admin/reports/generate` (admin, `settings:read`)
|
||||
- Body fields: `format` (pdf/csv, default `pdf`), `resourceType`, `resourceId`, `metricType` (optional), `start`/`end` (RFC3339, optional; defaults to last 24h), `title` (optional)
|
||||
- If `title` is omitted, the backend applies the canonical default title for that resource report.
|
||||
- When Pulse Assistant is configured, the configured AI provider narrates a PDF's executive summary; each narration counts against the AI budget and appears in AI usage. If the call fails, times out or the budget is spent, the PDF falls back to the deterministic summary. CSV output is never narrated. The settings UI uses this method.
|
||||
- `GET /api/admin/reports/generate` (admin, `settings:read`)
|
||||
- Takes the same fields as query params and returns the same report, except that it never calls an AI provider: a PDF carries the deterministic summary and a note saying AI narration needs `POST`. Other methods return `405` with `Allow: GET, POST`.
|
||||
- `POST /api/admin/reports/generate-multi` (admin, `settings:read`)
|
||||
- Body fields: `resources` (1-50 entries of `{resourceType,resourceId}`), `format`, `metricType` (optional), `start`/`end` (RFC3339, optional; defaults to last 24h), `title` (optional)
|
||||
- If `title` is omitted, the backend applies the canonical default fleet report title.
|
||||
@@ -851,8 +854,8 @@ Validation and limits:
|
||||
- Malformed `start`/`end` values, `end` not strictly after `start`, or report windows over 366 days return `400 invalid_time_range`.
|
||||
- `metricType` must match `[a-zA-Z0-9._:-]+` and be <= 64 chars, otherwise `400 invalid_metric_type`.
|
||||
- `title` must be <= 256 chars, otherwise `400 invalid_title`.
|
||||
- Multi-report body max size is 1MB; oversized payloads return `400 body_too_large`.
|
||||
- Multi-report bodies reject trailing payload and unknown JSON fields with `400 invalid_body`.
|
||||
- Report body max size is 1MB; oversized payloads return `400 body_too_large`.
|
||||
- Report bodies reject trailing payload and unknown JSON fields with `400 invalid_body`.
|
||||
- VM inventory export only accepts `csv`.
|
||||
|
||||
Common reporting error codes:
|
||||
@@ -891,7 +894,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
|
||||
|
||||
+4
-2
@@ -260,10 +260,12 @@ Each client runtime (or organization) generates its own reports, scoped to
|
||||
that client's resources:
|
||||
|
||||
- **UI**: Settings → Data & Reports.
|
||||
- **API**: `GET /api/admin/reports/generate` (single resource) and
|
||||
- **API**: `POST /api/admin/reports/generate` (single resource) and
|
||||
`POST /api/admin/reports/generate-multi` (up to 50 resources per report),
|
||||
returning PDF or CSV. In shared-process mode, scope with `X-Pulse-Org-ID`
|
||||
or an org-bound token.
|
||||
or an org-bound token. `GET /api/admin/reports/generate` with query
|
||||
parameters still returns a single-resource report, but never adds the
|
||||
Pulse Assistant narrative.
|
||||
- **Schedules**: `GET`/`POST /api/admin/reports/schedules`,
|
||||
`PUT`/`DELETE /api/admin/reports/schedules/{id}`, and
|
||||
`POST /api/admin/reports/schedules/{id}/run`. Schedules can target explicit
|
||||
|
||||
@@ -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,
|
||||
@@ -765,6 +777,15 @@ not fenced: a live report already past its mock-mode check when mock mode is
|
||||
switched on can still be evaluated after the clear, and its alerts can persist
|
||||
until mock mode is left.
|
||||
|
||||
Every tenant monitor registers its own fixture agents, and their node links,
|
||||
in its own alert manager, so the toggle forgets the fixture agents of every
|
||||
running monitor, not only those of the monitor it was called through. A link
|
||||
left behind would keep owning its fixture node's usage alerts after mock mode
|
||||
ends. `TestLeavingMockModeReleasesFixtureAgentNodeLinksOnEveryRunningMonitor`
|
||||
in `internal/monitoring/monitor_host_agent_removal_lifecycle_test.go` switches
|
||||
through the default monitor and fails if the other tenant's node stays
|
||||
suppressed.
|
||||
|
||||
Physical-disk evidence collected by a host agent must survive projection back
|
||||
into monitoring's models. Absent evidence has to carry its declared sentinel
|
||||
rather than a zero value that reads as a real measurement: an absent
|
||||
@@ -3669,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
|
||||
@@ -4703,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`,
|
||||
@@ -7630,6 +7665,16 @@ bounded and classified temperature nodes, and leaves the host report intact
|
||||
when the helper is absent or incompatible. It does not treat Windows ACPI
|
||||
thermal zones as hardware sensor evidence, accept remote provider locations,
|
||||
or grant Pulse hardware-control authority.
|
||||
### Single-resource report AI narration requires POST
|
||||
|
||||
`internal/api/metrics_reporting_handlers.go`, `internal/api/router.go` and
|
||||
`internal/api/ai_handlers.go` changed only how `/api/admin/reports/generate`
|
||||
reads its fields (a `POST` JSON body or `GET` query parameters), which of those
|
||||
may use the tenant's AI narrator (`POST` only), and how a `GET` finds Patrol
|
||||
findings (`ExistingAIService`, which never constructs a tenant AI service). No
|
||||
agent registration, enrolment, install, update, removal or report ingest path
|
||||
changed.
|
||||
|
||||
### Agent profile adoption is reported as a count only
|
||||
|
||||
The usage telemetry snapshot reads `LoadAgentProfiles` and reports
|
||||
@@ -8783,6 +8828,40 @@ Focused proof lives in `internal/monitoring/physical_disk_roundtrip_test.go`
|
||||
(`TestNestedProxmoxDefaultQEMUSerialsStayPerNode` and
|
||||
`TestTrueNASPlaceholderDiskSerialsStayPerApplianceAndShareOneHistoryKey`).
|
||||
|
||||
### Unraid serials key agent disk history
|
||||
|
||||
`internal/monitoring/monitor_agents.go` changed only so the SMART and disk I/O
|
||||
history writers key a host agent's disk by the serial its resource carries: the
|
||||
SMART row's own, else the one the host's Unraid inventory reports for that disk
|
||||
(`HostSMARTDiskMetricID`, `HostUnraidDeviceMetricID`). While the Unraid row
|
||||
names such a disk, its SMART and I/O history is written under that serial, which
|
||||
its chart already reads, instead of its WWN or host/device key. Agent
|
||||
registration, enrolment, install, update, removal, report identity and disk
|
||||
source IDs are unchanged. Focused proof lives
|
||||
in `internal/monitoring/monitor_host_agents_test.go`
|
||||
(`TestHostDiskIOMetricResourceIDFallbacks`) and
|
||||
`internal/monitoring/issue1595_collection_trust_test.go`
|
||||
(`TestAgentDiskHistoryFollowsTheSerialItsUnraidRowReports`).
|
||||
|
||||
### Unraid temperatures chart agent disk history
|
||||
|
||||
`internal/monitoring/monitor_agents.go` changed only so the SMART history
|
||||
writer charts the temperature a host agent's disk shows. A SMART row without
|
||||
its own reading shows the one the host's Unraid inventory reports for the disk,
|
||||
when that inventory row describes the disk (`HostSMARTDiskTemperature`), and a
|
||||
disk only the Unraid inventory reports,
|
||||
such as a member the agent's SMART collection skips under `--disk-exclude`,
|
||||
shows that row's reading (`HostUnraidDiskTemperature`). Both are now written
|
||||
as `smart_temp` under the key the disk's metrics target reads
|
||||
(`HostSMARTDiskMetricID`, `HostUnraidDiskMetricID`); a reading the disk does
|
||||
not show as collected, a spun-down disk's or an expired host's, is not. Agent
|
||||
registration, enrolment, install, update, removal, report identity and disk
|
||||
source IDs are unchanged. Focused proof lives in
|
||||
`internal/monitoring/monitor_host_agents_test.go`
|
||||
(`TestApplyHostReportChartsUnraidTemperatureOfDiskWithoutSMART`) and
|
||||
`internal/monitoring/issue1595_collection_trust_test.go`
|
||||
(`TestAgentDiskChartsTheUnraidTemperatureItShows`).
|
||||
|
||||
### Windows braced MachineGuid does not abort agent startup
|
||||
|
||||
The Windows unified agent resolves host information through gopsutil's combined
|
||||
@@ -8902,3 +8981,20 @@ agent from its last report instead of from the PVE poll, and mark it stale at
|
||||
their own heartbeat cutoff (`fleethealth.AgentStaleThreshold`, which is
|
||||
separate from the monitoring reporting lease). Agent registration, enrolment,
|
||||
install, update, removal and report identity are unchanged.
|
||||
|
||||
### Demo write guard ignores websocket upgrade headers
|
||||
|
||||
`internal/api/demo_middleware.go` changed only so a `POST`, `PUT`, `PATCH` or
|
||||
`DELETE` carrying `Upgrade: websocket` is judged by the demo read-only guard
|
||||
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.
|
||||
|
||||
@@ -128,6 +128,25 @@ on component mount. Closing clears the registered input so later keyboard
|
||||
commands cannot target a detached composer. A handoff must leave Escape and
|
||||
keyboard input in Assistant, not the underlying alert search.
|
||||
|
||||
### Process-wide memory ID sequences
|
||||
|
||||
`internal/ai/memory` mints incident, incident-event, remediation-record and
|
||||
context-memory IDs from package-level sequences that every store in the process
|
||||
shares. Multi-tenant monitoring gives each tenant monitor its own
|
||||
`IncidentStore`, and their alert checks call `RecordAlertFired` from separate
|
||||
goroutines, while a store's mutex covers only its own state. Each sequence is
|
||||
therefore an `atomic.Int64` minted through `nextSequencedID` in
|
||||
`internal/ai/memory/ids.go`, never a plain package counter. The ID shape stays
|
||||
`<prefix><local second>-<n>`, but `n` no longer wraps at 1000. The old
|
||||
`counter%1000` suffix repeated an ID whenever one generator minted more than a
|
||||
thousand in a wall-clock second, and incident notes resolve an incident by ID
|
||||
while timeline merges deduplicate events by ID. Within one process IDs are now
|
||||
unique by construction. Across restarts only the timestamp separates them, so a
|
||||
repeated local second (DST fall-back or a backward clock step) can still meet a
|
||||
restarted sequence's suffix. Persisted IDs and the stored format are unchanged.
|
||||
`internal/ai/memory/ids_test.go` mints from every generator concurrently and
|
||||
records alerts into two tenant stores concurrently; run it under `-race`.
|
||||
|
||||
### Canonical incident-history queries
|
||||
|
||||
Incident context reads `IncidentStore.QueryIncidents`, whose page contains
|
||||
@@ -3556,6 +3575,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
|
||||
@@ -7515,6 +7540,18 @@ prompt budget. Reporting is therefore an additive consumer of AI
|
||||
runtime, not a new ownership boundary, and the narrator/findings
|
||||
surfaces inherit the same governance the rest of the canonical AI
|
||||
runtime already enforces.
|
||||
Report narration is caller-requested spend, so on the single-resource HTTP
|
||||
route only `POST /api/admin/reports/generate` reaches it (fleet reports, from
|
||||
`POST .../generate-multi` or a report schedule, use the fleet narrator). The
|
||||
reporting narrator resolver goes through `AISettingsHandler.GetAIService`,
|
||||
which constructs a tenant service on first use and can then list provider
|
||||
models or start background discovery. A GET single-resource report never calls
|
||||
that resolver:
|
||||
it narrates deterministically and reads Patrol findings through
|
||||
`AISettingsHandler.ExistingAIService`, which returns only a service that is
|
||||
already running and never constructs one
|
||||
(`TestAISettingsHandler_ExistingAIServiceNeverConstructs`, and through the
|
||||
router's real wiring `TestContract_SingleReportGETDoesNotConstructTenantAIService`).
|
||||
|
||||
The same canonical AI runtime now also owns the fleet-level report
|
||||
narrative through `report_fleet_narrator.go`. `Service` implements
|
||||
|
||||
@@ -525,7 +525,7 @@ active-state and intent-policy linkage while persisting through the stable
|
||||
A per-metric threshold is off whenever its trigger is `<= 0`. That boundary is
|
||||
engine truth (`internal/alerts/canonical_metric.go`,
|
||||
`internal/alerts/config_runtime.go`), not a display convention: `-1` is the
|
||||
value Pulse writes, and `0` disables the metric just as completely because
|
||||
value the editors stage, and `0` disables the metric just as completely because
|
||||
earlier builds advertised `0` as the disable value and those overrides are still
|
||||
on disk. Every threshold editor reads that same `<= 0` rule through
|
||||
`frontend-modern/src/components/Alerts/alertResourceTableModel.ts` rather than
|
||||
@@ -543,7 +543,28 @@ the metric's enabled default instead, and only clears the override when the
|
||||
inherited default is already enabled. Editors must also not manufacture an off
|
||||
state out of an empty input: a cleared box is mid-edit, and coercing it to `0`
|
||||
disabled the metric in the engine while the row still showed On. Disabling is
|
||||
the toggle's job, and the toggle writes the canonical `-1`.
|
||||
the toggle's job, and the toggle stages the canonical `-1`.
|
||||
|
||||
The save path writes the backend's default contract, not the editors' sentinel.
|
||||
Alert config normalization reads a negative trigger on a global default as
|
||||
unset and restores the factory threshold, so `buildAlertsConfigurationPayload`
|
||||
in `frontend-modern/src/features/alerts/alertsConfigurationModel.ts` writes
|
||||
every Off global default as trigger `0`. Sending the staged `-1` through made
|
||||
every Off default with a positive factory threshold come back On at that
|
||||
threshold after save and reload; only guest and node CPU, memory and disk
|
||||
escaped, because normalization leaves them alone. Overrides keep `-1`, which
|
||||
the override path stores as written. Because `-1` and `0` are the same
|
||||
setting, the Global Defaults "Custom" badge compares Off values as equal
|
||||
rather than by raw number, so the factory `-1` I/O defaults do not read
|
||||
Custom against a saved `0`. An unset factory value stays distinct from Off,
|
||||
since unset can mean the default follows another setting. A row's
|
||||
custom-threshold marker keeps comparing raw values: it drives the row's revert
|
||||
control and override removal, so an explicit `-1` override must stay visible
|
||||
against an Off default even though both alert the same way.
|
||||
`frontend-modern/src/features/alerts/__tests__/alertsConfigurationModel.test.ts`
|
||||
pins the payload for every section, and
|
||||
`internal/alerts/config_defaults_off_test.go` pins that a zero default stays
|
||||
off through `UpdateConfig` and raises no alert.
|
||||
|
||||
External availability-probe reporting loss owns one canonical
|
||||
`external-probe-unavailable` alert per assigned agent, not one alert per target.
|
||||
@@ -1093,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
|
||||
@@ -1452,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
|
||||
@@ -1462,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
|
||||
@@ -1830,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
|
||||
@@ -1867,6 +2017,78 @@ well as its alerts.
|
||||
`internal/alerts/host_unraid_lifecycle_test.go` pins the clear, the holds, the
|
||||
restarted count and the pending-run and count cleanup.
|
||||
|
||||
### Proxmox node cleanup keeps platform metric alerts
|
||||
|
||||
`CleanupAlertsForNodes` runs on every Proxmox node poll (and every mock alert
|
||||
tick) and removes alerts whose `Node` is not a current Proxmox node or PBS
|
||||
instance. Kubernetes, TrueNAS and vSphere metric alerts carry a cluster or
|
||||
platform host in `Node` and a display label such as `TrueNAS Disk` or
|
||||
`Kubernetes Pod` in `metadata.resourceType`, which the preserve list's
|
||||
platform words never matched. The cleanup used to delete them silently on
|
||||
each poll while their reducer incident stayed firing, so the next evaluation
|
||||
re-created each one and handed it to the alert callback again as a new
|
||||
alert. `shouldPreserveAlertOutsideNodeCleanup` now keeps any alert carrying
|
||||
`metadata.platformType`, or whose label maps through
|
||||
`CanonicalAlertResourceType` to a Kubernetes, TrueNAS or vSphere type.
|
||||
`TestCleanupAlertsForNodesKeepsPlatformMetricAlerts` in
|
||||
`internal/alerts/alerts_test.go` pins both signals, and its
|
||||
sibling pins that a guest alert on a removed Proxmox node is still removed.
|
||||
|
||||
### Agent disk temperature alerts judge the reading the disk shows
|
||||
|
||||
`CheckHost` judges each agent disk on the temperature, collection state and
|
||||
disk type the disk shows, read by `unifiedresources.HostDiskTemperatureReadings`
|
||||
from the same adapters the registry builds the disks with. A SMART row shows its
|
||||
own reading or, when the agent's probe returned none, the one the host's Unraid
|
||||
inventory reports for that disk (`HostSMARTDiskTemperature`), and keeps its
|
||||
`agent:<host>/disk_temp:<device>` resource. An Unraid inventory row whose disk
|
||||
key no SMART row carries, a disk only the inventory lists, shows that row's
|
||||
reading (`HostUnraidDiskTemperature`) and its own transport as disk type, under
|
||||
its kernel block device token, the resource a plain Linux SMART row for the
|
||||
same device uses. A SMART row and an Unraid row with one key are one registry
|
||||
disk even under different device labels, such as a controller member
|
||||
(`0 [megaraid,0]`) and its Unraid device (`sda`), and alert once, under the
|
||||
SMART row's resource. When smartctl and Unraid name
|
||||
one device with different serials the registry shows two disks on it, and the
|
||||
device's one alert judges the reading that stands highest against its own disk
|
||||
type's threshold (`judgedHostDiskTemperature`): over its trigger first, then
|
||||
inside its recovery band, then the larger margin over the trigger, with a tie
|
||||
keeping the reading listed first.
|
||||
Before this `CheckHost` read only the SMART row's own field, while the Physical
|
||||
Disks Temp cell, the Running Hot verdict ("at or above its alert threshold")
|
||||
and Patrol judged the Unraid reading against the same trigger, so such a disk
|
||||
could read as at its alert threshold with no alert. Unraid's own warning and
|
||||
critical disk temperature notifications do not make this redundant: Pulse
|
||||
already alerts on the smartctl readings and array state of the same hosts,
|
||||
which Unraid also notifies on, and a user who routes alerts through Pulse sets
|
||||
these thresholds here.
|
||||
|
||||
Only a reading collected now fires or resolves
|
||||
(`HostDiskTemperatureReading.Collected`: no standby row, and
|
||||
`diskinventory.TemperatureCollected`), so a spun-down disk holds the alert
|
||||
like a listed disk without a temperature. A disk only the
|
||||
inventory lists whose device matches the agent's `--disk-exclude` patterns
|
||||
(`fsfilters.MatchesDeviceExclude`) raises nothing, and an alert it raised before
|
||||
the exclusion resolves on the next report (`clearHostDiskTemperatureResources`),
|
||||
whatever the SMART list holds, unless another listed reading uses the same
|
||||
resource: `docs/UNIFIED_AGENT.md` documents exclusion as
|
||||
removing the disk from monitoring, and linked Proxmox hosts already judge an
|
||||
excluded disk healthy. A disk only the inventory lists is also judged in a
|
||||
report whose SMART list is empty, because the disk shows the inventory's
|
||||
reading in that report. The absence cleanup above still runs only on a
|
||||
non-empty SMART list, and such a report neither counts toward nor restarts a
|
||||
disk's three, so a disk that leaves the inventory while every report's SMART
|
||||
list stays empty keeps its alert until the 24-hour stale-alert sweep.
|
||||
`TestCheckHostJudgesTheUnraidTemperatureADiskShows` and
|
||||
`TestCheckHostHoldsUnraidDiskTemperatureAlertUntilAReadingOrDeparture` in
|
||||
`internal/alerts/host_unraid_lifecycle_test.go` pin the fallback, the
|
||||
inventory-only disk, the empty SMART list, two disks on one device (also of
|
||||
different types), a controller member under another device label, standby,
|
||||
exclusion, the disk type
|
||||
and host override thresholds, the holds, the handover to a SMART reading,
|
||||
departure, and the exclusion clear with and without SMART rows and beside a
|
||||
listed SMART row. `TestJudgedHostDiskTemperatureOutranks` pins the ranking.
|
||||
|
||||
### Configured flapping thresholds remain reachable
|
||||
|
||||
Every accepted positive `FlappingThreshold`, including values above ten, must
|
||||
@@ -1894,6 +2116,42 @@ controls in `internal/alerts/flapping_threshold_test.go` exercise both sweeps,
|
||||
drained and retained windows, dispatch callbacks and delivery diagnosis. These
|
||||
modeled-time controls are not installed notification-destination acceptance.
|
||||
|
||||
### Default thresholds keep a positive trigger's clear below it
|
||||
|
||||
PBS, node temperature, agent, Kubernetes, TrueNAS and vSphere global defaults
|
||||
go through `normalizeThresholdPointer` in `internal/alerts/config/normalize.go`
|
||||
from `UpdateConfig`. The shared persistence normalization in
|
||||
`internal/config/persistence.go` applies the same normalizer, through
|
||||
`NormalizeHysteresisThreshold`, to the agent, node temperature and storage
|
||||
pairs. A missing or negative threshold takes the factory default and a zero
|
||||
trigger is off. A positive trigger always keeps a clear below it: a missing
|
||||
clear sits five points under the trigger, floored at 0, and a clear at or
|
||||
above the trigger is repaired. Guest and node usage, storage and Docker
|
||||
defaults hold the same bound through `EnsureValidHysteresis` in
|
||||
`ValidateHysteresisThresholds`, `NormalizeStorageDefaults` and
|
||||
`NormalizeDockerThreshold`. The canonical per-type entries (`nvme`, `sata` and
|
||||
`hdd` in `DiskFillByType`; `nvme`, `sas` and `sata` in `DiskTempByType`, the
|
||||
types the thresholds page edits) keep a positive trigger and follow the same
|
||||
clear rule; a non-positive trigger resets the entry to its type default,
|
||||
because the page cannot switch a type off on its own. Other per-type keys are
|
||||
stored as written.
|
||||
|
||||
The thresholds page sends `max(0, trigger - 5)`, so a 1-5% default arrives
|
||||
with clear 0 and must not fall back to the factory clear. Agent defaults used
|
||||
to do that, storing `{trigger: 1, clear: 75}` for a 1% Machines CPU default,
|
||||
and per-type disk entries replaced the whole pair, trigger included. The
|
||||
evaluator already ignored a clear at or above the trigger
|
||||
(`buildCanonicalMetricSpec` drops it and the reducer then clears at the
|
||||
trigger), so those alerts fired and resolved the same way; `/api/alerts/config`
|
||||
served the factory clear and firing alerts reported it as `clearThreshold`. A
|
||||
repaired pair above the margin, such as `{50, 80}` written through the API,
|
||||
now has a real recovery band at 45 where it used to clear at the trigger.
|
||||
`internal/alerts/config/normalize_low_trigger_clear_test.go` pins every
|
||||
default family and both per-type maps, `TestUpdateConfigKeepsLowTriggerClearBelowTrigger` in
|
||||
`internal/alerts/config_validation_test.go` pins the saved config and the
|
||||
firing alert's clear level, and the low-trigger and stored-clear tests in
|
||||
`internal/config/persistence_test.go` pin the written file and load repair.
|
||||
|
||||
### Monitor-only delivery is terminal
|
||||
|
||||
Monitor-only alerts remain visible, but neither a firing nor a recovery
|
||||
@@ -2597,8 +2855,10 @@ Metric enablement is an explicit On/Off interaction in row, global-default,
|
||||
mobile, and bulk editors. Enabled numeric inputs accept positive trigger values
|
||||
only and user-facing copy must not expose the persisted disable sentinel.
|
||||
Internally, the canonical `<= 0` read rule remains intact for legacy data, while
|
||||
new Off actions write `-1`, so existing configuration and the API contract
|
||||
continue to round-trip without making `0` a second customer-facing disable path.
|
||||
new Off actions stage `-1` (saved as trigger `0` on global defaults, which the
|
||||
backend otherwise resets to factory), so existing configuration and the API
|
||||
contract continue to round-trip without making `0` a second customer-facing
|
||||
disable path.
|
||||
Within the Proxmox tab, render-heavy ownership now further routes through
|
||||
`frontend-modern/src/components/Alerts/ThresholdsTableProxmoxNodesSection.tsx`,
|
||||
`frontend-modern/src/components/Alerts/ThresholdsTableProxmoxPBSSection.tsx`,
|
||||
@@ -2745,6 +3005,22 @@ derived alert read-model and Last 24 Hours stat refresh for
|
||||
shared acknowledgement owner instead of keeping its own alert mutation fork.
|
||||
Future overview action behavior should extend that shared acknowledgement hook
|
||||
instead of putting acknowledge mutations back into render shells.
|
||||
Overview ages and bands read the shared clock. `AlertOverviewAlertCard.tsx`
|
||||
passes its own `useRelativeTimeNow` reading as `now` to
|
||||
`formatAlertOverviewStartedAgo` and `getMetricAlertPresentation`, and the Last
|
||||
24 Hours count in `useAlertOverviewState.ts` reads the same clock, so all three
|
||||
measure from the wall clock. The hook no longer exposes a minute `tick`: read
|
||||
as "now", that tick measured a card mounted between ticks from up to a minute
|
||||
earlier, so an alert raised 70 seconds ago read "this minute", an alert
|
||||
raised since the last tick stayed out of the 24h count until the next one, a
|
||||
reading already past the 10-minute stale cut-off led the card as live, and the
|
||||
stale flip and 24h drop landed up to a minute late. A mounted card now turns stale,
|
||||
and an alert leaves the 24h count, within one 30-second tick with no data
|
||||
change. The delivery diagnoses refresh is a server read and keeps its own
|
||||
minute interval rather than following the 30-second clock.
|
||||
`AlertOverviewAlertCard.clock.test.tsx` pins the mid-tick mount, the stale flip
|
||||
and the 24h drop under fake timers, and `useAlertOverviewState.test.tsx` pins
|
||||
the diagnoses cadence.
|
||||
Render-heavy alert overview ownership now routes through
|
||||
`frontend-modern/src/features/alerts/AlertOverviewStatsCards.tsx`,
|
||||
`frontend-modern/src/features/alerts/AlertOverviewActiveAlertsSection.tsx`,
|
||||
@@ -3088,6 +3364,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
|
||||
@@ -3326,6 +3340,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
|
||||
@@ -3696,6 +3711,10 @@ a new API state machine, queue contract, or verification-accounting field.
|
||||
requested metric filters through the shared guest-chart batch loader in
|
||||
`internal/monitoring/monitor_metrics.go` instead of fetching the full guest
|
||||
metric set and trimming after the API payload is already assembled.
|
||||
`ChartsAPI.getInfrastructureSummaryCharts` is the only frontend client for
|
||||
`/api/charts/infrastructure`; the deprecated `getInfrastructureCharts`
|
||||
alias, which could not pass `metrics`, is removed and must not return as a
|
||||
second unfiltered entry point.
|
||||
39. Keep the retired compact dashboard overview route absent from that same
|
||||
shared API surface. `internal/api/resourceapi/resources.go`,
|
||||
`internal/api/router_routes_monitoring.go`, and
|
||||
@@ -4842,6 +4861,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
|
||||
|
||||
@@ -7119,12 +7142,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
|
||||
@@ -7178,7 +7203,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
|
||||
@@ -7641,6 +7666,34 @@ 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
|
||||
`/ws` and `/api/agent/ws` are `GET` requests and connect through that read
|
||||
branch, so an `Upgrade: websocket` or `Connection: Upgrade` header never
|
||||
carries a `POST`, `PUT`, `PATCH` or `DELETE` past the guard. A demo deployment that signs
|
||||
visitors in as the configured admin, as the public demo does, has only this
|
||||
guard between that session and admin writes such as `/api/system/mock-mode`.
|
||||
`TestContract_DemoModeUpgradeHeadersDoNotExemptWrites` in
|
||||
`internal/api/contract_test.go` signs in as that admin through the full router
|
||||
and pins both halves.
|
||||
That shared infrastructure install boundary now also preserves copied shell
|
||||
command payload continuity: any privilege-escalation wrapper applied at
|
||||
`frontend-modern/src/components/Settings/InfrastructureInstallerSection.tsx`
|
||||
@@ -9645,6 +9698,32 @@ narrator must fail closed: nil provider, parse failure, timeout, or
|
||||
empty response causes the engine to fall back to the heuristic
|
||||
narrative without surfacing the AI failure to the caller, so reporting
|
||||
is never blocked by AI availability.
|
||||
Only `POST /api/admin/reports/generate` may hand the tenant's AI narrator to
|
||||
the engine, because a narrated PDF makes a paid provider call and appends a
|
||||
`cost.UsageEvent` to the cost ledger. The single-resource handler accepts
|
||||
`POST` with a JSON body (the `generate-multi` decoding rules: 1MB cap, unknown
|
||||
fields and trailing payload rejected, through `decodeReportingRequestBody`) and
|
||||
`GET` with the same fields as query parameters, and answers any other method
|
||||
with `405` and `Allow: GET, POST`. GET never calls `resolveNarrator`, whose
|
||||
`GetAIService` can construct a tenant AI service (and with it list provider
|
||||
models or start background discovery): it narrates through `getReportNarrator`,
|
||||
the heuristic summary whose `Disclaimer` replaces the "Configure Pulse
|
||||
Assistant" tip with a note that AI narration needs POST, and reads Patrol
|
||||
findings through `SetExistingFindingsResolver`, which `router.go` backs with
|
||||
`AISettingsHandler.ExistingAIService` so only an already running service is
|
||||
used. GET passes the demo-mode guard and the CSRF check, and a SameSite=Lax
|
||||
session cookie rides a cross-site top-level GET navigation, so a link must not
|
||||
be able to spend AI budget. The reporting catalog advertises the transport as
|
||||
`performanceReport.singleResourceMethod` (`POST`), and the settings UI follows
|
||||
it. `generate-multi` and the manual schedule run endpoint
|
||||
(`POST /api/admin/reports/schedules/{id}/run`) were already POST-only; due
|
||||
schedules run from the server's scheduler, not an HTTP request.
|
||||
`TestContract_SingleReportAINarrationRequiresPOST` pins the rule on a real
|
||||
engine: GET, HEAD, PUT and DELETE make no narrator call and no AI service
|
||||
resolution, while POST makes one of each.
|
||||
`TestContract_SingleReportGETDoesNotConstructTenantAIService` drives
|
||||
`Router.wireReportingAIResolvers` with an uncached tenant: GET constructs no
|
||||
tenant AI service and POST resolves one.
|
||||
Report alerts keep the alert engine's resolution. The node, VM and
|
||||
container enrichers in `internal/api/metrics_reporting_handlers.go` build
|
||||
every row through `reportActiveAlertInfo` and `reportResolvedAlertInfo`,
|
||||
@@ -10270,6 +10349,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
|
||||
|
||||
@@ -6101,6 +6101,14 @@ summaries whose scope, format, and attachments the server fixes. The delivery
|
||||
surface (`/api/admin/reports/schedules`) and tenant-local storage are
|
||||
unchanged.
|
||||
|
||||
### Provider docs name POST for single-resource reports
|
||||
|
||||
`docs/MSP.md` and its shipped copy now give `POST /api/admin/reports/generate`
|
||||
for per-client single-resource reports and say a `GET` with query parameters
|
||||
still returns the report without the Pulse Assistant narrative, matching the
|
||||
server, which lets only `POST` narrate. Install, upgrade and provider bundle
|
||||
delivery are unchanged.
|
||||
|
||||
### Convergence log-reader compatibility (5 September 2026)
|
||||
|
||||
Scheduled reconciliation run 33939926237 failed when `gh api` refused terminal
|
||||
|
||||
@@ -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
|
||||
@@ -3011,9 +3031,9 @@ Agent`), with the plain-language source phrase available through accessible
|
||||
than receiving an unfiltered copy of the estate. Opaque unified-resource ids
|
||||
are not part of that visible search vocabulary and must not retain a node
|
||||
when the normalized guest table has no corresponding match.
|
||||
8. Keep summary chart interaction identity on one shared helper. Summary surfaces that expose row-hover, group-hover, chart-hover, or route-focus-driven chart emphasis must derive page/group/entity scope through `frontend-modern/src/components/shared/summaryCardInteraction.ts` and pass that same resolved scope into card-state, sparkline, and density-map primitives, rather than letting cards read `hovered || focused` while charts listen to a different page-local ID source. Hovering one summary chart must promote that series into the shared active entity so sibling cards highlight the same object instead of keeping chart-local hover islands, and hovering or pinning a workload group header, infrastructure cluster header, or storage pool-group header must scope the matching summary cards through that same shared contract instead of forking a page-local summary filter path. Sibling cards should surface that synchronized hover as one compact header readout through the shared summary-card contract, while the chart under the pointer keeps the only floating tooltip. Recovery is explicitly outside this interaction dialect: its retired posture-card strip must not return with row/group/chart hover behavior without a separate governed product decision.
|
||||
9. Keep page summaries page-scoped when table rows enter contextual focus. Route-backed row selection may add a focused label and shared series emphasis, but infrastructure, workloads, and storage summary cards must continue to render the page-level series set instead of collapsing the summary down to the selected row or replacing the global trend view with row-local empty states.
|
||||
10. Keep contextual row focus on the shared summary primitive. Summary surfaces and same-route table drill-ins must reuse `frontend-modern/src/components/shared/contextualFocus.ts` for interactive-series filtering, focused-name lookup, active-series derivation, local scroll preservation, and deliberate inline-detail reveal instead of rebuilding page-local `Set` filters, focused-label scans, drawer-aware scroll math, or ad hoc scroll restoration in each surface.
|
||||
8. Keep summary interaction identity on one shared helper. Pages that expose row-hover, group-hover, or route-focus-driven summary emphasis, today the Workloads and Storage tables through `useSummaryPageInteractionState` in `frontend-modern/src/components/shared/summaryTableFocus.ts`, must derive page/group/entity scope through `frontend-modern/src/components/shared/summaryCardInteraction.ts` rather than letting each surface read `hovered || focused` from its own page-local ID source. Hovering or pinning a storage pool-group header, or a workload group header on a surface that enables inline group drawers (the Proxmox and VMware surfaces pass `groupNodeDrawerMode="disabled"` today), must scope the matching rows through that same shared contract instead of forking a page-local summary filter path. No summary card or chart strip renders today, and the bridge carries no chart-hover input; one that returns must consume that same resolved scope, and the chart under the pointer must enter its hovered series into `resolveSummaryScopeState` as a preview input that outranks row hover instead of keeping a chart-local hover island. Recovery is explicitly outside this interaction dialect: its retired posture-card strip must not return with row/group/chart hover behavior without a separate governed product decision.
|
||||
9. Keep any page summary page-scoped when table rows enter contextual focus. No infrastructure, workloads, or storage summary cards render today. If one returns, route-backed row selection may add a focused label and shared series emphasis, but the summary must continue to render the page-level series set instead of collapsing down to the selected row or replacing the global trend view with row-local empty states.
|
||||
10. Keep contextual row focus on the shared primitives. Same-route table drill-ins must reuse `frontend-modern/src/components/shared/contextualFocus.ts` for local scroll preservation, off-screen reveal, inline-detail lookup, and deliberate inline-detail reveal, and must resolve the active series through `frontend-modern/src/components/shared/summaryCardInteraction.ts` behind `frontend-modern/src/components/shared/summaryTableFocus.ts`, instead of rebuilding page-local `Set` filters, focused-label scans, drawer-aware scroll math, or ad hoc scroll restoration in each surface. `frontend-modern/src/components/shared/summaryTableFocus.ts` measures and scrolls rows only for deliberate focus, a focused row's inline detail or a pinned group header, so it must not keep window-level scroll or resize listeners or re-measure the hovered row on hover changes and scroll frames; an off-screen affordance for the active row needs a rendered consumer before that cost returns.
|
||||
11. Keep summary-linked table row emphasis on the shared primitive contract. Workloads, infrastructure, and storage rows that mirror the active summary entity must expose that state through `data-summary-row-active` and let the shared presentation in `frontend-modern/src/index.css` render the row emphasis, rather than carrying page-local sky or blue fill classes inside each row renderer. Group-scoped preview and pin must use that same shared presentation boundary: child rows that belong to a hovered or pinned summary group should expose `data-summary-group-member-active="preview|pinned"` so the block-level emphasis stays subtle, consistent, and reversible instead of each table inventing its own outline, badge, or full-strength fill treatment. Static grouped row headers on workloads, infrastructure, storage, recovery, and future grouped tables must use `frontend-modern/src/components/shared/groupedTableRowPresentation.ts` plus the `.grouped-table-row` CSS contract in `frontend-modern/src/index.css`, rather than rebuilding local `bg-surface-alt` variants with subtly different light/dark behavior or page-local left-accent markers. That shared grouped-table primitive owns the subgroup cell padding, typography, small metadata, and badge treatment as well as the row background token, so a future adjustment to the subgroup visual language changes every grouped product table from one owner. Inline table detail rows on platform, workload, and infrastructure tables must compose `frontend-modern/src/components/shared/InlineDetailTableRow.tsx` for the full-width row, surface-alt cell, detail padding, and row-click containment instead of rebuilding page-local `TableRow` / `TableCell` / `div` shells around each drawer. Storage-backed reusable row presenters under `frontend-modern/src/features/storageBackups/` must also keep row height and alert accents on class/data-attribute presentation instead of runtime inline style maps, so the shared table contract stays CSP-safe on both steady-state and alert-highlighted routes.
|
||||
12. Keep retained-value data loading honest at the ownership boundary. Helpers
|
||||
that prevent a feature surface from falling through the app-level Suspense
|
||||
@@ -3860,22 +3880,19 @@ production table, router and styles; it does not qualify full-app scrolling.
|
||||
finding-presentation destinations such as the Patrol provider-settings
|
||||
route, while `patrol_autonomy` and legacy Pro activation URLs remain parser
|
||||
aliases only and verified review links use the plain Patrol history anchor.
|
||||
UI surfaces must not duplicate the `patrolControlStarter` query string or
|
||||
write Patrol control or legacy entry-point starter telemetry from local
|
||||
click handlers.
|
||||
The Patrol finding-to-Assistant handoff has one owner,
|
||||
`frontend-modern/src/components/AI/FindingsPanel.tsx`; the Patrol state
|
||||
hook keeps no second copy of it, and the Patrol workspace passes no
|
||||
handoff callback because nothing on the page reacts to a handoff.
|
||||
19. Keep shared summary-card emphasis coherent. When shared summary primitives enter an `inactive` state, `SummaryMetricCard`, `InteractiveSparkline`, and `DensityMap` must all demote background context together so storage, infrastructure, and workloads read as one interaction model instead of mixing page-local opacity, sticky-shell, or highlight rules.
|
||||
20. Keep density-map summaries overview-first. When a shared summary density map receives row focus or chart-hover emphasis, `frontend-modern/src/components/shared/DensityMap.tsx`, `frontend-modern/src/components/shared/useDensityMapState.ts`, and `frontend-modern/src/components/shared/densityMapModel.ts` must preserve the multi-entity overview rows and keep focused-entity detail in the hover tooltip instead of swapping the card into a single-series chart, dimming the rest of the map into unusable background noise, duplicating cursor-value tooltip copy, or adding persistent card chrome that steals heatmap space. The card body must stay overview-first; the tooltip may carry the active entity identity, current value, and peak, shared tooltip shells must follow semantic surface tokens instead of forcing a dark palette in light mode, the tooltip header must let long entity names consume the available width before truncating rather than clipping against an arbitrary fixed label cap, numeric metric readouts such as `16.9 MB/s` or `37.4 MB/s` must stay single-line instead of wrapping the unit onto a second row, and density-map detail that cannot fit cleanly inside the canonical tooltip shell must be omitted rather than introducing tooltip-specific chrome or a secondary chart inside the hover surface.
|
||||
UI surfaces must not duplicate the `patrolControlStarter` query string or
|
||||
write Patrol control or legacy entry-point starter telemetry from local
|
||||
click handlers.
|
||||
21. Keep retired self-hosted hosted-model and trial acquisition surfaces out of
|
||||
normal v6 GA runtime. Shared shells and helper-driven badges may continue to
|
||||
parse legacy payload fields, but ordinary self-hosted Assistant, Patrol, and
|
||||
settings flows must present provider setup as BYOK/local/self-managed and
|
||||
must not surface hosted-model credits, in-app trial starts, or generic
|
||||
managed-model claims.
|
||||
22. Keep sparkline scrubbing source-local and sibling-sync timestamp-based. The chart a user is actively scrubbing in `frontend-modern/src/components/shared/InteractiveSparkline.tsx` and `frontend-modern/src/components/shared/useInteractiveSparklineState.ts` must keep its dashed hover cursor on the real local mouse `x`, while sibling cards may map the shared hover timestamp onto their own timelines. Shared cursor sync must not snap the source chart back onto the nearest sample timestamp, the rendered SVG/canvas hover cursor must bind to the actual numeric cursor coordinate rather than a boolean guard state, the time cursor must span the chart viewport instead of collapsing to the series height, and the hover tooltip must track the pointer instead of anchoring to the chart top edge while following the active theme rather than a hardcoded dark shell. The hover tooltip must stay side-offset from the active scrub cursor and flip to the available side near viewport edges so it does not cover the highlighted guide or graph point.
|
||||
23. Keep shared contextual focus canonical after adoption. Once a summary or table surface enters route-backed contextual focus, future additions must extend `frontend-modern/src/components/shared/contextualFocus.ts` and its guardrail tests rather than forking another helper for workload IDs, resource IDs, or scroll-preserving same-route selection.
|
||||
24. Keep shared infrastructure/resource selectors on the canonical agent-facet
|
||||
truth. Shared primitives and settings-facing selector helpers must treat
|
||||
@@ -3905,12 +3922,14 @@ production table, router and styles; it does not qualify full-app scrolling.
|
||||
26. Keep the authenticated app root aligned with that same first-session path.
|
||||
That same shared-primitive ownership now includes contextual row focus.
|
||||
`frontend-modern/src/components/shared/contextualFocus.ts` is the canonical
|
||||
owner for interactive-series filtering, focused-label lookup, active-series
|
||||
resolution, and nearest-scrollable-ancestor preservation across page-scoped
|
||||
summary surfaces. Dashboard row focus, infrastructure summary emphasis,
|
||||
storage summary emphasis, and workloads summary emphasis must all route through
|
||||
that helper instead of maintaining page-local copies of the same hover/focus
|
||||
rules.
|
||||
owner for nearest-scrollable-ancestor preservation, off-screen element
|
||||
reveal, and inline-detail lookup and reveal, while page/group/entity scope
|
||||
resolution belongs to
|
||||
`frontend-modern/src/components/shared/summaryCardInteraction.ts` behind
|
||||
`frontend-modern/src/components/shared/summaryTableFocus.ts`. Workloads and
|
||||
Storage row focus and the Proxmox page's node-to-guests reveal must route
|
||||
through those helpers instead of maintaining page-local copies of the same
|
||||
hover/focus rules.
|
||||
`frontend-modern/src/App.tsx` must land authenticated `/` and `/login`
|
||||
handoffs through this subsystem's provider-first platform landing contract:
|
||||
the first visible provider/runtime platform wins, and the Machines surface
|
||||
@@ -4830,6 +4849,13 @@ performance-report transport (`/api/reporting` and `/api/reporting/generate-mult
|
||||
so the reporting panel does not go dead on mixed-version installs, but that
|
||||
compatibility path is intentionally report-only and must not invent the newer
|
||||
catalog-owned VM inventory export surface.
|
||||
`reportingPanelModel.ts` picks the single-resource transport from the catalog's
|
||||
`performanceReport.singleResourceMethod`: `POST` sends the request fields as a
|
||||
JSON body, which is the only transport the server lets narrate a PDF with Pulse
|
||||
Assistant, and `GET` sends the same fields as query parameters.
|
||||
`reportingCatalogModel.ts` reads a missing field as `GET`, the only method a
|
||||
backend that predates the field accepts, and the legacy compatibility catalog
|
||||
names `GET` for `/api/reporting`. Fleet reports stay `POST` on every backend.
|
||||
`ReportingPanel.tsx` must therefore treat `vmInventoryExport` as optional when
|
||||
it renders a governed reporting catalog. A legacy compatibility catalog with no
|
||||
inventory export still owns a valid enabled reporting surface and must continue
|
||||
@@ -4902,6 +4928,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,
|
||||
@@ -5109,52 +5145,27 @@ Enter/Space work through the native button, and lifecycle actions do not bubble
|
||||
into collapse. Generic `Ask Assistant` and `Copy context` actions do not belong
|
||||
in object drawer headers: Assistant remains available through the global shell,
|
||||
and raw context export must not compete with the operational reading path.
|
||||
The shared interactive sparkline now follows that same split.
|
||||
`frontend-modern/src/components/shared/InteractiveSparkline.tsx` stays the
|
||||
render shell, `frontend-modern/src/components/shared/useInteractiveSparklineState.ts`
|
||||
owns hover state, RAF throttling, canvas draw scheduling, and resize lifecycle,
|
||||
and `frontend-modern/src/components/shared/interactiveSparklineModel.ts` owns
|
||||
sparkline downsampling, gap segmentation, axis-tick math, and hover-selection
|
||||
policy. Future sparkline work should extend those owners instead of pushing
|
||||
canvas scheduling or chart-shape math back into the shared component shell.
|
||||
That same sparkline boundary now also owns floating tooltip shell routing:
|
||||
local hover tooltips must derive viewport anchor coordinates from the shared
|
||||
runtime/model path, keep the tooltip beside rather than on top of the scrub
|
||||
cursor, and render through
|
||||
`frontend-modern/src/components/shared/TooltipPortal.tsx`, not as HTML
|
||||
`foreignObject` shells inside the `preserveAspectRatio="none"` chart SVG where
|
||||
cross-browser scaling can stretch the tooltip surface or drop its semantic
|
||||
shell styling.
|
||||
That same shared sparkline boundary now also owns active-series isolation
|
||||
metadata. The shell may expose `data-active-series-display` and
|
||||
`data-rendered-series-count` for proof and inspection, but only the shared
|
||||
runtime/model owners may decide whether a hovered or focused series is merely
|
||||
emphasized or fully isolated; feature shells must not fork their own row-hover
|
||||
line filtering.
|
||||
`frontend-modern/src/components/Workloads/MetricMiniSparkline.tsx` and
|
||||
`frontend-modern/src/features/proxmox/BackupActivityChart.tsx` render their
|
||||
hover tooltips through `frontend-modern/src/components/shared/TooltipPortal.tsx`
|
||||
rather than as HTML `foreignObject` shells inside the chart SVG; the mini
|
||||
sparkline's SVG uses `preserveAspectRatio="none"`, where cross-browser scaling
|
||||
can stretch an embedded tooltip surface or drop its semantic shell styling.
|
||||
The retired dashboard overview route must not regain feature-local trend,
|
||||
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
|
||||
reintroduce route-local metrics-history fetch loops for CPU and memory
|
||||
sparklines; the infrastructure and workloads summary chart routes already own
|
||||
that chart contract.
|
||||
The shared density map now follows that same owner split.
|
||||
`frontend-modern/src/components/shared/DensityMap.tsx` stays the render shell,
|
||||
`frontend-modern/src/components/shared/useDensityMapState.ts` owns hover
|
||||
signals, canvas draw lifecycle, and resize handling, and
|
||||
`frontend-modern/src/components/shared/densityMapModel.ts` owns bucket/window
|
||||
math, hover target selection, focused-series tooltip detail, and density-cell
|
||||
opacity rules. Future density-map work should extend those owners instead of
|
||||
pushing canvas lifecycle, tooltip shaping, or chart math back into the shared
|
||||
shell.
|
||||
The shared trial banner is retired for self-hosted v6 GA. Future commercial
|
||||
notification work must start from the explicit Plans, hosted, activation,
|
||||
recovery, or support surfaces rather than reviving a global authenticated-shell
|
||||
@@ -5272,7 +5283,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
|
||||
@@ -5787,7 +5810,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,
|
||||
@@ -6172,14 +6203,6 @@ when local login is hidden behind SSO, and the `/docs/...` public-route contract
|
||||
keeps that destination readable without an authenticated session. The login
|
||||
surface must remain guidance-only: it may not expose secrets, imply an email
|
||||
reset flow, or create a browser-side authentication bypass.
|
||||
The shared summary strip primitives now follow that same owner split.
|
||||
`frontend-modern/src/components/shared/SummaryPanel.tsx` and
|
||||
`frontend-modern/src/components/shared/SummaryMetricCard.tsx` stay the render
|
||||
shells for summary-frame spacing and card density, while monitoring surfaces
|
||||
such as recovery, infrastructure, workloads, and storage only choose from the
|
||||
owned shared density modes instead of forking summary spacing with feature-
|
||||
local padding hacks. Future summary-density work should extend those shared
|
||||
primitives rather than hard-coding compact card chrome inside one surface.
|
||||
The shared tooltip now follows that same owner split.
|
||||
`frontend-modern/src/components/shared/Tooltip.tsx` stays the render shell and
|
||||
singleton API boundary, `frontend-modern/src/components/shared/useTooltipState.ts`
|
||||
@@ -6378,11 +6401,6 @@ That same shared table boundary now owns CSP-safe fill rendering for metric
|
||||
bars: `frontend-modern/src/components/shared/ProgressBar.tsx` must render fill
|
||||
width through DOM attributes rather than inline width styles that break the
|
||||
public demo CSP.
|
||||
That same shared-boundary rule applies to summary density. The shared compact
|
||||
mode on `SummaryPanel.tsx` and `SummaryMetricCard.tsx` exists for genuinely
|
||||
dense monitoring surfaces, but pages that are trying to align with the normal
|
||||
Pulse monitoring scan path should stay on the default shared density instead of
|
||||
using page-local compact overrides by habit.
|
||||
That same recovery shell boundary now also owns one canonical top-level filter
|
||||
controller in
|
||||
`frontend-modern/src/features/recovery/useRecoverySurfaceState.ts`. Route-backed
|
||||
@@ -6440,20 +6458,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
|
||||
@@ -6663,6 +6667,11 @@ icon/content layout, and action-link chrome. The
|
||||
outdated-sensor notices, and the
|
||||
`platform-inline-notice-local-amber-shell` pattern guard blocks future
|
||||
`platformPage` files from reintroducing page-local amber notice shells.
|
||||
A notice's render predicate reads what its page hydrates on every tab the
|
||||
notice renders on. `collectOutdatedSensorSetupNodes` lists the PVE nodes that
|
||||
carry the registry's `proxmox.sensorSetupOutdated` verdict and takes no disk
|
||||
rows, because only the Proxmox Storage tab loads them; a predicate over them
|
||||
left the notice silent on Overview.
|
||||
|
||||
Alert incident-event filter containers, labels, and chips must now route
|
||||
through the shared presentation helpers in
|
||||
@@ -7172,6 +7181,12 @@ shell stays in `frontend-modern/src/features/alerts/OverviewTab.tsx`, while
|
||||
alert stats, filtered ordering, and single/bulk acknowledge runtime behavior.
|
||||
Future overview control flow should extend that hook rather than restoring
|
||||
action timers or acknowledge mutations to the tab shell.
|
||||
The overview follows the shared-clock rule for relative ages: the alert card
|
||||
reads `useRelativeTimeNow` for its started age and the live reading's stale
|
||||
cut-off, and the hook reads it for the Last 24 Hours count. A hook-local
|
||||
minute signal used as "now" is a table-local interval under that rule; the
|
||||
hook keeps a minute interval only to refresh delivery diagnoses, a server read
|
||||
whose cadence is not tied to the age clock.
|
||||
Render-heavy overview ownership now lives in
|
||||
`frontend-modern/src/features/alerts/AlertOverviewStatsCards.tsx`,
|
||||
`frontend-modern/src/features/alerts/AlertOverviewActiveAlertsSection.tsx`,
|
||||
@@ -7894,10 +7909,12 @@ renders above the chart.
|
||||
TrueNAS disk rows follow it too: the drawer's Temperature row reads
|
||||
"(last known)" with the reason as title and no heat tone, and the one-line
|
||||
summary leaves a retained reading out because it has no room for the reason.
|
||||
The Proxmox outdated sensor setup notice (`features/platformPage/sensorSetup.ts`)
|
||||
counts a SATA or SAS disk as having a temperature only when the reading is
|
||||
current, so a retained one cannot hide the notice for a disk Pulse cannot read
|
||||
now.
|
||||
The Proxmox outdated sensor setup notice reads the registry's per-node
|
||||
`proxmox.sensorSetupOutdated` verdict, which counts a SATA or SAS disk as having
|
||||
a temperature only when the reading was collected now
|
||||
(`diskinventory.TemperatureCollected` in
|
||||
`internal/unifiedresources/proxmox_sensor_setup.go`), so a retained one cannot
|
||||
hide the notice for a disk Pulse cannot read now.
|
||||
|
||||
The focused browser proofs are
|
||||
`frontend-modern/src/features/patrol/__tests__/patrolRunAcceptance.test.ts`,
|
||||
|
||||
@@ -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
|
||||
@@ -1899,11 +1922,33 @@ a live report or poll already past its mock-mode check when mock mode is
|
||||
switched on can still evaluate after that clear, and its alerts can persist
|
||||
until mock mode is left.
|
||||
|
||||
The fence and the clear are per monitor while the mode is process-wide. A
|
||||
tenant monitor other than the one `SetMockMode` was called on neither drains
|
||||
nor clears, so fixture alerts it raised outlive the switch as they did before
|
||||
the fence; switching mock mode with several tenant monitors running is not
|
||||
covered.
|
||||
The mode is process-wide while each monitor keeps its own fence, alert manager,
|
||||
fixture agents and state, and the server runs one monitor per organization, so
|
||||
`SetMockMode` switches every running monitor, whichever tenant's monitor it is
|
||||
called on (the mock-mode API calls the requesting tenant's, the demo-fixture
|
||||
licence sync the default organization's). `Start` joins a process-wide set of
|
||||
running monitors under `mockModeSwitchMu`, and leaves it when its loop returns.
|
||||
A switch flips the mode once, then ends each monitor's epoch, waits for its
|
||||
admitted calls, clears its alerts, forgets its fixture agents when leaving mock
|
||||
mode and resets its state, and only then restarts the mode-dependent runtime of
|
||||
each monitor in the set. `Start` sets its runtime context before it joins and
|
||||
chooses the mock metrics sampler or discovery while it joins, under the same
|
||||
lock, so no switch lands between reading the mode and starting the runtime for
|
||||
it, and a switch that reaches a monitor not yet joined leaves the runtime to
|
||||
its `Start`. A monitor that missed switches between `New` and `Start` first
|
||||
leaves what it holds: `New` records the mode and the number of switches made so
|
||||
far, and `Start` compares both, so even a round trip that brought the flag back
|
||||
drops the fixture alerts a read path raised meanwhile, as well as the live
|
||||
alerts `New` restored outside mock mode. A monitor that is not running is
|
||||
switched only when the switch is made through it; a call for the mode the flag
|
||||
already holds still moves any reached monitor whose alerts belong to the other
|
||||
mode, and leaves the rest alone.
|
||||
|
||||
Release builds reach several tenant monitors in mock mode only on an instance
|
||||
running `DemoMode` with the `demo_fixtures` entitlement (release
|
||||
`ValidateEnablement` refuses mock fixtures otherwise) and with more than one
|
||||
organization's monitor running; the licence sync switches only for the
|
||||
default organization.
|
||||
|
||||
The proofs park a real evaluation at a fixed point, switch modes and release
|
||||
it. `TestLeavingMockModeRefusesTheRestOfAnInFlightPass` holds a mock pass at its
|
||||
@@ -1933,7 +1978,25 @@ previous-state carry returns fixture nodes or guests. All eight are in
|
||||
`TestStoreRefreshTakesItsMockModeScopeBeforeReadingState` in
|
||||
`internal/monitoring/canonical_guardrails_test.go` rejects a call that reads
|
||||
state in an argument ahead of `m.mockModeFence.begin()` and pins the
|
||||
end-epoch-then-clear order in `SetMockMode`.
|
||||
end-epoch-then-clear order in `endMockModeEpoch`, which `SetMockMode` runs for
|
||||
every monitor it reaches.
|
||||
|
||||
`TestLeavingMockModeClearsEveryRunningTenantMonitor` and
|
||||
`TestEnteringMockModeClearsEveryRunningTenantMonitor` start the default and one
|
||||
other tenant monitor through `MultiTenantMonitor`, wait until both have joined,
|
||||
raise alerts in both, switch through one of them and check both. Against a
|
||||
switch that reached only its own monitor, the default monitor kept its fixture
|
||||
alerts (69 to 104 across runs) and all 69 fixture agents after leaving mock
|
||||
mode through the other tenant, and that tenant kept its live alert after mock
|
||||
mode was entered through the default monitor. `TestMonitorStartedAfterAMockModeSwitchLeavesTheModeItWasBuiltIn`
|
||||
fails if `Start` joins without leaving the mode its monitor was built in, and
|
||||
`TestSetMockModeAlignsAMonitorWhoseModeTheFlagAlreadyHolds` fails if a call for
|
||||
the flag's current mode returns without moving such a monitor, and
|
||||
`TestMockModeSwitchLeavesAnUnjoinedMonitorsRuntimeToItsStart` fails if a switch
|
||||
starts the runtime of a monitor whose `Start` has not joined, and
|
||||
`TestMonitorStartedAfterAMockModeRoundTripDropsWhatItRaisedMeanwhile` fails if
|
||||
`Start` compares only the mode. All six are in
|
||||
`internal/monitoring/monitor_mock_alerts_test.go`.
|
||||
|
||||
The disk evaluation runs on the mock alert tick, not the poller's disk
|
||||
interval (five minutes by default, `PhysicalDiskPollingMinutes` per instance).
|
||||
@@ -2636,7 +2699,18 @@ truthfulness, not native thaw, containing-release or workload acceptance.
|
||||
ingestion so canonical host IDs stay stable across restarts (see the
|
||||
unified-resources contract's durable identity-pin obligation). Rebuild
|
||||
paths added to the adapter must keep that persistence step; ephemeral
|
||||
snapshot-bridge adapters stay read-only.
|
||||
snapshot-bridge adapters stay read-only. The rebuild, the live
|
||||
supplemental refresh and the read-state overlay ingest records with the
|
||||
adapter's configured stale thresholds, because record ingest joins
|
||||
operator links and the freshness gate of every metric merge, a link's or
|
||||
a source's into an existing row, reads them (unified-resources contract,
|
||||
"Operator links reach record-ingested resources"). Regression
|
||||
coverage:
|
||||
`TestMonitorAdapterJoinsLinkedRecordsWithConfiguredStaleThresholds` and
|
||||
`TestMonitorAdapterSourceMergesUseConfiguredStaleThresholds` in
|
||||
`internal/unifiedresources/monitor_adapter_read_state_test.go` and
|
||||
`TestManualLinkToSupplementalGuestHoldsWithAndWithoutContinuity` in
|
||||
`internal/monitoring/issue1913_host_continuity_test.go`.
|
||||
|
||||
11. The TrueNAS provider projects pools with `Storage.Topology` fixed to
|
||||
`pool` and the ZFS data vdev layout in `Storage.VDevLayout`. The
|
||||
@@ -3175,7 +3249,20 @@ Monitoring retains one current status per target and location, using server
|
||||
receipt time for remote freshness while keeping the agent-authored observation
|
||||
time as evidence metadata. A stale or disconnected agent path derives as
|
||||
indeterminate at read time; slow or fast agent clocks cannot manufacture or
|
||||
conceal a disconnect. Aggregation is conservative: all reachable paths are
|
||||
conceal a disconnect. A local path beside other locations that has not run
|
||||
within the same window derives as indeterminate too ("no recent local check").
|
||||
The target takes its freshness from its newest location, so otherwise a
|
||||
failing remote report kept an old local pass counted as reachable and the
|
||||
check read available with current evidence. For the same reason an available
|
||||
check's evidence lasts only as long as its newest reachable path, not a
|
||||
fresher path that failed. When the external-probe entitlement lapses, a
|
||||
single remote result stands in for the local location until the local poller
|
||||
runs, with the earlier of its agent check time and the server's receipt time
|
||||
as its check time, so a fast agent clock cannot keep it current. An aggregate
|
||||
over several locations never stands in: a target without a retained local
|
||||
observation reads as never checked, and not connected, until the local poller
|
||||
runs.
|
||||
Aggregation is conservative: all reachable paths are
|
||||
healthy, mixed reachable and failed/unknown paths are degraded, all current
|
||||
paths unreachable are unavailable, and no reachable path with incomplete or
|
||||
indeterminate coverage is unknown. Only aggregate unavailability advances the
|
||||
@@ -4051,6 +4138,14 @@ ambiguous. Direct SATA, SAS, and NVMe device fallback IDs retain their legacy
|
||||
shape, while multiple controller members behind one block path add their
|
||||
controller target to the fallback identity. Per-member I/O must never inherit
|
||||
an aggregate controller counter.
|
||||
A linked node's agent with no SMART reading for a device, such as a member's
|
||||
standby row, files that device's I/O under the Proxmox disk's metrics target
|
||||
(`proxmoxPhysicalDiskMatchesForLinkedNode` reads `MetricResourceID`). For a
|
||||
member merged with its Proxmox row that target is the member's own SMART key,
|
||||
scoped to the member once (`PhysicalDiskMetaMetricID`), so its SMART and I/O
|
||||
history share one series. The target used to carry the member topology twice,
|
||||
and I/O already stored under that doubled key is left to age out rather than
|
||||
migrated. Proof: `TestIdentitylessControllerMembersReadTheirWritersHistory`.
|
||||
The same rules apply to SATA and NVMe inventory: direct-disk source IDs keep
|
||||
their historical shape, controller-member IDs add their member target, and
|
||||
cross-source correlation is scoped to the canonical parent node. A successful
|
||||
@@ -4430,6 +4525,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`,
|
||||
@@ -4737,6 +4839,37 @@ disk history model, and mock seeding plus live mock ticks in
|
||||
`internal/monitoring/mock_metrics_history.go` must append to that same disk
|
||||
timeline instead of creating a second drawer-only or mock-only disk history
|
||||
path.
|
||||
The SMART-resolved id is the serial the disk resource carries: when a SMART row
|
||||
reports none, `HostSMARTDiskMetricID` takes the one the host's Unraid inventory
|
||||
reports for that disk, as the unified-resources adapter does, unless the Unraid
|
||||
row does not describe the disk (`unraidDiskDescribesSMARTRow`: a controller
|
||||
member, or several SMART rows on the disk's device path), and
|
||||
`hostDiskIOMetricResourceID` keys
|
||||
a device with no non-standby SMART row by its Unraid serial
|
||||
(`HostUnraidDeviceMetricID`, which refuses a device several Unraid rows name)
|
||||
before the linked Proxmox node's disks and the `<host>:<device>` fallback.
|
||||
Controller members sharing a device path keep distinct keys, so the I/O
|
||||
writer's aggregate-device guard, which compares every row on the path by its
|
||||
own identity (standby rows and legacy `sdc [sat]` labels included), files
|
||||
their counter under no member.
|
||||
Proof:
|
||||
`TestAgentDiskHistoryFollowsTheSerialItsUnraidRowReports` and
|
||||
`TestHostDiskIOMetricResourceIDFallbacks`.
|
||||
The agent's `smart_temp` series holds the reading the disk resource shows,
|
||||
not only the SMART row's own. `writeHostSMARTMetrics` writes
|
||||
`HostSMARTDiskTemperature`, which falls back to the host's Unraid inventory
|
||||
reading when the row has none, and `writeHostUnraidDiskTemperatures` writes
|
||||
`HostUnraidDiskTemperature` under `HostUnraidDiskMetricID` for each Unraid row
|
||||
whose key is not one of the host's SMART rows' keys, such as a `--disk-exclude`
|
||||
member, so an Unraid reading never overwrites a SMART row's own at the same
|
||||
timestamp.
|
||||
Unraid rows sharing a key are one disk in the registry, which shows the latest
|
||||
of their readings, so the last row with a reading decides that key's single
|
||||
sample. `diskinventory.TemperatureCollected` keeps a spun-down disk's or an
|
||||
expired host's leftover reading out of history.
|
||||
Proof:
|
||||
`TestAgentDiskChartsTheUnraidTemperatureItShows` and
|
||||
`TestApplyHostReportChartsUnraidTemperatureOfDiskWithoutSMART`.
|
||||
That same monitoring-owned disk-health boundary also includes shared storage
|
||||
risk assessment in `internal/storagehealth/`. When providers or host agents
|
||||
emit structured storage topology such as Unraid per-disk state, the shared
|
||||
|
||||
@@ -1512,15 +1512,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
|
||||
@@ -2687,6 +2701,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
|
||||
|
||||
|
||||
@@ -1507,7 +1507,6 @@ change may globally weaken the Task 03 lifecycle-state idempotency invariant.
|
||||
default filter-option labels must flow through the shared all-option
|
||||
presentation helper via the workload filter config model instead of the
|
||||
hot-path shell hard-coding local `All ...` strings.
|
||||
28. Keep summary-card hover emphasis on one bounded rendering budget: when a summary row is active, shared sparkline and density-map primitives must promote the selected series and demote background series through the same active-series ID rather than layering a second page-local highlight pass, so zoom-range and hover scrubbing stay visually coherent without reintroducing multi-series overdraw on the hot summary cards. Density maps on that hot path must stay overview-first under focus: preserve the multi-entity heatmap rows, layer focused-entity detail inside the card, and avoid swapping transient hover into a separate single-series chart path.
|
||||
29. Keep public self-hosted checkout handoff endpoints on the adjacent
|
||||
commercial/router boundary, not the summary-chart hot path. When
|
||||
`internal/api/router.go`, `internal/api/router_routes_cloud.go`, or
|
||||
@@ -1529,7 +1528,8 @@ them with new hot-path fetches. Node rows that draw infrastructure history
|
||||
must read the canonical infrastructure-summary route owned by
|
||||
`internal/api/router_routes_monitoring.go` and `internal/api/router.go`, today
|
||||
through `frontend-modern/src/components/Workloads/useWorkloadTableMetricHistory.ts`;
|
||||
storage summary cards must use the storage summary cache owners. The authenticated
|
||||
the storage pool Growth column must read through the storage summary cache
|
||||
owners (`useStorageSummaryCharts.ts` and `storageSummaryCache.ts`). The authenticated
|
||||
root path in `frontend-modern/src/App.tsx` must not prewarm a deleted
|
||||
dashboard-specific overview, trend, or summary transport.
|
||||
The same hot path must keep mock/demo chart identity on the canonical
|
||||
@@ -2262,22 +2262,11 @@ Route-backed workload `resource` focus on that hot path is contextual state
|
||||
only, not inferred filter state: opening or closing an inline drawer must not
|
||||
invent, retain, or clear `agent` or node-scope filters unless those filters
|
||||
were already explicit in the managed workload URL.
|
||||
That same hot-path ownership now covers top-of-page summary emphasis: infrastructure
|
||||
and workloads summary cards must treat row hover, chart hover, and route focus
|
||||
as one shared active-series contract so time-range switches, row scrubbing, and
|
||||
chart cross-highlighting reuse one existing chart path instead of repainting
|
||||
page-local “selected row” overlays on top of already downsampled summary
|
||||
history. Hovering a sparkline or density map for one entity must promote that
|
||||
entity into the shared active series so sibling cards highlight the same object
|
||||
at once rather than maintaining chart-local hover state, and the synchronized
|
||||
hover timestamp must remain visible across those sibling cards even when the
|
||||
active entity has no samples for one metric in the current range. Those
|
||||
sibling cards should expose the synchronized value through one compact
|
||||
header-level readout, not by spawning duplicate floating tooltips on every
|
||||
chart.
|
||||
Infrastructure cluster-header hover now belongs to that same bounded hot path:
|
||||
hovering a grouped infrastructure header must scope the top cards to that
|
||||
cluster's unified-resource members through the shared group/entity interaction
|
||||
That same hot-path ownership covers summary emphasis. No top-of-page summary
|
||||
cards render on infrastructure or workloads today; if they return, row hover,
|
||||
chart hover, and route focus must stay one shared active-series contract that
|
||||
reuses one existing chart path instead of repainting page-local “selected row”
|
||||
overlays on top of already downsampled summary history.
|
||||
That same hot path now also owns dashboard freshness discipline. Supported
|
||||
unified-resource dashboard reads may hydrate from REST for first paint, but
|
||||
once websocket `state.resources` is available they must consume that canonical
|
||||
@@ -2414,21 +2403,20 @@ must not tighten physical retention until the durable downgrade state reaches
|
||||
its governed day-60 purge timestamp. Re-upgrade clears that ceiling before the
|
||||
next retention run. The ceiling may only reduce configured retention; it must
|
||||
never expand an operator's shorter storage policy.
|
||||
contract instead of inventing an infrastructure-local summary filter branch.
|
||||
For shared line charts on that hot path, the shared sparkline primitive may
|
||||
isolate the selected series inside the existing render budget, but that
|
||||
isolation must still reuse the same summary series set and timeline data rather
|
||||
than triggering a second page-local chart recomputation.
|
||||
That same hot-path rule now covers contextual row focus on those pages.
|
||||
That same hot-path ownership now also forbids hover-driven table movement.
|
||||
When a summary chart promotes one active entity, the matching row may highlight
|
||||
in place if it is already mounted and visible, but the hot path must not
|
||||
auto-scroll the page or rebuild the table into a one-row filtered view on
|
||||
transient hover. Off-screen reveal must stay behind an explicit `Jump to row`
|
||||
action routed through the shared summary-table focus bridge.
|
||||
That same hot-path contract also owns the row-emphasis paint. Dashboard guest
|
||||
rows and infrastructure resource rows must expose summary-linked activity
|
||||
through the shared `data-summary-row-active` marker and let the shared frontend
|
||||
That same hot-path ownership forbids hover-driven table movement on the
|
||||
workloads and storage tables. Row hover may highlight the matching row in
|
||||
place, but the hot path must not auto-scroll the page or rebuild the table
|
||||
into a one-row filtered view on transient hover; only a deliberate row focus
|
||||
may reveal an off-screen row, through the shared summary-table focus bridge.
|
||||
The bridge measures row position only inside that deliberate reveal. It must
|
||||
not keep window-level scroll or resize listeners, or re-measure the hovered
|
||||
row on every hover change and scroll frame, for an off-screen affordance that
|
||||
nothing renders: the retired `Jump to row` visibility check did exactly that,
|
||||
costing 128 `getComputedStyle` calls across eight row hovers and 320 across
|
||||
ten scroll steps on the 12-node mock storage table, and none once removed.
|
||||
That same hot-path contract also owns the row-emphasis paint. Workload guest
|
||||
and group rows and storage pool, group, and disk rows must expose
|
||||
summary-linked activity through the shared `data-summary-row-active` marker and let the shared frontend
|
||||
primitive render the emphasis, instead of layering lane-local row-fill classes
|
||||
that diverge across pages or wash out inline metric bars.
|
||||
`frontend-modern/src/components/Workloads/useWorkloadSelectionState.ts` must
|
||||
@@ -2436,14 +2424,7 @@ write workload selection back into the workloads route through the shared
|
||||
same-path route-state scheduler, but the actual shell-position handoff for
|
||||
query-only row focus must go through `frontend-modern/src/utils/appShellScrollRestoration.ts`
|
||||
plus the root `frontend-modern/src/App.tsx` shell so opening a focused
|
||||
workload does not look like a full page reload, and the governed infrastructure and
|
||||
workloads summary surfaces must keep the summary page-scoped while that focus
|
||||
reuses the shared highlight contract; density maps may retain page-level
|
||||
context, but they must now also surface focused-entity detail inside the same
|
||||
card instead of dimming the rest of the map into unusable background noise or
|
||||
swapping the hot path into a transient single-series chart. Line-card
|
||||
isolation must still flow through the shared sparkline runtime instead of a
|
||||
page-local focus overlay. The shared same-path scheduler must also own cleanup
|
||||
workload does not look like a full page reload. The shared same-path scheduler must also own cleanup
|
||||
for every deferred scroll-restore timeout and animation frame it creates, so
|
||||
route-state cleanup cannot leave hot-path replay work running after the owning
|
||||
surface unmounts.
|
||||
@@ -2452,12 +2433,13 @@ focus may mount drawer-local facet and intelligence fetches, but those
|
||||
requests must retain the mounted page shell through the shared non-suspending
|
||||
query helper instead of bubbling a transient `Loading view...` fallback through
|
||||
`AppLayout.tsx` when an inline infrastructure drawer opens.
|
||||
That same hot-path rule now has one shared runtime boundary. Interactive-series
|
||||
filtering, active-series derivation, focused-label lookup, and local
|
||||
scroll-preserving row focus must extend
|
||||
`frontend-modern/src/components/shared/contextualFocus.ts` instead of
|
||||
rebuilding page-local `Set` scans or scroll repair logic in dashboard,
|
||||
infrastructure, or workloads hot paths.
|
||||
That same hot-path rule now has one shared runtime boundary. Local
|
||||
scroll-preserving row focus and inline-detail reveal must extend
|
||||
`frontend-modern/src/components/shared/contextualFocus.ts`, and active-series
|
||||
derivation must stay in
|
||||
`frontend-modern/src/components/shared/summaryCardInteraction.ts` behind
|
||||
`summaryTableFocus.ts`, instead of rebuilding page-local `Set` scans or scroll
|
||||
repair logic in workloads, storage, or platform-table hot paths.
|
||||
That same hot-path ownership now also covers deliberate inline-detail reveal.
|
||||
When a focused workload or infrastructure row opens its inline detail, the hot
|
||||
path may preserve scroll across same-route state writes, but the actual reveal
|
||||
@@ -2471,8 +2453,9 @@ fold, marking that movement as deliberate so route-state restore does not
|
||||
replay over it and then scrolling only enough to keep the row header plus the
|
||||
top of the detail visible instead of hard-centering every expansion.
|
||||
That same hot-path ownership now includes summary cache invalidation. A
|
||||
summary chart cache that hydrates charts for fast remounts, today the storage
|
||||
page's module-scoped `frontend-modern/src/utils/storageSummaryCache.ts`, must
|
||||
summary history cache that hydrates fast remounts, today the storage page's
|
||||
module-scoped `frontend-modern/src/utils/storageSummaryCache.ts` behind the
|
||||
pool Growth column and its sort, must
|
||||
carry the chart contract version in its key and advance it when the summary
|
||||
chart timeline contract changes, so a hot-reloaded or long-lived browser tab
|
||||
cannot rehydrate an older series set before the next live fetch completes.
|
||||
@@ -2720,6 +2703,41 @@ must remain distinguishable without hover. Workloads supplies this row metadata,
|
||||
not local table CSS or a duplicate renderer; non-filesystem rows keep the compact
|
||||
default. Verification covers the opt-in at this composition boundary and actual
|
||||
long-path wrapping in the shared renderer's browser proof.
|
||||
A guest's linked Pulse Agent can stop reporting while Proxmox keeps polling the
|
||||
VM. `useWorkloads` copies `resource.agent.stale` onto the row as `agentStale`,
|
||||
and sets `disksFromAgent` when Proxmox reported no guest filesystems and the
|
||||
agent's own `disks` filled in. When that agent is stale, `GuestDrawerOverview.tsx`
|
||||
reads the Guest RAID card as last known through the shared `RaidCard`
|
||||
`lastKnownReason` prop with `HOST_AGENT_STOPPED_REPORTING_REASON`, puts a status
|
||||
line over muted Network rows, reads agent-supplied Filesystems rows through the
|
||||
existing `last-known` disk read state, and marks the Pulse Agent row as stopped
|
||||
reporting. Agent-supplied filesystem rows follow the agent's freshness rather
|
||||
than the Proxmox `diskStatusReason`, which describes Proxmox's own guest reads,
|
||||
so a live agent's rows keep their usage and bars. Those rows read as
|
||||
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 (`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
|
||||
@@ -2767,7 +2785,7 @@ the filter and presentation rails wrap-capable instead of forcing a single
|
||||
no-wrap row that clips trailing actions. The workload shell leaves Type,
|
||||
Status, dynamic scope, and contextual `Clear filters` actions on
|
||||
the primary filter rail. Durable presentation choices — grouped/list mode,
|
||||
bars/trends, chart visibility, Guest/Host memory basis, and Columns — pass
|
||||
bars/trends, Guest/Host memory basis, and Columns — pass
|
||||
their panel content through `FilterBar.viewOptions` instead of importing or
|
||||
wrapping `ViewOptionsDisclosure` in the workload feature or rendering as
|
||||
equally prominent filter toggles. The shared FilterBar owns the inline View
|
||||
@@ -2790,10 +2808,13 @@ label so the values are not an orphaned set of durations. Columns must expand
|
||||
as a full-width responsive checkbox row inside the View disclosure instead of
|
||||
opening a nested absolute panel or a tall single-column desktop list. On
|
||||
Proxmox, the non-default Host basis must remain visible in the workload memory
|
||||
column header after the View disclosure closes. Workload chart
|
||||
visibility remains a display preference: its menu action must expose explicit
|
||||
`Show charts` / `Hide charts` pressed state, and hiding charts must remove the
|
||||
summary section rather than leaving an empty collapsed summary band on screen.
|
||||
column header after the View disclosure closes. No workload summary chart
|
||||
section renders today, so the chart-visibility control in
|
||||
`frontend-modern/src/components/Workloads/WorkloadsFilter.tsx` stays hidden
|
||||
(no surface passes `onChartsToggle`). If a summary section returns, that
|
||||
control must expose explicit `Show charts` / `Hide charts` pressed state, and
|
||||
hiding charts must remove the summary section rather than leaving an empty
|
||||
collapsed summary band on screen.
|
||||
The Workloads-owned filter-config assembly now lives in
|
||||
`frontend-modern/src/components/Workloads/useWorkloadsState.ts`, so future
|
||||
filter runtime changes must extend through those owners instead of
|
||||
|
||||
@@ -3025,6 +3025,18 @@ Removing the disconnected recorder does not alter alert, action approval or
|
||||
operator authority. An unrelated organization receives no default archive
|
||||
fallback.
|
||||
|
||||
### Report GETs cannot spend AI budget
|
||||
|
||||
`Router.wireReportingAIResolvers` in `internal/api/router.go` wires a second
|
||||
reporting resolver, `SetExistingFindingsResolver`, backed by
|
||||
`AISettingsHandler.ExistingAIService`.
|
||||
A `GET /api/admin/reports/generate` uses only that resolver and the
|
||||
deterministic narrator, so it makes no AI provider call, writes no cost-ledger
|
||||
entry and never constructs a tenant AI service. GET passes the demo-mode guard
|
||||
and the CSRF check, and a SameSite=Lax session cookie rides a cross-site
|
||||
top-level navigation, so AI narration of a single-resource report now requires
|
||||
a CSRF-checked `POST`. No authentication, scope or session rule changed.
|
||||
|
||||
### Node setup action v7 compatibility
|
||||
|
||||
Workflow consumers pin `actions/setup-node` to
|
||||
|
||||
@@ -944,6 +944,15 @@ Patrol coverage, schedule, finding, and approval state belongs on Patrol-owned
|
||||
surfaces or explicit Patrol affordances; Proxmox overview must not treat it as
|
||||
backup coverage, restore readiness, PBS verification/protection proof, or a
|
||||
replacement for the Proxmox Backups tab and workload Backup column.
|
||||
The outdated sensor setup notice above the Proxmox tabs is temperature
|
||||
guidance, not storage or recovery evidence. It reads the registry's per-node
|
||||
`proxmox.sensorSetupOutdated` verdict from the node rows the active tab already
|
||||
loads (Overview, Storage, Replication and Backups load nodes; Ceph and Mail do
|
||||
not, so it does not render there), so the Overview query must not add
|
||||
`physical_disk` rows to feed it; the derived `ProxmoxData` flag opens no storage
|
||||
or recovery path.
|
||||
`ProxmoxPageSurface.contract.test.tsx` pins the notice on Overview with no disk
|
||||
rows and an Overview query without `physical_disk`.
|
||||
The Proxmox inline estate totals are likewise an adjacent frontend-primitives
|
||||
and unified-resource projection, not backup or recovery evidence.
|
||||
`ProxmoxPageSurface` must flow the already-loaded workload inventory into the
|
||||
@@ -1404,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
|
||||
@@ -2770,11 +2785,9 @@ canonical pre-handler authorization proof covering both transfer routes.
|
||||
top-level GitHub install.sh SERVER installer, so a correct local script
|
||||
(signed or not) is always preferred over a wrong-identity proxied one on the
|
||||
unverified curl-piped-into-bash agent path (issue #1470).
|
||||
15. Keep storage summary chart identity and sticky-shell behavior on the
|
||||
shared storage path. Pool rows, disk rows, storage summary cards, and
|
||||
storage detail charts must all address history through the canonical
|
||||
unified-resource metrics-target IDs, and the storage page must reuse the
|
||||
shared sticky summary primitive instead of a storage-local scroll wrapper.
|
||||
15. Keep storage chart identity on the shared storage path. Pool rows, disk
|
||||
rows, the pool Growth column, and storage detail charts must all address
|
||||
history through the canonical unified-resource metrics-target IDs.
|
||||
Expanded pool and physical-disk details must also follow the canonical
|
||||
object-detail information architecture: current configuration, topology,
|
||||
health, and SMART facts land on `Overview`; stored capacity and I/O charts
|
||||
@@ -2820,10 +2833,9 @@ canonical pre-handler authorization proof covering both transfer routes.
|
||||
likewise native Assistant tool-visibility plumbing, not MCP transport
|
||||
state and not storage/recovery execution authority.
|
||||
When operators hover or focus pools versus physical disks, the storage
|
||||
summary must reuse one resolved active-series ID across card state and
|
||||
chart highlighting so pool-only cards demote cleanly during disk focus and
|
||||
disk-temperature cards demote cleanly during pool focus, instead of
|
||||
leaving stale row-local IDs or storage-local hover branches on the page.
|
||||
page must reuse one resolved active-series ID for row emphasis in both
|
||||
views instead of leaving stale row-local IDs or storage-local hover
|
||||
branches on the page.
|
||||
Any page, group, or entity scope that becomes pinned through storage
|
||||
interaction must stay row-first: the pinned row or group remains the
|
||||
visible scoped state, and explicit clearing belongs to the shared storage
|
||||
@@ -2836,16 +2848,15 @@ canonical pre-handler authorization proof covering both transfer routes.
|
||||
`data-summary-group-member-active="preview|pinned"` state so the grouped
|
||||
block reads as one scoped set without adding storage-local outlines, pill
|
||||
buttons, or heavy full-row fills.
|
||||
18. Keep storage summary remount caches versioned with the chart contract.
|
||||
`frontend-modern/src/components/Storage/StorageSummary.tsx` may keep a
|
||||
bounded in-memory cache for same-tab remounts, but its cache key must carry
|
||||
an explicit summary contract version so long-lived demo sessions do not
|
||||
rehydrate stale pool or disk sparkline shapes after the storage summary
|
||||
chart model changes. The shared storage history owner at
|
||||
`frontend-modern/src/utils/storageSummaryCache.ts` must likewise remain
|
||||
bounded to 20 recent node/range summaries, use LRU replacement, and abort
|
||||
in-flight requests when organization ownership changes so a late response
|
||||
cannot recreate an old-scope cache entry.
|
||||
18. Keep the storage summary history cache versioned with the chart contract.
|
||||
`frontend-modern/src/utils/storageSummaryCache.ts`, which
|
||||
`frontend-modern/src/components/Storage/useStorageSummaryCharts.ts` reads
|
||||
for the pool Growth column, must carry an explicit summary contract version
|
||||
in its cache key so long-lived demo sessions do not rehydrate stale pool or
|
||||
disk series shapes after the storage summary chart model changes. It must
|
||||
also remain bounded to 20 recent node/range summaries, use LRU replacement,
|
||||
and abort in-flight requests when organization ownership changes so a late
|
||||
response cannot recreate an old-scope cache entry.
|
||||
19. Keep cross-surface workload handoffs on canonical IDs too. Shared workload
|
||||
chart transport may look up provider-backed VM history through unified
|
||||
metrics targets, but infrastructure/workloads/storage/recovery navigation
|
||||
@@ -2997,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
|
||||
@@ -3686,11 +3706,6 @@ live wire-up and persistence resync so a finding the operator
|
||||
manually closed before a restart still reads as "Resolved by you"
|
||||
afterward, instead of being misattributed to Pulse's auto-detection.
|
||||
|
||||
`StorageSummary.tsx`, `StoragePageSummary.tsx`, and `useStoragePageSummary.ts`
|
||||
now surface `poolsDegraded` and `disksFailing` health indicators alongside
|
||||
pool/disk counts. These additions project from existing websocket pool/disk
|
||||
state; they must not introduce new API polling or widen the storage-fetch
|
||||
boundary.
|
||||
Agentless availability endpoints are adjacent infrastructure context only, not
|
||||
storage or recovery inventory. Storage/recovery consumers may receive
|
||||
`network-endpoint` resources through shared unified-resource snapshots, but
|
||||
@@ -3703,9 +3718,9 @@ not Standalone Machines membership, storage ownership, repository membership,
|
||||
backup coverage, or restore authority.
|
||||
That same owned summary path now also runs through
|
||||
`useStorageSummaryCharts.ts`: the storage page owns one page-scoped summary
|
||||
range and one shared storage-summary history fetch, and both the sticky
|
||||
summary cards and per-pool growth column reuse that payload instead of
|
||||
forking separate row-local history reads or duplicate polling loops.
|
||||
range and one shared storage-summary history fetch, and the per-pool Growth
|
||||
column and its growth sort reuse that payload instead of forking separate
|
||||
row-local history reads or duplicate polling loops.
|
||||
The storage table's default `priority` order is a risk list: records with an
|
||||
open, unacknowledged alert sort first by severity (fed into
|
||||
`sortStorageRecords` through `StorageSortContext.alertWeightByRecordId` from
|
||||
@@ -4343,38 +4358,26 @@ split: storage derives the health semantics through
|
||||
Storage components and `features/storageBackups` presentation helpers must not
|
||||
recreate raw green/yellow rounded-dot classes locally.
|
||||
That same storage page ownership now also includes contextual focus behavior
|
||||
for pools and disks. Expanding a storage row may set a focused metrics-target
|
||||
ID for shared summary emphasis, but `frontend-modern/src/components/Storage/StorageSummary.tsx`
|
||||
must keep the storage summary page-scoped instead of collapsing its sparklines
|
||||
to the single expanded row or replacing the page overview with row-local empty
|
||||
states.
|
||||
That same page-scoped summary contract now also owns canonical hover-isolation
|
||||
behavior. Pool and disk rows must publish the resolved metrics-target ID into
|
||||
the shared summary contract so pool usage, used capacity, and available space
|
||||
cards can isolate the active row through the shared sparkline primitive while
|
||||
non-matching cards such as disk temperature demote to inactive context instead
|
||||
of rebuilding a row-local summary surface.
|
||||
That same shared summary contract now also owns chart-driven emphasis.
|
||||
Hovering one storage summary chart must promote the same canonical metrics
|
||||
target ID through sibling cards, so pool charts cross-highlight the same pool
|
||||
while non-matching cards such as disk temperature demote to inactive context
|
||||
instead of keeping chart-local hover state. When a sibling storage card can map
|
||||
that same entity into its own series set, it must also surface the synchronized
|
||||
value as a compact card-header readout instead of opening a second floating
|
||||
tooltip away from the pointer.
|
||||
That same storage summary contract now uses the shared contextual-focus owner.
|
||||
`frontend-modern/src/components/Storage/StorageSummary.tsx` must route
|
||||
interactive-series filtering, focused-label lookup, and active-series
|
||||
resolution through `frontend-modern/src/components/shared/contextualFocus.ts`
|
||||
so storage keeps the same page-scoped focus semantics as infrastructure and
|
||||
workloads instead of preserving a storage-local hover/focus branch.
|
||||
That same storage ownership now also governs summary-to-table reveal. Hovering
|
||||
pool or disk charts may highlight the matching row when the active view already
|
||||
shows it, but storage hover must not auto-filter or auto-scroll the table.
|
||||
When the active chart entity is off-screen or hidden behind the other storage
|
||||
view, the page must use the shared summary-table focus bridge and reveal the
|
||||
target row only through an explicit `Jump to row` action, switching views or
|
||||
expanding the owning group only for that deliberate reveal path.
|
||||
for pools and disks. Expanding a pool row or selecting a disk row sets the
|
||||
focused summary series ID and row hover sets the hovered one;
|
||||
`frontend-modern/src/components/Storage/useStoragePageModel.ts` must resolve
|
||||
both through the shared `useSummaryPageInteractionState` bridge in
|
||||
`frontend-modern/src/components/shared/summaryTableFocus.ts`, so pool, disk,
|
||||
and pool-group row emphasis keeps the same page/group/entity semantics as
|
||||
workloads instead of preserving a storage-local hover/focus branch. The
|
||||
storage summary history that `useStorageSummaryCharts.ts` loads feeds only the
|
||||
pool Growth column and its sort; no storage summary card strip renders, and
|
||||
one that returns must stay page-scoped instead of collapsing to the expanded
|
||||
row or replacing the page overview with row-local empty states.
|
||||
That same storage ownership now also governs reveal. Row hover may highlight
|
||||
the matching row in place, but storage hover must not auto-filter or
|
||||
auto-scroll the table. Reveal belongs only to deliberate focus: the bridge
|
||||
hands the focused pool or disk series to the `revealActiveSeries` callback,
|
||||
which reopens a focused pool's collapsed owning group, and a pinned pool-group
|
||||
header that sits off-screen scrolls into view through the same bridge. Focus
|
||||
only ever names a row in the active pools or disks view, so the callback never
|
||||
switches views; the view-switching branches that served the retired jump to
|
||||
the active row are gone.
|
||||
That same reveal contract now also owns inline-detail expansion. When a pool or
|
||||
disk row is deliberately focused and its inline detail opens on the storage
|
||||
page, the detail row must publish the same canonical summary series ID through
|
||||
@@ -4397,8 +4400,8 @@ instead of a separate hardcoded recovery cache, so recovery filters, rollups,
|
||||
and shared route handoffs see the same platform set as settings and
|
||||
infrastructure.
|
||||
That same graph-owned mock boundary also owns demo-readiness for storage and
|
||||
recovery surfaces. Mock summary cards, seeded history, and provider-backed
|
||||
storage/recovery counts must come from the same canonical fixture graph so
|
||||
recovery surfaces. Mock seeded history and provider-backed storage/recovery
|
||||
counts must come from the same canonical fixture graph so
|
||||
storage and recovery demos show realistic healthy-versus-attention balance
|
||||
instead of blank history, stale provider context, or page-local fixture drift.
|
||||
That same recovery-facing demo contract also owns subject readability. When
|
||||
@@ -4407,16 +4410,11 @@ subject identity shown to operators must stay human-readable
|
||||
`<cluster>/<namespace>/<pvc>` context from the canonical graph instead of
|
||||
opaque hash-like IDs that break demo trust and cross-surface recognition.
|
||||
That same adjacent chart boundary now also assumes seeded and live mock
|
||||
storage timelines are one continuous series. Disk-temperature, pool-usage,
|
||||
used-capacity, and available-space cards may consume shaped chart payloads for
|
||||
presentation, but those payloads must still reflect one canonical mock metric
|
||||
timeline instead of a seeded seven-day sparkline with a second live tail
|
||||
stitched on afterward.
|
||||
That same chart boundary now also owns row-hover summary filtering. Storage
|
||||
pool and disk rows may focus the summary cards, but the shared storage summary
|
||||
must filter every supported card through the same canonical metrics-target
|
||||
identity rather than letting temperature, capacity, or detail cards drift onto
|
||||
page-local row identifiers.
|
||||
storage timelines are one continuous series. Storage chart consumers such as
|
||||
the pool Growth column and the pool and disk History charts may consume shaped
|
||||
chart payloads for presentation, but those payloads must still reflect one
|
||||
canonical mock metric timeline instead of a seeded seven-day series with a
|
||||
second live tail stitched on afterward.
|
||||
That same shared `internal/api/` dependency also assumes auth-persistence
|
||||
teardown is synchronous when recovery-adjacent runtimes reinitialize. Session,
|
||||
CSRF, and recovery-token workers may not leave stale background goroutines or
|
||||
@@ -4854,8 +4852,8 @@ the cloud-summary decision is derived from scope rather than stored as a
|
||||
separate boolean flag.
|
||||
The same storage-facing runtime paths now also normalize org scope through
|
||||
`frontend-modern/src/utils/orgScope.ts` before building cache keys or
|
||||
multi-tenant fetch state, so Dashboard, StorageSummary, and other storage
|
||||
adjacent consumers do not each keep a local `getOrgID() || 'default'`
|
||||
multi-tenant fetch state, so `storageSummaryCache.ts` and other
|
||||
storage-adjacent consumers do not each keep a local `getOrgID() || 'default'`
|
||||
fallback.
|
||||
|
||||
The frontend storage and recovery surfaces are also first-class embedded
|
||||
@@ -4897,10 +4895,10 @@ small table chrome to return to the primary page. It must not return as an
|
||||
equal-weight subtab or a top-level posture-card action strip without a separate
|
||||
governed product decision.
|
||||
Recovery is also intentionally outside the interactive page/group/entity
|
||||
summary-card contract used by workloads, infrastructure, and storage. The
|
||||
recovery route must not adopt `summaryCardInteraction.ts`, synchronized card
|
||||
hover, row-driven summary scope, or shared `SummaryPanel` card framing simply
|
||||
because adjacent monitoring pages use those primitives. Recovery may still show
|
||||
summary-scope contract that the workloads and storage tables use. The recovery
|
||||
route must not adopt `summaryCardInteraction.ts`, `summaryTableFocus.ts`,
|
||||
row-driven summary scope, or a summary card strip simply because adjacent
|
||||
monitoring pages use those primitives. Recovery may still show
|
||||
coverage breadth and platform context inside the Protection coverage table and
|
||||
event filters, but the page must read item-first so the unified recovery model
|
||||
is not visually anchored to one platform family.
|
||||
@@ -5191,8 +5189,8 @@ recovery-local grid overrides or width hacks. Protected-items controls should
|
||||
also use the same shared `Reset all` page-controls action pattern as storage
|
||||
and workloads when visible filters are active, instead of forcing operators to
|
||||
clear each inventory filter manually.
|
||||
That same handoff should keep Recovery out of shared summary-card density
|
||||
tuning unless a new governed product decision reintroduces a first-viewport
|
||||
That same handoff should keep Recovery free of a first-viewport summary card
|
||||
strip unless a new governed product decision reintroduces a first-viewport
|
||||
summary owner. The current route should spend its top-level density budget on
|
||||
the activity section, compact controls, and one primary data card.
|
||||
That same shell rule applies to the recovery-events workspace.
|
||||
@@ -6267,6 +6265,19 @@ is in `applyLicensedFeatureConfigSnapshot`
|
||||
(`pkg/server/telemetry_licensed_features.go`), pinned by
|
||||
`TestApplyLicensedFeatureConfigSnapshot_CountsScheduledReportingAndProfiles`.
|
||||
|
||||
### Single-resource report AI narration requires POST
|
||||
|
||||
`internal/api/metrics_reporting_handlers.go` now accepts `POST` with a JSON
|
||||
body for `/api/admin/reports/generate` and lets only that method use the
|
||||
tenant's AI narrator; `GET` keeps working with query parameters and the
|
||||
deterministic summary. A narrated PDF records the same AI cost-ledger entry as
|
||||
before, and only a `POST` can now produce one. A `GET` reads Patrol findings
|
||||
only from an AI service that is already running
|
||||
(`AISettingsHandler.ExistingAIService`), so it no longer constructs a tenant AI
|
||||
service, whose construction opens that tenant's SQLite resource export store.
|
||||
Report content, backup and recovery enrichment, and tenant storage are
|
||||
otherwise unchanged.
|
||||
|
||||
### Audit-read activity is a bounded, content-free local history
|
||||
|
||||
`audit_read_activity.json` follows the same shape as the existing external-agent
|
||||
@@ -6886,3 +6897,31 @@ migration or recovery authority is added or moved.
|
||||
The performance report and reporting runtime snapshot handlers apply the same
|
||||
test before tabulating a disk temperature. That changes only which held value a
|
||||
report shows, not any storage or recovery path.
|
||||
|
||||
### 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
|
||||
`Upgrade: websocket` from the demo read-only guard. On a `DEMO_MODE` instance a
|
||||
`POST`, `PUT`, `PATCH` or `DELETE` with websocket handshake headers is now
|
||||
judged like any other write: outside the login, logout, AI execute and OIDC
|
||||
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.
|
||||
|
||||
@@ -1609,7 +1609,12 @@ Source freshness is a separate unified-resource status input, not a fixed
|
||||
global health timer. Snapshot rebuilds, resource seeding, supplemental record
|
||||
overlays, and cloned read-state overlays must preserve the monitoring-provided
|
||||
stale thresholds for Proxmox, PBS, and PMG sources so resources do not flap to
|
||||
warning/degraded between successful configured poll cycles.
|
||||
warning/degraded between successful configured poll cycles. A source merge
|
||||
into an existing row judges metric freshness by the ingest's thresholds and
|
||||
the stale pass's own threshold rule, so the merge's freshness gate never
|
||||
calls a source current that the stale pass marks stale, or the reverse. The
|
||||
gate decides only where no stronger rule does: a hypervisor-managed guest
|
||||
keeps its platform CPU even while that source is stale.
|
||||
|
||||
Service-discovery readiness is a unified-resource payload contract, not a
|
||||
drawer-local decoration. Resource list/detail payloads that expose a
|
||||
@@ -2303,6 +2308,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
|
||||
@@ -2803,7 +2818,9 @@ application resource-provider or WebSocket lifecycle.
|
||||
ID and drops the superseded pin row (`ApplyCanonicalIDSuccessions`
|
||||
in `internal/unifiedresources/canonical_id_succession.go`).
|
||||
Successions never fire while the old ID still belongs to a live
|
||||
resource, never follow a contradicting machine key (a reinstalled
|
||||
resource or to one a manual link folded into its primary (see
|
||||
"Operator links reach record-ingested resources"), never follow a
|
||||
contradicting machine key (a reinstalled
|
||||
machine mints fresh, it does not absorb the old host's operator
|
||||
state), and never rewrite change-journal rows. Known limitation:
|
||||
machine-keyless hosts whose history already merged under a collapsed
|
||||
@@ -2884,8 +2901,9 @@ application resource-provider or WebSocket lifecycle.
|
||||
`ResourceRegistry.IngestRecords` applies the same
|
||||
`ApplyCanonicalIDSuccessions` semantics as pin-driven successions
|
||||
(operator state and action audits re-key, the superseded pin row
|
||||
drops, never while the old ID belongs to a live resource, journal
|
||||
rows never rewritten). Regression coverage:
|
||||
drops, never while the old ID belongs to a live resource or one a
|
||||
manual link folded into its primary, journal rows never rewritten).
|
||||
Regression coverage:
|
||||
`TestRegistryIngestRecordsKeepsSameHostnameSystemsDistinct` /
|
||||
`TestIngestRecordsSucceedLegacyHostnameScopedCanonicalIDs` /
|
||||
`TestIngestRecordsDoNotCompleteTrueNASIdentityFromPins` in
|
||||
@@ -2987,6 +3005,36 @@ or force every provider type back into the active route payload. Docker,
|
||||
Kubernetes, TrueNAS, and VMware surface contract tests pin their route query
|
||||
maps, and the shared hook test pins facet normalization.
|
||||
|
||||
### Proxmox node verdicts over disks arrive on the node
|
||||
|
||||
The Proxmox page hydrates one query per workflow, and only its Storage tab
|
||||
loads `physical_disk` rows, so a node verdict that depends on the node's disks
|
||||
is derived in the registry and carried on the node, never rebuilt in the page
|
||||
from disk rows. `ProxmoxData.SensorSetupOutdated` (`proxmox.sensorSetupOutdated`
|
||||
on REST and websocket rows) is that verdict for the outdated sensor setup
|
||||
notice. `refreshProxmoxSensorSetupLocked`
|
||||
(`internal/unifiedresources/proxmox_sensor_setup.go`) runs after
|
||||
`buildChildCounts` on every ingest path and gives every Proxmox node an
|
||||
explicit true or false (other resources omit it), because browser facet merges
|
||||
read an omitted field as a partial snapshot and would keep an earlier true. It
|
||||
is true when the node's temperature payload is available and in the legacy
|
||||
`sensors -j` format (`models.Temperature.LegacySensorsFormat`) and a disk whose
|
||||
canonical parent is the node has no reading collected now
|
||||
(`diskinventory.TemperatureCollected`) and a type whose temperature a PVE node
|
||||
gets only through SMART: `sata`, `sas`, or the `hdd` and `ssd` form factors
|
||||
Proxmox's own inventory reports. Proxmox also types a non-rotational USB device
|
||||
`ssd`, so a USB device without SMART counts. A retained reading does not count
|
||||
as current, including one kept while a host agent reports the disk in standby;
|
||||
a reading a linked host agent collects now clears the disk. NVMe disks never
|
||||
set it, because kernel hwmon reports them even on a legacy setup, and a host
|
||||
agent the registry has not linked to the node contributes no disks. The
|
||||
verdict reflects the registry's node and disk state at each ingest.
|
||||
`TestProxmoxNodeSensorSetupOutdatedFromItsDisks` and
|
||||
`TestProxmoxNodeSensorSetupOutdatedFollowsWhereTheReadingArrives` in
|
||||
`internal/unifiedresources/registry_test.go` pin the rule, the linked-agent
|
||||
case, the explicit false on the wire and the presented JSON;
|
||||
`resourceStateAdapters.test.ts` pins that a false clears a merged true.
|
||||
|
||||
### Canonical REST facets preserve realtime workload evidence
|
||||
|
||||
The frontend REST projection retains the complete source-authored Proxmox
|
||||
@@ -3431,6 +3479,20 @@ It owns probe status, incidents, history, evidence, and the outgoing `checks`
|
||||
relationship. The matched resource carries every correlated check in the canonical
|
||||
`availabilityChecks` facet, keyed by saved target id, while `availability`
|
||||
remains an additive singular compatibility summary selected from that set.
|
||||
The summary is the worst check: a confirmed outage first, then any other
|
||||
observed failure, then an unchecked, ambiguous or unresolved check, then a
|
||||
passing one, ties going to the first saved target id. A confirmed outage is an
|
||||
enabled check whose observed failures reach its failure threshold, with an
|
||||
unavailable aggregate when one is reported and an outcome that is not
|
||||
indeterminate. That is the gate the availability poller applies before it
|
||||
raises `availability_unreachable`, so a probe agent that stops reporting keeps
|
||||
its old failure count but confirms nothing. `availabilityOutageConfirmed` in
|
||||
`internal/unifiedresources/availability.go` holds that rule for both the
|
||||
ranking and `EvaluateResourceHealth`, which reads the summary. A confirmed
|
||||
outage on any attached check therefore makes the resource's health critical
|
||||
even when a check that sorts earlier has failed below its threshold, with the
|
||||
`availability_failed` reason unless a critical alert explains the verdict
|
||||
first.
|
||||
Adding a second explicit or unambiguously correlated check must retain both
|
||||
source-owned endpoint rows, project both facets onto the same resource, and
|
||||
emit one `checks` relationship per target from the check to that resource.
|
||||
@@ -3540,6 +3602,24 @@ remain preferred over device fallback. Direct SATA, SAS, and NVMe fallbacks
|
||||
retain their historical device-scoped shape; controller members sharing one
|
||||
block path add controller/target scope so they cannot collapse into one
|
||||
resource. Topology correlation is parent-scoped and ambiguity fails closed.
|
||||
That scope is added once. A controller member without a usable serial or WWN
|
||||
keeps its history under a source ID that already names the member: the host
|
||||
agent's `HostSMARTDiskMetricID`, and the Proxmox `PhysicalDiskMetricID`, which
|
||||
returns `ProxmoxPhysicalDiskSourceID`. `PhysicalDiskMetaMetricID` therefore
|
||||
returns a fallback that already ends in the disk's own device, controller and
|
||||
target unchanged as the metrics target. It appends that topology only to a
|
||||
fallback without it: a Proxmox source ID from before members were scoped,
|
||||
which `PhysicalDiskMetricID` scopes the same way, or the canonical resource ID
|
||||
`PhysicalDiskView.MetricResourceID` falls back to. The suffix survives a merge
|
||||
because a member target only reaches a disk together with the controller the
|
||||
agent reported beside it (the multiplexed smartctl path sets both), and the
|
||||
Proxmox row copies both from that agent row. A registry rehydrated from
|
||||
persisted resources rebuilds the agent source ID in the same scoped shape
|
||||
(`seedAgentPhysicalDiskSourceIDLocked`). TrueNAS disks carry no controller
|
||||
target and keep their fallback. The reader used to append the topology to an
|
||||
already scoped source ID as well, so such a member's chart read a key no
|
||||
writer used. Proof: `TestPhysicalDiskMetaMetricIDScopesAControllerMemberOnce`,
|
||||
`TestIdentitylessControllerMembersReadTheirWritersHistory`.
|
||||
|
||||
Host reports, monitoring models, registry merges, typed physical-disk views,
|
||||
API resources, websocket/REST snapshot merges, and storage presentation must
|
||||
@@ -5313,7 +5393,8 @@ TrueNAS API and the agent on that box), and an unknown machine never splits a
|
||||
disk. A split (operator exclusion) recorded against any ID the disk can hold,
|
||||
unscoped, current or machine-scoped, keeps the observation on its
|
||||
source-specific ID, keyed to its machine when another machine's disk already
|
||||
holds that ID (agent disk source IDs are the bare serial). Once the identity
|
||||
holds that ID (an agent disk's source-specific ID hashes its bare serial or
|
||||
WWN, below). Once the identity
|
||||
spans machines the unscoped ID no longer names one disk, so a split recorded
|
||||
against it applies to every copy, erring towards an extra row rather than a
|
||||
merge the operator forbade, and a manual link recorded against it stops
|
||||
@@ -5331,14 +5412,113 @@ same-serial disk first appears on another machine and back when it goes away,
|
||||
and alert-history rows owned under the unscoped ID do not follow a re-key; a
|
||||
disk two reporters share is scoped to its canonical parent when the identity
|
||||
first spans machines, which the fixed ingest order (snapshot sources, then
|
||||
supplemental records) and source priority decide; agent SMART and Unraid disk
|
||||
source IDs are the bare serial, so same-serial agent disks on different hosts
|
||||
share one `SourceAgent` mapping and only the last ingested keeps the agent
|
||||
source target; same-serial disks share one metrics history key; and across
|
||||
reporters, hostnames that normalize alike (default TrueNAS names, short names
|
||||
across domains) still join. `IdentityMatcher` keeps one resource per machine
|
||||
ID, so the registry indexes disks by hardware key (`physicalDisksByHardware`)
|
||||
to see every copy.
|
||||
supplemental records) and source priority decide; and across reporters,
|
||||
hostnames that normalize alike (default TrueNAS names, short names across
|
||||
domains) still join. `IdentityMatcher` keeps one resource per machine ID, so
|
||||
the registry indexes disks by hardware key (`physicalDisksByHardware`) to see
|
||||
every copy.
|
||||
An agent disk's source ID carries its host, because the registry keeps one
|
||||
resource per source ID: when it was the bare serial, same-serial agent disks on
|
||||
different hosts shared one `SourceAgent` mapping, every disk but the last
|
||||
ingested lost its agent source target, and a disk only the agent reports lost
|
||||
its metrics target and chart. `HostSMARTDiskSourceID` and
|
||||
`HostUnraidDiskSourceID` key a disk with a usable serial or WWN as
|
||||
`<agent ID>/physical-disk:<serial or WWN>`, which the SMART and Unraid
|
||||
observations of one disk share when both report its serial; a disk without
|
||||
hardware identity keeps its host/device/topology key. A registry seeded from
|
||||
unified resources rebuilds one key from the agent ID the host above the disk
|
||||
retains, directly or above its Unraid array or cache pool, and the merged
|
||||
disk's serial or WWN (`seedAgentPhysicalDiskSourceIDLocked`), so a rehydrated
|
||||
disk, which is what the resource API serves, keeps an agent source target,
|
||||
though not every key its observations ingest under (a SMART row missing the
|
||||
serial its Unraid row reports, or a host several agent IDs report from); with
|
||||
no agent host found, a disk with hardware identity seeds its bare serial or
|
||||
WWN. Canonical IDs do not move: a disk with hardware identity is keyed by it,
|
||||
and `SourceSpecificID` hashes an agent disk's bare serial or WWN without the
|
||||
host (`sourceSpecificIDKey`), so a disk split off onto its source-specific ID,
|
||||
and the exclusion that split it, keep the IDs recorded before the host joined
|
||||
the source ID. Every host's split copy shares that ID, so a split observation
|
||||
takes it keyed to its machine when another machine's disk holds it, and joins
|
||||
the holder on its own machine, whether the split reaches it through
|
||||
`physicalDiskIDForMachineLocked` or `findMatch`. A split therefore cannot
|
||||
separate observations of one disk that one machine reports under several agent
|
||||
IDs: they meet on that ID, as they met on one source key before. Disk history stays keyed by
|
||||
hardware identity, not host: `HostSMARTDiskMetricID`, like the Proxmox and
|
||||
TrueNAS writers and `PhysicalDiskMetaMetricID`, prefers the reported serial or
|
||||
WWN, so a drive keeps one SMART and I/O history across a move between hosts and
|
||||
across sources that report it by the same identifier, and both hosts of a
|
||||
dual-ported SAS disk chart the one drive. A host-scoped history key would
|
||||
orphan the stored history of every agent disk with hardware identity for the
|
||||
rare collision. Residual: distinct drives that share a usable serial on
|
||||
different hosts (cloned VM disks with an explicit serial, fixed-serial USB
|
||||
bridges) interleave their samples in one series, as they already merge into one
|
||||
disk on a single host. Proof:
|
||||
`TestSameSerialAgentDisksOnTwoHostsEachKeepTheirMetricsTarget`,
|
||||
`TestHostSMARTDiskSourceIDScopesHardwareIdentityToItsHost`,
|
||||
`TestRehydratedAgentDisksKeepTheirLiveSourceIDs`,
|
||||
`TestSplitAgentDisksOnTwoMachinesNeverOverwriteEachOther`.
|
||||
A SMART row that reports no serial, smartctl's standby row among them, takes
|
||||
the serial the host's Unraid inventory reports for the same disk
|
||||
(`hostSMARTDiskSerial` over `matchUnraidDisk`). The adapter shows that serial
|
||||
and the disk's metrics target reads it, so `HostSMARTDiskMetricID` resolves the
|
||||
same serial, and the disk I/O writer keys a device with no SMART reading by its
|
||||
Unraid serial (`HostUnraidDeviceMetricID`) before its Proxmox and host/device
|
||||
fallbacks. The writers used to key such a row on its WWN or host/device key,
|
||||
which the chart did not read, so the disk's SMART temperature and I/O history
|
||||
never appeared. The row's source ID stays its own (`HostSMARTDiskSourceID`
|
||||
reads only the row), so its SMART and Unraid observations still ingest under
|
||||
two keys onto one disk. Samples already written under the old key stay there:
|
||||
the chart did not read that key while the Unraid row named the disk (only
|
||||
Unraid OS reports an Unraid inventory, and it is never a Proxmox node whose
|
||||
disk row could replace the serial), and a device-keyed series can hold another
|
||||
disk's samples once device letters move. The key follows the inventory: while the host
|
||||
reports no Unraid row for the disk, the row's own key is both written and read.
|
||||
A row matches its Unraid row by device path alone, so a row on a path several
|
||||
of the host's SMART rows share, controller members behind one block device,
|
||||
takes no serial from it, and the I/O writer still files that device's counter
|
||||
under no member. The I/O writer's inventory fallback also skips a device
|
||||
several Unraid rows name, while a SMART row keeps the adapter's first match.
|
||||
A row's own serial is kept even when it is a placeholder, as the
|
||||
adapter keeps it, and the Unraid row then stays a disk of its own. Proof:
|
||||
`TestHostSMARTDiskMetricIDTakesTheSerialItsUnraidRowReports`,
|
||||
`TestAgentDiskHistoryFollowsTheSerialItsUnraidRowReports`.
|
||||
The adapters' disk temperature rule is shared with the agent disk history
|
||||
writer. `HostSMARTDiskTemperature` returns the temperature and collection state
|
||||
a SMART row's disk shows: the row's own reading, or, when it has none, its
|
||||
Unraid row's under `unraidDiskTemperatureStatus`. `matchUnraidDisk` finds that
|
||||
Unraid row by the SMART row's usable serial anywhere in the inventory before
|
||||
any device path, and a placeholder serial matches nothing. The fallback, like
|
||||
the serial a row without one takes (`hostSMARTDiskSerial`), comes only from an
|
||||
Unraid row that describes the row's disk (`unraidDiskDescribesSMARTRow`): one
|
||||
carrying the row's usable serial, or one naming the row's device path when the
|
||||
row is that whole kernel block device. A controller member, even the only one
|
||||
reported, and a row sharing its path with other SMART rows are not, because
|
||||
the Unraid row describes the block device as a whole. `HostUnraidDiskTemperature`
|
||||
returns what a disk built from an Unraid row alone shows, and
|
||||
`HostUnraidDiskMetricID` returns the key that disk's metrics target reads: its
|
||||
usable serial, else its `HostUnraidDiskSourceID`, and nothing for a row without
|
||||
a device, which is not ingested. The writer used to take only the SMART row's
|
||||
own reading, so a temperature shown from the Unraid inventory, for a disk whose
|
||||
SMART probe returned none or one excluded from SMART collection, was never
|
||||
charted. Proof: `TestAgentDiskChartsTheUnraidTemperatureItShows` and
|
||||
`TestHostUnraidDiskMetricIDMatchesItsDiskMetricsTarget`.
|
||||
`HostDiskTemperatureReadings` lists the reading each disk of a host agent shows
|
||||
for consumers that judge disk heat, the agent disk temperature alerts in
|
||||
`CheckHost`: one per SMART row the registry ingests (no virtual block device),
|
||||
then one per Unraid row whose disk key (`HostUnraidDiskMetricID`) no SMART row's
|
||||
`HostSMARTDiskMetricID` equals, the history writer's rule, with a row without a
|
||||
device skipped as the registry skips it. Rows with one key are one registry
|
||||
disk whatever device labels they carry, such as a controller member
|
||||
(`0 [megaraid,0]`) and its Unraid device (`sda`). When smartctl and Unraid
|
||||
report different usable serials for one device the registry shows both disks,
|
||||
and both are listed. Each reading is taken from the disk resource
|
||||
`resourceFromHostSMARTDisk` or `resourceFromHostUnraidPhysicalDisk` builds, so
|
||||
its temperature, collection state and disk type are the ones that row's disk
|
||||
shows. Unraid rows on different devices that share a key and no SMART row are
|
||||
one registry disk but keep a reading each, since alerts are keyed by device.
|
||||
Proof: `TestHostDiskTemperatureReadingsMatchTheDisksTheRegistryShows` compares
|
||||
each reading with the ingested disk, including two disks on one device and a
|
||||
controller member Unraid lists under its block device.
|
||||
That same canonical physical-disk view must also expose source-independent host
|
||||
context. When a disk is API-backed rather than node-backed, typed views should
|
||||
fall back to canonical host identity such as `identity.hostnames` instead of
|
||||
@@ -5431,8 +5611,11 @@ should extend these unified-resource owners instead of rebuilding status or
|
||||
badge logic inside PMG, recovery, dashboard, or infrastructure-local views.
|
||||
The shared resource-runtime adapter boundary is also owned here now.
|
||||
`frontend-modern/src/utils/agentResources.ts` owns canonical actionable
|
||||
resource identities, agent-facet detection, cluster-name fallbacks, and
|
||||
resource-derived chart-key candidates. `frontend-modern/src/utils/resourcePlatformData.ts`
|
||||
resource identities, agent-facet detection, and cluster-name fallbacks. It no
|
||||
longer carries a resource-wide chart-key candidate list: workload table
|
||||
sparklines match chart series to rows through the chart-key candidates in
|
||||
`frontend-modern/src/components/Workloads/workloadMetricHistoryModel.ts`.
|
||||
`frontend-modern/src/utils/resourcePlatformData.ts`
|
||||
owns the typed extraction of platform-data fragments from unified resources,
|
||||
and `frontend-modern/src/utils/resourceStateAdapters.ts` owns canonical
|
||||
projection from unified resources into node/PBS/PMG runtime view models.
|
||||
@@ -5452,12 +5635,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`
|
||||
@@ -5948,7 +6133,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
|
||||
@@ -6104,6 +6294,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
|
||||
@@ -6186,9 +6383,20 @@ pass and manual links both apply that function:
|
||||
the best of equal ones. A quiet source drops out of that decision.
|
||||
- Once every source is quiet, an `offline` verdict survives and any other
|
||||
verdict reads `warning`. The best of those wins.
|
||||
- A current facet sighting carries no verdict (the PBS host-agent association,
|
||||
an availability check projected onto its target). It counts as `online` only
|
||||
when no current source has a verdict, and reads `warning` once quiet.
|
||||
- A current facet sighting carries no verdict (the PBS host-agent association).
|
||||
It counts as `online` only when no current source has a verdict, and reads
|
||||
`warning` once quiet.
|
||||
- Availability checks projected onto a monitored resource rank like that facet,
|
||||
but their shared sighting is not their verdict. `availabilityChecksProveOnline`
|
||||
judges each check at the pass's `now` by its own evidence window
|
||||
(`Evidence.ValidUntil`: two poll intervals for a single local check, the
|
||||
probe report window for a remote or multi-location one). An enabled,
|
||||
passing check with current evidence proves the resource answers and reads
|
||||
`online` once one of the resource's own sources has gone quiet and none that
|
||||
is current has a verdict. Every other check abstains, current or quiet: a
|
||||
failing check proves only that one port or service does not answer, and a
|
||||
pass whose evidence lapsed proves nothing now. A check's own row keeps the ordinary
|
||||
rules.
|
||||
|
||||
The stale pass used to read delivery as the verdict. A Proxmox node the cluster
|
||||
reported offline on a live poll came back `online` once its linked agent fell
|
||||
@@ -6212,24 +6420,52 @@ online stays `online` when its linked agent stops reporting. A live agent keeps
|
||||
a node `online` when the Proxmox poll reports it offline, which the Proxmox
|
||||
nodes table reads with `connectionHealth: error` as a stale provider. When a
|
||||
guest's or container's only source goes quiet, a running one stays `warning`
|
||||
and a stopped one stays `offline`.
|
||||
and a stopped one stays `offline`. In each quiet case a projected availability
|
||||
check with current evidence lifts the resource to `online`, as described
|
||||
below.
|
||||
|
||||
The sighting still reads `stale`, so health keeps its `telemetry_stale` reason
|
||||
and monitored-system reasons are unchanged. In-memory clones keep the verdicts,
|
||||
including a facet's missing one. A serialized copy carries none, so
|
||||
`IngestResources` gives each sighting of a copy that lost them all the stored
|
||||
resource status; a merged row's separate source verdicts do not survive that
|
||||
round trip. A manual link that joins two resources reported by one source keeps
|
||||
round trip, and a copy whose status a passing check supplied gives that status
|
||||
to every source. A manual link that joins two resources reported by one source keeps
|
||||
the fresher sighting. `registry_merge_policy_test.go` pins the node,
|
||||
poller-expired, guest, round-trip, copy, agent, Docker, Kubernetes,
|
||||
mixed-source and manual-link cases, and the aggregation table.
|
||||
|
||||
Availability checks still count by delivery. With every source that has a
|
||||
verdict quiet, a current check keeps its target `online` even when the check
|
||||
fails, as it did before. Several checks share one sighting whose freshness
|
||||
comes from whichever check was projected last, so an availability verdict
|
||||
needs each check's own freshness, including local checks that miss their
|
||||
cadence, and has to survive manual links. That is left to a follow-up.
|
||||
Availability checks used to count by delivery too. With every source that
|
||||
has a verdict quiet, a current failing check kept a node `preserveOrExpireNodes`
|
||||
expired `online`, and a quiet one lifted it to `warning`. Several checks share
|
||||
one sighting whose freshness comes from whichever check was projected last, so
|
||||
a failing check that just ran could also vouch for another check's old pass.
|
||||
Each check is now judged by its own evidence. A failing probe no longer holds
|
||||
an expired node `online`, and it never makes a guest read stopped to the
|
||||
consumers that count `offline` as stopped (the workloads summary, the Proxmox
|
||||
page counts). The failure stays on the check's own row, which raises an
|
||||
outage incident once the check confirms one (past its failure threshold, from
|
||||
every location). A check observed from several locations relies on the poller
|
||||
ageing out a location that stopped running and lasting an available check's
|
||||
evidence only as long as its newest reachable path (monitoring.md), so one
|
||||
location's fresh failure cannot lend its freshness to another location's old
|
||||
pass.
|
||||
|
||||
The verdict is read from the checks on the resource when status is
|
||||
aggregated. Projecting, re-running or retargeting a check also re-applies the
|
||||
stale pass's rule to every target it touches whose status that pass owns,
|
||||
because a check is not a delivery from the target's own sources and the
|
||||
resources API replays checks after its stale pass. The pass owns a status only
|
||||
through the target's own sightings: the checks' sighting going quiet neither
|
||||
hands it the target nor, once removed, takes the target away, so a target
|
||||
whose own sources have not gone quiet keeps the status they gave it. A check
|
||||
retargeted away therefore takes its verdict with it at once. A
|
||||
manual link keeps only the primary's own checks, so the merged resource is
|
||||
judged by the checks it shows and never by the linked resource's fresher
|
||||
sighting. `registry_test.go` pins the expired-node and quiet-guest cases, both
|
||||
projection orders, re-judging on check changes, ownership through the
|
||||
target's own sightings and the manual link; the aggregation table pins the
|
||||
check rules.
|
||||
|
||||
### Canonical object drawer hierarchy
|
||||
|
||||
@@ -6368,6 +6604,75 @@ full heading. This shared drawer rule applies across resource types. The 390px
|
||||
browser qualification checks heading fit and keyboard expansion; no data model,
|
||||
authorisation, action dispatch or resource admission contract changes.
|
||||
|
||||
### Operator links reach record-ingested resources
|
||||
|
||||
Every registry ingest path applies the operator's manual links: snapshot
|
||||
ingest, resource ingest and record ingest (`IngestRecords`,
|
||||
`IngestRecordsWithStaleThresholds` and the read-state overlay's record pass).
|
||||
Either side of a link can arrive only as a supplemental record, such as a
|
||||
vSphere or TrueNAS VM with a Pulse agent inside, or an agent on a TrueNAS host,
|
||||
and the monitor's rebuild ingests those records after the snapshot. While only
|
||||
snapshot and resource ingest applied links, the rebuilt registry behind the
|
||||
websocket broadcast and `/api/state` kept both rows. The resources API, which
|
||||
seeds through resource ingest, showed them merged, and so did the published
|
||||
inventory whenever an unrelated absent machine made continuity hydration
|
||||
clone the registry. Mock mode builds its unified view from fixtures without
|
||||
the link store, so links do not reach the mock broadcast; that boundary is
|
||||
separate.
|
||||
|
||||
- The monitor adapter passes its configured stale thresholds to record
|
||||
ingest, so a link joined there judges each side's metrics the way a
|
||||
snapshot-time link does. Ordinary source merges into an existing row use
|
||||
the same thresholds on every ingest path: snapshot and record ingest hold
|
||||
the caller's thresholds while they run, and `mergeInto` judges each
|
||||
metric's freshness with them through `sourceStaleThreshold`, the rule the
|
||||
stale pass reads. Before this, those merges used the defaults, so a
|
||||
Proxmox node polled every two minutes showed a silent auto-linked agent's
|
||||
readings while the stale pass called the node's poll current, and a
|
||||
supplemental refresh of a linked vSphere VM kept the agent's memory over
|
||||
vSphere's current reading. Outside mock mode the monitor configures only
|
||||
Proxmox, PBS and PMG; mock mode also configures TrueNAS, vSphere and
|
||||
availability. Regression coverage:
|
||||
`TestMonitorAdapterSourceMergesUseConfiguredStaleThresholds` (rebuild,
|
||||
supplemental refresh, overlay, node with auto-linked agent),
|
||||
`TestSourceMergesJudgeFreshnessByTheIngestStaleThresholds` and
|
||||
`TestMetricMergeFreshnessMatchesTheStalePass`.
|
||||
- Continuity records are the one record ingest that never joins links. A
|
||||
saved enrollment only fills an absent machine, so it stays its own row
|
||||
instead of lending its linked guest an offline verdict and an old agent
|
||||
payload.
|
||||
- A link merge keeps the TrueNAS and vSphere payloads the primary lacks, so
|
||||
an agent chosen as primary over a TrueNAS system still carries the
|
||||
system's facet, and the system's pools re-parent to the merged row.
|
||||
- The registry remembers each resource a manual link folded into its
|
||||
primary. A folded resource is still observed, so neither pin-driven nor
|
||||
record-declared succession may succeed its ID. An agent-type primary
|
||||
carries the folded resource's machine keys and is pinned with them, and
|
||||
succeeding the folded ID re-keyed the link onto the primary itself, which
|
||||
split the pair on the next rebuild. That already broke snapshot links whose
|
||||
primary is a machine-keyless agent-type row, such as a Proxmox node.
|
||||
- Fold history is registry-local: resource seeding and overlay clones do not
|
||||
carry it. A reference to the folded resource's canonical ID, such as an
|
||||
availability check configured against an agent later linked into its
|
||||
guest, does not resolve to the primary, so the check loses its target in
|
||||
the broadcast as it already did in the resources API.
|
||||
|
||||
`registry_merge_policy_test.go` pins the record links in both directions
|
||||
through two rebuilds and the live supplemental refresh, and checks that a
|
||||
registry seeded from the monitor's listing through resource ingest, as the
|
||||
resources API seeds, lists the same rows
|
||||
(`TestManualLinksJoinRecordIngestedResourcesInMonitorRebuild`). The same file
|
||||
pins the node case (`TestManualLinkKeepsItsEndpointsThroughIdentityPinPersistence`),
|
||||
record-declared succession (`TestRecordDeclaredSuccessionSparesALinkFoldedResource`)
|
||||
and both payloads (`TestManualLinkKeepsProviderPayloadsThePrimaryLacks`).
|
||||
`monitor_adapter_read_state_test.go`
|
||||
(`TestMonitorAdapterJoinsLinkedRecordsWithConfiguredStaleThresholds`) pins the
|
||||
thresholds on the rebuild, supplemental and overlay paths, and
|
||||
`internal/monitoring/issue1913_host_continuity_test.go`
|
||||
(`TestManualLinkToSupplementalGuestHoldsWithAndWithoutContinuity`) pins the
|
||||
published inventory with no continuity, an unrelated absent machine and the
|
||||
linked agent's own saved enrollment.
|
||||
|
||||
### Provider link network corroboration
|
||||
|
||||
An existing reciprocal host/provider link remains authoritative, including a
|
||||
@@ -6436,9 +6741,52 @@ conflict): the agent's row may be the slot's previous occupant, retained by a
|
||||
silent agent, and the join would hand the replacement that disk's serial,
|
||||
readings and canonical resource. A hardware identity match still joins on its
|
||||
own, and without a WWN on either side the path fallback stands. The seeded
|
||||
source mapping reused after a restart is a separate path this does not cover.
|
||||
`TestRegistrySASPathJoinRefusesContradictingWWN` covers the stale row and both
|
||||
same-disk shapes; monitoring's SMART merge applies the matching guard first.
|
||||
source mapping a rehydrated registry reuses applies the same refusal (next
|
||||
section). `TestRegistrySASPathJoinRefusesContradictingWWN` covers the stale
|
||||
row and both same-disk shapes; monitoring's SMART merge applies the matching
|
||||
guard first.
|
||||
|
||||
### Seeded slot mapping refuses a replaced disk
|
||||
|
||||
`ingestRecord` honours a source key's existing mapping, such as one
|
||||
`IngestResources` seeds from persisted unified resources, before any
|
||||
identity matching. Proxmox keys a disk by its slot
|
||||
(`ProxmoxPhysicalDiskSourceID`), so a disk swapped into the slot arrived
|
||||
under the previous disk's key, and `mergeInto` kept that disk's canonical ID
|
||||
and, where an agent report had been merged in, its serial, WWN, temperature
|
||||
and failed health. A Proxmox disk observation now refuses the mapping when
|
||||
the two carry hardware identities naming different disks
|
||||
(`proxmoxDiskSlotHoldsAnotherDisk`: `HardwareIdentityConflict` after
|
||||
`HardwareIdentityMatch`). Only fields reported on both sides count, so a
|
||||
missing serial or WWN, including Proxmox's literal `unknown`, is no evidence.
|
||||
Differing serials are no evidence when either side is SAS, because Proxmox
|
||||
may report the SAS address there (#1595), or when the mapped disk was merged
|
||||
with an agent report, whose serial `mergeInto` may have kept (collection
|
||||
status does not reliably say which serial it holds), unless Proxmox read the
|
||||
drive's own serial too, by the rule monitoring's
|
||||
`hostAgentSMARTSerialComparable` applies: NVMe on both sides, or an agent
|
||||
SATA disk under Proxmox vendor `ATA` that Proxmox does not type `usb`, not a
|
||||
SCSI designator or a USB bridge's serial. A refused key falls through to the linked-disk join,
|
||||
`findMatch` and `chooseNewID` like an unmapped one, except that none of them
|
||||
may merge it into the refused disk: an ID that lands on it is replaced with
|
||||
the key's source-specific ID. Agreement still wins, as it does in identity
|
||||
matching, so drives that share a serial they do not own, such as a USB
|
||||
bridge's, read as one disk: a second swap in the same enclosure joins the
|
||||
first replacement, which reported the same bridge serial. That belongs to
|
||||
identity matching, not this guard. The refused disk keeps its Proxmox
|
||||
facet and is not updated, so until the registry is rebuilt both disks answer
|
||||
to the slot's PVE alert reference, and canonical reference resolution treats
|
||||
that reference as ambiguous rather than handing it to either. A later
|
||||
rehydration may seed the slot key to the old disk again, and the refusal
|
||||
repeats. No current caller ingests Proxmox disk records over a seeded
|
||||
mapping: the monitor rebuilds a fresh registry each poll, and the resource
|
||||
API, host-continuity and Patrol registries seeded from unified resources do
|
||||
not replay Proxmox disk records. This closes the registry contract, not a
|
||||
reported symptom. `TestRegistrySeededSlotMappingRefusesReplacedDisk` covers
|
||||
SATA (with and without a new WWN), SAS, USB-enclosure and Proxmox-only
|
||||
replacements, plus eight same-disk controls (including a SCSI designator
|
||||
from an agent without collection status and a USB bridge serial under
|
||||
Proxmox vendor `ATA`), each through two rehydrations.
|
||||
|
||||
### Drawer tab selection survives a transient snapshot change (#1723)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user