mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-10-23 20:25:56 +00:00
Merge origin/main into fix/link-fold-references
PR 2739 added Resource.linkFolds, a record of every link folded into a resource that rides in-memory clones, which is the same fold set this branch carried as Resource.linkFoldedIDs. Keep one record: drop linkFoldedIDs and index folded IDs from linkFolds when a link applies and when resource seeding copies the record in. Main's unlink test treated a lookup by the folded agent's ID as proof that the agent was still listed. That lookup now answers with the VM holding it, as lookups by source IDs and canonical aliases already do, so the test checks which row answered and pins the VM answer.
This commit is contained in:
@@ -2480,12 +2480,13 @@ agent inventory, registration state, or command-channel readiness.
|
||||
or fleet-command evidence. Lifecycle flows must not
|
||||
recreate the retired Patrol quickstart bootstrap path, mint server-issued
|
||||
hosted-model tokens, or derive AI provider state from installation identity.
|
||||
Per-request `/api/ai/chat` execution-mode overrides follow that same
|
||||
boundary: lifecycle-adjacent consumers may rely on Assistant approval
|
||||
semantics, but scoped `autonomous_mode:false` chat requests must not be
|
||||
reinterpreted as agent registration, assignment, installer, or connection
|
||||
lifecycle state. Patrol finding handoffs that force approval-required mode
|
||||
from a non-empty `finding_id` are likewise AI/runtime governance, not an
|
||||
The server-set `/api/ai/chat` execution mode follows that same boundary:
|
||||
lifecycle-adjacent consumers may rely on Assistant approval semantics, but
|
||||
the approval-required mode `HandleChat` installs on every chat request
|
||||
(`ChatRequest` decodes no `autonomous_mode` field) must not be reinterpreted as agent
|
||||
registration, assignment, installer, or connection lifecycle state. Patrol
|
||||
finding handoffs running in that mode from a non-empty `finding_id` are
|
||||
likewise AI/runtime governance, not an
|
||||
agent command grant or lifecycle authorization primitive.
|
||||
Patrol queued-fix approvals may now seed the shared action-audit store with
|
||||
planned and pending lifecycle evidence, but lifecycle surfaces must treat
|
||||
@@ -4929,8 +4930,8 @@ withheld) is an AI-runtime privacy concern; it is not agent enrollment config,
|
||||
installer readiness, command reachability, or any fleet-control capability signal.
|
||||
Patrol finding chat handoffs follow the same ownership split: when
|
||||
`/api/ai/chat` resolves a `finding_id` into model-only Patrol briefing,
|
||||
resource, or action context, the backend-enforced `autonomous_mode:false`
|
||||
clamp is Assistant action-governance, not agent readiness, fleet command
|
||||
resource, or action context, the backend-enforced approval-required clamp
|
||||
is Assistant action-governance, not agent readiness, fleet command
|
||||
reachability, or enrollment health.
|
||||
If the same request also carries recognized Patrol product handoff context,
|
||||
resources, or action references, the API handler may merge only same-finding
|
||||
@@ -8982,6 +8983,45 @@ 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.
|
||||
|
||||
### Safe methods refused on discovery settings and SAML logout
|
||||
|
||||
`internal/api/method_guard.go`, `internal/api/discovery_handlers.go` and
|
||||
`internal/api/saml_handlers.go` changed only so `/api/discovery/settings` and
|
||||
`/api/saml/{id}/logout` answer `405` to every method outside their allowed set,
|
||||
`GET` and `HEAD` included. The settings write still takes `PUT` or `POST` and
|
||||
logout still takes `POST`. No agent registration, enrolment, install, update or
|
||||
removal path changed.
|
||||
|
||||
### SAML SLO responses bound to the logout that requested them
|
||||
|
||||
`internal/api/saml_service.go` and `internal/api/saml_handlers.go` changed only
|
||||
so `/api/saml/{id}/slo` accepts an IdP LogoutResponse when its signed
|
||||
`InResponseTo` names an unexpired LogoutRequest that `/api/saml/{id}/logout`
|
||||
issued, once, in a browser carrying no session or the session that logout
|
||||
ended. Other responses are refused, the SLO handler writes no session or
|
||||
cookie for any response (the logout request already cleared both), and
|
||||
`/api/saml/{id}/logout` refuses a request that carries no session. No agent
|
||||
registration, enrolment, install, update or removal path changed.
|
||||
|
||||
### Manual SAML IdP certificate published as base64 DER
|
||||
|
||||
`internal/api/saml_service.go` changed only so a SAML provider configured with
|
||||
`idpSsoUrl` and a PEM `idpCertificate` or `idpCertFile` puts that certificate
|
||||
into its in-memory IdP descriptor as base64 DER, which crewjam/saml can read
|
||||
when it verifies the IdP's ACS Responses and LogoutResponses. No agent
|
||||
registration, enrolment, install, update or removal path changed.
|
||||
|
||||
### SAML logins bound to the browser that started them
|
||||
|
||||
`internal/api/saml_service.go` and `internal/api/saml_handlers.go` changed only
|
||||
so `/api/saml/{id}/login` records each SAML AuthnRequest against a short-lived
|
||||
login cookie and `/api/saml/{id}/acs` accepts the IdP's signed answer to it
|
||||
once, in that browser, at default settings, reposting the IdP's cross-site
|
||||
delivery once from Pulse's origin so the cookie arrives. Signed content
|
||||
carrying a prefixed attribute or namespace declaration named like a SAML
|
||||
attribute is refused. No agent
|
||||
registration, enrolment, install, update or removal path changed.
|
||||
|
||||
### Demo write guard ignores websocket upgrade headers
|
||||
|
||||
`internal/api/demo_middleware.go` changed only so a `POST`, `PUT`, `PATCH` or
|
||||
@@ -8998,3 +9038,24 @@ No agent registration, enrolment, install, update or removal path changed.
|
||||
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.
|
||||
|
||||
### Broadcast host coalescing honours operator splits
|
||||
|
||||
`internal/monitoring/monitor.go` changed only so the websocket broadcast
|
||||
coalesces host views with the operator's merge exclusions
|
||||
(`MonitorAdapter.CoalesceForPresentation`), as the resources API's
|
||||
`ListForPresentation` already did, so an agent host the operator split from
|
||||
a same-named platform view by unlink or report-merge is coalesced on the
|
||||
websocket as REST coalesces it (the filter's limits are recorded under
|
||||
"Unlink replaces the pair's operator link" in the unified-resources
|
||||
contract). Agent registration, enrolment, install, update, removal, report
|
||||
identity and continuity are unchanged.
|
||||
|
||||
### Report-merge splits a linked agent
|
||||
|
||||
`internal/api/resourceapi/resources.go` changed only so report-merge on a
|
||||
resource an operator linked an agent into (a VM, a node or a Docker host)
|
||||
replaces that link, splitting the agent back out as unlink does. Agent
|
||||
registration, enrolment, install, update, removal, report identity and
|
||||
continuity are unchanged, and an agent's own declared node link still
|
||||
ignores exclusions.
|
||||
|
||||
@@ -2563,8 +2563,9 @@ deriving an older display status from `workflowStatusHistory`.
|
||||
popup/dialog only; it must not also close the Assistant drawer.
|
||||
Failed-turn retry is part of that same local chat-runtime boundary: a
|
||||
retryable in-memory assistant error may replay the original user turn's
|
||||
structured mentions, finding id, approval override, handoff resources,
|
||||
handoff actions, and handoff metadata, but must not reconstruct scoped
|
||||
structured mentions, finding id, model route, handoff context, handoff
|
||||
resources, handoff actions, and handoff metadata (execution mode is not
|
||||
part of that request; `/api/ai/chat` sets it), but must not reconstruct scoped
|
||||
context from prompt history or saved transcript prose. Provider/model route
|
||||
recovery is explicit user-visible recovery, not hidden chat execution. A
|
||||
failed turn may expose route-and-retry actions through the existing drawer
|
||||
@@ -3556,7 +3557,7 @@ query...`, and `Reading storage...` before streamed tool arguments are
|
||||
browser-safe session summary, clear redo state when a new message or trim
|
||||
changes the transcript, and keep forked sessions independent from the source
|
||||
redo stack. The drawer must restore the removed prompt, structured mentions,
|
||||
finding id, handoff metadata, approval/autonomous override, and selected
|
||||
finding id, handoff context and metadata, and selected
|
||||
model route when available so the operator can edit and resend without
|
||||
reconstructing hidden context from transcript prose.
|
||||
Retry and regenerate are turn re-runs over the same undo boundary, not a
|
||||
@@ -3811,8 +3812,10 @@ query...`, and `Reading storage...` before streamed tool arguments are
|
||||
dispatches the shared evidence-first explanation request through
|
||||
`frontend-modern/src/components/AI/Chat/hooks/useExplanationRequest.ts`.
|
||||
It preserves drafts and current work, captures the selected context before
|
||||
asynchronous initialization, and uses normal send, queue, and retry handling
|
||||
with request-local `autonomousMode:false`. It must not prescribe a diagnosis
|
||||
asynchronous initialization, and uses normal send, queue, and retry handling.
|
||||
The store marks that context `autonomousMode:false` so the drawer discloses
|
||||
approval-required posture; the request itself carries no execution-mode
|
||||
field, because `/api/ai/chat` sets that mode server-side. It must not prescribe a diagnosis
|
||||
or a tool sequence or grant action authority. The drawer
|
||||
presentation must stay compact: source, status, one primary subject, and an
|
||||
optional safe route link. It must not render Patrol-authored remediation
|
||||
@@ -4626,10 +4629,11 @@ resolve canonical/source IDs and unique aliases before collection, reject
|
||||
completed, failed, expired, or otherwise historical action references must
|
||||
remain action context without being relabeled as requiring approval.
|
||||
When the Assistant drawer restores any session from that `handoff_summary`,
|
||||
it must restore the scoped request-local approval boundary as well as the
|
||||
safe visible briefing: the next chat turn must carry
|
||||
`autonomous_mode:false` even when the summary is context-only and has no
|
||||
queued action, while the visible badge/action copy must still reflect the
|
||||
it must restore the scoped approval-required disclosure (the drawer-only
|
||||
`autonomousMode:false` context flag) as well as the safe visible briefing,
|
||||
even when the summary is context-only and has no queued action. The next
|
||||
chat turn carries no execution-mode field; `/api/ai/chat` runs it
|
||||
approval-required server-side. The visible badge/action copy must still reflect the
|
||||
actual last-known action state or Patrol assessment context instead of
|
||||
inventing a pending approval or restoring a Patrol recommendation. That
|
||||
restoration is success-bound: if the underlying session message load fails,
|
||||
@@ -4663,8 +4667,8 @@ resolve canonical/source IDs and unique aliases before collection, reject
|
||||
model-context text parsing must not resurrect them as a legacy fallback.
|
||||
After that send succeeds, the drawer
|
||||
must clear those request payloads while preserving the safe visible
|
||||
briefing and request-local
|
||||
approval-required posture; later turns must rely on backend-owned session
|
||||
briefing and the drawer's
|
||||
approval-required disclosure; later turns must rely on backend-owned session
|
||||
model-context hydration and current canonical stores instead of resending
|
||||
stale browser handoff payloads. Patrol approval-row Assistant entries are
|
||||
still Patrol finding handoffs, not local prompt-only shortcuts: live
|
||||
@@ -4676,34 +4680,38 @@ resolve canonical/source IDs and unique aliases before collection, reject
|
||||
Proposed-fix command text must stay out of both the persisted chat message
|
||||
and the model-only handoff context, and command payloads remain
|
||||
approval-context data, not conversational copy.
|
||||
`/api/ai/chat` must also clamp Patrol finding handoffs to
|
||||
approval-required mode when a request carries a non-empty `finding_id` or
|
||||
resolves to model-only briefing, resource, or action context, by forcing the
|
||||
request-local autonomous-mode override to false, even when a caller supplied
|
||||
`autonomous_mode:true`. That clamp belongs to the
|
||||
`/api/ai/chat` clamps every exchange to approval-required mode, Patrol
|
||||
finding handoffs included: `ChatRequest` has no `autonomous_mode` field, so
|
||||
caller JSON (a stale client's `autonomous_mode:true` included) is dropped at
|
||||
decode, and `HandleChat` passes `AutonomousMode:false` into the chat service
|
||||
whether or not the request carries a `finding_id` or model-only briefing,
|
||||
resource, or action context. The browser sends no execution-mode field.
|
||||
That clamp belongs to the
|
||||
backend/API execution boundary, does not mutate the user's persistent AI
|
||||
control setting, and prevents product-originated Patrol action context from
|
||||
becoming silent command authority.
|
||||
The chat runtime must apply any request-local autonomous-mode override to
|
||||
The chat runtime must apply that server-set request-local mode
|
||||
(`chat.ExecuteRequest.AutonomousMode`) to
|
||||
both the per-request `AgenticLoop` and the cloned `PulseToolExecutor`;
|
||||
persistent autonomous settings must not leak into scoped approval-required
|
||||
handoffs through executor state. When such an override forces approval mode
|
||||
persistent autonomous settings must not leak into approval-required
|
||||
requests through executor state. When the request forces approval mode
|
||||
and the saved control level is autonomous, the executor clone must clamp its
|
||||
effective control level to controlled for that request only, so even
|
||||
policy-allowed diagnostic commands require operator approval in scoped
|
||||
handoffs without mutating the user's saved setting.
|
||||
policy-allowed diagnostic commands require operator approval in public chat
|
||||
without mutating the user's saved setting.
|
||||
The Assistant drawer may also render an attached context briefing for that
|
||||
handoff, but the briefing is runtime context visibility only: it must not
|
||||
mutate chat control settings, execute tools, or reveal raw command payloads.
|
||||
Resource-drawer Assistant entries use that same briefing path with
|
||||
`handoff_metadata.kind=resource_context`, a structured `handoff_resources`
|
||||
reference, and `autonomous_mode:false`; they must not prefill or submit a
|
||||
reference, and the drawer-only `autonomousMode:false` approval disclosure;
|
||||
they must not prefill or submit a
|
||||
browser-authored prompt, and any rich resource facts must be hydrated by the
|
||||
backend context-pack path rather than reconstructed in the browser.
|
||||
Safe route-owned briefing actions may render as app links when the handoff
|
||||
includes an `actionHref`, but those links are navigation guidance only and
|
||||
do not grant tool execution or approval authority.
|
||||
Request-local approval-required scoped handoffs must present that boundary
|
||||
Scoped handoffs must present the approval-required boundary
|
||||
through compact source-named drawer state and the effective control label,
|
||||
so Patrol approval/finding handoffs and alert-investigation handoffs are
|
||||
named by their source rather than as generic dashboard briefs.
|
||||
@@ -6753,6 +6761,11 @@ The browser does not read the global learned-correlation list:
|
||||
`/api/ai/intelligence/correlations`, and neither the shared AI intelligence
|
||||
store nor the Patrol page loads it, because no page renders it. The resource
|
||||
drawer reads per-resource correlations from the resource-intelligence payload.
|
||||
The browser does not read `GET /api/ai/circuit/status` either:
|
||||
`frontend-modern/src/api/ai.ts` carries no client for it and the shared AI
|
||||
intelligence store holds no breaker state, because an open breaker already
|
||||
reaches Patrol as the `circuit_open` runtime block cause. The route stays a
|
||||
backend API surface.
|
||||
AI-facing policy metadata must also be cloned through the shared unified-
|
||||
resource policy helper so chat and tools consumers do not maintain their own
|
||||
policy copy logic. Chat mention prefetch now calls that shared helper directly
|
||||
@@ -6969,19 +6982,22 @@ operator paragraph. Future overview or brief surfaces need a governed product
|
||||
owner first, must pass fact-bound structured context from owning Infrastructure,
|
||||
Workloads, Patrol, storage, recovery, and alert summaries, and must not let an
|
||||
unbounded prompt become a route's source of truth.
|
||||
Future route-to-Assistant handoffs must also keep their execution mode scoped
|
||||
to the request. When an overview brief opens Assistant, the drawer may prefill
|
||||
only governed prompt/context data, but the submitted chat request must set
|
||||
`autonomous_mode:false`, preserve the operator's persistent Assistant
|
||||
control-level setting, and disclose the temporary approval-required mode in
|
||||
the drawer instead of showing the generic Autonomous warning.
|
||||
Future route-to-Assistant handoffs inherit the server-set execution mode
|
||||
instead of carrying one. When an overview brief opens Assistant, the drawer may
|
||||
prefill only governed prompt/context data; the submitted chat request carries
|
||||
no execution-mode field (`/api/ai/chat` runs every request approval-required),
|
||||
the operator's persistent Assistant control-level setting stays untouched, and
|
||||
the drawer discloses the approval-required mode through the
|
||||
`autonomousMode:false` context flag instead of showing the generic Autonomous
|
||||
warning.
|
||||
Scoped Assistant handoffs that originate in owned product surfaces may also
|
||||
send bounded `handoff_context` text, structured `handoff_resources`, and safe
|
||||
structured `handoff_actions` through `frontend-modern/src/api/aiChat.ts` and
|
||||
`/api/ai/chat`. That context is model-only session metadata, not saved
|
||||
user-authored message text, and the backend must clamp the exchange to
|
||||
approval-required mode whenever such scoped handoff context, resources, or
|
||||
action references are present. Patrol finding IDs remain stricter: when
|
||||
action references are present (`HandleChat` currently clamps every exchange).
|
||||
Patrol finding IDs remain stricter: when
|
||||
`finding_id` resolves, backend-refreshed durable Patrol context remains the
|
||||
canonical authority; the handler may merge only a recognized same-finding
|
||||
Patrol product handoff section as secondary model-only briefing, and it must
|
||||
|
||||
@@ -1609,6 +1609,34 @@ resolve overrides through the shared
|
||||
candidate set `buildProjectedOverrides` indexes platform alert rows under).
|
||||
Static metric-color defaults are only fallback presentation behavior for
|
||||
callers that do not have alert configuration in scope.
|
||||
Guest bars follow `pulse-relaxed` the same way. `resolveMetricDisplayThresholds`
|
||||
takes the tags the engine reads for a guest-scope resource and, when they
|
||||
include `pulse-relaxed`, applies the `applyRelaxedGuestThresholds` step to
|
||||
CPU, memory and disk before mapping to display thresholds: an unset threshold
|
||||
gets the 95/92/95 floor with a clear five below it, a lower trigger is raised
|
||||
to the floor and keeps its own clear point (a missing one filled five below,
|
||||
as `ensureHysteresisThreshold` does) unless the raised trigger reaches it, and
|
||||
an Off threshold stays Off. A relaxed guest's bar therefore turns critical
|
||||
where its alert fires; 88% memory reads warning, not critical, under the
|
||||
relaxed 92% trigger. The display resolver does not read filter-driven custom
|
||||
rules, which the engine folds in before the override, so a guest a custom
|
||||
rule matches can still colour by thresholds the engine does not use, relaxed
|
||||
or not. Only Proxmox VMs and LXCs carry their tags into alert
|
||||
policy, so `getWorkloadAlertPolicyTags` in
|
||||
`frontend-modern/src/components/Workloads/workloadTopology.ts` passes tags for
|
||||
those guests alone, and a vSphere VM or a container keeps its thresholds
|
||||
whatever it is tagged. The Alerts thresholds row for a relaxed Proxmox guest
|
||||
keeps showing the configured values its editor changes and adds one line
|
||||
naming the tag and the thresholds it raises
|
||||
(`getAlertResourceRelaxedTriggerSummary` in
|
||||
`frontend-modern/src/components/Alerts/alertResourceTableModel.ts`, fed by
|
||||
`pulseRelaxed` from `useThresholdsGuestData`); before it, the tag overrode
|
||||
those settings with no visible sign in the UI (issue #863). Proof: the
|
||||
pulse-relaxed cases in `frontend-modern/src/utils/__tests__/metricThresholds.test.ts`,
|
||||
the alert policy tag cases in
|
||||
`frontend-modern/src/components/Workloads/__tests__/workloadTopology.test.ts`,
|
||||
and the pulse-relaxed guest cases in
|
||||
`frontend-modern/src/components/Alerts/ResourceTable.test.tsx`.
|
||||
|
||||
Docker container image-update alerts are lifecycle-governed by the alerts
|
||||
runtime. Disabling Docker update alerts globally, disabling alerts for a
|
||||
@@ -2642,7 +2670,7 @@ table subgroup-row contract. `AlertHistoryTableGroupRow` and grouped rows in
|
||||
`AlertResourceTableDesktop` must route their date/resource group bands through
|
||||
`frontend-modern/src/components/shared/groupedTableRowPresentation.ts` instead
|
||||
of local `bg-surface-alt` fills, so alert subgroup hierarchy stays visually
|
||||
consistent with Infrastructure, Workloads, Storage, and Recovery tables.
|
||||
consistent with Infrastructure, Workloads, and Storage tables.
|
||||
Alert history table shells must also rely on the shared `TableCard` frame and
|
||||
the shared `Table` primitive for horizontal overflow rather than adding
|
||||
alert-local bordered or `overflow-x-auto` wrappers inside the history section.
|
||||
@@ -3136,21 +3164,22 @@ issue sequential route writes. That reset removes all three query parameters
|
||||
in one navigation and clears any transient chart-bucket selection, so a
|
||||
search-only result set remains visibly resettable and an older URL write
|
||||
cannot resurrect another filter.
|
||||
That same history surface now also owns the canonical resource-incident
|
||||
handoff. `frontend-modern/src/features/alerts/AlertResourceIncidentsPanel.tsx`
|
||||
must treat the selected incident resource as a unified-resource consumer,
|
||||
linking back into canonical infrastructure/resource detail first and then into
|
||||
shared workloads, storage, and recovery surfaces through
|
||||
`frontend-modern/src/routing/resourceLinks.ts` rather than leaving the panel
|
||||
as a dead-end investigation card or rebuilding provider-local route strings for
|
||||
platforms such as TrueNAS.
|
||||
That same alert handoff must now stay on the shared resolved-resource link
|
||||
builder. `AlertResourceIncidentsPanel.tsx` must resolve its chip set through
|
||||
`buildResolvedResourceSurfaceLinks(...)`, which owns exact unified-resource
|
||||
handoffs plus the infrastructure fallback when alert history still references a
|
||||
resource ID before the backing unified record has hydrated. Future incident-link
|
||||
work must not reintroduce local infrastructure-link assembly, local dedupe, or
|
||||
provider-local route strings inside the alert feature shell.
|
||||
That same history surface also owns the resource-incident panel, and the panel
|
||||
links nowhere. `frontend-modern/src/features/alerts/AlertResourceIncidentsPanel.tsx`
|
||||
names the selected resource through the unified-resource lookup it is given
|
||||
(`getResource`, falling back to the history state's lookup) and
|
||||
`getPreferredInfrastructureDisplayName`, and shows the resource name the
|
||||
selected history row carried while no unified record matches. It imports nothing from
|
||||
`frontend-modern/src/routing/resourceLinks.ts`, and it offers no resource
|
||||
cross-jump links: investigation continues in place through the incident
|
||||
timeline cards or hands off to Pulse Assistant through
|
||||
`IncidentAssistantHandoffButton`, as the retired cross-jump chip note above
|
||||
describes. Future incident-link work
|
||||
must not reintroduce a chip set, local infrastructure-link assembly, or
|
||||
provider-local route strings inside the alert feature shell;
|
||||
`frontend-modern/src/pages/__tests__/Alerts.helpers.test.ts` pins the panel
|
||||
free of `buildResolvedResourceSurfaceLinks`, `buildResourceSurfaceLinksForResource`,
|
||||
and `buildInfrastructureResourceLink`.
|
||||
|
||||
Alert configuration load/save state, notification config reloads, and threshold
|
||||
override normalization now route through
|
||||
|
||||
@@ -2702,9 +2702,10 @@ a new API state machine, queue contract, or verification-accounting field.
|
||||
remediation-plan handoffs must use the same boundary for model-only
|
||||
context: plan status, risk, step labels, and command counts are allowed,
|
||||
while raw command and rollback command payloads remain in governed action
|
||||
surfaces. Frontend Patrol finding-discussion handoffs must force a
|
||||
request-local approval-required Assistant mode instead of inheriting the
|
||||
user's persistent autonomous control setting; live approval, action artifact,
|
||||
surfaces. Frontend Patrol finding-discussion handoffs run in the
|
||||
approval-required Assistant mode that `/api/ai/chat` sets server-side for
|
||||
every request, never the user's persistent autonomous control setting, and
|
||||
send no execution-mode field of their own; live approval, action artifact,
|
||||
fix-outcome, and remediation-plan references only add structured action
|
||||
metadata, they are not the trigger for the boundary. Frontend-visible Patrol
|
||||
briefing payloads must stay compact and must not include suggested prompt
|
||||
@@ -2712,7 +2713,7 @@ a new API state machine, queue contract, or verification-accounting field.
|
||||
recommendation metadata. Frontend queued-fix recovery handoffs
|
||||
where the live approval or action artifact payload is unavailable must still
|
||||
carry that Patrol-owned finding briefing, current `fix_queued` posture,
|
||||
request-local approval-required mode, and model-only evidence context; they must
|
||||
approval-required disclosure, and model-only evidence context; they must
|
||||
not degrade into generic Assistant investigation chat or imply that
|
||||
execution can proceed from missing command payloads. Expired-approval
|
||||
recovery handoffs may use a still-available structured action artifact payload
|
||||
@@ -3325,6 +3326,7 @@ a new API state machine, queue contract, or verification-accounting field.
|
||||
and any frontend presentation of the AI summary's `policy_posture` counts, which must render that canonical snapshot through one shared component instead of page-local posture loops; no frontend surface renders those counts today, and the resource drawer stays on per-resource policy lines
|
||||
and the dedicated `frontend-modern/src/features/patrol/patrolInvestigationContextModel.ts` owner, so if Patrol recent-change, learned-correlation, or policy-coverage summary text returns, it is derived from the canonical AI payload in that one place instead of as hook-local count and pluralization logic; Patrol renders no such summary today and the model carries no such derivation
|
||||
and the Watch-only forward-path handoffs owned by `frontend-modern/src/features/patrol/patrolControlPresentation.ts` together with the Actions inbox empty-state read of `GET /api/ai/patrol/autonomy`, so finding-level and inbox-level mode guidance consumes the canonical Patrol autonomy read/save contract through the Patrol state hook (`handleAutonomyChange`) instead of introducing a second mode mutation path or a page-local autonomy dialect
|
||||
and the Patrol finding-to-Assistant handoff owned by `frontend-modern/src/components/AI/FindingsPanel.tsx`, so the pending-approval, investigation, and proposed-fix reads behind that handoff happen once in the panel instead of in a second, unwired copy inside the Patrol state hook
|
||||
and that same Patrol investigation-context owner, so visible Assistant
|
||||
drawer handoffs may include live pending-approval metadata only as safe
|
||||
operator context: approval ID, status, risk, requested/expiry timestamps,
|
||||
@@ -3911,6 +3913,13 @@ counters exist to measure.
|
||||
onboarding telemetry. Customer diagnostics may expose runtime health,
|
||||
supportability, and sanitized troubleshooting state; admin analytics must
|
||||
stay behind admin-owned metrics routes.
|
||||
25. Handlers that perform a caller-requested write (settings, sessions,
|
||||
credentials, workflow or resource state) while answering `GET` or `HEAD`.
|
||||
A route registered without a mux method pattern, or a sub-resource
|
||||
dispatched by path, must refuse safe methods in the mutating handler
|
||||
through `requireRequestMethod` in `internal/api/method_guard.go` (or an
|
||||
equivalent explicit check) instead of relying on its callers, the
|
||||
demo-mode read-only guard, or the CSRF check.
|
||||
|
||||
## Completion Obligations
|
||||
|
||||
@@ -4297,7 +4306,7 @@ the authoritative analysis outcome.
|
||||
run-history consumers may pass a bounded `[Patrol Run Context]` block,
|
||||
scoped resource references, run outcome/coverage facts, sanitized analysis,
|
||||
and structured runtime failure summary/detail as model-only chat context
|
||||
while forcing request-local approval-required mode and leaving retries,
|
||||
under the server-set approval-required mode, leaving retries,
|
||||
configuration changes, and remediation authority outside the chat payload
|
||||
and the main Patrol page composition boundary, so once that governed
|
||||
secondary area exists inside the Current issues and history workspace the same payloads must not
|
||||
@@ -5161,6 +5170,134 @@ pass the bounded non-symlink regular-file boundary in
|
||||
`internal/api/sso_outbound.go`. Cloud handoff redirects consume the shared
|
||||
host-local redirect validator rather than carrying route-local prefix checks.
|
||||
|
||||
A handler that performs a caller-requested write behind a route whose mux
|
||||
pattern carries no method must refuse `GET` and `HEAD` itself.
|
||||
`requireRequestMethod` in `internal/api/method_guard.go` is the shared
|
||||
handler-level guard: it admits only the listed methods and answers any other
|
||||
method that reaches the handler with `405` and an `Allow` header naming them.
|
||||
The demo-mode read-only guard and the CSRF check both admit `GET` and `HEAD` as
|
||||
reads, and the `SameSite=Lax` session cookie rides cross-site top-level `GET`
|
||||
navigations, so the method contract belongs to the mutating handler rather than
|
||||
to its callers. `/api/discovery/settings` accepts `PUT`, its documented method,
|
||||
and `POST`, which it took before the guard existed. SP-initiated SAML logout at
|
||||
`/api/saml/{id}/logout` accepts only `POST`, matching the `/api/logout`
|
||||
fallback it delegates to. The IdP's LogoutResponse keeps returning to
|
||||
`/api/saml/{id}/slo`, which must accept the HTTP-Redirect binding's `GET` and
|
||||
validates the signed response before it touches the session.
|
||||
`TestDemoModeSafeMethodsCannotReachRouteMutations` in
|
||||
`internal/api/ai_handlers_more_test.go` drives both routes through the full
|
||||
`DEMO_MODE` router and proves the threshold and the session survive.
|
||||
The rule does not cover `GET` flows the protocol or the browser handoff
|
||||
requires (OIDC login and callback, SAML login initiation and SLO, magic-link
|
||||
verify, cloud handoff, the checkout start and activation bridges, websocket
|
||||
handshakes), or reads that run one-time lazy initialization or idempotent
|
||||
reconciliation, such as the license and onboarding-overflow bootstrap behind
|
||||
the entitlement reads and the action-expiry sweep behind the action reads.
|
||||
Single-resource report generation at `GET /api/admin/reports/generate` still
|
||||
runs the configured AI narrator, a paid provider call recorded in the cost
|
||||
ledger; that is a known open exception, not part of this contract.
|
||||
|
||||
The SAML SLO callback at `/api/saml/{id}/slo` accepts a LogoutResponse only as
|
||||
the answer to a logout this SP started. crewjam/saml verifies the response's
|
||||
signature, `Destination`, `Issuer` and `Status` and rejects one issued more
|
||||
than 90 seconds ago, but never checks its `InResponseTo`, and the IdP signs its
|
||||
answer to every user's logout, so a valid signature alone let one user replay
|
||||
the answer to their own logout through a cross-site `GET` and clear another
|
||||
user's session. `SAMLService.MakeLogoutRequest` in
|
||||
`internal/api/saml_service.go` therefore records each LogoutRequest ID, on the
|
||||
provider's own service, against the session-store hash of the session
|
||||
`handleSAMLLogout` is ending. A record is honored for ten minutes unless the
|
||||
1,024-record cap evicts it first, oldest first. `SAMLService.ValidateLogoutResponse`
|
||||
reads `InResponseTo` from the same payload crewjam verified, taking only the
|
||||
root element's attribute written without a prefix, the one the signature
|
||||
covers (a namespace declaration such as `xmlns:InResponseTo` is ignored,
|
||||
because exclusive canonicalization can leave an unused one out of the signed
|
||||
form), and
|
||||
spends the matching record. The response is accepted only when that record
|
||||
exists and is unexpired, and the browser carries either no session cookie (the
|
||||
normal case, because `handleSAMLLogout` clears the cookie before redirecting to
|
||||
the IdP) or the session that request ended. Unsolicited, expired, evicted,
|
||||
replayed and other-session responses are refused with `403` and a failed
|
||||
`saml_slo_callback` audit event, and the handler leaves sessions and cookies
|
||||
alone. A misdelivered response still spends its record, so each LogoutRequest
|
||||
is answered at most once. The handler writes no session state or cookie for an
|
||||
accepted response either: `handleSAMLLogout` already invalidated the session
|
||||
and expired its cookies, and a cross-site HTTP-POST delivery withholds the
|
||||
`SameSite=Lax` cookie, so a deletion cookie written there could log out a
|
||||
browser whose session the request never presented. For the same reason
|
||||
`handleSAMLLogout` answers `401` to a request that carries no session cookie,
|
||||
API-token-only callers included, instead of falling back to a cookie-clearing
|
||||
logout: the SAML routes are public and skip CSRF, so that fallback was a
|
||||
cross-site forced logout of a kind a deployment with authentication configured
|
||||
refuses on `/api/logout` as unauthenticated. The records live in memory,
|
||||
so a restart or provider re-initialization during the IdP round trip leaves the
|
||||
returning response unmatched and refused; the local session was already
|
||||
cleared when logout began. `TestSAMLSLOAuditsLogoutResponseNotBoundToSession`
|
||||
in `internal/api/audit_handlers_test.go` replays one user's signed response
|
||||
into another user's browser and proves the victim's session survives and the
|
||||
refusal is audited.
|
||||
|
||||
The SAML ACS at `/api/saml/{id}/acs` accepts a Response, at default settings,
|
||||
only as the IdP's answer to a login the presenting browser started. crewjam/saml
|
||||
refuses a Response unless its `InResponseTo`, and that of each bearer
|
||||
`SubjectConfirmationData`, is one of the request IDs its caller supplies, and
|
||||
Pulse used to supply none, so with `allowIdpInitiated` off, the default, every
|
||||
SAML login failed with "Authentication failed". `SAMLService.MakeAuthRequest`
|
||||
in `internal/api/saml_service.go` therefore records each AuthnRequest ID, on
|
||||
the provider's own service, with the sanitized `returnTo` and the hash of a
|
||||
random token that `handleSAMLLogin` sets as the `HttpOnly`, `SameSite=Lax`
|
||||
cookie `pulse_saml_login` (`__Host-pulse_saml_login` over HTTPS) for ten
|
||||
minutes. A browser that already holds a token keeps it, so logins started one
|
||||
after another in several tabs all stay valid. A record is honored for ten
|
||||
minutes, judged when the Response spends it, unless the 1,024-record cap
|
||||
evicts it first, oldest first. The IdP delivers its Response by a cross-site
|
||||
HTTP-POST, which browsers send without `SameSite=Lax` cookies, so
|
||||
`handleSAMLACS` answers a POST that carries a `SAMLResponse` or
|
||||
`SAMLart` but no login cookie with a page that posts the same fields once more
|
||||
from Pulse's own origin. The form has no `action`, so it returns to the URL
|
||||
the IdP posted to, path prefix included, its script carries the request's CSP
|
||||
nonce, and a marker field prevents a second repost. `SAMLService.ProcessResponse`
|
||||
then has crewjam check the Response against every outstanding request ID and
|
||||
reads the request it answers from the canonical form goxmldsig verified for
|
||||
the signature covering the assertion crewjam returned, never from crewjam's
|
||||
parsed fields, taking only an `InResponseTo` written without a prefix: the
|
||||
signed `Response`'s, or that of the `Response` inside a signed
|
||||
`ArtifactResponse`, when the IdP signed the message, and otherwise that of
|
||||
every `SubjectConfirmationData` in the returned assertion's own `Subject`,
|
||||
which must agree and of which there must be at least one, and no other signed
|
||||
assertion may carry its ID. Other assertions in the Response, and assertions
|
||||
in `Advice`, do not take part. That record is
|
||||
spent only when it is bound to the presenting browser's token, and the login
|
||||
returns to the recorded `returnTo`, not to the posted `RelayState`, which
|
||||
`MakeAuthRequest` now escapes into the redirect query so a `returnTo` with its
|
||||
own query string is not cut short at the IdP. A Response answering another browser's login, an unknown,
|
||||
expired or already spent request, or none is refused with the
|
||||
`saml_validation_failed` redirect and a failed `saml_login` audit event, and
|
||||
refusing another browser's Response leaves that browser's record for it. With
|
||||
`allowIdpInitiated` on, crewjam skips `InResponseTo`, so a Response answering
|
||||
any request or none is accepted in any browser without the cookie or the
|
||||
repost and returns to its posted `RelayState`, as before. In both modes the
|
||||
signature verifier refuses signed content carrying a prefixed attribute or a
|
||||
namespace declaration named like an un-namespaced attribute crewjam reads from
|
||||
a Response or Assertion, such as `xmlns:IssueInstant`, `xmlns:NotOnOrAfter` or
|
||||
`ext:ID`: encoding/xml fills an un-namespaced attribute field from any
|
||||
attribute with that local name, and exclusive canonicalization leaves an
|
||||
unused declaration out of the signed form, so appending one let a stale signed
|
||||
Response pass crewjam's age checks, and an IdP-initiated login could be
|
||||
replayed indefinitely. The records live in memory, so a
|
||||
restart or a saved provider change (which replaces the provider's service;
|
||||
a metadata refresh keeps the records) during the IdP round trip, or a login
|
||||
started on a host that does not share cookies with the configured public
|
||||
URL's, leaves the Response unmatched and the user starts the login again. Two logins started at
|
||||
the same moment in a browser that holds no token yet each mint one and only
|
||||
the last cookie set survives, so the other fails the same way, and, as with
|
||||
OIDC login state, a flood of unauthenticated login starts can evict pending
|
||||
records.
|
||||
`TestSAMLACSAuditsResponseNotAnsweringThisBrowsersLogin` in
|
||||
`internal/api/audit_handlers_test.go` delivers one browser's signed Response to
|
||||
another browser with a login in progress and proves it is refused and audited
|
||||
while its own browser completes the login.
|
||||
|
||||
Alert delivery diagnosis is a read-only monitoring API contract.
|
||||
`GET /api/alerts/delivery-diagnosis?alertIdentifier=<id>` returns the alert
|
||||
manager's current delivery-policy projection for one active alert, including
|
||||
@@ -6446,6 +6583,25 @@ That same SSO boundary also owns manual SAML endpoint validation payloads.
|
||||
through the same validated absolute HTTP(S) helpers instead of letting the
|
||||
manual logout URL drift out of the request model or bypass the governed URL
|
||||
normalization path.
|
||||
That same manual SAML configuration must verify signatures with the IdP
|
||||
certificate the administrator supplied. With no IdP metadata,
|
||||
`SAMLService.buildManualMetadata` in `internal/api/saml_service.go` builds the
|
||||
IdP descriptor from `idpSsoUrl`, optional `idpSloUrl` and the PEM signing
|
||||
certificate in `idpCertificate` or `idpCertFile`, and must put that certificate
|
||||
in the signing `KeyDescriptor` as base64 DER, the `<X509Certificate>` content
|
||||
IdP metadata carries. crewjam/saml base64-decodes that field to verify every
|
||||
ACS Response and LogoutResponse, so the PEM block it once held failed each
|
||||
check with "illegal base64 data" before any key was compared, and no manually
|
||||
configured provider could accept a login or complete SLO. The stored
|
||||
`idpCertificate` stays the PEM the administrator supplied; only the descriptor
|
||||
rebuilt in memory on each load changed, so no saved configuration needs
|
||||
migrating. `TestContract_SAMLManualIDPCertificateVerifiesIdPSignatures` in
|
||||
`internal/api/contract_test.go` signs an ACS Response and a LogoutResponse with
|
||||
one test IdP key and proves an inline-certificate provider, a certificate-file
|
||||
provider and a metadata provider accept both alike, while a Response signed by
|
||||
another key is refused. The proof runs at default settings, with each
|
||||
Response answering an AuthnRequest that `SAMLService.MakeAuthRequest`
|
||||
recorded for the test browser, as the SAML ACS binding above requires.
|
||||
That same SSO provider-detail boundary must return the non-secret nested
|
||||
provider configuration used by the settings edit form. `GET
|
||||
/api/security/sso/providers/{id}` may keep the flat list/card fields for
|
||||
@@ -7143,8 +7299,8 @@ unified-resource mentions as well; VMware network inventory does not create a
|
||||
provider-local mention family.
|
||||
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
|
||||
`autonomous_mode` field, so a stale client's `autonomous_mode` is dropped at
|
||||
decode (the browser and the `internal/ai/eval` runner send none), 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
|
||||
@@ -7184,8 +7340,9 @@ status, and a runtime-failure flag; the backend rebuilds their context), but the
|
||||
must not turn their evidence or proposed remedies into user-authored message
|
||||
text. Ordinary attachment is context only. A labelled Explain action may
|
||||
submit the shared user-selected explanation request over the existing chat
|
||||
transport, with the evidence kept in model-only handoff fields and
|
||||
`autonomous_mode:false`. This starts explanation/review and grants no new
|
||||
transport, with the evidence kept in model-only handoff fields and no
|
||||
execution-mode field, since the handler already runs it approval-required.
|
||||
This starts explanation/review and grants no new
|
||||
approval or execution authority. When a
|
||||
Patrol `finding_id` resolves,
|
||||
backend-refreshed durable finding context remains canonical; the handler may
|
||||
@@ -7205,10 +7362,10 @@ recommendation fields from legacy handoffs.
|
||||
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
|
||||
request-local autonomous mode to false even if a caller's JSON carries
|
||||
`autonomous_mode:true`. That server-side clamp is part of the public API
|
||||
contract because the frontend handoff setting is only advisory unless the
|
||||
backend preserves the approval-required boundary.
|
||||
contract because the drawer's `autonomousMode:false` flag is a disclosure only
|
||||
and never reaches the request.
|
||||
That same backend API boundary now also owns the negative space around
|
||||
assistant control. Wiring native TrueNAS app actions into
|
||||
`internal/api/router.go`, `internal/api/ai_handler.go`, or adjacent backend
|
||||
@@ -7329,6 +7486,12 @@ no client for it, and neither the dashboard load bundle in
|
||||
fetches it. The resource drawer's correlation evidence comes from the
|
||||
resource-intelligence payload above. The route stays an API surface behind the
|
||||
`ai:execute` scope.
|
||||
The frontend does not load `GET /api/ai/circuit/status` either. No surface
|
||||
renders provider breaker state, so `frontend-modern/src/api/ai.ts` carries no
|
||||
client or `CircuitBreakerStatus` type for it, and neither that dashboard bundle
|
||||
nor the Patrol refresh path fetches it. An open breaker reaches the Patrol page
|
||||
through `/api/ai/patrol/status` as the `circuit_open` blocked cause. The route
|
||||
stays a backend API surface.
|
||||
That correlations route now reads through the canonical AI intelligence
|
||||
facade first, so the handler and its payload keep the detector behind one
|
||||
shared access layer instead of routing directly to Patrol-local correlation
|
||||
@@ -8734,7 +8897,7 @@ briefings may reuse that same safe metadata for factual action labels and safety
|
||||
while approval command text remains inside the governed approval/remediation
|
||||
surface.
|
||||
Patrol approval-row Assistant handoffs must use the same safe metadata boundary
|
||||
and set `autonomousMode:false` for the request-local chat handoff; they must not
|
||||
and set the drawer's `autonomousMode:false` approval disclosure; they must not
|
||||
paste raw approval or action command text into a chat prompt.
|
||||
Patrol remediation-plan or action-artifact Assistant handoffs must pass only safe
|
||||
status, risk, description, and command-count posture as non-authoritative context
|
||||
@@ -11493,3 +11656,17 @@ Router's or the package-global limiters. Limits, windows, tenant selection,
|
||||
revocation and response payloads are unchanged. The connected lifetime and
|
||||
ownership controls are in `router_limiter_lifecycle_test.go`; the integration
|
||||
fixture's cleanup also runs on a failed listener/request assertion.
|
||||
|
||||
### Report-merge undoes operator links
|
||||
|
||||
`POST /api/resources/{id}/report-merge` keeps its request, status codes and
|
||||
response shape. For a reported source that an operator link brought in, it
|
||||
now records the exclusion against the link's own pair instead of a candidate
|
||||
ID derived from the merged resource's type, so the store replaces the link
|
||||
and the pair splits as after unlink (contract under "Report-merge splits
|
||||
operator-linked resources" in unified-resources). `exclusions` counts those
|
||||
pairs with the identity-match exclusions. `400 Resource is not merged` and
|
||||
`400 No source targets found` now also need a resource with no applied link,
|
||||
so two linked agents, which share their only source, can be reported.
|
||||
`TestContract_ResourceReportMergeReplacesOperatorLink` pins the response
|
||||
shape and the link's removal for a VM and the agent linked into it.
|
||||
|
||||
@@ -1161,18 +1161,21 @@ hands-on Patrol modes, issue investigation, verified fixes, and longer history`.
|
||||
absent platform families must not render as disabled placeholders in the
|
||||
paid or hosted app shell. Infrastructure is not an equal primary tab in
|
||||
that list. Workloads, Storage, and Recovery are not standalone aggregate
|
||||
workspace tabs; their component surfaces remain reusable inside platform
|
||||
pages via embedded `tableOnly` surfaces. Platform primary-tab settings
|
||||
workspace tabs. Workload and storage rows render inside platform pages,
|
||||
through the canonical `WorkloadsSurface` or `StorageSurface` where a page
|
||||
embeds one and through platform-owned tables elsewhere; recovery rows
|
||||
render only in platform-owned tabs such as Proxmox Backups and TrueNAS
|
||||
Protection, because no Recovery surface exists. Platform primary-tab settings
|
||||
handoffs in `AppLayout.tsx` must target the canonical
|
||||
`/settings/infrastructure` workspace and must not retain retired settings
|
||||
aliases such as `/settings/workloads/docker` or nested
|
||||
`/settings/infrastructure/platforms/*` paths. Each platform page must
|
||||
remain chrome-only: routing plus sub-tab navigation that embeds the
|
||||
canonical `WorkloadsSurface`, `StorageSurface`, or `RecoverySurface` in
|
||||
`embedded tableOnly` mode with a forced platform/source filter. The shell must not introduce dashboard cards,
|
||||
bespoke per-family tables, synthetic placeholder data, or reintroduce
|
||||
Infrastructure as a primary navigation entry without a governed contract
|
||||
decision recorded by the frontend-primitives owner.
|
||||
`/settings/infrastructure/platforms/*` paths. Which platform tabs embed a
|
||||
canonical surface and which render platform-owned tables is the
|
||||
frontend-primitives platform-page rule; cloud-paid must not carry a
|
||||
second copy of it. The paid or hosted shell must not introduce dashboard
|
||||
cards, synthetic placeholder data, or forked per-family table primitives,
|
||||
or reintroduce Infrastructure as a primary navigation entry, without a
|
||||
governed contract decision recorded by the frontend-primitives owner.
|
||||
The presence of the user-facing `Machines` tab, backed by the internal
|
||||
`standalone` route/id, is not a new commercial usage unit: hosted and paid
|
||||
surfaces continue to meter the governed monitored-system grouping result
|
||||
@@ -1583,9 +1586,10 @@ follow the same canonical home. The hosted bootstrap / activation / recovery
|
||||
routes (`/pricing`, `/preview/setup-complete`, `/login`, settings panels
|
||||
under `/settings/...`) are unchanged. Pricing-handoff and hosted-signup
|
||||
upgrade paths must use platform routes as their canonical "back to product"
|
||||
destination instead of the retired top-level Infrastructure route; aggregate
|
||||
Workloads / Storage / Recovery pages are product workspaces, not hosted
|
||||
acquisition destinations. The Docker / Podman runtime route remains `/docker`
|
||||
destination instead of the retired top-level Infrastructure route. Platform
|
||||
pages are product workspaces, not hosted acquisition destinations, and the
|
||||
retired aggregate Workloads / Storage / Recovery pages are not destinations at
|
||||
all. The Docker / Podman runtime route remains `/docker`
|
||||
for search familiarity, but the shell may label the
|
||||
destination as Docker so it reads as the recognizable container runtime lens
|
||||
rather than an owning infrastructure platform.
|
||||
|
||||
@@ -730,8 +730,8 @@ history, and must not discard the saved note.
|
||||
Platform-owned workload controls extend the shared `WorkloadsFilter` view
|
||||
options rather than creating page-local toolbar shells. Persistent presentation
|
||||
choices compose the shared `ViewOptionsDisclosure` instead of occupying the
|
||||
primary filter rail: layout, metric style, chart visibility, memory basis, and
|
||||
columns remain discoverable behind one `View` trigger. The history range stays
|
||||
primary filter rail: layout, metric style, row hover, memory basis, inventory
|
||||
totals, and columns remain discoverable behind one `View` trigger. The history range stays
|
||||
inline in both metric modes because bars now expose an intent-driven row
|
||||
history lens and Trends keeps the same charts persistent. In Bars the range
|
||||
renders only at desktop widths while Row hover is set to `History`, the mode
|
||||
@@ -2355,6 +2355,20 @@ not a replacement status card, CTA band, or page-local nested card.
|
||||
Proxmox backup source/state chips follow the same boundary: storage/recovery
|
||||
owns backup-source labels and state semantics, while the visible chip shell
|
||||
and tone vocabulary route through `MetadataBadge`.
|
||||
Resource drawer identity chips (the IP address and alias chips and the
|
||||
alias overflow count in `ResourceDetailSummary.tsx`) compose `MetadataBadge`
|
||||
too. Badges stay on one line by default; an identity value that can outgrow
|
||||
a narrow drawer cell opts into the badge's `wrap` mode, which caps the chip
|
||||
at its container and breaks inside a token only where no space or hyphen
|
||||
fits. These values differ at their ends (`...:pve1:113`, a container hash)
|
||||
and a phone has no hover to show a title, so they wrap rather than
|
||||
truncate. A one-line alias used to overflow its fixed-width value cell at
|
||||
phone widths, where the cell's hidden overflow and the inline drawer's
|
||||
clipping content shell cut it off without an ellipsis.
|
||||
`tests/integration/tests/04-mobile.spec.ts` opens a Docker container drawer
|
||||
at phone width and checks that no element or text in its Identity section
|
||||
crosses either edge of those clipping boxes, apart from values that end in
|
||||
their own ellipsis.
|
||||
Workload backup freshness follows a separate shared presentation boundary:
|
||||
`frontend-modern/src/utils/workloadGuestPresentation.ts` owns the canonical
|
||||
tone and icon mapping consumed by workload rows and drawers. A recorded
|
||||
@@ -2638,15 +2652,20 @@ not a replacement status card, CTA band, or page-local nested card.
|
||||
drifting off-screen from page-local flex behavior. The shared action rail
|
||||
must align to the trailing edge at wrapped desktop widths and remain
|
||||
separate from the filter-control wrap zone instead of waiting for a wide
|
||||
breakpoint, so Recovery events, Workloads, Storage, Infrastructure, and
|
||||
breakpoint, so Workloads, Storage, Infrastructure, and
|
||||
future dense toolbars do not strand Filter/Columns/Reset actions as an
|
||||
isolated second-row fragment. Shared `FilterToolbarPanel` owns
|
||||
default filter-popover geometry, and `FilterToolbar` owns the shared chart
|
||||
visibility display action: Workloads, Storage, Infrastructure, and future
|
||||
summary-bearing pages must use `ChartVisibilityToggleButton` so the
|
||||
affordance exposes one `Show charts` / `Hide charts` pressed-state contract
|
||||
instead of rebuilding a one-option segmented control or an in-summary
|
||||
collapse chevron page by page. Feature state hooks under
|
||||
default filter-popover geometry. The workload summary chart section that the
|
||||
shared chart show/hide action toggled is deleted and no other page exposes
|
||||
one, so the shared toolbar carries no chart show/hide action: the
|
||||
`ChartVisibilityToggleButton` primitive, its
|
||||
`chart-visibility-toggle-button` rule, and its
|
||||
`chart-visibility-local-toggle-labels` pattern guard are retired, and
|
||||
`SharedPrimitives.guardrails.test.ts` keeps them out. A summary section that
|
||||
returns through a governed product decision must register one shared
|
||||
show/hide primitive with an explicit pressed state rather than a page-local
|
||||
one-option segmented control or in-summary collapse chevron.
|
||||
Feature state hooks under
|
||||
`frontend-modern/src/features/` own route-backed query state, selected item
|
||||
state, and data-window selection for their product surfaces; shared
|
||||
primitives and reusable presentation helpers may own viewport-safe chrome,
|
||||
@@ -3034,9 +3053,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 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.
|
||||
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 must scope the matching rows through that same shared contract instead of forking a page-local summary filter path. Workloads grouped host rows are static identity dividers and pass no group scope to the bridge; a grouped table that wants group hover or pin again needs a demand-ledger entry and must use this shared scope rather than reviving a Workloads-local 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.
|
||||
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, and reveals a focused row once per focus change: its reveal effect subscribes to the focused series id and table root alone (a remounted table root starts a new reveal for the same focus) and runs the consumer's `revealActiveSeries` callback and the inline-detail scroll untracked, so page state the callback reads (live group lookups, expanded groups) cannot start another reveal, scroll the page back to the row, or reopen a group the operator collapsed. A reveal ends when its row and detail settle in view or its bounded retry window lapses, and filter changes do not re-run the consumer callback, so a row that stays filtered out past that window is not revealed when it returns. 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
|
||||
@@ -3379,15 +3398,30 @@ Agent`), with the plain-language source phrase available through accessible
|
||||
default` instead of fusing provider and badge text such as
|
||||
`OpenRouterdefault`.
|
||||
Platform-first top-level pages registered through
|
||||
`frontend-modern/src/App.tsx` must stay chrome-only and route through the
|
||||
canonical app shell: each per-platform surface owns navigation and sub-tab
|
||||
chrome, then embeds the canonical `WorkloadsSurface`, `StorageSurface`, or
|
||||
`RecoverySurface` in `embedded tableOnly` mode with a forced platform or
|
||||
source filter. Per-platform features must not
|
||||
fork their own table primitives, header layouts, or summary cards when a
|
||||
shared canonical surface already exists; new shared platform-page
|
||||
primitives live under `frontend-modern/src/features/platformPage/` so the
|
||||
chrome stays reusable across families.
|
||||
`frontend-modern/src/App.tsx` route through the canonical app shell: each
|
||||
per-platform page surface owns its route, `PlatformSectionTabs` sub-tab
|
||||
navigation, and shared loading, empty, and error states, and its tabs are
|
||||
built around tables rather than estate overview or attention summary
|
||||
panels. Three tabs embed a cross-platform canonical surface with the
|
||||
platform scope locked: the Proxmox and vSphere Overview tabs embed
|
||||
`WorkloadsSurface` with `forcedPlatform` (the page owns the workloads
|
||||
state and the one `WorkloadsFilter` toolbar), and the Proxmox Storage tab
|
||||
embeds `StorageSurface` with `forcedSourceFilter`. Those tabs must keep
|
||||
their guests, VMs, and storage rows on those embeds rather than a
|
||||
platform-local copy. Neither surface has an `embedded` or `tableOnly`
|
||||
mode, and no `RecoverySurface` exists: the aggregate Recovery surface was
|
||||
deleted on 2026-05-26. Every other tab, including Docker containers,
|
||||
Kubernetes pods, Machines, `/truenas/storage`
|
||||
(`TrueNASStorageTopologyTable`), the Proxmox Backups tab, and the TrueNAS
|
||||
Protection tab, renders a platform-owned table built from the shared
|
||||
platform-page primitives in
|
||||
`frontend-modern/src/features/platformPage/sharedPlatformPage.tsx`, even
|
||||
where a canonical surface could list the same resource types.
|
||||
Per-platform features must not fork their own table primitives, header
|
||||
layouts, or summary cards when a shared canonical surface already exists;
|
||||
new shared platform-page primitives live under
|
||||
`frontend-modern/src/features/platformPage/` so the chrome stays reusable
|
||||
across families.
|
||||
Source-specific platform product surfaces under
|
||||
`frontend-modern/src/features/`, such as the Proxmox Backups tab, may own
|
||||
domain IA and row models in their product subsystem while consuming shared
|
||||
@@ -3715,9 +3749,12 @@ production table, router and styles; it does not qualify full-app scrolling.
|
||||
IDs into setup payloads. The shared settings shell should let the backend
|
||||
resolve the effective BYOK model and then render that returned state rather
|
||||
than guessing a model in the modal.
|
||||
Scoped Assistant handoffs must keep request-local execution overrides in
|
||||
drawer context. Dashboard and other route-owned entry points may open the
|
||||
Assistant drawer with source context and `autonomousMode:false`, but they
|
||||
Scoped Assistant handoffs keep their approval-required disclosure in
|
||||
drawer context: `autonomousMode:false` on `AIChatContext` is a drawer-only
|
||||
flag the chat transport never sends, because `/api/ai/chat` sets
|
||||
approval-required mode server-side for every request. Dashboard and other
|
||||
route-owned entry points may open the
|
||||
Assistant drawer with source context and that flag, but they
|
||||
must not infer a user task from an ordinary context-only open. Explicit
|
||||
labelled explanation actions use `aiChatStore.explain` and the shared
|
||||
explanation dispatcher, which captures the request context, waits for open
|
||||
@@ -3735,11 +3772,11 @@ production table, router and styles; it does not qualify full-app scrolling.
|
||||
assessment readout, but shared drawer/chrome primitives stay free of
|
||||
the `FindingsTrustSummary` shape so adding new trust signals goes
|
||||
through the contract first rather than per-shell branching.
|
||||
Patrol header refresh controls stay on that same feature-owned shell
|
||||
Patrol refresh controls stay on that same feature-owned shell
|
||||
boundary: `frontend-modern/src/features/patrol/usePatrolIntelligenceState.ts`
|
||||
must make the refresh affordance generation-aware and timeout-bounded, so
|
||||
a slow supporting intelligence read cannot permanently disable the shared
|
||||
Patrol header control while Patrol findings and status remain visible.
|
||||
a slow supporting intelligence read cannot permanently disable the
|
||||
stale-data Retry control while Patrol findings and status remain visible.
|
||||
That feature-owned
|
||||
presentation helper is the single emitter for investigation-record
|
||||
`impact` and `rollback` fields: when an investigation record exists but those fields
|
||||
@@ -3783,7 +3820,7 @@ production table, router and styles; it does not qualify full-app scrolling.
|
||||
chips in the drawer and must not become another primitive path for raw
|
||||
approval, command, or rollback command payload text. Missing-detail
|
||||
queued-fix recovery actions must still provide the feature-owned Patrol
|
||||
briefing and request-local approval-required posture rather than opening the
|
||||
briefing and approval-required disclosure rather than opening the
|
||||
shared drawer as context-free generic Assistant chat. If a feature-owned
|
||||
expired-approval recovery action still has structured action artifact metadata,
|
||||
the shared drawer may receive only safe summary fields and command counts;
|
||||
@@ -3810,16 +3847,16 @@ production table, router and styles; it does not qualify full-app scrolling.
|
||||
feature-owned finding handoff helper rather than hand-written prompt-only
|
||||
drawer opens: safe approval metadata, action artifact summaries, resource
|
||||
references, and bounded `handoff_actions` may enter the prompt and context,
|
||||
but raw command text stays out and the scoped request must pass
|
||||
`autonomousMode:false` instead of changing the user's persistent Assistant
|
||||
control level. Patrol remediation-plan drawer handoffs must use the same
|
||||
but raw command text stays out and the handoff must set the drawer's
|
||||
`autonomousMode:false` disclosure instead of changing the user's persistent
|
||||
Assistant control level. Patrol remediation-plan drawer handoffs must use the same
|
||||
primitive boundary: plan title/status/risk, step labels, and command counts
|
||||
may enter Assistant context; raw command and rollback command payloads must
|
||||
stay in the governed remediation/action panel. All Patrol finding
|
||||
discussion handoffs, including context-only findings without a live approval
|
||||
or proposed fix, must pass `autonomousMode:false` as a request-local
|
||||
override so the drawer shows approval-required posture without mutating the
|
||||
persistent Assistant control setting.
|
||||
or proposed fix, must set `autonomousMode:false` as that drawer-only flag
|
||||
so the drawer shows the approval-required posture the server enforces,
|
||||
without mutating the persistent Assistant control setting.
|
||||
14. Keep shared filter primitives coherent with source-owned option hydration.
|
||||
Active platform/runtime pages and Settings infrastructure surfaces must keep
|
||||
canonical options visible in shared filter controls even when current
|
||||
@@ -3883,6 +3920,10 @@ 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.
|
||||
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.
|
||||
UI surfaces must not duplicate the `patrolControlStarter` query string or
|
||||
write Patrol control or legacy entry-point starter telemetry from local
|
||||
click handlers.
|
||||
@@ -4423,12 +4464,13 @@ labels) bounded — individual subject names and full PolicyRule contents stay
|
||||
outside the rendered surface, mirroring the agent and unified-resource
|
||||
contracts.
|
||||
|
||||
Embedded Recovery workspace controls now use the shared filter-toolbar
|
||||
primitive boundary. Platform pages may choose a default Recovery workspace,
|
||||
such as TrueNAS opening on protection coverage, but the compact
|
||||
protection/events selector must use `FilterSegmentedControl` and the
|
||||
recovery-owned `useRecoverySurfaceState` owner rather than page-local tabs,
|
||||
nested cards, or independent protection/event state in the embedding surface.
|
||||
No platform page embeds a Recovery workspace. The aggregate Recovery surface,
|
||||
its protection/events selector, and `useRecoverySurfaceState` were deleted on
|
||||
2026-05-26, and TrueNAS opens on its Overview tab. Recovery rows render in
|
||||
platform-owned tabs whose controls stay on the shared filter-toolbar
|
||||
primitives: the Proxmox Backups tab filters through the shared `FilterBar`,
|
||||
and `TrueNASProtectionTable.tsx` through `PlatformTableToolbar`, rather than
|
||||
page-local tab strips or nested cards.
|
||||
|
||||
Cross-jump chip strips on alert and Patrol surfaces were retired on
|
||||
2026-05-16 alongside the platform-first migration. The
|
||||
@@ -4472,10 +4514,13 @@ and `getActiveTabForPath` matcher must not recognize aggregate workspace URLs
|
||||
as owned shell destinations. New palette commands and shortcut chords must
|
||||
flow through the same shell owners; do not reintroduce hidden platform
|
||||
families or retired top-level aggregate routes by reanimating legacy paths.
|
||||
The shared route-state helpers follow the same boundary: workload, storage,
|
||||
and recovery helpers in `frontend-modern/src/routing/resourceLinks.ts` may
|
||||
build query strings for an already-owned platform/runtime route, but must not
|
||||
export pathname builders for `/workloads`, `/storage`, or `/recovery`.
|
||||
The shared route-state helpers follow the same boundary: the workload and
|
||||
storage helpers in `frontend-modern/src/routing/resourceLinks.ts` may build
|
||||
query strings for an already-owned platform/runtime route, but must not export
|
||||
pathname builders for `/workloads`, `/storage`, or `/recovery`. The module
|
||||
exports no recovery query serializer: the Proxmox Backups tab owns its query
|
||||
through `PROXMOX_BACKUPS_QUERY_PARAMS`, and the TrueNAS Protection tab keeps
|
||||
its search, status filter, and sort out of the URL.
|
||||
|
||||
The shared table chrome now allows `TableCardHeader` to expose a right-aligned
|
||||
action slot, currently used by the Workloads/Proxmox metric display control.
|
||||
@@ -5683,6 +5728,13 @@ identity candidates) and pass them into the shared metric bar primitives
|
||||
(`ResponsiveMetricCell`, `StackedMemoryBar`, `StackedDiskBar`); the static
|
||||
`METRIC_THRESHOLDS` display constants remain fallback-only presentation for
|
||||
callers without alert configuration in scope.
|
||||
Workload guest bars pass the guest's alert policy tags
|
||||
(`getWorkloadAlertPolicyTags`) through that same `getMetricThresholds` call,
|
||||
so a Proxmox guest tagged `pulse-relaxed` colours CPU, memory and disk by the
|
||||
relaxed floor the engine applies to its configured thresholds; bar
|
||||
primitives never read tags themselves. The Alerts thresholds table's shared row and mobile card render
|
||||
the relaxed-trigger line from `getAlertResourceRelaxedTriggerSummary`, not
|
||||
from per-section copy.
|
||||
The vSphere ESXi host table keeps power state distinct from aggregate resource
|
||||
health without spending a phone column on the repeated normal case. At phone
|
||||
widths the Power column is hidden and a shared `MetadataBadge` appears beside
|
||||
@@ -5774,11 +5826,6 @@ compose that primitive instead of copying rounded-full active selector styling.
|
||||
`ResourcePicker` report-domain filters are part of that boundary: the picker
|
||||
owns the reportable resource categories and labels, but the type selector shell
|
||||
must come from `FilterButtonGroup`.
|
||||
Chart visibility display actions are registry-backed too.
|
||||
`ChartVisibilityToggleButton` owns the `Show charts` / `Hide charts` label,
|
||||
pressed-state, title, icon, and toolbar action styling for summary-bearing
|
||||
filter surfaces. Pages must compose that primitive instead of recreating local
|
||||
chart visibility buttons or one-option segmented controls.
|
||||
Column visibility controls are registry-backed too. `ColumnPicker` owns the
|
||||
column chooser trigger, panel title, reset action, empty-state copy, hidden
|
||||
count badge, dropdown width, and outside-click lifecycle through the shared
|
||||
@@ -5961,14 +6008,14 @@ pages that need workspace tabs or count chips next to search should route that
|
||||
through the shared `searchLeading` slot instead of recreating a second local
|
||||
header strip above the control bar.
|
||||
|
||||
Pages that filter a list-of-resources surface (Infrastructure, Workloads,
|
||||
Storage, Recovery Protection coverage, Recovery events) compose the chip-based
|
||||
Pages that filter a list-of-resources surface (Workloads, Storage, and the
|
||||
Proxmox Backups tab) compose the chip-based
|
||||
`frontend-modern/src/components/shared/FilterBar/FilterBar.tsx` shell instead
|
||||
of `PageControls`. Each page declares a `FilterDef[]` catalog (label, options,
|
||||
value, defaultValue, group); `FilterBar` renders chips for active filters and
|
||||
exposes the rest behind a "+ Filter" menu, with type-ahead at both the menu
|
||||
and chip popovers (`AddFilterMenu` and `FilterChip`). Low-frequency view options
|
||||
(grouping segmented control, charts toggle, columns picker, sort key) compose
|
||||
(grouping segmented control, columns picker, sort key) compose
|
||||
the shared `ViewOptionsDisclosure` through `FilterBar`'s `viewOptions` prop
|
||||
instead of remaining as permanent toolbar controls. `FilterBar` owns the View
|
||||
trigger and inline disclosure; feature consumers pass only panel content and
|
||||
@@ -5978,9 +6025,9 @@ than passing an empty conditional wrapper that leaves a dead View trigger.
|
||||
Contextual frequent actions may use `leadingControls`, while table counters,
|
||||
active trend ranges, and other persistent orientation readouts may use
|
||||
`trailingControls`. Platform tables inherit the same ownership through
|
||||
`PlatformTableToolbar`. Recovery is event-first and does not use equal
|
||||
workspace subtabs for protected rollups versus event history; Storage subtabs
|
||||
(Pools / Physical Disks) sit above the bar as navigation, not filters.
|
||||
`PlatformTableToolbar`. View switches sit above the bar as navigation, not
|
||||
filters: the Proxmox Backups tab's `By date` / `Coverage` `PlatformSectionTabs` and
|
||||
the Storage subtabs (Pools / Physical Disks).
|
||||
`FilterBar` owns committed infrastructure search terms as removable pills,
|
||||
separate from its consumer-owned structured `FilterDef` chips. An exact
|
||||
infrastructure completion or a recognized abbreviated query clears the draft
|
||||
@@ -6220,6 +6267,13 @@ When a shared portal tooltip is already visible, that same owner must
|
||||
reschedule positioning on live coordinate and viewport changes so chart hover
|
||||
tooltips keep following the active pointer instead of sticking to their first
|
||||
anchor.
|
||||
Only a visible tooltip holds a window resize listener. Hidden instances (one
|
||||
per metric bar or sparkline on every table row) and portals suppressed on a
|
||||
touch device register none and re-read the viewport when they open, so the
|
||||
listener count no longer grows with rows. `TooltipPortal.test.tsx` proves that
|
||||
hidden and touch-suppressed portals add no listener, that an open one re-clamps
|
||||
on a window resize, and that one opened after an unheard resize starts from the
|
||||
live viewport.
|
||||
Floating hover tooltips are a fine-pointer interaction only. The shared
|
||||
tooltip hook, portal, and singleton API must suppress them when the primary
|
||||
device reports no hover capability or a coarse pointer, because touch browsers
|
||||
@@ -6273,7 +6327,7 @@ an absent list as an empty audit history.
|
||||
That shared filter-option primitive is also the canonical owner for default
|
||||
`All <scope>` option wording wherever a product surface exposes filter selects
|
||||
or segmented filter choices. Workloads filters, storage source
|
||||
filters, recovery history and platform/type filters, Kubernetes namespace
|
||||
filters, Kubernetes namespace
|
||||
drawers, resource-change timeline filters, and alert configuration options must
|
||||
call `frontend-modern/src/components/shared/filterOptionPresentation.ts` through
|
||||
their nearest presentation/model owner instead of hard-coding page-local `All
|
||||
@@ -6368,18 +6422,13 @@ landing fallback. Shared desktop and mobile navigation must tolerate a missing
|
||||
active tab for those paths while still highlighting canonical active routes
|
||||
such as Proxmox, Docker, Kubernetes, TrueNAS, vSphere, Machines, Alerts,
|
||||
Patrol, and Settings.
|
||||
The recovery feature shell now also depends on the shared
|
||||
`frontend-modern/src/components/shared/Subtabs.tsx` primitive for its primary
|
||||
protected-items versus recovery-events workspace switch. The recovery lane may
|
||||
own the active view and route-state semantics, but the top-level tab framing
|
||||
must stay on the canonical shared subtabs control instead of reviving a
|
||||
recovery-local switcher pattern. When recovery embeds that switcher inside the
|
||||
page shell, it should follow the same ordering already used by storage: shared
|
||||
subtabs row first, shared controls card second, and data card after that. The
|
||||
contained styling should come from the same canonical subtabs shell, list, and
|
||||
button class treatment already used by established Pulse surfaces rather than
|
||||
from a recovery-only variant boundary, adjacent chip row, or recovery-local
|
||||
filter-row embedding.
|
||||
Recovery has no workspace switch of its own: the protected-items versus
|
||||
recovery-events `Subtabs` switch left with the aggregate Recovery page on
|
||||
2026-05-26. The Proxmox Backups tab switches between `/proxmox/backups/date`
|
||||
and `/proxmox/backups/coverage` through the shared `PlatformSectionTabs`
|
||||
row above its `FilterBar`, and the TrueNAS Protection tab is a single table.
|
||||
A future recovery view switch belongs on those platform-page primitives, not
|
||||
on a recovery-local switcher, chip row, or filter-row embedding.
|
||||
The shared table primitives now also need to preserve caller-owned separator
|
||||
styling. `TableHeader` and `TableBody` may provide canonical default borders
|
||||
and dividers, but when a caller supplies explicit border or divide classes the
|
||||
@@ -6400,63 +6449,38 @@ 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 recovery shell boundary now also owns one canonical top-level filter
|
||||
controller in
|
||||
`frontend-modern/src/features/recovery/useRecoverySurfaceState.ts`. Route-backed
|
||||
recovery filters such as the provider-neutral `itemType` selector must be
|
||||
derived, normalized, and fanned out to inventory, history, activity, facets,
|
||||
and series consumers from that shared state owner rather than being recreated
|
||||
as page-local toolbar state inside individual recovery sections.
|
||||
That same shared recovery filter boundary also owns canonical recovery
|
||||
item-type derivation through
|
||||
`frontend-modern/src/utils/recoveryItemTypePresentation.ts`. Recovery shell
|
||||
state, tables, summaries, and point-detail surfaces must resolve rollup and
|
||||
point item types through the shared presenter helpers instead of repeating
|
||||
`display.itemType` / `subjectType` / `subjectRef.type` fallback chains in
|
||||
page-local consumers.
|
||||
That same shared recovery decode boundary also owns canonical recovery display
|
||||
shape. `frontend-modern/src/utils/recoveryPlatformModel.ts`,
|
||||
`frontend-modern/src/hooks/useRecoveryPoints.ts`, and
|
||||
`frontend-modern/src/hooks/useRecoveryRollups.ts` must normalize legacy
|
||||
transport display aliases like `subjectLabel` and `subjectType` into canonical
|
||||
runtime `itemLabel` and `itemType` fields before recovery presenters consume
|
||||
the model.
|
||||
The same shared recovery-column boundary must keep legacy `subject` and
|
||||
`source` column ids at migration-only scope once
|
||||
`frontend-modern/src/hooks/useColumnVisibility.ts` owns alias rewrites.
|
||||
Recovery table runtime helpers and render switches should operate on canonical
|
||||
`item` and `platform` ids rather than carrying the deleted ids as live cases.
|
||||
That same shared recovery state owner now also keeps `platform` as the
|
||||
canonical route and transport filter name for operator-facing recovery links,
|
||||
while any accepted legacy `provider` aliases remain parser compatibility only.
|
||||
Caller-facing shared recovery route builders must therefore stay
|
||||
platform-first as well: compatibility `provider` aliases may be accepted while
|
||||
parsing legacy links, but they should not remain a first-class input on new
|
||||
recovery link construction helpers.
|
||||
Recovery frontend decode and derived option builders must treat payload
|
||||
`platform` / `platforms` as the canonical response fields and only fall back
|
||||
to legacy `provider` / `providers` aliases for compatibility, so route,
|
||||
filter, and table state do not keep backend-era vocabulary alive as the
|
||||
default client model.
|
||||
That normalization belongs at the shared recovery transport boundary in
|
||||
`frontend-modern/src/hooks/useRecoveryPoints.ts` and
|
||||
`frontend-modern/src/hooks/useRecoveryRollups.ts`, not in individual tables,
|
||||
drawers, or summary cards. Recovery components should receive canonical
|
||||
platform-first runtime models rather than re-deriving legacy alias fallback
|
||||
locally.
|
||||
Recovery section owners under `frontend-modern/src/components/Recovery/` must
|
||||
consume that shared `platform` filter surface directly. They must not keep
|
||||
recovery-local `provider` route/query vocabulary alive behind renamed labels,
|
||||
or the UI will drift back to backend-shaped navigation even when the copy says
|
||||
`Platform`.
|
||||
That same shared recovery filter owner must also preserve route-owned platform
|
||||
visibility while transport-backed options are still hydrating. If
|
||||
`frontend-modern/src/features/recovery/useRecoverySurfaceState.ts` restores a
|
||||
canonical `platform` selection such as `truenas` from the route before the
|
||||
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.
|
||||
Recovery has no shell, filter controller, route serializer, or column model
|
||||
left for frontend primitives to govern. The aggregate Recovery page, its
|
||||
section owners under `frontend-modern/src/components/Recovery/`, and its state
|
||||
owner `frontend-modern/src/features/recovery/useRecoverySurfaceState.ts` were
|
||||
deleted on 2026-05-26 together with the rollup, facet, and series hooks
|
||||
(`useRecoveryRollups.ts`, `useRecoveryPointsFacets.ts`,
|
||||
`useRecoveryPointsSeries.ts`), and the recovery query serializer and
|
||||
`recoveryItemTypePresentation.ts` that only it read have been deleted since.
|
||||
Recovery rows render in two platform-owned tabs built from the shared
|
||||
platform-page primitives: the Proxmox Backups tab and the TrueNAS Protection
|
||||
tab.
|
||||
The Proxmox Backups tab parses `/api/backups/pve` and `/api/backups/pbs`
|
||||
inside `ProxmoxBackupsTable.tsx` and reads posture through
|
||||
`frontend-modern/src/hooks/useProtectionPostures.ts`. The TrueNAS Protection
|
||||
tab reads recovery points, and what binds that path is the decode boundary.
|
||||
`frontend-modern/src/utils/recoveryPlatformModel.ts` maps legacy transport
|
||||
aliases onto the canonical runtime model: `provider` to `platform`,
|
||||
`subjectResourceId` to `itemResourceId`, `subjectRef` to `itemRef`, and
|
||||
`display.subjectLabel` / `display.subjectType` to `display.itemLabel` /
|
||||
`display.itemType`. `frontend-modern/src/hooks/useRecoveryPoints.ts`, the
|
||||
only recovery-points reader, applies `normalizeRecoveryPointsResponse` to
|
||||
every `/api/recovery/points` response, so the TrueNAS Protection tab receives
|
||||
platform-first, item-first points. The `display.subjectLabel`,
|
||||
`display.subjectType`, and `subjectRef` fallbacks that
|
||||
`TrueNASProtectionTable.tsx` and `truenasPageModel.ts` still read are dead on
|
||||
that path, because normalized points never carry those fields; new recovery
|
||||
code reads the canonical fields only. New recovery filters, links, or view
|
||||
switches belong to the platform tab that owns the rows, on the platform-page primitives, rather than in a revived
|
||||
cross-platform recovery shell or a shared recovery serializer in
|
||||
`frontend-modern/src/routing/resourceLinks.ts`. The rollup normalizer
|
||||
(`normalizeRecoveryRollupsResponse`) has had no production reader since
|
||||
`useRecoveryRollups.ts` was deleted.
|
||||
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
|
||||
@@ -6671,6 +6695,16 @@ 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.
|
||||
A notice names each affected resource by the label its page's table shows for
|
||||
the row. The sensor-setup notice leads with the node's trimmed resource name
|
||||
(the operator-set display name when one exists, else the Proxmox node name),
|
||||
falling back to the id as the nodes table does, and adds the Proxmox node name
|
||||
in parentheses unless the label already contains it as a whole word, ignoring
|
||||
ASCII case, or as the host part of a domain name, as in
|
||||
`West Production C (pve3)`. Both names are needed: the nodes table switches to
|
||||
the Proxmox node name in its narrowest layout, and the setup script is re-run
|
||||
on that Proxmox node, so the rule is stricter than `hasAlternateDisplayName` in
|
||||
the guest table's node group header and keeps `pve-3` beside a `PVE3` label.
|
||||
|
||||
Alert incident-event filter containers, labels, and chips must now route
|
||||
through the shared presentation helpers in
|
||||
@@ -6826,11 +6860,12 @@ That same Patrol shell ownership includes refresh affordance state:
|
||||
`frontend-modern/src/features/patrol/usePatrolIntelligenceState.ts` must keep
|
||||
operator refresh controls generation-aware, timeout-bounded, and separate from
|
||||
background polling state, so a slow supporting intelligence read cannot make the
|
||||
shared Patrol header Refresh Patrol action spin indefinitely or stay disabled
|
||||
Patrol stale-data Retry action spin indefinitely or stay disabled
|
||||
while Patrol findings and status remain visible.
|
||||
Neither the Patrol load and poll path (the store's `loadDashboardData` bundle)
|
||||
nor the Retry path's background supporting reads fetch the global
|
||||
learned-correlation list, which no Patrol surface shows.
|
||||
learned-correlation list or the provider circuit-breaker status, neither of
|
||||
which any Patrol surface shows.
|
||||
That same Patrol shell should make scoped trigger policy legible without
|
||||
another navigation step. `frontend-modern/src/features/patrol/PatrolIntelligenceHeader.tsx`
|
||||
should keep actionable scoped-trigger state legible without promoting
|
||||
@@ -7424,20 +7459,19 @@ customer-facing action copy, while
|
||||
provider labels, empty states, and action/status messaging. Future settings
|
||||
copy changes in those areas should extend these helpers instead of inlining
|
||||
panel-local strings inside the shell or reactive state owners.
|
||||
Shared infrastructure action-link framing now also owns recovery entry wording
|
||||
for service resources. `frontend-modern/src/components/Infrastructure/serviceDetailLinks.ts`
|
||||
must keep platform-service recovery links on canonical recovery-events
|
||||
framing and route state, so upstream service surfaces do not drift back to
|
||||
PBS-backup wording or inherit the page-default inventory workspace when they
|
||||
are actually deep-linking into recovery activity.
|
||||
That same shared primitive boundary also owns resource handoff chip framing for
|
||||
cross-surface investigation UI. Alerts, Patrol, and similar feature shells may
|
||||
choose which governed surfaces to show, but they must build those links through
|
||||
the shared resolved-resource route helpers in
|
||||
`frontend-modern/src/routing/resourceLinks.ts` instead of freezing raw route
|
||||
strings, local link dedupe, or provider-specific link chips inside feature
|
||||
panels. Shared chip styling belongs in the feature shell; canonical href and
|
||||
label truth belongs in the shared route helper.
|
||||
Shared infrastructure action-link framing owns service-resource action links.
|
||||
`frontend-modern/src/components/Infrastructure/serviceDetailLinks.ts` returns
|
||||
only the PMG thresholds link today: the PBS recovery-events link it used to
|
||||
build was retired with the platform-first route migration, and PBS backups are
|
||||
reached through the Proxmox Backups tab. A future service recovery link must
|
||||
target the platform tab that owns those rows rather than a recovery-events
|
||||
workspace.
|
||||
Alerts, Patrol, and similar feature shells render no resource handoff chips
|
||||
since the cross-jump strips were retired on 2026-05-16 (see the retired-helper
|
||||
note above). A future cross-surface link must take its href and label from a
|
||||
route helper in `frontend-modern/src/routing/resourceLinks.ts` instead of
|
||||
freezing raw route strings, local link dedupe, or provider-specific link chips
|
||||
inside a feature panel; chip styling belongs in the feature shell.
|
||||
That same shared primitive boundary now also owns persisted column-identity
|
||||
migration for governed surfaces. When a v6 surface canonicalizes saved column
|
||||
IDs, `frontend-modern/src/hooks/useColumnVisibility.ts` must accept explicit
|
||||
|
||||
@@ -5826,3 +5826,24 @@ that actually accepted the same occurrence. The connected
|
||||
HTTP 200/503 split, reopens the persistent queue, and requires the old accepted
|
||||
destination's recovery plus the new firing, with no recovery to the unannounced
|
||||
destination. Quiet hours and disabled recovery controls still apply.
|
||||
|
||||
### Broadcast host coalescing honours operator splits
|
||||
|
||||
The websocket broadcast coalesced top-level host views with
|
||||
`CoalescePresentationHostResources`, which applies no merge exclusions, while
|
||||
the resources API's `ResourceRegistry.ListForPresentation` applies the
|
||||
registry's. A host pair the operator split (unlink or report-merge) could
|
||||
therefore be one broadcast row and two REST rows.
|
||||
`Monitor.buildBroadcastFrontendStateFromSnapshotWithClock` now calls
|
||||
`coalesceResourcesForPresentation`, which asks the view's read state for
|
||||
`MonitorAdapter.CoalesceForPresentation` when its registry is store-backed
|
||||
(the resource store's adapter, or a host-continuity overlay that loaded the
|
||||
store's exclusions afresh), and otherwise the resource store's adapter: the
|
||||
mock-mode view lists through a store-less registry that carries no operator
|
||||
decisions. Both surfaces share `presentationExclusionFilter`. Polling, rebuild cadence,
|
||||
alert evaluation and the broadcast payload shape are unchanged.
|
||||
`TestBroadcastPresentationCoalesceHonoursMergeExclusions` compares broadcast
|
||||
and REST row counts with and without the split, and
|
||||
`TestBroadcastPresentationCoalescePrefersTheListingReadState` pins the
|
||||
listing read state over an older store generation and the store's exclusions
|
||||
for a mock-style view.
|
||||
|
||||
@@ -329,13 +329,14 @@ or historical proof/counting for resolved-only work.
|
||||
shared parent node, or explicit correlated finding IDs. Grouping must not
|
||||
infer causality from title or description prose, and the queue's affected
|
||||
resource count must continue to count canonical resources rather than
|
||||
visual groups. Because a
|
||||
contextual Assistant handoff from that workflow is still a first-party Patrol
|
||||
starter for the same governed journey, it must record content-free workflow
|
||||
prompt activity through the shared marker route with the `pulse_patrol`
|
||||
surface before the drawer opens; it must not include finding IDs, prompt
|
||||
text, resource context, model output, or direct-action payloads in that
|
||||
marker.
|
||||
visual groups. A contextual Assistant handoff from that workflow opens
|
||||
through `frontend-modern/src/components/AI/FindingsPanel.tsx` and records no
|
||||
workflow starter today, so the first-party `pulse_patrol` marker has no
|
||||
browser producer. If that handoff starts recording one, it must write
|
||||
content-free workflow prompt activity through the shared marker route with
|
||||
the `pulse_patrol` surface before the drawer opens, with no finding IDs,
|
||||
prompt text, resource context, model output, or direct-action payloads in
|
||||
that marker.
|
||||
A compact Patrol work-group row may appear above the list only when it adds
|
||||
cross-source current-work grouping that the row title alone cannot express:
|
||||
pending approvals, failed governed actions, a failed/latest Patrol check,
|
||||
@@ -471,8 +472,11 @@ or historical proof/counting for resolved-only work.
|
||||
compact progress label must describe current work state (`ready`, `needs
|
||||
attention`, `approval needed`, `outcome verified`, `no active work`) instead
|
||||
of repeating the selected mode; the Patrol mode selector/header
|
||||
owns current-mode copy, and local Patrol state must expose work evidence as
|
||||
owns current-mode copy, and any local Patrol work evidence must be
|
||||
Patrol-owned issue/work counts rather than legacy proof counts.
|
||||
No Patrol surface reads issue-evidence, governed-action decision, or
|
||||
verified-outcome counts today, so `usePatrolIntelligenceState.ts` computes
|
||||
none of them.
|
||||
When no active finding or pending approval remains, terminal verified or
|
||||
rejected outcomes must read as history behind a `no active work` current
|
||||
state; current-state copy must not ask operators to reconcile old approved
|
||||
@@ -771,12 +775,12 @@ attention`, `approval needed`, `outcome verified`, `no active work`) instead
|
||||
only block the selected run view while the run references finding ids that
|
||||
still need the direct Patrol findings payload.
|
||||
Patrol page refresh state must also separate background data loads from the
|
||||
operator-clicked Update status action: slow or stalled supporting
|
||||
intelligence reads may continue in the background, but they must not make the
|
||||
shared header action spin or stay disabled once Patrol findings and status
|
||||
remain visible. The header action is a status/history sync affordance; it
|
||||
must not read like another Patrol run or imply that it changes
|
||||
infrastructure.
|
||||
operator-clicked Retry on the stale-data banner: background polls hold no
|
||||
busy state, and slow or stalled supporting intelligence reads may continue
|
||||
in the background, but they must not make Retry spin or stay disabled once
|
||||
Patrol findings and status remain visible. Retry is a status/history sync
|
||||
affordance; it must not read like another Patrol run or imply that it
|
||||
changes infrastructure.
|
||||
Patrol trigger-status copy in the default header and activity strip must stay
|
||||
actionable. Runtime policy pauses for alert/anomaly-triggered background
|
||||
checks, such as the local development safety guard, may explain a blocked
|
||||
@@ -986,7 +990,7 @@ clear`, `Found N new issues`, `Fixed N issues`, `N issues still open`, or
|
||||
answer. Finding
|
||||
handoffs must be assembled through the Patrol-owned handoff model so the
|
||||
prompt, visible briefing, model-only finding context, resource reference,
|
||||
bounded action reference, and request-local approval-required posture stay in
|
||||
bounded action reference, and drawer approval-required disclosure stay in
|
||||
sync. The model-only context may include
|
||||
current finding status, recurrence, investigation record facts, evidence,
|
||||
verification, approval state, dry-run posture, existing action artifact
|
||||
@@ -997,8 +1001,8 @@ clear`, `Found N new issues`, `Fixed N issues`, `N issues still open`, or
|
||||
prompt-only local shortcut: pass approval ID/status/risk/target plus safe
|
||||
summary/count metadata as review context, attach the target resource
|
||||
reference, include bounded `handoff_actions` for live approvals or structured
|
||||
action artifacts when present, force the request-local approval-required mode,
|
||||
attach the Patrol-owned visible drawer briefing for the pending approval or
|
||||
action artifacts when present, mark the drawer's approval-required
|
||||
disclosure, attach the Patrol-owned visible drawer briefing for the pending approval or
|
||||
queued-fix recovery state, and never paste the approval command or
|
||||
action command text into the chat prompt. Existing remediation-plan or
|
||||
action-plan artifacts follow the same boundary: plan status, risk, and
|
||||
@@ -1006,11 +1010,12 @@ clear`, `Found N new issues`, `Fixed N issues`, `N issues still open`, or
|
||||
the LLM to critique, but visible handoffs must not render Patrol step lists
|
||||
or suggested prompt chips, and command or rollback command text stays in the
|
||||
governed remediation or approval surface. Generic finding
|
||||
discussion handoffs must also force request-local approval-required mode for
|
||||
any non-empty Patrol `finding_id`, including context-only findings and
|
||||
findings that reference a live approval, action artifact, fix outcome, or
|
||||
remediation plan, so default autonomous Assistant settings cannot bypass the
|
||||
Patrol action-governance boundary. The assembled handoff must still pass
|
||||
discussion handoffs for any non-empty Patrol `finding_id`, including
|
||||
context-only findings and findings that reference a live approval, action
|
||||
artifact, fix outcome, or remediation plan, run in the approval-required
|
||||
mode `/api/ai/chat` sets server-side for every request; the browser sends no
|
||||
execution-mode field, so default autonomous Assistant settings cannot bypass
|
||||
the Patrol action-governance boundary. The assembled handoff must still pass
|
||||
through the Assistant runtime's resource-policy sanitizer before prompt
|
||||
injection, so Patrol-owned prose
|
||||
cannot leak governed resource names, IDs, aliases, nodes, paths, or
|
||||
@@ -1190,8 +1195,9 @@ fix`, or `Explain` based on current finding state), while secondary
|
||||
the model-only run context: it resolves the run ID from Patrol history,
|
||||
rebuilds bounded run facts, scoped resource references, sanitized analysis,
|
||||
and classified failure detail server-side, and rehydrates the same context
|
||||
from stored metadata on follow-up turns. It must force request-local
|
||||
approval-required mode, present a source-named visible drawer briefing, and
|
||||
from stored metadata on follow-up turns. The exchange runs in the
|
||||
server-set approval-required mode; the handoff must present a source-named
|
||||
visible drawer briefing and
|
||||
frame Assistant as explanation and next-step review rather than execution or
|
||||
automatic retry authority.
|
||||
9. Keep the normal Patrol status summary plain and operator-first rather
|
||||
@@ -2191,10 +2197,13 @@ requester identity as safe metadata, without copying the approval command
|
||||
payload into Assistant.
|
||||
That same store now owns the Patrol dashboard load bundle as well, so the
|
||||
page refresh path stays aligned on a single orchestrated AI bundle instead of
|
||||
repeating the individual summary, findings, circuit-breaker, and approval
|
||||
fetches inline. Neither that bundle nor the Patrol refresh path fetches the
|
||||
global learned-correlation list: the Patrol page renders no correlations, so
|
||||
the store holds no copy of that list.
|
||||
repeating the individual summary, findings, and approval fetches inline.
|
||||
Neither that bundle nor the Patrol refresh path fetches the global
|
||||
learned-correlation list: the Patrol page renders no correlations, so
|
||||
the store holds no copy of that list. Nor do they fetch the provider
|
||||
circuit-breaker status: an open breaker already reaches the page as the
|
||||
`circuit_open` Patrol runtime block on `/api/ai/patrol/status`, so the store
|
||||
holds no separate breaker state.
|
||||
The shared
|
||||
`frontend-modern/src/components/Infrastructure/ResourceChangeSummary.tsx` and
|
||||
`frontend-modern/src/components/Infrastructure/ResourceCorrelationSummary.tsx`
|
||||
|
||||
@@ -1092,11 +1092,13 @@ change may globally weaken the Task 03 lifecycle-state idempotency invariant.
|
||||
mounted, report errors out of band, and must not rely on Solid
|
||||
`createResource` in a way that can bubble into the route-level Suspense
|
||||
fallback or the setup/welcome shell.
|
||||
Workload row drawer expansion, local row focus, and summary group pinning
|
||||
are local interaction state. Inbound Workloads deep links may hydrate
|
||||
`resource` and `summaryGroup` into the focused row or group, but opening,
|
||||
closing, or clearing an inline drawer must not write those params back to
|
||||
route state, schedule router navigation, or trip the app-shell fallback.
|
||||
Workload row drawer expansion and local row focus are local interaction
|
||||
state. Inbound Workloads deep links may hydrate `resource` into the focused
|
||||
row, but opening, closing, or clearing an inline drawer must not write that
|
||||
param back to route state, schedule router navigation, or trip the
|
||||
app-shell fallback. Workloads has no group focus, so it reads no
|
||||
`summaryGroup` param: row expansion stopped writing it and no link builder
|
||||
passes one, so its hydration was removed with the group pin.
|
||||
URL synchronization for Workloads stays limited to filter-owned scope in
|
||||
`frontend-modern/src/components/Workloads/useWorkloadUrlSync.ts`.
|
||||
The Workloads page may show a bounded partial-inventory banner when a
|
||||
@@ -1125,6 +1127,10 @@ change may globally weaken the Task 03 lifecycle-state idempotency invariant.
|
||||
8. Render workload row identity directly from the shared canonical workload helper so row selection, hover, and fallback metadata lookup stay aligned with the same workload contract
|
||||
9. Format infrastructure sensor labels through the shared `frontend-modern/src/utils/textPresentation.ts` presentation helper instead of maintaining a local title-casing implementation in `frontend-modern/src/components/Infrastructure/resourceDetailMappers.ts`
|
||||
10. Extend workload row contract and per-row hot-path derivations through `frontend-modern/src/components/Workloads/guestRowModel.tsx` and `frontend-modern/src/components/Workloads/useGuestRowState.ts`, and extend tooltip-backed row cell presentation through `frontend-modern/src/components/Workloads/GuestRowCells.tsx`, rather than rebuilding column metadata, row identity, cell tooltips, or anomaly correlation inside `frontend-modern/src/components/Workloads/GuestRow.tsx`
|
||||
Guest metric thresholds read the guest's alert policy tags through one
|
||||
`getWorkloadAlertPolicyTags` memo per row and per drawer, beside the
|
||||
existing scope and override-candidate memos, so `pulse-relaxed` bar
|
||||
colouring adds no per-render tag parsing and no extra store reads.
|
||||
On coarse-pointer touch layouts, those cell details must not spawn floating
|
||||
hover tooltips from synthesized mouse events; a row tap remains the primary
|
||||
drawer action. Tooltip suppression belongs to the shared hover-capability
|
||||
@@ -1473,13 +1479,16 @@ change may globally weaken the Task 03 lifecycle-state idempotency invariant.
|
||||
`TableCardHeader` inside both the populated and filtered-empty table card.
|
||||
A platform page must not place a parallel floating title above its filters;
|
||||
the title/count band belongs to the same framed table surface as the rows.
|
||||
`WorkloadPanel` owns the mutually exclusive host/guest drawer handoff:
|
||||
clicking a grouped host row while a guest drawer is open must clear the
|
||||
selected guest and keep/focus the host summary group so the node drawer can
|
||||
replace the guest drawer. Platform pages that render a dedicated host
|
||||
table above an embedded Workloads table may disable the grouped host drawer
|
||||
through the explicit Workloads surface option, but standalone Workloads
|
||||
must keep the inline grouped-row drawer as the default.
|
||||
`WorkloadPanel` renders only the guest drawer. Grouped host rows are
|
||||
static dividers: no click handler, hover preview, group pin, or inline
|
||||
node drawer. They keep `data-summary-group-id` so a click on a divider
|
||||
does not clear an open guest drawer. Host details open from the platform
|
||||
page's own hosts table (`ProxmoxNodesTable` mounts `NodeDrawer`;
|
||||
`VsphereHostsTable` owns its detail row). The former
|
||||
`groupNodeDrawerMode` option defaulted to an inline grouped-row drawer
|
||||
that only the retired standalone Workloads page used; both production
|
||||
mounts passed `disabled`, so the option, the inline `NodeDrawer` mount
|
||||
and the Workloads group hover/pin scope were removed.
|
||||
Compact icon headers inside `WorkloadTableHeader.tsx` may stay visually
|
||||
dense for responsive workload tables, but the icon must be decorative and
|
||||
the column label must remain present through an `sr-only` label so the
|
||||
@@ -1691,18 +1700,26 @@ shell clickable behind another overlay.
|
||||
scoped table metric-history interaction that requested them. Platform
|
||||
landing should stay route-module warm and data-light until the selected
|
||||
platform page owns its normal resource/table query.
|
||||
Platform pages that embed `WorkloadsSurface` reuse the canonical
|
||||
workloads filter toolbar through the `showFilterToolbar` +
|
||||
`suppressPlatformFilter` props in `WorkloadsSurfaceProps`. The page
|
||||
keeps `tableOnly` to hide the dashboard cards and summary strip but
|
||||
opts in to the same shared `FilterBar`, `GroupedTableModeSegmentedControl`,
|
||||
`ColumnPicker`, status/type chips, and search-history primitives that
|
||||
the global Workloads page renders, so platform operators get
|
||||
dense-table search, sort, grouping, view, status, and column controls
|
||||
on every embedded workloads tab without spawning a forked toolbar.
|
||||
The platform scope flows through `forcedPlatform` as a typed page
|
||||
input; `suppressPlatformFilter` drops the now-redundant Platform chip
|
||||
from the rendered toolbar so the user never sees a removable lock.
|
||||
Two platform pages embed `WorkloadsSurface`: the Proxmox and vSphere
|
||||
Overview tabs (`frontend-modern/src/features/proxmox/ProxmoxPageSurface.tsx`
|
||||
and `frontend-modern/src/features/vmware/VmwarePageSurface.tsx`). Each page
|
||||
builds the workloads state itself with `useWorkloadsState`, renders the
|
||||
one shared `WorkloadsFilter` toolbar (search, status filter, a type
|
||||
filter that vSphere suppresses, and the View options for grouping, metric display, and columns on the
|
||||
shared `FilterBar`) from that state, and hands the same state to `WorkloadsSurface` through its
|
||||
`state` prop with `suppressFilterToolbar`, so the surface skips its own
|
||||
filter row and the page never stacks two toolbars wired to one state.
|
||||
Proxmox also passes that state's search, plus its page-owned metric mode
|
||||
and history range, to the nodes table above the guests, so one toolbar
|
||||
drives both tables. `WorkloadsSurface` has no `tableOnly` mode, no
|
||||
`showFilterToolbar` prop, and no dashboard cards or summary strip to
|
||||
hide: it renders its filter row (unless suppressed or in kiosk mode), the
|
||||
workloads table, or an empty-state table card. The platform scope flows
|
||||
through `forcedPlatform` as a typed page input to `useWorkloadsState`,
|
||||
the toolbar, and the surface. `suppressPlatformFilter: true` makes the
|
||||
state expose no Platform filter config and both pages pass
|
||||
`platformFilter={undefined}` to the toolbar, so the user never sees the
|
||||
locked platform as a removable chip.
|
||||
|
||||
### Navigation resolves from a bounded request
|
||||
|
||||
@@ -1788,6 +1805,22 @@ final measurements and host-load limitation remain recorded in
|
||||
`records/resource-payload-static-metadata-2026-08-24.md`. They do not establish
|
||||
a controlled performance improvement or satisfy the open SLO qualification.
|
||||
|
||||
### Window resize listeners do not grow with rows
|
||||
|
||||
Table rows must not each register a window `resize` listener.
|
||||
`frontend-modern/src/hooks/useBreakpoint.ts` keeps one module-level width
|
||||
signal, fed by a single rAF-debounced listener while at least one caller is
|
||||
mounted, and a new caller starts from the live `innerWidth`. Shared tooltip
|
||||
state in `frontend-modern/src/components/shared/useTooltipState.ts` listens
|
||||
only while its tooltip is visible. Measured on 2026-10-07 on a mock-mode
|
||||
`/proxmox` page with 140 workload rows at 1440 px, the active listener count
|
||||
fell from 859 (699 tooltip instances, 156 breakpoint callers) to 5; at 390 px
|
||||
with 36 rows it fell from 203 to 5. An open tooltip adds one listener and
|
||||
removes it on close, and crossing the phone breakpoint in either direction
|
||||
still switches the workload table and row layout. `useBreakpoint.test.ts`
|
||||
proves one listener for 50 callers, that back-to-back resizes leave one pending
|
||||
frame, and that every caller crosses a breakpoint when that frame runs.
|
||||
|
||||
### Large API responses negotiate gzip without corrupting edge cases
|
||||
|
||||
The main listener compresses eligible JSON and frontend text assets for
|
||||
@@ -2003,7 +2036,10 @@ because both remaining production mounts (Proxmox and vSphere overview)
|
||||
set it unconditionally, so the non-compact path no longer rendered
|
||||
anywhere. A new embedded consumer that needs host
|
||||
stats beside its workloads must render its own hosts table rather than
|
||||
re-growing metric cells in the shared group row.
|
||||
re-growing metric cells in the shared group row. The same applies to host
|
||||
interaction: the group row has no drawer, hover preview, or pin, and the
|
||||
Workloads state no longer builds per-group summary scopes for every
|
||||
filtered-guest change.
|
||||
|
||||
WorkloadsSurface stays monitoring-first. It must not render a persistent
|
||||
aggregate banner just because running VMs or system containers lack an
|
||||
@@ -2066,7 +2102,7 @@ the hovered timestamp, so density does not remove point-in-time metric detail.
|
||||
Proxmox node thermals follow that same drawer-only rule: temperature remains a
|
||||
node-context metric, not a universal Workloads table column, and the node detail
|
||||
drawer may add one compact `Thermals` history group beside utilization and I/O
|
||||
only after a grouped node row is selected. Host-agent CPU temperature must be
|
||||
only after a node's drawer is opened from the Proxmox nodes table. Host-agent CPU temperature must be
|
||||
persisted into the same metrics-store history stream as other agent metrics,
|
||||
with current node temperature displayed only as a drawer-local current reading
|
||||
while history is still accumulating; it must not become a synthetic history
|
||||
@@ -2106,7 +2142,7 @@ still reads nothing extra.
|
||||
The Proxmox node drawer overview should follow the existing guest drawer
|
||||
compact detail-section pattern and expose node-specific context such as platform,
|
||||
kernel, hardware, raw capacity, telemetry, and thermal facts rather than
|
||||
repeating the metric cells already visible in the grouped table row.
|
||||
repeating the metric cells already visible in the Proxmox nodes table row.
|
||||
Object drawers across workload, node, Docker, and unified-resource surfaces
|
||||
must keep their default render bounded to active attention plus a small
|
||||
additive context projection. Full provider-specific support workflows remain
|
||||
@@ -2415,16 +2451,19 @@ 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
|
||||
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
|
||||
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. The shared same-path scheduler must also own cleanup
|
||||
`frontend-modern/src/components/Workloads/useWorkloadSelectionState.ts` keeps
|
||||
workload selection local and never writes it back into the route; an inbound
|
||||
`resource` link only hydrates it. Local row focus keeps the scroll position
|
||||
through `preserveScrollableAncestorVerticalOffset` in
|
||||
`frontend-modern/src/components/shared/contextualFocus.ts` and reveals the
|
||||
opened detail through `summaryTableFocus.ts`, so opening a focused workload
|
||||
does not look like a full page reload. The root `frontend-modern/src/App.tsx`
|
||||
shell applies a pending `frontend-modern/src/utils/appShellScrollRestoration.ts`
|
||||
restore only when the route itself changes. 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.
|
||||
@@ -2739,11 +2778,12 @@ 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
|
||||
boundary for collapse: the owning table (Workloads for guests, the Proxmox
|
||||
nodes table for nodes, the Docker hosts table for Docker hosts) owns which
|
||||
inline row is selected and the close handler, while
|
||||
`frontend-modern/src/components/shared/ObjectDrawerHeader.tsx` owns the
|
||||
full-width semantic header button, phone target height, focus treatment, and
|
||||
collapse chevron. `WorkloadPanel` must pass the grouped node close handler into
|
||||
collapse chevron. `ProxmoxNodesTable` must pass its node close handler into
|
||||
`NodeDrawer`, the Docker hosts table must pass its close handler into
|
||||
`DockerHostDrawer`, and those shells must not fall back to a small icon-only
|
||||
collapse target or wrap interactive header actions inside a local button.
|
||||
@@ -2812,12 +2852,12 @@ 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. 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.
|
||||
section renders today, and `WorkloadsFilter` carries no chart-visibility
|
||||
control or `onChartsToggle` / `chartsCollapsed` props. A summary section that
|
||||
returns through a governed product decision must bring its own visibility
|
||||
control with 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
|
||||
|
||||
@@ -5836,6 +5836,7 @@
|
||||
"frontend-modern/src/features/platformPage/__tests__/platformEstateOverviewModel.test.ts",
|
||||
"frontend-modern/src/features/platformPage/__tests__/platformOverviewLayout.guardrails.test.ts",
|
||||
"frontend-modern/src/features/platformPage/PlatformOutdatedAgentNotice.test.tsx",
|
||||
"frontend-modern/src/features/platformPage/PlatformOutdatedSensorSetupNotice.test.tsx",
|
||||
"frontend-modern/src/features/platformPage/platformSearchSuggestions.test.ts"
|
||||
]
|
||||
},
|
||||
@@ -7822,7 +7823,6 @@
|
||||
"frontend-modern/src/types/recovery.ts",
|
||||
"frontend-modern/src/utils/recoveryArtifactModePresentation.ts",
|
||||
"frontend-modern/src/utils/recoveryDatePresentation.ts",
|
||||
"frontend-modern/src/utils/recoveryItemTypePresentation.ts",
|
||||
"frontend-modern/src/utils/recoveryOutcomePresentation.ts",
|
||||
"frontend-modern/src/utils/recoveryTimelineChartPresentation.ts",
|
||||
"frontend-modern/src/utils/recoveryTimelinePresentation.ts",
|
||||
@@ -7867,7 +7867,6 @@
|
||||
"frontend-modern/src/types/recovery.ts",
|
||||
"frontend-modern/src/utils/recoveryArtifactModePresentation.ts",
|
||||
"frontend-modern/src/utils/recoveryDatePresentation.ts",
|
||||
"frontend-modern/src/utils/recoveryItemTypePresentation.ts",
|
||||
"frontend-modern/src/utils/recoveryOutcomePresentation.ts",
|
||||
"frontend-modern/src/utils/recoveryTimelineChartPresentation.ts",
|
||||
"frontend-modern/src/utils/recoveryTimelinePresentation.ts"
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -2489,8 +2489,10 @@ signal source instead of advertising empty object browsers. The
|
||||
canonical TrueNAS adapter (`internal/truenas/provider.go::
|
||||
truenasRecordsFromSnapshot`) already emits the top-level TrueNAS
|
||||
appliance as a unified `agent` row tagged with the `truenas`
|
||||
platform, so TrueNAS defaults to `/truenas/overview` (the Systems
|
||||
sub-tab); the embedded `StorageSurface` lives at `/truenas/storage`.
|
||||
platform, so TrueNAS defaults to `/truenas/overview`, whose Overview
|
||||
sub-tab lists those systems in `TrueNASSystemsTable`. `/truenas/storage`
|
||||
renders the TrueNAS-owned `TrueNASStorageTopologyTable`, not
|
||||
`StorageSurface`; only the Proxmox Storage tab embeds `StorageSurface`.
|
||||
Any future platform that wants to default to a Systems / Hosts
|
||||
overview must first have its canonical resource adapter project the
|
||||
platform's top-level system as a unified resource so the builder
|
||||
@@ -2601,10 +2603,12 @@ application resource-provider or WebSocket lifecycle.
|
||||
in place through that shared runtime and the root app-shell restore path,
|
||||
instead of looking like a page refresh or remount.
|
||||
12. Keep summary-chart visibility a display preference, not a unified-resource
|
||||
filter. Platform/runtime pages may hide or restore chart sections through
|
||||
shared presentation controls, but those controls must not mutate resource
|
||||
identity, table membership, source scope, or summary-hover state. The
|
||||
retired top-level `/infrastructure` page, its summary chart strip, and its
|
||||
filter. The workload and infrastructure summary chart sections are
|
||||
retired and no platform page exposes a chart show/hide control today. If a
|
||||
governed product decision brings one back, hiding or restoring it must not
|
||||
mutate resource identity, table membership, source scope, or summary-hover
|
||||
state. The retired top-level
|
||||
`/infrastructure` page, its summary chart strip, and its
|
||||
saved-view/route-state machinery must not be reintroduced for this purpose.
|
||||
15. Keep operator-local resource search on the operator's own names.
|
||||
Resource-policy redaction is a transmission boundary (`docs/PRIVACY.md`),
|
||||
@@ -3060,10 +3064,11 @@ canonical WebSocket snapshot, including the public `vmware-vsphere` alias for
|
||||
the raw `vmware` source, rather than degrading a source-scoped page to periodic
|
||||
REST refreshes. VMware Overview passes that same source-scoped snapshot into
|
||||
the embedded Workloads state, so hosts and VMs share one inventory generation
|
||||
and one explicit refresh path. Beyond disabling the grouped host drawer, it
|
||||
passes no option that adds host metrics to grouped rows: the shared Workloads
|
||||
group row carries host identity only, and per-host stats stay in the page's
|
||||
own hosts table.
|
||||
and one explicit refresh path. It passes no option that adds host metrics to
|
||||
grouped rows: the shared Workloads group row carries host identity only, with
|
||||
no drawer, hover preview, or group pin, and per-host stats and details stay in
|
||||
the page's own hosts table. The Workloads route model no longer parses a
|
||||
`summaryGroup` param, because no group focus remains to hydrate.
|
||||
Its page-owned workload toolbar consumes the complete shared
|
||||
`getWorkloadsMetricFilterProps` binding, so vSphere VMs expose the same Bars,
|
||||
Trends, Details, History, range, and first-use discovery contract as every
|
||||
@@ -3208,9 +3213,10 @@ in the same pass. The K8s Namespaces and Deployments drawer "Open Pods" /
|
||||
route to `/kubernetes/workloads` rather than building
|
||||
retired aggregate workload URLs; the legacy context/namespace query filter
|
||||
does not carry forward because the new Kubernetes Workloads tab does not
|
||||
consume those parameters. Workloads, storage, and recovery route-state helpers
|
||||
in `frontend-modern/src/routing/resourceLinks.ts` now serialize query state
|
||||
only; the owning platform/runtime route supplies the pathname. New drawer or
|
||||
consume those parameters. The workloads and storage route-state helpers in
|
||||
`frontend-modern/src/routing/resourceLinks.ts` serialize query state only, and
|
||||
the owning platform/runtime route supplies the pathname; the recovery
|
||||
serializer was deleted once nothing read it. New drawer or
|
||||
correlation surfaces must anchor on platform routes (or stay-in-place drawer
|
||||
expansion) instead of resurrecting the retired top-level paths.
|
||||
The Proxmox Backups route-state vocabulary includes the opaque `location`
|
||||
@@ -3232,8 +3238,9 @@ recreate raw labelled `<select>` controls for those filters locally.
|
||||
The Proxmox platform page is a route-level consumer of canonical unified
|
||||
resources, not a new resource source. It filters the existing resource snapshot
|
||||
to Proxmox VE, Backup Server, Mail Gateway, storage, disk, and workload rows,
|
||||
then composes the existing Workloads, Storage, Recovery, and infrastructure
|
||||
table owners. Proxmox host row version, uptime, temperature, CPU, memory, disk,
|
||||
then composes the existing Workloads and Storage surfaces, its own Backups
|
||||
tab tables, and the infrastructure table owners; no Recovery surface exists to
|
||||
compose. Proxmox host row version, uptime, temperature, CPU, memory, disk,
|
||||
network I/O, and disk I/O presentation must derive from canonical resource
|
||||
facets and the shared `nodeFromResource` adapter; platform pages must not
|
||||
rebuild resource identity, merge policy, or metric-target inference locally.
|
||||
@@ -4791,32 +4798,30 @@ canonical workload identity (`<instance>:<node>:<vmid>`) in the shared
|
||||
`resource` query rather than an opaque unified-resource id, so direct route
|
||||
loads and cross-surface drill-downs reopen the correct workload drawer instead
|
||||
of landing on an unselected table state.
|
||||
That same routing contract now also owns recovery route-state vocabulary for
|
||||
unified resources without restoring the retired `/recovery` top-level route.
|
||||
Infrastructure drawers and other cross-surface consumers must carry canonical
|
||||
`platform` and `node` query state into an owning platform/runtime route instead
|
||||
of rebuilding drawer-local recovery links, assuming only PBS services can
|
||||
expose recovery handoffs from infrastructure, or sending operators to a
|
||||
standalone aggregate workspace URL.
|
||||
When shared recovery links include protected-inventory posture, they must use
|
||||
the recovery-owned `state` query instead of overloading event `status`. Event
|
||||
outcome `status` remains recovery-history state, and compatibility input such
|
||||
as legacy `stale=1` must be normalized by the recovery route owner before
|
||||
cross-surface links are rebuilt.
|
||||
That same shared routing boundary now also owns alert-investigation handoffs.
|
||||
Resource-incident panels and other alert-side resource drill-down consumers
|
||||
must route operators back through canonical infrastructure resource detail and
|
||||
then into platform-owned workload, storage, and recovery surfaces via
|
||||
`frontend-modern/src/routing/resourceLinks.ts` route-state vocabulary, instead
|
||||
of treating alert investigation as a provider-local dead end, freezing
|
||||
per-surface route strings outside the unified-resource contract, or reviving
|
||||
Unified resources carry no recovery route-state vocabulary. The recovery
|
||||
query serializer (`RECOVERY_QUERY_PARAMS`, `parseRecoveryLinkSearch`,
|
||||
`buildRecoveryRouteSearch`) was deleted from
|
||||
`frontend-modern/src/routing/resourceLinks.ts` after the aggregate Recovery page
|
||||
that read it went on 2026-05-26, and the protected-inventory `state`, event
|
||||
`status`, and legacy `stale=1` rules went with it. Infrastructure drawers and
|
||||
other cross-surface consumers that expose recovery context must land in the
|
||||
owning platform tab (the Proxmox Backups tab or the TrueNAS Protection tab)
|
||||
instead of rebuilding drawer-local recovery links, assuming only PBS services
|
||||
can expose recovery from infrastructure, or sending operators to a standalone
|
||||
aggregate workspace URL.
|
||||
Alert investigation offers no resource cross-jump links. The
|
||||
resource-incident panel names its resource through the unified-resource lookup
|
||||
and links to no other surface (its Pulse Assistant handoff aside): its
|
||||
cross-jump chips were retired on 2026-05-16, and an alert card's single
|
||||
resource link opens the owning platform page. Alert-side consumers must not freeze
|
||||
per-surface route strings outside the unified-resource contract or revive
|
||||
retired aggregate workspace URLs.
|
||||
That same routing contract also owns Patrol finding handoffs. Expanded
|
||||
Patrol-finding rows and scoped-run finding snapshots must resolve the backing
|
||||
unified resource and surface the same platform-owned workload/storage/recovery
|
||||
route-state vocabulary there, including exact workload and physical-disk route
|
||||
state when the selected resource is itself the workload or disk, instead of
|
||||
stopping at finding text, rebuilding patrol-local route strings, or reviving
|
||||
Patrol findings stay in place the same way: expanded Patrol-finding rows
|
||||
carry the finding's own primary action and manual controls rather than
|
||||
surface-link chips, and
|
||||
`frontend-modern/src/components/AI/__tests__/FindingsPanel.test.ts` keeps
|
||||
`buildResolvedResourceSurfaceLinks` and `useResources()` out of the panel. A
|
||||
future finding handoff must not rebuild patrol-local route strings or revive
|
||||
retired aggregate workspace URLs.
|
||||
That drawer shell now routes its canonical timeline filter, facet-bundle, and
|
||||
resource-intelligence state through
|
||||
@@ -5680,50 +5685,14 @@ contract for reporting/ignored infrastructure state; future settings-row
|
||||
grouping or reporting-surface scope changes must be routed through that backend
|
||||
projection instead of teaching the frontend to reinterpret raw resource facets
|
||||
or removed-runtime arrays locally.
|
||||
Canonical route-state helpers must also preserve recovery-specific drill-down
|
||||
state when they serialize governed resource views. Recovery timeline day
|
||||
selection is part of the durable route-state contract, so recovery query state
|
||||
must round-trip the selected day inside an owning platform/runtime route
|
||||
instead of dropping it as transient local UI state or restoring `/recovery`.
|
||||
The same recovery route-state contract also applies to the selected timeline
|
||||
range: canonical recovery query state must preserve explicit non-default chart
|
||||
windows such as `7d`, `90d`, and `1y` so recovery drill-down transport does
|
||||
not widen back to the default `30d` window on reload or shared navigation.
|
||||
That same shared recovery route-state contract also owns the primary recovery
|
||||
workspace selection. When an operator explicitly switches between protected
|
||||
items and recovery events, canonical recovery query state must round-trip that
|
||||
`view` selection unless the active `rollupId` or selected day already implies
|
||||
the default recovery-events workspace.
|
||||
That same shared recovery route helper contract now also owns canonical
|
||||
protected-inventory posture encoding. Visible recovery inventory filters must
|
||||
round-trip through the owned `state=<value>` query form instead of leaking ad
|
||||
hoc booleans, overloading event `status`, or disappearing from shared links on
|
||||
reload. Legacy `stale=1` may be parsed only as compatibility input that
|
||||
rewrites to canonical inventory state.
|
||||
That same route-state contract now also owns the canonical recovery `itemType`
|
||||
query. Recovery query state must round-trip a provider-neutral item category
|
||||
such as `vm`, `dataset`, or `pvc`, and
|
||||
`frontend-modern/src/routing/resourceLinks.ts` may canonicalize provider-native
|
||||
aliases like `proxmox-vm` into that shared vocabulary during parse/build, but
|
||||
recovery route state must not drift back to raw platform-specific
|
||||
`subjectType` values in shared navigation.
|
||||
That same route-state contract also owns the canonical recovery `platform`
|
||||
query. Recovery query state must emit `platform=<owned-source-key>` as the
|
||||
shared operator-facing shape, while accepted legacy `provider` aliases may be
|
||||
parsed only as compatibility input that rewrites back to canonical platform
|
||||
route state. `frontend-modern/src/routing/resourceLinks.ts` must not keep
|
||||
legacy `provider` as a first-class build option once parse-time compatibility
|
||||
has converted it back to canonical recovery route state.
|
||||
Recovery-linked consumers that decode `/api/recovery/*` payloads must likewise
|
||||
prefer canonical `platform` / `platforms` response fields over legacy
|
||||
`provider` aliases, so unified resource drill-downs and shared recovery links
|
||||
carry the same platform vocabulary on both route and payload boundaries.
|
||||
That same shared drill-down contract also owns which primary recovery workspace
|
||||
an upstream surface is targeting. Infrastructure service links that open
|
||||
platform-level recovery activity must emit canonical recovery route state with
|
||||
`view=events` inside an owning platform/runtime destination instead of
|
||||
inheriting the inventory default, and those entry links should describe the
|
||||
destination as recovery events rather than platform-specific backup wording.
|
||||
No shared helper serializes recovery drill-down state any more: the
|
||||
recovery timeline day, chart range, workspace `view`, inventory `state`,
|
||||
`itemType`, and `platform` route-state rules served the deleted aggregate
|
||||
Recovery page. The Proxmox Backups tab owns its own day selection through
|
||||
`PROXMOX_BACKUPS_QUERY_PARAMS.day`; its chart range is component state. Readers
|
||||
of `/api/recovery/*` payloads must still prefer canonical `platform` /
|
||||
`platforms` response fields over legacy `provider` aliases, which
|
||||
`frontend-modern/src/utils/recoveryPlatformModel.ts` rewrites at decode time.
|
||||
Shared API consumers now also depend on a single registry-list snapshot per
|
||||
request when deriving canonical type aggregations for resource list and stats
|
||||
responses. Re-reading `registry.List()` for the same `/api/resources` request
|
||||
@@ -6699,9 +6668,10 @@ separate.
|
||||
a later record pass of the same rebuild merges into an already-joined row
|
||||
reaches neither side's pin.
|
||||
- A folded resource's canonical ID resolves to the primary holding it. The
|
||||
merged resource carries its folded IDs in memory, including IDs folded
|
||||
into those along a chain of links, and resource seeding indexes them. The
|
||||
monitor's rebuild, a registry seeded from its listing (the resources API,
|
||||
registry indexes the folded IDs named by the merged resource's link fold
|
||||
record (`linkFolds`, `recordManualLinkFold`), which covers IDs folded along
|
||||
a chain of links, both when a link applies and when resource seeding
|
||||
copies the record in. The monitor's rebuild, a registry seeded from its listing (the resources API,
|
||||
which then replays availability checks) and a read-state overlay therefore
|
||||
resolve them alike. The index follows the holding rows, so a holder that a
|
||||
later link merges away or that is re-keyed in place keeps the answer
|
||||
@@ -6713,8 +6683,8 @@ separate.
|
||||
`ResolveReferenceID` and `GetByReference` follow the fold as well, as the
|
||||
folded agent's alert reference (`agent:<host ID>`) already did through the
|
||||
merged row's canonical aliases. Alert intent, operator-state reads and
|
||||
writes through the resources API and API lookups by the folded ID
|
||||
therefore reach the merged row. Patrol's finding auto-acknowledgement
|
||||
writes through the resources API and resource lookups by the folded ID
|
||||
(`GET /api/resources/{id}`) therefore reach the merged row. Patrol's finding auto-acknowledgement
|
||||
still reads a row stored under the exact reference first, so an
|
||||
operator-state row kept under the folded ID applies there. History does
|
||||
not follow the fold: a history binding persists, and
|
||||
@@ -6731,6 +6701,9 @@ separate.
|
||||
(`TestAvailabilityLinkToALinkFoldedResourceFollowsItsPrimary`), a chain
|
||||
(`TestAvailabilityLinkFollowsChainedLinkFolds`) and the index cases
|
||||
(`TestLinkFoldIndexFollowsItsHolders`).
|
||||
`TestResourceUnlinkSplitsManuallyLinkedPair` pins the API lookup: the
|
||||
folded agent's ID answers with the VM while linked and with the agent's own
|
||||
row once unlinked.
|
||||
|
||||
`registry_merge_policy_test.go` pins the record links in both directions
|
||||
through two rebuilds and the live supplemental refresh, and checks that a
|
||||
@@ -6946,3 +6919,155 @@ above the hosts table without its filter toolbar. Nothing renders when
|
||||
vCenter reports no signals. The Overview's own model, workload snapshot and
|
||||
navigation facets still come from the Overview query; the Health tab keeps
|
||||
the filterable table.
|
||||
|
||||
### Unlink replaces the pair's operator link
|
||||
|
||||
`POST /api/resources/{id}/link` records a `ResourceLink`; unlink and
|
||||
report-merge record a `ResourceExclusion`. An operator decides each resource
|
||||
pair one way, and the later decision replaces the earlier: the store's
|
||||
`AddExclusion` deletes the pair's link rows and `AddLink` deletes its
|
||||
exclusion rows, in the same transaction as the write, matching both ID
|
||||
orders because canonical-ID succession can store either. Unlink used to add
|
||||
only the exclusion. The link row survived, and `applyManualLinks`, which
|
||||
never consults exclusions, kept the pair folded in the monitor's rebuilds and
|
||||
in the resources API's registry, so the agent facet stayed on the VM and the
|
||||
agent row stayed hidden although the API answered "Resources unlinked".
|
||||
`GetLinks` and `GetExclusions` return only each pair's latest decision
|
||||
(`effectiveManualPairDecisions` in `manual_link_decisions.go`), so a store
|
||||
that still holds both rows for a pair, written before this change or
|
||||
rewritten onto one pair by succession, reads its later write, and a tie
|
||||
reads as apart. Every registry loads its links and exclusions from its store
|
||||
when it is constructed, so the monitor's rebuild and the resources API's
|
||||
store-backed registry apply one decision with no exclusion check in
|
||||
`applyManualLinks`. Unlink keeps the exclusion, so identity matching and
|
||||
presentation coalescing also keep a pair apart that would otherwise match
|
||||
automatically.
|
||||
|
||||
Identity pins honour the exclusion too. While a Proxmox node was linked to a
|
||||
standalone agent, the merged resource's pin could record the node's endpoint
|
||||
hostname with the agent's machine key. After unlink the node completed that
|
||||
key in `completeIdentityFromPins`, minted the agent's machine-derived ID and
|
||||
folded into the agent by ID, a path no exclusion check sees, so the pair
|
||||
stayed merged with the link row gone. A pin whose canonical ID the operator
|
||||
excluded from the ID the record takes without the pin (its `chooseNewID` or
|
||||
source-specific ID) now completes nothing. This covers pins already written
|
||||
that way, not only new ones. Merges through an agent's own declared node link
|
||||
(`resolveLinkedResource`) still ignore exclusions.
|
||||
|
||||
The resources API seeds its registry from the monitor's listing, which
|
||||
already has the link applied, so it cannot split the pair itself: an unlink
|
||||
shows on REST, `/api/state` and the websocket from the monitor's next
|
||||
registry rebuild, the same generation in which a new link reaches the
|
||||
broadcast. REST applies a new link at once, because its own registry applies
|
||||
the store's links on top of the seed. The mock-mode view applies no operator
|
||||
links, so in mock mode REST splits a manually linked pair as soon as the
|
||||
unlink is recorded. Mock fixtures that identity matching merged stay merged
|
||||
on every surface: the fixture graph is built without the store's exclusions.
|
||||
|
||||
The websocket broadcast coalesced host views with
|
||||
`CoalescePresentationHostResources`, which honours no exclusions, while the
|
||||
resources API's `ListForPresentation` honours the registry's, so a host pair
|
||||
the operator split could be one broadcast row and two REST rows. The
|
||||
broadcast now coalesces through `Monitor.coalesceResourcesForPresentation`,
|
||||
which asks the read state that listed the rows for
|
||||
`MonitorAdapter.CoalesceForPresentation` when its registry is store-backed
|
||||
(the resource store's adapter, or a host-continuity overlay that loaded the
|
||||
store's exclusions afresh). The mock view and other views built from an
|
||||
already-unified list sit on a store-less registry, which declines, so the
|
||||
resource store's adapter supplies the exclusions instead. Either applies its
|
||||
current registry's exclusions through the same `presentationExclusionFilter`
|
||||
that `ListForPresentation` uses; a rebuild published between listing the
|
||||
rows and coalescing them is judged by the newer generation's exclusions for
|
||||
that one broadcast. Both surfaces share one known limit: the filter compares
|
||||
the IDs of the rows being merged, so a third same-host fragment that merges
|
||||
first can carry an excluded pair into one row.
|
||||
|
||||
A write leaves one row for its pair: `AddLink` and `AddExclusion` also delete
|
||||
a row of their own kind that succession stored in the other order, and the
|
||||
in-memory store replaces a pair's rows on every write and stamps `CreatedAt`
|
||||
as SQLite does. Reads keep the newest link per pair, so an older reversed copy
|
||||
cannot win with its primary, and drop exclusions older than it; duplicate
|
||||
exclusions already stored stay, which changes no decision. A registry resolves its `GetLinks` and `GetExclusions` results
|
||||
together in `loadOverrides`, so an unlink committed between the two reads
|
||||
cannot load both decisions for one generation. Report-merge undoes a link
|
||||
the same way (see "Report-merge splits operator-linked resources" below).
|
||||
`TestManualPairDecisionReplacesThePreviousOne` and
|
||||
`TestManualPairDecisionReadsResolveStoredConflictsByRecency` pin the store
|
||||
rule on SQLite and the in-memory store,
|
||||
`TestUnlinkSplitsHostPairWhosePinTookTheOtherSidesKey` links and unlinks
|
||||
node and agent pairs (IP and FQDN endpoints, either side primary) through
|
||||
three monitor rebuilds per step,
|
||||
`TestRegistryResolvesDecisionsAcrossItsTwoReads` loads a link read before an
|
||||
unlink and the exclusion read after it,
|
||||
`TestResourceUnlinkSplitsManuallyLinkedPair` drives link, unlink and relink
|
||||
through the API handlers against a monitor adapter rebuilt twice per step,
|
||||
and `TestMonitorAdapterCoalesceForPresentationHonoursExclusions`,
|
||||
`TestBroadcastPresentationCoalesceHonoursMergeExclusions` and
|
||||
`TestBroadcastPresentationCoalescePrefersTheListingReadState` pin broadcast
|
||||
and REST row counts for a split host pair.
|
||||
|
||||
### Report-merge splits operator-linked resources
|
||||
|
||||
`POST /api/resources/{id}/report-merge` records exclusions for the reported
|
||||
sources. For each reported source mapped to the merged resource it excludes
|
||||
the merged resource's ID and the candidate ID that source's record takes when
|
||||
no match is allowed (`ResourceRegistry.SourceTargets`), as before, which
|
||||
keeps an identity match apart. That candidate is derived from the merged
|
||||
resource's type, so for a source an operator link brought in it named
|
||||
neither side of the link: an agent linked into a VM got a `vm-`
|
||||
candidate, and an agent linked with a node or a Docker host keyed by its
|
||||
machine ID got a source-specific one. No link row matched, the link stayed
|
||||
applied, and the API answered "Merge reported" while the drawer's Split
|
||||
dialog toasted "Resource split applied". Two linked agents share their only
|
||||
source, so the API refused them as "Resource is not merged"; an applied link
|
||||
now marks a merge as well (the drawer still offers Split only for a resource
|
||||
listing two sources).
|
||||
|
||||
`applyManualLinks` now records each link it applies on the merged resource
|
||||
(`recordManualLinkFold`): the link row's own pair, the resource that took the
|
||||
other in and the one folded into it, plus the sources the folded side
|
||||
brought, including anything links had already folded into it, so every link
|
||||
along a chain keeps its own pair. A pair is recorded once, since record
|
||||
ingest can recreate a folded side whose source mapping points at a holder of
|
||||
another type and fold it again, so a repeated pair adds its sources to the
|
||||
fold already recorded (the holder still carries what the earlier fold
|
||||
brought). The record is unexported and rides in-memory
|
||||
clones, so it reaches the resources API's registry, which seeds from the
|
||||
monitor's already-linked listing and never holds the folded row.
|
||||
Report-merge excludes the pair of every link whose folded side brought a
|
||||
reported source category (`ResourceRegistry.ManualLinkFolds`), and the
|
||||
store's one-decision-per-pair rule deletes that link row, so the pair splits
|
||||
on every surface from the monitor's next rebuild, as with unlink. The drawer
|
||||
reports every merged source. Categories are not attributed to individual
|
||||
contributors: naming one that some folded side carried undoes that link even
|
||||
where the holder carries it too, and a source that arrived along a chain of
|
||||
links undoes every link on its way to the merged resource.
|
||||
|
||||
A link joins only the pair its row names. Canonical-ID succession re-keys
|
||||
link endpoints with `UPDATE OR IGNORE` but moves `primary_id`
|
||||
unconditionally, so a row whose re-key collided with the successor's own row
|
||||
kept the retired endpoint and took the successor as its primary.
|
||||
`applyManualLinks` honoured that primary and merged the successor through a
|
||||
row no exclusion of the merged pair removes, so an unlink or report-merge of
|
||||
that pair came back on the next rebuild. A primary that names neither side
|
||||
of its row now falls back to the row's first side, which leaves such a row
|
||||
inert while the retired ID is gone.
|
||||
|
||||
`TestResourceReportMergeSplitsOperatorLinkedPair` links and reports through
|
||||
the API handlers against a monitor adapter on the same SQLite store, rebuilt
|
||||
three times per step, for VM/agent, node/agent and agent/Docker pairs linked
|
||||
from either side and for two linked agents. Every two-source case but a node
|
||||
linked from its agent fails with the previous handler, whose candidate for
|
||||
that node happened to be the node's own source-specific ID.
|
||||
`TestResourceReportMergeSplitsPairBehindASuccessionShadowedRow` reports a
|
||||
pair that a shadowed row also named,
|
||||
`TestResourceReportMergeSourceFilterSelectsLinks` pins the source filter,
|
||||
`TestResourceReportMergeSplitsIdentityMergedPair` keeps an identity-merged
|
||||
agent and Docker host apart through the rebuilds,
|
||||
`TestManualLinkFoldsNameEachLinkPairAlongAChain` pins a chain's pairs on the
|
||||
monitor's registry and on one seeded from its listing, and
|
||||
`TestManualLinkFoldsRecordEachPairOnceAcrossRecordIngests` folds a TrueNAS
|
||||
system into a VM across repeated record ingests, and
|
||||
`TestManualLinkFoldRepeatedPairKeepsEarlierSources` refolds a side with fewer
|
||||
sources than it first brought. `TestResourceAPIReportMergeExcludesRegistryLinkFolds`
|
||||
keeps the handler on the registry's fold record.
|
||||
|
||||
Reference in New Issue
Block a user