Use the fulfilled API window and a common observed envelope across metric groups so sparse samples cannot look like a full selected range. Show dated endpoints, retain them with matching failed-refresh data, and clear them on range replacement. Preserve edge observations and reject non-date geometry. Pin mounted/model regressions, update both affected contracts, and retain production PBS drawer browser proof with its installed-acceptance limits. Change-source: pulse-maintainer
527 KiB
Frontend Primitives Contract
Contract Metadata
{
"subsystem_id": "frontend-primitives",
"lane": "L8",
"contract_file": "docs/release-control/v6/internal/subsystems/frontend-primitives.md",
"status_file": "docs/release-control/v6/internal/status.json",
"registry_file": "docs/release-control/v6/internal/subsystems/registry.json",
"dependency_subsystem_ids": [
"agent-lifecycle",
"api-contracts",
"cloud-paid",
"storage-recovery"
]
}
Purpose
Shipped documentation fragment navigation
The shared documentation renderer assigns GitHub-compatible, document-local
heading IDs from sanitized text, retaining explicit anchors and avoiding
duplicate IDs. The documentation viewer follows fragments after asynchronous
content rendering as well as in-page navigation. Fragment targets receive
keyboard focus without entering the normal tab order. Missing or malformed
fragments do not throw or move focus. The renderer and fragment helper are
covered by frontend-modern/src/features/docs/__tests__/docMarkdown.test.ts,
with direct-link, reload and keyboard navigation verified in the live viewer.
Disk mount scrolling
DisksCard keeps every supplied mount in its parent's scrolling flow. It must not cap the mount list or create a nested scroll target whose only overflow cue is a platform scrollbar. Large lists intentionally increase card height; aggregate usage, individual mount data and empty-state behaviour remain unchanged. This boundary is local to DisksCard, not a global scrollbar styling requirement. SharedPrimitives.guardrails.test.ts protects this composition; component tests preserve mount counts and totals. The disk-mounts qualification fixture checks short/long lists, themes and keyboard reachability with production CSS.
Ollama credential editing
The provider panel exposes the existing Basic Auth configuration. Saved passwords are represented by presence text, never a placeholder secret or input value. Blank password input preserves the saved value; explicit clearing takes precedence over a draft replacement. Successful saves discard the password draft. Username clearing is independent. Password bytes are not trimmed. Tests exercise preservation, replacement and clearing through the Settings save action. Connection testing uses saved settings; the panel tells users to save before testing and use HTTPS remotely.
Assistant owns composer registration and focus on every open, rather than only on component mount. Closing clears the registered input so later keyboard commands cannot target a detached composer. A handoff must leave Escape and keyboard input in Assistant, not the underlying alert search.
Mobile incident drawers transfer their exact context to Assistant and close through the shared explicit handoff callback. Keeping the source drawer above Assistant, or dropping its occurrence identity to make navigation work, fails the linked investigation journey. Both timeline and resource handoffs require mounted regression and final-build narrow browser proof.
Shared incident evidence disclosure
Alerts timeline and resource-history events share IncidentTimelineEventCard.
Its native details/summary disclosure preserves keyboard activation and keeps
forensic provenance out of the default event summary. The same timestamp
formatter rejects missing or invalid evidence times. Timeline and resource
history compose persistent failure copy with their existing retry controls,
independently of transient notification toasts. The affected interaction and
viewport qualification is recorded in
docs/qualification/PATROL_ASSISTANT_CUSTOMER_JOURNEY.md.
Canonical Patrol and Assistant continuation, 2026-09-07
Patrol's Assistant context preserves unknown destructive risk and distinguishes
canonical action state from legacy approval state. Transcript scrolling is owned
by the shared Assistant message container, including streaming and Latest, so
it cannot move outer document ancestors. Browser proof must inspect the header,
composer, nested evidence and scroll position after streaming and viewport resize.
The current scoped matrix is recorded in
docs/qualification/PATROL_ASSISTANT_CUSTOMER_JOURNEY.md.
Recovery feedback composes shared controls without a timer
The alerts-owned AlertQueueActionFeedback composes Card and Button rather than altering global toast lifetimes. Its polite atomic status region is mounted before failure text arrives; the warning card uses semantic foreground and wraps text and the explicit “Clear recovery message” control at narrow widths. The enclosing labelled region remains mounted and receives focus before clear removes the button, avoiding focus loss to the document body. Updating feedback does not steal focus. This is view-local feedback, not durable delivery history.
The live region stays independent of delivery-health conditional rendering: a later healthy observation can remove the health warning without removing failed-action information. AlertDeliveryHealthCard.test.tsx verifies status and focus composition. scripts/check-recovery-feedback.mjs verifies both real feature views, keyboard activation/clear and text/control containment at 1440, 900 and 390px, with scripted responses and genuine toast expiry. It does not establish screen-reader announcement quality or installed notification delivery.
The shared action evidence disclosure preserves the named observer independently
of the executor. Its observation timestamp uses the neutral label Observed,
followed by the separate Pulse receipt time. Independent Proxmox API evidence
must not be labelled as an agent observation. Browser qualification expands
this disclosure in completed action reviews at desktop and narrow widths.
Disk I/O presentation preserves each observed direction independently. Shared formatting renders a missing rate as a dash and measured idle as numeric zero. Partial observations cannot form a complete throughput total for sorting or comparison. Machines column preferences must preserve an explicit user choice across the first reload, including default-hidden migrations. Final-source browser proof covers Docker host details, Machines column selection and tooltip focus/dismissal at desktop, intermediate and narrow widths.
Overview delivery diagnoses use latest-started refresh ownership. Older bulk
responses cannot overwrite newer card notification status, and an empty active
alert set invalidates outstanding reads. Disposal also prevents updates. Failed
refreshes retain the existing snapshot; this ordering repair does not add a
freshness indicator or establish recipient receipt. Verify response overlap in
OverviewTab.deliverystatus.test.tsx, empty-set invalidation in
useAlertOverviewState.test.tsx, and rendered ordering at three widths using
scripts/check-alert-diagnosis-ordering.mjs.
The Destinations delivery-log state primitive assigns a generation to each
refresh and rejects stale completions before updating rows, unavailable state
or loading state. Held-event reads share that generation without blocking the
attempt-log spinner. Cleanup prevents abandoned requests from updating state
and makes subsequent calls through the disposed loader inert; it does not
cancel transport requests. Latest-request failure remains unavailable rather
than being concealed by an older successful response.
Verification combines the hook's ordinary race/disposal tests, registered
mount/Retry integration, and scripts/check-delivery-log-ordering.mjs Chromium
content assertions at desktop and narrow widths. This is component-level
presentation proof with scripted APIs, not full-tab or installed qualification.
The shared delivery-health card wraps action groups according to available space, retaining readable explanation width when Review, Retry, Dismiss and Refresh appear together. Its heading uses the opaque semantic foreground, not the translucent palette shades reserved for status backgrounds. Verify light/dark layouts at desktop, intermediate and narrow widths, including unavailable health, pending refresh and recovery.
The alerts overview offers the existing delivery-status refresh control when health is unavailable, including after a successful retained-queue action whose follow-up health read fails. The warning remains until a verified healthy read; a successful queue action alone is not evidence of delivery health. Normal degraded summary presentation continues to omit refresh.
Proxmox backup presentation treats every manifestless PBS artifact as
non-recoverable. It renders the artifact as Running when current writer
visibility is absent or a matching writer is active, and as danger-tone
Failed when a complete current-task observation finds no live writer.
Running and failed/incomplete artifacts remain inspectable but cannot become a
workload's latest recoverable point; search includes running, failed, and
incomplete state vocabulary. The compact recovery table reserves enough of
its fixed phone-width layout for the complete state badge instead of clipping
that recovery answer at the horizontal scroll edge.
Own reusable frontend primitives and canonical page-shell patterns so feature
work extends shared components instead of creating new local variants.
Feature-owned warning cards, including notification delivery health, compose
the shared Button variants for retry, dismiss, refresh, loading, and disabled
states. Feature code owns the action copy and confirmation consequences, but
must not recreate local button chrome for those controls.
Feature panels embedded in a shared Dialog or drawer must not duplicate the
overlay's accessible heading. A reusable panel may suppress its standalone
title when the owning overlay supplies the canonical title, while preserving
that title in inline and desktop contexts; the overlay remains responsible for
one visible heading, its accessible label, dismissal, and focus return.
The shared Dialog component requires exactly one accessible-name strategy at
its component boundary: consumers provide either ariaLabelledBy for a visible
heading or ariaLabel when no visible label is available. Unnamed dialogs and
consumers that provide both strategies must fail the frontend type boundary.
The alert schedule's initial-delivery selector composes SettingsPanel and
FormSelect, uses the shared alert-configuration presentation vocabulary, and
exposes the same email, webhook, Apprise, and all-destination labels used by
escalation. It must not introduce a page-local select shell or a second
destination-label map.
The centralized Findings surface loads active and historical Patrol findings,
keeps operator notes editable (including clearing a note with an empty value),
and exposes Reopen finding only for dismissed rows. Reopening uses the
finding-backed suppression removal API, refreshes both unified and Patrol
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
inline in both metric modes because bars now expose an intent-driven row
history lens and Trends keeps the same charts persistent. That inline range
must carry a visible contextual label.
Controls inside the View disclosure must expand in place rather than opening
nested absolute panels that can clip or create competing overlay stacks. The
Proxmox page owns and persists the Guest / Host memory basis;
the workload state, table, panel, and row contracts carry the selected basis
and resolved parent-node data to the canonical memory bar, and the memory
column header must expose the non-default Host basis after the control closes.
The shared ColumnPicker may also expose Reset widths when an owning table
has active manual column sizing. That action is separate from restoring column
visibility defaults: feature state owns the width reset callback and active
flag, while the shared picker owns the discoverable menu placement and button
presentation. Tables without manual sizing omit both properties and retain the
existing picker unchanged.
The default Workloads metric presentation keeps compact progress bars at rest.
A fine-pointer preview or keyboard focus on one guest row replaces CPU, memory,
and disk together with the existing MetricMiniSparkline presentation without
changing row height; touch pointer entry does not trigger this transient lens,
and the persistent Trends View choice remains the touch-accessible fallback.
The active chart owns its local tooltip while its normalized cursor position is
shared across sibling charts in that guest row, so every guide represents the
same relative point in the selected history range. Leaving the row clears the
cursor and restores all three bars together. The lens mounts with a short
reduced-motion-safe fade and must not leave both bar and chart semantics in the
accessibility tree simultaneously.
Bar mode resolves history only for that active guest through its canonical
metrics target and the selected compact range; it must not start an
estate-wide chart request merely because the range changes. The active request
key is stable across equivalent live guest snapshots, and leaving the row or
selecting another range aborts superseded browser work. Persistent Trends may
retain the shared estate reader, but range changes must clear prior-range data
unless an exact-key cache entry exists.
Feature-owned scope controls that use the shared filter rail must keep their
state in the owning route and use stable, domain-authored option identities.
The Proxmox Backups Backup location control composes the shared filter
catalog, reads PBS instance plus datastore identity from the recovery model,
and persists unchanged between the By date and Coverage views. Clearing the
shared filter rail must remove that route value along with the other active
facets; the feature must not replace the shared rail with a page-local select.
The TrueNAS Storage Storage type scope follows the same boundary with stable
volumes and disks route values. It composes the shared filter bar, exposes
the current option through pressed-state semantics, preserves the scope in the
URL across reload, and removes the query value for the default all state.
On narrow screens the physical-disk scope may reprioritize its canonical table
columns to endurance, temperature, and health while retaining the shared table
overflow and touch-target behavior; it must not introduce a second mobile-only
filter or table shell.
Large-estate platform pages must keep one canonical inventory snapshot for the initial read and explicit refresh path. The Proxmox and VMware vSphere overviews own their source-scoped unified-resource requests and pass those snapshots into the shared workloads state; the workloads adapter may map the snapshot into the legacy guest boundary, but must not issue a second workload inventory request or create a second infrastructure poll. Refreshing an overview must invalidate that owner snapshot and update both the host/node and guest regions from the same result, so a large estate cannot render contradictory counts, flash a false empty workload state, or pay duplicate transport and reconciliation costs. Provider workspaces must also constrain their canonical query at the source boundary instead of downloading same-type rows from unrelated platforms and discarding them in the page model. Docker and Kubernetes use their provider source directly, including merged agent rows that carry that source. Proxmox keeps separate route-family queries, but only the active route may enable its query: inactive desktop and phone tabs must not create a background inventory burst. The Backups route composes the existing Overview guest snapshot with a PBS-only addition rather than issuing a second guest-estate request. The overview's bounded structural summary remains ahead of its long virtualized inventory at desktop and narrow widths. In particular, Proxmox must show up to six phone rows or twelve larger-layout rows before the guest list instead of visually moving the nodes after the guest list's full virtual scroll extent; estates at or below the applicable threshold render in full without a continuation control, while revealing a larger node estate remains an explicit table-preview action.
App-shell navigation tab lists rendered through reference-keyed <For>
consumers keep stable item identity across websocket state frames. AppLayout
derives its primary and utility tab arrays through the shared
frontend-modern/src/components/shared/stableNavTabs.ts reuse helper, which
returns previous tab object references (and the previous array identity) when a
rebuilt list is structurally unchanged, so an alerts-bearing state frame with
unchanged badge content cannot recreate nav button DOM and drop an in-flight
tap. New nav or tab-strip consumers that rebuild their item arrays from live
store reads must route through the same helper instead of <For>-ing over
freshly constructed objects.
Estate-sized table and card rendering routes through the shared
PlatformWindowedRows, PlatformWindowedList, and
usePlatformWindowedItems primitives. They preserve the complete filtered and
sorted result plus native scroll extent while bounding mounted DOM to 140 items
on wider layouts and 36 on phones unless a feature declares a smaller budget.
Wheel projection may prewarm a directional keyed-row runway before native
scrolling exposes it. Touch scrolling must remain compositor-native: windowed
renderers must not attach touch listeners or replace keyed rows before the
browser moves the page, and must update their runway only from the passive
native scroll event. Spacer geometry is structural only: no feature may present
it as loading, pagination, or an intentionally blank data region. Settings
resource pickers, Availability target lists, Actions, alerts, and every
provider-native platform table share this contract. For table rows with unique
logical ids, or an explicit unique key extractor, PlatformWindowedRows owns a
stable wrapper and independently reconciled store per logical row. A live
snapshot may reorder those wrappers without remounting row-local input or
drawer state, and must never reconcile one row's nested value through another
row or duplicate rows after sorting. Missing or duplicate keys retain the
reference-keyed fallback.
The window's item-height estimate is measured from representative content, not the leading sibling alone. Grouped surfaces render a short group header before their first content row, and sampling only that header collapses the estimate so the mounted window advances far faster than the real scroll position and drops a group's rows mid-scroll. The controller samples several leading siblings and keeps the tallest, so uniform tables still measure their real row height while mixed group/content lists keep a content-scale estimate.
Shared workload, node, Docker-host, and resource-drawer history presentation
must scope retained observations to the exact resource type, resource ID and
range. An uncached target or range change clears the former points while its
read is pending, including when a PBS host link is withdrawn. A failed
replacement must never cache former-host points under the new target. Matching
cached reads and same-source background polls retain their chart without a
loading flash. Superseded, locked, unavailable and unmounted requests propagate
the query's abort signal to the Charts API; late results cannot replace current
observations. GuestDrawerHistory.source-isolation.test.tsx exercises the real
renderer, cache readback and cancellation, not a mocked chart. The mock-backed
PBS browser runner verifies delayed range and withdrawn-target reads at desktop
and phone widths; neither proof establishes installed collection or #1723 relief.
Failed same-source history refreshes keep valid previously loaded observations
visible with an explicit warning, rather than hiding the entire chart. Initial
or uncached replacement failures show unavailable history, never borrowed points
or a collecting claim. The existing target/range owns every manual refresh;
locked or absent targets expose no refresh control. The control remains mounted
and focusable through retry and recovery, rejects activation while busy, and
updates a pre-mounted polite status region without exposing transport diagnostics.
The latest query read settles loading even when background polling supersedes
a foreground refresh; late superseded results remain inert. Verification:
GuestDrawerHistory.refresh.test.tsx, createNonSuspendingQuery.test.tsx, and
browser-tests/pbs-history-refresh.cjs (direct production History renderer, scripted failures,
keyboard retry, overlap, target withdrawal, phone/desktop and light/dark themes).
These proofs establish presentation/recovery, not installed collection or delivery.
Shared workload, node, Docker-host, and resource-drawer history presentation
keeps current readings separate from stored samples. A current metric may
populate the legend while history is still being collected, but it must never
be expanded into synthetic timestamps or chart geometry. An empty stored
series renders the shared collecting-history state; zero remains a valid
reported reading, while an absent metric remains unavailable.
Shared history-chart gridlines must carry numeric labels derived from the
plotted scale rather than semantic Avg / Max placeholders. Byte and
byte-rate axes include their human-readable unit at each gridline, and the
canvas measures both value and time labels into the same plot bounds used by
geometry and hover selection so neither edge clips or drifts from the data.
Related history charts that share a time range must opt into the shared hover
group. The group owns one absolute hovered timestamp, while each chart maps
that timestamp through its own plot geometry and nearest stored sample so
crosshairs and tooltips remain time-aligned without coupling unrelated ranges
or assuming identical value scales.
Workload tables expose their inline history lens through one shared filter
contract. Its first-use hint is visible only while bar or history metrics are
available, disappears after a populated guest preview succeeds, and is passed
through getWorkloadsMetricFilterProps. The generic WorkloadsSurface and
provider-owned compositions such as ProxmoxPageSurface and
VmwarePageSurface must consume that binding atomically rather than selecting
display, hover, range, or hint accessors independently. Hover and range
interactions remain session deduplicated so the presentation layer cannot
create per-row or per-frame telemetry traffic. Provider-native inventories
that do not render WorkloadsFilter are outside this guest-row contract and
must not imitate only part of it under a second page-local View vocabulary.
Object-detail navigation follows that same canonical split across platform and
feature owners. Overview is the stable landing tab for current operational
facts, while stored metric charts appear only after selecting an evidence-gated
History tab rendered through frontend-modern/src/components/shared/Subtabs.tsx.
The active History range belongs in the shared subtab row's trailing slot, and
uses the shared filter-select presentation; storage pools, physical disks,
guests, nodes, and unified resources must not render historical charts inline
on Overview or reintroduce object-local tab or range-selector chrome. The
metrics and chart groups may remain object-specific without changing this
navigation contract.
Node history consumes the canonical metricsTarget carried by the resource
projection before legacy node identifiers. Proxmox-only history uses the
collector's node coordinates even when the unified resource has an agent
display identity. The presence of discovery routing does not enable Agent-only
disk-throughput charts. Explicit Agent linkage owns that capability distinction.
nodeDrawerModel.branchcov0713.test.ts pins target precedence and unsupported
target fallback, while NodeDrawer.test.tsx verifies the chart request and
API-only chart groups through the rendered History tab.
Platform inventory presentation has one structural owner across provider pages,
drawers, and inline detail rows. PlatformTableShell owns framed page tables;
PlatformDetailTable, PlatformDetailTableHeader, and
PlatformDetailTableBody own cardless nested tables while reusing the same
header band, borders, single-line density, responsive table class, and overflow
boundary. Provider-specific columns and cell contents are intentional variants,
but a drawer or expanded row must not rebuild raw table / thead / tbody
chrome or duplicate the shared header class strings.
PlatformDetailTable also declares phoneVerticalScrollOwner="page" through
the shared Table API. At the phone stage, canonical platform columns already
fit their real container, so that variant clips accidental overflow instead of
creating a nested scrollport that Chrome Android can stretch. Wider tables keep
horizontal overflow, while overscroll-behavior-y: chain progressively removes
Chromium's table-local boundary effect without blocking propagation to the app
scroll shell. A platform page must not remove this variant or create a competing
phone table scroller to recover columns that should be handled by responsive
priority.
Expandable platform summary rows use
getPlatformResourceDetailRowInteractionProps (or
createPlatformResourceDetailState, which owns the same state contract) for
whole-row pointer activation, Enter/Space keyboard activation, focus treatment,
aria-expanded / aria-controls, and exclusion of embedded links and controls.
PlatformResourceDetailToggleButton is the desktop disclosure affordance and
is visually removed on phone layouts where the complete row is the touch target;
provider tables must not add a second mobile chevron. When row activation
performs a different primary action, such as selecting a node's guests on
Proxmox, the shared toggle remains visible on phones through its
hideWhenRowTappableOnMobile={false} option. The node name supplies native
keyboard activation for guest selection, and embedded controls retain their
separate actions. Explicit guest selection focuses the guest heading with
scroll prevention. The shared revealElementInViewport helper leaves a
visible heading anchored and reveals an off-screen heading only far enough
to show the start of its results. It uses the actual scroll container and
marks deliberate movement so route-state restoration cannot compete with it. Activating the selected node again clears its node scope without
moving scroll or focus, while hover leaves the guest inventory unchanged.
Operator overrides remain in the shared Manage tab and use the explicit compact density of FormSelect
and FormTextarea, keeping form labels, help relationships, touch targets, and
control chrome canonical without expanding the low-frequency management surface.
When the summary row already owns a canonical unified Resource, its expanded
content composes PlatformResourceDetailTableRow instead of rebuilding a
provider-local fact grid. This keeps Overview, History, Manage, accessibility,
focus restoration, and responsive drawer behavior on one primitive. Proxmox
Backup Server rows follow this rule and request the shared host-details
disclosure open initially so a merged agent's system, hardware, network, disk,
and thermal facts remain discoverable from the PBS surface.
ProxmoxBackupServersTable belongs to the Proxmox Backups tab. Proxmox
Overview must not duplicate that domain table between its node and guest
regions; the tab boundary owns PBS server, datastore, and artifact detail.
The shared Proxmox section rail renders only tabs backed by current capability
evidence. It reads the source-filtered facets.byType from one compact
type=pmg&source=proxmox,pbs,pmg,agent request, not estate-wide
aggregations.byType: unrelated VMware VMs and TrueNAS storage must not
advertise Proxmox workflows. Agent is included for Proxmox-owned physical
disks, whose fact source may remain agent; because that source is shared, a
generic agent disk can still expose Storage until the route filters its rows.
A fresh all-resources cache does not carry source-filtered facets, so the
compact query must revalidate even when that cache can paint rows. While the
facets are unknown the rail must not show every optional
tab as a loading fallback; an independently fetched positive replication-job
count may still expose Replication. A bookmarked section remains the active
hydration target until counts can distinguish unavailable from unsupported,
then unsupported sections fall back to Overview. This rule applies at desktop
and phone widths and must not rewrite the URL or discard a valid PBS-only
Backups tab when its count arrives.
When that surface receives the provider-owned PBS resource and its host Agent
as separate canonical resources, ProxmoxBackupServersTable may assemble a
presentation-only drawer resource only after one unique normalized host
identity match. The row retains the PBS id and service facet while current host
telemetry, host details, and stored History use the Agent facet and Agent
metrics target. Zero or multiple matches must leave the PBS resource unchanged
rather than guessing; this presentation correlation must not mutate either
canonical input or create a second mobile disclosure interaction.
When the PBS service has a registry-corroborated pbs.linkedAgentId, the
Backups drawer selects only an Agent-bearing resource with that source-native
ID, even if same-host PVE API labels or a token-auth PBS connection have no
matching hostname. PVE-only rows cannot masquerade as a host series. A new
link cannot reuse a retained old host target, and the displayed PBS row keeps
its own canonical resource ID while History uses the selected host target.
Presentation helpers that mirror a server-side classification must name the
predicate they mirror and expose it as a single exported function rather than
inlining the boundary at each call site. isPhysicalDiskWearoutReported mirrors
storagehealth.WearoutReported, and both the health-status and life-column
helpers gate on it, so a wearout reading cannot be classified one way in a
status label and another way in the cell beside it.
Feature surfaces classify resources through the shared classifier that already
owns the distinction rather than re-deriving it from a raw resource type.
isPulseAgentPlatformResource is the single authority for whether a resource is
a standalone Pulse-agent machine or a provider-owned node, and the standalone
page, platform navigation, the resource drawer and the alerts threshold sections
all read it. Selecting by bare type === 'agent' collapses that distinction and
puts the same machine on two surfaces that do not share an identity.
The Machines threshold tab owns one responsive SMART rules card ahead of the resource groups. It composes the existing threshold tab layout and native accessible checkbox/number controls rather than creating a second settings shell. Failed-health is presented as a toggle; sector, media, CRC-growth, remaining-life, and spare policies are integer inputs; percentage inputs are bounded to 0..100; and the card states that zero disables an individual numeric rule. Every edit uses the canonical agent-default setter and dirty-state path, so the shared save/discard bar owns persistence on desktop and narrow layouts. The single-column narrow layout must keep labels, help, values, percent suffixes, and the full scroll journey inside the app scroll shell without horizontal overflow.
The System Updates channel selector presents Stable and Preview as the two
operator choices. Preview is the user-facing umbrella for governed alpha,
beta, and release-candidate publications: its guidance must say that beta is
for user testing while product changes are still expected, and that an RC is
only appropriate when the build could become stable without product changes.
The historical rc settings value remains a compatibility identifier, not a
display label or permission to describe every preview as release-ready.
Automatic stable updates remain unavailable while Preview is selected, and
that manual-channel consequence must stay visible at desktop and narrow widths.
Canonical Files
frontend-modern/src/components/shared/1a.frontend-modern/src/components/Infrastructure/resourceDetailDrawerMetricsHistoryModel.ts1b.frontend-modern/src/components/Workloads/nodeDrawerModel.ts1c.frontend-modern/src/features/docker/dockerHostDrawerModel.ts1d.frontend-modern/src/components/Workloads/AvailabilityProbeSuggestionCard.tsxfrontend-modern/src/components/Settings/Settings.tsxfrontend-modern/src/components/Settings/SettingsDialogs.tsxfrontend-modern/src/components/Settings/SettingsPageShell.tsxfrontend-modern/src/components/Settings/settingsPanelRegistry.tsfrontend-modern/src/components/Settings/APIAccessPanel.tsx6a.frontend-modern/src/components/Settings/AgentIntegrationsPanel.tsxfrontend-modern/src/components/Settings/AIChatMaintenanceSection.tsxfrontend-modern/src/components/Settings/AIModelSelectionSection.tsxfrontend-modern/src/components/Settings/AIProviderConfigurationSection.tsxfrontend-modern/src/components/Settings/AIRuntimeControlsSection.tsxfrontend-modern/src/components/Settings/AISettings.tsxfrontend-modern/src/components/Settings/AISettingsDialogs.tsxfrontend-modern/src/components/Settings/AISettingsStatusAndActions.tsxfrontend-modern/src/components/Settings/aiSettingsModel.tsfrontend-modern/src/components/Settings/AuditLogPanel.tsxfrontend-modern/src/components/Settings/useAuditLogPanelState.tsfrontend-modern/src/components/Settings/AuditWebhookPanel.tsxfrontend-modern/src/components/Settings/useAuditWebhookPanelState.tsfrontend-modern/src/components/Settings/CopyCommandBlock.tsxfrontend-modern/src/components/Settings/diagnosticsModel.tsfrontend-modern/src/components/Settings/DiagnosticsPanel.tsxfrontend-modern/src/components/Settings/DiagnosticsResultsPanel.tsxfrontend-modern/src/components/Settings/OperationsPanel.tsxfrontend-modern/src/utils/diagnosticsPresentation.tsfrontend-modern/src/utils/discoveryPresentation.tsfrontend-modern/src/components/Settings/GeneralSettingsPanel.tsxfrontend-modern/src/components/Settings/NetworkSettingsPanel.tsxfrontend-modern/src/components/Settings/RecoverySettingsPanel.tsxfrontend-modern/src/components/Settings/SecurityAuthPanel.tsxfrontend-modern/src/components/Settings/SecurityOverviewPanel.tsxfrontend-modern/src/components/Settings/settingsHeaderMeta.tsfrontend-modern/src/components/Settings/selfHostedBillingPresentation.tsfrontend-modern/src/components/Settings/SSOProvidersPanel.tsxfrontend-modern/src/components/Settings/useAISettingsState.tsfrontend-modern/src/components/Settings/useDiagnosticsPanelState.tsfrontend-modern/src/components/Settings/useSettingsShellState.tsfrontend-modern/src/components/Settings/useSSOProvidersState.tsfrontend-modern/src/components/Settings/ssoProvidersModel.tsfrontend-modern/src/utils/ssoProviderPresentation.tsfrontend-modern/src/utils/systemSettingsPresentation.tsfrontend-modern/src/utils/aiSettingsPresentation.tsfrontend-modern/src/utils/settingsShellPresentation.ts42a.frontend-modern/src/i18n/frontend-modern/src/utils/textPresentation.tsfrontend-modern/src/components/Settings/UpdateInstallGuide.tsxfrontend-modern/src/components/Settings/updatesSettingsModel.tsfrontend-modern/src/components/Settings/UpdatesSettingsPanel.tsxfrontend-modern/src/components/Settings/ReportingPanel.tsxfrontend-modern/src/components/Settings/reportingPanelModel.ts48a.frontend-modern/src/components/Settings/reportingSchedulesModel.tsfrontend-modern/src/components/Settings/reportingInventoryExportModel.tsfrontend-modern/src/components/Settings/useReportingPanelState.tsfrontend-modern/src/utils/reportingPresentation.tsfrontend-modern/src/utils/updatesPresentation.tsfrontend-modern/src/components/Settings/__tests__/settingsArchitecture.test.tstests/integration/tests/15-settings-shell-consistency.spec.tsfrontend-modern/src/components/shared/FilterBar/FilterBar.tsxfrontend-modern/src/components/shared/FilterBar/FilterChip.tsxfrontend-modern/src/components/shared/FilterBar/AddFilterMenu.tsxfrontend-modern/src/components/shared/FilterBar/filterCatalog.tsfrontend-modern/src/components/shared/FilterBar/index.ts59c.frontend-modern/src/components/shared/FilterBar/filterOptionPresentation.tsxfrontend-modern/src/components/shared/TypeColumn.guardrails.test.tsfrontend-modern/src/features/(including Patrol presentation, where Patrol control starter counts are context only even when mirrored throughpatrolAutonomy*compatibility fields, successful direct Patrol control saves may record the content-freepatrol_controlstarter only when paid control is available and the effective control posture changes and must then refresh Patrol status, findings, approvals, and run history before the operator waits for polling, legacyproActivation*starter aliases must not render a separate proof strip by themselves, Patrol control completed/resolved counts may only project backend-owned terminal proof, current active findings and pending approvals outrank historical completion proof in the primary operator state, selected run history must read as a Patrol run record rather than a findings filter or snapshot workflow, terminal verified/rejected outcomes with no active finding or pending approval must stay history detail without rendering a no-op proof strip, resolved-only issue history must not be promoted into current-work copy or actions, compact recurrence/trust counters must read as historical evidence rather than current issue state, Patrol-owned status/history evidence must keep the assessment visible when the broader intelligence summary is missing, Patrol work-group chips may group current approvals, failed actions, failed checks, recurring active issues, and stale scheduled protection but must not become a separate status/trust/proof strip, the Patrol route and page title must lead with Patrol while the default workspace underneath may use Open work and run history stays a deliberate secondary review surface, setup-only Patrol runtime failures must instead useFix Patrol setupframing with a dedicated setup task and direct provider-settings action while suppressing generic issue-row chips and filter chrome, Patrol must not expose a generic Details/supporting-context panel for nearby activity, related patterns, or policy limits, locked-control copy must state the watch-only boundary in positive capability language by saying Patrol checks infrastructure and shows current issues, avoid repeating the same sentence across the header and control, and avoid repeatedly restating infrastructure-unchanged caveats or relying on disabled controls, compact Pro badges, Limits controls, or manual-review framing,patrolControlValueStatedecides whether a terminal decision is partial review context or verified value proof whilepatrolAutonomyValueStateremains a compatibility mirror, legacyproActivation*fields are compatibility fallback only, native Patrol state must not load the operations-loop status projection to decide current work, local Patrol state must expose issue-backedpatrolWork*evidence rather than legacy proof naming, Patrol mode labels remain domain copy that must describe backend-owned risk policy without creating page-local safety thresholds, the selected Patrol mode sentence must state the approval and policy boundary without adding a second limits panel or proof strip, the Patrol schedule and model drawer must stay separate from the always-visible Patrol mode selector instead of duplicating the four control choices or reintroducing save/apply configuration framing, and Patrol header refresh controls must call an explicit operator-refresh handler whose spinning/disabled state is separate from background polling and initial data loads)frontend-modern/src/components/SetupWizard/SetupWizard.tsxfrontend-modern/src/components/Settings/useSettingsInfrastructurePanelProps.tsfrontend-modern/src/components/SetupWizard/SetupCompletionPreview.tsxfrontend-modern/src/components/SetupWizard/steps/WelcomeStep.tsxfrontend-modern/src/components/SetupWizard/__tests__/SetupWizard.test.tsxfrontend-modern/src/components/SetupWizard/__tests__/SetupCompletionPreview.test.tsxfrontend-modern/src/components/SetupWizard/__tests__/WelcomeStep.test.tsxfrontend-modern/src/components/Settings/SystemLogsPanel.tsxfrontend-modern/src/components/Settings/useSystemLogsPanelState.tsfrontend-modern/src/utils/systemLogsPresentation.tsfrontend-modern/src/components/Settings/__tests__/SystemLogsPanel.test.tsxfrontend-modern/src/components/Settings/ResourcePicker.tsxfrontend-modern/src/utils/reportableResourceTypes.tsfrontend-modern/src/utils/reportingResourceTypes.tsfrontend-modern/src/utils/workloadEmptyStatePresentation.tsfrontend-modern/src/utils/workloadGuestPresentation.tsfrontend-modern/src/utils/emptyStatePresentation.tsfrontend-modern/src/utils/semanticTonePresentation.tsfrontend-modern/src/components/Toast/Toast.tsxfrontend-modern/src/utils/toast.tsfrontend-modern/src/utils/semanticTonePresentation.tsfrontend-modern/src/utils/emptyStatePresentation.tsfrontend-modern/src/utils/typeColumnPresentation.tsfrontend-modern/src/components/Settings/NetworkBoundarySettingsSection.tsxfrontend-modern/src/components/Settings/networkSettingsModel.tsfrontend-modern/src/components/Settings/useDiscoverySettingsState.tsfrontend-modern/src/components/Settings/useSettingsInfrastructurePanelProps.tsfrontend-modern/src/components/Settings/AvailabilitySettingsPanel.tsxfrontend-modern/src/components/Settings/availabilitySettingsModel.tsfrontend-modern/src/components/Settings/settingsPanelRegistryContext.tsxfrontend-modern/src/components/Settings/settingsPanelRegistryLoaders.tsfrontend-modern/src/components/Settings/settingsNavigationModel.tsfrontend-modern/src/components/Settings/settingsNavCatalog.tsfrontend-modern/src/components/Settings/settingsNavVisibility.tsfrontend-modern/src/components/Settings/settingsRouting.tsfrontend-modern/src/components/Settings/settingsTabSaveBehavior.tsfrontend-modern/src/components/Settings/settingsTypes.tsfrontend-modern/src/components/Settings/useSettingsNavigation.tsfrontend-modern/src/components/Settings/useSettingsPanelRegistry.tsxfrontend-modern/src/components/Settings/useSettingsSystemPanels.tsxfrontend-modern/src/components/Settings/DockerRuntimeSettingsCard.tsxfrontend-modern/src/components/shared/EnvironmentLockBadge.tsxfrontend-modern/src/utils/environmentLockPresentation.tsfrontend-modern/src/utils/docsLinks.tstests/integration/tests/20-local-doc-links.spec.tsfrontend-modern/src/index.cssfrontend-modern/src/components/shared/summaryInteractionA11y.tsfrontend-modern/src/components/shared/SummaryRowActionButton.tsxfrontend-modern/src/hooks/createNonSuspendingQuery.ts111a.frontend-modern/src/utils/storageSummaryCache.tsfrontend-modern/src/components/shared/TableCardHeader.tsxfrontend-modern/src/components/shared/UpgradeLink.tsxfrontend-modern/src/components/shared/useUpgradeNavigation.tsfrontend-modern/src/utils/upgradeNavigation.tsfrontend-modern/src/components/DemoBanner.tsx116a.frontend-modern/src/components/CommercialMigrationBanner.tsx116b.frontend-modern/src/components/GitHubStarBanner.tsxfrontend-modern/src/components/Login.tsxfrontend-modern/src/stores/sessionCapabilities.tsfrontend-modern/src/stores/sessionPresentationPolicy.tsfrontend-modern/src/stores/licenseCommercial.tsfrontend-modern/src/useAppRuntimeState.tsfrontend-modern/src/routing/routePreload.tsfrontend-modern/src/stores/aiChat.tsfrontend-modern/scripts/header-audit.mjsfrontend-modern/src/components/Settings/DataHandlingPanel.tsxfrontend-modern/src/components/Settings/dataHandlingPanelModel.tsfrontend-modern/scripts/canonical-platform-audit.mjsfrontend-modern/scripts/settings-diagnostics-boundary-audit.mjsfrontend-modern/scripts/shared-template-audit.mjsfrontend-modern/scripts/shared-template-registry.json129a.frontend-modern/scripts/planning-doc-status-audit.mjs129b.frontend-modern/scripts/__tests__/planning-doc-status-audit.test.mjsfrontend-modern/src/features/platformPage/sharedPlatformPage.tsx131a.frontend-modern/src/features/platformPage/platformSearchSuggestions.ts131b.frontend-modern/src/features/platformPage/PlatformResourceDetailTableRow.tsx131c.frontend-modern/src/features/platformPage/PlatformOutdatedAgentNotice.tsx131d.frontend-modern/src/features/platformPage/PlatformOutdatedSensorSetupNotice.tsx131e.frontend-modern/src/features/platformPage/platformEstateOverviewModel.ts131f.frontend-modern/src/components/shared/Form.ts131g.frontend-modern/src/components/shared/FormSelect.tsx131h.frontend-modern/src/components/shared/FormTextarea.tsx131i.frontend-modern/src/components/Infrastructure/ResourceOperatorStateSection.tsxfrontend-modern/src/utils/platformSupportManifest.generated.tsfrontend-modern/src/utils/platformSupportManifest.tsfrontend-modern/src/utils/sourcePlatformOptions.tsfrontend-modern/src/utils/sourcePlatforms.tsfrontend-modern/src/utils/infrastructureOnboardingPresentation.tsfrontend-modern/src/components/shared/Button.tsxfrontend-modern/src/components/shared/buttonModel.tsfrontend-modern/src/components/shared/Button.test.tsx139a.frontend-modern/src/components/shared/InlineNotice.tsx139b.frontend-modern/src/components/shared/InlineNotice.test.tsx139c.frontend-modern/src/components/shared/ExternalTextLink.tsx139d.frontend-modern/src/components/shared/ExternalTextLink.test.tsxfrontend-modern/src/components/shared/CopyableCodeRow.tsxfrontend-modern/src/components/shared/DetailSectionTable.tsxfrontend-modern/src/components/shared/detailSectionModel.tsfrontend-modern/src/components/Settings/__tests__/settingsLocalization.test.tsfrontend-modern/src/i18n/__tests__/i18n.test.ts
Shared Boundaries
Maintenance schedule copy
Operator maintenance banners show local absolute start and end timestamps, including future dates. Past-time relative formatting must not turn a future expiry into "just now". The banner describes paused attention rather than promising that rejected alert observations are acknowledged. Active and future-window mounted regressions pin these user-visible claims.
Retained state in bounded platform windows
PlatformWindowedRows and PlatformWindowedList keep one keyed renderer owner
while the visible window moves. Items present in both windows retain component
identity, active detail tabs and unsaved edits. Passing a new window must update
the existing renderer rather than instantiate another renderer around it.
Removed items still unmount, and row budgets and spacer behavior are unchanged.
The mounted scroll regression in PlatformWindowedRows.test.tsx exercises both
renderers with an edited input retained across overlapping windows. The browser
journey is a narrow Proxmox node Manage form, scrolling to lifecycle Save while
the synthetic estate continues updating, plus the alert timeline and note form.
PBS host history correlation
The Backups surface passes its complete deduplicated route inventory to the PBS table. Only PBS servers render as rows; other resources supply correlation inputs. Preserve PBS drawer identity and use the correlated canonical history target. Missing disk utilisation does not gate CPU/memory history.
One agent can be surfaced twice for a single PBS host: folded into its PVE guest and as a standalone source=pbs host row. Those two rows are one machine, not an ambiguous pair. Correlation must collapse candidates that share an agent identity and prefer the guest representation, whose canonical metrics target carries the persisted host history; the PBS service target has no host series and renders the collecting-history state. Two candidates with distinct agent identities remain ambiguous, and a candidate with no agent identity must not be treated as proof of sameness.
A live snapshot can briefly omit the correlated host row while the PBS server row remains, for example while a realtime refresh replaces the merged estate. The correlation must retain the last resolved host per PBS server across that omission instead of falling back to the PBS service target, so the drawer's Discovery and Metrics Target rows and its History series do not flicker. Reuse the remembered host only while it is still fresh relative to the server, and drop it once stale so a removed or replaced host is not advertised indefinitely; a host row that is present but ambiguous still declines.
Verification: ProxmoxBackupServersTable.drawer.test.tsx covers standalone and merged guest targets, missing disks, duplicate guest/host representations of one agent, genuinely ambiguous identities, and retaining the resolved host target across a transient host-row omission; ProxmoxBackupServersTable.test.ts covers retention, staleness and pruning; ProxmoxPageSurface.contract.test.tsx covers hydration and deduplication.
The settings panel registry supplies organisation overview, access and sharing with security-status currentUsername. An explicitly empty principal must not fall back to configured administrator identity; only older responses lacking the field use proxy/SSO/configured-username compatibility fallback. This identity plumbing must not change settings shell framing or bypass panel capability gates.
Settings navigation discoverability is part of the shared settings-shell
boundary. A settings route that is available in normal commercial presentation
must be reachable through the sidebar unless it is explicitly a hidden
deep-link flow. Ordinary free self-hosted sessions use the explicit opt-in
boundary: system-billing is hidden from navigation while
presentationPolicy.hideUpgrade is true, and paid-feature items including
system-relay, support-reporting, security-roles, security-users,
security-audit, and security-webhooks use hideWhenUnavailable. Their
direct routes remain available because the owning panels still handle explicit
activation, recovery, and feature-gate handoffs. Capability-based hiding still
applies independently, so a session that lacks route authority cannot mount a
panel merely because it has paid or recovery context.
Candidate import-plan presentation inside the Infrastructure settings dialog is
a shared primitive composition boundary. NodeCandidateImportPlan.tsx may use
shared Button, checkbox styling, lucide icons, and
MonitoredSystemImpactPreview, while InfrastructureWorkspace.tsx owns the
route-backed dialog state that feeds probe or Discovery candidates into the
credential slot. That surface must keep the approval card readable inside the
existing dialog body and must not introduce a second nested modal, detached
wizard shell, or page-local preview renderer for monitored-system impact.
Frontend localization is a shared primitive boundary. Locale support must flow through typed message catalogs with an English fallback and explicit seed locale coverage rather than page-local string switches. Alert snooze and resume copy follows that same boundary: action labels, bounded duration presets, monitoring-continuity wording, exact-expiry presentation, failure feedback, and timeline labels must stay in the typed English, German, and Spanish catalogs. The product surface may format the chosen instant in the viewer locale, but must not duplicate those lifecycle strings inside the alert card or dialog component.
Date and number formatting is part of that boundary and is separate from the
message catalog. Every toLocaleString, toLocaleDateString,
toLocaleTimeString, and Intl.* constructor must pass undefined as the
locale so the runtime resolves the viewer's own locale, including whether they
expect a 12 or 24 hour clock and which order the date parts go in. A literal
tag such as 'en-US' shows US conventions to every reader everywhere. The
canonical-shared/no-hardcoded-format-locale rule in
frontend-modern/scripts/canonical-platform-audit.mjs blocks new occurrences;
a call that genuinely needs a fixed locale, such as a stable machine-readable
export, belongs in that rule's allowFiles with a reason rather than as a bare
literal. The rule exists because this regressed silently once already: #1279
delocalized the "Last refresh" clock in App.tsx, and the v6 rewrite that
moved that logic into frontend-modern/src/useAppRuntimeState.ts reinstated
the hardcoded form, which shipped through v6 GA until #1685.
frontend-modern/src/i18n/locales.ts owns locale normalization, the supported
locale registry, and fallback chains; frontend-modern/src/i18n/messages.ts
owns the typed catalog shape; and frontend-modern/src/i18n/policy.ts owns the
first-wave non-translatable token rules. The active app locale is a shared user
preference initialized from stored or browser language and exposed through
Settings > General; individual surfaces must consume that shared preference
instead of creating local language toggles. Customer-facing shell, navigation,
settings, first-run, empty-state, commercial handoff, and alert copy may be
localized through this catalog — including the alert-to-Patrol action surface ("Have Patrol
investigate" and its targeted-check menu hint) that is primary on
resource-backed active alert cards, plus the secondary Assistant explanation
handoff strings — but
machine-facing values must remain stable: commands, environment variables, API
fields, config keys, log lines, error codes, hostnames, resource names, product
identifiers, and vendor object names stay untranslated unless the owning
runtime contract explicitly says otherwise. Shared settings-shell header copy
for the self-hosted plan must keep first-wave locale catalogs aligned with the
English product stance: on Pro, the operator chooses how autonomous Patrol
should be. The settings shell must not teach a separate activation loop, MCP
readiness, or operations-loop proof model as the default plan setup story.
Pulse Intelligence external-agent setup uses the same shell language with a
Choose Patrol mode handoff before scoped-token setup, and the expanded setup
checklist must say to set how autonomous Patrol should be before connected
agents request work rather than repeating internal automation/proof wording.
Developer-only external-agent posture uses External agents plus Patrol mode
before surfacing MCP or workflow prompt wire names.
Migrated settings surfaces must render customer-facing copy through the catalog
and shared presentation helpers rather than reintroducing panel-local English.
Migrated first-session surfaces,
including the Setup Wizard shell, welcome/security steps, setup completion
handoff, and runtime-home loading handoff, follow the same catalog path; their
guardrails must fail if the migrated journey returns to page-local English or
translates commands, URLs, generated credentials, product/source identifiers, or
reported resource names. Migrated Alerts Overview surfaces, including the page
shell, overview stat cards, active-alert triage list, acknowledgement actions,
incident timeline panel/filter controls, and Pulse Assistant alert handoff
briefing, must also route user-visible copy through the catalog and
alert-owned presentation helpers. Alert IDs, alert types, resource IDs,
resource names, node names, source messages, event payloads, commands, command
output, logs, and Assistant model-context labels stay machine-stable and
untranslated.
The Alerts Overview hydration boundary must localize its pending, unavailable,
and retry presentation through the same catalog. Pending and unavailable copy
must explicitly withhold an all-clear until active-alert truth is confirmed;
the surface must not reuse the localized zero-alert empty state while the
canonical alert snapshot is unknown. First-wave catalogs and their focused
catalog proof must advance together so this reliability distinction cannot
silently fall back to English or collapse into ordinary empty-state wording.
The active-alert card's delivery-status row remains an alerts-owned compact
presentation composed inside the shared responsive card shell. It consumes one
bulk diagnosis snapshot per overview refresh, wraps beside started-at and
threshold metadata at narrow widths, and does not create a second card or
page-level banner. Acknowledged cards omit the redundant row, and unavailable
diagnosis data leaves the existing alert card intact.
The Destinations delivery-activity surface follows the same ownership split:
alerts own the AlertDeliveryLogCard composition inside the shared Card,
button, badge, list, and responsive wrapping primitives. Its single
newest-first list may mix notification delivery attempts with alert-policy held
events, but each row retains its owning outcome vocabulary and evidence
tooltip. At narrow widths, resource, reason, and relative-time fields wrap or
truncate inside the card without creating page-level horizontal overflow, and
the shared refresh action remains independently usable while the bounded list
scrolls.
The legacy pricing handoff page may also route its visible redirect title and
manual-link copy through the catalog, but Pulse Account, route paths, feature
keys, query parameters, public URLs, and purchase-return state remain
machine-stable and untranslated.
Customer-facing copy must use complete sentences or an intentional visual
separator such as a middle dot. Semicolons are reserved for machine-facing
syntax including commands, cookies, encoded data, CSS declarations, and HTML
entities. frontend-modern/scripts/copy-style-audit.mjs enforces that boundary
across production TypeScript and TSX, and every ordinary product-copy exception
must be rewritten instead of allowlisted.
Alert thresholds consume the shared FilterBar primitive and route state, while
the alerts subsystem owns the resource data and platform-specific threshold
tabs. The thresholds platform IA is platform-shaped: Proxmox, Docker,
Kubernetes, TrueNAS, vSphere, PBS, PMG, and Systems. Frontend primitives own
the chip, reset, "+ Filter", and route-backed shell pattern; alerts must not
replace that with page-local search/tab chrome or legacy neutral buckets.
Threshold edit semantics under frontend-modern/src/features/alerts/thresholds/
also stay alerts-owned: the override mutation hooks write sparse enabled-only
backup/snapshot overrides that inherit global thresholds at evaluation time,
and the warning/critical pair reconciliation in the thresholds helpers adjusts
the untouched field on a conflicting single-field edit. Primitives must not
absorb those persistence or validation rules into shared form/table chrome
(#1126).
The alerts-owned threshold surface also owns rolling metric evaluation
configuration. ThresholdsTab presents the understandable policy (current
value or a named rolling duration) rather than a query language, with one
global CPU rule, a canonical all-workload fallback, and sparse platform
overrides that visibly inherit the effective parent. The select options and
inherit labels must mirror the runtime hierarchy: VMs and application
containers inherit guest, agent-backed systems inherit node, and every
chain terminates at all. The configuration snapshot and payload models must
round-trip explicit zero and
resource-specific maps without flattening inheritance. Copy must explain that
incomplete history holds incident state instead of firing or recovering, and
the control must remain usable at desktop and narrow viewports through shared
Card, label, and native-select primitives.
PlatformTableToolbar may accept compact consumer-owned context through its
shared leading-control rail when the context is actionable for the inventory
immediately below it. Consumers must use that extension point for local
attention counts and review actions instead of introducing a parallel filter
card or a routine page-wide posture banner; the toolbar continues to own
responsive placement alongside View preferences and row counts.
Frequently used leading context remains visible when the mobile filter body is
collapsed and moves into the canonical action rail when that body is expanded;
lower-priority trailing orientation controls remain collapsed with the rail.
Native multiline form fields are also a shared primitive boundary.
FormTextarea owns label/id/help wiring, controlled value synchronization, and
textarea chrome for alert, settings, and infrastructure runtime surfaces; those
surfaces must not recreate raw native <textarea> shells locally.
Toast notification chrome is a shared primitive boundary. Toast owns the
global notification stack shell, status icon placement, and dismiss action
chrome; status glyphs must come from the shared library icon set and dismiss
controls must compose ActionIconButton. Consumer-specific raw SVG status
icons, toast-local close-button class strings, and page-local toast stacks are
forbidden unless the shared-template registry is intentionally extended first.
The System Updates install guide is a shared settings primitive, not a
deployment-lane-only panel. UpdateInstallGuide must render the canonical
update-plan readiness verdict inline with the update action, and a blocked
readiness status must make automatic install visibly unavailable until the
blocking check is resolved.
That install guide also owns the Pro-runtime Docker guidance: when the
compiled runtime identity is pro (runtimeCapabilities().runtime.build,
the same signal the update banner keys off), the Docker install steps and
the idle Docker box must render the license server broker's digest-pinned
commands from UpdateInfo.dockerUpdate (or an explicit Pro notice when they
are absent) and must never show the community rcourtman/pulse pull
commands, which would silently downgrade the container to the community
build.
frontend-modern/src/components/shared/DiscoveryReadinessBadge.tsx is the
shared presentation primitive for discovery freshness/readiness indicators.
It may render the canonical presentation model from
frontend-modern/src/utils/resourceDiscoveryReadiness.ts, but it must remain
presentational: no local storage, network reads, discovery fetches, or
resource-target inference belongs inside the badge. Workload rows, drawers,
and Assistant handoff surfaces must share that primitive instead of inventing
local freshness chips.
Platform page subnavigation is a shared frontend primitive. Docker / Podman
and Kubernetes platform pages may add native API-backed sections, but the tabs
must use PlatformSectionTabs, canonical table alignment helpers, and shared
resource type presentation/reporting helpers rather than page-local tab shells,
alignment classes, or ad hoc report-category coercion. Platform tabs are
responsively scrollable within that shared shell. When the rail overflows, the
primitive must expose focusable, labeled previous/next controls and keep their
availability synchronized with the rail position so hidden workflows remain
discoverable without widening the document or requiring precision swipes. At
the leading edge, the first tab must begin flush with the rail instead of
reserving an empty previous-control slot; trailing space may be reserved only
while the next-control affordance is actually present.
Platform tabs are
feature-owned consumers of canonical resource payloads: Docker page model
helpers may prefer backend-authored DockerData stack and Podman metadata for
search/display while shared primitives continue to own only the tab shell,
filter controls, and reusable presentation affordances. Platform tabs are
workflow-level navigation, not one visible tab per API resource kind, and they
must be evidence-gated by their owning row or signal model. Overview is the
stable landing surface; supporting workflow tabs appear only when the current
setup has native inventory or signal for that workflow, and legacy object URLs
resolve to their owning workflow only when that workflow is visible. Docker /
Podman may expose Overview, Images, Storage, Networks, and Swarm,
while legacy /docker/containers resolves to the Overview landing surface
rather than remaining a separate visible tab. Kubernetes may expose Overview,
Nodes, Workloads, Services, Storage, Configuration, and Events;
TrueNAS and vSphere follow the same evidence-gated primitive for native
storage, service, app, VM, protection, datastore, network, health, and activity
workflows. API-native tables remain bespoke under those workflows, so Docker
Overview owns runtime hosts plus primary container workloads, Docker Storage
owns engine disk usage plus volumes, and Docker Swarm owns services, tasks,
nodes, secrets, and configs; Kubernetes
Workloads owns Pods, Deployments, controllers, and autoscaling, Services
owns Services plus ingress/endpoint inventory, and Configuration owns config
plus policy inventory. Backup and recovery platform pages follow the same
navigation primitive boundary: source-specific evidence tables may be exposed as
secondary drilldowns under an owning workflow tab, but the shared tab shell must
not grow one top-level tab for each API source merely because that source has a
table. Legacy object-specific URLs may resolve to the owning
workflow tab, but they must not reappear as top-level platform navigation unless
the product IA is intentionally changed. Overview tabs must stay deliberately
shaped around the primary operator job instead of repeating every detail table:
The Proxmox backup workflow follows this same boundary: its chronological date
feed and workload coverage posture are route-backed sections under
/proxmox/backups, and their navigation must compose PlatformSectionTabs
instead of a page-local segmented control or query-only view switch.
Docker / Podman Overview owns runtime hosts and primary container workloads in
the proven host-then-workloads pattern, while Kubernetes Overview owns
cluster/control-plane rollup; supporting object tables live in their dedicated
workflow tabs. Docker / Podman native subsections now
include runtime containers, engine storage usage, Swarm node inventory, and
metadata-only Swarm secret/config inventory where the documented Docker APIs
report those resources; Podman-only libpod pod inventory must not be represented
until the collector has a libpod-native source. Kubernetes config
inventory must preserve the same trust boundary for metadata-only ConfigMap
and Secret rows: the shared table wording may indicate metadata-only inventory,
but must not imply payload fields were read when the agent used Kubernetes
metadata-only API responses, and the unified-resource owner supplies the
Namespace, ConfigMap, Secret, and ServiceAccount-specific columns. Kubernetes
Node inventory must also be reachable through a dedicated native tab, not only
the overview stack, while retaining the shared PlatformSectionTabs shell.
Primary app-shell navigation consumes unified-resources-owned resource evidence:
empty or generic compatibility facets do not admit runtime-lens tabs on their
own, and cached resource evidence must not render primary platform navigation
before the first authoritative resource snapshot resolves. A platform that is
not admitted stays reachable by direct setup URL, but does not occupy primary
navigation with an empty page.
Feature-owned Docker / Podman action controls may render backend
actionReadiness disabled reasons, but the shared primitive layer owns only the
button/table affordance shell; it must not invent action availability, command
agent state, or alternate execution routes.
Kubernetes Service inventory must likewise stay on the shared tab, toolbar,
table, table-alignment, and inline-detail primitives while the unified-resource
owner supplies Service type, virtual IP, published port, and selector columns.
Kubernetes storage inventory follows that same primitive boundary while the
unified-resource owner supplies StorageClass, PersistentVolume, and
PersistentVolumeClaim-specific columns. Kubernetes networking inventory also
follows that same primitive boundary while the unified-resource owner supplies
Service, Ingress, and EndpointSlice-specific columns.
Outdated-agent notices on platform pages are part of this same shared
frontend/platform primitive boundary and must compose
frontend-modern/src/components/shared/InlineNotice.tsx for the dense notice
shell, icon/content layout, and action-link chrome. Agent-backed and hybrid
platform pages may
surface a compact stale-agent cue when their row model carries Pulse agent
identity and version evidence, but the CTA must route to the canonical
Infrastructure settings update-command surface with scoped agent IDs instead of
duplicating installer command assembly, tokens, or lifecycle copy in each
platform page. These notices must compare against the API-owned
agentUpdateTargetVersion rather than the app build version, so development
builds can show their dirty server version without implying agents can or
should update toward that build. Agentless API-only platforms such as vSphere
must not grow this notice unless a concrete guest or monitored system row
actually carries a Pulse agent identity. On vSphere specifically, stale-agent
copy belongs to correlated in-guest VM agents and must not describe ESXi hosts
as Pulse-agent update targets just because phase-1 host resources use the
canonical agent resource type. Kubernetes is cluster-agent-backed: canonical
k8s-node rows may be pure Kubernetes API rows rather than merged agent
rows, so the unified-resource owner must project the cluster agent identity and
cluster-scoped agent version onto those node rows before the shared stale-agent
collector can decide whether the node inventory is gated by an older agent.
The Docker duplicate-identity warning follows the same shared notice boundary:
it composes InlineNotice, is driven only by server-authored
DockerData.identityConflict evidence (never by page-local hostname
comparison), and names the flapping hostnames with the machine-id remedy in
the notice copy because the fix happens on the operator's machines, not on a
Pulse settings surface, so it carries no action link. Unlike stale-agent cues
it is a data-reliability warning and is not gated on the read-only
presentation policy.
Global dismissible notice bars are also part of the shared InlineNotice
boundary. frontend-modern/src/components/DemoBanner.tsx and
frontend-modern/src/components/CommercialMigrationBanner.tsx must compose
InlineNotice with the banner layout, the shared icon library, action slots,
and the primitive's dismiss slot instead of carrying local colored notice
shells, raw SVG status icons, or page-local action and close button classes.
Kubernetes policy inventory follows that same primitive boundary while the
unified-resource owner supplies NetworkPolicy policy type and rule-count
columns, PodDisruptionBudget budget and observed health columns, ResourceQuota
hard/used quota columns, and LimitRange item-type columns.
Kubernetes autoscaling inventory follows that same primitive boundary while the
unified-resource owner supplies HorizontalPodAutoscaler scale target, replica
bounds, current/desired replicas, and metric source columns.
Kubernetes events inventory follows that same primitive boundary while the
unified-resource owner supplies Event type, reason, involved-object, count,
observed-time, and message columns.
Docker empty-state guidance on platform pages follows the same shared platform
primitive boundary: it may use the route-specific Docker / Podman vocabulary,
but it must distinguish standalone Docker host installation from the Proxmox
LXC Docker host-side inventory path without adding page-local installer command
assembly or token handling. The empty state must provide a direct action into
the Docker-specific Infrastructure add flow rather than leaving the operator at
a descriptive dead end.
Docker / Podman inventory follows that same primitive boundary while the
unified-resource owner supplies API-object-specific container, image, volume,
network, Swarm node, task, secret, and config columns through dedicated native
tables. The Docker containers tab must use the native
DockerContainersTable for container state, health, restart, image, port,
network, mount, update, governed lifecycle actions, and host/runtime columns
rather than embedding WorkloadsSurface. The lifecycle action column is a
compact icon-button primitive over unified-resource capabilities and the shared
resource-action API client; it must not grow Docker/Podman shell, SSH, or
provider calls inside the table component. The same governed lifecycle controls
may appear in the resource detail header, while the platform table and drawer
shells remain presentation owners only: they may pass a post-success refresh
callback to their existing resource query, but must not own execution,
approval, policy, or provider dispatch. Swarm services must surface the
API-reported rollout/update
state in the native services table, and engine storage rows must expose a
stable row hook so platform-page browser proof can verify the storage tab is
hydrated from runtime disk-usage data. Kubernetes deployments must surface the
API-native observed generation and metadata age columns through the shared
table shell without page-local alignment helpers or generic infrastructure
columns.
Docker network rows use the same shared table/detail primitive split:
the default table columns prioritize attached workloads, attention state,
subnets, driver, and host, while lower-level scope, addressing, flags, and
network id details belong in the inline row disclosure. Attached container
names, network addresses, image, health/state, and published ports are
feature-owned data, but search and disclosure behavior must remain inside the
shared table chrome rather than a card deck, nested card, or route-changing
object browser. Dense networks must keep the inline disclosure bounded by
default and provide local attached-container search, status grouping, and
attention/running/other filters so large bridge or overlay networks remain
scan-friendly without hiding any container from drilldown.
frontend-modern/src/components/CommercialMigrationBanner.tsxshared withcloud-paid: the global commercial migration notice is both a cloud-paid entitlement recovery surface and a shared app-shell notice primitive consumer.frontend-modern/src/components/Infrastructure/useTableWindowing.tsshared withperformance-and-scalability: the shared bounded table-window controller is both a canonical frontend rendering primitive and a fleet-scale scrolling hot-path boundary.frontend-modern/src/components/Settings/AgentIntegrationsPanel.tsxshared withai-runtime,api-contracts: the External agents settings panel is the optional settings-shell projection of Pulse MCP onboarding, the AI runtime connected-agent onboarding surface, and a presentation consumer of the shared agent capabilities frontend client.frontend-modern/src/components/Settings/APIAccessPanel.tsxshared withsecurity-privacy: the API Access settings intro is both a security/privacy token-management trust surface and a canonical settings-shell presentation boundary. The panel may own shell placement and local action layout, but token-specific Docker / Podman copy must come fromfrontend-modern/src/utils/apiTokenPresentation.tsrather than page-local text.frontend-modern/src/components/Settings/AgentIntegrationsPanel.tsxstays under Pulse Intelligence Assistant settings because connected agents are an optional access path to Patrol work, while API Access remains the token minting surface linked from setup. Its setup copy must present one sharedpulse-mcpruntime contract with client-native wrappers: OpenCode's top-levelopencode.json/mcpshape and the commonmcpServersshape for Claude-style clients. The server name, command, base URL flag/default, token environment variable, supported config families, and copied config snippets must flow throughfrontend-modern/src/api/agentCapabilities.tsfrom/api/agent/capabilities.mcpAdapter; the component may arrange and label them, but must keep setup mechanics, raw client config snippets, and developer details behind deliberate disclosures so the default Assistant settings view stays focused on chat command access and optional external access rather than raw JSON. External-agent posture should presentExternal agentsas the visible surface and reservePulse MCPorpulse_operations_loopfor wire-name/debugging detail. The Patrol control handoff must route to the Patrol operator surface where the inline control level is configured. When setup is opened, the setup order must put Patrol control before scoped-token creation and client connection, while installer commands, client config snippets, and developer details remain deliberate disclosures. It must not frame Pulse MCP as a Claude-only surface, force OpenCode through a Claude-style wrapper, or duplicate a client-specific tool list. Full-surface token guidance in that panel must render the manifest-providedrequiredScopeslist throughfrontend-modern/src/api/agentCapabilities.ts; it must not hardcode a partial scope set in browser copy. Capability category order, labels, and descriptions must also come from the same manifest client; the settings panel may provide compatibility fallback rendering through that client, but it must not own a local category presentation table or/api/agent/capabilitiesfetcher. The panel's Pulse Intelligence surface summary is the same manifest projection: Pulse Intelligence Core, Patrol, Assistant, and MCP labels/descriptions plus surface affordance badges must flow throughfrontend-modern/src/api/agentCapabilities.tsfrom/api/agent/capabilities.surfaceContract, leaving the component to own only settings-shell layout. The visible external-adapter label in onboarding copy must also come from that manifest-derived capability posture instead of a hard-coded panel-local product label, so the settings shell cannot drift from the published agent surface contract. MCP capability-posture chips in that same panel must also flow throughgetAgentManifestSurfaceToolContract(manifest, AGENT_SURFACE_ID_PULSE_MCP)andgetAgentSurfaceToolPosturePresentation, with the static inventory read only from/api/agent/capabilities.surfaceToolContracts; missingsurfaceToolContractsentries must not make the browser infer MCP tools from raw capabilities. The panel must not own request / response capability filtering, call a per-surface projection alias, know thesubscribe_eventsstreaming exception, or maintain a local MCP tool count. The shared frontend manifest client must keep MCP onboarding on the generic surface resolver rather than exporting a Pulse-MCP-specific tool-contract helper. The panel's settings shell may arrange the external-agent setup hierarchy, but it must keep connected agents framed as optional access to Pulse context and Patrol work, with Patrol as the built-in operator that checks infrastructure, follows Patrol mode before acting, asks when approval is required, verifies outcomes, and records history, state that external agents use that same boundary, and make the canonical/settings/pulse-intelligence/assistant#external-agent-setuproute land on and briefly focus this panel with setup open rather than leaving the user at the generic API token inventory. Legacy/settings/security/api#external-agent-setupand/settings/security/api#pulse-mcp-setuplinks must remain accepted and may redirect to the canonical Pulse Intelligence Assistant route. Normal API Access visits remain token-management first, and external-agent setup must not reorder the API token inventory because it no longer lives on that page. The Assistant settings default must keep setup mechanics behind aShow connector setupdisclosure so it does not introduce a separate external-agent operator journey or make copied MCP config blocks or tool-contract proof badges the default visual weight of Pulse Intelligence settings. Its Developer details disclosure may show posture and policy summaries behind Patrol access model, but prompt, scope, failure-code, and tool inventories must sit behind a nested Live manifest details disclosure so the advanced surface remains navigable. The panel's manifest-ownedpulse_operations_loopprompt row may expose the stable prompt id for client builders, but its visible label, description, and badge must keep the user-facing Patrol framing from the manifest instead of reintroducing operations-loop proof wording in the settings shell. The panel's explanatory onboarding copy must name published manifest-owned surface contracts as the publication boundary rather than suggesting raw backend capabilities become visible automatically. The visible token preset name in that setup must bePatrol external agent, notPulse Intelligence agent; the latter may survive only as a route/model compatibility id. Manifest-backed stable failure-code summaries may use settings-shell chips or compact lists, but code selection and capability attribution stay owned by the API manifest client. Token setup handoff buttons or anchors in the Agent integrations panel are settings-shell chrome only: they may route to the API Access token creation section, but token preset semantics and required-scope derivation remain owned by the API/security boundary.frontend-modern/src/components/Settings/DataHandlingPanel.tsxshared withsecurity-privacy: the data-handling settings surface is both a security/privacy trust surface and a canonical settings-shell presentation boundary.frontend-modern/src/components/Settings/dataHandlingPanelModel.tsshared withsecurity-privacy: the data-handling settings model is both a security/privacy posture projection and a canonical settings-shell presentation boundary.frontend-modern/src/components/Settings/GeneralSettingsPanel.tsxshared withsecurity-privacy: the general settings privacy panel is both a security/privacy control surface and a canonical settings-shell presentation boundary. The panel owns compact settings-shell framing for outbound usage telemetry, but its vocabulary must stay aligned withsecurity-privacy: coarse deployment and lifecycle buckets, aggregate resource and outcome counts, coarse feature flags, and content-free Patrol, Assistant, and capability-API usage counters may be named, while hostnames, credentials, infrastructure identifiers, URLs, paths, locale, browser events, prompts, chat messages, command text, action output, token values, and personal information must stay explicitly excluded.Preview payloadis the panel's primary action, because the exact runtime payload is the disclosure an operator can verify; the enable toggle andReset IDstay secondary controls, and the summary copy opens with what the data is for before enumerating categories and exclusions. The shared settings shell no longer accepts atelemetryActiondeep link that changes the preference on arrival; the preference changes only from the panel. The summary copy also states what the data is never used for (sold, shared, advertising, account or license linkage) in every locale, with thesecurity-privacydisclosure as the source of those statements.frontend-modern/src/components/Settings/SecurityAuthPanel.tsxshared withsecurity-privacy: the authentication settings surface is both a security/privacy control surface and a canonical settings-shell presentation boundary.frontend-modern/src/components/Settings/SecurityOverviewPanel.tsxshared withsecurity-privacy: the security overview settings surface is both a security/privacy control surface and a canonical settings-shell presentation boundary. These settings panels consume the privileged security-status projection, while the shared status type also represents intentionally sparse public and authenticated tiers. Privileged posture booleans therefore remain optional at the transport boundary and must be normalized fail-closed before the settings shell derives posture summaries or hardening actions. The first-run shell must use generic host, Docker, and LXC bootstrap commands rather than probing public status for deployment identity.frontend-modern/src/features/platformPage/PlatformWindowedList.tsxshared withperformance-and-scalability: the shared bounded list renderer is both a canonical platform-page primitive and a fleet-scale mounted-DOM performance boundary.frontend-modern/src/features/platformPage/PlatformWindowedRows.tsxshared withperformance-and-scalability: the shared bounded table-row renderer is both a canonical platform-page primitive and a fleet-scale mounted-DOM performance boundary.frontend-modern/src/features/platformPage/usePlatformWindowedItems.tsshared withperformance-and-scalability: the platform windowing controller is both a canonical frontend scroll primitive and a directional-runway performance hot path.frontend-modern/src/routing/routePreload.tsshared withperformance-and-scalability,unified-resources: the app-shell route preload registry is a canonical frontend shell boundary, an authenticated hot-path performance boundary, and the entry point for the unified-resource Actions workspace.frontend-modern/src/stores/aiChat.tsshared withai-runtime: the assistant drawer and session store is both an AI runtime control surface and a canonical app-shell presentation boundary. Assistant session pickers and reloads must restore only safehandoff_summarypresentation state from the session list. Loading a plain session or starting a new conversation must clear stale scoped handoff briefing state so Patrol and alert context does not visually leak between conversations. Browser-originated model handoff payloads are one-shot request seeds: after the first successful chat send, this store must clearhandoffContext,handoffResources,handoffActions, and safehandoffMetadata; it must also clear any preferred workflow prompt request once the manifest-rendered starter has seeded the composer and the first scoped send succeeds. The store must preserve the safe visible briefing and scoped approval-required posture, so later turns rely on backend session hydration instead of resending stale browser context. Patrol handoffs must not include Persisted Assistant redo availability is safe session chrome, not transcript content. The drawer may consumeChatSession.can_redofrom the backend session list to re-enable Redo after reload or session refresh, but it must not read or duplicate the redo stack in frontend state. Undo restores the editable prompt draft and safe structured send metadata into the composer; Redo clears that recovered draft only after the backend restores the turn. Browser-owned session-management comments, command request handling, and question-answer plumbing in this store and the adjacent chat hook must name the native drawer surface as Pulse Assistant rather than reviving the retired genericPulse AIlabel. Shared model/provider settings guidance still belongs to Pulse Intelligence > Provider & Models. Patrol handoffs must not include safe next-step labels, action kinds, or whitelisted app-route hrefs inhandoffMetadata; the drawer must treathandoff_summary.requires_approvalas a current pending-decision flag, not a historical action marker, so completed or rejected handoff actions render as action context rather than pending approval. A restored Patrol run summary must remain visibly sourced to Pulse Patrol, restore apatrol-runtarget plus run ID/type/status/runtime-failure presentation only, and must not rehydrate model-only runtime failure detail into browser context. New Patrol run requests follow the same drawer boundary: source-owned context and briefing copy may show classified, redacted failure summaries for operator review, buthandoffContext,handoffResources, andhandoffActionsfor run-history context must stay absent from the browser request so the backend can rebuild model-bound context from the stored Patrol run. Restored Patrol assessment, Patrol finding, and Patrol control save-failure sessions follow the same safe-summary rule: the drawer may restore source label, title, target type, status badge, bounded resource facts, and approval/action status fromhandoff_summary, but it must not infer a finding target from bounded action references or reconstruct hidden model context, provider details, retry payloads, commands, preflight output, or action results in the browser. If the safe summary was created by a legacy build that stored Patrol next-step metadata, recommendation detail, action labels, safe action kind, or whitelisted app-route href, the session picker plus restored drawer must ignore those fields rather than carrying them forward as hidden context or visible recommendation copy. Session-load and new-conversation transitions must be success-bound: if the underlying session operation fails, the shared drawer store must not clear or replace the current scoped handoff context. Live Patrol assessment drawer opens must use that samepatrol-assessment/pulse-patrol-assessmenttarget identity rather than a retired dashboard target, so first-open and restored-session chrome remain source-named. The sharedfrontend-modern/src/components/shared/AIModelPicker.tsxprimitive must keep model route presentation delegated to the AI runtime label helpers. Pulse-owned local Assistant routes such aspulse:local-inventoryandpulse:mock-assistantare implementation routes and must render as named choices without secondary raw route IDs, while external provider route IDs may remain visible where they disambiguate catalog entries.frontend-modern/src/utils/platformSupportManifest.generated.tsshared withunified-resources: the generated platform support projection is both a canonical unified-resource platform union boundary and a shared frontend source/platform vocabulary boundary. It must expose the manifestsurface_kindfield so runtime lenses such asdockerare not collapsed back into owning platform semantics. It must also preserve canonical projection lists from the governed manifest without page-local narrowing; for example, TrueNAS exposes both nativevm,network-share, andapp-containerworkloads through the same generated platform projection used by route helpers, badges, source filters, reportable-resource pickers, and type unions.frontend-modern/src/utils/sourcePlatforms.tsshared withunified-resources: the source platform normalizer is both a canonical unified-resource source adapter boundary and a shared frontend source/platform vocabulary boundary. That shared boundary must preserveavailabilityas the agentless monitoring source fornetwork-endpointresources and settings presets, so source badges and platform/source type resolution do not fall back togenericwhen an endpoint is represented by ping, TCP, or HTTP probe data rather than an installed agent or provider API.
Extension Points
The planning-document status audit is a repository-governance support boundary
owned here because it runs through the frontend lint entrypoint. It must derive
its input set from Git-tracked docs/ files, skip separately governed subsystem
contracts, and ignore untracked or ignored workspace notes. Local scratch
documents cannot become mainline demand signals or block an otherwise valid
push merely because their names end in _SPEC.md, _PLAN.md, or
_CONTRACT.md.
Global Actions review uses the canonical shared Dialog, Button, Subtabs,
Card, MetadataBadge, and mobile navigation primitives. The Open/History
selector is an in-page sub-navigation surface and must compose Subtabs
instead of recreating a segmented tablist in Actions.tsx; queue rows may own
action state and resource semantics, but their frame and badge chrome stay on
the shared primitives. Actions uses the full content width supplied by the app
shell, matching Patrol instead of adding a page-local maximum-width container.
The review's policy provenance uses a native keyboard-operable disclosure so
the initial dialog layer stays calm without removing audit detail; intent,
exact target identity, safety/authority posture, and fail-closed provenance
warnings remain visible before expansion. The responsive route must preserve
named tabs, keyboard focus, dialog focus containment, and phone-width overflow
checks; journey 83 is the desktop/browser accessibility proof and is not
mobile-device proof.
The shared Dialog runtime in useDialogState.ts owns focus containment,
body-scroll locking, background isolation, and focus restoration for every
modal and drawer. While a dialog is open, every body-level surface outside the
topmost dialog portal is inert, including surfaces added after opening; nested
dialogs make their underlying dialog inert. The final close restores each
surface's pre-existing inert state. Closing an overlay must restore its
previous trigger with preventScroll; a plain
focus() may make the app scroll shell jump to a virtualized or previously
off-screen trigger just as the operator dismisses the overlay. Escape belongs
to the top dialog and must stop the same keydown from reaching later global or
shell listeners before it closes. Feature-owned
dialogs may provide a stable fallback target when their original virtual row
has unmounted, but they must use the same scroll-neutral focus contract rather
than compensating with page-level scroll writes.
InfrastructureWorkspace.tsx exercises that extension point after its shared
Manage dialog closes: both the shared captured-trigger restoration and its
delayed stable-row fallback must use preventScroll, preserve the app scroll
offset, and leave focus on the originating Manage action at desktop and narrow
viewports.
Assistant shell entry changes must keep Assistant contextual rather than
generic: AppLayout.tsx and the command palette may expose a compact launcher,
but that launcher must attach current-view context before opening the drawer
and label the action around the current monitoring, Patrol, Alerts, or Settings
view.
The app shell may surface Patrol current-work pressure as a secondary count on
the Patrol navigation tab, but it must not rename that tab to Needs Attention, create a Home-like action queue, or route the operator away from the
Patrol route. Desktop and mobile accessible names should combine the stable
Patrol label with concise open-work count context when a count is present.
SSO provider settings changes must preserve the shared Community-tier action
path: SAML and OIDC provider creation stay on the same settings-shell control
surface, while paid-plan copy and compatibility feature probes stay out of the
frontend primitive boundary. The SSO provider settings shell is a fully
migrated shared-action consumer: add, test, preview, copy, close, cancel, save,
delete, edit, and dismiss controls must compose the shared Button,
ActionIconButton, and CopyValueButton primitives rather than restoring
panel-local <button> shells.
Feature surfaces under frontend-modern/src/features/ may own product-specific
assessment semantics, but they must keep those semantics in their governed
presentation helpers and render them inside the shared neutral Pulse surface
language rather than introducing page-local verdict bands or nested cards.
The Patrol operator home composes the shared PageHeader, Button, Toggle,
MetadataBadge, native disclosure, dialog, tab, and neutral bordered-surface
patterns. Patrol owns the meaning of background posture and the mutually
exclusive Inbox, Protection, and Activity modes; the shared primitive
boundary owns visible selection, Arrow/Home/End keyboard movement, roving tab
focus, touch targets, responsive stacking, and visual consistency. Only the
selected mode's panel is rendered. Plan-locked paid-mode discovery is not a
daily-page primitive and must not be added beside the effective mode.
Current API-owned decisions must follow the compact posture row in the default
Inbox at every viewport. Objective configuration and verified/history surfaces
belong to their explicit modes rather than following the queue on the same
canvas. The compact Verified outcomes list consumes server-authored Patrol work
receipts rather than filtering generic history in the browser. It may format
the canonical capability name and verification time with shared presentation
helpers, and it reuses the action resource presenter for durable resources that
have left the live registry. It must preserve the server's verified-only
membership, evidence class, newest-first order, empty state, and
last-truthful-data behavior during a refresh error. Raw executor verification
summaries stay in Actions history rather than becoming quiet-home copy.
Selected Patrol decisions compose shared Button, ButtonLink, and
CopyValueButton actions with native disclosures rather than introducing a
detail-local control vocabulary. The primary reading order is summary,
contextual Assistant explanation, affected resource, distinct impact, next
step, and current lifecycle action. Raw canonical resource identifiers remain
available through the shared copy affordance instead of occupying the reading
surface. Evidence, protection, and timeline may share one collapsed disclosure
when all typed detail remains reachable, keyboard-operable, and truthfully
labelled. Compact actions retain the shared minimum touch target at phone widths
even when their desktop presentation uses the xs or sm size.
For Patrol, that includes the Open work description: it may use concise
row-level guidance such as review evidence, approve a change, inspect automatic
actions, or review verification results, but it must remain descriptive copy
rather than another card, strip, or proof counter above the findings list.
Watch-only forward-path guidance follows the same rule: the finding-row
Switch to Ask first nudge and the Actions inbox Watch-only calm-state copy
live in their governed presentation helpers
(patrolControlPresentation.getPatrolWatchOnlyInvestigationNudge,
actionPresentation.getActionsWatchOnlyEmptyState) and render as descriptive
copy plus a single shared-primitive action inside the existing expanded-row
and calm-state layouts rather than as new cards, banners, or proof strips.
Feature-owned runtime hooks may also own non-visual side effects when those
effects are part of the governed feature workflow. For Patrol, current-work
action chrome must keep active findings in the Patrol findings workflow first;
Assistant handoffs stay contextual actions on selected findings, approvals, or
history records rather than the primary current-work CTA. When provider/model
readiness blocks manual Patrol, the feature header must render the shared
primary-action chrome as a Provider & Models setup link instead of a disabled
run button that looks primary but cannot act. The Patrol hook owns the
content-free pulse_operations_loop starter marker so render components do not
fork telemetry or privacy behavior.
When the shared operations-loop status projection reports contextual
Assistant/external-agent collaboration inside the Assistant step, Assistant or
Pulse MCP starter counts, Patrol control starter evidence, or Patrol control
completed-loop or resolved-loop proof, feature presentation helpers may render
those facts as compact title or step detail inside the existing journey layout.
They must read primary patrolControl* fields first, then fall back to legacy
patrolAutonomy* compatibility fields, and may fall back to legacy
proActivation* fields only when the Patrol-control projections are absent.
They must keep the neutral shared surface chrome stable and must not add
page-local badges, nested cards, or alternate progress widgets for the same
evidence.
Feature-owned table drawers use shared disclosure and inline-detail primitives
as local interaction state. Unless a surface has a separately governed deep-link
write contract, opening or closing a row drawer must preserve the current
document and URL instead of writing route state or reloading the page shell.
Feature-owned sortable table headers must render real button controls inside
the shared TableHead primitive and expose column state through aria-sort;
the feature owner may define the sort keys and data comparator, but the header
interaction must update that canonical owner state rather than forking
table-local sort state or making header labels look clickable while inert.
Storage pool headers are intentionally presentation-only rather than sortable
headers: their one canonical sort interaction lives in StoragePageControls
under View -> Order, so the table headings remain plain labels without a
second hidden or competing sort affordance.
Sortable header direction presentation belongs to
frontend-modern/src/components/shared/tableSortPresentation.ts. Inactive
sortable columns stay visually quiet; only the active column renders its
ascending or descending marker. Workloads, backup, and shared platform
tables must consume that helper rather than repeating dormant up/down icons in
every header or defining page-local direction glyphs.
Shared filter/search primitives may provide the common shell, keyboard behavior,
history, and reset mechanics, but the owning page or table must supply
domain-specific visible copy, scope filters, status labels, and searchable
field coverage. Platform pages must not surface generic "rows/resources"
search affordances when the visible table is actually pods, VMs, datastores,
apps, mail gateways, storage pools, backup jobs, or another product-owned
object model. Add-filter controls that sit beside a page-level search box must
prefer direct selectable filter values over a second nested search affordance;
a page that needs filter-value search must keep that search inside the active
filter chip or an explicit page-owned advanced selector. Platform-owned filter
selectors must also exclude facet options from other platform scopes, even when
the underlying shared surface is mounted from the same Workloads or Storage
component. When every menu-backed filter is already active or has no selectable
non-default value, the shared FilterBar must omit the exhausted Add filter
control instead of leaving a disabled No filters affordance in the toolbar.
Alert configuration tables follow the same primitive boundary: the alerts
owner supplies platform-specific threshold groups and filter catalog values,
while the shared FilterBar owns the chip, reset, and "+ Filter" interaction
shape so thresholds do not reintroduce page-local search/tab chrome.
The shared alert resource table's global-defaults row and card are the single
editing surface for a section's defaults. A section that supplies
globalDefaults must not also inject a synthetic resource row that mirrors
the same record; the table renders defaults once, in both desktop table and
narrow card layouts, and per-section metric columns must resolve to metric
keys the shared column normalizer produces so the defaults editor reads and
writes the same record keys the section persists.
Platform sub-routes that add native provider inventory must stay on the shared
platform page and table primitives. The vSphere Networks surface routes through
/vmware/networks, the shared platform tab model, the command palette
navigation model, and the canonical table/detail primitives rather than a card
deck or VMware-local page shell. Its rows are canonical network resources in
the shared reportable/resource vocabulary, so source badges, resource pickers,
command-palette search, table chrome, and detail disclosure must all consume
shared primitives before VMware-specific presentation logic.
Patrol's primary assessment strip is descriptive only; it must not render a
Patrol-authored recommended next step, suggested prompt chips, or a secondary
action band inside the assessment shell. If the same assessment opens
Assistant, the Patrol-to-Assistant handoff must carry only bounded evidence,
resource references, and factual governed approval/action metadata as model-only
context. Feature-owned Assistant handoffs may provide source context and safe
metadata, but the shared drawer boundary must not turn those handoffs into
frontend-authored prompts, tool routes, or remediation plans; the configured
model owns tool choice and diagnostic reasoning after the request reaches the
AI runtime.
The compact Patrol assessment strip may include factual recent activity mix and
trigger-mode labels when those values are derived from the Patrol run-history
and status payloads. Those labels are summary context inside the same strip,
not a replacement status card, CTA band, or page-local nested card.
-
Add shared primitives in
frontend-modern/src/components/shared/Filterable table surfaces that separate high-frequency narrowing from durable presentation preferences must composefrontend-modern/src/components/shared/FilterBar/ViewOptionsDisclosure.tsx. The shared disclosure owns trigger, expanded state, Escape focus-return, remembered-preference explanation, and responsive option-grid geometry; feature surfaces own the controls placed inside it. They must not restore a row of equally prominent preference toggles or a page-local popover shell. The disclosure renders in normal flow beneath the filter rows so durable settings push the affected table down instead of covering it. Its option grid uses compact, equal-width tracks that wrap from one column on phones to as many columns as the available desktop width supports. Floating, viewport-docked, and nested overlay implementations are forbidden for this persistent settings surface. Every option control fills its compact grid track, uses the shared control height, and divides segmented choices evenly; content-sized toggle islands with mismatched footprints are forbidden. An inline Columns picker expands as a full-width row below the compact controls and flows its checkboxes into responsive columns, so its option list becomes neither a narrow nested panel nor a tall single-column desktop list. Standard command buttons and button-styled route actions belong to the sharedButtonprimitive family. Feature pages may choose the action label, icon, route, click handler, and contextual layout, but secondary/primary/ danger/outline/ghost button chrome, sizes, focus rings, disabled/loading behavior, and safe new-tab link behavior must come fromButton,ButtonLink, andbuttonModel.ts. Empty states, platform page notices, Patrol controls, settings panel actions, infrastructure setup controls, compact row actions, and other repeated command affordances must not copy localinline-flex ... rounded-md ...Tailwind shells just because the page needs a one-off action. The sharedmdCompactsize owns the common settings/actionpx-3 py-2 text-smshape, the sharedxssize owns the compact settings row-actionpx-2.5 py-1 text-xsshape, the sharedsettingsActionXssize owns compact settings/privacypx-3 py-2 text-xsaction controls such as telemetry preview/reset buttons, and the sharediconMdsize owns the settings dialog close-buttonh-9 w-9icon shape. Positive completion or continuation actions such as infrastructure handoff and reporting exports use the sharedsuccess,successOutline, andsuccessGhostButton variants instead of carrying page-local emerald action shells. Patrol approval and remediation controls use that same primitive family: Patrol owns approval/reapproval/denial/review/Assistant handoff behavior, whileButtonowns success, warning-solid, primary, secondary, ghost, disabled, focus, and compact action chrome. Shared error-boundary fallback actions are also command buttons: reset, reload, and retry controls must composeButtonso emergency UI does not become a separate local button vocabulary. Update confirmation and progress modal actions are part of the same command boundary: cancel, start, retry, close, history, reload-now, and close-icon controls must composeButtonorActionIconButtoninstead of carrying modal-local blue, neutral, or icon-button class strings. Compact icon-only row, inline, and floating action controls belong toActionIconButton. Feature surfaces may own the icon choice, click handler, label text, and layout slot, but icon-button size, tone, focus ring, disabled treatment, title fallback, and accessible name wiring must come from that shared primitive rather than page-local<button>plus inline SVG shells. Standalone machine row action triggers follow the same rule: the Machines table owns remove-agent semantics and menu placement, whileActionIconButtonowns the compact muted trigger chrome. AI Chat follows that same boundary for drawer header controls, session row actions, transcript fallback close/download actions, activity-dock queued follow-up controls, composer send, footer help/route actions, and compact dismiss controls: the Assistant owns chat/session behavior and copy, whileActionIconButtonowns h-5/h-6/h-7/h-8/h-9 sizing, outline, primary, accent, warning, info, danger, disabled, title, and focus chrome. AI Chat message and tool copy controls follow the copy-action boundary: MessageItem and ToolExecutionBlock own the copied text and timer semantics, whileCopyValueButtonowns copied-state iconography, disabled handling, focus, and embedded-row propagation behavior. Global app-shell prompts are part of the same action boundary.frontend-modern/src/components/GitHubStarBanner.tsxmay own its display timing, product copy, and GitHub destination, but its primary and dismiss controls must composeButtonandActionIconButtoninstead of carrying local floating-prompt button shells. The prompt must wait for fourteen distinct active days with connected infrastructure, consume at most one lifetime appearance, treat close as permanent dismissal, and never restore recurring snooze behavior. It must yield the browser session to an existing release, disclosure, or other low-priority app-shell notice and suppress itself for the remainder of a session that presents a blocking dialog. Low-priority app-shell consumers must coordinate throughreserveLowPriorityNoticeSessionrather than restoring component-local cooldowns that can produce consecutive prompts. Settings selection helpers such asResourcePickermust use the sameButtonprimitive for select-all, clear, and chip remove actions instead of restoring footer-local action shells. The picker must also keep initial all-resource hydration distinct from a genuinely empty estate: while the shared resource source is loading it presents a status placeholder, and only renders its empty-state copy after hydration completes. Intentional all-resource consumers use the shared websocket-first/canonical-REST race rather than waiting for every REST page before their first usable snapshot. Reporting surfaces must use the same primitive for retry, generate, and export actions rather than restoring large local CTA button shells. Self-hosted commercial plan, retry, activation, and clear-key actions follow the same shared Button boundary: commercial surfaces own the labels, entitlement state, and click handlers, whileButton,ButtonLink, andUpgradeButtonLinkown the primary, outline, warning, and upgrade/link chrome. Manual self-hosted key recovery is a secondary detail in that same boundary: settings surfaces may expose the fallback, but they must label it as license recovery/key recovery and keep normal checkout plus the Pro Patrol-mode setup path ahead of recovery mechanics or activation-key terminology. Hosted billing admin organization row actions follow the same boundary: cloud-paid surfaces own Suspend, Activate, Reload, tenant state, and mutation semantics, whileButtonowns the row-action chrome through the secondarysmandxssizes. Security authentication settings actions follow the same boundary: security/privacy owns auth setup, password-change, credential-rotation, and read-only semantics, whileButtonowns the warning, primary, secondary, and settings-action chrome. Organization RBAC settings actions follow the same boundary: organization settings owns role creation, role editing/deletion, user-access assignment, feature-gate, and row-action semantics, whileButton/ActionIconButtonown primary, ghost, accent, danger, focus, disabled, and settings-action chrome. Organization overview, access, invitation, member, and sharing actions stay in that same primitive family: organization settings owns membership and share semantics, whileButtonowns primary, danger-outline, success-ghost, danger-ghost, disabled, focus, and row-action chrome. If a new surface needs a variant that the shared primitive does not expose, extend the primitive and registry guard rather than adding a page-local class string. Drawer header command and icon actions belong to that same shared Button primitive family. Workload and infrastructure drawers may own which actions appear and the action labels, but theh-8Assistant, copy-context, close, and future drawer-header action chrome must composeDrawerHeaderActionGroup,DrawerHeaderActionButton, orDrawerHeaderIconButtoninstead of copying drawer-local button classes. The object-drawer subject row and its collapse interaction belong toObjectDrawerHeader: the complete visible header is one semantic button with phone-safe target height, focus treatment, and a collapse chevron, while lifecycle or other object-specific action controls remain independent siblings above that button. Workload, infrastructure, shared inline, Ceph cluster, and Proxmox Mail Gateway detail consumers must compose this owner instead of leaving collapse on a small icon-only target or adding a local clickable wrapper around nested buttons. Copy-value affordances belong to the same shared button family. Feature surfaces may own the copied value, success/error notification, and adjacent product copy, but icon/chip copy controls must useCopyValueButton, and copyable command/path/value rows must useCopyableCodeRowinstead of recreating local copy icons, copied-state checks, disabled empty-value handling, orfont-monocode-row shells. Compact information-card frames in drawer overviews, discovery summaries, resource detail sections, web-interface URL editors, shared overview cards, and storage backup empty states belong toInfoCardFrame. Feature surfaces own the title, rows, actions, and sizing context, but the borderedbg-surface p-3 shadow-smframe must be composed throughInfoCardFrame,getInfoCardFrameClass, orINFO_CARD_FRAME_CLASSrather than copied as a page-local class string. Compact label/value facts inside those frames belong toInfoCardKeyValueRow. The shared row preserves endpoint alignment on phones, then switches to a fixed 7rem label track with the value immediately adjacent and left aligned on wider viewports. Consumers may opt into thesmtransition when their own card grid already splits at that breakpoint; otherwise the sharedlgtransition is canonical. Feature surfaces own the labels, values, wrapping, and selection behavior, but must not restore drawer-widthjustify-betweenrows on desktop. This contract also covers secondary drawer facts in availability status and suggestion cards, resource change-history entries, Docker/PBS/PMG service support panels, Docker container-update management cards, action history, Discovery summaries, and the specialized Proxmox Mail Gateway drawer. Headers, status summaries, actions, disk capacity summaries, and RAID state pairs may retain intentional endpoint alignment; ordinary label/value facts in those surfaces may not. Read-only metadata chips belong toMetadataBadgeand domain wrappers over it. Organization role and share-status chips must useOrganizationRoleBadgeandOrganizationShareStatusBadge, so role/status tone mapping, pill shape, fit behavior, and whitespace handling do not drift across organization overview, access, and sharing surfaces. Patrol finding, investigation, approval risk/state, tool-call result, run-history/status-bar resource, outcome, snapshot, scoped-run, workspace-tab count, and contextual metadata chips must also composeMetadataBadge; Patrol remains the label/count/semantics owner, but the visible badge shell, sizing, tone vocabulary, and whitespace behavior stay in the shared primitive andshared-template-registry.json. 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 throughMetadataBadge. Workload backup freshness follows a separate shared presentation boundary:frontend-modern/src/utils/workloadGuestPresentation.tsowns the canonical tone and icon mapping consumed by workload rows and drawers. A recorded backup may be green or amber according to the configured freshness thresholds, but age alone must not produce a red failure treatment. Red is reserved for the materially differentneverstate where no backup has been recorded; unsupported workloads and templates remain neutral rather than being classified as unprotected. Inline detail content belongs to the shared detail-section primitive family. Feature surfaces may own the platform-specific rows, section labels, and source model, but section row shaping, empty-row compaction, value tone classes, table rendering, and inline close-action chrome must come fromdetailSectionModel.ts,DetailSectionTable, andInlineDetailPanelinstead of localDetailFieldgrids or provider-named reusable primitives. A compact detail row may carry optional bounded rich value content for links, tags, aliases, and address badges while retaining a canonical text value for titles, tests, and operator-readable fallback. It may also carry optional bounded progress metadata. A detail section may carry bounded full-width footer content for supporting evidence such as a compact change summary; that content remains inside the section card and must not be squeezed into the value column or detached into a feature-local card. The sharedDetailSectionTablemust render progress metadata through the CSP-safeProgressBarwhile preserving the row's textual value as the primary operator-readable fact. Feature surfaces must omit the metadata when the measurement is unavailable rather than presenting an empty bar as 0%. Resource-detail drawer byte labels, integer labels, and count pluralization are part of that same primitive family: provider drawer models choose the fields and domain labels, but numeric detail values must route throughformatDetailBytesValue,formatDetailIntegerValue, andformatDetailCountValueindetailSectionModel.ts. VMware vSphere drawer detail sections follow the same boundary as TrueNAS and Kubernetes: vSphere owns which rows are meaningful, while row shape, section shape, tone classes, and table rendering must stay onDetailSection,DetailRow,makeDetailRow,compactDetailRows,compactDetailSections, andDetailSectionTablerather than provider-local row/section aliases or custom vSphere card loops. Framed product table surfaces must consume the sharedTableCardframe andTableCardHeadertitle/action band instead of composing page-localCardborder, background, overflow, or table-title chrome. Feature owners may own the table data, filters, columns, and row behavior, but the outer product-table frame, section header band, and light/dark border treatment belong to frontend primitives so Infrastructure, Workloads, Storage, and Recovery do not drift visually. The sharedTableprimitive owns the horizontal scroll shell (overflow-x-autoplus touch scrolling), and the canonical.table-scroll-shellCSS explicitly suppresses vertical overflow so a phone gesture that starts over a table scrolls the page rather than an incidental nested vertical scroll range. Feature tables must not wrap it in page-local scroll containers just to restore table sides or mobile overflow. Headerless product tables, including alert history, still useTableCardfor the outer frame instead of hand-coded rounded/bordered wrappers. Product tables already inside a canonical section frame, including storage pools, physical disks, and infrastructure settings source/configured-node tables, must useTabledirectly rather than nesting another card or scroll wrapper. If a framed table needs bounded vertical height, that constraint belongs onTable.wrapperClassso the shared table shell still owns overflow behavior. Resource-detail drawer tables that consumeTable, including Docker Swarm services, Kubernetes namespaces/deployments, and PMG detail tables, inherit the same scroll-shell owner instead of carrying drawer-localoverflow-x-autowrappers. Other product table surfaces, including deploy wizard target tables, AI cost tables, Ceph tables, PMG resource panels, andPulseDataGrid, must follow the same rule: feature owners may passwrapperClassfor bounded height, border, radius, or scrollbar hiding, but they must not add raw table markup or local scroll wrappers around the shared table primitive.PulseDataGridalso owns its root frame variants: feature surfaces embedded directly inside an existing panel/card frame must use the sharedframe="flush"mode rather than caller-local border overrides, horizontal-scroll wrappers, or negative margin compensation. Dense platform tables must also preserve useful operational context at phone widths.PlatformTableShellowns the responsive minimum-width policy, preserves any explicit base floor declared by the feature table, and otherwise applies the shared zero-width platform minimum before breakpoint-specific widths take over. Its class composition removes a consumer's redundantmin-w-fulltoken because the sharedTablealready fills the available width and that second minimum would override the phone-width policy. Platform consumers must explicitly select the three to five high-value phone columns that answer the row's operational questions, keep the identity column at the canonical 30 percent of the available width, use the shared compact label and column-width classes, and preserve the fuller tablet and desktop presentation. A sixth track is valid only when rendered values remain legible at the measured phone width. Duplicate state or severity text must not consume a separate phone column when the identity indicator already carries the same condition; the full value remains in the desktop table and inline detail. Consumers mark a normal-phone demotion withplatform-table-phone-hiddenon both its header and its body cell. The shared container rule owns the matchingth/tdvisibility so the two cannot drift. Below a 360-pixel content width, the shared narrow layout promotes identity to 40 percent and may remove one additional lowest-priority context column viaplatform-table-narrow-hiddenrather than shrinking names and metric values into illegible fragments. The shared container rule then applies that stage across provider tables, direct Storage tables, and nested provider detail tables without a global reveal or page-local media-query exception.PlatformTableShellmarks every owned table with the sharedplatform-tableclass; direct Workloads and Storage consumers and nested provider detail tables must apply that same marker so the phone contract cannot vary by rendering path. The marker owns a canonical 32-pixel summary-row rhythm across every provider and excludes inline detail rows, which remain content-sized. Summary cells must render one line only: secondary identity, raw provider labels, and descriptive context belong in an existing operational column, a supplemental tooltip, or its inline detail drawer rather than a stacked subtitle. Text-only operational rows may usegetPlatformTableRowClassto declare that same shared rhythm explicitly; providers must not introduce local row-height exceptions. Mobile truncation may rely on a full-value row detail only when that row actually has a keyboard- and touch-operable disclosure; a non-expandable identity cell must otherwise expose its complete value without requiring hover. Default data rows stay single-line so scan density and row rhythm are not traded for automatic text wrapping. When a platform table row itself owns that keyboard- and touch-operable disclosure,SummaryRowActionButtonremoves the redundant visible chevron below the shared mobile breakpoint while preserving the button for screen readers and revealing it on keyboard focus.PlatformResourceDetailToggleButtonapplies that policy to provider-native platform tables, and workload rows opt in only while their compact row remains the disclosure target. Every disclosure-button consumer must declare that row-target relationship explicitly through the requiredhideWhenRowTappableOnMobileboolean, so a new table cannot silently restore a redundant mobile chevron. Rows whose click action does something other than expand details must passfalseand keep a visible disclosure control. A grouped row may retain its summary focus or pin behavior while also owning disclosure, but when it does, the row click must perform both actions and suppress the redundant mobile chevron; summary interaction is not a reason for a one-off visual exception. They must not rely on a global rule that reveals every hidden column: phone priority remains source-specific because state, capacity, freshness, and recovery posture do not carry equal value for every resource type. Narrow viewports keep document-level overflow contained byTableand scroll the table itself. The shared scroll shell owns inline-size and paint containment plus horizontal overscroll containment so a readable table floor cannot widen or horizontally pan the application document. Feature tables must not squeeze every declared column into the viewport, override the shared floor, or add a second page-local scroll wrapper. Product-table subgroup/header rows must likewise consume the sharedfrontend-modern/src/components/shared/groupedTableRowPresentation.tshelper and.grouped-table-rowCSS token contract instead of localbg-surface-altor page-specific hover fills. This applies to grouped rows across Infrastructure, Workloads, Storage, Recovery, alert history, alert threshold tables, and Infrastructure Settings source-manager tables; feature owners may own group content and behavior, but not duplicate the subgroup band styling. Shared progress and metric-fill motion belongs to the frontend primitive CSS contract infrontend-modern/src/index.css. Generic progress bars must keep the CSP-safeProgressBar/foreignObjectshape and use the shared.progress-fill-frameand.progress-fillclasses for width and color transitions instead of inline styles or page-local animation wrappers. The same global CSS owner must provide theprefers-reduced-motiondisable path for these fills so feature surfaces inherit one accessibility policy. Numeric readout motion belongs tofrontend-modern/src/components/shared/AnimatedNumber.tsxand itsuseAnimatedNumberStateowner. Feature surfaces may opt metric labels and compact counters into that primitive, but must not create local counter timers, page-specific easing, or independent reduced-motion policy. The shared owner must also cap concurrent numeric animations and snap overflow readouts to their current target so a realtime estate update cannot make animation cost grow with the number of visible resources. Shared primitives must not reintroduce app-shell monitored-system capacity banners. Monitored-system grouping and ledger presentation belongs in the owned settings surfaces, while commercial plan explanation belongs incloud-paidplan surfaces. Mobile navigation under the same shared boundary owns tab accessible names: icon components may keep their standalone labels, but the nav must treat those icons as decorative inside tab buttons so names come from the tab label plus meaningful badge counts, not duplicated icon titles. That mobile route surface must remain a bounded navigation rail rather than compressing every destination into unreadable tabs. It keeps the active priority platform destination together with Alerts, Actions, and Patrol in the fixed rail, groups the remaining platform and utility destinations (including Settings) behind a labeled More menu, and preserves active-state and badge context on that disclosure. The route rail is a semanticnav, not a tablist; the More menu must support first/last/active focus, arrow-key movement, Escape focus return, outside dismissal, and route-change closure. Shared grouped-resource presentation primitives must keep grouped resource labels operator-readable: count-led labels may aggregate repeated resources, but uncountable or category-like resource types such as storage must use resource wording instead of naive pluralization. Infrastructure table chrome on active platform/runtime pages must use mode-oriented labels for table presentation controls: grouped table mode isGrouped, notCluster, because cluster remains a platform/resource concept for Proxmox, Kubernetes, and similar inventory details. The retired top-level infrastructure feature directory must not be recreated for table chrome ownership. Settings shell search copy belongs tofrontend-modern/src/utils/settingsShellPresentation.ts. Shared settings search must not display non-actionable shortcut chips such asAny key; if the shell exposes a shortcut hint, it must name an actual key chord, otherwise the hint must remain unset so the sharedSearchInputrenders no shortcut chip. Native select state belongs to the sharedfrontend-modern/src/components/shared/FormSelect.tsxprimitive. It must apply controlledvalueprops after options are mounted so settings panels such as Discovery show the persisted option instead of falling back to the first option while the collapsed summary shows a different value. The Assistant runtime controls infrontend-modern/src/components/Settings/AIRuntimeControlsSection.tsx— e.g. the service context scanToggle— are settings-shell chrome bound to the canonicaluseAISettingsStateform and/api/settings/aipayload, not local browser state. Each must bind to astate.form.*field, round-trip through the field-by-field settings payload, and source its label, help, and summary copy fromfrontend-modern/src/utils/aiSettingsPresentation.tsrather than inlining strings or reaching for a bespoke fetch outside the canonical payload. There is no cloud-context-privacy control here: cloud context behavior is a fixed posture (seeai-runtime), not an operator setting. -
Add feature-specific presentation only when no shared primitive should own it. Feature surfaces under
frontend-modern/src/features/that display product labels must consume the owning subsystem's presentation utilities rather than hard-coding divergent page-local copy. Shared primitives and feature shells may compose those labels, but they must not become a second source of truth for alert, storage, recovery, infrastructure, workload, or adjacent product vocabulary. Table-mode segmented controls that expose a grouped/list view mode must usefrontend-modern/src/components/shared/GroupedTableModeSegmentedControl.tsxso the shared primitive owns theGroup byaccessible label,GroupedandListvisible labels, tooltip titles, and icons instead of each resource surface rebuilding that language with subtly different resource-specific concepts. SharedPageControlsowns trailing filter-row actions such as toolbar display controls, utility buttons, Columns, and Reset. Controls that should wrap with the column/reset cluster must enter throughtoolbarTrailinginstead of staying as loose filter-row children, and those controls must stay grouped when dense toolbars wrap so popovers remain viewport-safe instead of 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 future dense toolbars do not strand Filter/Columns/Reset actions as an isolated second-row fragment. SharedFilterToolbarPanelowns default filter-popover geometry, andFilterToolbarowns the shared chart visibility display action: Workloads, Storage, Infrastructure, and future summary-bearing pages must useChartVisibilityToggleButtonso the affordance exposes oneShow charts/Hide chartspressed-state contract instead of rebuilding a one-option segmented control or an in-summary collapse chevron page by page. Feature state hooks underfrontend-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, focus treatment, pressed-state affordances, and accessible label builders for repeated controls. Estate-sized Proxmox backup tables follow that split: the storage/recovery feature owns its full-set-to-window projection and spacer placement while reusing the canonicaluseTableWindowingmath; it must not invent a feature-local pagination or load-more button shell to avoid mounting the complete result set. Recovery timeline columns follow that split: storage/recovery owns the range, selected day, chart/table transport windows, and bucket data, while the shared frontend boundary owns the reusable button focus/selected styling and ARIA wording so columns expose singular/plural recovery-point labels plus selected state consistently. Frontend primitives must not fetch recovery data, infer recovery date ranges, or carry a parallel selected-day store just to render timeline columns. Compact, stable, mutually-exclusive filters with two to five options should useLabeledFilterToggleGroupas a responsive control: toggle buttons at wide desktop widths and the native select fallback below that. Dynamic and user/environment-sized option filters remainLabeledFilterSelectsurfaces so estate-sized lists such as nodes never become button groups. A fixed six-state operational lifecycle may useLabeledFilterToggleGroupwhen every choice is a frequent triage action, the complete segmented group fits at its wide breakpoint, and the native select fallback owns all narrower layouts; this bounded exception must not be generalized to dynamic six-plus filters. Filters that change which other filters exist, such as Workloads Type, must stay in a stable primary filter band ahead of the dependent estate/data filters so changing the parent filter does not move its own click target or the adjacent primary filters; when multiple filters are expanded into button groups at wide desktop widths, each expanded group must have its own row rather than sitting immediately after another expanded group. User-facing filter options must use operator mental models rather than implementation categories: Workloads Type exposes a singleContainersbucket while thesystem-container/app-containerdistinction remains an internal data/deep-link compatibility detail.PageControlsowns the default stacked control deck for page-level filters: filter controls, display/chart controls, Columns, and Reset inherit one shared structured command deck with visible section boundaries instead of each page passing localcontrolDeckClass, action-rail, border, or background strings. Pages that have multiple semantic filter groups may set the sharedfilterControlsVariant="sectioned-children"mode and wrap those groups withpageControlsFilterSectionClass, but the deck chrome and trailing action section remain frontend-primitives owned. Those structured decks must give each semantic section a visible boundary so adjacent radio groups, scope filters, and display actions do not collapse into one hard-to-scan strip. Narrow consumers such asColumnPickermust opt into their panel width through that primitive rather than layering competing width classes page by page. -
Add guardrail tests when a new shared pattern is introduced. Shared monitored-system primitives must prove they remain informational grouping or ledger surfaces rather than admission-freeze banners, cap summaries, or
current / limitquota math. Shared modal scroll containment follows that same owner split. The dialog shell infrontend-modern/src/components/shared/dialogModel.tsmust keep shared panelsmin-h-0, and page-owned modal bodies may useoverflow-y-autoonly under shrinkable flex columns instead of clipping lower fields behind a fixed-height shell. Shared filter popovers follow the same primitive-level ownership. The sharedFilterToolbarpanel class must render above nested card, table, and empty-state shells, and feature pages embedding those controls must make only their immediate control shell overflow-visible rather than forking local z-index or popover positioning rules. The shared navigation guide owns route-aware first focus: when it opens from a platform-owned product surface such as a recovery tab, the first highlighted step should match that route instead of always restarting at Dashboard. -
Keep shared infrastructure shell state on the reusable settings boundary:
frontend-modern/src/components/Settings/useSettingsInfrastructurePanelProps.tsandfrontend-modern/src/components/Settings/InfrastructureWorkspace.tsxmust continue to derive provider counts and shared subtab copy from one infrastructure-settings source — via the unified aggregator throughfrontend-modern/src/components/Settings/useConnectionsLedger.ts— instead of creating provider-local summary fetches or VMware-only shell vocabulary. Phase 9 retired the oldPlatformConnectionsWorkspaceper-type shell; setup guidance should now useAdd infrastructureplus source-strategy language for API-backed onboarding. The standalone connections-table presenter is retired;frontend-modern/src/components/Settings/InfrastructureSourceManager.tsxis the only landing-ledger presenter for configured infrastructure rows, and it must exclude agentless availability probes because those belong tofrontend-modern/src/components/Settings/AvailabilitySettingsPanel.tsx. The first-run setup wizard inherits that same source-strategy vocabulary: step labels and completion copy must frame the final setup step as choosing the first infrastructure source, not installing a host. Successful token validation and security setup transitions should rely on the wizard progress state instead of transient success toasts that can cover the credential handoff. Generated first-run admin passwords must use browser cryptographic randomness rather thanMath.random. That same shared shell boundary now owns the first-run posture for/settings/infrastructure: the landing route should read as one source-manager workspace with configured infrastructure instances first and no redundant monitored-systems ledger beneath it. The landing route may keepAdd infrastructurein the Connected systems header while the ledger remains the first primary content. Discovery is optional setup after the ledger, not a competing first-viewport toolbar. Per-source add actions, includingInstall Pulse Agent, belong on the governed source rows, andDetect addressstays inside the single API-platform probe path instead of a duplicate toolbar action. It may also show one compact posture line derived from the same unified connection rows so operators can confirm the top-level connected-system count, active state, actionable health, and limited coverage without opening a tour or second ledger. That line must expose the relevant install action when host telemetry is missing rather than expanding into a metric strip. Existing sources stay visible in stable source-catalog order, and add, detect, install, review, and manage flows open as secondary interactions from that same destination instead of taking over the whole page. The same source-manager workspace may show a compact fleet-governance strip and row-level fleet attention badges, but those badges must be presentation of the canonical/api/connectionsfleetobject rather than another frontend-owned lifecycle classifier. Those secondary views must stay under the same singleInfrastructuresidebar destination, but they may open in governed modal/dialog chrome when that preserves the persistent source-manager page behind them. That governed dialog chrome must also preserve inner form scrolling:InfrastructureWorkspace.tsxandConnectionEditor.tsxkeep the add/edit shell onmin-h-0flex columns so long credential forms scroll inside the modal body instead of trapping the lower fields below the fold. The same shared shell boundary now also owns grouped source-row composition.useConnectionsLedger.ts,InfrastructureSourceManager.tsx, andInfrastructureWorkspace.tsxmust render attached collection methods as a compact labeled badge beside the owning system (API,Agent, orAPI + Agent), with the plain-language source phrase available through accessible metadata and fuller detail in the edit dialog, instead of duplicating the same machine across multiple peer groups or spending a dedicated Method column on implementation detail. The table-level product/system group rows inInfrastructureSourceManager.tsxmust also use the shared grouped table row presentation helper, not local table-background classes, so source-manager grouping stays visually consistent with the product tables. That same shared shell boundary owns the landing taxonomy too: the primary grouping labels in the infrastructure manager must describe real platform/system owners, not collection methods. Agent-only machines belong in a standalone-host bucket, whilePulse Agentremains a collection- method label, install path, and detail-surface concept rather than a peer top-level pseudo-platform beside Proxmox, VMware, and TrueNAS. That same shared shell boundary also owns compact version visibility for agent-backed rows. The infrastructure source table must not grow a dedicated always-on version column for Pulse Agent; exact version text belongs in the edit/detail surfaces, while the landing table only surfaces a compact warning badge when an attached or standalone agent actually has an update available. That same table boundary must reuse theSystemcell for compact standalone-agent identity such asUnraid 7.1.0; raw reported addresses belong in the governed Manage detail or an explicitly expanded cluster-member row, not an always-on diagnostics column. That same shared shell boundary now owns one canonical infrastructure destination in the Settings sidebar.InfrastructureWorkspace.tsxowns the source-manager landing inside that destination, while route-backed add flows and local edit flows stay single-purpose instead of stacking multiple page-level workspaces at once. The source-manager landing now also owns the explicit discovery strip for that destination.InfrastructureSourceManager.tsxexposes one optional Discover Proxmox systems status/action band after the systems ledger with scan state, saved scope, last result metadata, errors,Run discovery,Settings/Configure discovery, and candidate review when discovered sources are waiting. It must not start a network scan just because the page rendered. New-source admission belongs on the table's per-platformAddactions, the compact first-run/readiness actions, or the discovery band's explicit review action, and the direct address-probe utility may appear as first-run setup guidance instead of a second saved-network-scan command. Discovered API-backed candidates stay visible in the same platform-group table as configured sources, using the existing tree/table hierarchy instead of spawning a second discovery-only page or card stack.InfrastructureWorkspace.tsxmust still open a new connection throughfrontend-modern/src/components/Settings/ConnectionEditor/ConnectionEditor.tsx, but the editor now serves as governed dialog content under the source manager rather than replacing the page inline. The?add=pickroute owns the search-first infrastructure source finder,?add=detectowns the detect-from-address utility, and typed add routes jump straight into the matching credential slot throughinitialType. The picker must keep that first choice in recognizable system/service vocabulary, while the typed add dialog may use the shared source-strategy vocabulary after selection to explain API inventory, Agent telemetry, or API + Agent coverage. Agent-backed typed add routes keep the same governed dialog shell, but their embedded installer surface should stay focused on the selected system so the first visible command path does not re-expand into irrelevant platform choices. When an already-configured source is an agent-backed host profile, the source manager must group it under the operator-facing profile family and keep its add action on the same typed route instead of collapsing it back into generic standalone-agent copy. Credential slots are dispatched by the detected or manually-selected type and must still reach the canonical form body rather than diverging into a revived provider-specific workspace. For PVE, PBS, and PMG, the credential slot isfrontend-modern/src/components/Settings/ConnectionEditor/CredentialSlots/NodeCredentialSlot.tsxand it must composeNodeModalBasicInfoSection,NodeModalAuthenticationSection,NodeModalMonitoringSection, andNodeModalStatusFooterinline under the editor shell rather than embedding the full Proxmox workspace (discovery card, configured nodes table, delete dialog, node modal stack). For TrueNAS and VMware the credential slots arefrontend-modern/src/components/Settings/ConnectionEditor/CredentialSlots/TrueNASCredentialSlot.tsxandfrontend-modern/src/components/Settings/ConnectionEditor/CredentialSlots/VMwareCredentialSlot.tsxand they must render only the connection form body inline under the editor shell — no connection list, no row actions, no surrounding panel chrome. Showing a ledger of other systems inside the credential slot is exactly the ledger-inside-editor drift this contract forbids. The configured-connections summary and source-manager rows themselves must render exclusively from the aggregator.InfrastructureWorkspace.tsxcomposes the platform-banded systems table fromfrontend-modern/src/components/Settings/useConnectionsLedger.ts(pollingGET /api/connections). Table rows may open a governed edit dialog for mutable sources, but pause, resume, remove, last-error detail, and agent uninstall commands must live inside that owned edit surface or the shared row-action owner rather than returning to inline landing-page action clutter or a revived provider-specific detail page. When the backend marks a grouped Proxmox row with canonical cluster identity, the table primitive must render that cluster moniker as the row title instead of falling back to one sibling node hostname or reopening a standalone-host presentation for cluster-member agents. When that grouped row also carries backend-authored cluster members, the table primitive must render those nodes as child composition beneath the cluster row rather than flattening them back into peer top-level systems or hiding them entirely. The governed connection editor may assign an optional display name to each PVE cluster member only through its backend-authored immutablenodeIdentity. It must show the current native Proxmox name alongside an override, explain that clearing restores the native name, and keep the connection address as a separate control. Optimistic settings projection may update the display field only; it must not rewrite native name, host, IP override, credential, fingerprint, node identity, or provider node ID. Duplicate display values are valid presentation, so row keys, expansion, search ownership, edit targeting, and grouped-member joins must continue to use backend identity rather than label text. The same table shell must keep fulfilled rows visible across polling and manual reloads by using a retained-value query boundary, not app-level Suspense or a blank loading replacement, so configured infrastructure does not disappear while the next/api/connectionsrequest is in flight. The systems table and setup summary must count the same visible posture highlights they render, not hidden raw fleet signals. Passive attached-agent config or rollout handshakes whose only cause is a missing comparable applied configuration fingerprint may stay in the raw row model for deeper diagnostics, but they must not create duplicate cluster-parent badges or aNeeds attentioncount when the visible row/member posture is otherwise healthy. That same landing-shell boundary also owns represented-host dedupe between the unified ledger and the discovery strip.InfrastructureWorkspace.tsx,frontend-modern/src/components/Settings/useConnectionsLedger.ts, andfrontend-modern/src/components/Settings/infrastructureSettingsModel.tsmust treat backend-authored hostname/IP aliases as canonical identity so an already-represented platform row, attached agent augmentation, or grouped member suppresses the matching discovered candidate instead of showing the same machine twice under hostname-versus-IP drift. Phase 9 retired the parallel reporting/inventory surface entirely:useInfrastructureReportingState,InfrastructureOperationsController,InfrastructureInventorySection,InfrastructureActiveRowDetails,InfrastructureIgnoredRowDetails,InfrastructureStopMonitoringDialog, and the per-type shellsPlatformConnectionsWorkspace,ProxmoxSettingsPanel,ProxmoxDirectWorkspace,NodeModal.tsx,TrueNASSettingsPanel, andVMwareSettingsPanelno longer exist. The aggregator plusConnectionEditoris the only path; no parallel reporting state, stop-surface dialog, ignored-row fallback, or per-type workspace may be reintroduced.connectionsTableModel.tscarries only the connection-scopedSystemManageActionvariant —inventory-active/inventory-ignoredmanage kinds must not return. Active infrastructure settings and platform/runtime surfaces inherit that same source/platform vocabulary. Settings may label configured ingestion entries and endpoint probes asSource, while resource tables label their primary identity column asSystem. Lower-level unified-resource contracts preserve merged-source detail for tooltips, accessibility metadata, and routing. Collection methods such as Pulse Agent and runtime capabilities such as Docker may appear as option or detail labels, but they must not become the primary top-level system wording when a provider/API platform or reported host OS/appliance identity better explains what the operator is looking at. -
Keep settings deep-link route selection on the shared settings-navigation boundary.
frontend-modern/src/components/Settings/settingsNavigationModel.tsandfrontend-modern/src/components/Settings/useSettingsNavigation.tsmust treat the canonical PBS and PMG Proxmox deep links as agent-selection authority even though those URLs resolve to the sharedinfrastructure-operationstab. Reloading or remounting on a PBS or PMG deep link must not silently fall back to the PVE selector state. Assistant OAuth callback compatibility queries such asai_oauth_errorandai_oauth_successmust route the bare settings root to Pulse Intelligence > Provider & Models while preserving the query long enough foruseAISettingsStateto consume and clear it, rather than normalizing the user back to Infrastructure and dropping the callback result. The canonical Agent Doctor browser route is /settings/infrastructure/agent-doctor with optional agents scope query parameters; it is the only live routed subpath under the /settings/infrastructure workspace path (all other infrastructure subpaths stay retired compatibility paths), it resolves to theinfrastructure-systemstab, and the settings navigation hook must canonicalize pre-route agentDoctor=1 and agentUpdates=1 workspace queries onto that route with their agents scope preserved instead of rendering Agent Doctor as a dialog stacked over the workspace. -
Keep shared storage feature presenters on canonical platform truth. When reusable storage presenters under
frontend-modern/src/features/storageBackups/classify canonical resources for the shared storage route, API-backed virtualization datastores such as VMware must stay inventory-only datastores instead of inheriting PBS-specific backup-repository or protected-target copy from older fallback branches. Those reusable storage presenters must also keep primary issue copy separate from contextual impact copy. Composite posture fields may include dependent resource or protected workload impact, but shared table/presenter primitives must derive primary issue labels and summaries from explicit incidents, storage risk summaries, or storage-risk reasons so healthy rows do not render impact text as a warning. The sharedresolveResourcePlatformType(resource)helper infrontend-modern/src/utils/sourcePlatforms.tsis the canonical reader for "what platform family does this unified resource belong to" and must be used by every frontend consumer that buckets unified resources by family (platform pages, filter resolvers, presentation pickers). The helper prefersresource.platformTypewhen present and falls back to the resource'ssourcesarray via the existing source-platform normalization, so client-side family grouping behaves identically against mock fixtures and live backends that leaveplatformTypeempty on a subset of canonical resource types. Workload page membership must use the canonicalplatformScopeslist when present instead of treatingplatformTypeas exclusive ownership. A Docker or Podman app-container can therefore carry both the container runtime lens and its owning platform page in routing/filter context, such as Proxmox when the runtime is detected inside a PVE LXC, while TrueNAS app containers stay scoped to TrueNAS even when their runtime metadata uses the shared Docker facet. That membership overlap is not permission to duplicate the detailed container table into every platform overview: Proxmox Overview keeps the default Workloads peer table to VMs and LXCs, and Docker-in-LXC evidence belongs as LXC drawer detail while/dockerremains the canonical detailed Docker / Podman container lens. The overview table should not add peer rows, badges, or child rows for Docker containers; those signals compete with VM/LXC state and belong one click down. The default row may carry only a quiet icon/count cue beside the guest name to show nested runtime presence; names, metrics, state, and actions stay in the drawer or Docker lens. Shared platform-scoped storage presenters follow the same membership rule. A physical disk may be sourced only from the agent while canonically owned by a Proxmox node, so hidden provider-family filters must consultplatformScopesbefore falling back tosourcesorplatformType. Drawer-to-runtime navigation is still part of the shared platform-table affordance contract: when the LXC drawer exposes anOpen Dockeraction for nested containers, that action must use the Docker host facet route state so the target Docker Overview opens scoped to the same runtime instead of a broad, visually unrelated container list. Primary navigation uses that same membership model: the Docker route is the container-runtime lens and may be labelledContainersin the shell, while shared source badges, filters, and runtime management copy continue to useDocker / Podmanwhere the capability itself is being named. Kubernetes workload rows on/kubernetes/workloadsmust render through the Kubernetes-native workload tables rather than a generic infrastructure or inventory table. Pods render throughfrontend-modern/src/features/kubernetes/KubernetesPodsTable.tsxwith Pod-native phase, readiness, restart, owner, node, image, and age columns; legacy/kubernetes/podsresolves to the same workflow. Controller rows render throughfrontend-modern/src/features/kubernetes/KubernetesControllersTable.tsx; legacy/kubernetes/controllersresolves to the same workflow. The table boundary preserves platform-native API fields for StatefulSets, DaemonSets, Jobs, and CronJobs, including targets, active/current counts, ready/succeeded counts, availability, exceptions, service names, schedules, and last run metadata. Kubernetes Overview keeps the cluster inventory above the native workload inventory so multi-cluster operators can orient and scope in one surface, matching the Docker Overview host-to-container flow. Selecting a cluster name writes the canonicalclusterquery parameter and scopes every Deployment, Pod, StatefulSet, DaemonSet, ReplicaSet, Job, CronJob, and autoscaling section beneath it; selecting the active cluster again clears that scope. The same URL-backed Cluster facet is available on Workloads, Services, and Configuration and composes with the existing namespace, search, and status parameters so bookmarks and shared links retain the complete scope. Changing cluster scope clears namespace scope to prevent a namespace from the previous cluster from silently hiding all rows. Cluster names remain separate from their external web-interface control and detail disclosure: the name scopes inventory, the adjacent external-link icon opens the persisted cluster URL, and the row continues to own drawer expansion. -
Keep shared source/platform vocabulary on the governed manifest boundary.
frontend-modern/src/utils/platformSupportManifest.generated.tsmust be the tracked frontend projection ofdocs/release-control/v6/internal/PLATFORM_SUPPORT_MANIFEST.json,frontend-modern/src/utils/platformSupportManifest.ts,frontend-modern/src/utils/sourcePlatforms.ts, andfrontend-modern/src/utils/sourcePlatformOptions.tsmust consume that generated projection instead of embedding divergent future-label lists, setup/onboarding path allowlists, host-profile labels, surface-kind guesses, readiness-state guesses, or presentation-only guesses, andfrontend-modern/scripts/canonical-platform-audit.mjsmust fail when the generated projection drifts from the governed manifest. The generated governance/readiness split is authoritative: supported platform arrays drive current support claims, while admitted platform arrays may keep route/navigation and add-flow vocabulary available without turningfirst-lab-readyentries such as VMware into supported-source copy. Kubernetes manifest projections must enumerate the native API-backed page sections the shared tab shell can expose, including controllers, networking, storage, config, policy, autoscaling, and events, so platform pages do not invent local support claims outside the governed JSON. The generatedsurface_kindis the machine-readable boundary between owning platform entries and runtime lenses:dockeris aruntime-lens, not aplatform, even when the container-runtime route stays available as a primary shell destination. The genericdockersource-platform label is "Docker / Podman" in shared selectors, badges, and filter options so v5 Docker users can find the runtime surface while Podman-backed rows are not mislabeled as Docker-only; "Container runtime" remains the governed runtime family, not the primary customer-facing label. Identity colour is semantic, not page-local decoration: shared source/platform badges, host identity badges, and container runtime badges must use the shared presentation helpers so Docker remains on the Docker/Podman blue runtime tone, Podman uses its distinct runtime tone, Proxmox PVE remains orange, and those meanings do not drift across table rows, filters, drawers, or platform pages. Agent host-profile entries, including Unraid, stay in the generatedagentHostProfilesprojection and shared wrapper helpers; frontend primitives may render those labels for Pulse Agent install/identity copy but must not add them to the first-class platform union. The generated host-profile projection also carries runtime platform fallback metadata for shared explanation and parity with backend normalization, but frontend primitives must still render host-profile labels through explicit backend profile fields such asagentIdentity.hostProfileand unified-resourceplatformData.agent.hostProfilerather than prettifying presentation-only ids as platform values. Raw appliance identity aliases such asunraid-osbelong only in the generated host-profile token list so shared helpers resolve them tounraidbefore presentation. InfrastructureSystembadges must append the platform runtime version when the payload proves that version belongs to the displayed platform identity, such as PVEpveVersionor a Pulse Agent report whose OS identity resolves to Unraid or Proxmox VE. They must omit the version rather than showing unrelated collector OS versions, such as Debian 12, beside an API-backed PVE badge. Shared row primitives that render Proxmox node identity, includingfrontend-modern/src/components/shared/NodeGroupHeader.tsx, must route raw PVE manager payloads throughfrontend-modern/src/utils/proxmoxVersion.tsrather than inlining page-local parsing or falling back to unrelated agent OS versions. System title metadata must apply the same identity rule: once the primary system badge names a platform with its version, source/method context may still add collection labels such as Pulse Agent, but it must not repeat the same platform again as an unversioned source badge. Proxmox Overview node rows also preserve provider availability explicitly. An offline or stale provider observation keeps the node identity, external link, detail affordance, and provider-scoped guest counts visible on desktop and mobile, while live uptime, temperature, bars, and sparklines render as unavailable. The row must expose visibleOfflineorStaletext rather than relying on an aria-hidden dot or opacity. Same-named cluster and member rows use Proxmox instance/platform scope for grouping, search ownership, and guest counts; a display label alone is not a cross-provider join key. Shared resource search must match the preferred display label, current and prior native Proxmox node names, and immutable node identity. Overview, storage, backups, alerts/history, infrastructure settings, API/websocket, and responsive/mobile rows must present the same preferred label while retaining native diagnostics; page-local formatting such ascluster (node)must not replace the backend-authored presentation. On the Proxmox overview, the committed Workloads search is shared state for both the guest table and the node table: guest matches retain their owning node, direct node matches retain that node, and unrelated nodes are hidden. The node table must use the same provider-scoped search vocabulary rather 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. -
Keep summary chart interaction identity on one shared helper. Summary surfaces that expose row-hover, group-hover, chart-hover, or route-focus-driven chart emphasis must derive page/group/entity scope through
frontend-modern/src/components/shared/summaryCardInteraction.tsand pass that same resolved scope into card-state, sparkline, and density-map primitives, rather than letting cards readhovered || focusedwhile charts listen to a different page-local ID source. Hovering one summary chart must promote that series into the shared active entity so sibling cards highlight the same object instead of keeping chart-local hover islands, and hovering or pinning a workload group header, infrastructure cluster header, or storage pool-group header must scope the matching summary cards through that same shared contract instead of forking a page-local summary filter path. Sibling cards should surface that synchronized hover as one compact header readout through the shared summary-card contract, while the chart under the pointer keeps the only floating tooltip. Recovery is explicitly outside this interaction dialect: its retired posture-card strip must not return with row/group/chart hover behavior without a separate governed product decision. -
Keep page summaries page-scoped when table rows enter contextual focus. Route-backed row selection may add a focused label and shared series emphasis, but infrastructure, workloads, and storage summary cards must continue to render the page-level series set instead of collapsing the summary down to the selected row or replacing the global trend view with row-local empty states.
-
Keep contextual row focus on the shared summary primitive. Summary surfaces and same-route table drill-ins must reuse
frontend-modern/src/components/shared/contextualFocus.tsfor interactive-series filtering, focused-name lookup, active-series derivation, local scroll preservation, and deliberate inline-detail reveal instead of rebuilding page-localSetfilters, focused-label scans, drawer-aware scroll math, or ad hoc scroll restoration in each surface. -
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-activeand let the shared presentation infrontend-modern/src/index.cssrender 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 exposedata-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 usefrontend-modern/src/components/shared/groupedTableRowPresentation.tsplus the.grouped-table-rowCSS contract infrontend-modern/src/index.css, rather than rebuilding localbg-surface-altvariants 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 composefrontend-modern/src/components/shared/InlineDetailTableRow.tsxfor the full-width row, surface-alt cell, detail padding, and row-click containment instead of rebuilding page-localTableRow/TableCell/divshells around each drawer. Storage-backed reusable row presenters underfrontend-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. -
Keep retained-value data loading honest at the ownership boundary. Helpers that prevent a feature surface from falling through the app-level Suspense boundary during in-flight refresh should stay feature-local until multiple governed surfaces truly share the behavior. Once that boundary is shared, promote the helper into an explicit shared hook owner such as
frontend-modern/src/hooks/createNonSuspendingQuery.tsrather than re-copying suspense-escape logic into each feature area or burying it inside one feature's private state model. -
Keep shared commercial warning banners truthful about destination intent. When a shared banner renders both explanatory and commercial CTAs, those labels must resolve to distinct owned destinations or section anchors instead of presenting two different labels that land on the same unscoped billing screen. Monitored-system capacity warning banners are retired; shared commercial banners must not render stale
current/limitcounts, paid-plan CTAs, usage summaries, or upgrade-impression telemetry from legacy monitored-system limit payloads. When a banner does need a review destination for a current paid feature, it must scope the operator into the usage-owned policy ledger rather than plan-selection intent or CTA copy that frames the flow as monitored-system-cap expansion. -
Keep assistant availability bootstrap on the shared app-shell boundary.
frontend-modern/src/useAppRuntimeState.ts,frontend-modern/src/App.tsx,frontend-modern/src/stores/aiChat.ts, andfrontend-modern/src/components/AI/Chat/index.tsxmust consume the backend-owned/api/security/status.sessionCapabilities.assistantEnabledfact instead of probing/api/settings/aior/api/ai/*during ordinary route bootstrap. Closed assistant chrome and non-AI settings panels may not initialize assistant runtime state until an owned assistant or Patrol surface is actually open.frontend-modern/src/stores/aiChat.tsis the shared drawer shell owner for assistant open/close state, focus handoff, and tenant-local context/session persistence; the app shell must not fork that state acrossApp.tsx,AppLayout.tsx, or page-level helpers. The same shared shell boundary must keep Pulse Assistant coherent while a blocking shared dialog owns the viewport: closed launcher affordances must hide until the dialog clears, and the shell must close any already-open assistant drawer instead of leaving background assistant controls visibly active behind the modal. Non-critical app-shell prompts, including promotional or feedback prompts, must not use the shared blocking dialog stack because they must not suppress Pulse Assistant access or look like required operational acknowledgement. AI-owned frontend surfaces that need shared settings or model-catalog truth must route those reads throughfrontend-modern/src/stores/aiRuntimeState.tsrather than each feature bootstrapping/api/settings/aior/api/ai/modelsindependently. Non-AI settings panels such asfrontend-modern/src/components/Settings/useAgentProfilesPanelState.tsmust stay on the app-shell assistant-availability fact instead of re-reading raw AI settings just to decide whether assistant affordances should render. -
Keep Patrol shell composition and product-first provider vocabulary on the shared feature-presentation boundary.
frontend-modern/src/features/patrol/PatrolIntelligenceWorkspace.tsx,frontend-modern/src/features/patrol/PatrolIntelligenceHeader.tsx,frontend-modern/src/features/patrol/PatrolIntelligenceBanners.tsx,frontend-modern/src/features/patrol/usePatrolIntelligenceState.ts,frontend-modern/src/features/patrol/patrolInvestigationContextModel.ts,frontend-modern/src/components/patrol/RunHistoryEntry.tsx, andfrontend-modern/src/utils/patrolRuntimeActions.tsmust keep Patrol assessment, verification, and findings primary; surface recent changes, learned correlations, and policy coverage only as backend, Assistant, selected-finding, or selected-run context when investigation makes that evidence relevant; and use Patrol/provider wording for the shared provider settings, provider model, and provider circuit-breaker affordances instead of generic AI labels inside Patrol-owned shells. The shared app shell infrontend-modern/src/App.tsxandfrontend-modern/src/AppLayout.tsxmust likewise expose/patrolas the canonical route and navigation target, while retired/aibrowser entry points stay unregistered rather than a second Patrol-branded primary route.PatrolIntelligenceHeader.tsxmust also keep the page heading's accessible name singular: when thePulsePatrolLogoappears beside visible Patrol heading text, it is decorative rather than a second label source. The Patrol workspace must not expose a generic Details/supporting-context panel for nearby activity, learned correlations, or policy buckets; those payloads stay backend and Assistant context rather than a default page section. Patrol initial data refresh failures must stay inside the Patrol feature shell as one compact stale-data retry banner; they must not replace the route with Suspense, blank loading, raw transport errors, or page-local diagnostic panels. The Patrol investigation-context owner normalizes same-state recent-change records into changed-substate wording before Assistant handoff renders them. The same shared feature-shell boundary owns the commercial-facing Patrol capability language: autonomy segmented controls and run-history/result labels must present the operator-facing policy levels asWatch only,Ask first,Safe auto-fix, andAutopilot, while legacy API names remain hidden from operators and compact controls do not collapse into unexplained shorthand. Patrol run-history rows must lead with what Patrol did or could not do before exposing trigger, token, tool-call, or raw trace details. Plan-locked Patrol controls must keep the free watch-only surface clean and must not render a Pro-absence explainer, a disabled paid-level matrix, compact Pro badges, or any paid-mode disclosure. The free Patrol working surface stays clear of paid-feature surfacing entirely; Pro discovery belongs in Settings, website/docs, and contextual at-need prompts, not beside the daily-use selector. The one allowed at-need prompt is a single finding-level Pulse Pro capability line in the expanded finding primary-action area for plan-locked installs on active critical or warning findings, with its upgrade action gated by the upgrade-prompt policy. Visible product copy calls the selectorPatrol mode; compatibility route and wire identifiers may keep stable names such aspatrol_controlandpatrolControl*. The always-visible Patrol mode selector must stay on the selected mode and one plain summary, without a separateLimitsdisclosure or hard-limit matrix beside the picker. Shared feature shells must not invent their own Patrol safety thresholds, policy labels, or disabled-control explanations. The Patrol page header must consume the same effective control state, using watch-and-report copy for locked orWatch onlymode and full governed-operations copy only for modes where that capability is actually available. Paid-control availability and commercial-plan copy must describe the same decision as choosing what Patrol may handle automatically; they must not ask users to decide how far Patrol can go or how much control Patrol has. The Open work workspace copy belongs to that same product-facing boundary: empty and descriptive text must explain what Patrol-found problems will appear there, what the selected control level allows, and the next useful operator action; it must not fall back to activation-loop, proof, queue, or verification-accounting language. Active Patrol issue rows may use shared definition-list and muted text primitives to show problem, affected resource, checked evidence, next step, and verification state inside the row, but that scaffold must not become a nested card, status strip, trust strip, or page-level proof block. A calm Patrol queue must not use shared compact-list or badge primitives to create protection-current, verification-waiting, schedule-freshness, drift, trust, or proof strips; empty work belongs to the plain empty-state and deliberate History affordance. When canonical Patrol evidence is stale, that same empty-state primitive may become warning-toned and direct the operator to run Patrol. The stale label may appear in the compact work-group row only alongside real current work; a calm queue must not repeat the same stale condition in both places. Monitor-context Patrol coverage posture must not use the shared compact list and badge primitives as a generic Proxmox overview or monitor-first launch-page proof strip. A future scoped monitor affordance may use these primitives only when it is attached to an operator action or selected Patrol context, uses distinct monitor labels, and does not become a nested card, generic dashboard strip, trust summary, or duplicate Patrol empty-work list. The Patrol schedule and model drawer is part of that shared feature-presentation boundary: it must stay viewport-bounded, expose an accessible dialog label, keep the four-level control policy on the default Patrol header, and keep provider model, schedule, trigger tuning, and readiness validation inside the secondary disclosure. Backend save rejection reasons must pass through as inline dialog state instead of being replaced with generic toast copy, and that advanced disclosure must open when the inline state exists. When the failure includes Patrol readiness context, the inline state must expose the provider, model, and readiness summary next to a direct provider-settings action instead of hiding that diagnosis behind Assistant alone. The provider-model selector in that popover must stay bound to the shared runtime settings/model catalog even when the popover mounts after async catalog loading, but the full catalog must stay behind an explicit change action so the default advanced drawer leads with the current effective model summary rather than a raw provider route list. A saved direct-provider Patrol model still renders as that model instead of visually falling back to the default selection. Successful provider-model saves that return a not-ready Patrol readiness snapshot must use that same inline surface withneeds attentionwording, while Assistant receives a saved configuration issue rather than a failed-save handoff. When governed fixes are locked, the same Patrol state owner must clear stale full-mode unlock state before persisting the monitor-only autonomy payload, so disabled paid controls cannot leak stale permission into a save. If that inline state opens Assistant, the Patrol feature must hand off a source-named, model-only briefing and close the popover so the shared Assistant drawer is not visually hidden behind feature chrome. When a Patrol assessment handoff is attached, the shared Assistant drawer empty state must stay aligned with that source-named briefing and must not render generic cluster/system starter prompts below the Patrol-owned context. The Patrol feature shell must also consume the Patrol-owned findings source for its findings tab, run-scoped findings panels, and tab badges so shared feature composition does not rebuild Patrol state by filtering the cross-product unified findings feed. -
Keep Pulse Intelligence settings product-first and page-scoped.
frontend-modern/src/components/Settings/AISettings.tsx,frontend-modern/src/components/Settings/settingsHeaderMeta.ts,frontend-modern/src/components/Settings/settingsNavCatalog.ts,frontend-modern/src/components/Settings/settingsNavigationModel.ts,frontend-modern/src/components/Settings/settingsPanelRegistry.ts,frontend-modern/src/components/Settings/settingsPanelRegistryContext.tsx,frontend-modern/src/components/Settings/useAISettingsState.ts, andfrontend-modern/src/utils/aiSettingsPresentation.tsmust present that surface to operators under thePulse Intelligencesettings group as separate focused pages rather than as a genericAI Servicesshell or one oversized mixed form.Provider & Modelsowns API keys, default model selection, provider health/preflight, provider runtime budget/timeout, and usage/cost visibility.Patrolowns schedule, alert/anomaly triggers, runtime readiness, Patrol model override, and a simpleOpen Patrolhandoff to the/patroloperator page; the actual watch/investigate/act/verify/record operator loop stays on/patrol.Assistantowns chat/tool permission, command-access, model override, and session maintenance. Service context may exist under Pulse Intelligence only for model-backed or continuous service discovery that supplies Assistant and Patrol context; normal infrastructure discovery and onboarding remain under Infrastructure, and the Pulse Intelligence navigation item, route header, model override, reset/save affordances, and setup copy must use theService Contextlabel so operators do not confuse it with infrastructure discovery. TheProvider & Modelspage must not carry a discovery summary, Patrol-control banner, or Patrol CTA; the Patrol-control handoff belongs on thePatrolsettings page and/patrol, where copy describes Patrol autonomy in plain operator terms rather than exposing an internaloperations policyconcept. Settings-save feedback must preserve provider-specific preflight failures and successful save responses that carry Patrol readiness warnings, including the provider, selected Patrol model, failure cause, safe recommendation, and readiness summary when those fields are present. The settings shell may compose that safe backend diagnostic for display, but it must not infer provider remediation by parsing raw upstream error strings in the browser. Provider model identifiers, custom provider URLs, and API-key controls must compose the shared semantic form surface in both themes and must not be classified as website login credentials. The Provider & Models form and manual model identifiers opt out of credential autofill, provider secrets use thenew-passwordautocomplete purpose, and the global form CSS preserves the active semantic surface and text tokens when a browser legitimately autofills another control. Dark mode must not expose the browser's pale credential fill or inject an admin username into a model identifier. Provider setup cards must describe provider families through the current backend-owned provider contract; DeepSeek setup copy is the V4 family and must not regress to old V3 or compatibility-alias wording. First-class provider cards onProvider & Modelsmust remain model-driven throughAI_PROVIDERS,AI_PROVIDER_CONFIGS, and theuseAISettingsStateprovider payload mapping: adding direct chat-compatible providers such as Z.ai, Groq, Mistral, Cerebras, Together, or Fireworks extends those shared arrays/maps and the backend registry projection instead of introducing provider-specific JSX branches, local configured-state inference, or browser-owned default endpoint facts. The OpenAI card must present its API key as optional for a custom compatible endpoint and treat a saved base URL as configured provider state without inventing model-family prefixes in the browser. Readiness presentation consumestransport_healthyandpatrol_capable: a reachable provider whose model did not emit Patrol tools is amber and explicitly remains usable for ordinary Assistant chat. Provider removal sends the completeremove_providerslifecycle mutation and rehydrates endpoint, credential, model, enabled state, and catalog from the response instead of clearing only the visible credential input. A blank Ollama keep-alive control means inherit the server default and must round-trip as blank. Local subscription-agent providers are the deliberate exception to credential inputs: their setup rows and first-run options render an explicit boolean opt-in, explain that Pulse uses an already authenticated same-machine CLI, and direct the operator to run provider readiness after saving. They must not ask for, accept, display, or imply storage of an OAuth token or API key, and must not present the opt-in itself as proof that the CLI login or selected model works. Their models remain normal provider-prefixed catalogue entries so Assistant, Patrol, service-context, and shared-default selectors do not invent alias parsing in the browser. The Ollama guided quickstart on that card (the copyableollama pullcommand block, the hardware-expectation note, and the post-test next-step hint) renders the backend registry'ssuggested_modelprojection from the settings payload; the browser must not hardcode blessed model IDs or their equivalent tags, and the hint must compare the tested model against the server-authored suggestion set rather than a frontend literal. Provider connection controls are page-scoped: the global Pulse Intelligence enable toggle, provider readiness strip, and Test Connection action belong toProvider & Models; Patrol, Assistant, and Service Context subpages keep their focused settings plus reset/save actions without repeating provider health chrome. Those reset/save actions and save notifications must be page-scoped as well: savingPatrol,Assistant, orService Contextsettings must not reportProvider & Models settings savedor render a genericSave changesaffordance that hides which Pulse Intelligence page owns the change. Runtime controls insidefrontend-modern/src/components/Settings/AIRuntimeControlsSection.tsxmust stay split by page-specific exports: provider runtime controls onProvider & Models, Assistant chat actions onAssistant, and service context controls onService Context. Assistant chat-action copy must name Patrol control as configured on the Patrol page, not as an Assistant command mode, because/patrolremains the operator surface for choosing how much autonomy Patrol has. Service context copy must describe the model-backed loop that supplies concrete service facts to Pulse Assistant and Patrol, not as generic discovery or AI context.frontend-modern/src/components/Settings/useAISettingsState.tsmust save service context scan enablement and interval as one explicit settings pair so selecting "Every 6 hours" or "Manual only" round-trips through/api/settings/aiwithout depending on stale read-side diffing. The same Service Context settings section must expose a manual context-scan action wired through/api/discovery/runwhen service context scanning is enabled in manual-only mode, while resource-drawer discovery remains the forced single-resource refresh path. The collapsed section and run-action copy must make automatic scheduling visible by distinguishingAuto <interval>,Manual only, andOff, and the run action must describe whether it is running the scheduled scan or a one-off manual-only sweep rather than implying recurring scans were enabled. Assistant-only controls such as execution permissions and session maintenance must stay explicitly labeled as Pulse Assistant controls, while Patrol schedule and trigger readiness live on the Patrol settings page and Patrol autonomy/control level lives on the/patroloperator page rather than drifting back into the provider shell. Session maintenance is limited to Pulse-owned conversation operations such as summarization; OpenCode-style file diff, revert, or unrevert actions must not appear in Settings unless Pulse owns a real governed infrastructure action-history/reversal contract for the affected resources. Shared/default model choice belongs onProvider & Models, while Assistant, Patrol, and service context model overrides belong on their respective settings pages instead of a generic advanced AI bucket. Each per-surface override must fall back to the shared default when left empty rather than silently using a hard-coded backend default, soProvider & Modelsstays the single place an operator picks the default model for all three surfaces. Assistant model copy must describe chat, explanation, and review support; it must not present Assistant as the approved-fix executor because Patrol is the hands-on operator for checks, governed fixes, and verification. The shared shell must not show Pro-only autonomous execution as a default free-user control when upgrade prompts are suppressed; it may surface that option only when the entitlement is present, commercial prompts are explicitly allowed, or the current saved setting already uses autonomous mode and needs to remain visible for operator review. Provider model catalogs must remain curated on that same shell:frontend-modern/src/components/shared/AIModelPicker.tsxowns the searchable, notable-first model picker pattern, andAIModelSelectionSection.tsxmust feed it configured-provider models plus the current manual selection instead of rendering raw provider catalogs as plain select options. The picker must also constrain its dropdown and internal result list to the available viewport height so settings model catalogs remain usable on mobile and tablet layouts with bottom navigation, and it must flip above its trigger when prompt/composer chrome leaves insufficient room below. Caller-owned alignment may choose left or right anchoring, but the shared picker still owns the fixed-position dropdown, viewport cap, search shell, result list sizing, and keyboard navigation model. Chat-owned selectors must reuse this shared picker instead of carrying a parallel dropdown implementation. Recent/priority model sections, external open-and-focus requests, selected older model visibility, route labels, explicitprovider:modelcustom-route validation, and catalog-disclosure rows belong to the shared picker so Assistant, settings, and future model-selection surfaces do not drift apart. Unknown custom or recent routes may remain visible only when they have a valid non-empty provider and model segment; malformed route strings such as empty provider/model values, URL-shaped text, whitespace, or path-only payloads must be dropped instead of becoming selectable model routes. The shared picker must also mark the selected catalog, recent, override, custom, or inherited-default route as the current row with visibleCurrentmetadata andaria-selected; selected model state must not be communicated by background color alone. The model picker dropdown is a named search/listbox surface: opening it must focus search, the trigger must expose its owned listbox while expanded, and keyboard movement from search through the option rows must support current-row focus, filtered-result focus, catalog-disclosure focus, up/down, page, home/end, Enter/Space activation, and Escape return to the trigger so model choice and catalog expansion do not depend on mouse interaction. Picker-owned navigation keys, including Escape, must be consumed by the picker so parent shells do not also treat the same keypress as drawer or page-level Escape. Gateway-routed model choices must not look like direct-provider choices: the shared picker, System AI settings status strip, and per-surface inherited-default descriptions must render OpenRouter-hosted provider models with an explicitvia OpenRouterroute label while leaving direct DeepSeek/OpenAI/Anthropic/Gemini/Ollama selections unqualified. When a selected route also carries a shared-default or override badge, the shared picker owns that badge as separate metadata in both visible text and the button accessible name; labels must render asmodel via OpenRouter · defaultinstead of fusing provider and badge text such asOpenRouterdefault. Platform-first top-level pages registered throughfrontend-modern/src/App.tsxmust stay chrome-only and route through the canonical app shell: each per-platform surface owns navigation and sub-tab chrome, then embeds the canonicalWorkloadsSurface,StorageSurface,RecoverySurface, orUnifiedResourceTableinembedded tableOnlymode 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 underfrontend-modern/src/features/platformPage/so the chrome stays reusable across families. Source-specific platform product surfaces underfrontend-modern/src/features/, such as the Proxmox Backups tab, may own domain IA and row models in their product subsystem while consuming shared primitives for table shells, alignment, filter buttons, charts, and empty states. Frontend-primitives owns those reusable controls and guardrails, not the storage/recovery semantics that decide which PBS, PVE archive, snapshot, task, or workload-coverage rows are shown. Storage/recovery-owned Proxmox backup subcomponents may live underfrontend-modern/src/features/when they are listed in the subsystem registry and continue to compose the shared filter, table, chart, and empty-state primitives rather than local shell variants.frontend-modern/src/AppLayout.tsxmay extend thePrimaryTablist with new platform or runtime-family entries, but primary navigation is a support-and-evidence-gated surface: rendered tabs, command/search destinations, keyboard shortcuts, and authenticated landing fallbacks must derive from the governed support manifest plus current runtime resource evidence. Supported platform/runtime families appear when evidence proves they are present; admitted-only, presentation-only, unsupported, or absent families stay hidden rather than rendering as disabled placeholders. TheMOBILE_NAV_PLATFORM_PRIORITYordering infrontend-modern/src/components/shared/mobileNavBarModel.tsmirrors that platform-first set only, so mobile and desktop navigation stay aligned without reintroducing aggregate Workloads / Storage / Recovery workspace tabs or the legacy Infrastructure entry. Frontend primitives owns the sole user-facingMachinesIA contract for the support-manifestagentplatform and agentless availability endpoints. The compatibility route, internal navigation id, and builders remainstandalone/buildStandalonePath(); adjacent subsystem contracts may reference this owner for dependencies but must not restate the route, navigation, or landing semantics. Its primary tab, mobile priority, command-palette destination, and keyboard shortcut must all route throughbuildStandalonePath()and thePrimaryInfrastructureNavIdstandaloneevidence gate; they must not create a generic Hosts, Nodes, Other, or mixed-systems bucket, and they must not include provider-owned platform nodes that are not canonical machine-page resources. The Machines page is a platform/runtime page, not a legacy Infrastructure page: it must use the shared platform tab, toolbar, table-card, and kind-aligned column primitives, and it must not reintroduce the old top-of-page InfrastructureSummary chart strip. The Machines surface must also remain secondary in the shell hierarchy when provider/runtime platform evidence exists:PRIMARY_INFRASTRUCTURE_NAV_IDS, desktop primary tabs, mobile primary priority, app-shell preload order, authenticated landing fallback, and command-palette ordering must prefer Proxmox, Docker, Kubernetes, TrueNAS, and vSphere ahead of Machines. The Machines surface may win those first/default positions only when the current estate has standalone Pulse Agent machines or agentless availability endpoints and no provider/runtime platform evidence. Provider/runtime pages must make narrower inventory scope explicit rather than presenting it as the complete machine estate: the Docker overview labels its runtime-specific tableDocker hosts, and when those rows are backed by canonical Pulse Agent machines it exposes aView all machinesaction throughbuildStandalonePath(). Patrol workflow components underfrontend-modern/src/features/patrol/may compose sharedButtonandButtonLinkchrome for issue actions, but the workflow state, route anchors, single-finding direct-action selection, Assistant handoff, autonomy label, and multi-finding fallback semantics stay withpatrol-intelligence; the canonical Patrol control anchor belongs on the visible selector, not the workspace shell. Setup-only readiness may hide run, schedule, model, trigger, and provider-repair controls, but it must not hide that selector or replace it with a setup/status explainer. Shared primitives must not grow Patrol-specific activation, autonomy, provider-settings, or Assistant-routing behavior. The default loop is a Patrol-owned watch / investigate / act under policy / verify / record loop. Active current-issue expansion is a task surface, not a history transcript: raw finding lifecycle rows may render in explicit all/resolved/history or selected-run review states, but not in the default active Patrol issue expansion. Compact Patrol status chrome may render work/health evidence passed by Patrol, but trigger/scheduling status remains header/control context and must not be repeated by shared default status primitives. Shared primitives must preserve that plain visible label instead of exposing internal assessment terminology on the default page. External-agent readiness from Pulse MCP may remain compact optional context derived from the shared manifest-client contract verdict and backend operations-loopexternalAgentReadysignal, but shared primitives must not create page-local MCP setup constants, token-scope checks, tool filters, readiness shortcuts, MCP readiness props, or a visible external-agent stage as the primary first-party loop. The journey's loaded progress state must come from the canonical operations-loop status projection exposed by the shared agent-capabilities frontend client, while shared primitives remain passive renderers of the state Patrol passes them. Patrol control starter, completed-loop, or resolved-loop evidence may change compact journey copy only after Patrol has derived it from that projection; shared primitives must not infer Patrol control or legacy Pro activation state from route anchors, billing state, generic Patrol recency, or MCP readiness alone. Shared presentation helpers may render the operations-loop state they receive, but they must not infer operations-loop progress from a generic Patrol run, recency timestamp, or MCP readiness alone; Patrol owns the issue-backed evidence model that decides when the loop can advance through Assistant, approval, rejected no-execution terminal decisions, approved-action verification, and external-agent parity. -
Keep user column sorting on platform tables on the shared sort fabric. Tables composed from
frontend-modern/src/features/platformPage/sharedPlatformPage.tsxthat offer user-facing column sorting must own it throughcreatePlatformTableSortStateplusPlatformSortableTableHeadrather than page-local sort signals, ad hoc header buttons, or v5-style sort-drives-grouping designs. The shared fabric owns the interaction contract: click cycles a column's natural first direction, then the flipped direction, then back to the table's built-in order; sort state persists per table throughusePersistentSignalstorage keys; rows with missing values sink to the bottom regardless of direction; headers exposearia-sortand the arrow indicator consistent with the workloads table header through the active-onlytableSortPresentation.tshelper; and header alignment stays on the canonicalgetPlatformTableHeadClassForKindhelpers fromcolumnAlignment.ts. A table's default order remains its page-model status-first compare until the user selects a column, and grouped modes (for example Docker's grouped-by-host containers view) sort within groups while grouping itself stays orthogonal to sort state. -
Keep estate orientation inside the canonical controls operators already scan. Large-estate workload totals must flow from the unfiltered shared workload inventory into
WorkloadsFilter.tsx, through the canonicalFilterBar/FilterButtonGroupoption-count contract, so each number sits directly beside the type or status label it describes without another fetch or a competing summary panel. Provider topology belongs in the existing table header; Proxmox derives cluster and standalone-node context throughplatformEstateOverviewModel.tsand supplies it toProxmoxNodesTable.tsxrather than creating a page-level summary. The existingplatformEstateOverviewVisiblepreference now governs these inline totals from the shared View menu, preserving the global browser-persisted choice across platform workload surfaces. A page with adjacent totals, such as Proxmox's Nodes header, must own one visibility signal and pass it to both the shared filter and table instead of creating independently persisted signals that only synchronize after a reload. A provider host or node table must render before the workload filter, and the workload filter must sit immediately before theWorkloadsSurfaceit controls. It must never precede a provider table whose rows it does not filter; Proxmox and VMware vSphere use the same host-or-node, workload-controls, workload-table reading order. When a platform names the embedded workload collection, that title and its inventory count must enterWorkloadsSurfacethrough thetableTitleslot.WorkloadsTablerenders the slot inside the sharedTableCardHeader, including the filtered-empty table state; platform pages must not leave the same title floating above the filter card or recreate table-header chrome outside the canonical Workloads frame. Large provider inventories use the sharedcreatePlatformTablePreviewandPlatformTablePreviewFooterboundary to keep the controlled workload table in the initial reading flow: Proxmox shows eight node rows by default on larger layouts and four on phone-sized layouts. The accessible, reversible show-all control belongs below the bounded rows, where a shared fade, remaining-row hint, and directional chevron communicate that the list continues; it must not compete with topology context in the Nodes header. Expansion is deliberate session state and must not persist a page-burying expanded default. Docker / Podman, Kubernetes, TrueNAS, VMware vSphere, and Standalone Machines must continue using their sharedPlatformTableToolbarcounters and table headers; none may add a parallel estate card grid or provider-only spotlight surface.
Forbidden Paths
- Reinventing table/filter/toggle primitives when a shared version exists
- Feature-local styling forks of canonical shared components without explicit justification
- Direct imports that bypass shared presentation helpers where guardrails exist
- Top-level settings panels introducing bespoke page-level headers or outer
framing instead of the canonical settings shell and
SettingsPanelcontract - User-facing diagnostics or settings panels rendering maintainer/admin analytics such as commercial funnel, sales funnel, pricing/checkout conversion, or infrastructure onboarding telemetry. Those signals belong in admin-owned metrics surfaces, not the product diagnostics UI or customer frontend event emission.
- User-facing diagnostics panels rendering the native Pulse Assistant runtime
as an MCP connection. Settings diagnostics must consume
assistantRuntimeConnectedand label it as Assistant runtime availability;mcpConnected,mcpToolCount, and "MCP Connection" are forbidden on the first-party diagnostics surface. - Settings route models normalizing retired aliases such as
/settings/operations/*,/settings/integrations/api,/settings/system-pro,/settings/workloads/*, or nested/settings/infrastructure/*paths back into current settings panels. Retired aliases must fail route eligibility instead of being kept as compatibility redirects. - Platform pages implementing local estate metric cards, operational spotlight panels, or separate visibility preferences instead of projecting totals into the canonical filter and table-header controls.
Completion Obligations
Coverage-table polling must preserve the DOM identity of an unchanged logical
row, including its focused expansion control, rather than keying rendering by
replacement snapshot object identity. The isolated Chromium polling check in
scripts/check-backup-browser-polling.mjs exercises this boundary with the
production table, router and styles; it does not qualify full-app scrolling.
- Update guardrail tests when new shared primitives are added, including
new Settings controls that drive backend verification surfaces (for
example the Verify Patrol button in
AIModelSelectionSection.tsx, which must drive the typedrunPatrolPreflightclient throughuseAISettingsState.tsrather than inlining fetch calls in the section component, must hydrate its result panel from thepatrol_preflightsnapshot on/api/settings/aiso the "last verified" state survives page reloads without forcing a re-click, must pass the form's pendingpatrolModelas the model override so the click tests the operator's unsaved dropdown selection rather than whatever was previously saved, and must surface a stale-cache warning when the form's selection differs from the cached result's model so the green badge cannot silently mislead). The readiness banner's tone and headline are pure functions exported fromAIModelSelectionSection.tsxso this presentation is provable without mounting the settings shell, and a result that was never assessed —status: not_assessed, or theinterruptedcause left by an operator cancel or a severed request — must render in a neutral "check did not complete" treatment. It must not use the failure treatment or the "model not verified" headline, because a run that measured nothing is not a verdict on the model, and it must not claim verification from amax_verified_moderecorded before the interruption (#1640) - Keep top-level settings surfaces routed through the canonical settings shell
and maintain both
frontend-modern/src/components/Settings/__tests__/settingsArchitecture.test.tsplustests/integration/tests/15-settings-shell-consistency.spec.tsThe shared Settings content column must remain explicitly width-constrained and at rest across thelgbreakpoint. Do not attach a transform animation unconditionally to the responsive panel container: entering a narrow viewport must not reactivate motion that translates or clips the active panel. Responsive proof must cover a desktop-to-390-pixel resize and the direct Plans & Billing route as well as ordinary Settings navigation. - Keep Settings loading placeholders on the shared
SettingsLoadingSkeletonprimitive and thesettings-loading-skeleton-shellregistry rule instead of localanimate-pulseblock templates. Pure Settings loading indicators that are spinners rather than skeleton placeholders must stay onLoadingSpinnerand theloading-spinner-shellregistry rule instead of localborder-t-transparentorborder-b-2animate-spin shells. - Update this contract when a new canonical UI pattern is adopted
- Remove local forks after the shared primitive is introduced
- Keep shared feature-level presenters on capability truth. When reusable
presenters under
frontend-modern/src/features/explain why a control, chart, or detail surface is unavailable, they must describe the owned identity or capability gap instead of prescribing a provider-local install path that conflicts with API-backed platforms like TrueNAS. - When a settings route header and a top-level settings shell describe the same
commercial surface, keep them on the same shared presentation owner instead
of allowing route metadata in
settingsHeaderMeta.tsor labels insettingsNavCatalog.tsto drift into independent title or description copy, and keep adjacent settings-shell referrals such asInfrastructureWorkspace.tsxon that same shared owner instead of reintroducing local “go to Pulse Pro” variants. That same shared owner must keep self-hosted commercial settings coherent when deliberately reached: the direct route, page shell, and any navigation shown for paid or recovery context use the sharedPlans & Billinglabel, and the owned plan shell must foreground the active plan name plus available capabilities before secondary billing or recovery detail so paid upgrades can confirm their entitlement immediately after activation without making default Community look like it is missing an activation key. Routine plan and capability-status copy must stay product-facing: describe what is available on the instance, point failed capability checks to refresh the plan or open recovery, and avoid raw entitlement-payload phrasing or activation language as the normal setup story. - When settings surfaces need informational, warning, success, or danger
callouts, compose
frontend-modern/src/components/shared/CalloutCard.tsxand register the consumer infrontend-modern/scripts/shared-template-registry.jsoninstead of adding feature-local colored panel shells. Compact settings notices must use the sharedscale="compact"density and keep proof in bothfrontend-modern/src/components/shared/SharedPrimitives.guardrails.test.tsandfrontend-modern/src/components/Settings/__tests__/settingsArchitecture.test.ts. Connection-editor status, feature-disabled, delete-error, and probe-result notices are part of the same settings callout boundary: the editor and credential slots own the source-specific lifecycle or API meaning, whileCalloutCardowns the warning/success/danger shell and compact density. Update confirmation and progress modal notices share that same primitive boundary. The update flow owns version, prerequisite, root-access, restart, and error copy;CalloutCardowns the info/warning/danger shell, spacing, dark-mode tone, and icon layout. - Keep hosted settings-shell framing imports safe for bundle initialization.
Self-hosted billing titles, descriptions, and referral copy used by
settingsHeaderMeta.ts,settingsNavCatalog.ts, and adjacent settings shells must flow throughfrontend-modern/src/components/Settings/selfHostedBillingPresentation.tsinstead of importing generic commercial presentation helpers directly into hosted settings route shells. Contextual settings feature gates must use capability-owned presentation helpers and neutral paid-plan copy. They must not reintroducePro featurebadge titles, Pro-suffixed option labels, monitored-system limit claims, or browser-local commercial/onboarding metrics wrappers in SSO, audit, reporting, AI controls, agent profiles, or shared warning banners. - Keep shared settings-shell AI control copy capability-scoped rather than
upsell-scoped.
AIRuntimeControlsSection.tsxmay describe read-only, approval-required, and autonomous action posture, but option labels and helper text must avoid tier labels or broad "executes everything" wording; paid capability availability belongs to entitlement-backed visibility and lock state, not local select copy. Provider & Models settings copy must keep Patrol autonomy distinct from Assistant chat actions: Patrol's hands-on control level belongs on the Patrol page, while the shared settings shell may only describe whether Assistant chat can run eligible chat actions. - Keep first-session dashboard empty-state copy on
frontend-modern/src/utils/workloadEmptyStatePresentation.ts, and make infrastructure setup guidance name the canonical destination explicitly instead of falling back to generic settings CTA labels. - Keep the live first-session wizard on the canonical three-step runtime
shape in
frontend-modern/src/components/SetupWizard/SetupWizard.tsx(Welcome,Security, thenInstall), and keep the step indicator plus completion CTA language aligned with the governed infrastructure install workspace instead of regressing to a route jump that leaves the next action implicit. Preview-only follow-up surfaces such asfrontend-modern/src/components/SetupWizard/SetupCompletionPreview.tsxmust stay deterministic and scenario-driven: they may not poll the live/api/stateruntime or inherit whatever connected systems happen to exist on the current backend, and browser proof for/preview/setup-completemust select explicit preview scenarios instead of ambient runtime state. That determinism boundary also covers the setup completion Pro activation pointer: every preview scenario infrontend-modern/src/components/SetupWizard/setupCompletionPreviewScenarios.tsmust carry an explicitproActivationboolean so preview rendering never falls through to the live license probe, and thepro-unlicensedscenario is the canonical browser proof for the pointer. The pointer's localized strings (setup.completion.proActivation.*) are part of the first-session monitoring journey catalog and must stay inFIRST_SESSION_MONITORING_MIGRATED_MESSAGE_KEYSwith non-identical DE/ES translations. - Keep AI settings setup UI backend-driven:
frontend-modern/src/components/Settings/useAISettingsState.tsandfrontend-modern/src/components/Settings/AISettingsDialogs.tsxmay collect provider credentials or runtime URLs, but they must not bake vendor model 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 andautonomousMode:false, but they must not infer a user task from an ordinary context-only open. Explicit labelled explanation actions useaiChatStore.explainand the shared explanation dispatcher, which captures the request context, waits for open initialization, preserves drafts, and acknowledges each request once. Older request completion must not clear newer handoff context. Neither path may mutate persistent AI control-level settings or trigger background Assistant settings/model bootstrap before the drawer is open. Patrol finding handoffs that add structured investigation-record framing must derive that context throughfrontend-modern/src/features/patrol/patrolInvestigationContextModel.tsso shared drawer primitives stay shell-owned rather than becoming a Patrol-specific diagnosis formatter; shared drawer primitives must not branch on intent themselves. Patrol-page surfaces must not add standalone trust strips to shared header or workspace chrome; high-signal trust facts may feed the Patrol-owned assessment readout, but shared drawer/chrome primitives stay free of theFindingsTrustSummaryshape 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 boundary:frontend-modern/src/features/patrol/usePatrolIntelligenceState.tsmust 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. That feature-owned presentation helper is the single emitter for investigation-recordimpactandrollbackfields: when an investigation record exists but those fields are empty, the helper emits explicitImpact not assessedandRollback not specifiedlines into the model-only Patrol finding prompt context so the operator-visible gap is surfaced to Assistant rather than hidden, and shared chat primitives stay free of that placeholder logic. Patrol assessment-level handoffs must use that same feature helper to attach bounded model-only assessment, verification, latest-run, supporting-context evidence, active-finding, and resource reference context while forcing request-local approval-required mode. Patrol run-history handoffs must also use that feature helper rather than a row-local Assistant prompt, so the shared drawer receives only a generic visible briefing plus bounded model-only run context, scoped resource references, runtime failure summary/detail, andautonomousMode:falsewhile the Patrol feature remains the source of run copy and retry/configuration guidance. Active-finding entries in that assessment handoff may add live pending approval posture only as safe structured metadata: approval ID, pending status, risk, target, requested/expiry timestamps, action plan identity, requester identity, approval policy, plan expiry, dry-run posture, and command count. Those entries may be passed through shared chat transport ashandoff_actionsfor model-only refresh, but the shared drawer stays a generic shell rather than a Patrol summary prompt builder. The Patrol helper may turn those same safe references into visible action labels and safety notes for assessment and finding-level handoffs, but it must not produce Patrol-authored suggested prompt chips, recommendation titles, recommendation reasons, or route-owned next-step actions. Assessment-level Patrol prompts, action labels, and safety notes must describe active findings, pending approvals, governed action references, and coverage caveats as evidence for the configured model, not as a frontend-authored decision tree. Finding-level drawer opens may also pass one bounded model-only finding context, one target resource reference, and onehandoff_actionsreference for a live approval or proposed fix. It must not expose raw command or execution payloads. The drawer may render a generic context-briefing band fromfrontend-modern/src/stores/aiChat.ts, but feature-owned helpers must provide compact source labels, primary subject, status, and governed approval/action artifact metadata while keeping detailed evidence, safety notes, and model-only finding context outside drawer chrome. Prompt suggestions, attention-reason copy, and operator-decision framing must stay out of Patrol drawer chrome. Patrol finding and action-artifact handoffs must not render suggested prompt 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 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; raw command text remains outside shared Assistant primitives. When those feature-owned helpers attach backend model-only context, the drawer store may carry only bounded handoff text and structured resource references for the shared chat transport; approval, lifecycle, and command authority remain with the owning runtime surfaces. Patrol finding handoffs should still provide that briefing from current finding facts when a durable Patrol investigation record is not attached yet, rather than opening the shared drawer as empty generic chat. When the feature helper adds live approval state to the generic drawer briefing, it may pass only safe approval metadata intoAIChatContextBriefing, including generated approval summaries and command counts when available; raw approval commands remain owned by the governed approval/remediation panels. If the generic finding-level helper hydrates latest investigation detail to recover action artifact context, it may pass only safe summary fields and command counts into the drawer briefing. Shared approval-required posture must derive its subject from that briefing or structured finding context, so Patrol handoffs render as Patrol handoffs or Patrol findings, and alert handoffs render as alert investigations, rather than generic dashboard briefs. Patrol approval-row Assistant prompts must route through the same feature-owned finding handoff helper rather than hand-written prompt-only drawer opens: safe approval metadata, action artifact summaries, resource references, and boundedhandoff_actionsmay enter the prompt and context, but raw command text stays out and the scoped request must passautonomousMode:falseinstead 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 passautonomousMode:falseas a request-local override so the drawer shows approval-required posture without mutating the persistent Assistant control setting. - 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 results do not contain that option, so provider- or endpoint-scoped handoffs do not flash back to generic host-only language.
- Keep the first welcome screen in
frontend-modern/src/components/SetupWizard/steps/WelcomeStep.tsxexplicit about operator context. The shell must explain that the bootstrap token only unlocks first-run setup, state where the command should run, and adapt command/help text to detected Docker or containerized deployments instead of assuming the operator already knows which host or container owns the Pulse install. Bootstrap validation must remain an explicit operator action rather than auto-submitting on a token-length heuristic, and it must be single-flight so one successful validation advances the wizard exactly once even when click and keyboard submission overlap. - Keep the settings-shell infrastructure landing path aligned with that same
first-session story.
frontend-modern/src/components/Settings/settingsNavigationModel.tsmust treat/settingsand the infrastructure settings tab as the canonical path to the bare/settings/infrastructure, which renders the unified Connections table, not to a separate install subview or to reporting/ control. The first-session story is owned by that table's own empty state and theAdd infrastructureentry point on it, not by a second landing route, so first-time operators and returning operators see one consistent infrastructure surface by default. - Keep Infrastructure and Workloads onboarding copy on the shared
presentation owner in
frontend-modern/src/utils/workloadEmptyStatePresentation.ts. Both the infrastructure empty state and the Workloads no-resources state must route first-time operators into the canonical/settings/infrastructure?add=picksource picker, let operators choose by recognizable system/service names instead of collection-method taxonomy, and avoid falling back to either passive “nothing here yet” wording or the retired install-first /Platform connectionssplit. Workloads routes that already have canonical unified-resource infrastructure sources but no workload inventory must use a distinct no-inventory presentation that points operators at credentials, permissions, and collection status in the canonical infrastructure workspace instead of reusing first-run onboarding copy. That handoff is conditional on the session being able to open the workspace: the no-inventory presentation takes the session'sinfrastructureReadcapability as a parameter, and a session without it must render neither the/settings/infrastructureaction nor copy naming that page, directing the operator to an administrator instead. The gate reads the destination's own capability — the same onesettingsNavCatalogrequires for the Infrastructure nav item — so the offer and the nav gate cannot drift apart, and the variant stays a parameter of the shared presentation owner rather than a branch at any call site, including the surface's own inline fallback. Sessions whose capabilities have not resolved keep the action, so an administrator never flickers through the restricted copy. Inventory-source health itself stays visible to every session that can read monitoring data; only the Settings handoff is gated. - Keep cross-surface investigation handoffs on shared route ownership.
Feature shells such as Alerts and Patrol may decide which governed
destination chips to render, but canonical href, label, dedupe, and
infrastructure-fallback truth must stay in
frontend-modern/src/routing/resourceLinks.tsinstead of freezing raw route strings or provider-local link builders inside feature panels. Patrol workflow handoffs follow the same rule: start/continue Patrol control links must compose the route-backedpatrol_controlhelper fromresourceLinks.ts, single-finding direct action links must use canonical finding-presentation destinations such as the Patrol provider-settings route, whilepatrol_autonomyand legacy Pro activation URLs remain parser aliases only and verified review links use the plain Patrol history anchor. UI surfaces must not duplicate thepatrolControlStarterquery string or write Patrol control or legacy entry-point starter telemetry from local click handlers. - Keep shared summary-card emphasis coherent. When shared summary primitives enter an
inactivestate,SummaryMetricCard,InteractiveSparkline, andDensityMapmust all demote background context together so storage, infrastructure, and workloads read as one interaction model instead of mixing page-local opacity, sticky-shell, or highlight rules. - Keep density-map summaries overview-first. When a shared summary density map receives row focus or chart-hover emphasis,
frontend-modern/src/components/shared/DensityMap.tsx,frontend-modern/src/components/shared/useDensityMapState.ts, andfrontend-modern/src/components/shared/densityMapModel.tsmust preserve the multi-entity overview rows and keep focused-entity detail in the hover tooltip instead of swapping the card into a single-series chart, dimming the rest of the map into unusable background noise, duplicating cursor-value tooltip copy, or adding persistent card chrome that steals heatmap space. The card body must stay overview-first; the tooltip may carry the active entity identity, current value, and peak, shared tooltip shells must follow semantic surface tokens instead of forcing a dark palette in light mode, the tooltip header must let long entity names consume the available width before truncating rather than clipping against an arbitrary fixed label cap, numeric metric readouts such as16.9 MB/sor37.4 MB/smust stay single-line instead of wrapping the unit onto a second row, and density-map detail that cannot fit cleanly inside the canonical tooltip shell must be omitted rather than introducing tooltip-specific chrome or a secondary chart inside the hover surface. - Keep retired self-hosted hosted-model and trial acquisition surfaces out of normal v6 GA runtime. Shared shells and helper-driven badges may continue to parse legacy payload fields, but ordinary self-hosted Assistant, Patrol, and settings flows must present provider setup as BYOK/local/self-managed and must not surface hosted-model credits, in-app trial starts, or generic managed-model claims.
- Keep sparkline scrubbing source-local and sibling-sync timestamp-based. The chart a user is actively scrubbing in
frontend-modern/src/components/shared/InteractiveSparkline.tsxandfrontend-modern/src/components/shared/useInteractiveSparklineState.tsmust keep its dashed hover cursor on the real local mousex, while sibling cards may map the shared hover timestamp onto their own timelines. Shared cursor sync must not snap the source chart back onto the nearest sample timestamp, the rendered SVG/canvas hover cursor must bind to the actual numeric cursor coordinate rather than a boolean guard state, the time cursor must span the chart viewport instead of collapsing to the series height, and the hover tooltip must track the pointer instead of anchoring to the chart top edge while following the active theme rather than a hardcoded dark shell. The hover tooltip must stay side-offset from the active scrub cursor and flip to the available side near viewport edges so it does not cover the highlighted guide or graph point. - Keep shared contextual focus canonical after adoption. Once a summary or table surface enters route-backed contextual focus, future additions must extend
frontend-modern/src/components/shared/contextualFocus.tsand its guardrail tests rather than forking another helper for workload IDs, resource IDs, or scroll-preserving same-route selection. - Keep shared infrastructure/resource selectors on the canonical agent-facet
truth. Shared primitives and settings-facing selector helpers must treat
top-level TrueNAS appliances as agent-facet infrastructure via shared
helper ownership instead of reviving a direct
resource.type === 'truenas'branch inside page shells, selectors, or reporting-resource type helpers. - Keep shared feature-shell Patrol run fixtures on the canonical run-record
contract. When
frontend-modern/src/features/patrol/consumes Patrol run history, the shared normalized record must preserve provider-backed counts such astruenas_checkedinstead of letting feature-local fixtures or fallback objects collapse API-backed TrueNAS systems back into generic agent-host presentation. That same shared route-shell boundary also owns header-composition audit.frontend-modern/scripts/header-audit.mjs,.github/workflows/release-dry-run.yml, and.github/workflows/create-release.ymlmust prove the same shared top-level page-header contract before publication. The audit may follow local imports when a route shell composesPageHeaderthrough a nested surface, and settings coverage must stay limited to top-level registry panels rather than every helper*Panel.tsxfile. The canonical Settings shell therefore owns the sharedPageHeaderfor support tools, and the retired top-level/operations/*browser path must not regrow a route-local heading, tab strip, or page shell for diagnostics, reporting, or logs. Because the dashboard route is retired, that audit must also discover live top-level pages fromsrc/pages/and may not keep a hard required-header entry forfrontend-modern/src/pages/Dashboard.tsx. - Keep the authenticated app root aligned with that same first-session path.
That same shared-primitive ownership now includes contextual row focus.
frontend-modern/src/components/shared/contextualFocus.tsis the canonical owner for interactive-series filtering, focused-label lookup, active-series resolution, and nearest-scrollable-ancestor preservation across page-scoped summary surfaces. Dashboard row focus, infrastructure summary emphasis, storage summary emphasis, and workloads summary emphasis must all route through that helper instead of maintaining page-local copies of the same hover/focus rules.frontend-modern/src/App.tsxmust land authenticated/and/loginhandoffs through this subsystem's provider-first platform landing contract: the first visible provider/runtime platform wins, and the Machines surface is eligible only when the current estate has standalone Pulse Agent machines or agentless availability endpoints and no provider/runtime evidence. The retired Infrastructure aggregate route and nested settings infrastructure aliases are not compatibility commitments: first-time operator setup must enter through the canonical Settings → Infrastructure workspace and its query-backed add flow, while provider evidence still owns the operational landing surface.frontend-modern/src/components/Login.tsxis part of that same pre-authenticated and first-session shell ownership: auth-check, setup fallback, and submit-pending loading indicators must composefrontend-modern/src/components/shared/LoadingSpinner.tsxthrough the shared-template registry instead of recreating page-local spinner shells. The subsystem registry must keepLogin.tsxcovered by thefirst-session-runtime-and-previewproof policy so login loading affordances cannot drift from the shared Settings, Patrol, AI, and primitive spinner contract. The authenticated app shell's boot-time route preloads must be owned byfrontend-modern/src/routing/routePreload.tsso top-level cold-tab readiness cannot drift from the route-module preloader. The delayed boot-time set is bounded to the lightweight global Actions review destination. Alerts, platform, Patrol, and Settings modules load from current route or interaction intent instead of compiling an unseen workspace behind the active phone route. Route-module preloads and chart-cache fetches are separate shell responsibilities: the shared route preload inventory must stay module-only, while chart payload warming must route through the route or interaction that renders the chart.frontend-modern/src/useAppRuntimeState.tsmust not prewarm retired Infrastructure summary-chart caches or eager Workloads chart caches as a generic authenticated-shell side effect. - Keep relay settings shell copy on the shared presentation owner in
frontend-modern/src/utils/relayPresentation.ts. The route metadata insettingsHeaderMeta.tsand the leadingSettingsPanelinRelaySettingsPanel.tsxmust reuse the same description and availability copy instead of drifting into separate rollout or pairing wording. Relay availability copy must describe the Relay tier boundary as Relay and higher plans rather than collapsing Remote Access back into a Pro-only feature. - Keep shared settings-shell legal and docs referrals on
frontend-modern/src/utils/docsLinks.ts. Shared settings surfaces such asAIRuntimeControlsSection.tsxmust not hardcode GitHubmaindoc URLs for privacy, security, proxy-auth, scope-reference, or Terms-of-Service links. - Keep shared settings-shell telemetry transparency controls on the governed
general settings panel. Preview/reset affordances for outbound usage telemetry
must stay rendered inside
frontend-modern/src/components/Settings/GeneralSettingsPanel.tsxinstead of drifting into route-local modals, hidden dev tools, or shell chrome that operators would not naturally inspect. - Keep the short telemetry/privacy summary copy on that same shared surface
accurate to the governed privacy doc. If the trust boundary depends on a
specific retention window or on “IP addresses are not stored” rather than
“IPs are never seen,” the summary copy in
GeneralSettingsPanel.tsxmust state those facts plainly instead of reverting to a stronger but inaccurate shorthand. The one-time schema-v2 upgrade disclosure must compose the sharedInlineNoticebanner layout on the existing post-update communication boundary, use shared button and external-link primitives, and route its Preview and Disable actions to the anchored General settings telemetry control rather than duplicating privacy state in shell-local UI. - Keep maintainer commercial-event controls out of customer settings.
The shared general settings privacy panel may expose outbound usage
telemetry controls, preview, and reset affordances, but it must not render
local commercial handoff event toggles,
PULSE_DISABLE_LOCAL_UPGRADE_METRICS, or other commercial-debug controls as normal customer-facing preferences. - Keep shared storage-route feature presentation on neutral capability truth.
Reusable mappers and presenters in
frontend-modern/src/features/storageBackups/must distinguish inventory datastores from backup repositories so VMware rows on the shared storage route stay canonical to the admitted phase-1 floor instead of reviving backup-target, protected-target, or recovery-local semantics on a shared page. Those presenters must also source ZFS pool health from the canonicaldetails.zfsPoolpayload (meta-firststorage.zfsPool, flatplatformData.zfsPoolfallback) when building pool detail and bar summaries, rather than re-deriving device-level health from risk-reason strings or presenting flattened pool-state scalars as the full report. Ceph dedup is part of the same shared-presenter truth: cluster-internal pool rows must be consolidated into their mounting storage rows throughconsolidateCephClusterPoolRecordsinfrontend-modern/src/features/storageBackups/cephRecordPresentation.ts(lifting worse health onto the survivor) before shared storage tables render, instead of each table double-listing the same Ceph storage with conflicting raw-pool versus mounted-capacity accounting. Storage row models built byfrontend-modern/src/features/storageBackups/storagePoolRowPresentation.tsmust only carry fields the row actually renders; per-row source-platform badges and other identical-on-every-row decorations belong in the row expansion, not inStoragePoolRowModel. - Keep infrastructure settings-shell API alternatives on the shared shell
contract.
frontend-modern/src/components/Settings/InfrastructureWorkspace.tsx,frontend-modern/src/components/Settings/settingsHeaderMeta.ts, andfrontend-modern/src/components/Settings/settingsNavigationModel.tsmust present the unified add flow as the canonical API-backed entry for Proxmox, TrueNAS, VMware, and future provider integrations instead of reviving top-levelDirect Proxmoxwording or shell-local provider routes. Phase 9 retired thePlatform connectionsnomenclature along with the shells that owned it — there is noPlatformConnectionsWorkspaceand no per-typeProxmoxSettingsPanel/TrueNASSettingsPanel/VMwareSettingsPanelto route through; the provider is a field inside oneConnectionEditor, not a destination. - Keep the infrastructure settings connection inventory on one shared
source.
frontend-modern/src/components/Settings/InfrastructureWorkspace.tsxcomposes rows exclusively fromfrontend-modern/src/components/Settings/useConnectionsLedger.ts, which pollsGET /api/connections. Provider connection counts and availability must derive from that aggregator, not from a top-level ledger plus parallel provider-specific fetches. The retiredPlatformConnectionsWorkspace/TrueNASSettingsPanel/VMwareSettingsPanelpanels must not be reintroduced as a second fetch path. - Keep alert-history feature composition on the current owned state contract.
frontend-modern/src/features/alerts/tabs/HistoryTab.tsxmust react to the sharedalertData()history state instead of reviving deleted aliases. Unified-resource resolution reachesfrontend-modern/src/features/alerts/AlertResourceIncidentsPanel.tsxthrough the history state rather than a prop chain:useAlertHistoryStatealready receivesgetResourceand re-exposes it, so the panel resolves a display name wherever it is mounted. That replaced the previous tab-passes-the-resolver rule, which only worked while the panel was a page-level sibling of the tab; it now mounts inside the history row that opened it (see the alerts contract, #1687) and the tab no longer renders it at all. Neither route may create another page-local resource lookup or a provider-specific handoff layer. - Keep the alert-thresholds containers surface on the canonical shared owner.
alertOverridesModel.ts,useAlertOverridesState.ts, anduseAlertsConfigurationState.tsmust surface API-backedapp-containerparents such as TrueNAS as first-classContainer Runtimes, whileThresholdsTab.tsxmust bridge function-valued selectors intoThresholdsTable.tsxexplicitly instead of relying on spread-based adapter props that can collapse functions on the live Solid surface. Docker-only controls inThresholdsTableDockerTab.tsxmust remain gated to realdocker-hostresources instead of leaking onto platform-managed runtimes. Threshold host selectors follow the same single-owner rule: canonicalplatformTypewins over secondary discovery facets, so TrueNAS and vSphereagentresources remain in their platform threshold tabs even when they retain Proxmox evidence, and only Proxmox PVE-owned hosts enterVirtualization Hosts. A missing canonical owner may use the legacy Proxmox scope resolver as a compatibility fallback. - Keep shared commercial upgrade navigation typed and destination-aware.
Shared paywall shells and upgrade actions must route internal billing or
cloud destinations through
frontend-modern/src/utils/upgradeNavigation.ts,frontend-modern/src/components/shared/UpgradeLink.tsx, andfrontend-modern/src/components/shared/useUpgradeNavigation.tsinstead of guessing from labels, hardcodingtarget="_blank", or callingwindow.open(...)from each feature surface. Inline upgrade links may useUpgradeLink; button-styled upgrade CTAs must useUpgradeButtonLinkso width, tone, focus, route/new-tab behavior, and opener preservation stay on the sharedButtonLinkprimitive instead of page-local Tailwind anchors or commercial helper class strings. - Keep same-shell platform/runtime route transitions on retained shared state. Active infrastructure consumers may show full-page loading only before the first compatible resource snapshot exists; once a fresh canonical snapshot is already present in the shared app shell, top-level platform/runtime tab switches must reuse that state boundary instead of flashing a transient page takeover between tabs.
- Keep self-hosted paid-service prompts opt-in at the shared shell layer.
settingsNavCatalog.ts,settingsNavVisibility.ts, shared upgrade link primitives, trial banners, monitored-system warning banners, history-lock overlays, and Patrol lock helpers must honorpresentationPolicy.hideUpgradeby hiding paid prompts by default on ordinary self-hosted installs. Direct activation/recovery routes may render their owned content, but sidebar discovery, trial CTAs, plan upsells, monitored-system limit pressure, feature upgrade links, and plan-lock Patrol banners must require hosted mode, explicit handoff, or active entitlement. Cloud interest links from self-hosted plan surfaces must hand off to Pulse Account/public Cloud ownership rather than route to an in-product Cloud trial/signup page. - Keep the identified-service reducer on
discoveryPresentation.ts. Any surface that wants to label a workload with the AI-identified service (drawer overview card, future row chips, MCP capability payloads) must consumegetDiscoveryIdentifiedSummaryrather than re-implement the empty/low-signal gate. The helper returns null when the stored record has no useful identification — mirroring the Discovery tab'shasValidDiscovery— so the same record either renders in all surfaces or hides in all surfaces, preventing "Unknown" rows or zero-confidence noise from drifting into peripheral UI. CLI access, confidence fields, and no-URL diagnostics are support metadata; they must not by themselves promote a record into the identified-service summary when the service name, category, version, paths, ports, facts, and suggested URL are all absent or placeholders, including generic workload types such asserviceorcontainerand diagnostic facts such as metadata-only status, config-availability failures, or missing-config errors. Discovery is an opt-in observed-context layer, not an automatic row-link owner. The reducer must carry provenance, observed time, service version, endpoint candidates, and URL-source copy so drawer surfaces can show "Observed by Discovery" context and pass suggested URLs into the shared web-interface field. Persisted/manual web-interface metadata remains the only row-link source until the operator explicitly adopts a suggested URL. Discovery-sourced values rendered outside the Discovery tab must carry the shared compact provenance marker fromfrontend-modern/src/components/shared/DiscoveryProvenanceMarker.tsx, so operators can distinguish opt-in Discovery context from API-owned resource facts without reading a drawer-specific explanation. - Keep settings sidebar search able to find pages by the vocabulary users
actually type, not only by rendered copy.
SettingsNavItem.keywordsinfrontend-modern/src/components/Settings/settingsNavigationModel.tsis the canonical search-only alias channel, matched alongside labels and header descriptions infrontend-modern/src/components/Settings/useSettingsAccess.ts; keywords are never rendered and must not become a second copy surface. The Assistant nav item must keep the external-agent connector aliases (mcp,model context protocol,external agent,claude,opencode,connector,pulse-mcp) so the pulse-mcp setup hosted on that page stays reachable from search, and the Assistant header description must continue to name external agent (MCP) connectors across locales. Proof lives infrontend-modern/src/components/Settings/__tests__/settingsArchitecture.test.tsand the parameterized search cases infrontend-modern/src/components/Settings/__tests__/useSettingsAccess.test.tsx.
Attention workbench shell contract
The app shell and Patrol attention workbench share the same canonical summary
for desktop and mobile accessible navigation counts. The label remains
Patrol; its accessible description may add the active attention count.
frontend-modern/src/features/patrol/PatrolAttentionWorkbench.tsx uses native
buttons, visible focus, selected-state semantics, focus restoration, narrow
viewport ordering, and reduced-motion-safe behavior. Detail deep links use
frontend-modern/src/routing/resourceLinks.ts and remain stable across reload.
The temporary-suppression reason and duration are labelled native controls
owned by FormTextarea and FormSelect; Patrol must not recreate their label,
focus, responsive touch-target, or controlled-value shells locally.
The Lasting decisions section reuses the same ownership: its four decision
triggers are shared Button primitives in a labelled list, the inline
confirmation note or rule reason is a FormTextarea, and the alert-only
guidance link is a ButtonLink. PatrolIntelligenceSurface.tsx passes the
Patrol findings accessor into the workbench; the workbench does not fetch or
poll findings itself.
The feature shell keeps the independent attention workbench mounted when
Patrol is off or its model needs setup, while retaining the existing setup task
above it. Desktop and narrow browser checks must show the task and attention
list together, then allow a selected decision to open and return without
changing the disabled Patrol controls or overflowing the viewport.
The objective brief and optional-context fields in PatrolObjectivesPanel
share the same FormTextarea ownership contract.
The default queue shows severity, lifecycle state, plain-language consequence,
resource, the required review posture, and observation age. It sorts severity
first, then items with a governed approval or existing action, then newest
observation, while retaining the server-authored membership. Provider evidence,
protection posture, lifecycle controls, and history belong in selected detail;
legacy Patrol analytics belong in collapsed supporting context. Unavailable and
partial states must use explicit copy rather than success styling. Selected
detail keeps the typed facts with named fields and plain unavailable-state
labels rather than raw token pairs. On narrow viewports, selecting a row moves
and focuses that decision context without horizontal page overflow. The narrow
detail header uses a visible Back to list button, while the compact close-icon
button is wide-layout-only; either path clears the attention deep link and
restores focus to the source row. When the shared action inbox reports pending
governed work, the attention header may render a primary /actions handoff
beside its secondary refresh action. That handoff displays the action count but
does not combine it with the attention badge or list membership.
The attention shell uses one responsive master/detail primitive. Its daily briefing owns the operator headline, three compact current-work counters, and a single highest-priority start action. Desktop keeps the decision inbox visible beside either the recommended-first-decision preview or selected detail; mobile hides the inbox while detail is selected and restores it before returning focus to the source row. Empty preview space must contain the recommended decision and an explicit review action near the top of the pane rather than vertically centering content below the initial viewport. The Patrol enabled/mode/check toolbar stays compact and does not repeat the page-level value sentence. Once review starts, the same master/detail primitive exposes a live queue position plus previous/next controls at both breakpoints. Its order stays identity-stable for the selected session, successful acknowledge or suppression announces the remaining count and advances to the next current item, and the final settled item returns to the calm inbox. Lifecycle copy must explain that review removes one occurrence from today's inbox while suppression is bounded; generic backend verbs must not leave the user guessing whether the alert was resolved, hidden permanently, or merely recorded as seen. Both the position label and calm evaluation age are reactive text nodes so detail navigation and refresh cannot leave their first-render values frozen. The selected metadata keeps latest-observation age visible, action verification copy stays generic across capabilities, and the list itself is not a broad live region; only errors, calm/empty transitions, and explicit completion announcements receive scoped assistive announcement semantics.
The selected attention detail may compose the shared Actions review for an
eligible backend-authored offer. The detail owns only the expected
postcondition, explicit-review warning, verification summary, and one review
trigger. ActionReviewDialog remains the sole approve/reject/run and durable
outcome primitive. After a decision or execution refreshes and replaces the
detail subtree, dialog close must resolve and focus the current action trigger,
not a detached element reference. Browser proof covers desktop and
390-pixel mobile layouts, reduced motion, screen-reader names, exactly one run
request, and focus restoration for both confirmed and contradicted
verification.
-
Keep the Patrol model-readiness panel on the canonical AI settings state and shared model-selection surface. The panel must render connectivity, tool protocol, context quality, latency, and per-mode suitability as separate accessible evidence, support request cancellation, hydrate the latest compatible snapshot, and label failed or stale runs as evaluations rather than verified results. Safe auto-fix and Autopilot remain visibly
not_assesseduntil their governed canaries exist. -
Frontend consumers identify a storage resource by
storage.topology, never by a ZFS vdev layout. TrueNAS pools arrive astype: 'storage', sotopology === 'pool'is the only pool discriminator available to the page model, and layout strings belong instorage.vdevLayout. Regression coverage: thepool identity boundarycases infrontend-modern/src/features/truenas/__tests__/truenasPageModel.test.tsandshows the vdev layout as the storage kind while topology stays the pool discriminatorinfrontend-modern/src/components/Infrastructure/__tests__/resourceDetailDrawerTrueNASModel.test.ts. -
The diagnostics export sanitizer owns the redaction boundary for the whole diagnostics payload, not just the fields that existed when it was written. Any provider failure string reaching the export must be passed through the IP redactor, including nested probe results and state reasons. Proofs assert on the serialized bundle rather than on the sanitizer's source, because the failure mode is a payload field added later that the sanitizer never learned about. Regression coverage:
redacts PBS probe failures and state reasons in the exported bundleinfrontend-modern/src/components/Settings/__tests__/diagnosticsModel.test.tsandkeeps every PBS diagnostic failure string inside the export redaction boundaryinfrontend-modern/src/components/Settings/__tests__/settingsArchitecture.test.ts. -
An org switch must leave every live query surface with data for the new org, not merely cleared of the old org's data. Clearing state is not sufficient in
createNonSuspendingQuery, because the reset writes signals the source effect does not track, so a consumer with a constant source and no polling would sit empty until it remounted. The org-switch handler therefore refetches, matching the clear-and- refetch shape every other org-switch handler in the app already uses. Stale pre-switch responses are still discarded by request generation. Regression coverage:refetches a constant-source query after an org switchanddoes not repopulate the cache when an old-org request resolves lateinfrontend-modern/src/hooks/__tests__/createNonSuspendingQuery.test.tsx.
Current State
Manual update freshness
The update panel has a dedicated proof route to updatesPresentation.test.ts,
with exact-content browser verification still required for rendered changes.
Check Now propagates explicit freshness through the shared update store and API client. A manual request arriving during a background check waits for it and then performs its own fresh read rather than returning early. A successful response records completion time, while failed refreshes retain the last successful result and its age. The settings release date uses the shared presentation helper, which omits absent, invalid and legacy zero timestamps. Cached update responses remain valid without a release date. Browser qualification covers the current settings route, pending/retry states, keyboard checks and date presentation at desktop, intermediate and narrow widths.
Provider tabs use compact canonical evidence during route hydration
Evidence-gated provider tabs consume the unified-resource owner's source-scoped type facets while rendering only the active workflow inventory. The shell keeps requested direct routes available during first hydration, then uses the settled facet bundle as the navigation truth. A temporarily absent row payload is loading state, not proof that the provider or workflow is empty; provider empty states may render only after the active route query settles. Docker / Podman, Kubernetes, TrueNAS, and VMware continue to reuse the shared platform tabs, loading, error, and empty-state primitives rather than adding a provider-local skeleton or navigation model.
Patrol objectives reuse the shared dialog, button, badge, and resource picker contracts
The Patrol retained-objective surface composes the existing shared Dialog,
Button, MetadataBadge, and ResourcePicker primitives. It does not add a
Patrol-only overlay, selector, badge vocabulary, or focus model. The modal keeps
the standard backdrop, Escape, focus trap/return, bounded viewport, scrolling,
and responsive footer behavior while the feature owns only objective-specific
copy and orchestration.
The same shared metadata badge renders a healthy server-owned proxy observer as
Useful signal only with warning tone. The row keeps the backend explanation
that the signal does not directly measure the full objective, while only
server-authored covered state may render Watching in background. This is a
presentation of the canonical objective contract, not a frontend inference or
a new badge primitive.
System member rows are source-type aware
The Infrastructure source manager's member composition primitives now label
by owning system type instead of assuming Proxmox: buildMemberRow threads
the system type so vSphere host members present the "vSphere host" subtitle
(table mode carries it in the row tooltip, exactly like Proxmox cluster
members), and the expand toggle counts "hosts" for vmware rows and "nodes"
elsewhere. No new member primitive was introduced — vSphere composition
reuses the same expandable member-row rendering, status presentation, and
merge semantics Proxmox clusters already use.
Assistant availability in the app shell is derived from the
sessionCapabilities.assistantEnabled security-status capability, and
aiChatStore.refreshEnabledFromServer() is the canonical way to re-derive it
mid-session. AI settings save paths (setup modal, enable toggle, Provider &
Models save) must call it after a successful save so assistant entry points
appear or disappear without a full reload; no surface may flip
aiChatStore.setEnabled from settings state directly.
Patrol fix_rejected presentation is owned by the Patrol/AI finding surfaces
that render the governed-action loop, while the surrounding badge, button,
loading, and icon composition still uses shared frontend primitives. Shared
primitive code must not special-case rejected Patrol fixes outside the standard
metadata badge, button, and lucide-icon contracts.
Setup-only Patrol action chrome is also governed by the shared Patrol workspace
composition contract: provider-blocked states may show only the setup task and
direct Open Provider & Models action, must suppress the duplicate readiness
banner, and must not expose run-history buttons as a competing primary action
before Patrol can check infrastructure.
Patrol page setup banners must stay at operator level: render Patrol readiness
payloads as Patrol setup issue or Patrol setup warning, keep provider/model
context visible, and keep preflight/tool-call diagnostic wording inside
Provider & Models rather than the first-party Patrol header.
Pulse Intelligence settings now keep Provider & Models focused on provider
setup, default model selection, health, budget, usage, and provider checks; it
must not reintroduce the Patrol-control banner or Open Patrol control CTA
that belongs to the Patrol settings page and /patrol operator surface.
The Patrol settings page may still hydrate the cached Patrol diagnostic
snapshot, but the rendered model-check panel must summarize it as model
readiness rather than exposing preflight/tool-call implementation wording.
The canonical Provider & Models browser route is
/settings/pulse-intelligence/provider; /settings/system-ai remains a
routeable compatibility alias for old deep links, while new settings
navigation, OAuth callback redirects, Assistant repair actions, and Patrol
provider-repair CTAs must emit the Pulse Intelligence route.
Shared loading indicators are part of the active frontend primitive contract.
LoadingSpinner owns pure loading and action-pending spinner shells for shared
primitive internals such as Button, PulseDataGrid, and
HistoryChartOverlay, as well as Login, Settings, Patrol, and AI finding
surfaces; local animate-spin spinner shells in those consumers are governed
by the shared-template registry rather than page-local discretion.
Update progress status indicators are included in that loading boundary:
progress-stage loading must compose LoadingSpinner rather than local spinner
SVGs.
DiscoveryLoadingFallback owns the discovery-tab Suspense fallback row for
resource, workload, and Docker host drawers: centered row layout, status
semantics, discovery loading copy, and canonical LoadingSpinner composition
live there rather than in drawer-local fallback markup.
FilterButtonGroup owns feature table view toggles as well as settings
segmented selectors: page-specific labels and selected values stay in the
owning feature model, but the visible segmented selector shell must come from
the shared primitive rather than a local bordered button group.
AI settings provider fields are a governed frontend primitive, not a
provider-local form fork. The shared provider configuration section must render
provider-specific controls from aiSettingsModel.ts extraFields, including
Ollama keep_alive and the Z.ai custom base URL override, so Assistant and
Patrol keep one settings shape across
labeling, help affordances, helper copy, and persistence binding.
The shared AI model picker owns model route search and presentation for
Assistant surfaces. External open requests may seed an initial search query,
but filtering, current/default route badges, recent routes, and custom route
selection must remain inside AIModelPicker; callers should not duplicate that
logic in command handlers or feature-local model selectors. Optional provider
management actions belong in the same picker header as model refresh so
Assistant and settings surfaces can expose provider repair without forking the
model-list shell; callers own the destination, while the shared picker owns the
button placement, labeling, close behavior, and keyboard-safe dropdown state.
The Patrol alert-trigger severity selector under
frontend-modern/src/features/patrol/ is built on the shared FormSelect
primitive (label-for/id wiring, selectBaseClass styling hook) rather than a
hand-rolled <select>, so its labeling and disabled-state affordances stay
consistent with the rest of the AI settings surface. That selector belongs in the
advanced Patrol settings disclosure, below the control policy that defines what
Patrol may do.
The advanced Patrol control toggles in the same surface bind each Toggle
primitive's accessible name through ariaLabelledBy pointing at the row's
heading span, so renaming a control's visible label (for example
"Alert-Triggered Analysis" to "Container Update Risk") updates the accessible
name automatically without a parallel aria-label string. New Patrol toggle
copy must keep using the shared Toggle ariaLabelledBy wiring rather than
hard-coding a divergent accessible name.
Kubernetes RBAC inventory (Roles, ClusterRoles, RoleBindings,
ClusterRoleBindings) is part of the existing Kubernetes platform-page
Configuration tab, not a new sidebar entry or top-level route, and the
reporting-resource-type mapping at
frontend-modern/src/utils/reportingResourceTypes.ts folds all four RBAC
kinds into the existing k8s transport token alongside ConfigMaps, Secrets,
and ServiceAccounts. Configuration tab rendering keeps RBAC summary fields
(rule count, role kind / role name, subject count, subject Kinds, aggregation
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.
Cross-jump chip strips on alert and Patrol surfaces were retired on
2026-05-16 alongside the platform-first migration. The
buildResolvedResourceSurfaceLinks and buildResourceSurfaceLinksForResource
helpers (and the per-surface builders for Infrastructure / Workloads /
Storage / Recovery hrefs) were deleted from
frontend-modern/src/routing/resourceLinks.ts; the alert resource-incidents
panel and Patrol findings panel that consumed them now keep investigation
in-place through their existing handoff buttons and inline actions. Future
cross-surface drilldown chips must not reanimate the legacy helpers.
Command palette and keyboard shortcuts moved to platform-first on 2026-05-16,
and top-level aggregate workspace routes were retired on 2026-05-25
(frontend-modern/src/components/shared/commandPaletteModel.ts,
frontend-modern/src/components/shared/useCommandPaletteState.ts,
frontend-modern/src/components/shared/KeyboardShortcutsModal.tsx,
frontend-modern/src/hooks/useKeyboardShortcuts.ts,
frontend-modern/src/routing/routePreload.ts,
frontend-modern/src/routing/navigation.ts). The legacy
nav-infrastructure palette entry and g i chord remain retired with the
unregistered Infrastructure route. nav-workloads, nav-storage,
nav-recovery, and the g w / g s aggregate chords are also retired rather
than hidden as compatibility commands. Platform commands remain nav-proxmox,
nav-docker, nav-kubernetes, nav-truenas, nav-vmware (chords g p /
g d / g k / g n / g v) plus a dedicated nav-kubernetes-workloads
entry that lands on /kubernetes/workloads. The route-module preload registry
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 table chrome now allows TableCardHeader to expose a right-aligned
action slot, currently used by the Workloads/Proxmox metric display control.
That slot belongs to the table header band and must not reintroduce nested
cards or page-local toolbar wrappers inside TableCard. Proxmox host grouping
also extends the shared NodeGroupHeader row pattern: host metrics may align
with workload table columns, but the shared primitive owns the header/table
shell boundary rather than platform pages copying their own card headers.
Compact PVE version text in that header must come from the shared Proxmox
version formatter so raw pve-manager/... payloads and platform-page host
version cells stay consistent.
Mobile navigation now recognizes proxmox as a first-class platform tab in
the shared priority model so app-shell ordering remains centralized.
ResourceOperatorStateSection.tsx on the resource detail drawer
overview tab uses createNonSuspendingQuery to fetch
/api/resources/{id}/operator-state so the drawer's parent
Suspense boundary does not flicker the page-level "Loading view…"
fallback while operator-set state is in flight. New self-fetching
sections inside the drawer must follow the same pattern (or wrap in
their own local Suspense) rather than relying on createResource,
which propagates suspension to the closest ancestor.
The Patrol page header copy lives in a single canonical helper at
frontend-modern/src/utils/patrolPagePresentation.ts. The page-title
tooltip on PatrolIntelligenceHeader.tsx must read from
PATROL_PAGE_TITLE_TOOLTIP exported alongside the description rather
than carrying an inline copy, so hover and inline never drift apart on
what Patrol actually owns: watching infrastructure, detecting issues,
recording findings, and escalating into governed investigation/action
only when the selected Patrol mode allows it.
The same PatrolIntelligenceHeader.tsx shell also renders a compact
trust-at-a-glance summary directly under the page title (a
render-only consumer of state.patrolStatus()?.trust), gated on at
least one non-zero trust signal so fresh installs render no header
strip. The detailed breakdown stays in
PatrolIntelligenceWorkspace.tsx for the canonical view; the header
line is the entry-point summary so operators see active, regressed,
and verified-fix counts before scrolling into the workspace tabs.
The recency line beside the header actions also renders coverage
alongside time when the canonical getPatrolRecencyPresentation helper
returns resourcesCheckedLabel from the latest completed run. Render code
must gate on <Show when={recency().resourcesCheckedLabel}> (truthy) so
zero-coverage runs do not surface a misleading coverage phrase, failed or
scoped runs use neutral checked wording, and only successful full patrols read
as verified. The primary Patrol assessment shell must pass the same run-history
facts into getPatrolAssessmentPresentation so assessment coverage caveats do
not contradict the header's verified full-run coverage state.
The same header row may surface Trigger status when
getPatrolTriggerStatusSummary returns a runtime-relevant value from the
Patrol status payload. That text is page-owned operational metadata inside the
existing header row, not a new shared primitive, nested status card, or
secondary verdict band.
frontend-modern/src/utils/discoveryPresentation.ts owns resource discovery
command guidance targets. Discovery surfaces that need to tell operators where
to enable command execution or verify agent:exec scope must use that helper's
canonical Settings → Infrastructure and Settings → API Access handoffs
instead of hard-coding legacy settings labels or old route paths.
Shared frontend empty states, thresholds empty states, and discovery guidance
that mention the Infrastructure settings destination now consume
frontend-modern/src/utils/infrastructureSettingsPresentation.ts for the
canonical Settings → Infrastructure label and source-strategy copy. Shared
primitives must not fork that string or revive removed nested route labels.
The shared Assistant drawer owns compact source-named approval posture for
governed handoffs. Patrol handoffs render as Patrol, and alert plus alert
incident timeline handoffs render as alert investigations rather than dashboard
briefs. Those same drawer handoffs may carry model-only chat context and
resource references to the backend, but the drawer remains a presentation and
transport owner rather than the source of approval or execution truth. Patrol
briefings must stay simple: source, status, one primary subject, and an optional
safe route link. They must not render remediation step lists, evidence chips,
command summaries, or suggested-prompt chips as drawer chrome. If a
feature-owned briefing includes a safe route-owned actionHref, the drawer may
render the briefing action label as a normal app link; that link is navigation
guidance only and must not become approval or execution authority.
SettingsTab no longer includes infrastructure-connections or
infrastructure-install. The single infrastructure-systems entry in
settingsNavCatalog.ts, settingsPanelRegistry.ts, and
settingsNavigationModel.ts replaces both. Panel routing within the
infrastructure area uses InfrastructurePanelStep in-page state.
The shared monitored-system warning banner has been retired. Ordinary hosted
and self-hosted sessions must not render app-shell monitored-system capacity
warnings, plan-review links, or upgrade-impression telemetry from stale finite
policy data.
Shared alert presentation surfaces (OverviewTab.tsx, HistoryTab.tsx,
AlertOverviewActiveAlertsSection.tsx, AlertHistoryTableSection.tsx,
AlertHistoryTableAlertRow.tsx, AlertOverviewAlertCard.tsx) no longer accept
hasAIAlertsFeature or runtimeCapabilitiesLoading props. Feature gating for
AI alerts flows through the shared entitlements layer; surfaces must not
re-introduce per-surface capability fetch props.
Recovery's retired posture-card strip remains outside the shared
hover-synchronization dialect. Per the Extension Points constraint, Recovery
must not introduce a new summary-card component with row/group/chart hover
wiring without a separate governed product decision.
frontend-modern/src/components/Storage/useStorageSummaryCharts.ts now owns
the reusable polling/caching state for storage summary history, while
frontend-modern/src/features/storageBackups/storageCapacityDeltaPresentation.ts
keeps pool-growth label/tone formatting inside the shared feature presentation
layer. The storage page must keep reusing those shared owners instead of
rebuilding storage-history timers or byte-delta formatting inside row
components.
That same shared alerts feature boundary now also owns legacy shared-storage
override migration. frontend-modern/src/features/alerts/alertOverridesModel.ts
and frontend-modern/src/features/alerts/useAlertOverridesState.ts must
canonicalize per-node shared-storage override keys such as
Main-pve1-ceph-pool, hashed /api/resources storage ids, and Ceph pool
storage rows onto the storage metrics target id before the thresholds table
derives rows, so old Ceph override records and newly projected Ceph pool
overrides survive the v6 feature-shell path instead of silently disappearing
from the live editor.
Docker container override identity follows the same single-owner rule.
dockerContainerOverrideIdCandidates in
frontend-modern/src/features/alerts/alertOverridesModel.ts is the only
builder of container override keys (re-exported by
thresholdsResourceModel.ts): it leads with the stable
docker:{host}/{containerName} key that survives container recreates (#1601)
and trails the legacy container-ID, short-ID, unified-hash, and slash-tail
forms as lookup candidates. Threshold rows carry that chain as
overrideIdCandidates/overrideStorageId so every mutation path (toggle,
connectivity, offline state, edit, remove) strips the historical keys and
writes only the name key; docker surfaces must not derive container override
keys locally from resource-id parsing.
The frontend already has several guardrail tests. The next step is to keep
turning repeated local patterns into explicit shared primitives with hard usage
bounds, including provider-backed alert-history wording. frontend-modern/src/features/alerts/helpers.ts,
frontend-modern/src/features/alerts/tabs/HistoryTab.tsx, and
frontend-modern/src/features/alerts/OverviewTab.tsx must present VMware-
backed host and VM incidents with the shared resource-incident vocabulary
and existing alert-history shells instead of introducing VMware-only labels,
badges, or panel copy just because the underlying signal came from vSphere.
That same shared settings and modal boundary now also owns the public usage-data
vocabulary. frontend-modern/src/components/Settings/GeneralSettingsPanel.tsx,
frontend-modern/src/components/Settings/useSystemSettingsState.ts, and
frontend-modern/src/utils/systemSettingsPresentation.ts must present one
explicit Usage data and privacy model centered on Outbound usage telemetry;
maintainer commercial-event controls, upgrade-metrics labels, and
sales/onboarding reporting language must not appear in customer-facing Settings
or support diagnostics, and public configuration docs must not list their
internal compatibility switches as ordinary operator settings. Customer
frontend code must also not import, define, or call upgradeMetrics,
conversionEvents, infrastructure onboarding metrics wrappers, or POST those
events to /api/upgrade-metrics/events.
The telemetry copy must describe normalized release identity rather than
falling back to ambiguous telemetry, upgrade metrics, or raw-version
wording.
Shared table, disclosure, and form primitives must also stay explicitly typed
at the browser edge. Summary rows may memoize repeated pending-update reads,
shared buttons must preserve discriminated disclosure props, toggle and a11y
helpers must expose exact event signatures, shared rows must accept typed
data-* props, and reporting-panel helpers must remain ES2020-safe instead of
depending on feature-local casts or newer string helpers.
That same shared settings-shell and banner boundary now also owns demo-mode
commercial suppression. frontend-modern/src/components/Settings/settingsNavCatalog.ts,
frontend-modern/src/components/Settings/settingsNavVisibility.ts,
frontend-modern/src/stores/sessionCapabilities.ts,
frontend-modern/src/stores/sessionPresentationPolicy.ts,
frontend-modern/src/stores/demoMode.ts,
frontend-modern/src/stores/license.ts,
frontend-modern/src/stores/licenseCommercial.ts,
frontend-modern/src/useAppRuntimeState.ts,
frontend-modern/src/components/shared/HistoryChartOverlay.tsx,
frontend-modern/src/features/patrol/PatrolIntelligenceBanners.tsx, and
frontend-modern/src/features/patrol/PatrolIntelligenceHeader.tsx
must consume one shared bootstrap truth from /api/security/status. The
backend capability fact sessionCapabilities.demoMode remains part of that
payload, but the browser-owned shared primitive is now the resolved
sessionPresentationPolicy contract, which hides billing tabs, trial nudges,
monitored-system warning banners, dashboard upsells, Patrol upgrade CTAs,
history-lock paywalls, and other public-demo commercial affordances when the
browser is rendering a public demo runtime.
Platform stale-agent notices are also command-style upgrade affordances:
frontend-modern/src/features/platformPage/PlatformOutdatedAgentNotice.tsx
must stay hidden while the resolved presentation policy marks the session
read-only, including public demo mode, even though the same notice remains
available for ordinary customer installs that report outdated agents.
That same shared settings-shell boundary also owns demo-mode organization
suppression. frontend-modern/src/components/Settings/settingsNavigationModel.ts,
frontend-modern/src/components/Settings/settingsNavCatalog.ts,
frontend-modern/src/components/Settings/settingsNavVisibility.ts,
frontend-modern/src/stores/sessionPresentationPolicy.ts, and
frontend-modern/src/useAppRuntimeState.ts must fail closed on organization
navigation and app-shell org chrome until the resolved presentation policy is
known, then keep org switchers, visible Default Organization labels, and
organization-scoped settings groups hidden when the browser is rendering a
public demo runtime.
Shared primitives must not perform their own ad hoc /api/health polling,
response-header inference, hostname heuristics, or per-banner demo branching;
the runtime bootstrap, shared presentation-policy store, and shared banner
hooks stay on one canonical owner so suppression stays coherent across
customer-facing surfaces.
That same shared primitive boundary now also treats runtime capability reads
and commercial reads as separate stores. Shared settings shells and banner
hooks may read feature truth from frontend-modern/src/stores/license.ts, but
commercial identity, upgrade routing, and trial state must stay in
frontend-modern/src/stores/licenseCommercial.ts, which suppresses public-demo
loads locally and defers its first fetch until the presentation policy has
resolved instead of depending on route-local guards.
That same shared primitive boundary now also centralizes authenticated-shell
commercial posture bootstrap. frontend-modern/src/useAppRuntimeState.ts
owns the first shared loadCommercialPosture() read after authenticated app
runtime has mounted, while frontend-modern/src/AppLayout.tsx,
frontend-modern/src/components/Settings/Settings.tsx, Patrol state hooks,
and settings-panel state hooks must consume the
resolved store state instead of reissuing mount-time posture fetches from each
surface. Shared commercial posture loading may still dedupe or force-refresh
through the store for governed billing or first-run flows, but route-local or
panel-local bootstrap ownership is forbidden.
Storage disk drawers now also sit on that same shared-primitives floor.
frontend-modern/src/components/Storage/DiskDetail.tsx must render physical-
disk read, write, and busy charts through HistoryChart plus
useHistoryChartState, using the canonical physical-disk history resource id,
instead of reviving diskMetricsHistory, a page-local ring buffer, or another
storage-only live chart primitive for the same telemetry.
That same shared-primitive floor now also owns upgrade-navigation semantics.
frontend-modern/src/utils/upgradeNavigation.ts is the canonical typed
internal-vs-external destination helper, while
frontend-modern/src/components/shared/UpgradeLink.tsx and
frontend-modern/src/components/shared/useUpgradeNavigation.ts own how shared
paywall surfaces navigate those destinations. Feature shells may request a
commercial destination, but they must not re-decide whether that destination
opens in-app or in a new tab once the shared primitive exists.
That same shared-primitive floor now also owns prerelease shell guidance.
frontend-modern/src/AppLayout.tsx is the canonical authenticated-shell owner
for prerelease presentation, and the remaining user-facing treatment is the
compact Preview badge keyed from resolved release metadata. Feature pages,
settings panels, shared components, and route-local shells must not add a
second release-candidate banner, hardcoded GitHub release or feedback links,
or page-local prerelease notices once that shared shell contract exists.
Browser proof for that shell rule now lives in
tests/integration/tests/57-release-candidate-shell.spec.ts, which must keep
rc-channel builds banner-free while preserving the compact preview badge.
The subsystem registry now also requires explicit proof-policy coverage for all
shared runtime files, and shared-component guardrails fail if raw table
composition is reintroduced in new shared components outside the canonical
allowlist.
Retained-value query ownership is now part of that shared floor too.
frontend-modern/src/hooks/createNonSuspendingQuery.ts is the canonical
shared helper for page-local fetches that must stay inside the mounted
surface instead of falling through the app-level Loading view... fallback.
Feature slices such as recovery and infrastructure drawers may consume that
helper, but they must not fork new suspense-escape helpers once the shared
contract exists.
That retained-value boundary is explicitly bounded for long-lived browser
sessions. Fulfilled resource/range queries use a 64-entry LRU with a five-minute
inactive lifetime, clear on organization changes, and reject late old-scope
completions from the shared cache. Large storage summary history follows the
same ownership rule through frontend-modern/src/utils/storageSummaryCache.ts:
it keeps at most 20 recent node/range summaries and aborts in-flight requests
when organization ownership changes. Drawer navigation, chart range churn,
background tabs, reconnects, and server restarts must not turn either cache
into an append-only browser history.
Consumers whose visible meaning changes with the source key may set
retainPreviousValueOnSourceChange to false. When the new key has no retained
entry, the helper must clear to the consumer's initial value in the same
reactive turn before starting the replacement request; a failed or slow history
range request must never leave data from the previous range labeled as the
newly selected range. Cached data for the exact new key may still render
immediately and refresh in the background.
Each query run receives an AbortSignal; changing the source, resetting the
query, or unmounting its owner must abort the superseded browser request before
starting replacement work. Consumers must forward that signal through their
API/cache layer when the transport supports cancellation.
The settings reporting shell now also owns a deliberate split between
historical performance reports and current-state VM inventory export.
frontend-modern/src/components/Settings/ReportingPanel.tsx,
frontend-modern/src/components/Settings/useReportingPanelState.ts,
frontend-modern/src/components/Settings/reportingCatalogModel.ts,
frontend-modern/src/components/Settings/reportingPanelModel.ts, and
frontend-modern/src/components/Settings/reportingInventoryExportModel.ts must
keep those as separate operator jobs with separate request builders and success
copy, rather than collapsing inventory export back into the metrics-report
controls.
That same settings shell must now also render both historical performance
options and VM inventory schema from the backend-owned reporting catalog rather
than hardcoding panel copy, routes, or range presets in the frontend. The
frontend models may validate and present the catalog, but the canonical panel
title, descriptions, endpoints, filename prefixes, range windows, and column
list belong to the API reporting contract.
That same settings-shell boundary now also owns operator-facing docs referrals
for governed security panels. APIAccessPanel.tsx and
SecurityOverviewPanel.tsx must route scope and proxy-auth guidance through
the shared shipped-doc helper in frontend-modern/src/utils/docsLinks.ts
instead of hardcoding GitHub main URLs that can drift from the running
build, and tests/integration/tests/20-local-doc-links.spec.ts must keep
browser proof on those settings-shell surfaces.
That same settings-shell boundary now also owns the remediation framing for
Security Overview itself. SecurityOverviewPanel.tsx may not stop at a score
card and static best-practices copy once low-risk security debt has been
demoted out of the global banner; it must render explicit next-step hardening
actions on the canonical settings shell, source those actions from the shared
security presentation owner, and keep direct operator links pointed at the
owning auth, API-access, or shipped security-guide surface. The canonical
proof for that shell framing remains
frontend-modern/src/components/Settings/__tests__/settingsArchitecture.test.ts.
The same reporting catalog ownership now also governs the operator resource-
selection cap for performance reports. ReportingPanel.tsx and
ResourcePicker.tsx may present or enforce that limit, but they must receive
it from the backend-owned multiResourceMax definition rather than hardcoding
the reporting cap in frontend-local constants.
That same catalog-owned capability contract also governs which optional
performance-report controls appear at all. The settings shell and reporting
request builder may not assume metric filtering or custom titles are always
available; they must honor supportsMetricFilter and supportsCustomTitle
from the backend catalog and avoid emitting unsupported controls or request
parameters from frontend-local defaults.
That same backend-owned catalog also owns the initial reporting selections and
transport details. useReportingPanelState.ts,
reportingPanelModel.ts, and reportingInventoryExportModel.ts may not seed
format/range selections from legacy frontend constants or invent fallback
report endpoints, filename prefixes, default report titles, or range windows
or fallback filename date-stamp styles
when the catalog is
present; the first valid selection and all request semantics must come from the
parsed backend definition.
The same rule applies to VM inventory export transport details: request
builders and fallback filenames must derive the export format and extension
from the parsed inventory definition instead of hardcoding csv in frontend
helpers.
That same fallback contract also includes the single-report filename subject,
so frontend download builders may not substitute resource display names when
the catalog says fallback attachment names are keyed off canonical resource IDs.
That same reporting shell must also route failed catalog/report/export
responses through the shared API error extractor in frontend-modern/src/utils/apiClient.ts
rather than surfacing raw JSON payload text from response.text() directly in
warning UI.
That same reporting transport contract also means the frontend download path
must prefer the backend Content-Disposition filename over any locally built
fallback name when a report or inventory export response arrives.
That same settings shell must also read the reporting catalog for locked users,
not just entitled users, so the paywalled reporting panel does not drift onto a
separate frontend-owned title or description contract.
That same settings shell must now also treat the reporting catalog as the
feature-identity source once it loads. ReportingPanel.tsx and
useReportingPanelState.ts may use a generic loading or error shell before the
catalog is available, but they must not hardcode the reporting feature key or
the entitled and locked panel title and description once the catalog has
loaded.
The same metadata route is readable without the reporting feature gate, so the
settings shell must not delay the catalog fetch on licenseLoaded() before it
can render its canonical loading, locked, or entitled states.
That same shell must also stay usable against older Pulse backends that do not
yet expose /api/admin/reports/catalog. When that specific metadata route
returns 404, useReportingPanelState.ts may fall back to the governed legacy
performance-report transport (/api/reporting and /api/reporting/generate-multi)
so the reporting panel does not go dead on mixed-version installs, but that
compatibility path is intentionally report-only and must not invent the newer
catalog-owned VM inventory export surface.
ReportingPanel.tsx must therefore treat vmInventoryExport as optional when
it renders a governed reporting catalog. A legacy compatibility catalog with no
inventory export still owns a valid enabled reporting surface and must continue
to render the performance-report workflow instead of collapsing back to the
unavailable shell.
That same catalog load must also remain retryable after transient failure.
useReportingPanelState.ts may memoize or dedupe in-flight work, but it must
not permanently latch a failed first fetch and force operators to reload the
entire settings page before the reporting shell can recover.
That same contract also includes the locked teaser copy itself. The reporting
catalog owns report-builder identity once the feature is available, but a locked
session must render neutral feature-gate copy from the reporting panel state so
Community users do not see enabled report-builder language before advanced
reporting is available.
That same reporting catalog also owns the enabled-shell guidance callout that
explains when to use performance reports versus VM inventory export.
ReportingPanel.tsx may choose the presentation primitive, but the callout
title and description must come from the parsed catalog instead of a
frontend-local explainer paragraph.
The shared updates settings owner also defines the user-facing framing for
rc-tagged builds. frontend-modern/src/components/Settings/updatesSettingsModel.ts
and frontend-modern/src/utils/updatesPresentation.ts must present that
channel as a prerelease or preview path with manual validation expectations,
not as a near-ready release candidate promise.
The root app shell now also treats backend availability as distinct from
websocket liveness: frontend-modern/src/AppLayout.tsx and
frontend-modern/src/useAppRuntimeState.ts must keep the top-right connection
badge aligned to overall backend availability so a healthy dev/runtime backend
does not present the whole shell as reconnecting just because the live stream
is transiently renegotiating. That shell badge must now stay on an explicit
state model as well: healthy runtime, backend-healthy-but-stream-degraded,
full reconnect, and full disconnect are distinct operator states, and the
shared shell may not collapse them back into one generic reconnect label.
Shared feature presentation helpers under frontend-modern/src/features/ now
also need to preserve route-owned page-health semantics when the owning surface
is REST-backed: operators should only see reconnect or disconnected shells when
the route's own data contract is unhealthy, not because a global websocket
singleton is transiently reconnecting.
Those same feature-owned header badges must also stay aligned to the owning
runtime state instead of surfacing stale auxiliary counters as primary status.
Legacy hosted-model credit counters may remain parseable on transport payloads,
but shared Patrol headers must not render them as customer-facing badges in the
normal self-hosted GA app.
The same shared shell rule applies to retired hosted availability copy: when an
older backend payload describes hosted model activation, credit exhaustion, or
account-backed AI availability, shared feature shells must normalize the
operator-facing guidance back to provider setup or local model setup rather than
rendering the retired offer.
The same primitive boundary now also owns the first AI enable control in
AISettings.tsx: the primary toggle must remain explicitly addressable with a
stable accessible label and route unconfigured installs into the canonical
provider setup modal instead of falling back to generic "first pressed toggle"
selectors, provider-model-load heuristics, legacy hosted-model enablement, or
in-app trial acquisition.
That same route-owned presentation rule also governs Patrol findings empty
states: shared section shells under frontend-modern/src/features/patrol/
must not render a green healthy empty state from 0 active findings alone
when the owning Patrol runtime or overall-health summary is degraded, blocked,
or not fully verified.
The same empty-state helper must consume Patrol trust-history evidence so a
historical regression reads as history review context, not as a current issue
and not as a healthy all-clear.
The same hierarchy also applies inside the Patrol summary shell: once the
primary assessment strip states Patrol's current risk and verification basis,
supporting metrics under that strip must stay metric-oriented and must not
repeat assessment or verification labels as a second compact verdict row.
The collapsed Patrol assessment strip itself must remain a compact readout
rather than a headline-plus-paragraph block; explanatory assessment and
recommendation copy belongs in the owning Findings, Runs, Details,
or Assistant chat surfaces rather than a normal-path summary details expansion.
That readout should lead with current operator state and score rather than
mixing a reassuring grade label with issue-state copy in the same line.
That same summary shell should also keep the shared Pulse surface neutral:
severity belongs in compact accents, inline readouts, and badges rather than
turning the whole assessment into a tinted warning banner, nested card, or
hero-style block that breaks the surrounding operator workflow.
That same summary-shell rule also applies to timing metadata: if the header,
verification card, or findings footer already presents the governed Patrol
activity timestamp, the summary chip row must not add another recency badge
that competes with those owned timing surfaces.
The same default-readout rule applies to collapsed Patrol issue rows:
MetadataBadge may carry severity, recurrence, and active decision/work states,
but the default Patrol page must not render raw lifecycle or investigation
process badges such as detected, review finding, loop state, status, outcome,
or confidence as row chrome. Those details belong in expansion, run history,
Assistant handoff context, or diagnostics.
The same ownership split applies to supporting counts: if the Patrol summary
surface renders the metric strip for active findings, warnings, criticals, and
fixes, the primary assessment strip should not repeat those same counts in badge
form beside the assessment and verification copy.
That same ownership rule applies to empty-state timing metadata. When the
Patrol page header already carries schedule and recency context, the findings
empty state should not add its own footer for Last activity, Next run, or
run interval text.
Those supporting cards must also keep their content factual and count-based:
active findings, critical findings, warnings, and fixes are valid secondary
readouts, while labels such as Issues detected or Partial verification
belong only to the primary Patrol assessment and verification surfaces.
The same applies to Patrol operational context during active execution: the
shared feature surface may add an explicit run-in-progress badge, but any
activity support surface or integrated summary panel must remain factual
activity copy rather than shifting into a second Patrol verdict label while a
run is underway.
frontend-modern/src/components/shared/TagBadges.tsx is now also the
canonical tag-badge primitive. Workload rows and the unified-resource detail
drawer must import that shared owner instead of keeping a workload-local tag
badge variant or importing a feature-local path into infrastructure
surfaces.
That same owner now also holds the CSP-safe tag-dot rendering contract: tag
color and active-state emphasis must travel through SVG fill/stroke attributes
or stable classes, not inline background-color, box-shadow, or other
style= mutations that break the hosted demo CSP.
The shared tag owner also carries the complete accessibility contract for
compact tag disclosure. Every rendered tag must expose its name without
depending on pointer hover. Callers that provide filtering behavior must get
named native toggle buttons with current selection represented by
aria-pressed; informational tags and collapsed tag counts must remain
keyboard focusable so the same tooltip content is available on focus. Pointer
leave, blur, and Escape must dismiss those tooltips, focus must remain visibly
identifiable, and the compact dot presentation must use spaced 20px targets
rather than restoring an 8px click-only hit area. Feature rows and detail
surfaces must extend these shared semantics instead of wrapping tag dots in
feature-local click or tooltip handlers.
TagBadges also owns Proxmox tag color fidelity. When a caller supplies a
source instance, the primitive must read that instance's pveTagStyles entry
before the legacy aggregate pveTagColors map, and it must honor the Proxmox
caseSensitive flag for both override lookup and deterministic fallback color
generation. Feature rows may pass instance identity into the primitive, but
they must not rebuild Proxmox color-map lookup locally.
frontend-modern/src/components/Settings/OperationsPanel.tsx is now also the
canonical shared settings wrapper for operations-style panels such as
diagnostics, reporting, and system logs. Those surfaces must extend that owner
instead of rebuilding a local SettingsPanel wrapper, panel-header action
slot, or divided content-body framing inline.
The system logs operations surface now follows the same shell/runtime split as
the other modernized settings panels: frontend-modern/src/components/Settings/SystemLogsPanel.tsx
owns the operations framing and consumes the canonical stream-copy/status
helpers from frontend-modern/src/utils/systemLogsPresentation.ts, while
frontend-modern/src/components/Settings/useSystemLogsPanelState.ts owns the
stream lifecycle, buffering, level updates, and download action. Future system
logs work must extend that split instead of pulling EventSource, API calls,
notification flow, or customer-facing system-log copy back into the panel
render shell.
Self-hosted trial CTA removal is now part of that same primitive boundary for
settings and shared paywalls. Shared/settings runtime owners may derive neutral
plan and entitlement posture from the canonical commercial contracts, but
ordinary self-hosted feature gates must not present in-app trial starts,
trial-status banners, or trial-specific rate-limit copy. Operator-facing paid
handoff remains limited to explicit Plans, hosted, activation, recovery, or
support surfaces; feature gates may show neutral "View plans" links through
frontend-modern/src/utils/upgradePresentation.ts only where presentation
policy allows commercial discovery.
The shared self-hosted billing presentation and plan-section prop contract must
therefore expose only the ordinary typed plan handoff. It must not retain a
parallel trial label, card-required note, trial=1 destination, or dormant
trial-action slot after self-hosted trial acquisition has been retired.
Top-level route files are now expected to stay thin when a feature owns the
real product surface, but the former /infrastructure surface is not one of
those compatibility cases. It never shipped as a stable v6 route, so
frontend-modern/src/App.tsx must not register it and future feature surfaces
must extend Settings infrastructure, platform/runtime pages, or shared
Infrastructure components instead of recreating
frontend-modern/src/features/infrastructure/ as a hidden page shell.
Infrastructure resource consumers may opt into websocket-first unified
resource hydration only when they also schedule canonical REST revalidation
after the first-paint settle window; shared route composition must not re-route
the table through a blocking resource fetch just to confirm infrastructure that
the realtime store has already reported. Authenticated cold starts must render
from retained realtime or unified-resource state without falling back to
first-run/welcome posture or replaying stale setup success notifications, and
background revalidation may update rows in place but may not blank the page.
Realtime resource adapters must defensively coalesce split host identities by
the same source-bridge rule as the API boundary so a transient backend rebuild
cannot surface duplicate infrastructure rows while the next canonical REST
snapshot is settling.
Infrastructure summary and detail surfaces now also use the shared normalized
identity lookup helper from frontend-modern/src/utils/resourceIdentity.ts
so dotted hostnames and alias variants stay consistent between the shared
table, drawer, and detail views instead of each component carrying its own
identifier-variant logic.
Those same surfaces also share the trimmed-string helper from
frontend-modern/src/utils/stringUtils.ts so shared components do not keep
their own copy of the same whitespace-trimming identity logic.
The shared infrastructure summary table now also follows the same
shell/runtime/model shape as the rest of the modernized primitives.
frontend-modern/src/components/shared/InfrastructureSummaryTable.tsx stays
the table shell, frontend-modern/src/components/shared/useInfrastructureSummaryTableState.ts
owns alert wiring, sort state, breakpoint state, and expanded-row lifecycle,
frontend-modern/src/components/shared/infrastructureSummaryTableModel.ts
owns sorting, count, identity-alias, and linked-agent derivation, and
frontend-modern/src/components/shared/InfrastructureSummaryTableRow.tsx
owns the per-row render/runtime surface. Future work should extend those
owners instead of pushing websocket, alert, or identity plumbing back into the
shared table shell.
The shared infrastructure summary row may consume alert-backed metric
thresholds from the table state, but threshold selection itself remains
alerts-owned through frontend-modern/src/stores/alertsActivation.ts and
frontend-modern/src/utils/metricThresholds.ts; shared primitive cells and rows
must only pass resolved warning/critical values into metric presentation.
That alerts-owned boundary distinguishes detector state from external
notification delivery. Shared tables, cells, navigation, and localized copy
may use detectionEnabled to decide whether in-product alert evidence is
applicable, but they must not derive visibility or threshold presentation from
activationState. The activation control is presented as notification
delivery only, and its localized paused copy must state that detection and
in-product active-alert visibility continue.
The shared infrastructure selector now follows that same owner split.
frontend-modern/src/components/shared/InfrastructureSelector.tsx stays the
render shell, frontend-modern/src/components/shared/useInfrastructureSelectorState.ts
owns selected-node state, tab-reset and escape-key lifecycle, plus hook-backed
resource and recovery composition, and
frontend-modern/src/components/shared/infrastructureSelectorModel.ts owns
resource-family counts, agent-backed node-summary projection, unified-node and
PBS-instance projection, and recovery backup-count derivation. Future
infrastructure-selector work should extend those owners instead of pushing
resource aggregation or selection lifecycle back into the shared shell.
That shared selector projection must also preserve canonical local operator
identity for agent-backed infrastructure labels. Governed or AI-safe resource
summaries may inform policy/detail surfaces, but the selector's summary and
drawer-facing agent labels must continue to use the same local instance
identity boundary as the operator-facing infrastructure tables so multiple PBS,
PMG, or other governed resources remain distinguishable.
The shared infrastructure details drawer now follows that same owner split.
frontend-modern/src/components/shared/InfrastructureDetailsDrawer.tsx stays
the render shell, frontend-modern/src/components/shared/useInfrastructureDetailsDrawerState.ts
owns tab-selection runtime, and
frontend-modern/src/components/shared/infrastructureDetailsDrawerModel.ts
owns canonical metadata-id and discovery-hostname derivation. Future
infrastructure-details-drawer work should extend those owners instead of
pushing tab state or resource-identity normalization back into the shared
shell.
Object detail drawers follow one operator-first information hierarchy across
platform implementations. Overview must begin with DrawerAttentionSection
when active alert or health evidence exists and show the actual problem text,
not only repeat a coloured status. DrawerAttentionSection is a compact
bounded alert list, not a generic detail-table card: every row preserves the
backend-authored problem text, renders the actual info/warning/critical/
acknowledged severity, and reveals overflow through an in-place accessible
disclosure rather than an inert hidden-count row. Aggregate drawers that include
alerts from child resources must also preserve the affected resource identity
and alert metric beside that text rather than collapsing them into generic VM
or host labels. The remaining Overview rows are additive
operator context that the parent table row cannot carry, such as OS/runtime,
Pulse observation or action coverage, primary reachability, protection gaps,
pending updates, or an identified service. Routine health, placement, and
metric values already visible in the row must not be restated merely to fill
the drawer. Curated raw IDs, kernel/build strings, interface and disk facts, and
provider metadata stay visible through TechnicalDetailsSection and the same
compact DetailSectionTable rows as Overview; those facts must not switch to a
provider-local card mosaic or require another tap. Only genuinely large or
interactive provider-support content belongs behind
TechnicalDetailsDisclosure, which owns its collapsed, lazily mounted
boundary. DetailSectionTable keeps the single bordered table at narrow widths
where density matters, then presents those same canonical rows as bounded
section cards on desktop. Desktop cards share the available row width, stretch
to the same row height, and use stable three- or four-column tracks. An
incomplete final row must fill those same tracks with explicit integral spans;
it must not independently flex-grow every remaining card into unrelated column
edges. Five- and six-section drawers remain balanced across three-column rows,
while seven-section drawers use a two-column span for the first card in the
final row so the remaining cards keep the four-column alignment. Cards use a
bounded local label column with left-aligned values so the layout has no ragged
fixed-width island, stranded full-width final card, or full-drawer scan
distance. Unified-resource technical summaries are part of this boundary and
must not retain a full-width local table on desktop. The responsive
presentation stays owned by the shared primitive;
provider drawers must not fork their own desktop card renderers. Monitoring
Optional detail-row progress is also owned by that shared presentation: the
value text remains visible, DetailSectionTable composes ProgressBar for the
bounded visual fill and accessible value, and unknown measurements render no
bar. Provider drawers must not add inline width styles or local progress
geometry to their section rows.
mode, lifecycle, notes, maintenance,
automatic-action policy, saved access configuration, and action audit belong
to a dedicated Manage tab and must not render inside Overview. Guest, node,
Docker-host, and unified-resource drawers must compose these shared primitives
instead of defining provider-local attention or technical disclosure shells.
The same drawer family must consume the canonical
useDiscoveryFeatureAvailability boundary before exposing Discovery chrome or
content. Until the shared AI runtime settings explicitly report Discovery as
enabled, drawers render no Discovery tab, readiness state, explanatory copy,
analysis reveal, or identified-service suggestion and issue no passive
discovery-record read. Provider-specific drawer shells must not reinterpret a
technical discovery target as operator consent to enable the feature.
Drawer headers reserve their limited space for the subject and the canonical
full-row collapse control, plus a direct object-specific lifecycle action only
where the object contract requires one. ObjectDrawerHeader owns that
interaction across guest, node, Docker/unified-resource, Ceph cluster,
Proxmox Mail Gateway, and shared inline detail panels: pressing anywhere in the subject row collapses the detail,
Enter/Space work through the native button, and lifecycle actions do not bubble
into collapse. Generic Ask Assistant and Copy context actions do not belong
in object drawer headers: Assistant remains available through the global shell,
and raw context export must not compete with the operational reading path.
The shared interactive sparkline now follows that same split.
frontend-modern/src/components/shared/InteractiveSparkline.tsx stays the
render shell, frontend-modern/src/components/shared/useInteractiveSparklineState.ts
owns hover state, RAF throttling, canvas draw scheduling, and resize lifecycle,
and frontend-modern/src/components/shared/interactiveSparklineModel.ts owns
sparkline downsampling, gap segmentation, axis-tick math, and hover-selection
policy. Future sparkline work should extend those owners instead of pushing
canvas scheduling or chart-shape math back into the shared component shell.
That same sparkline boundary now also owns floating tooltip shell routing:
local hover tooltips must derive viewport anchor coordinates from the shared
runtime/model path, keep the tooltip beside rather than on top of the scrub
cursor, and render through
frontend-modern/src/components/shared/TooltipPortal.tsx, not as HTML
foreignObject shells inside the preserveAspectRatio="none" chart SVG where
cross-browser scaling can stretch the tooltip surface or drop its semantic
shell styling.
That same shared sparkline boundary now also owns active-series isolation
metadata. The shell may expose data-active-series-display and
data-rendered-series-count for proof and inspection, but only the shared
runtime/model owners may decide whether a hovered or focused series is merely
emphasized or fully isolated; feature shells must not fork their own row-hover
line filtering.
The retired dashboard overview route must not regain feature-local trend,
KPI, problem-resource, or card shells. Workload-table and guest-row fallback
copy that lives under frontend-modern/src/components/Workloads/ must keep
using frontend-modern/src/utils/workloadEmptyStatePresentation.ts and
frontend-modern/src/utils/workloadGuestPresentation.ts. New route-level empty
states, tone mapping, or compact issue copy must extend the shared
emptyStatePresentation, semanticTonePresentation, and
problemResourcePresentation helpers instead of reviving deleted
dashboard-only KPI, metric, storage, recovery, or trend presentation helpers.
That shell must also stay passive with respect to data ownership: future
overview trend cards may render summary-range controls and operator-facing
empty or error copy only after they have a governed owner, and they must not
reintroduce route-local metrics-history fetch loops for CPU and memory
sparklines when the canonical infrastructure summary surface already owns the
chart contract.
The shared density map now follows that same owner split.
frontend-modern/src/components/shared/DensityMap.tsx stays the render shell,
frontend-modern/src/components/shared/useDensityMapState.ts owns hover
signals, canvas draw lifecycle, and resize handling, and
frontend-modern/src/components/shared/densityMapModel.ts owns bucket/window
math, hover target selection, focused-series tooltip detail, and density-cell
opacity rules. Future density-map work should extend those owners instead of
pushing canvas lifecycle, tooltip shaping, or chart math back into the shared
shell.
The shared trial banner is retired for self-hosted v6 GA. Future commercial
notification work must start from the explicit Plans, hosted, activation,
recovery, or support surfaces rather than reviving a global authenticated-shell
trial banner.
The shared column picker now follows that same owner split.
frontend-modern/src/components/shared/ColumnPicker.tsx stays the render
shell, frontend-modern/src/components/shared/useColumnPickerState.ts owns
dropdown open state and outside-click listener lifecycle, and
frontend-modern/src/components/shared/columnPickerModel.ts owns hidden-column
count, reset visibility policy, and column-option text-class/copy policy.
Column-picker trigger badges must describe what the count means, such as
N hidden, rather than exposing a bare number or ratio that competing table
surfaces can interpret differently. Shared column-picker tests must cover that
copy alongside the owner split so governed product tables do not regress to
ambiguous utility badges.
Future column-picker work should extend those owners instead of pushing
document-level listener logic or column-count policy back into the shell.
The shared tag input now follows that same owner split.
frontend-modern/src/components/shared/TagInput.tsx stays the render shell,
frontend-modern/src/components/shared/useTagInputState.ts owns input state,
container-focus runtime, and tag add/remove/backspace orchestration, and
frontend-modern/src/components/shared/tagInputModel.ts owns delimiter keys,
placeholder policy, remove-title copy, and canonical next-tag derivation.
Future tag-input work should extend those owners instead of pushing DOM reach-in
or tag-mutation policy back into the shell.
The shared scroll-to-top button now follows that same owner split.
frontend-modern/src/components/shared/ScrollToTopButton.tsx stays the render
shell, frontend-modern/src/components/shared/useScrollToTopButtonState.ts
owns scroll-listener lifecycle, visible state, and smooth-scroll runtime, and
frontend-modern/src/components/shared/scrollToTopButtonModel.ts owns
scrollable-ancestor discovery, visibility threshold policy, aria label, and
button class policy. Future scroll-to-top work should extend those owners
instead of pushing scroll-container discovery or listener lifecycle back into
the shell.
The shared toggle now follows that same owner split.
frontend-modern/src/components/shared/Toggle.tsx stays the render shell,
frontend-modern/src/components/shared/useToggleState.ts owns disabled gating
plus the synthetic toggle change-event runtime, and
frontend-modern/src/components/shared/toggleModel.ts owns size resolution,
track/knob/container class policy, and the canonical toggle event type.
Future toggle work should extend those owners instead of pushing synthetic
event behavior or size/class policy back into the shell.
Binary on/off controls are registry-backed too. Product surfaces must compose
Toggle or TogglePrimitive for shared track/knob styling, disabled behavior,
label/description wiring, and synthetic checked events instead of recreating
local role="switch" buttons with aria-checked and page-local classes.
Ordinary checkboxes, radio groups, and row-selection controls are separate
affordances and must not be forced through this toggle primitive.
The shared status badge now follows that same owner split.
frontend-modern/src/components/shared/StatusBadge.tsx stays the render shell,
frontend-modern/src/components/shared/useStatusBadgeState.ts owns disabled
gating and click runtime, and
frontend-modern/src/components/shared/statusBadgeModel.ts owns size padding,
label/title fallback policy, and status-badge class selection. Future status
badge work should extend those owners instead of pushing label/title policy or
disabled click handling back into the shell.
Read-only health/state badges are a separate shared primitive.
frontend-modern/src/components/shared/StatusIndicatorBadge.tsx owns
status-to-tone mapping, optional dot wiring, sizing, shape, and label
presentation for product surfaces that display state rather than toggle it.
Product components must compose StatusIndicatorBadge instead of calling
getStatusIndicatorBadgeToneClasses directly; low-level status utilities may
still expose the tone mapping for that primitive and utility-level tests.
Platform alert severity indicators are a governed specialization of that same
primitive family. frontend-modern/src/components/shared/AlertSeverityBadge.tsx
owns the alert severity badge and dot shells, while
frontend-modern/src/utils/alertSeverityPresentation.ts owns alert severity
label formatting, severity-bucket-to-status-indicator mapping, and
severity-bucket-to-detail-row tone mapping. Docker, Kubernetes, TrueNAS,
vSphere, and future platform alert tables must compose AlertSeverityBadge,
AlertSeverityDot, and getAlertSeverityDetailTone; they must not recreate
severityVariant, severityTextClass, alertTone, or severity badge spans
locally.
Platform alert severity filters follow the same shared-template rule.
frontend-modern/src/features/platformPage/platformAlertSeverityFilterOptions.tsx
owns the canonical All/Critical/Warning/Info option labels, tones, and leading
dots for platform alert table toolbars. Platform alert tables must call
getPlatformAlertSeverityFilterOptions instead of declaring local severity
filter arrays or calling filterChipStatusDot directly for those filters.
Platform alert detail formatting follows the same by-construction primitive
rule. frontend-modern/src/utils/alertDetailPresentation.ts owns the provider
code labels, provider-specific resource-type labels, vSphere alert entity
labels, started-at row labels, and full detail timestamp labels consumed by
Docker, Kubernetes, TrueNAS, and vSphere alert tables. Those tables must call
formatPlatformAlertCode, formatPlatformAlertResourceType,
formatPlatformAlertEntityType, formatPlatformAlertStartedAt, and
formatPlatformAlertDetailDateTime instead of declaring local formatter
helpers.
Read-only metadata badges follow the same primitive-owned shell rule.
frontend-modern/src/components/shared/MetadataBadge.tsx owns filled and
outlined appearances, compact sizing, shape, typed tone vocabulary, fit
behavior, and whitespace handling. Product surfaces such as Patrol findings
may own the labels and state-to-tone mapping in their presentation helpers, but
they must render visible metadata chips through MetadataBadge instead of
recreating local bordered xs spans.
Neutral and muted badge treatments must use semantic surface/text tokens rather
than hardcoded gray palettes; non-gray typed tones may retain their state color
vocabulary so success, warning, danger, info, and platform-adjacent metadata do
not collapse into visually identical chips.
Patrol run-history labels follow this state-badge boundary:
Patrol may derive the status label and typed variant in
patrolRunPresentation.ts or patrolSummaryPresentation.ts, but
RunHistoryEntry.tsx must render visible state badges through
StatusIndicatorBadge rather than runStatus.badgeClass or a local span.
The shared segmented selector now follows that same owner split.
frontend-modern/src/components/shared/FilterButtonGroup.tsx stays the render
shell, frontend-modern/src/components/shared/useFilterButtonGroupState.ts
owns variant resolution plus disabled selection/change runtime, and
frontend-modern/src/components/shared/filterButtonGroupModel.ts owns the
variant class catalog, compact-label policy, and segmented button class
selection. Future filter-button-group work should extend those owners instead
of pushing label truncation or segmented variant policy back into the shell.
Compact labels that begin with All must preserve that scope word rather than
collapsing to a trailing noun such as time; feature-owned compactLabel
overrides remain authoritative when a domain needs different concise copy.
Pressed/unpressed selector pills follow the same primitive rule.
frontend-modern/src/components/shared/SelectablePillButton.tsx owns the
pressed button shell and aria-pressed wiring, while
frontend-modern/src/components/shared/selectablePillModel.ts owns the active
and inactive pill class catalog. API token scope surfaces may own the security
scope labels and click handlers, but must not recreate rounded-full selector
pill class strings locally. A token's in-place scope editor may use semantic
native checkboxes because it is a multi-select form checklist rather than a
pressed pill selector; it must not imitate or fork the selectable-pill class
catalog.
Filter-toolbar segmented controls must delegate to this primitive rather than
calling segmentedButtonClass directly, and icon+text labels must render as
one inline-flex button label so compact bars keep the v5 single-line control
language across Type/Status, grouped/list, bars/trends, columns, and reset.
The shared selection-card primitive now follows that same owner split.
frontend-modern/src/components/shared/SelectionCardGroup.tsx stays the render
shell, frontend-modern/src/components/shared/useSelectionCardGroupState.ts
owns variant resolution plus disabled selection/change runtime, and
frontend-modern/src/components/shared/selectionCardGroupModel.ts owns the
tone fallback, group/button class catalog, and title/description presentation
policy. Future selection-card-group work should extend those owners instead of
pushing tone or active-card presentation logic back into the shell.
The shared dialog now follows that same owner split.
frontend-modern/src/components/shared/Dialog.tsx stays the render shell,
frontend-modern/src/components/shared/useDialogState.ts owns focus trap,
body-scroll lock, previous-focus restoration, shared blocking-dialog
visibility, and backdrop-close runtime, and
frontend-modern/src/components/shared/dialogModel.ts owns focusable-element
lookup plus layout and panel class policy. Future dialog work should extend
those owners instead of pushing focus-trap lifecycle or layout policy back into
the shared shell.
App-shell consumers such as frontend-modern/src/App.tsx and
frontend-modern/src/AppLayout.tsx may read that shared blocking-dialog state
to suppress background affordances, but they must not reimplement their own
parallel modal-stack bookkeeping.
The shared history chart now follows the same owner shape.
frontend-modern/src/components/shared/HistoryChart.tsx stays the render
shell, frontend-modern/src/components/shared/useHistoryChartState.ts owns
license gating, history fetch/refresh, canvas draw lifecycle, and hover state,
and frontend-modern/src/components/shared/historyChartModel.ts owns tooltip
formatting, scale and axis math, and closest-point selection. Lock overlays in
ordinary self-hosted surfaces must stay informational rather than presenting
trial-start or upgrade-link actions. Future history-chart work should extend
those owners instead of pushing fetch, license, commercial trial actions, or
canvas math back into the shared component shell.
The shared history range catalog is also owned here. The canonical product
range sequence is 24h, 7d, 14d, 30d, and 90d, with 14d preserved
as the Relay entitlement surface rather than hidden behind the Pro-only
long-range controls. Lock copy must derive its target days and tier label from
the selected range instead of assuming every locked history selection is a
30-day or 90-day Pro ask.
The remaining header, overlay, and tooltip render surfaces now live in
frontend-modern/src/components/shared/HistoryChartHeader.tsx,
frontend-modern/src/components/shared/HistoryChartOverlay.tsx, and
frontend-modern/src/components/shared/HistoryChartTooltip.tsx instead of
re-accumulating those sections inline in the shell.
That tooltip owner now also holds the CSP-safe hover contract: chart tooltips
must render inside the chart surface with model-owned layout and SVG/attribute
positioning, not through fixed portals or inline left/top style attributes
that violate the public demo CSP.
Tooltip shell chrome must follow semantic surface, text, and border tokens
rather than hardcoded dark palette utilities so light and dark themes share one
primitive-owned contrast contract.
The shared container update badge now follows that same owner split.
frontend-modern/src/components/shared/ContainerUpdateBadge.tsx stays the
render surface for the badge, icon, and update button shells,
frontend-modern/src/components/shared/useContainerUpdateButtonState.ts owns
Docker update mutation flow, persistent update-store state, settings gating,
and button lifecycle, and
frontend-modern/src/components/shared/containerUpdateBadgeModel.ts owns badge
and button tooltip formatting, class selection, and label/state presentation.
Future container-update work should extend those owners instead of pushing
store wiring, settings reads, or mutation flow back into the shared shell.
For governed container updates, the row's current-session pending state is
bound to the action ID. A changed registry update badge, a legacy command for
the same container, or elapsed time cannot turn an unconfirmed action green or
offer a fresh plan. The row reopens that action with a read-only action GET;
only its recorded completed outcome earns the transient completed state.
Operator-closed unknown outcomes remain reviewable and must not be presented
as failed. A browser reload loses this row-local shortcut, not the durable
Actions audit; the audit remains the authority before any later retry.
The shared web interface URL field now follows that same owner split.
frontend-modern/src/components/shared/WebInterfaceUrlField.tsx stays the
render shell, frontend-modern/src/components/shared/useWebInterfaceUrlFieldState.ts
owns metadata fetch/save/remove lifecycle, success/error state, and suggested
URL runtime, and frontend-modern/src/components/shared/webInterfaceUrlFieldModel.ts
owns URL validation, target-label normalization, and suggested-URL presentation
rules. The shared primitive now also supports an embedded mode with a caller-
owned title so feature drawers can place web-interface controls inside a larger
access surface without forking the save/remove/runtime behavior. Future
web-interface URL work should extend those owners instead of pushing metadata
transport or validation back into the shared shell.
Missing-suggested-URL diagnostics remain useful only when the operator has no
saved or entered URL; once a custom web-interface URL is present, the shared
field must suppress "no suggested URL" warnings so Discovery does not make a
valid manual endpoint look broken.
Saved web-interface launch affordances must also stay on a shared primitive
instead of page-local table columns or one-off external-link anchors.
frontend-modern/src/components/shared/WebInterfaceLink.tsx owns URL
classification, the distinct adjacent launch control, new-tab safety
attributes, row-click/key propagation containment, invalid-URL presentation,
and accessible launch labels. The resource name remains inert identity text;
the launch control is a separate keyboard-focusable target with at least a
24-by-24 CSS-pixel hit area so activating it never doubles as row selection or
drawer navigation. Workload guest rows, grouped node headers, standalone
machine rows, Proxmox node rows, Docker/Podman rows, Kubernetes rows, unified
host/PBS/PMG rows, and alert resource rows/group headers compose that primitive
for every comparable overview surface. Missing URLs render no fake control.
Malformed or non-HTTP(S) saved values render a non-interactive accessible
warning rather than disappearing or becoming executable. Runtime/platform
tables must not add separate Web columns, page-local external-link anchors,
linked resource names, or duplicated new-tab safety handling for that launch
affordance.
Docker host rows follow the same primitive-owned contract end to end.
DockerHostsTable.tsx composes ResourceNameWithWebInterfaceLink beside the
inert host name, while the host drawer's Manage access surface embeds
WebInterfaceUrlField with metadataKind="docker-host" and the stable host
source id. The drawer must return saved URL changes to its owning table row so
the adjacent launch control updates immediately; neither the table nor drawer
may fork metadata transport, URL validation, or new-tab behavior into a
Docker-local implementation.
Shared-template drift enforcement is registry-backed:
frontend-modern/scripts/shared-template-registry.json is the canonical list of
standardized repeated affordances, required consumers, and forbidden local
patterns, while frontend-modern/scripts/shared-template-audit.mjs enforces
that registry. Future repeated-affordance migrations must add or extend a
registry rule as part of the same change that extracts or adopts the shared
primitive.
Button-styled commercial upgrade CTAs are one of those registry-backed
templates. frontend-modern/src/components/shared/UpgradeLink.tsx owns
UpgradeButtonLink, and the registry requires gated settings, audit, agent
profiles, and self-hosted plan CTA surfaces to compose it rather than styling
UpgradeLink or anchors locally. Commercial presentation helpers may own the
label and destination intent; they must not own CTA button chrome.
Platform table frames are one of those registry-backed templates.
frontend-modern/src/features/platformPage/sharedPlatformPage.tsx owns
PlatformTableShell, including the canonical table card, header row, and body
divide styling, plus the single-line 32-pixel summary-row rhythm inherited by
Proxmox, Docker / Podman, Kubernetes, TrueNAS, vSphere, Standalone, Workloads,
and direct Storage consumers. Inline detail rows remain content-sized.
Secondary context must move to an existing column, a supplemental tooltip, or
the inline detail drawer instead of stacking a second visual line. The shell
also owns the final responsive table-class composition, so
feature tables may supply breakpoint floors but cannot accidentally override
the shared phone-width floor with min-w-full. Platform table frames now have
no local-frame exceptions in shared-template-registry.json: new and existing
platform tables must compose PlatformTableShell instead of recreating the
TableCard, header row, or body divide frame locally. Platform table consumers
must preserve the owner split:
frontend-primitives owns the repeated PlatformTableShell frame and guardrail
registry, while platform and unified-resource consumers own the source-specific
row fields, drawers, and resource semantics.
The rendered phone contract now applies consistently to Proxmox, Docker,
Kubernetes, TrueNAS, vSphere, Standalone, and direct Storage data tables. At
less than 360 pixels of content width, the shared platform-table container
rule gives identity 40 percent and hides only consumer-marked
platform-table-narrow-hidden cells; ordinary phone widths retain the richer
five-to-seven-column projection. Summary rows remain single-line, and any
value that must truncate is backed by a touch- and keyboard-operable inline
detail row that exposes the complete value.
Standalone Pulse Agent and Availability consumers may compose one compact
status summary directly above that shared table frame. The consumer owns the
already-loaded resource counts, freshness-aware attention ordering, and
settings action; the shared primitive boundary still forbids a second fetch,
detached proof strip, decorative chart, or locally recreated table frame.
The shared platform toolbar owns count grammar and canonical status-route
normalization: one visible row uses the singular form, and legacy provider
status values such as running or stopped normalize into the page's shared
health filter rather than leaking provider vocabulary between platform routes.
Platform table empty states follow the same registry-backed ownership.
PlatformTableEmptyState owns the repeated table-card empty-state shell for
Docker, Kubernetes, Proxmox, Standalone, TrueNAS, vSphere, and future platform
feature tables; source-specific consumers own only the empty-state icon, title,
description, and actions. Platform feature tables must not import
EmptyState directly or recreate a Card-wrapped empty-state shell.
Embedded settings and patrol panel empty states follow the same
shared-template registry, but compose EmptyState directly with
variant="panel" when they are not platform table/card empty states. The
primitive owns compact spacing, icon treatment, text hierarchy,
framed-versus-panel density, and action-slot layout; feature panels own only
the empty-state copy, icon choice, and callbacks. Migrated panels such as
Agent profiles, Audit Webhooks, Audit Log, Availability, Diagnostics, SSO
providers, and Patrol Run History must not recreate local text-center icon
stacks, dashed empty-state frames, or page-local empty-state action buttons.
Platform table loading states are registry-backed too.
PlatformTableLoadingState owns the repeated table-card compact
role="status" loading row for platform pages and tables; platform consumers
own only the title and description copy. Platform feature surfaces must not
recreate the TableCard plus compact status-row shell locally.
Platform table text-cell fallback formatting is a shared primitive as well.
formatPlatformTableTextValue in
frontend-modern/src/features/platformPage/sharedPlatformPage.tsx owns the
trimmed-string plus canonical empty-cell marker behavior. Kubernetes platform
tables must compose that helper instead of declaring local textValue helpers
or inlining asTrimmedString(...) || '—' fallback expressions for table text
cells.
Platform table title-case fallback formatting follows the same rule.
formatPlatformTableTitleCaseValue owns the repeated trimmed-string plus
Unknown fallback behavior for state/status labels that need simple title
case. TrueNAS platform tables must compose that helper instead of declaring
local titleCase helpers.
Platform table compact list summaries follow the same rule.
summarizePlatformTableValues owns the repeated trimming, empty-marker label,
visible-value count, +N overflow suffix, full-title text, and normalized
value-list behavior for dense platform table cells. Kubernetes
service/network/config/policy/autoscaling tables, vSphere datastore/network
tables, and TrueNAS network-share tables must compose that helper instead of
declaring local compactList or summarizeValues helpers.
Platform table uptime formatting follows that rule too.
formatPlatformTableUptimeValue owns the repeated compact/full uptime label
selection plus canonical empty-cell marker behavior for dense platform table
cells. Docker / Podman hosts, Kubernetes nodes, Proxmox nodes and backup
server rows, Proxmox Mail Gateway instance and drawer node rows, Standalone
machines, TrueNAS systems, and vSphere ESXi hosts must compose that helper
instead of declaring local formatUptime helpers or importing the generic
formatter in table files for the same days/hours/minutes fallback.
Platform table byte-size formatting follows the same rule.
formatPlatformTableBytesValue owns the repeated positive-byte formatting plus
canonical empty-cell marker behavior for dense platform table cells. Docker /
Podman native storage cells, Docker / Podman engine storage-usage cells,
Kubernetes node and storage capacity cells, Proxmox backup server, Ceph,
coverage, and recoverable-artifact size cells, and TrueNAS system, VM,
storage-topology, and protection byte cells must compose that helper instead
of declaring local formatBytes wrappers, importing the generic formatter in
table files, or reimplementing byte-unit precision there.
Compact platform table timestamps follow the same rule.
PlatformTableDateTimeValue and formatPlatformTableDateTimeValue own compact
date-time parsing, invalid/empty markers, optional minimum-year filtering, Intl
format options, and tabular-number styling for dense timestamp cells. TrueNAS
protection completed-time cells and vSphere activity "When" cells must compose
that primitive instead of declaring local compact toLocaleString helpers, and
timestamp columns use the canonical numeric-value alignment kind because they
are scannable scalar values.
Relative timestamp-age cells follow the same rule.
PlatformTableRelativeTimeValue and formatPlatformTableRelativeTimeValue own
the repeated formatRelativeTime composition, compact-label default,
invalid/empty markers, and tabular-number styling for dense platform table
cells. Docker / Podman volume created-at cells, Kubernetes deployment age
cells, Kubernetes event observed-time cells, Proxmox backup created-age cells,
Proxmox replication last-sync cells, Standalone machine last-seen cells, and
Standalone availability-check checked-at cells must compose that primitive
instead of importing formatRelativeTime directly or declaring local
timestamp-age helpers in table files.
Duration and interval cells follow the same rule.
PlatformTableDurationValue and formatPlatformTableDurationValue own
seconds/minutes/hours duration labels, explicit fallback text, canonical
empty-cell markers, and tabular-number styling for dense platform table cells.
Proxmox replication last-duration cells and Standalone availability-check poll
interval cells must compose that primitive instead of declaring local
seconds/minutes helpers.
Responsive platform table width normalization is registry-backed too.
getPlatformTableWeightedColumnWidthStyle owns visible-column weight
normalization, zero-width fallback, and stable four-decimal percentage
formatting for dense platform table models. Docker / Podman container columns
and Proxmox host columns must keep their domain column IDs, layout breakpoints,
and weight maps local, but must call that shared helper instead of declaring
local formatPercentage / toFixed(4) width helpers.
Platform table numeric fallback rendering is registry-backed too.
PlatformTableNumberValue owns finite-number checking, tabular-number styling,
custom empty-marker support, and caller-owned number formatting for dense
optional numeric table cells. Docker / Podman native count helpers, Kubernetes
optional count cells, Docker Swarm service desired/running counts, Kubernetes
Deployment replica counts, Proxmox Mail Gateway count columns, and TrueNAS
system share/service and storage-topology disk count cells must compose that
primitive instead of declaring local numberValue, numericValue,
replicaCount, countCell, diskCountLabel, or cell-level tabular-nums
variants. If a scheduler, service-domain, or inventory count is intentionally
zero-defaulted, the consuming table owns that field/default choice and still
renders through PlatformTableNumberValue.
Locale-formatted integer count labels share the same primitive boundary.
formatPlatformTableIntegerValue owns rounded integer formatting, locale
grouping, and empty-marker behavior for dense platform table and drawer count
cells. Kubernetes namespace drawers, Proxmox Backup Server backup counts,
Ceph pool object counts, and Proxmox Mail Gateway table/drawer counts must
compose that helper, usually through PlatformTableNumberValue, instead of
declaring local formatInteger, formatLocaleCount, formatNumber, or direct
toLocaleString() count formatting.
PlatformTableCountRatioValue owns the companion healthy/total or ready/total
count-ratio skeleton: numerator, slash, muted denominator, tabular styling, and
empty marker behavior. formatPlatformTableCountRatioValue owns the same
zero-default and suffix behavior for string-only table summaries and titles.
Kubernetes cluster child counts compose the component instead of keeping a
table-local childCountCell renderer, and Kubernetes networking endpoint
summaries compose the formatter instead of hand-building 3/3 ready strings;
the consuming table owns only which current/total values, suffix, and warning
tone apply.
One-decimal percent and positive Celsius cells are also shared platform-table
value primitives. PlatformTablePercentValue owns percent formatting,
tabular-number styling, and empty markers; formatPlatformTablePercentValue
owns the same one-decimal percent string for overlay labels, titles, and
sparkline labels, including caller-selected ratio normalization and clamping.
PlatformTableTemperatureValue owns finite positive Celsius validation,
one-decimal °C formatting, tabular-number styling, and empty markers. Docker
/ Podman host, Proxmox backup/Ceph/node/mail-gateway, and TrueNAS
system/storage-topology tables or drawers must compose those primitives instead
of carrying local formatPercent, formatPercentLabel, toFixed(1)%, or
temperature label helpers.
Platform table metric fallback rendering is also shared.
PlatformTableMetricFallback owns the centered muted empty marker used in
metric bar cells plus optional caller-owned fallback label/title text, and
getPlatformTableFiniteMetric owns finite-number normalization for CPU and
memory, disk, and capacity values. Docker / Podman, Kubernetes, Proxmox,
Standalone, TrueNAS, and vSphere platform tables and their table-model helpers
must compose those helpers instead of declaring local metricFallback /
finiteMetric helpers or inlining centered muted dash fallback markup in
metric cells.
Platform table metric severity coloring is alert-backed, not hardcoded. The
Docker host and container, Proxmox node, Kubernetes cluster and node, TrueNAS
system and app, and vSphere host tables must resolve display thresholds
through the alerts subsystem's activation store
(getMetricThresholds with the platform's runtime scope and override
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.
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
host identity only for Off, Suspended, or Unknown; powered-on hosts retain only
the canonical health dot there. Tablet and desktop widths keep the full Power
column, and the mobile exception badge must not replace or recolor the health
indicator.
Canonical Linux memory usage-unavailable is a first-class metric fallback,
not a numeric zero. Shared workload bars, platform tables, drawers, and live
history labels must render N/A (while retaining known capacity where useful)
and must not synthesize a zero-width used segment or a healthy-looking 0%.
When unified-resource metadata carries an unavailable raw Proxmox, agent, or
Docker memory facet alongside a trusted metric from another source, the
trusted merged metric wins; the raw facet remains diagnostic evidence only.
Platform load-failure states are registry-backed as well.
PlatformErrorState owns the repeated table-card error shell, warning icon,
and Refresh action for platform page and table load failures; platform
consumers own only the failure title, description, and refresh callback.
Platform feature surfaces must not recreate an EmptyState plus local Refresh
button for Could not load... states.
Platform section tabs are registry-backed too. PlatformSectionTabs owns the
workflow tab shell, hidden-single-tab behavior, active-link styling, link
targeting, active-page aria state, and minimal active-tab visibility scrolling
after route or viewport-size changes; platform page surfaces own only tab specs,
the active tab choice, and aria-label copy.
The visibility behavior is a shared horizontal-rail boundary:
horizontalRailVisibilityModel.ts owns the bounded minimal-scroll calculation,
and useActiveHorizontalRailItemVisibility.ts owns route-state, resize, and
rail-resize synchronization. Platform section tabs, Alerts mobile navigation,
and future horizontally scrolling destination rails must compose that owner so
the selected destination cannot remain clipped after direct navigation or a
viewport change.
Platform feature surfaces must not rebuild local nav tab bars with
aria-current and border-tab styling.
Filter bars are registry-backed too. FilterBar owns resource-list filtering
as a catalog of FilterDef entries, while filterChipStatusDot owns the
small leading status-dot glyph used by filter options. Page and feature
surfaces must not copy the chip-dot <span> factory or import the legacy
PageControls deck for resource-list filtering; those drift checks live in
shared-template-registry.json and run through shared-template-audit.mjs.
Status indicator dots are registry-backed too. StatusDot owns the shared
size, color-token, pulse, title, aria, and decorative-status behavior for
resource and health dots, while feature owners supply only the status
semantics. Storage linked-disk health rows must derive a StatusDot variant
through getLinkedDiskHealthDotVariant and must not recreate local rounded
green/yellow span classes in storage components or storage-backup presentation
helpers.
Loading indicators are registry-backed too. LoadingSpinner owns the shared
border-based spinner shell, size catalog, tone catalog, decorative status, and
accessible status label behavior. Shared primitive internals such as Button,
PulseDataGrid, and HistoryChartOverlay, plus Login, Settings, Patrol, and
AI finding surfaces, must compose that primitive for pure loading and
action-pending spinners; icon-specific refresh rotation remains local icon
state, not a loading-spinner shell.
Native select controls are registry-backed too. FormSelect owns label/id
wiring, helper-text description merging, value synchronization, default select
chrome, dynamic-option value synchronization, and compact styling hooks for
native selects. Product components and shared filter/menu internals must
compose FormSelect rather than recreating screen-reader labels, native
<select> shells, value-reapply effects, or compact select chrome locally; the
only raw native select in frontend runtime code should live inside that
primitive.
Native textarea controls follow the same contract. FormTextarea owns
label/id wiring, helper-text description merging, value synchronization, default
textarea chrome, and compact styling hooks for multi-line text fields. Alert
destination fields, incident notes, infrastructure merge reports, commercial
recovery input, and agent-profile prompt/description fields must compose
FormTextarea; alert, settings, and infrastructure runtime code must not
recreate raw native <textarea> shells outside that primitive.
Search controls are registry-backed too. SearchField owns simple search
input chrome, clear affordance, keyboard forwarding, focus handling, aria
labels, and trailing-control padding, while SearchInput owns resource-list
search enhancements such as history, tips, and type-to-search wiring. Product
surfaces must compose those primitives instead of rendering native
type="search" inputs or recreating search icon/clear/input classes locally.
Settings resource selectors must use SearchField for both primary text
search and secondary tag/resource filters instead of restoring native
type="text" filter inputs beside a shared search field.
Segmented selectors are registry-backed too. FilterButtonGroup owns the
settings, prominent, compact, and equal segmented selector shells, including
active-button tone, disabled-option behavior, pressed-state semantics,
compact labels, and horizontal scroll treatment through the shared
shell/state/model split. Settings and compact feature surfaces must compose
that primitive instead of copying active-button selector styling locally.
Selectable pill buttons are registry-backed too. SelectablePillButton owns
rounded pressed/unpressed selector pills, including active tone, disabled
treatment, focus ring, and aria-pressed; settings and security surfaces must
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
shell/state/model split. Table surfaces must compose that primitive instead of
recreating local column chooser buttons or panels.
Selection-card groups are registry-backed too. SelectionCardGroup owns the
compact/detail card grid, active-card tone, disabled selection behavior,
pressed-state semantics, title/description styling, and icon container
treatment through the shared shell/state/model split. Settings and feature
surfaces must compose that primitive instead of copying compact/detail
border-card styling locally.
Grouped/list table-mode controls are registry-backed as well.
GroupedTableModeSegmentedControl owns the shared Group by label,
Grouped / List labels, tooltip titles, and icons for table mode switching.
Resource surfaces must compose that primitive instead of copying grouped/list
segmented-control labels locally.
Product table cards are registry-backed too. TableCard owns the shared
bordered, no-padding, card-tone table frame, while TableCardHeader owns the
title/action/clear chrome, clear button copy, aria label, and propagation
containment for table-card headers. Product table surfaces must compose those
primitives instead of recreating local overflow-hidden bordered wrappers or
retired summary-table header aliases.
Inline detail table rows are also registry-backed. InlineDetailTableRow
owns the row/cell/content shell and row-click containment for platform,
workload, and infrastructure inline drawers; callers may pass row-specific
data-* attributes, colspan, and content classes, but they must not recreate
the surface-alt detail row shell locally. The content shell must clip
horizontal paint below the large breakpoint without becoming a scroll
container, reset the parent table's whitespace-nowrap inheritance, and allow
its descendants to shrink, then restore visible overflow for the static
desktop layout. Long operator-state copy must wrap inside the shared row border
instead of painting beneath adjacent controls or disappearing at the clip edge.
When focused detail content is removed, InlineDetailTableRow restores focus
to its current aria-controls disclosure with preventScroll; live refresh,
collapse, and row replacement must not move the surrounding application
viewport merely to reveal that control.
Inline detail section content is registry-backed separately from the row shell.
DetailSectionTable, InlineDetailPanel, and detailSectionModel.ts own
detail row compaction, section-table rendering, value-tone classes, and the
inline collapse action for platform alert/activity/protection/service detail
panels. InlineDetailPanel composes ObjectDrawerHeader, so its entire heading
row closes the panel instead of presenting a separate text button; consumers
may own the platform-specific section data, but they must not recreate local
DetailField grids or route platform-neutral detail tables through a
provider-named primitive.
Long identifying labels can opt into DetailRow.layout: 'stacked': the shared
renderer places the complete label above its value and progress bar in one
full-width cell on both narrow and desktop layouts. The label wraps even an
unbroken path segment rather than inheriting table nowrap/ellipsis; it must be
readable without hover or horizontal scrolling. Compact two-cell rows remain
the default. This is a presentation choice only: value formatting, unknown
usage, tone and accessible progress metadata retain their existing semantics.
Platform row-detail disclosure controls are also registry-backed templates.
frontend-modern/src/features/platformPage/PlatformResourceDetailTableRow.tsx
owns PlatformResourceDetailToggleButton, which composes
SummaryRowActionButton for the canonical row-detail affordance, accessible
label, aria-expanded, aria-controls, and propagation containment. Platform
tables that use createPlatformResourceDetailState or render local inline
detail rows must compose that toggle; they may still own the detail row content,
drawer payload, post-success refresh callbacks, and platform-specific fields.
The shared help icon now follows that same owner split.
frontend-modern/src/components/shared/HelpIcon.tsx stays the render shell,
frontend-modern/src/components/shared/useHelpIconState.ts owns open state,
popover-position lifecycle, and global click/escape listeners, and
frontend-modern/src/components/shared/helpIconModel.ts owns help-content
resolution, icon sizing, missing-content warnings, and popover-position math.
Future help-icon work should extend those owners instead of pushing registry
lookups or DOM listener lifecycle back into the shared shell.
The shared mobile nav now follows that same owner split.
frontend-modern/src/components/shared/MobileNavBar.tsx stays the render
shell, frontend-modern/src/components/shared/useMobileNavBarState.ts owns
the mutually exclusive platform/overflow menu state, keyboard focus return,
outside-click dismissal, last-active-platform continuity, and click handoff
lifecycle, and
frontend-modern/src/components/shared/mobileNavBarModel.ts owns platform and
utility tab ordering, the dedicated platform-switcher projection, alert badge
counts, and tab
button class policy. Future mobile-nav work should extend those owners instead
of pushing tab-order or DOM lifecycle logic back into the shared shell. With
support/admin controls moved under Settings, that utility ordering must no longer
reserve a standalone operations slot; alerts, Patrol, and Settings are the
remaining authenticated utility tabs.
Platform switching is a first-level mobile action. The first bottom-rail slot
must show the current or most recently active platform and open the dedicated
platform menu containing every evidence-admitted platform; platform choices
must not be buried in the generic More menu. Alerts, Patrol, and Actions remain
pinned daily-operation destinations. More is reserved for secondary utilities
such as Settings, keeping the bottom rail to five predictable targets without
making infrastructure switching a two-level navigation task.
The shared command palette now follows that same owner split.
frontend-modern/src/components/shared/CommandPaletteModal.tsx stays the
render shell, frontend-modern/src/components/shared/useCommandPaletteState.ts
owns query state, selected-row keyboard state, open-reset/focus lifecycle,
route-path wiring, and command selection, and
frontend-modern/src/components/shared/commandPaletteModel.ts
owns canonical command construction plus query normalization and filtering
policy. Future command-palette work should extend those owners instead of
pushing route construction or search policy back into the shared shell.
The palette search composes SearchField as an editable list-autocomplete
combobox: DOM focus remains on the search input while arrow-key selection is
exposed through aria-activedescendant, and the active listbox option remains
scrolled into view. Palette options stay outside the Tab sequence; filtering
to an empty result collapses the combobox and clears its active descendant.
The OpenCode reference for this interaction is
packages/opencode/src/cli/cmd/run/footer.command.tsx at origin/dev
e82542b8023a8374f29c23b70ec019c8f256354e, where RunCommandMenuBody
builds and filters command rows, holds selected menu state via
createFooterMenuState, resets selection as the query changes, and routes
keyboard movement and selection through handleKey. Pulse adapts that contract
by keeping command construction in the shared palette model and command
execution in the shared palette state / Assistant store instead of moving
editor-specific command routing into the render shell.
Assistant command-palette actions are shell requests, not duplicated chat
logic. New session, session picker, model picker, Undo, and Redo commands must
flow through the shared frontend-modern/src/stores/aiChat.ts command request
contract so the drawer owns disabled/loading state, prompt restoration, and
notifications while the palette remains only a searchable command surface.
The command-palette Assistant open command is also contextual shell routing:
frontend-modern/src/components/shared/useCommandPaletteState.ts must derive
current-view context through frontend-modern/src/utils/assistantPageContext.ts
and pass that context into aiChatStore.open(...), while
frontend-modern/src/components/shared/commandPaletteModel.ts owns the
corresponding Ask about <view> label. It must not fall back to an empty
generic Assistant open action.
The shared search field now follows that same owner split.
frontend-modern/src/components/shared/SearchField.tsx stays the render shell,
frontend-modern/src/components/shared/useSearchFieldState.ts owns focused-
Escape clear/blur behavior and input-ref lifecycle, and
frontend-modern/src/components/shared/searchFieldModel.ts owns clear/shortcut
visibility rules plus trailing-control padding policy. Future search-field work
should extend those owners instead of pushing event behavior or layout policy
back into the shared shell. Forwarded keyboard and blur events must preserve
native browser event getters and methods while normalizing currentTarget and
target; shared search-field wrappers must not proxy native event properties or
methods through a receiver that can break KeyboardEvent/FocusEvent getters,
preventDefault(), or stopPropagation() in live browser surfaces.
The shared search input now follows that same owner split.
frontend-modern/src/components/shared/SearchInput.tsx stays the render shell,
frontend-modern/src/components/shared/useSearchInputState.ts owns input-ref
lifecycle, type-to-search registration, and enhancement runtime composition,
and frontend-modern/src/components/shared/searchInputModel.ts owns the shared
search-input contract plus shortcut-hint and trailing-control policy. Future
search-input work should extend those owners instead of pushing type-to-search
or enhancement wiring back into the shared shell.
Infrastructure-aware completion follows that owner split. Product surfaces
provide safe canonical identity projections through SearchInput suggestions;
useSearchInputEnhancements.ts ranks those identities and exposes only the
single dimmed inline suffix after the current query. It must not introduce a
second results dropdown. When several identities match, the suffix stops at
their unambiguous common prefix instead of selecting the first object. Tab or
Right Arrow accepts that inline text. Enter commits either an exact identity or
a shorter query that still resolves to known suggestions; unmatched prose
remains ordinary free-text search.
The shared page-controls bar now follows that same owner split.
frontend-modern/src/components/shared/PageControls.tsx stays the render shell
for canonical page-level control composition, while
frontend-modern/src/components/shared/FilterToolbar.tsx owns the shared
search-row, filter-row, and inline-leading-slot layout surface. Monitoring
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
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
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
must not import or render ViewOptionsDisclosure themselves. Consumers with
no currently applicable presentation choice must omit viewOptions rather
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.
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
field, adds one search-term pill, and serializes committed terms as
comma-separated inclusive alternatives so operators can select several
objects without turning arbitrary phrases into chips. Removing a search-term
pill must immediately update the consumer search state. Platform consumers
project only canonical object identity, type, scope, status, and safe aliases through
frontend-modern/src/features/platformPage/platformSearchSuggestions.ts;
arbitrary resource metadata and secrets are not autocomplete candidates.
PlatformTableToolbar must always expose contextual Clear filters for a
non-empty search, including simple table-local search/status state that does
not need a feature-owned reset. Route-owned or multi-facet platform surfaces
may supply one composite reset, which the toolbar delegates exactly once;
otherwise the shared FilterBar fallback clears its search and inline status
catalog without forcing every platform table to repeat reset plumbing.
The compact Add filter variant is the shared FilterBar default: the visible
uppercase field label and labelled-field shell stay hidden while the native
select retains its accessible Filter name. Platform table toolbars and other
resource-list consumers inherit that treatment automatically instead of
choosing different label and width chrome page by page. A surface that truly
needs form-style field labelling must opt in explicitly through
showAddFilterLabel.
Primary filters with small, stable option sets should stay one-click controls
inside that same FilterDef[] catalog by setting inline: true; FilterBar
renders those as unlabeled compact segmented controls in the same second-row
rail as view options, matching the v5 filter-bar pattern, and keeps longer or
dynamic scope filters in the menu/chip path. Feature surfaces must not fork
local filter rows or bury high-frequency Type, Status, or other true filter
facets behind an extra menu just to regain one-click behavior; persistent
presentation preferences such as grouped versus flat layout belong in View.
Detailed multi-state catalogs are not primary one-click controls merely because
they filter by status. A catalog large enough to create a horizontally clipped
rail on narrow viewports, including Storage's seven-state status catalog, must
stay in the Add filter menu and surface a non-default selection as a chip.
Legacy PageControls and labelled select/toggle primitives are not the
resource-list filter shape. If a future surface needs a new filtering
affordance, it should extend the FilterBar catalog model or add a new
registry-backed shared primitive rather than reintroducing a per-page select
row.
FilterBar does not carry a saved-views affordance. The former
savedViewsKey / useSavedViews / SavedViewsMenu trio persisted named
query strings to localStorage under pulse:filterbar:saved-views:<key>; it
was removed because a saved view was only ever the page's URL query string,
which the browser's own bookmarks already capture, sync, search, and share.
The URL-ownership rule it depended on survives it and is now the primary
contract: every filter a surface exposes must be URL-owned, so a filtered page
is a shareable, bookmarkable link. Do not reintroduce an in-app view library;
extend URL coverage instead. The Machines surface uses the canonical
STANDALONE_QUERY_PARAMS query and status keys and delegates one composite
reset to StandalonePageSurface; it must not mix a local search signal with a
route-owned status facet or issue consecutive route writes that can resurrect
one cleared parameter.
The Proxmox Backups surface follows the same URL-ownership boundary with
PROXMOX_BACKUPS_QUERY_PARAMS: search, workspace, scope, per-workspace facet,
and selected activity day are all route-owned.
Its feature owner may clear incompatible hidden facets when the workspace
changes and may preserve that workspace during a composite reset, but the
shared FilterBar remains the sole owner of Clear, filter-menu, and
popover chrome across desktop and narrow layouts.
ProxmoxBackupsCoverageStrip keeps its compact context on the title baseline at
desktop widths and gives that context a full-width, left-aligned row below the
title on narrow screens. A wrapped context must not retain an auto margin that
creates an apparent empty column.
That surface also keeps asynchronous posture presentation explicit: an
unresolved canonical posture request uses a neutral Checking row state and
coverage segment, while host and orphan recovery rows without a canonical
workload identity use neutral Not evaluated. Neither presentation state may
reuse the server-owned Unknown label, enter the posture filter contract, or
be styled as a protection success or failure.
Alert History follows the same rule for its route-owned search, period, and
severity state. Its feature hook owns one composite reset and exposes a
search-aware active-state accessor to FilterBar; the shared shell owns where
the contextual Clear filters action appears, while the alerts feature must not
fall back to sequential per-control URL writes or hide that action for a
search-only result set.
Alert History severity option counts also follow the shared estate-orientation
contract: the alerts feature supplies counts from the exact predicate used by
its list, while FilterBar renders the values and the shared Inventory totals
visibility preference decides whether they are shown. A feature must not build
counts from page-wide alert totals after search or another active facet has
narrowed the rendered rows, and a time-scope option must not claim a count from
an unfetched period.
The Alert History frequency axis is also a responsive contract. Desktop keeps
the complete model-owned tick set; below the sm breakpoint the render surface
keeps only start, midpoint, and end labels so locale-formatted timestamps do
not overlap or create horizontal overflow. The 390px operator qualification
must assert three visible labels, non-overlapping client rectangles, keyboard
reachability, and a contained document before its actual-pixels receipt is
recorded.
Alert History investigation detail is also responsive by interaction model,
not only by CSS. Desktop may keep Timeline and Resource incident detail inline
with its table row, while the virtualized phone card list must open the shared
detail components inside the feature-owned full-height Dialog drawer in
frontend-modern/src/features/alerts/MobileAlertHistoryInvestigationDialog.tsx.
The drawer owns a bounded overflow-y-auto and overscroll-contain evidence
scrollport, while the app scroll shell and the fixed-estimate history window
remain unchanged underneath it. Escape and the explicit close action dismiss
the drawer and restore focus to the originating row action when that
virtualized row still exists, falling back to the phone history list when it
does not. Future expandable content inside a fixed-estimate list must use the
same independent-detail boundary or move to a variable-height virtualizer; it
must not change a mounted virtual row's height and compensate with ad hoc page
scroll writes.
Because that popover combines view application, default selection, removal,
and an inline naming form, it is a labelled non-modal dialog rather than an
ARIA menu. Its trigger exposes the dialog relationship, Escape returns focus
to the trigger (or from the naming form to the save action), the naming field
has a persistent label, and destructive view controls remain visibly
discoverable at narrow touch viewports instead of depending on hover. The
panel anchors from the trigger's leading edge on the expanded mobile filter
rail, where the trigger wraps to the left edge, and returns to trailing-edge
alignment on desktop so its management actions cannot run past either viewport
edge.
Filter-bar popovers must compose the shared FilterPopoverTrigger; the
shared trigger owns their matching text emphasis, icon geometry, disclosure
chevron, active state, and button alignment instead of allowing each popup
wrapper to restyle that contract independently. When no menu-backed filter row
is present, contextual Clear filters joins the mobile action row instead of
forcing View onto an isolated line. View takes the row's available
trailing space and anchors its panel from the mobile action rail's trailing
edge, then returns to trigger-relative trailing alignment on desktop; Saved
keeps its leading-edge mobile anchor.
When no menu-backed filter chip, contextual Clear action, or leading action is
present, the compact Add filter control joins that same action cluster instead
of occupying an otherwise empty row by itself. Once a menu-backed filter is
active, Add filter remains with the active chips so scope editing stays grouped.
The mobile action cluster is non-breaking as a unit; orientation readouts such
as result counts and trend ranges wrap separately so they cannot strand View
on a line by itself.
The desktop controls rail keeps inline filters and utility actions at opposite
edges while both fit on one line. Its wrapping parent owns that split through
space distribution rather than an auto margin on the action cluster, so a
wrapped Add filter / Saved / Clear / View row starts at the left edge instead
of presenting an empty leading column. Feature surfaces must not compensate
with page-local alignment or width overrides.
The Add filter select and adjacent action/popover triggers share the canonical
filterToolbarControlClass height. When the Add filter label is visually
hidden, its FormSelect must also omit the labelled group's outer padded/ring
shell so the native select does not become a double-framed, oversized control;
the select uses one stable compact width instead of expanding to its longest
hidden option. The visible Add filter value is a disabled, hidden
placeholder, not an actionable option repeated at the top of the opened native
list; only real filter values belong in that choice list.
Counts or orientation strips presented as part of a filtered resource table
must derive from that table's canonical filtered collection as well. They must
not continue showing page-wide inventory totals after FilterBar state, saved
views, or search has narrowed the rows the operator can see. Filtered summary
strips should omit zero-value categories and their separators rather than
turning absent states into persistent visual noise.
Implicit "remember last filters" is intentionally not added — defaulting
to yesterday's filter state on a monitoring page hides real problems.
That same shared filter-toolbar boundary also owns controlled select continuity
when filter options materialize asynchronously. LabeledFilterSelect must keep
the caller-owned value visibly selected after option children arrive so
dashboard, recovery, and other canonical filter bars do not drop their active
selection until the operator reopens the control. The same primitive must keep
its <label for> association reactive when a route-owned filter swaps the
select id, label, and option set in place, so controls such as the workloads
node/K8s cluster filter remain accessible after mode changes.
That same boundary also owns live option propagation through shared page-control
composition. Callers such as storage and recovery must pass source/filter
option collections through reactive accessors instead of snapshot arrays when
those options depend on post-load unified-resource state, so the shared toolbar
can reconcile late-arriving options and preserved route selections without
requiring page-local reset hacks.
Shared default filter labels must also stay on the same primitive-level
contract. Generic All … option text should route through
frontend-modern/src/components/shared/filterOptionPresentation.ts, with
domain presentation helpers supplying the noun phrase, so storage, alerts,
recovery, settings, and future filter bars do not drift between title-case and
sentence-case local strings.
When those workspace tabs need an embedded control-bar treatment, they should
still stay on the one canonical frontend-modern/src/components/shared/Subtabs.tsx
primitive and reuse the established shell, list, and button class pattern
already proven on owning surfaces like operations rather than introducing new
variant APIs on the primitive. When that rail overflows on phone widths,
Subtabs owns visible, accessible edge-scroll controls and keeps them in sync
with native scrolling and rail resize; callers must not add drawer-local arrow
overlays or leave clipped tab labels as the only overflow cue. Selection
changes reveal the active tab by moving only that horizontal rail through the
shared rail-visibility controller; Subtabs must not use scrollIntoView,
which can also move page and drawer ancestors vertically.
The search-input enhancement surfaces now follow that same owner split.
frontend-modern/src/components/shared/SearchInputEnhancements.tsx stays the
render shell, frontend-modern/src/components/shared/useSearchInputEnhancements.ts
owns search-history persistence, menu-open lifecycle, blur commit policy, and
tips/history interaction runtime, and
frontend-modern/src/components/shared/searchInputEnhancementsModel.ts owns
history-toggle copy plus history-menu button and row class policy. Future
search-input-enhancement work should extend those owners instead of pushing
history copy or menu presentation policy back into the shell.
Search-history menus must remain full-width on narrow search surfaces while
using a bounded desktop width aligned to the search field's leading edge; a
full-page or full-toolbar search field must not turn the history popover into a
screen-wide overlay that obscures unrelated controls.
The shared search tips popover now follows that same owner split.
frontend-modern/src/components/shared/SearchTipsPopover.tsx stays the render
shell, frontend-modern/src/components/shared/useSearchTipsPopoverState.ts
owns open-state, pointer/focus continuity, and outside-click/Escape listener
runtime, and frontend-modern/src/components/shared/searchTipsPopoverModel.ts
owns trigger variant, label/id defaults, hover policy, and trigger/popover
class selection. Future search-tips work should extend those owners instead of
pushing listener lifecycle or trigger policy back into the shared shell.
Canonical customer disclosures inside shared shells route through
frontend-modern/src/utils/docsLinks.ts, so settings privacy links resolve to
shipped /docs/... assets instead of hard-coded GitHub main URLs that can
drift from the running build.
The pre-authenticated Login shell uses that same canonical docs-link boundary
for self-hosted access recovery. TROUBLESHOOTING_DOC_URL routes the always-
visible Can’t sign in? action to the shipped Troubleshooting guide, including
when local login is hidden behind SSO, and the /docs/... public-route contract
keeps that destination readable without an authenticated session. The login
surface must remain guidance-only: it may not expose secrets, imply an email
reset flow, or create a browser-side authentication bypass.
The shared summary strip primitives now follow that same owner split.
frontend-modern/src/components/shared/SummaryPanel.tsx and
frontend-modern/src/components/shared/SummaryMetricCard.tsx stay the render
shells for summary-frame spacing and card density, while monitoring surfaces
such as recovery, infrastructure, workloads, and storage only choose from the
owned shared density modes instead of forking summary spacing with feature-
local padding hacks. Future summary-density work should extend those shared
primitives rather than hard-coding compact card chrome inside one surface.
The shared tooltip now follows that same owner split.
frontend-modern/src/components/shared/Tooltip.tsx stays the render shell and
singleton API boundary, frontend-modern/src/components/shared/useTooltipState.ts
owns tooltip positioning lifecycle, RAF scheduling, and singleton visibility
state, and frontend-modern/src/components/shared/tooltipModel.ts owns tooltip
sanitization plus viewport-clamped positioning math. Future tooltip work should
extend those owners instead of pushing singleton state, DOM measurement, or
sanitization logic back into the shared shell. Shared portal-mounted tooltip
shells such as frontend-modern/src/components/shared/TooltipPortal.tsx must
use the same semantic surface tokens as the canonical tooltip instead of
introducing light-mode-inverted palettes.
That same tooltip owner now also holds the CSP-safe portal contract: shared
tooltip shells must render through SVG/attribute positioning and viewport-
clamped layout helpers rather than fixed inline left/top style attributes.
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.
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
may synthesize mouse-enter before the row click that should open canonical
details. Desktop pointer and keyboard interaction remain unchanged; mobile row
activation must continue directly to its drawer or other primary action.
The shared collapsible search input now follows that same owner split.
frontend-modern/src/components/shared/CollapsibleSearchInput.tsx stays the
render shell, frontend-modern/src/components/shared/useCollapsibleSearchInputState.ts
owns expand/collapse state, focus choreography, and type-to-search handoff, and
frontend-modern/src/components/shared/collapsibleSearchInputModel.ts owns
trigger-label, expanded-visibility, and full-width layout policy. Future
collapsible-search work should extend those owners instead of pushing
expand/collapse runtime or layout rules back into the shared shell.
The shared pulse data grid now follows that same owner split.
frontend-modern/src/components/shared/PulseDataGrid.tsx stays the render
shell, frontend-modern/src/components/shared/usePulseDataGridState.ts owns
breakpoint-driven min-width selection and stable-row reconciliation, and
frontend-modern/src/components/shared/pulseDataGridModel.ts owns alignment
class policy plus interactive-target row-click protection. Future pulse-data-
grid work should extend those owners instead of pushing breakpoint lifecycle or
interaction policy back into the shared shell.
The audit log settings surface now follows that same owner split.
frontend-modern/src/components/Settings/AuditLogPanel.tsx stays the canonical
SettingsPanel shell, while
frontend-modern/src/components/Settings/useAuditLogPanelState.ts owns the
license/paywall lifecycle, persisted filters, verification flow, and audit-log
fetch orchestration. The shell must not re-accumulate localStorage or API
runtime logic inline. Audit-log filter option labels must come from
frontend-modern/src/utils/auditLogPresentation.ts and the shared filter-option
label primitive instead of hard-coded title-case strings in the settings shell.
When the shared runtime-capabilities store reports paid_runtime_required for
audit_logging, Settings navigation must keep the Audit Log surface reachable
and the panel must render paid-runtime-required copy with the private Pulse Pro
download action. The runtime mismatch is not a plan upsell and must not be
hidden by ordinary missing-feature navigation filtering.
Audit-log fetch failures must preserve the structured backend error object
through apiErrorFromResponse and render customer-facing copy from
frontend-modern/src/utils/auditLogPresentation.ts; the settings shell may
own refresh and pagination state, but it must not show raw Internal Server Error strings or unbounded page sizes as local hook behavior.
Audit-log page loads are latest-request-wins. Changing the page size atomically
resets the offset and starts one replacement request, and any superseded
request is aborted or ignored so an older response cannot overwrite the new
page. Page size is a durable table-presentation preference and belongs inside
the shared FilterBar.viewOptions popover rather than as a permanent filter
control. A failed load clears previously rendered events and totals, and a
successful payload must contain an event array rather than treating null or
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
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 ... labels.
The audit webhook settings surface now follows that same owner split.
frontend-modern/src/components/Settings/AuditWebhookPanel.tsx stays the
canonical SettingsPanel shell, while
frontend-modern/src/components/Settings/useAuditWebhookPanelState.ts owns the
license/paywall lifecycle, webhook fetch/save flow, validation, paywall
tracking, and hidden-upgrade copy posture. The shell must not re-accumulate API
calls or paywall tracking inline.
The same paid-runtime-required route applies to Audit Webhooks: missing
audit_logging caused by a community runtime must keep the panel reachable,
hide normal upgrade-plan prompts, and present the private Pulse Pro runtime
download action instead of describing the feature as an unlicensed Pro upsell.
The diagnostics settings surface now follows that same owner split.
frontend-modern/src/components/Settings/DiagnosticsPanel.tsx stays the
top-level diagnostics shell, while
frontend-modern/src/components/Settings/useDiagnosticsPanelState.ts,
frontend-modern/src/components/Settings/DiagnosticsResultsPanel.tsx,
frontend-modern/src/components/Settings/diagnosticsModel.ts, and
frontend-modern/src/utils/diagnosticsPresentation.ts own the diagnostics
run/export lifecycle, results rendering, sanitization/model helpers, and
customer-facing diagnostics copy. The shell must not re-accumulate inline API
calls, export-download plumbing, diagnostics-card composition, or diagnostics
surface copy.
PBS diagnostics status badges render the backend's canonical monitored
connected/state result. The one-off diagnostics request remains a nested
probe result; when the probe and monitor disagree, the row must name both
states instead of turning a successful live check into a green monitored badge
or hiding recovery evidence behind an undifferentiated failure.
That same diagnostics owner split also keeps maintainer analytics out of the
customer diagnostics surface. DiagnosticsResultsPanel.tsx,
diagnosticsModel.ts, and the diagnostics export path must not render or
preserve commercial funnel, sales funnel, pricing/checkout conversion, or
infrastructure onboarding telemetry from /api/diagnostics; those signals
belong in admin-owned metrics surfaces instead of Settings support UI.
frontend-modern/scripts/settings-diagnostics-boundary-audit.mjs, called by
the canonical frontend audit runner, enforces that boundary by failing if the
diagnostics API, diagnostics results panel, or diagnostics payload model
reintroduce those analytics fields outside the defensive strip helper, and by
failing if production customer frontend source reintroduces the retired
commercial/onboarding analytics wrappers or /api/upgrade-metrics/events
calls. That same audit also fails if the retired conversion/funnel or metering
packages return under the compiled product licensing path, because Settings
support diagnostics cannot be the only customer-facing guard if the normal
product binary still carries the maintainer analytics pipeline.
Diagnostics cards that summarize Docker and Podman agent coverage must use the
shared docker source-platform label from
frontend-modern/src/utils/sourcePlatforms.ts for their heading and body copy,
so diagnostics results stay aligned with the governed settings/source-platform
vocabulary instead of inventing a local runtime family label.
The settings shell registry now also treats extracted feature prop contracts as
canonical shell inputs instead of reaching back into feature panels for type
ownership. frontend-modern/src/components/Settings/useSettingsPanelRegistry.tsx
must consume the direct Proxmox panel contract through
frontend-modern/src/components/Settings/proxmoxSettingsModel.ts, so the
registry stays a shell/composition owner and does not depend on
ProxmoxSettingsPanel.tsx as though the panel still owned the runtime model.
The retired /operations route is unregistered rather than a compatibility
redirect. Diagnostics, reports, and logs belong to the shared Settings shell
instead of a bespoke page-local tab surface. Support-only navigation must
therefore route through the shared settings owners rather than rebuilding a
second route-level shell, and public demo posture must keep those support
entries hidden from the Settings navigation instead of reviving a standalone
operations page.
that are unavailable in demo mode.
The dashboard overview route and its feature-owned summary surfaces are
retired. Authenticated root entry now lands on the first visible
provider/runtime platform, so first-viewport estate orientation belongs to that
platform page plus the Add infrastructure flow rather than a separate dashboard
or legacy Infrastructure shell. Future overview or brief-style surfaces must
be governed as new product surfaces before they add route-level data
orchestration, section anchors, or Assistant prompt handoffs; they must not
restore frontend-modern/src/pages/Dashboard.tsx,
frontend-modern/src/features/dashboardOverview/, or deleted dashboard-only
presentation helpers as compatibility paths.
The primary navigation active-tab contract follows that retirement boundary:
retired or unknown routes such as /dashboard must not be coerced into the
nearest platform tab just because the authenticated shell has a provider-first
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.
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
shared primitive must defer to that local contract instead of silently forcing
the default separator treatment back into the rendered DOM.
That same shared table boundary now owns CSP-safe sizing for infrastructure
tables and metric bars. frontend-modern/src/components/Infrastructure/useUnifiedResourceTableState.ts
and frontend-modern/src/components/Infrastructure/unifiedResourceTableStateModel.ts
must express table layout and column sizing as shared class/attribute
presentation instead of inline style= maps, and
frontend-modern/src/components/shared/ProgressBar.tsx must render fill width
through DOM attributes rather than inline width styles. Infrastructure host and
service tables may still vary by breakpoint and column family, but they must do
so through the shared presentation owner instead of lane-local style objects
that break the public demo CSP.
That same shared-boundary rule applies to summary density. The shared compact
mode on SummaryPanel.tsx and SummaryMetricCard.tsx exists for genuinely
dense monitoring surfaces, but pages that are trying to align with the normal
Pulse monitoring scan path should stay on the default shared density instead of
using page-local compact overrides by habit.
That same recovery shell boundary now also owns one canonical top-level filter
controller in
frontend-modern/src/features/recovery/useRecoverySurfaceState.ts. Route-backed
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.
frontend-modern/src/utils/problemResourcePresentation.ts now also belongs to
that same dashboard overview boundary so the problem-resource severity contract
stays shared with ProblemResourcesTable.tsx instead of floating as an
unowned helper.
Problem-resource table readability belongs to that same owner. Repeated rows
may collapse only when they share the same governed display label, resource
type, and problem signal; the header count and Pulse Brief counts must continue
to represent the underlying affected resources, and grouped links must route to
the broad owning surface rather than inventing a synthetic resource target.
Problem Resources and Pulse Brief wording must not amplify generic
status-shaped names such as storage (offline) into first-viewport prose or
grouped-row sublabels; when the resource name is only a type plus status, the
surface should summarize the type-level issue in operator language instead of
repeating raw backend-shaped labels.
The retired dashboard action queue must not be reintroduced as a compact
Patrol or infrastructure issue panel. Patrol-owned runtime findings remain
governed by frontend-modern/src/utils/aiFindingPresentation.ts and their
own Patrol route/store surfaces; any future cross-surface issue queue needs a
new governed owner rather than reviving dashboard action-panel files.
Feature-owned alert shells under frontend-modern/src/features/alerts/ now
also treat shared action runtime as a first-class feature owner instead of
rebuilding it per surface. The overview shell must compose
frontend-modern/src/features/alerts/useAlertAcknowledgementState.ts for
acknowledge/restore behavior rather than keeping duplicate API and notification
logic inline in useAlertOverviewState.ts or a revived dashboard recent-alert
panel.
The same feature-owner rule now applies to the alert scheduling surface:
frontend-modern/src/features/alerts/tabs/ScheduleTab.tsx must remain the
schedule render shell, while
frontend-modern/src/features/alerts/useAlertScheduleState.ts owns schedule
reset/update policy and canonical default application. The tab should not
re-accumulate quiet-hours, cooldown, grouping, or escalation mutation logic
inline.
The thresholds editor now follows that same split more tightly:
frontend-modern/src/features/alerts/thresholds/hooks/useThresholdsTableState.ts
must stay the table-shell owner for route sync and local UI state, while
frontend-modern/src/features/alerts/thresholds/hooks/useThresholdsData.ts
stays the composition shell for threshold resource-family projectors,
frontend-modern/src/features/alerts/thresholds/hooks/useThresholdsRecoveryDefaultsState.ts
owns backup/snapshot default sanitization and factory-drift policy, and
frontend-modern/src/features/alerts/thresholds/thresholdsOverrideMutationModel.ts
owns pure override upsert/hysteresis/state-strip helpers,
frontend-modern/src/features/alerts/thresholds/hooks/useThresholdsOverrideMutations.ts
owns threshold-save and backup/snapshot override persistence, and
frontend-modern/src/features/alerts/thresholds/hooks/useThresholdsAvailabilityMutations.ts
owns availability-state policy and alert-removal side effects. The table-shell
hook should not re-accumulate raw override mutation logic,
recovery-threshold defaults policy, or resource-family projection engines
inline.
TrueNAS system threshold rows follow that same feature-owner split and the
canonical alert identity chain. useThresholdsPlatformData.ts must project
the current canonical resource ID as the writable storage ID, accept bounded
superseded/metric-target candidates for legacy readback, and retain the
projected row while editingId is active so WebSocket resource repolls,
reordering, or changing display telemetry cannot reset the input.
useThresholdsOverrideMutations.ts owns the blur commit: it removes every
bounded candidate, writes exactly one current-ID raw override, marks the
configuration dirty, and leaves the global Save Changes control to perform the
API persistence. Multiple same-hostname TrueNAS systems must stay independent
because configured connection identity, not display name or DMI serial, owns
the row. The regression boundary is
frontend-modern/src/features/alerts/thresholds/hooks/__tests__/truenasThresholdPersistence.test.tsx;
the global payload boundary is
frontend-modern/src/features/alerts/__tests__/useAlertsConfigurationState.test.tsx.
The updates settings surface now follows the same presentation-owner rule.
Source builds have no published release-update target. The shared update
store must discard a cached release offer when the current runtime reports
isSourceBuild or isDevelopment, even if its version string did not change.
The update panel labels this state Source build, disables release checks
and automatic updates, and omits release notes. UpdateInstallGuide suppresses
release install actions and generic Docker pull commands for source builds,
including stale cached offers. Docker source builds instead explain manual
image replacement. Stable and preview release flows retain their existing
checks and installation guidance. The update-store, install-guide, and
presentation tests own this cross-control contract.
frontend-modern/src/components/Settings/UpdatesSettingsPanel.tsx stays the
top-level settings shell, while
frontend-modern/src/components/Settings/UpdateInstallGuide.tsx,
frontend-modern/src/components/Settings/CopyCommandBlock.tsx, and
frontend-modern/src/components/Settings/updatesSettingsModel.ts plus
frontend-modern/src/utils/updatesPresentation.ts own the
deployment-specific install guide, copy-command block, and update-channel/install
model data plus customer-facing update status/action copy.
frontend-modern/src/components/Settings/UpdateHistorySection.tsx joins that
split as the presentation owner for the update history table and the
rollback confirmation dialog: the panel shell mounts it as a section and must
not inline history rows, rollback gating, or rollback confirmation copy
itself, and the section starts rollbacks through the shared
updateStore.rollbackUpdate action rather than its own POST path. The panel shell must
not rebuild copy-to-clipboard command cards, deployment instruction trees, or
update-surface wording inline. CopyCommandBlock must use the shared
copyToClipboard helper so install/update/agent snippets keep the same
Clipboard API fallback path and only report copied state after the shared copy
path succeeds.
The running published version must keep a direct Current release notes link
in this settings shell so suppressing or dismissing the one-time post-update
notice never makes the changelog undiscoverable. Development and source-build
identities must not render that link as if they named a published release.
The update verdict is honest about its age. The frontend serves the update
verdict from a 24-hour localStorage cache, so the panel's "Up to date" state
must render the age of the check it came from rather than reading as a live
comparison (#1601). frontend-modern/src/stores/updates.ts exposes
lastCheckedAt, the epoch milliseconds of the check backing the currently
displayed verdict, set on fresh checks, on cached-verdict reuse, and on the
cached fallback after a failed check.
frontend-modern/src/utils/updatesPresentation.ts owns the customer-facing
wording through getUpdateCheckedLabel ("Checked 3 hours ago", "Not checked
yet"); the panel shell must not inline that copy and hides the line for
source builds, where update checks are disabled. Regression boundary:
frontend-modern/src/components/Settings/__tests__/settingsArchitecture.test.ts.
The reporting operations surface now follows the same shell-state-model rule.
frontend-modern/src/components/Settings/ReportingPanel.tsx stays the
operations-panel shell, while
frontend-modern/src/components/Settings/useReportingPanelState.ts owns the
license/trial lifecycle and report generation flow,
frontend-modern/src/components/Settings/reportingPanelModel.ts plus
frontend-modern/src/utils/reportingResourceTypes.ts own the
request/range/filename model and reporting-type API mapping,
frontend-modern/src/components/Settings/ResourcePicker.tsx plus
frontend-modern/src/utils/reportableResourceTypes.ts own the reportable
resource selection, filter, sort, and empty-state contract, and
frontend-modern/src/utils/reportingPresentation.ts owns the user-facing
range/status copy. Consumers import @/utils/reportingResourceTypes directly;
the former one-line compatibility re-export under components/Settings/ was
removed as dead code once its last importer moved. Native Kubernetes inventory-only resource
types, including ReplicaSets, EndpointSlices, NetworkPolicies, StorageClasses,
ConfigMaps, Secrets, ServiceAccounts, Roles, ClusterRoles, RoleBindings,
ClusterRoleBindings, ResourceQuotas, LimitRanges, PodDisruptionBudgets, and
HorizontalPodAutoscalers, must map to the existing reporting k8s transport
token at this edge rather than widening the metric-report picker into
platform-object inventory. Kubernetes RBAC inventory (Roles, ClusterRoles,
RoleBindings, ClusterRoleBindings) joins the existing K8s inventory bucket on
that transport without introducing a separate reporting token: from the
reporting transport's point of view it is platform-object inventory the same
way ConfigMaps and ServiceAccounts already are, even though the rendered
Configuration tab surfaces it through RBAC-specific lifecycle/data-shape
columns inside KubernetesConfigTable. The shell must not
re-accumulate license
bootstrapping, inline report API requests, blob-download plumbing, or local
resource-type filter and reporting-token maps.
General settings segmented selectors for theme preference and temperature unit
must now also route through the shared FilterButtonGroup primitive instead of
maintaining local button-group styling forks inside
frontend-modern/src/components/Settings/GeneralSettingsPanel.tsx.
Reporting time-range/export selectors and General settings Proxmox VE polling
presets must now also route through the shared FilterButtonGroup prominent
variant instead of maintaining local blue segmented-control styling forks in
feature components.
That same shared FilterButtonGroup primitive must stay CSP-safe: touch-scroll
overflow behavior must come from canonical CSS classes rather than inline
style attributes so settings and reporting selectors do not reintroduce
browser console CSP violations under the release build policy.
Selectable settings cards for compact provider pickers and detail choice panels
must now route through the shared SelectionCardGroup primitive instead of
duplicating border-2 active-card styling in feature components.
Settings informational, warning, success, and danger callouts with icon-plus-copy
layouts must now route through the shared CalloutCard primitive instead of
maintaining feature-local colored bordered wrappers. The primitive owns the tone
palette and the scale="compact" density used by smaller settings notices, and
the settings-callout-card-shell shared-template registry rule requires current
settings consumers to compose it instead of reintroducing local panel shells.
Connection-editor status, feature-disabled, delete-error, and probe-result
notices are part of that same settings callout boundary: the editor and
credential slots own the source-specific lifecycle or API meaning, while
CalloutCard owns the warning/success/danger shell and compact density.
The settings-connection-editor-local-*-callout-shell pattern guards block
future connection-editor files from reintroducing amber, red, or rose local
notice shells.
Shared error-boundary fallbacks use the same boundary: the fallback owns error
copy and reset/reload handlers, while CalloutCard owns danger tone, spacing,
dark-mode styling, and alert layout instead of inline red panels or raw SVG
alert glyphs.
Update confirmation and progress modals use the same shared boundary: the
modal flow owns update state and copy, while CalloutCard, Button,
ActionIconButton, LoadingSpinner, and lucide icons own the colored notice,
command, icon-only close, and status indicator chrome.
Settings loading placeholders must route through the shared
SettingsLoadingSkeleton primitive instead of local animate-pulse blocks.
Feature panels may choose the loading shape and row counts, but the shared
primitive owns pulse animation, skeleton fill tokens, metric-card grids,
progress-card rows, table header/body shells, and labelled status semantics.
The settings-loading-skeleton-shell registry rule covers the current
organization, security overview, and resource data policy loading surfaces, the
settings-loading-state-shared-skeleton-required guard requires future
Settings *LoadingState files to compose that primitive, and the
settings-local-loading-skeleton-block-shell guard blocks local pulse skeleton
blocks from returning inside Settings components.
Settings external documentation text links must route through
ExternalTextLink, while button-styled external actions route through
ButtonLink/UpgradeButtonLink. Shared primitives own new-tab safety, rel
policy, link tone, compact action density, and focus styling; settings panels
must not hand-code raw <a target="_blank"> anchors for documentation links.
The settings-external-text-link-shell and
settings-external-text-link-local-anchor registry entries enforce that split,
and the Button registry owns the info variant for blue documentation CTAs.
Platform inline notices that sit inside platform pages but are not settings
callouts must route through the shared InlineNotice primitive. Platform owners
provide only the affected-resource copy, render predicate, and destination; the
shared primitive owns the dense warning/info/danger/success tone palette,
icon/content layout, and action-link chrome. The
platform-inline-notice-shell registry rule covers current outdated-agent and
outdated-sensor notices, and the
platform-inline-notice-local-amber-shell pattern guard blocks future
platformPage files from reintroducing page-local amber notice shells.
Alert incident-event filter containers, labels, and chips must now route
through the shared presentation helpers in
frontend-modern/src/utils/alertIncidentPresentation.ts instead of allowing
frontend-modern/src/pages/Alerts.tsx and
frontend-modern/src/features/alerts/OverviewTab.tsx to fork their own filter
button styling.
Alert incident acknowledged badges, event cards, and note-editor controls must
also route through frontend-modern/src/utils/alertIncidentPresentation.ts
instead of letting the alerts page and overview timeline maintain duplicate
inline incident-detail styling.
Alert incident meta-row and detail-text presentation must also route through
frontend-modern/src/utils/alertIncidentPresentation.ts instead of letting
the alerts page and overview timeline maintain duplicated inline incident
typography rules.
Alert incident timeline event card structure must also route through
frontend-modern/src/components/Alerts/IncidentTimelineEventCard.tsx so the
alerts page and overview timeline share one canonical event-card renderer
instead of reimplementing the same summary/detail/output block twice.
The full expanded alert incident detail panel and event-filter controls must
also route through frontend-modern/src/components/Alerts/IncidentTimelinePanel.tsx
and frontend-modern/src/components/Alerts/IncidentEventFilters.tsx rather
than rebuilding loading/error copy, filter controls, note-editor wiring, or
event-card composition separately inside the alerts page and overview tab.
Resource incident panel card and summary-row presentation must also route
through frontend-modern/src/utils/alertIncidentPresentation.ts instead of
maintaining page-local incident panel styling inside
frontend-modern/src/pages/Alerts.tsx.
The settings shell now also has an explicit five-way ownership split.
frontend-modern/src/components/Settings/useDiscoverySettingsState.ts owns the
shared discovery draft and subnet-validation state,
frontend-modern/src/components/Settings/useSettingsInfrastructurePanelProps.ts
owns infrastructure workspace prop assembly and resource-derived infrastructure
read-model shaping for the shell,
frontend-modern/src/components/Settings/settingsNavigationModel.ts owns
settings tab identity, canonical route derivation, route eligibility, and
retired infrastructure/workloads alias rejection. settingsRouting.ts and
settingsTypes.ts remain thin compatibility re-export shims only, so external
consumers can bridge to the canonical owner without reintroducing a second
settings navigation model. settingsNavCatalog.ts owns settings navigation
metadata and item lookup, settingsNavVisibility.ts owns
feature/capability visibility and lock policy for settings navigation,
useSettingsNavigation.ts owns reactive URL sync and canonical tab-selection
state, SettingsDialogs.tsx owns shared settings modal composition,
including the route-owned billing focus contract where
/settings/system/billing/plan is the canonical settings-tab destination,
/settings/system/billing/usage is a same-tab child state, and legacy billing
base/hash links are compatibility inputs rather than primary runtime routes.
Infrastructure settings no longer has route-level platform-selection state:
/settings/infrastructure and /settings/infrastructure?add=<step> are the
only routeable Infrastructure settings entry points for platform/API and
agent-backed source setup. Agentless ping/TCP/HTTP checks are monitoring
availability settings at /settings/monitoring/availability, with
/settings/monitoring/availability?add=target as the route-owned add dialog.
Machine entry points must add targetKind=machine, while the focused
availability checks entry points for services and devices must add
targetKind=service, so deep links open the same owned dialog with the correct
bounded target kind already selected. That target kind only scopes the
availability form copy and payload; it must not make agentless reachability
targets eligible for the Machines table.
Former nested aliases such as /settings/infrastructure/install,
/settings/infrastructure/platforms/proxmox/pbs,
/settings/infrastructure/api/pve, and /settings/workloads/docker must fail
route eligibility instead of being normalized back into the Infrastructure
workspace.
That same settings access boundary must keep route eligibility separate from
sidebar visibility. Panel-owned feature gates such as Relay, Reporting, RBAC,
Audit Log, and Audit Webhooks may be hidden from the navigation on Community
installs, but their direct settings routes must stay routeable so the owning
panel can render its locked, non-flashing state instead of being bounced to the
default Infrastructure tab.
useSettingsShellState.ts owns shell-local sidebar/search/password-modal
state, and settingsTabSaveBehavior.ts owns settings tab save-behavior lookup,
frontend-modern/src/components/Settings/useSettingsSystemPanels.tsx owns
system panel prop assembly for general, network, updates, and recovery, and
frontend-modern/src/components/Settings/settingsPanelRegistryContext.tsx owns
registry context assembly for dispatchable settings tabs while
frontend-modern/src/components/Settings/settingsPanelRegistryLoaders.ts owns
the lazy settings panel loader table and route-to-panel import boundary, and
frontend-modern/src/components/Settings/useSettingsPanelRegistry.tsx owns the
final memoized registry composition only. frontend-modern/src/components/Settings/Settings.tsx
must stay a shell that wires those owners together instead of re-accumulating
infrastructure workspace props, registry context maps, system panel prop maps,
lazy loader definitions, or discovery draft state inline.
That same settings-routing contract now also owns the Support group for
Diagnostics & Health, Data & Reports, and System Logs: the navigation
model must reject old /settings/operations/* settings paths instead of
normalizing them into /settings/support/*, and the top-level /operations/*
browser path must stay unregistered. The catalog plus visibility owners must
still treat support surfaces as Settings-native pages rather than as a second
top-level utility destination.
The resource incident panel's collapsed activity summary is now part of that
same shared primitive boundary. Event-type count chips, visible-event copy,
and the summary-ordering helper in frontend-modern/src/features/alerts/types.ts
must stay shared across alert timeline surfaces instead of rebuilding
page-local event summaries or bespoke incident-card markup.
Feature-owned route surfaces under frontend-modern/src/features/ must also
keep their shell/runtime split explicit once a subsystem grows real transport
or polling lifecycle. The Patrol feature is the current reference shape:
frontend-modern/src/features/patrol/PatrolIntelligenceSurface.tsx stays the
feature shell, frontend-modern/src/features/patrol/usePatrolIntelligenceState.ts
owns the runtime state machine, frontend-modern/src/features/patrol/patrolInvestigationContextModel.ts
owns the pure investigation-context summary and Patrol-to-Assistant operator
briefing derivation, including the rule that active findings, pending
approvals, and governed action references outrank secondary coverage caveats
when building the Assistant prompt, action label, and safety note,
frontend-modern/src/stores/aiIntelligenceSummaryModel.ts owns canonical AI
summary normalization at the shared store boundary, and the Patrol-owned
header/banner/summary/workspace section files under
frontend-modern/src/features/patrol/ own the heavy render surfaces. Shared
shell governance should reinforce that pattern instead of letting feature render
surfaces re-accumulate API and timer orchestration inline.
That same route-owned page-health rule now also applies to Patrol: a feature
surface may not present a green or all-clear primary summary when the owning
runtime contract says the page is blocked or unavailable, even if the last
successful snapshot was healthy.
That same rule also applies to compact Patrol summary fragments inside the
feature surface: count-only strips or metric cards must not emit No issues found or other reassuring copy when the owning overall-health summary is
degraded or not fully verified.
That same summary shell should also surface verification scope from the
owning run-history contract. Operators should be able to see, inside the same
summary surface, whether Patrol recently completed a full verification pass or
whether recent activity was limited to scoped/erroring patrol runs.
When the same governed run-history contract shows a recent full patrol plus
same-day scoped follow-up work, that summary shell should also carry a compact
activity-mix explanation rather than forcing operators to infer why Patrol
looked busy from a second competing status band.
That explanation belongs on the verification surface itself when operators are
reconciling Recently verified copy against same-day scoped Patrol bursts; the
supporting activity context may complement the readout, but it is not
sufficient as the only explanation path.
That same shell rule also owns Patrol recency labels. Shared Patrol header and
status-shell surfaces must keep Last full patrol tied only to the full-sweep
transport fact and use Last activity for scoped or verification work instead
of collapsing both timestamps back into a generic Last run label. Coverage
phrases on those recency surfaces must come from the Patrol recency presenter
instead of hardcoding verified wording in the shell.
That same run-history ownership applies to assessment caveats: Patrol summary
shells should not present Recent coverage is incomplete when the shared
recency/verification helpers already prove a successful full patrol with
non-zero resource coverage.
That same Patrol shell ownership includes refresh affordance state:
frontend-modern/src/features/patrol/usePatrolIntelligenceState.ts must keep
operator refresh controls generation-aware, timeout-bounded, and separate from
background polling state, so a slow supporting intelligence read cannot make the
shared Patrol header Refresh Patrol action spin indefinitely or stay disabled
while Patrol findings and status remain visible.
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
background-only policy pauses as default operator guidance; run-trigger details
belong in run history or explicit secondary context.
That same Patrol-facing primitive vocabulary must stay product-first. Patrol
summary actions, runtime banners, run-history runtime-failure actions,
runtime-finding actions, circuit-breaker copy, and Patrol control/provider controls
may point at the shared provider settings route or model catalog, but they
should describe those controls as Patrol/provider surfaces through
frontend-modern/src/utils/patrolRuntimeActions.ts rather than falling back to
generic AI Settings, AI Model, or AI circuit breaker copy inside the
Patrol shell itself.
That same product-first naming rule also applies to Pulse Intelligence settings:
frontend-modern/src/components/Settings/AISettings.tsx,
frontend-modern/src/components/Settings/settingsHeaderMeta.ts,
frontend-modern/src/components/Settings/settingsNavCatalog.ts,
frontend-modern/src/components/Settings/useAISettingsState.ts, and
frontend-modern/src/utils/aiSettingsPresentation.ts must present the provider
surface to operators under the Pulse Intelligence settings group as
Provider & Models provider/model configuration rather than as a generic
AI Services shell. The canonical browser route for that surface is
/settings/pulse-intelligence/provider; legacy /settings/system-ai links may
remain routeable compatibility aliases, but new navigation, setup, Assistant,
and Patrol repair CTAs must emit the Pulse Intelligence route.
On the main Patrol page, though, governed activity context belongs inside
frontend-modern/src/features/patrol/PatrolIntelligenceWorkspace.tsx as current
work, selected run history, or explicit details. Do not reintroduce a
parallel page-level status strip above the current-work workspace.
That same composition rule applies to the workspace: the default path should
move directly into findings and run history instead of repeating runtime
context through a second pre-tab status strip.
Details follows that same composition rule. Recent changes,
learned correlations, and policy coverage belong behind an explicitly secondary
supporting-context affordance that only appears when Patrol has active findings
or a selected run that needs explanation; healthy fully verified Patrol states
and degraded summary health by themselves must not advertise that supporting
evidence as a peer workflow. The default workspace may show the compact
Details control, but the full panel must render only after the operator
opens it. When that disclosure expands, the workspace must explicitly label the
selected finding or run as Patrol's record and frame the supporting cards as
explanatory context rather than as a fresh Patrol result or raw evidence
console.
Selected-run history should also suppress generic findings filter chrome and
read as a Patrol run record. Missing legacy finding_ids remains an internal
fail-closed scoping condition, but the visible caveat should say the finding
record is unavailable rather than exposing snapshot/filter vocabulary.
Workspace section descriptions must use Patrol-owned mode presentation copy
that reflects the selected mode and lock state; they must not hardcode
an all-mode sentence that tells watch-only users Patrol can investigate, ask for
approval, or fix issues. The Patrol workspace must not add a generic Details
panel to explain learned correlations, recent changes, or policy buckets; those
signals may support Assistant and backend reasoning without becoming default
operator chrome. Setup-only Patrol runtime failures must use the Patrol-owned
Fix Patrol setup workspace title and setup description plus a dedicated setup
task with the direct provider-settings action, rather than presenting that state
as a normal Current issues infrastructure queue, and they must suppress
generic issue-row chips, expand chevrons, and Active / All / Resolved
filter chrome in that setup-only state. That setup-only workspace must also be
the only visible provider-repair CTA for that state: it should use the
Open Provider & Models action and suppress the readiness banner so setup does
not appear twice. Run history must stay hidden as a competing action until
Patrol can check infrastructure or an operator is already reviewing a specific
run record.
Shared primitive consumers that split status-dot tone and status-text tone must now keep both values routed through the same exported presentation helper. Feature cards such as RAID status may not call shadow local aliases that drift from the canonical shared class/variant helpers.
Alert resource display labels used by the thresholds editor and alerts page
must now route through the shared helper in
frontend-modern/src/features/alerts/helpers.ts instead of rebuilding
resource display-name fallback chains inline. Governed resources must preserve
their canonical policy-aware label across grouped node headers, docker host
grouping, and saved override rows rather than collapsing back to raw names or
friendly-name truncation.
Shared search inputs must now keep their forwarded keyboard, blur, and clear handlers as explicit callable functions instead of relying on loose Solid event-handler unions. Shared search primitives still need to accept the real input/button event targets, but direct invocation inside the primitive must stay type-safe so consumers do not reintroduce union-call regressions while adding history, shortcut, or trailing-control behavior.
Shared shared-shell primitives that expose semantic title or value-level
onChange props must now explicitly omit the conflicting DOM attribute names
from their inherited HTML props. CalloutCard, FilterSegmentedControl, and
Subtabs may still forward ordinary div attributes, but their canonical API
must preserve JSX element titles and value-callback handlers instead of
widening back to raw DOM attribute unions.
Shared entitlement/migration warning banners that live under
frontend-modern/src/components/shared/ must also keep their counted fleet
surface on the Pulse Unified Agent term. Shared primitive copy may describe
legacy/API-connected resources separately, but it may not regress the primary
banner label or CTA text back to host-agent product language.
The self-hosted commercial paywall copy on those shared warning surfaces is
now also explicitly locked to monitored systems rather than agents. When a
shared banner or shared settings shell would explain monitored-system plan
caps, the correct primitive decision is absence: monitored-system volume is not
a current paid-capacity surface. Shared headers and descriptions may use
monitored-system language for inventory grouping and support ledgers, but they
must not talk about monitored-system limits, cap pressure, plan capacity,
admission freezes, or upgrade actions. Future work must not recreate
MonitoredSystemLimitWarningBanner, its state hook, or banner-local
monitored-system copy strings.
Shared frontend label-formatting helpers now also have an explicit owner here.
frontend-modern/src/utils/textPresentation.ts is the canonical shared owner
for token humanization, identifier label formatting, title-casing, and
arrow-delimited label presentation used across AI, Patrol, Storage/Recovery,
and other feature surfaces. Feature contracts may depend on that helper, but
they should not re-home or fork those generic text-formatting rules into
feature-local utilities.
That same shared presentation boundary now also owns operator feedback and
shared table-label semantics. frontend-modern/src/components/Toast/Toast.tsx
stays the render shell for the global toast stack,
frontend-modern/src/utils/toast.ts owns the app-level trigger helper,
frontend-modern/src/utils/semanticTonePresentation.ts owns canonical toast
and diagnostics tone classes, frontend-modern/src/utils/emptyStatePresentation.ts
owns the shared empty-state tone styling consumed by EmptyState, and
frontend-modern/src/utils/typeColumnPresentation.ts owns the single
canonical type-column label used across dashboard and alert tables. Future
feedback, empty-state, or shared type-column work should extend those helpers
instead of reintroducing panel-local tone classes, app-local toast wiring, or
copy drift between tables.
First-session educational surfaces must also stay brief, flat, and model-led.
When Pulse needs to teach a user how a flow works, the primary on-screen
guidance should collapse to a few short descriptions of the real product
mental model instead of a logo wall, feature brochure, or verbose internal
mechanics dump. The runtime wizard itself now stays on the two-step
Welcome -> Security path, while the separate setup-completion preview owns
the brief three-step explanation: install the Unified Agent, get the first
Pulse resource, then layer on additional context.
The settings shell is now also a governed frontend primitive boundary.
frontend-modern/src/utils/settingsShellPresentation.ts now owns the
customer-facing settings-shell framing copy for navigation, search, loading,
and unsaved-change banners so SettingsPageShell.tsx stays a render shell
instead of re-accumulating product wording inline.
At phone widths that shell is a two-level preference workspace rather than a
compressed desktop card: the searchable grouped Settings index owns level one,
the sticky back/title bar owns level two, and content uses edge-to-edge shallow
section framing with 44-pixel interactive targets. Preference labels and their
current controls should remain in one scan row where they fit; verbose trust,
environment, and explanatory copy must be subordinated through a short summary,
bounded line clamp, or explicit detail/documentation action instead of consuming
the default viewport. Desktop retains the persistent sidebar, page description,
and roomier panel spacing. Future top-level Settings work must extend
SettingsPageShell.tsx and SettingsPanel.tsx rather than recreating a second
mobile shell or returning to nested desktop padding.
The alerts page shell now follows that same page-shell rule for feature tabs:
frontend-modern/src/pages/Alerts.tsx owns navigation and cross-surface
routing, while feature-owned tab surfaces such as
frontend-modern/src/features/alerts/tabs/DestinationsTab.tsx and
frontend-modern/src/features/alerts/tabs/HistoryTab.tsx plus
frontend-modern/src/features/alerts/tabs/ScheduleTab.tsx and
frontend-modern/src/features/alerts/tabs/ThresholdsTab.tsx own their
tab-local rendering and interaction logic. Future alert tab cleanup should
continue by extracting page-local tab blocks into feature modules rather than
expanding the top-level page file again, and history-table behavior or
thresholds-table adapter logic should stay feature-owned unless it graduates
into a shared primitive used by more than one alert surface.
Within that thresholds surface, frontend-modern/src/components/Alerts/ThresholdsTable.tsx
is now explicitly a shell consumer rather than the data or controller owner,
and the tab render owners live in
frontend-modern/src/components/Alerts/ThresholdsTableProxmoxTab.tsx,
frontend-modern/src/components/Alerts/ThresholdsTablePMGTab.tsx,
frontend-modern/src/components/Alerts/ThresholdsTableAgentsTab.tsx,
frontend-modern/src/components/Alerts/ThresholdsTableDockerTab.tsx,
frontend-modern/src/components/Alerts/ThresholdsTableKubernetesTab.tsx,
frontend-modern/src/components/Alerts/ThresholdsTableTrueNASTab.tsx,
frontend-modern/src/components/Alerts/ThresholdsTableVMwareTab.tsx, and
frontend-modern/src/components/Alerts/ThresholdsTablePBSTab.tsx.
frontend-modern/src/features/alerts/thresholds/hooks/useThresholdsTableState.ts
owns the platform-shaped thresholds sub-route contract:
/alerts/thresholds/proxmox, /alerts/thresholds/docker,
/alerts/thresholds/kubernetes, /alerts/thresholds/truenas,
/alerts/thresholds/vmware, /alerts/thresholds/pbs,
/alerts/thresholds/pmg, and /alerts/thresholds/systems. Legacy neutral
links such as /alerts/thresholds/infrastructure,
/alerts/thresholds/containers, and /alerts/thresholds/mail-gateway must
redirect to the matching platform-shaped route; legacy
/alerts/thresholds/agents links must continue to resolve to Systems.
Thresholds section state is also owned there and composed through the shared
collapsible-section pattern. Resource sections open collapsed by default on
desktop and narrow layouts, while persisted operator choices, Expand all, and
Collapse all stay authoritative. The thresholds shell must pair that compact
default with an immediately visible custom-override summary whose actions open
the selected section under the Custom-only filter, so inherited state remains
clear without forcing every table open. Metric editors on desktop, mobile, and
bulk surfaces use explicit On/Off controls and positive enabled-value bounds;
legacy values at or below zero still read as Off, while the backend disable
sentinel remains compatibility data rather than customer-facing input or copy.
The Proxmox tab is itself now a shell that composes
frontend-modern/src/components/Alerts/ThresholdsTableProxmoxNodesSection.tsx,
frontend-modern/src/components/Alerts/ThresholdsTableProxmoxPBSSection.tsx,
frontend-modern/src/components/Alerts/ThresholdsTableProxmoxGuestsSection.tsx,
frontend-modern/src/components/Alerts/ThresholdsTableProxmoxGuestFilteringSection.tsx,
frontend-modern/src/components/Alerts/ThresholdsTableProxmoxBackupsSection.tsx,
frontend-modern/src/components/Alerts/ThresholdsTableProxmoxSnapshotsSection.tsx,
and frontend-modern/src/components/Alerts/ThresholdsTableProxmoxStorageSection.tsx
using the shared contract in
frontend-modern/src/features/alerts/thresholds/thresholdsTableSectionProps.ts.
Future infrastructure-thresholds presentation changes should extend those section
surfaces rather than restoring mixed JSX ownership to
frontend-modern/src/components/Alerts/ThresholdsTableProxmoxTab.tsx.
The Proxmox threshold tab includes a separate Guest Filesystems section for
QEMU guest-agent mounts. Rows use the live per-filesystem alert resource ID for
status and delay actions, but carry explicit override candidates and a stable
override storage ID into the shared mutation owners. This split is required:
using the alert ID as the persistence key strands settings on VM node moves,
while using only the persistence ID breaks active-alert and intent-policy
linkage in the shared resource table.
The Docker tab now follows that same composition pattern through
frontend-modern/src/components/Alerts/ThresholdsTableDockerIgnoredPrefixesSection.tsx,
frontend-modern/src/components/Alerts/ThresholdsTableDockerServiceGapSection.tsx,
frontend-modern/src/components/Alerts/ThresholdsTableDockerHostsSection.tsx,
and frontend-modern/src/components/Alerts/ThresholdsTableDockerContainersSection.tsx.
Future Docker thresholds presentation changes should extend those section
surfaces rather than restoring mixed JSX ownership to
frontend-modern/src/components/Alerts/ThresholdsTableDockerTab.tsx.
The systems tab now follows that same composition pattern through
frontend-modern/src/components/Alerts/ThresholdsTableAgentsResourcesSection.tsx
and frontend-modern/src/components/Alerts/ThresholdsTableAgentDisksSection.tsx.
Future systems-thresholds presentation changes should extend those section
surfaces rather than restoring mixed JSX ownership to
frontend-modern/src/components/Alerts/ThresholdsTableAgentsTab.tsx.
The thresholds tab adapter contract now lives in
frontend-modern/src/features/alerts/thresholds/thresholdsTabModel.ts, so
frontend-modern/src/features/alerts/tabs/ThresholdsTab.tsx stays a thin shell
instead of carrying a duplicate table adapter contract inline. That adapter
must bridge function-valued selectors and mutation props into
frontend-modern/src/components/Alerts/ThresholdsTable.tsx explicitly; spread-
based table prop adapters are not allowed here because they can collapse
function props on the live Solid surface and break thresholds runtime state.
Canonical threshold row shaping now routes through
frontend-modern/src/features/alerts/thresholds/thresholdsResourceModel.ts
plus the family-owned feature hooks
frontend-modern/src/features/alerts/thresholds/hooks/useThresholdsHostData.ts,
frontend-modern/src/features/alerts/thresholds/hooks/useThresholdsDockerData.ts,
frontend-modern/src/features/alerts/thresholds/hooks/useThresholdsGuestData.ts,
frontend-modern/src/features/alerts/thresholds/hooks/useThresholdsInfrastructureData.ts,
with frontend-modern/src/features/alerts/thresholds/hooks/useThresholdsData.ts
limited to composing them. Thresholds-table controller state lives in
frontend-modern/src/features/alerts/thresholds/hooks/useThresholdsTableState.ts,
so future cleanup should extend those feature hooks or tab owners instead of
rebuilding resource normalization, tab render surfaces, or thresholds-table
runtime state inside the shell component.
The alerts page now also applies the same shell-versus-feature rule to
configuration orchestration. frontend-modern/src/pages/Alerts.tsx is the page
shell, while frontend-modern/src/features/alerts/AlertsConfigurationSurface.tsx
is the feature shell. The canonical runtime owner is now
frontend-modern/src/features/alerts/useAlertsConfigurationState.ts for alert
config transport and org-switch reload orchestration,
frontend-modern/src/features/alerts/useAlertsConfigurationSnapshotState.ts
for the default-backed mutable configuration snapshot plus apply/capture/reset
ownership,
frontend-modern/src/features/alerts/alertsConfigurationModel.ts for config
normalization, factory defaults, docker-gap validation, and payload
serialization, frontend-modern/src/features/alerts/alertOverridesModel.ts
for override normalization and resource-backed projection. That shared
feature-model boundary must also canonicalize legacy shared-storage override
keys and hashed storage resource ids onto the storage metrics target id before
thresholds rows are derived, so migrated Ceph/shared-datastore overrides and
Ceph pool overrides survive the feature-shell path instead of dropping out of
the live editor, and
frontend-modern/src/features/alerts/useAlertOverridesState.ts
for reactive override state and thresholds-facing resource selectors, and
frontend-modern/src/features/alerts/alertDestinationsModel.ts for
destination config normalization and payload shaping, and
frontend-modern/src/features/alerts/useAlertDestinationsState.ts for
notification destination reload and persistence orchestration.
Within that alerts configuration runtime, canonical container-runtime projection
now belongs to alertOverridesModel.ts,
useAlertOverridesState.ts, and useAlertsConfigurationState.ts. The
thresholds Containers workspace must treat API-backed app-container
parents such as TrueNAS as first-class Container Runtimes, while Docker-only
controls in ThresholdsTableDockerTab.tsx remain gated to real
docker-host resources instead of leaking onto platform-managed runtimes.
frontend-modern/src/features/alerts/useAlertWebhookDestinationsState.ts now
owns webhook runtime, and
the email and webhook destination forms compose the shared TagInput for
resource-tag routing. Their feature state owns tagFilter plus the all/any
mode, an empty filter presents global delivery, and webhook list cards render
the saved tags without creating a second tag-input primitive or destination-
local normalization contract.
frontend-modern/src/components/Alerts/ResourceTable.tsx now follows the same
shell rule: the shell only picks desktop vs mobile render ownership and bulk-edit
composition, while
frontend-modern/src/components/Alerts/AlertResourceTableDesktop.tsx,
frontend-modern/src/components/Alerts/AlertResourceTableMobile.tsx, and
frontend-modern/src/components/Alerts/AlertResourceGroupHeader.tsx own the
render-heavy table/card/group-header surfaces. Shared runtime state remains in
frontend-modern/src/components/Alerts/useAlertResourceTableState.ts, shared row
rendering remains in
frontend-modern/src/components/Alerts/AlertResourceTableRow.tsx, and shared
metric normalization remains in
frontend-modern/src/components/Alerts/alertResourceTableModel.ts.
Desktop rows and mobile resource cards also share one alert-delay handoff:
their timer action emits the canonical resource id plus the first supported
CPU, memory, or disk signal, while ThresholdsTab.tsx owns selection state and
AlertIntentPolicyPanel.tsx owns expansion, scrolling, field inheritance, and
API persistence. The action remains available for non-threshold-editable rows
and falls back to state.offline when no supported metric exists. New
platform threshold sections must forward this shared handoff rather than add a
platform-local delay editor or a second intent-policy transport.
frontend-modern/src/features/alerts/useAlertDestinationsTabState.ts now owns
destination test actions, retry orchestration, and delivery-health loading while
frontend-modern/src/features/alerts/tabs/DestinationsTab.tsx stays the
render shell and should compose the dedicated email, Apprise, webhook, and
load/error section owners plus
frontend-modern/src/features/alerts/AlertDeliveryHealthCard.tsx instead of
carrying those panels inline. That card is a route-owned danger surface, not a
global success badge: degraded retained terminal delivery state and unavailable
health reads remain visible, while healthy and retry-pending state add no
banner. Its operator copy stays centralized in
frontend-modern/src/utils/alertDestinationsPresentation.ts. Future
cleanup should extend the transport hook, config model, override hook, or
destinations runtime hook based on the true owner, not move config control
flow back into the top-level page shell.
The alert email provider picker now also follows the shell/runtime split:
frontend-modern/src/components/Alerts/useEmailProviderSelectState.ts owns
provider-catalog loading and provider-default application, while
frontend-modern/src/components/Alerts/EmailProviderSelect.tsx stays the
render shell and should not re-accumulate NotificationsAPI.getEmailProviders
or a second local email-config contract inline.
The alert scheduling surface now follows the same shell-versus-section split:
frontend-modern/src/features/alerts/tabs/ScheduleTab.tsx should compose the
dedicated quiet-hours, cooldown, grouping, recovery, escalation, and summary
section owners while frontend-modern/src/features/alerts/useAlertScheduleState.ts
remains the canonical runtime owner.
The same rule now also covers cross-tab incident timelines: the shared runtime
owner is frontend-modern/src/features/alerts/useAlertIncidentTimelineState.ts,
while frontend-modern/src/features/alerts/OverviewTab.tsx and
frontend-modern/src/features/alerts/tabs/HistoryTab.tsx stay focused on
surface composition. Future incident timeline fetch, note-save, or expansion
control flow should extend that feature hook rather than forking back into
either tab surface.
Every surface invoking that owner must pass the canonical alert identifier and
the occurrence start time as distinct arguments; a row key is local UI state,
not a substitute for either API identity field.
The shared IncidentTimelinePanel consumes loading, error, timeline, draft,
and save state as accessors so an asynchronous result remains reactive across
Overview, desktop History, and mobile History instead of freezing the values
present when the panel first expands.
Overview alert runtime now follows that same shell-versus-runtime split. The
shell stays in frontend-modern/src/features/alerts/OverviewTab.tsx, while
frontend-modern/src/features/alerts/useAlertOverviewState.ts owns derived
alert stats, filtered ordering, and single/bulk acknowledge runtime behavior.
Future overview control flow should extend that hook rather than restoring
action timers or acknowledge mutations to the tab shell.
Render-heavy overview ownership now lives in
frontend-modern/src/features/alerts/AlertOverviewStatsCards.tsx,
frontend-modern/src/features/alerts/AlertOverviewActiveAlertsSection.tsx,
and frontend-modern/src/features/alerts/AlertOverviewAlertCard.tsx, so
future card-list or timeline-card presentation work should extend those
surfaces rather than expanding frontend-modern/src/features/alerts/OverviewTab.tsx
back into a mixed shell.
Alert history runtime now follows that same pattern. The shell stays in
frontend-modern/src/features/alerts/tabs/HistoryTab.tsx, while
frontend-modern/src/features/alerts/useAlertHistoryState.ts owns history
fetch, persistent filters, history-clear behavior, and composition of the
derived history owners. Resource-incident panel runtime now lives in
frontend-modern/src/features/alerts/useAlertResourceIncidentsState.ts, while
frontend-modern/src/features/alerts/alertHistoryModel.ts owns grouped/trend
derivation and the bucket/range analytics contract. The render-heavy surfaces
now route through
frontend-modern/src/features/alerts/AlertHistoryFrequencyCard.tsx,
frontend-modern/src/features/alerts/AlertHistoryFiltersCard.tsx,
frontend-modern/src/features/alerts/AlertResourceIncidentsPanel.tsx,
frontend-modern/src/features/alerts/AlertHistoryTableSection.tsx,
frontend-modern/src/features/alerts/AlertHistoryTableGroupRow.tsx,
frontend-modern/src/features/alerts/AlertHistoryTableAlertRow.tsx, and
frontend-modern/src/features/alerts/AlertHistoryAdministrationCard.tsx.
Future alert-history control flow should extend the hook, pure history analytics
should extend the model, and section rendering should extend those owners
rather than rebuilding any of those concerns in the tab shell.
The resource-resolution handoff into the resource-incident panel now belongs to
the history state rather than the tab shell.
frontend-modern/src/features/alerts/useAlertHistoryState.ts re-exposes the
getResource resolver it is already given, and
frontend-modern/src/features/alerts/AlertResourceIncidentsPanel.tsx reads it
from there. The panel mounts inside the history row that opened it rather than
beside the tab's other cards, so a prop chain from
frontend-modern/src/features/alerts/tabs/HistoryTab.tsx would have to thread
through the table section, the group row, the alert row, and the mobile list to
reach it. The tab shell itself should still only react to the current
alertData() contract rather than reviving deleted history-state aliases such
as filteredAlerts(). The
panel may render compact route chips, but it must stay on shared route helpers
and feature-owned composition instead of growing provider-local routing logic
or another page-local resource lookup path.
Top-level settings surfaces must route through Settings.tsx,
SettingsPageShell.tsx, and
frontend-modern/src/components/shared/SettingsPanel.tsx instead of
reintroducing bespoke outer page headers or one-off top-level panel framing.
The shell metadata driving those surfaces is part of the same boundary as
well: frontend-modern/src/components/Settings/settingsHeaderMeta.ts and
representative top-level panels such as
frontend-modern/src/components/Settings/APIAccessPanel.tsx,
frontend-modern/src/components/Settings/AISettings.tsx,
frontend-modern/src/components/Settings/AIModelSelectionSection.tsx,
frontend-modern/src/components/Settings/AIRuntimeControlsSection.tsx,
frontend-modern/src/components/Settings/AIChatMaintenanceSection.tsx,
frontend-modern/src/components/Settings/AISettingsStatusAndActions.tsx,
frontend-modern/src/components/Settings/AIProviderConfigurationSection.tsx,
frontend-modern/src/components/Settings/AISettingsDialogs.tsx, and
frontend-modern/src/components/Settings/aiSettingsModel.ts now also define
the canonical AI settings runtime boundary. AISettings.tsx is the shell,
frontend-modern/src/components/Settings/useAISettingsState.ts owns the
runtime lifecycle and persistence flow, model/provider setup now routes
through AIModelSelectionSection.tsx, discovery, budget, timeout, and
permission controls route through AIRuntimeControlsSection.tsx, chat
maintenance routes through AIChatMaintenanceSection.tsx, and readiness plus
save/test actions route through AISettingsStatusAndActions.tsx.
AISettingsStatusAndActions.tsx may expose provider connection status and
test actions only for the Provider & Models page; section pages reuse the save
bar without making Patrol, Assistant, or Discovery look like provider setup
screens. Future AI settings work must extend those section owners instead of
re-inlining large runtime subsections into the shell.
Provider-specific settings fields inside
AIProviderConfigurationSection.tsx must remain model-driven through
aiSettingsModel.ts extraFields, including Ollama keep_alive, so the
shared provider panel owns framing, labels, help affordances, helper copy, and
form binding instead of adding provider-local bespoke controls.
That same AI settings boundary now also owns
frontend-modern/src/utils/aiSettingsPresentation.ts, so shared loading,
empty, OAuth, action/error, shell-description, and workload-discovery copy
for the settings shell stays on one governed helper instead of drifting back
into section-local strings.
frontend-modern/src/components/Settings/AuditLogPanel.tsx,
frontend-modern/src/components/Settings/AuditWebhookPanel.tsx,
frontend-modern/src/components/Settings/GeneralSettingsPanel.tsx,
frontend-modern/src/components/Settings/NetworkSettingsPanel.tsx,
frontend-modern/src/components/Settings/NetworkBoundarySettingsSection.tsx,
frontend-modern/src/components/Settings/networkSettingsModel.ts,
frontend-modern/src/components/Settings/SecurityAuthPanel.tsx,
frontend-modern/src/components/Settings/SecurityOverviewPanel.tsx,
frontend-modern/src/components/Settings/RecoverySettingsPanel.tsx,
frontend-modern/src/components/Settings/SSOProvidersPanel.tsx,
frontend-modern/src/components/Settings/useSSOProvidersState.ts, and
frontend-modern/src/components/Settings/ssoProvidersModel.ts now also define
the canonical SSO provider settings runtime boundary: SSOProvidersPanel.tsx
is the shell, useSSOProvidersState.ts owns the reactive/API lifecycle, and
ssoProvidersModel.ts owns provider-form normalization and payload building.
That boundary must keep SAML creation on the same first-class action path as
OIDC. SSOProvidersPanel.tsx may show read-only state from settings
capabilities, but it must not render a self-hosted Pro upsell, UpgradeLink,
or advanced_sso feature probe before opening the SAML provider modal.
useSSOProvidersState.ts must treat provider type as form state only; SSO
entitlement truth belongs to the backend/runtime capability contract, where
OIDC, SAML, and multi-provider SSO are Community-tier capabilities.
The group-to-role mapping form must retain IdP group names with embedded
spaces through provider detail, edit, payload, and reload. Its entries are
comma- or newline-delimited group=roleId pairs; whitespace within a group
name is not an entry separator. Keep the existing whitespace parsing for
OIDC scopes and the other allowed lists separate from mapping parsing.
ssoProvidersModel.test.ts and SSOProvidersPanel.test.tsx pin this round
trip, and browser verification must inspect the saved mapping in the desktop
and narrow edit dialog.
frontend-modern/src/components/Settings/UpdatesSettingsPanel.tsx must keep
page-shell titles, descriptions, and lead panel framing aligned instead of
letting navigation/header labels drift away from the actual settings surface.
The self-hosted Pulse Pro settings navigation item and route header metadata
for frontend-modern/src/components/Settings/settingsNavCatalog.ts and
frontend-modern/src/components/Settings/settingsHeaderMeta.ts are part of
that same shell boundary as
frontend-modern/src/components/Settings/ProLicensePanel.tsx and the shared
settings billing presentation owner in
frontend-modern/src/components/Settings/selfHostedBillingPresentation.ts;
the system-billing navigation label, header title/description, and billing
shell framing must all route through SELF_HOSTED_PRO_BILLING_PRESENTATION
instead of drifting independently. The owned split is now explicit: the
navigation label comes from navLabel, while the route header and billing
shell reuse shellTitle plus shellDescription, so the settings IA and page
shell stay aligned on Plans & Billing without reintroducing local label drift.
That same settings-shell framing boundary also covers adjacent top-level
settings references to the self-hosted commercial surface. When
InfrastructureWorkspace.tsx or other settings-shell surfaces point operators
toward Plans & Billing for billing, license status, Patrol mode, or paid feature access, they
must reuse the shared referral copy from
SELF_HOSTED_PRO_BILLING_PRESENTATION rather than drafting local “go there
for billing” variants.
That same shared presentation owner now also carries the entitlement-first
commercial summary contract for self-hosted settings. The top-level navigation
entry stays product-IA owned through navLabel (Plans & Billing), while the
page header and shell title stay owned through shellTitle
(Plans & Billing), and the billing shell must foreground the active plan
name plus available capabilities before secondary billing or recovery detail.
Paid upgrades should be able to confirm “Current plan: Pulse Pro” immediately
after activation without hunting through generic billing language or a second
page-local summary card model.
That same shell boundary also has to stay safe for hosted tenant bundles.
Settings-shell framing copy for self-hosted billing must route through
selfHostedBillingPresentation.ts, with settingsNavCatalog.ts,
settingsHeaderMeta.ts, and adjacent hosted settings shells consuming that
settings-owned adapter instead of importing generic commercial presentation
helpers in ways that can reintroduce top-level bundle-init cycles.
frontend-modern/src/components/Settings/NetworkSettingsPanel.tsx is now a
shell only for network-boundary controls.
frontend-modern/src/components/Settings/NetworkBoundarySettingsSection.tsx
owns the public URL, CORS, embedding, and webhook-boundary UI, while the
editable discovery configuration entry point is owned by the infrastructure
workspace instead of the System/Network route. Shared prop contracts for the
network-boundary surface must extend
frontend-modern/src/components/Settings/networkSettingsModel.ts instead of
re-expanding the shell or reintroducing page-local section types.
The Public URL control must describe both customer alert links and copied
agent install/update command targets, recommend the externally reachable HTTPS
domain used through a reverse proxy, and present request auto-detection as a
fallback rather than implying the field affects notifications alone. The
settings architecture proof pins that copy, and the responsive browser receipt
proves the label and helper text remain readable without horizontal overflow.
For hosted runtimes this is also a fail-closed artifact contract: the backend
accepts only a canonically valid authoritative agent-connect URL or explicit
Public URL and does not substitute auto-detection, direct Host, or forwarded
headers. The same backend rule applies to the diagnostics Docker/Podman
migration artifact before its token exists. Existing setup/install and
diagnostics error presentation must therefore retain the backend failure and
offer configuration recovery rather than synthesizing a copyable command or
download from local form state. No frontend runtime file changes in this
slice; registered-route proof lives in
TestContract_HostedInstallerOriginsFailClosedAtRouter and
TestContract_HostedDiagnosticsDockerPrepareTokenValidatesOriginBeforeMutation,
while the existing settings architecture proof continues to own the Public
URL guidance.
The same backend boundary now covers returning-user and Stripe post-checkout
magic links before their one-time token is persisted. The public request UI
contract remains unchanged: unavailable hosted URL configuration produces the
same generic accepted response for registered and unknown email and exposes no
configuration diagnostic or account-existence signal, while checkout simply
skips its optional sign-in delivery. No frontend runtime file changes in this
slice; the API proofs are
TestContract_HostedMagicLinkRequestValidatesOriginBeforeMutation and
TestStripeWebhook_CheckoutMagicLinkValidatesOriginBeforeMutation.
frontend-modern/src/utils/discoveryPresentation.ts now owns the
customer-facing discovery-section framing copy, scan-scope labels, subnet
guidance, command-execution settings targets, API Access handoff labels, and
environment-lock messaging so
frontend-modern/src/components/Settings/DiscoverySettingsForm.tsx stays a
shared presentation shell instead of re-accumulating that wording inline.
Resource discovery command guidance must use that same presentation owner for
settings handoffs. frontend-modern/src/utils/discoveryPresentation.ts owns
the shared command-execution and agent:exec token-scope links; discovery
surfaces may explain those states, but the visible links must remain
Settings → Infrastructure and Settings → API Access through that helper,
not inline legacy labels or old settings paths.
That same presentation owner also packages the identified-service summary
consumed by surfaces outside the Discovery sub-tab.
getDiscoveryIdentifiedSummary is the canonical reducer that turns a stored
ResourceDiscovery into the compact card payload (service name, category,
confidence percent, port and path counts, cli access hint, service version,
observed timestamp, provenance label, and suggested web-interface URL
metadata). New surfaces that want to label a workload with its identified
service or offer a Discovery-sourced endpoint candidate must read through that
helper rather than re-implementing the empty/low-signal gate, so the
Discovery tab and out-of-tab surfaces collapse the same records and avoid
surfacing "Unknown" rows or zero-confidence noise. Manual/persisted
web-interface URLs still win: Discovery suggestions may be copied, opened, or
adopted through the shared WebInterfaceUrlField, but they must not silently
replace metadata or make row-name links active until the operator saves them.
No-URL diagnostics and command access hints are not endpoint candidates; they
can explain a Discovery result inside Discovery-owned surfaces, but they must
not trigger out-of-tab identified-service cards or suggested-URL panels without
another meaningful service signal.
The visible provenance marker for those values is the shared
DiscoveryProvenanceMarker; local surfaces may choose the labelled or
icon-only variant, but must not invent alternate Discovery badges or hide the
source on compact cards.
That same settings-shell boundary now also owns the shared settings
presentation helpers that those panels consume. frontend-modern/src/utils/systemSettingsPresentation.ts
is the canonical owner for shared system-settings presets, summaries, and
customer-facing action copy, while
frontend-modern/src/utils/ssoProviderPresentation.ts owns the shared SSO
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.
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
legacy-to-canonical aliases so existing local preferences migrate forward
without resetting user choices or forcing the runtime to keep deleted column
IDs alive indefinitely.
That same shared primitive boundary now also owns environment-lock
presentation. frontend-modern/src/components/shared/EnvironmentLockBadge.tsx
stays the reusable badge shell,
frontend-modern/src/utils/environmentLockPresentation.ts owns the canonical
badge label, title, and lock-button copy, and
frontend-modern/src/components/Settings/DockerRuntimeSettingsCard.tsx stays
the settings-shell consumer for environment-variable-locked container-update
controls. Future environment-lock UX should extend those owners instead of
reintroducing panel-local lock labels, badge styling, or title copy.
The release-ready shell proof now also includes a representative desktop
Playwright rehearsal in
tests/integration/tests/15-settings-shell-consistency.spec.ts so general,
organization, billing, relay, security, AI, updates, and recovery panels are
all exercised through the built app shell under a seeded multi-tenant runtime.
The security-facing settings panels within that shell now also follow an
explicit shared boundary with security-privacy so shell framing stays here
while auth posture, token controls, and privacy semantics remain governed as a
trust surface instead of generic UX copy.
That shared shell boundary now also covers version-matched docs-link framing:
customer-facing privacy disclosures in shared settings surfaces must route
through frontend-modern/src/utils/docsLinks.ts rather than panel-local
external URLs.
That same shared-shell framing also covers the concise telemetry summary in
General settings. The shell may present the privacy contract in compact product
copy, but the vocabulary for outbound usage telemetry must stay aligned
with security-privacy: coarse deployment and lifecycle buckets, aggregate
resource and outcome counts, coarse feature flags, and content-free Patrol,
Assistant, and capability-API usage counters are allowed, while hostnames,
credentials, infrastructure identifiers, URLs, paths, locale, browser events,
prompts, chat messages, command text, action output, token values, and personal
information are not.
That same docs-link boundary also governs local legal docs surfaced from the
settings shell: shared settings surfaces such as
AIRuntimeControlsSection.tsx must route Terms-of-Service links through the
shipped TERMS.md asset instead of hardcoding GitHub main URLs that can
drift from the running build.
The same shell boundary now also owns shared relay route framing copy:
frontend-modern/src/utils/relayPresentation.ts is the canonical owner for
the top-level relay settings description and availability copy used by both
settingsHeaderMeta.ts and RelaySettingsPanel.tsx, so the route shell and
its first SettingsPanel cannot drift into separate rollout or pairing
descriptions or describe Relay as a Pro-only feature after Relay became its
own self-hosted paid tier.
Single-surface settings pages that only render one canonical SettingsPanel
must stay rooted directly at that panel instead of wrapping it in an extra
page-level space-y-* container. frontend-modern/src/components/Settings/UpdatesSettingsPanel.tsx
frontend-modern/src/components/Settings/RecoverySettingsPanel.tsx, and
frontend-modern/src/components/Settings/AuditLogPanel.tsx are the current
reference cases, and
frontend-modern/src/components/Settings/__tests__/settingsArchitecture.test.ts
locks that direct-root contract so single-surface pages do not quietly regain
redundant outer spacing chrome.
The same shared settings-shell boundary now also owns the API-backed source
path inside Infrastructure.
frontend-modern/src/components/Settings/InfrastructureWorkspace.tsx,
frontend-modern/src/components/Settings/settingsHeaderMeta.ts,
frontend-modern/src/components/Settings/settingsNavigationModel.ts,
frontend-modern/src/utils/workloadEmptyStatePresentation.ts,
adjacent setup guidance must use Add infrastructure as the operator-facing
first-run label for API-backed onboarding, resolve that label to the shared
Infrastructure destination and its inline ConnectionEditor add flow, and
avoid reviving a standalone platform shell, Platform connections label, or
provider-local route.
That same settings-shell contract also owns the shared infrastructure summary
state. frontend-modern/src/components/Settings/useInfrastructureSettingsState.ts,
frontend-modern/src/components/Settings/useSettingsInfrastructurePanelProps.ts,
frontend-modern/src/components/Settings/InfrastructureWorkspace.tsx,
frontend-modern/src/components/Settings/useTrueNASSettingsPanelState.ts, and
frontend-modern/src/components/Settings/useVMwareSettingsPanelState.ts must
derive Proxmox/PBS/PMG/TrueNAS/VMware counts and availability from one shared
infrastructure settings state source instead of letting the top-level ledger
and inline credential flows fetch the same connection state separately. Phase
9 retired the standalone PlatformConnectionsWorkspace.tsx,
TrueNASSettingsPanel.tsx, and VMwareSettingsPanel.tsx shells; they remain
labels and proof history, not live presentation surfaces.
That same shared settings-shell boundary also owns provider parity inside the
inline add flow. Adding VMware may extend the same card, empty-state, dialog,
and summary-shell patterns used by TrueNAS, but it must not introduce a
VMware-only outer page shell, alternate settings route hierarchy, or another
summary vocabulary for connection health and contribution counts. While VMware
remains admitted rather than supported, shared settings primitives must render
its source-picker card with the manifest-derived preview badge and keep
supported-source empty-state copy from listing VMware as available now.
That same shared filter-presentation boundary also owns infrastructure source
continuity on active surfaces. Settings infrastructure and platform/runtime
pages must keep known canonical source options such as truenas and
availability visible when configuration or route context establishes them,
even when current unified-resource results do not contain that source, so
platform handoffs from settings and other surfaces do not flash back to
generic host-only language while the operator is still in a provider- or
endpoint-scoped investigation flow.
That same shared feature-presentation boundary also owns storage disk-detail
fallback messaging in frontend-modern/src/features/storageBackups/. Shared
detail presenters must describe the actual capability or identity gap that
prevents history from rendering, rather than reviving agent-install guidance
on API-backed platforms like TrueNAS when the canonical disk metrics target is
already the owning history path.
That same shared chart primitive boundary now also owns physical-disk live I/O
drawers. frontend-modern/src/components/Storage/DiskDetail.tsx must render
read, write, and busy charts through HistoryChart plus
useHistoryChartState, using the canonical physical-disk history resource id,
instead of reviving diskMetricsHistory, a page-local ring buffer, or another
storage-only live chart primitive for the same telemetry.
The shared shell boundary now also includes
frontend-modern/src/contexts/appRuntime.ts as the only neutral owner for
app-level websocket and dark-mode consumption. Shared shells and primitives
such as frontend-modern/src/components/Settings/Settings.tsx,
frontend-modern/src/components/shared/TagBadges.tsx, and
frontend-modern/src/components/shared/useInfrastructureSummaryTableState.ts
may consume that module, but they must not import @/App or recreate shell
providers. frontend-modern/src/App.tsx owns provider placement; primitives
own reusable consumption only.
That same shared settings-shell and banner boundary now also owns demo-mode
commercial suppression. frontend-modern/src/components/Settings/settingsNavCatalog.ts,
frontend-modern/src/components/Settings/settingsNavVisibility.ts,
frontend-modern/src/stores/sessionCapabilities.ts,
frontend-modern/src/stores/demoMode.ts,
frontend-modern/src/useAppRuntimeState.ts,
frontend-modern/src/components/shared/HistoryChartOverlay.tsx,
frontend-modern/src/features/patrol/PatrolIntelligenceBanners.tsx, and
frontend-modern/src/features/patrol/PatrolIntelligenceHeader.tsx
must consume one shared bootstrap truth from
/api/security/status.sessionCapabilities.demoMode and hide billing tabs,
trial nudges, monitored-system warning banners, dashboard upsells, Patrol
upgrade CTAs, history-lock paywalls, and other public-demo commercial
affordances when the browser is rendering a public demo runtime.
Shared primitives must not perform their own ad hoc /api/health polling,
response-header inference, hostname heuristics, or per-banner demo branching;
the runtime bootstrap, shared session-capability store, and shared banner
hooks stay on one canonical owner so suppression stays coherent across
customer-facing surfaces.
That same session-presentation boundary owns the non-promotional self-hosted
v6 app posture. Settings navigation, shared upgrade links, trial banners,
monitored-system warning banners, history-lock overlays, and paid-feature gate
primitives must honor resolved presentationPolicy.hideUpgrade by hiding
prompts by default on ordinary self-hosted installs. Direct
activation/recovery routes may still render their owned content, but sidebar
discovery, trial CTAs, plan-review links, plan upsells, and feature upgrade
links must not appear unless an explicit handoff, hosted-mode policy, or active
entitlement says they should.
That same shared app-shell boundary now also owns assistant bootstrap silence
on non-AI routes. frontend-modern/src/useAppRuntimeState.ts,
frontend-modern/src/App.tsx,
frontend-modern/src/stores/aiChat.ts,
frontend-modern/src/components/AI/Chat/index.tsx must treat
/api/security/status.sessionCapabilities.assistantEnabled as the only
general-route assistant availability fact, while closed assistant chrome and
non-AI settings panels stay off /api/settings/ai and /api/ai/* until an
owned assistant or Patrol surface is actually open. frontend-modern/src/stores/aiChat.ts
must therefore stay presentation-only with respect to assistant bootstrap:
org-switch cleanup, keyboard focus, drawer state, and local context/session
persistence belong there, while backend settings/model reads stay on
frontend-modern/src/stores/aiRuntimeState.ts. The governed browser proof in
tests/integration/tests/11-first-session.spec.ts must continue to assert
that plain settings routes render without assistant bootstrap traffic or
console noise.
When an owned Patrol or alert surface attaches a source-named Assistant
handoff, that same drawer shell must keep the empty conversation state aligned
with the attached briefing as neutral Context attached copy instead of
rendering generic cluster/system starter prompts or feature-authored suggested
prompt chips below the source-owned context.
The global Assistant launcher and the command-palette Assistant open command
follow the same contextual rule. They must derive current-view context through
frontend-modern/src/utils/assistantPageContext.ts, label the action as asking
about the current monitoring, Patrol, alerts, or settings view, and open the
drawer with that pulse-view context attached. They must not call
aiChatStore.toggle() or aiChatStore.open() without context from the
authenticated shell, because that reintroduces a generic Assistant front door.
Shared table, disclosure, and form primitives must also stay explicitly typed
at the browser edge. Summary rows may memoize repeated pending-update reads,
shared buttons must preserve discriminated disclosure props, toggle and a11y
helpers must expose exact event signatures, shared rows must accept typed
data-* props, and reporting-panel helpers must remain ES2020-safe instead of
depending on feature-local casts or newer string helpers.
Settings report scheduling follows the same shell/runtime/model split as the
rest of the Reports panel. ReportingPanel.tsx owns layout and shared controls,
useReportingPanelState.ts owns API lifecycle and save/run/delete control
flow, and reportingSchedulesModel.ts owns schedule payload normalization,
labels, default form state, and cadence formatting. Schedule scope selection
must reuse the shared ResourcePicker and reporting catalog types rather than
creating a separate resource selector or browser-local schedule API contract.
The settings navigation model now exposes a single infrastructure-systems
sidebar entry for the infrastructure settings area. The former
infrastructure-connections and infrastructure-install entries have been
removed from SettingsTab, settingsNavCatalog.ts, settingsPanelRegistry.ts,
and settingsNavigationModel.ts. No future additions to the settings nav may restore
infrastructure-connections or infrastructure-install as independent tab
identifiers; panel routing within the infrastructure area must use
InfrastructurePanelStep in-page state instead of URL sub-routes.
frontend-modern/src/components/Settings/settingsNavigationModel.ts owns the
explicit routeability check that rejects retired infrastructure/workloads
aliases before the settings shell mounts. useSettingsNavigation.ts may
redirect /settings and still canonicalize current settings destinations, but
it must not translate removed infrastructure subpaths into onboarding queries or
derive Proxmox platform state from those paths.
The shared frontend source/platform vocabulary now also includes
availability as an agentless monitoring source and network-endpoint as the
canonical resource projection. Source labels, badges, settings add-flow copy,
and availability management copy must use shared presentation helpers instead
of feature-local wording, so availability probes stay visually aligned with
the Monitoring availability settings surface without pretending to be a host
agent install or a platform API connection.
Availability setup presets for pingable machines/devices, MQTT, ESPHome, or
similar agentless endpoints must also stay on the shared settings form
vocabulary: presets may fill target kind, protocol, port, and path defaults,
but display badges and drawers still derive Availability and
Network Endpoint labels from the shared resource presentation helpers rather
than from preset-local copy.
Infrastructure rows for those same agentless endpoints must surface probe
evidence directly in the row, not just as a green status dot or an
Availability badge. The shared row presentation must expose the probe method
and latest latency or failure result once, inline in the agentless endpoint's
metric slot, while keeping recent check timing and fuller failure context in
the tooltip or drawer so operators can understand what was measured without
duplicated row chrome.
Known platform resources use that same compact presentation when availability
is attached. AvailabilityProbeStatusCard is the shared detail primitive for
Workloads and Docker host drawers; it renders the complete target, protocol,
latest result, latency when relevant, evidence freshness, and last observation.
Its fact rows, and the matching service/probe/target rows in
AvailabilityProbeSuggestionCard, compose InfoCardKeyValueRow so phone
layouts remain condensed while wide cards keep each value adjacent to its
label.
Plural attached checks render as repeated bounded cards from
availabilityChecks, while the row keeps one compatibility summary. Expired
successful evidence must render an amber Stale state with no green
Responding normally copy, and a never-observed check must render
Not checked. A matched machine or service carrying an attached projection
must not appear as a primary row in the Machines Availability checks tab;
the distinct source-owned network-endpoint for that configured check must
appear there regardless of whether its correlation state is attached,
standalone, ambiguous, or unresolved.
Operational navigation for those agentless endpoints belongs to the
frontend-primitives-owned Machines surface as a focused Availability checks tab
rather than a new primary nav item. The page may show availability checks beside
standalone Pulse Agent machines, but Settings remains the add/edit owner and
the app shell must not add a separate top-level Availability destination.
That Availability checks tab owns a URL-addressable view=fleet presentation
alongside the existing table; table remains the default and q plus status
filters are shared between both modes. Fleet tiles combine the canonical
current-health projection with the bounded history batch: categorical state
coverage is labelled in text as reachable, unreachable, indeterminate, or
unknown, and latency is drawn only for reachable evidence so gaps cannot be
misread as zero latency. A tile opens the existing ResourceDetailDrawer
rather than a service-monitor-specific detail model. History failure must stay
inside the fleet surface with explicit copy while current status and resource
navigation remain usable. Desktop and phone layouts must keep every tile
keyboard-operable, preserve the textual legend, and avoid horizontal clipping
at fleet scale.
The Machines page must not pretend its machine list is a generic overview:
the default tab is Machines, the Machines table is only for Pulse Agent-backed
resources with host telemetry, and the full availability-check row list belongs
to the Availability checks tab. Its default disk column follows the platform
host-table scan pattern: multi-disk machines render compact per-disk mini-bars
so operators can quickly see disk count and pressure distribution, while sorting
still uses the highest-usage operational filesystem, platform plumbing stays out
of the visible disk set, and full per-filesystem labels remain in hover/detail
affordances rather than turning the row into a raw mount browser. Servers,
laptops, desktops, and comparable
computers monitored only by agentless reachability checks may use
targetKind=machine in the availability form, but they stay in Availability
checks until a Pulse Agent registers and supplies CPU, memory, disk, and network
telemetry. Machines empty and handoff actions must lead to Pulse Agent install
or the Availability checks tab, not to an agentless machine row in Machines.
Mobile product layout is a shared primitive contract, not a page-local styling exception. At supported phone widths, tab rails and dense data surfaces must preserve their readable intrinsic width inside an owned horizontal scroll container; expanded inline detail must remain bounded by the visible viewport; and active destinations must be scrolled into view. Shared search, filter, disclosure, navigation, copy, and row-action controls must keep a 40-pixel mobile touch floor while retaining their compact desktop density. Settings navigation must own a viewport-bounded vertical scroll region so its route list does not push the active panel below the page. Settings data grids must also define a phone information hierarchy instead of depending on horizontal scrolling as their first responsive behavior. Identity, current state, and immediately available actions stay visible; lower-priority timestamps, external IDs, and verbose detail columns may be hidden through the shared symmetric column boundary. When a hidden field is still needed for the phone decision, its concise value belongs as mobile-only secondary context in the surviving identity cell. Multi-action cells may collapse visible labels on phones only when every action keeps an accessible name and the shared touch floor.
Patrol finding handoffs must derive approval posture from the canonical typed
action state and approval policy, not only from a legacy approval id. A
pending_approval action or any non-none approval floor remains explicitly
approval-bound in shared handoff metadata so Assistant, the collapsed finding
row, and the expanded action review cannot disagree.
The shared Actions dialog remains the responsive and accessible review
primitive for typed APT maintenance. Its heading supplies the dialog accessible
name, the close control has an explicit name, pending action rows are keyboard
reachable, and the scroll-bounded panel keeps safety, execution, verification,
recovery, delivery, and next-step content actionable at desktop and 390-pixel
phone viewports. Responsive layout must not hide the exact parameter authority,
evidence source, or recovery instruction, and it must not add a duplicate legacy
action path or verification card when ActionResultV2 is present. Read-only
sessions keep the review packet inspectable but must not render approve, reject,
or run controls, while settled historical records must not be mislabeled as
expired actionable reviews.
For an aged, receipt-pending execution, the same responsive review dialog may
expose an audit-recovery disclosure to an eligible local administrator. It must
show the receipt-pending state to every viewer as an unknown outcome, with a
non-mutating in-dialog re-read of the same action and a warning not to create a
second plan while the first outcome is unknown. The re-read remains available
before the recovery window and in read-only sessions; a failed read retains the
unknown state and never implies that the action was sent again. Recovery must
first instruct the operator to check the resource's actual state outside the
action record, then require a written reason and an explicit acknowledgement
before a fresh action read and guarded force-fail call. Desktop and phone
layouts keep this confirmation visible without implying a retry, cancellation
or failed mutation. Read-only and settled records show no recovery control;
permission hints in the client never replace the server's authority check.
Its action controls are also plan-identity-bound: a missing reviewed planHash
renders explicit replan guidance and hides approve, reject, and run controls,
while an actionable record sends the exact displayed hash on every mutation.
The dialog is also route-backed through the canonical action query parameter.
Contextual surfaces use the shared button-link primitive to hand off an exact
typed action id; the Actions route opens that durable review directly, selects
the matching Open or History subtab from server-authored lifecycle state, and
removes the query when the dialog closes. Feature pages may summarize action
context, but they must not recreate approve, reject, run, progress, or outcome
controls outside the shared Actions review.
The Actions route owns overlapping reads by request generation. A slower detail
response or late dialog refresh must not replace a newer URL-selected action or
reopen a closed review; a mismatched server action id is rejected. An older Open
or History list response must not overwrite the newer tab's results or error.
Actions.requestOwnership.test.tsx covers both response orders and close while
a receipt re-read is pending; the browser navigation proof checks the rendered
dialog and URL at desktop and phone widths without sending an action mutation.
The Actions ledger is a peer top-level navigation destination. Patrol remains
the primary detection and investigation home and may expose a route-backed
Actions handoff, but Actions owns its pending-approval count and selected state.
The canonical /actions route remains stable for exact action deep links and
universal audit records originating from Patrol, Assistant, MCP, or manual
controls. While that route is open, desktop and mobile navigation select
Actions and the browser title identifies Actions. The first-class navigation
entry composes the existing shared review dialog, route, API client, and durable
action identity rather than creating another action client. When the trusted
audit origin is present, both the queue row and decision packet show bounded
product attribution such as From Patrol; unknown first-party surfaces fall
back to From Pulse, and absent origin remains absent rather than guessed. The
shared action review also exposes Open Patrol record only when a Patrol origin
carries its canonical operationalRecordId; the link targets the existing
route-backed Patrol attention selection and never derives identity from display
copy, resource IDs, finding IDs, or action reasons. Older correlated Patrol
actions may expose Open Patrol to the Patrol home, but never label that
fallback as a record-specific return.
Protection posture presentation boundary
Platform coverage tables render the storage/recovery-owned four-state
ProtectionPosture contract without inventing age, failure, verification, or
coverage states from raw browser payloads. Protected uses the shared success
tone, attention uses warning, unprotected uses danger, and unknown uses muted
presentation. The compact row remains actionable: plain-language rationale and
provider history/permission limitations live one disclosure deeper beside
bounded restore evidence. A posture fetch failure must keep the evidence
inspectable and show unknown, never a locally inferred healthy fallback.
Protection table controls continue to compose shared filter, table, status,
counter, loading, error, disclosure, and inline-detail primitives. The bounded
batch hook retains fulfilled values during refresh and issues at most one
request per 200 resource rows, rather than placing a request under each row.
Table-local provider histories keep issue prominence inside that existing
control rail through withPlatformAttentionCount: TrueNAS Protection carries
the compact failed/warning count, vSphere Health carries the compact
critical/warning count, and each built-in row order keeps attention ahead of
routine activity. They must not add page-level attention summary cards above
their toolbars. Full-width attention summaries are reserved for genuinely
cross-section overview state, such as Kubernetes aggregating nodes, workloads,
and health signals with distinct destinations. Running replication remains an
ordinary event state rather than page-level alarm copy.
Operational Trust attention interaction boundary
The Patrol selected-detail surface owns the smallest lifecycle interaction set: acknowledge, return to open, temporary suppression with a required reason and one of the bounded 1-hour, 24-hour, or 7-day durations, and return to active. Every mutation refreshes both the shared detail and list projection. The queue retains screen-reader names, keyboard activation, focus restoration after node replacement, reduced-motion behavior, and a no-overflow phone layout. Raw evidence history, lifecycle timeline, provider limitations, and action audit remain one disclosure or shared review deeper; no platform table or Assistant surface recreates these controls.
Trust-gate state presentation
Shared frontend composition must preserve the typed domain state supplied by
Patrol, alerts, storage, and unified resources rather than flattening it into a
generic success/error or loading flag. After the backend accepts a manual
Patrol run, the page leaves the transient Starting state using the accepted run
identity and performs one bounded status/history reconciliation; a structured
backend rejection remains distinct from a browser or network failure. Any
action proposed from that run still hands off to the shared canonical Actions
review instead of adding approve, execute, or retry-mutation controls to the
Patrol feature.
Patrol autonomy controls follow the same server-truth discipline: after any
successful autonomy PUT, the feature reloads the canonical GET projection
before rendering the selected mode. Compact paid-runtime acknowledgements are
not frontend state and missing nested fields in them must not crash the page.
An absent, malformed, or Go zero-time expiresAt value is rendered as no
expiry; it must never become a year-one locale date. A real bounded future
expiry remains visible beside the acknowledgement status.
Storage detail primitives render physical-disk collection truth explicitly: temporarily unavailable, provider/controller unsupported, and unexpectedly missing evidence use different copy, and an unavailable per-disk I/O stream must not render an apparently live chart or synthetic zero counters. Alert threshold primitives similarly carry the current canonical override ID through edit, save, reload, and refetch while legacy IDs remain read-only compatibility candidates. Domain ownership stays with Patrol intelligence, storage recovery, alerts, and unified resources; the primitive layer owns consistent rendering, accessibility, and handoff behavior only.
The focused browser proofs are
frontend-modern/src/features/patrol/__tests__/patrolRunAcceptance.test.ts,
frontend-modern/src/components/Storage/__tests__/DiskDetail.test.tsx,
frontend-modern/src/components/Storage/__tests__/useDiskDetailModel.test.ts,
and
frontend-modern/src/features/alerts/thresholds/hooks/__tests__/truenasThresholdPersistence.test.tsx.
Agent Doctor settings framing
Settings labels the application update panel Updates under the System group, whose description sends agent updates to Infrastructure, and keeps agent lifecycle triage in the separate Agent Doctor dialog. Platform update notices, Diagnostics, and Infrastructure rows use the canonical Agent Doctor route handoff instead of recreating installer or repair controls. The dialog may enrich the shared connections ledger with structured diagnostics, but it must preserve loading, unavailable, unsupported, waiting-for-auto-update, removed, warning, and critical states rather than flattening them into a generic update badge.
The canonical query is agentDoctor; agentUpdates remains a compatibility
alias that opens the same dialog and does not create a second settings surface.
Scoped connection IDs filter active rows without hiding removed-agent history
from the unscoped view. Copy-command controls render only for a backend- and
frontend-confirmed supported platform; unknown and unverified FreeBSD/pfSense
states show bounded guidance with no executable command. The focused proofs are
frontend-modern/src/components/Settings/__tests__/infrastructureAgentDoctorModel.test.ts,
frontend-modern/src/components/Settings/__tests__/InfrastructureWorkspace.test.tsx,
frontend-modern/src/components/Settings/__tests__/DiagnosticsResultsPanel.test.tsx,
frontend-modern/src/components/Settings/__tests__/settingsHeaderMeta.branchcov0713.test.ts,
and frontend-modern/src/utils/__tests__/updatesPresentation.test.ts.
Alert intent and three-state availability presentation
The Alerts thresholds surface composes the versioned intent-policy editor from the shared form, status, disclosure, and loading primitives. It must preserve field-wise inheritance: omitted fields inherit, while explicit false and zero values remain deliberate overrides. Save uses the displayed revision, reports revision conflict without replacing local edits, and refreshes from the server-owned document after success. Preview renders clear, expected-transient, pending-grace, and would-activate as distinct states and never presents preview as a write.
The powered-off default is presented and persisted as the guest resource-type
rule so VM and LXC resources inherit it without changing node or agent
connectivity. Blank means inherit, 0 explicitly means no wait, and the UI
states both meanings next to the control. Duration fields accept only base-10
whole seconds from zero through 30 days; an enabled backup hard cap must be
positive. Invalid, fractional, negative, or oversized values remain local,
show an actionable error, and issue no API write. Disabling backup extension is
an explicit enabled: false rule and remains separate from disabling a guest's
powered-off alerts.
Availability controls expose UDP mode, request payload, and optional expected
response only where valid for the selected protocol. Unified-resource
presentation keeps indeterminate visibly distinct from reachable and
unreachable: open-or-filtered UDP uses warning treatment and bounded evidence
copy, never a green success tone or a fabricated latency. The primitive layer
does not infer detector, operator-intent, or recovery truth.
The focused proofs are
frontend-modern/src/features/alerts/__tests__/AlertIntentPolicyPanel.test.tsx,
frontend-modern/src/features/alerts/__tests__/ThresholdsTab.test.tsx,
tests/integration/tests/85-powered-off-tolerance.spec.ts,
frontend-modern/src/components/Settings/ConnectionEditor/__tests__/AvailabilityTargetSlot.test.tsx,
and
frontend-modern/src/utils/__tests__/availabilityProbePresentation.test.ts.
Pool-health evidence presentation
TrueNAS storage detail composition presents canonical and native pool state, structured scrub or resilver progress, pool error totals, affected vdev role and topology, recommendation, evidence codes, and evidence source from the canonical resource payload. It does not derive failed-disk claims from a missing disk row or replace native path-only leaf identity with a guessed device name.
TrueNAS alert presentation deduplicates a native provider signal projected onto system, pool, and disk rows by provider, native ID, and code, preferring the most specific canonical resource. Distinct evidence codes remain distinct rows. All acknowledgement, suppression, history, and action handoffs continue through shared alert primitives; no provider-only alert shell or email action is introduced.
frontend-modern/src/components/Infrastructure/__tests__/resourceDetailDrawerTrueNASModel.test.ts
and
frontend-modern/src/features/truenas/__tests__/truenasPageModel.test.ts
are the focused presentation proofs. The governed browser proof must open the
shared resource detail surface and verify the same evidence labels in rendered
UI.
SSO endpoint URLs are presented as copyable only when the server supplied one
frontend-modern/src/components/Settings/SSOProvidersPanel.tsx renders the
OIDC Callback / Redirect URL and the SAML SP metadata and ACS URLs as
CopyValueButton chips that admins are told to register with their Identity
Provider. The backend may now legitimately omit those values when it cannot
resolve a public URL for the deployment, so each block falls back to guidance
text from getSSOEndpointUnavailableHint in
frontend-modern/src/utils/ssoProviderPresentation.ts instead of rendering a
copy affordance around an empty or wrong value. The panel must never embed a
localhost endpoint URL of its own.
The OIDC edit modal distinguishes the two reasons the URL can be missing: for a
provider being created it explains the URL is generated on save and copied from
the provider card afterwards (the modal closes on save, so it cannot show the
URL itself), and for an existing provider it shows the same public-URL guidance
as the card. settingsArchitecture.test.ts pins the guidance owner, the absent
localhost literal, and the corrected post-save copy; the rendered fallbacks and
copy affordances are covered by
frontend-modern/src/components/Settings/__tests__/SSOProvidersPanel.test.tsx.
Entitled application branding stays app-shell-owned
frontend-modern/src/stores/systemSettings.ts owns the narrow reactive
runtime-brand payload loaded during authenticated bootstrap. The shared
frontend-modern/src/AppLayout.tsx shell is the only owner of applying that
payload to the centered header lockup and route-aware browser title. A custom
bounded banner logo replaces the built-in mark; a non-empty display name
replaces the Pulse wordmark, while a logo with an empty display name may stand
alone. Kiosk mode keeps its existing hidden-header behavior.
The Appearance surface edits the already-canonical reportBranding object
through BrandingSettingsCard; it accepts PNG, JPEG, or GIF files no larger
than the persisted inline-logo boundary, previews the exact saved material,
marks the shared settings form dirty, and provides an explicit remove action.
It must not create a page-local branding cache or render configured values
when white_label is unavailable. Focused proofs live in
BrandingSettingsCard.test.tsx, AppLayout.test.tsx, and
stores/__tests__/systemSettings.test.ts.
ZFS datasets extend the existing storage detail primitive
StoragePoolDetail remains the owner of the expandable ZFS pool presentation.
When a pool carries optional datasets, its already-expanded detail region
renders the canonical dataset name and formatted used, available, referenced,
and mountpoint values. Empty dataset collections add no new panel, route, or
navigation state. The presentation mapper owns byte formatting and missing
mountpoint fallback so components do not reinterpret provider data.
Host GPU telemetry reuses shared metric and history primitives
The standalone machine table may expose typed host GPU utilization as a
toggleable, sortable metric-bar column, but it must render through the shared
MetricBar primitive and use the unified resource's existing metric key. The
row model owns validation, maximum-per-host selection, and the per-device
inspection title; the component must not invent vendor-specific probes or a
parallel GPU table. The table and shared View column picker expose GPU only
when at least one current machine has finite utilization evidence, so estates
without GPU telemetry do not carry an empty default column or an inert toggle.
The shared resource drawer keeps GuestDrawerHistory as the sole history
renderer. Agent-backed hosts and explicit docker-host metrics targets,
including standalone Unraid machines, Proxmox nodes, and Docker/Podman hosts,
consume the single
frontend-modern/src/components/shared/hostMetricsHistoryModel.ts
HOST_METRICS_HISTORY_GROUPS catalog so CPU temperature history cannot drift
out of one host surface while remaining on another. Host resources provide the
current canonical CPU temperature as the initial fallback; SMART disk
temperature history remains on the physical-disk resource rather than being
mislabelled as host CPU temperature. Resources with typed GPU sensors extend
the applicable host groups with core utilization, VRAM pressure, and GPU
temperature series and provide current typed readings as initial fallbacks.
API-only Proxmox nodes select their canonical node metrics target and reuse a
source-aware subset of that catalog: utilization, network, and thermals remain,
while host disk throughput is omitted because the PVE node API does not expose
that stream. The drawer must not leave an unsupported disk-I/O chart in a
permanent Collecting history state. Non-host workloads retain the default
workload history groups unchanged. Storage metrics targets select a dedicated
capacity catalog backed by the canonical usage series, while physical-disk
targets select only the canonical busy, read, write, and SMART-temperature
series. Neither target may inherit guest CPU, memory, or network charts that
its backend history model does not record.
Rendered table proof belongs in
frontend-modern/src/features/standalone/__tests__/AgentsMachinesTable.test.tsx;
drawer grouping and fallback proof belongs in
frontend-modern/src/components/Infrastructure/__tests__/resourceDetailDrawerMetricsHistoryModel.branchcov0712.test.ts.
Proxmox Storage reuses the shared product-family source scope
frontend-modern/src/features/proxmox/ProxmoxPageSurface.tsx mounts the
canonical shared Storage surface for its Storage tab and supplies the hidden
proxmox-all filter. Shared storage source matching owns that internal
umbrella and admits normalized PVE, PBS, and other Proxmox-family source keys;
visible source-picker filters remain exact. Agent-only physical-disk telemetry
whose canonical parent is a Proxmox node is admitted through its explicit
platform membership rather than being hidden because its fact source remains
agent. The page must not create a PBS-only disk table or duplicate storage
state in the Proxmox feature.
The rendered route contract in
frontend-modern/src/features/proxmox/__tests__/ProxmoxPageSurface.contract.test.tsx
and the shared-surface source guard in
frontend-modern/src/features/proxmox/__tests__/ProxmoxBackupsTable.test.tsx
pin this composition boundary.
ZFS dataset table rows inside StoragePoolDetail inherit their separator from
the shared STORAGE_DETAIL_ROW_CLASS presentation constant. The component
must not reintroduce a raw border-token class for dataset rows.
The settings nav gates Infrastructure, and the blocked-route fallback is capability-aware
infrastructure-systems now declares requiredCapability: 'infrastructureRead'
in frontend-modern/src/components/Settings/settingsNavCatalog.ts, so
shouldHideSettingsNavItem and shouldBlockSettingsRouteItem withhold both the
sidebar entry and the route from a session the backend says cannot read it.
This is a different gate from system-relay and support-reporting. Paid
feature navigation is hidden from ordinary free sessions, but its panel-owned
direct route remains available for an explicit activation or recovery handoff.
Infrastructure has no such route exception: every endpoint behind it is
RequireAdmin, so a non-admin got an all-empty page whose pollers logged a
warn-level denial on every tick. Hiding and blocking the item is what stops
those pollers mounting.
The same rule now covers the admin-only System tabs. system-network,
system-updates, and system-recovery declare
requiredCapability: 'systemSettingsRead' in the same catalog, so both the
sidebar entry and the route are withheld from a session that cannot read
settings. They share Infrastructure's rationale rather than the paid-feature
one: a free install can act on a paid tab by upgrading, but a non-admin cannot
grant themselves admin, so the tab can only end in a panel they will never
populate.
system-general is deliberately excluded. Theme, language, and unit
preferences on that tab are user-scoped, so gating it would take personal
settings away from every non-admin, and the panel is not empty for them. Proof:
frontend-modern/src/components/Settings/__tests__/systemNavCapabilityGate.test.ts,
which pins the withheld, granted, and unresolved cases plus the
system-general exclusion.
Because DEFAULT_SETTINGS_TAB is infrastructure-systems, the blocked-route
fallback in useSettingsAccess.ts can no longer resolve to the constant — that
sent a refused session straight back to the tab that had just refused it. The
fallback uses an explicit preference order: the default when reachable, then
the user-scoped system-general tab, and only then the first remaining route.
Catalog order is not a safety policy; after the admin-only routes are removed it
can otherwise land a viewer on Plans, an upgrade surface they cannot administer.
The capability rule also covers every remaining panel whose mount read is
admin-only: Availability checks, all three Pulse Intelligence tabs, Diagnostics
& Health, Data & Reports, and System Logs declare their own named capability.
Data & Reports keeps its independent advanced_reporting feature gate because
feature discovery and administrative reachability answer different questions.
Navigation remains stable while /api/security/status is loading, but
canMountSettingsPanel refuses to instantiate a capability-gated panel until
the exact named capability is true. A failed or incomplete status response is
a resolved denial: the route is removed and the fallback selects General. This
prevents first-paint panel fetches from racing the capability request while
keeping a successful status usable during later refreshes.
Pinned by the infrastructure-systems block assertion in
settingsArchitecture.test.ts and by
__tests__/infrastructureNavCapabilityGate.test.ts, which also pins that
neither gate fires before the security status resolves — hiding on an
unresolved status would flash the default tab away from an admin on every load.
__tests__/adminOnlySettingsNavGates.test.ts and
__tests__/useSettingsAccess.test.tsx pin the complete named-capability,
fail-closed mount, failed-status, deduplicated-load, and General-fallback rules.
Alert monitoring actions preserve domain ownership
The Alerts overview may offer a compact per-resource Monitoring menu, but the menu is an adapter over the canonical resource operator-state API. It must preserve unrelated state on every write, including one-shot/recurring maintenance and descendant scope, distinguish availability-only expected-offline from all-attention mute, and state that retirement changes Pulse monitoring rather than deleting provider inventory. Resource detail and alert surfaces use the same typed monitoring and lifecycle vocabulary and provider-ownership presentation helper. They must not create local alert mute, archive, or removal state. The menu remains keyboard reachable, uses ordinary shared surface and border tokens, and retains usable controls at phone width.
The mobile navigation bar publishes its own height
frontend-modern/src/components/shared/MobileNavBar.tsx is the only owner of
the bottom navigation bar's height. That height is content-driven and already
includes the safe-area inset through pb-safe, so no consumer may derive it.
The bar measures itself after mount, publishes the result as the
--pulse-mobile-nav-height custom property on the document element, keeps it
current through a resize observer and a window resize listener, and removes it
when the bar unmounts. Measurement happens in onMount rather than in the
element ref, because a ref runs before the node is in the document, where the
measured height is zero.
Every surface that must sit on top of the bar reads that property: the
Assistant overlay panel and its backdrop, the compact post-update notice, and
the global GitHub star banner. Consumers must not add
env(safe-area-inset-bottom) on top of the published value, and must not
reintroduce a literal bar height. The declared :root value is a
pre-measurement fallback only and deliberately under-estimates: reserving more
than the bar's real height leaves a band between an overlay's backdrop and the
bar that is neither dimmed nor click-blocked, while reserving slightly less is
covered by the opaque bar. Surfaces with their own placement at wider
viewports, such as the star banner's md:bottom-4, keep it.
Proofs live in frontend-modern/src/components/shared/__tests__/MobileNavBar.test.tsx,
frontend-modern/src/components/__tests__/GitHubStarBanner.test.tsx, and
frontend-modern/src/__tests__/App.architecture.test.ts, which fails if any
runtime source reintroduces a literal bar height.
Alert delivery log presentation
The destinations-tab delivery log renders through the shared Card primitive
in the feature-owned AlertDeliveryLogCard. Outcome badges use the
plain-language labels from alertDestinationsPresentation rather than queue
vocabulary ("Failed, retries exhausted", never "dead letter"), failure detail
lines use the shared red emphasis tokens in both themes, and entry rows wrap
without horizontal overflow at mobile widths. The unavailable state is a
role="alert" message distinct from the empty state, because "cannot read
the log" and "no attempts" mean opposite things to someone deciding whether
to trust their alerting. The card renders immediately after the delivery-health
warning and recovery controls, and the Overview warning uses the shared
ButtonLink primitive to navigate to the Notifications route. Attempt and
held-event rows use semantic time elements with visible absolute local
timestamps for timeline correlation and retain relative time only as hover
context. The explanatory copy names the seven-day completed and 30-day
dead-letter windows separately.
Storage rows distinguish retained observations
Storage table state presentation consumes the storage record's canonical
freshness field before provider-native health labels. When polling fails and
the last-known capacity remains visible, the row uses the shared amber warning
tokens, labels the observation Stale, and exposes the last successful refresh
age in its title. Freshness presentation remains in the pure storage row model;
the table component must not infer age from render time or restyle retained
capacity independently.
Credential-bearing destination panels use replacement semantics
The feature-owned external-watchdog panel composes SettingsPanel without
creating a second settings shell. A stored credential-bearing URL renders as
an empty password input with a configured replacement placeholder and an
explicit Remove action; it must not be inserted into the DOM, tooltip, status
copy, or client logs. Only a newly entered value may be revealed with the
panel-local Show/Hide control. Status badges use shared theme tokens, error and
unavailable states remain textually distinct, and the four-part status grid
collapses without horizontal overflow at phone widths. This pattern is the
required primitive composition for future secret-bearing destination panels.
Alert destinations share one severity-policy primitive
Email, webhook, Apprise, and entitled mobile-push panels compose the
feature-owned DestinationSeveritySelect rather than implementing separate
labels, option vocabularies, or responsive layouts. The control builds on the
shared FormSelect primitive, exposes one associated label, and uses the same
All alerts / Critical alerts only presentation at desktop and phone widths.
Destination-specific help may explain transport semantics—mobile copy states
its privacy and current-state boundary—but it must not redefine the policy.
Alert feature state owns persistence and entitlement gating; the primitive
owns presentation only. alertDestinationsPresentation.test.ts pins the
shared vocabulary and the distinct mobile guidance.
Escalation configuration uses destination identity, not channel aliases
The alerts-owned escalation section composes shared settings, toggle, and form controls while presenting a feature-owned checkbox catalog keyed by logical destination ID. It sources that catalog from the same loaded destination state used by the Notifications tab, preserves selected-but-disabled and deleted entries visibly, and prevents the final selection from being removed. The critical-repeat control exposes a bounded numeric interval only when enabled and states every lifecycle condition that stops paging. Desktop and mobile layouts must retain associated labels and avoid horizontal overflow. Escalation level delays and repeat intervals share the rendered 5–180 minute bounds; the feature state clamps typed values before they can leave the control surface.
Alert groups use the shared localization boundary
Alert group disclosure and group acknowledgement copy routes through
alertOverviewPresentation and the shared English, German, and Spanish
catalogs. The presentation distinguishes a backend-declared shared-system
relationship (linked signal(s)) from multiple detectors on the exact same
resource (related) while keeping expansion and acknowledgement controls in
the existing alert surface. Components must not hardcode this vocabulary or
render the backend correlation reason or key as operator copy.
The labels retain singular/plural behavior, use the existing responsive button
primitives, and introduce no parallel group component. Localization and
presentation proofs live in frontend-modern/src/i18n/__tests__/i18n.test.ts
and frontend-modern/src/utils/__tests__/alertOverviewPresentation.test.ts.
Infrastructure synthesis reuses that alert group and disclosure interaction
rather than introducing an incident dashboard beside Alerts. A backend-declared
infrastructure-incident group renders a compact summary immediately above its
primary alert, labels the failure layer and whether the backend established a
supported cause or only a related observation set, and exposes the backend
reason, affected count, observation times, and bounded evidence IDs inside a
native details disclosure. The existing linked-signal control expands every
supporting detector card, so the operator can compare timing and challenge the
inference without losing alert-level acknowledgement, snooze, timeline,
monitoring-policy, or Patrol actions.
The summary must not derive relationships, choose a root cause, hide
contradictory observations, or render an observation set as causal. Failure
class and synthesis labels route through the shared English, German, and
Spanish catalogs. AlertIncidentSynthesisSummary.test.tsx,
useAlertOverviewState.test.tsx, and i18n.test.ts pin inspectability,
uncertainty wording, and additive group behavior at the frontend boundary.
Mobile destinations are links and update evidence stays explicit
Fixed mobile destinations and menu destinations that navigate are anchors with
real href values. Plain primary activation remains in the SPA so route
warming and per-platform route memory continue to apply; modified activation,
middle-click, and context-menu actions remain native browser behavior.
Platform and overflow controls that open menus remain buttons. Disabled
platform anchors resolve to infrastructure setup, and menu focus, Escape
return, active state, badges, and narrow-layout containment remain unchanged.
MobileNavBar.test.tsx, mobileNavBarModel.branchcov0712.test.ts, and
AppLayout.test.tsx pin those distinctions.
The Proxmox node drawer presents checked zero, current positive, stale,
unavailable, and not-checked package evidence as a labelled detail row with
bounded copy and checked time. The compact table badge remains reserved for a
current positive observation. Provider errors are never rendered. Component
and browser evidence covers both 1440px and 390px layouts in
NodeDrawerOverview.updateEvidence.test.tsx, ProxmoxNodesTable.test.tsx, and
frontend-modern/browser-verification.json.
Docker drawers expose reduced helper coverage without actions
The Docker host drawer consumes the canonical optional collectionMode field.
For typed-helper-summary it adds one bounded warning to the shared attention
section and omits the container update management card; it does not fabricate
zero update state or offer an action that the reporting collector cannot
execute. Unknown or absent values preserve the direct-runtime presentation.
Component and browser proofs cover the warning, action omission, mode
transition, and desktop/narrow containment.
Patrol model choice carries guidance and a cost preview
The Pulse Intelligence settings surface answers "which model should I pick
and what will it cost" at the point of choice instead of after the budget
trips. The shared AIModelPicker accepts per-model annotations (badge, note,
tone) rendered beside the model name and under its description in both
pinned sections and provider groups, with the badge and note folded into the
accessible option name. The Patrol model field and the shared default field
(when no Patrol override is set) pin guided models in a "Suggested for
Patrol" section, repeat the selected model's marker under the closed picker,
and render the server-computed Patrol cost preview: monthly estimate,
per-run assumption with tokens explained once, 30-day spend against budget,
and the schedule recommendation. The Patrol schedule select prices each
preset from the same projection and the schedule card explains a schedule
that Pulse slowed for a per-token model; a schedule the install already
chose is never changed. Dollars and prices never derive from model names in
the settings surface; useAISettingsState fetches
/api/ai/patrol/cost-preview and /api/ai/patrol/model-guidance and
aiPatrolCostPresentation.ts owns the copy. AIModelPicker.test.tsx,
AISettings.test.tsx, and settingsArchitecture.test.ts pin those
distinctions.
Patrol weekly digest card is a read-only summary
The Patrol Activity tab gains PatrolWeeklyDigestCard ("This week") above
Verified outcomes. It renders the server-computed GET /api/ai/patrol/digest
rollup as definition-list stat tiles built from the shared Button and
ButtonLink primitives and the existing surface, border, and muted text
tokens; it introduces no new shared primitive, theme token, or layout helper.
The only navigation it offers is the existing /actions route, shown only when
Patrol-origin fixes are waiting for approval. Loading, failed-load, no-runs,
and truncated-history states carry distinct copy, and a failed load never
renders zero counts as if the week were quiet. Browser proof covers the desktop
and narrow Activity tab in frontend-modern/browser-verification.json.
Phone Docker update labels preserve page-owned scrolling
Below the 33.999rem table-container boundary, Docker update cells reduce inline
padding and wrap existing badge/button content within the allocated cell. The
rule is scoped to docker-container-update-cell; it must not restore a nested
horizontal scrollport or change app-shell touch handling. Phone platform wrappers
retain overflow: clip. Labels remain text, not icon-only substitutes.
App.architecture.test.ts protects the CSS scope and scroll ownership; 96-navigation-socket-recovery.spec.ts checks real 390px text ranges and the non-scrolling wrapper. Browser receipts do not establish physical Android touch behaviour or every asynchronous update state.
The explicit issue explanation journey is qualified separately from provider
reasoning and real remediation in
docs/qualification/PATROL_ASSISTANT_CUSTOMER_JOURNEY.md. The repeatable browser
proof is scripts/check-patrol-assistant-journey.mjs. A passing scripted
response does not establish a useful customer outcome or model qualification.
Alert health attention preserves asynchronous ownership
The existing delivery-health card and shared buttons consume only the latest
started health read's state. Configuration Retry can overlap a disabled card
refresh; disabling that button is not a concurrency guard. Older completions
must neither clear the latest request's busy flag nor replace its attention or
unavailable presentation. Existing danger tone, accessible alert role, labels,
confirmation and wrapping controls remain unchanged; no new primitive is added.
The focused hook/caller tests and scripts/check-delivery-health-ordering.mjs
cover this dependency at desktop and narrow widths using scripted health and
queue-action responses, without claiming backend notification delivery.
Alert status distinguishes dispatch from destination evidence
The active alert card renders a valid diagnosis lastNotified timestamp as
“Dispatch requested”, never “Notified”: the alert manager records this field
before invoking delivery callbacks. A cooldown's nextEligibleAt is labelled
“next eligible”, not a promised send time. Neither field proves destination
acceptance or recipient receipt; that evidence must not be inferred from the
muted presentation tone. Missing or invalid timestamps retain the existing
pending/cooldown fallback; acknowledged alerts retain their badge without a
second status line. No API field, notification policy or shared primitive changes.
The existing wrapping status text must remain readable at desktop and phone
widths despite the longer labels. The presentation and Overview delivery-status
tests cover the evidence boundary; scripts/check-alert-dispatch-copy.mjs
qualifies the real Overview with scripted API data in Chromium, not installed
notification delivery.
Resource incident reads retain lifecycle ownership
The resource incident hook gives each started read a unique per-resource owner. Only that owner may publish history, set the failed-read state, report a failure or clear loading. Reset invalidates all pending owners before clearing state; disposal invalidates them and prevents new loads. Overlapping reads for different resources remain independent. Closing a row still permits its in-flight result to populate the existing cache; reopening cached history and explicit refresh are unchanged. Requests are not transport-cancelled. No API, retention or notification-delivery policy changes. A retry clears the current failed-read state while retaining cached history until the owning request succeeds, and a superseded success cannot clear a newer failure.
The hook's ten ordinary regression/control cases cover success, catch and
finally writes, reset/reopen and disposal. The existing panel tests cover its
presentation. scripts/check-incident-request-ownership.mjs exercises the real
hook and panel in Chromium at desktop and phone widths with scripted responses
and fixture reset, overlap and unmount controls. It is component lifecycle
acceptance, not an installed full-page or notification-delivery receipt.
The incident-history continuation also projects retained desktop expansion into the mobile drawer at the shared CSS breakpoint and keeps expansion state on return to desktop. The mobile action label describes the visible drawer. Operator note text and attribution survive the Assistant handoff while raw command output remains excluded. Shared event cards override inherited table no-wrap styling, and notes preserve line breaks. Long notes must remain readable in both inline desktop history and the mobile drawer. Final source-bound browser/model qualification is recorded in the customer-journey document.
The shared type-to-search registry excludes inputs in inert modal backgrounds, including prepared shortcut targets. Escape belongs to the active dialog and must not clear a background history filter or invalidate its return-focus target. Ordinary search shortcuts resume when the background becomes interactive again. When Assistant is already open, a desktop-to-phone transition must retain that destination rather than reopening the underlying history drawer above it.
Compact Organization header wrapping
The shared header and its controls must wrap below the small-screen breakpoint instead of forcing the document wider when an entitled Organization selector joins the session controls. Keep the selector, kiosk/logout controls and connection indicator available; do not hide overflow to conceal an inaccessible action. Desktop grid placement remains unchanged.
Proof: AppLayout.test.tsx pins wrapping and retained logout semantics. The
signed-offline 05-settings-mobile-audit.spec.ts exercises real Organization,
Access and Sharing routes at 320px and 390px, with app-shell width and full-scroll
assertions; header screenshots retain the compact layout. This is independent
of private RBAC implementation and hosted probation acceptance.
Alert history clear/read ordering
A successful history clear invalidates reads started before its completion and settles their loading state, so delayed responses cannot repopulate deleted history rows. Failed clearing leaves the pending read valid; later range refreshes remain available. This is view-state ordering, not a change to retention, active alerts, acknowledgement or notification recovery semantics.
The deferred-response cases in useAlertHistoryState.test.tsx verify successful and failed clearing plus subsequent range refresh. The registered scripts/check-incident-request-ownership.mjs browser proof also exercises the real history hook and administration card: load a row, start a pending range read, confirm clear, then release the obsolete response at desktop and phone widths. Scripted API responses establish component behaviour, not installed backend deletion or destination delivery.
Large platform notices keep the inventory in view
PlatformOutdatedAgentNotice previews at most three affected names and exposes
the full list through a keyboard-operable button when more hosts are outdated.
The count, update guidance and action link remain visible. This keeps a
large-estate stale-agent warning from pushing the platform inventory and
Storage search below the phone viewport while preserving every affected name
on demand. The component test pins collapsed, expanded and collapsed-again
states; 1440px, 768px and 390px browser checks verify placement and overflow.
The shared InlineNotice action text uses opaque 800-level colors for its four
tones. The current Tailwind configuration overrides several 900-level palette
tokens with 25%-alpha colors for translucent backgrounds, so using those
tokens for notice links made a working action look disabled. The browser proof
also follows the outdated-agent action to Agent Doctor with all 49 host IDs.
The broader palette override should be corrected in its own shared-design
slice, with background users migrated to explicit alpha utilities so other
900-level text consumers can use normal opaque color semantics.
Actions empty state follows the AI capability
getActionsWatchOnlyEmptyState takes an explicit aiEnabled input, which
pages/Actions.tsx fills from the assistantEnabled session capability. While
AI is off it returns no guidance, so the empty Open inbox keeps its plain copy
instead of saying Patrol runs in Watch only mode or pitching Pro Patrol modes
for a feature that is not running (issue #905). With AI on, the Watch only,
switch and upgrade branches are unchanged. actionsWatchOnlyEmptyState.test.ts
pins both states and the page wiring.
Pulse Mobile settings section label
The system-relay settings section is labelled Pulse Mobile in the nav
catalog, the header metadata and every locale catalog; the product name is not
translated. Its route id, feature gate and read capability are unchanged.
Relay never provided remote access to the web UI, so no settings chrome may
label it Remote Access.
Drawer History is inspectable without pointer hover
GuestDrawerHistory exposes a labelled native range input for groups with
multiple stored observation times. Native arrow keys, Home/End and touch input
select real stored timestamps; the control's value text includes the full local
date/time and separately formatted series values. A series without a sample at
the selected time remains missing rather than borrowing a neighbouring or live
reading. The SVG has a linked textual description, including lone observations
and the absence of stored data; a lone observation does not fabricate a trend.
Selection follows its timestamp across same-source refreshes, not an ordinal index that shifts when samples arrive. An expired selection snaps to an actual remaining observation. Changing resource type, id or range clears pointer and keyboard selection even when matching cached data arrives immediately. Empty, failed initial, absent-target and licence-locked views expose no inspection control. Existing matching-point retention and scoped retry remain unchanged. Mounted inspection regressions cover these boundaries. The direct-renderer browser fixture verifies native keys, focus, pointer coexistence and touch at desktop/phone widths across Chromium, Firefox and WebKit; it is not installed PBS collection or a screen-reader announcement-quality claim.
Drawer History pointer values share one observed time
Pointer inspection snaps to the nearest actual stored timestamp across the group's series, with equidistant observations resolved to the earlier time. Every displayed value and marker must belong to that timestamp. Missing series remain unavailable at the inspected time, not a nearest neighbour, latest observation or current reading. A lone stored sample, including zero, can be inspected without fabricating a trend. The SVG's dated description follows the same active time as the visible legend. Leaving the plot restores the normal latest/current legend; focused native inspection still takes precedence.
Matching refreshes reconcile the pointer with the current set of actual
observations; resource/range replacement still clears pointer state. Mounted
GuestDrawerHistory.pointer.test.tsx regressions cover sparse and disjoint
series, zero rates, single observations, ties, focus precedence and replacement.
browser-tests/history-pointer.cjs verifies the production renderer and query
in Chromium/Firefox desktop and Chromium/WebKit phone emulation in both themes,
including failed refreshes and source replacement. Scripted APIs establish
presentation accuracy, not real PBS collection or installed #1723 acceptance.
Drawer History panels share a dated time window
GuestDrawerHistory uses one common time interval for every configured metric
group, including utilization, network, disk I/O and thermals. The fulfilled
API response's valid start/end interval remains in view even if it contains
only a few minutes of observations. A sample at a given timestamp occupies the
same horizontal position in every panel; a group's sparse coverage must not
stretch independently to fill the selected range. Visible date/time endpoints
and full local timestamps in their accessible labels distinguish overnight and
multi-day windows. Native inspection still selects actual observations.
Returned edge observations widen the common envelope instead of being clipped or discarded, including aggregated bucket timestamps. An invalid API interval falls back to the valid observed envelope across configured groups, not a fabricated range. Non-date timestamps and unconfigured metrics cannot poison that geometry. A valid empty window has labelled endpoints but no trend or inspection control. Failed matching refreshes retain the window with its data; target/range replacement clears both until matching data arrives.
GuestDrawerHistory.window.test.tsx pins geometry, labels, refresh/replacement,
empty/invalid windows and edge observations. browser-tests/history-window.cjs
uses the production PBS table, resource drawers, History query and CSS with
synthetic APIs, checking three separately mapped drawers and range/refresh
behaviour in desktop and phone-emulated engines. This is presentation proof,
not installed PBS/VirtualBox collection or a complete #1723 acceptance result.