Retire dashboard landing surface

This commit is contained in:
rcourtman
2026-04-29 16:22:40 +01:00
parent cc470635b9
commit c743a231b8
248 changed files with 2525 additions and 11412 deletions
+1 -2
View File
@@ -97,7 +97,7 @@ All backend code lives under `cmd/`, `internal/`, and `pkg/`. The binary is asse
9. **Recovery Engine (`internal/recovery`)**
- Aggregates backup data (PBS snapshots, ZFS snapshots, replication tasks) into a unified recovery point timeline.
- Provides faceted queries: filter by type, source, status. Rollup summaries for dashboards.
- Provides faceted queries: filter by type, source, status, and source health.
- API: `/api/recovery/points`, `/api/recovery/series`, `/api/recovery/facets`, `/api/recovery/rollups`.
10. **Audit Logging (`pkg/audit`)**
@@ -137,7 +137,6 @@ Navigation is organised by **task**, not by platform:
| `/storage` | Storage | Proxmox storage, ZFS pools, Ceph |
| `/recovery` | Recovery | Backups, snapshots, replication |
| `/ceph` | Ceph | Detailed Ceph cluster view |
| `/dashboard` | Dashboard | Summary panels and metrics |
| `/alerts/*` | Alerts | Alert rules, active alerts, history |
| `/ai/*` | AI Intelligence | Patrol findings, investigations, forecasts |
| `/settings/*` | Settings | Configuration, security, diagnostics, reporting, AI, relay |
+3 -3
View File
@@ -16,11 +16,11 @@
## 🚀 Overview
Pulse is a modern, unified dashboard for monitoring your **infrastructure** across Proxmox, Docker, Kubernetes, and TrueNAS. It consolidates metrics, alerts, and AI-powered insights from all your systems into a single, beautiful interface.
Pulse is a modern, unified monitoring workspace for your **infrastructure** across Proxmox, Docker, Kubernetes, and TrueNAS. It consolidates metrics, alerts, and AI-powered insights from all your systems into a single, beautiful interface.
Designed for homelabs, sysadmins, and MSPs who need a "single pane of glass" without the complexity of enterprise monitoring stacks.
![Pulse Dashboard](docs/images/01-dashboard.jpg)
![Pulse Infrastructure](docs/images/01-dashboard.jpg)
## 🧭 Unified Navigation
@@ -94,7 +94,7 @@ docker run -d \
rcourtman/pulse:vX.Y.Z
```
Access the dashboard at `http://<your-ip>:7655`.
Open Pulse at `http://<your-ip>:7655`.
## Local Development
+1 -1
View File
@@ -176,7 +176,7 @@ A typical team configuration:
|------|------|--------|
| alice | `admin` | Full access to everything |
| bob | `operator` | Can view nodes/VMs and manage alerts |
| carol | `viewer` | Read-only access to dashboards and metrics |
| carol | `viewer` | Read-only access to monitoring views and metrics |
| monitoring-bot | API token with `alerts:read` scope | Automated alert polling |
---
@@ -0,0 +1,44 @@
# Infrastructure Default Landing Dashboard Retirement
Date: 2026-04-29
Lane: First-session post-RC polish
Follow-up: `first-session-post-rc-polish`
Assertion: `infrastructure-default-landing-surface`
## Outcome
Pulse v6 now treats Infrastructure as the authenticated landing surface. The old Dashboard route is retired instead of preserved as a compatibility wallboard: primary navigation no longer includes Dashboard, root runtime routing sends authenticated operators to Infrastructure, and first-session/setup completion handoffs now point into Infrastructure or Add infrastructure.
The Dashboard-specific overview stack is also removed rather than rebranded locally. Workload table code that still belongs to the product is owned under `frontend-modern/src/components/Workloads/`, and the deleted Dashboard overview API (`/api/resources/dashboard-summary`) is absent from the router, handler inventory, client API, and route tests.
## Proof
- `npm --prefix frontend-modern run type-check`
- Result: pass.
- `npm --prefix frontend-modern run lint`
- Result: pass; ESLint, theme audit, and canonical platform audit completed.
- `npm --prefix frontend-modern test -- src/components/Workloads src/pages/__tests__/Workloads.helpers.test.ts src/utils/__tests__/frontendResourceTypeBoundaries.test.ts src/utils/__tests__/workloadEmptyStatePresentation.test.ts src/utils/__tests__/workloadGuestPresentation.test.ts src/__tests__/App.architecture.test.ts src/__tests__/AppLayout.test.tsx src/routing/__tests__/navigation.test.ts`
- Result: pass, 36 files and 563 tests.
- `go test ./internal/api -run 'Test(RouterRouteInventory|RouteInventoryContractCoversAllRouteModules|RouterFrontendRouteInventory|ResourceList|ResourceGet|ResourceAndStorageResponses|ResourceContract|ApplyResource|ComputeResource|UnsupportedResourceType|ParseResource|SecurityStatus|SecurityTokens|RouterCSRF|AdminEndpoints|SSO|OIDC|SAML|MagicLink|HandlePublicMagicLink|EstablishOIDCSession|EstablishSAMLSession|BuildSSOOIDCCallbackURL|SanitizeOIDCReturnTo|RedirectOIDC|RedirectSAML|Contract_Resource|Contract_Security|Contract_ResetFirstRunSecurity)' -count=1`
- Result: pass.
- `python3 scripts/release_control/contract_audit.py --pretty`
- Result: pass.
- `python3 scripts/release_control/status_audit.py --pretty`
- Result: pass.
- `python3 scripts/release_control/registry_audit.py --pretty`
- Result: pass.
- `python3 scripts/release_control/canonical_completion_guard.py`
- Result: pass.
- `python3 scripts/release_control/canonical_completion_guard_test.py`
- Result: pass, 136 tests.
- `python3 scripts/release_control/subsystem_lookup_test.py`
- Result: pass, 181 tests.
- Browser proof on the local v6 app:
- Authenticated `/` redirected to `/infrastructure`.
- `/infrastructure` rendered the Infrastructure heading with no Dashboard primary-nav tab.
- `/dashboard` rendered the Not Found page with no Workloads surface and no Dashboard primary-nav tab.
## Verification Residual
- `go test ./internal/api -count=1`
- Result: fail outside this slice. `TestSLO_WorkloadCharts` exceeded its strict local p95 SLO under package load (`p95=92.83075ms` against a 90ms target), and `TestSLO_WorkloadsSummaryCharts` exceeded the same local p95 SLO (`p95=135.267ms` against a 90ms target). The focused API route/resource/auth contract tests above are green.
+15 -15
View File
@@ -2896,21 +2896,6 @@
"performance-and-scalability"
],
"evidence": [
{
"repo": "pulse",
"path": "frontend-modern/src/components/Dashboard/__tests__/Dashboard.performance.contract.test.tsx",
"kind": "file"
},
{
"repo": "pulse",
"path": "frontend-modern/src/components/Dashboard/Dashboard.tsx",
"kind": "file"
},
{
"repo": "pulse",
"path": "frontend-modern/src/components/Dashboard/workloadSelectors.ts",
"kind": "file"
},
{
"repo": "pulse",
"path": "frontend-modern/src/components/Infrastructure/__tests__/UnifiedResourceTable.performance.contract.test.tsx",
@@ -2931,6 +2916,21 @@
"path": "frontend-modern/src/components/Infrastructure/UnifiedResourceTable.tsx",
"kind": "file"
},
{
"repo": "pulse",
"path": "frontend-modern/src/components/Workloads/__tests__/WorkloadsSurface.performance.contract.test.tsx",
"kind": "file"
},
{
"repo": "pulse",
"path": "frontend-modern/src/components/Workloads/workloadSelectors.ts",
"kind": "file"
},
{
"repo": "pulse",
"path": "frontend-modern/src/components/Workloads/WorkloadsSurface.tsx",
"kind": "file"
},
{
"repo": "pulse",
"path": "internal/api/http_metrics.go",
@@ -307,16 +307,16 @@ helper instead of carrying lifecycle-local background or hover classes.
That same presentation-only boundary must preserve canonical millisecond timestamps when it serializes chart points, so lifecycle-adjacent first-host and fleet surfaces do not misread rounded chart samples as duplicate or restarted heartbeat evidence.
The same rule now applies to storage summary interaction. Shared sticky-card or row-hover focus behavior on infrastructure, workloads, and storage may reuse the canonical chart transport, but lifecycle-adjacent install, enrollment, and fleet surfaces must not treat highlighted summary series or sticky-shell state as agent freshness or setup progress.
The same rule now applies to infrastructure-summary metric filters. Shared
dashboard and infrastructure consumers may narrow the canonical
infrastructure and other route-owned consumers may narrow the canonical
`/api/charts/infrastructure` payload with a `metrics` query for
presentation hot paths, but lifecycle surfaces must not reinterpret
omitted disk or network series as missing lifecycle telemetry, missing
agent capabilities, or reduced fleet freshness truth.
The same rule now applies to compact dashboard summary payloads. Shared
`internal/api/resources.go` summary routes may collapse resource counts,
problem rows, and top-resource rankings for dashboard hot paths, but
lifecycle surfaces must not treat `/api/resources/dashboard-summary` as
install inventory authority, enrollment proof, or fleet freshness truth.
The same rule now applies to retired compact dashboard summary payloads.
Shared `internal/api/resources.go` routes must not restore
`/api/resources/dashboard-summary` as a compatibility read; lifecycle
surfaces must continue to use install inventory, enrollment proof, and
fleet freshness truth from their owning contracts.
The same presentation-only boundary now covers compact storage summary
chart reads as well. Shared `/api/charts/storage-summary` transport may
request only the canonical `used` and `avail` storage series needed for the
@@ -597,8 +597,8 @@ helper instead of carrying lifecycle-local background or hover classes.
platform-management shell. The secondary agent handoff must land on
`/settings/infrastructure?add=agent`.
When the first host reports successfully, the install workflow must treat
that as a completion handoff with direct navigation into `/dashboard` and
`/settings/infrastructure/operations` instead of leaving operators on a
that as a completion handoff with direct navigation into `/infrastructure`
and `/settings/infrastructure/operations` instead of leaving operators on a
generic lookup result. When the workspace starts from zero active connected
infrastructure and install commands are available, the same lifecycle path
must auto-watch the canonical `/api/state` projection for the first
@@ -247,7 +247,7 @@ assistant drawer may dock and push the authenticated shell only when the
viewport is wide enough to preserve a usable primary operating surface; once
the available viewport drops below that shell threshold, the drawer must
become an overlay owned by `frontend-modern/src/components/AI/Chat/index.tsx`
instead of compressing Infrastructure, Dashboard, Storage, or other primary
instead of compressing Infrastructure, Workloads, Storage, or other primary
runtime pages into an unusable narrow column or forking page-local layout
exceptions.
The closed assistant launcher follows the same shared-shell rule. While the
@@ -875,19 +875,19 @@ The `/api/ai/intelligence/changes` endpoint should also route through the
canonical unified-intelligence recent-change accessor before any
patrol-local detector fallback, so the API surface reads the same unified
timeline source that powers the summary payload.
Dashboard Pulse Brief context follows the same monitoring-first AI boundary:
Pulse may surface an Assistant-ready operator paragraph on the dashboard, but
the first render must be fact-bound to governed dashboard, Patrol, storage,
recovery, and alert summaries and must pass those facts as structured context
when the operator asks Assistant to continue. Future server-generated dashboard
briefs must keep that structured fact contract and policy boundary rather than
letting an unbounded prompt become the dashboard's source of truth.
The dashboard-to-Assistant handoff must also keep its execution mode scoped to
the request. When a Pulse Brief opens Assistant from the dashboard, the drawer
may prefill the governed dashboard prompt and context, but the submitted chat
request must set `autonomous_mode:false`, preserve the operator's persistent
Assistant control-level setting, and disclose the temporary approval-required
mode in the drawer instead of showing the generic Autonomous warning.
Retired dashboard Pulse Brief context follows the same monitoring-first AI
boundary in negative space: `frontend-modern/src/features/dashboardOverview/`
and the Dashboard route must not be restored just to create an Assistant-ready
operator paragraph. Future overview or brief surfaces need a governed product
owner first, must pass fact-bound structured context from owning Infrastructure,
Workloads, Patrol, storage, recovery, and alert summaries, and must not let an
unbounded prompt become a route's source of truth.
Future route-to-Assistant handoffs must also keep their execution mode scoped
to the request. When an overview brief opens Assistant, the drawer may prefill
only governed prompt/context data, but the submitted chat request must set
`autonomous_mode:false`, preserve the operator's persistent Assistant
control-level setting, and disclose the temporary approval-required mode in
the drawer instead of showing the generic Autonomous warning.
Those backend AI and Patrol change summaries should derive their canonical
labels and provenance fragments from
`internal/unifiedresources/change_presentation.go`, so the resource-model
@@ -25,7 +25,7 @@ operator-facing alert routing behavior for live runtime alerts.
3. `internal/alerts/canonical_metric.go`
4. `internal/alerts/canonical_lifecycle.go`
5. `internal/alerts/unified_incidents.go`
6. `frontend-modern/src/components/Alerts/RecentAlertsPanel.tsx`
6. `frontend-modern/src/features/alerts/AlertOverviewActiveAlertsSection.tsx`
7. `frontend-modern/src/utils/alertOverviewPresentation.ts`
8. `frontend-modern/src/utils/alertResourceTablePresentation.ts`
9. `frontend-modern/src/utils/alertDestinationsPresentation.ts`
@@ -501,31 +501,25 @@ inline in `frontend-modern/src/features/alerts/OverviewTab.tsx`.
The canonical shared alert-acknowledgement runtime owner is now
`frontend-modern/src/features/alerts/useAlertAcknowledgementState.ts`, which
owns optimistic single/bulk acknowledge control flow, restore behavior, and
notification feedback for both
`frontend-modern/src/features/alerts/useAlertOverviewState.ts` and
`frontend-modern/src/components/Alerts/RecentAlertsPanel.tsx`.
notification feedback for `frontend-modern/src/features/alerts/useAlertOverviewState.ts`
and the alert overview render shells.
`frontend-modern/src/features/alerts/useAlertOverviewState.ts` now owns the
derived alert read-model and Last 24 Hours stat refresh for
`frontend-modern/src/features/alerts/OverviewTab.tsx`, while composing that
shared acknowledgement owner instead of keeping its own alert mutation fork.
Future overview or dashboard recent-alert action behavior should extend that
shared acknowledgement hook instead of putting acknowledge mutations back into
either render shell.
Future overview action behavior should extend that shared acknowledgement hook
instead of putting acknowledge mutations back into render shells.
Render-heavy alert overview ownership now routes through
`frontend-modern/src/features/alerts/AlertOverviewStatsCards.tsx`,
`frontend-modern/src/features/alerts/AlertOverviewActiveAlertsSection.tsx`,
and `frontend-modern/src/features/alerts/AlertOverviewAlertCard.tsx` instead
of rebuilding stats-card, active-alert, and timeline-card presentation inline
inside `frontend-modern/src/features/alerts/OverviewTab.tsx`.
Dashboard recent-alert rendering and dashboard alert summary/tone copy now
route through that same alert overview presentation owner and the alert-owned
`frontend-modern/src/components/Alerts/RecentAlertsPanel.tsx` surface instead
of living as a dashboard-page-local panel plus a second dashboard-only alert
presentation helper.
That same dashboard recent-alert surface must rank structural health warnings
above state-only warning messages such as powered-off, stopped, or paused
workloads before falling back to recency, so chosen workload state does not
hide active infrastructure or storage problems on the first dashboard screen.
The retired dashboard recent-alert panel must not be reintroduced as a
parallel alert surface. Alert summary/tone copy belongs to the alert overview
presentation owner, and any future compact alert surface must compose the
shared alert read-model and acknowledgement hook rather than creating a
dashboard-only panel.
Alert threshold and schedule surfaces must now also treat
`discoveryTarget` as optional frontend input and keep grouping-card state on
@@ -428,22 +428,21 @@ the canonical monitored-system blocked payload.
`internal/api/router_routes_monitoring.go`, `internal/api/router.go`,
`internal/api/types.go`, and `internal/api/contract_test.go` must route
optional infrastructure-summary `metrics` filters through one governed
transport contract, so dashboard-specific consumers can request only CPU
and memory without inventing a second summary endpoint or silently widening
back to disk/network payloads. The same contract must carry those requested
metric filters through the shared guest-chart batch loader in
transport contract, so route-owned consumers can request only the series
they render without inventing a second summary endpoint or silently
widening back to disk/network payloads. The same contract must carry those
requested metric filters through the shared guest-chart batch loader in
`internal/monitoring/monitor_metrics.go` instead of fetching the full guest
metric set and trimming after the API payload is already assembled.
37. Keep the compact dashboard overview route canonical on that same shared API
surface. `internal/api/resources.go`,
`internal/api/router_routes_monitoring.go`,
`frontend-modern/src/api/resources.ts`,
`frontend-modern/src/hooks/useDashboardOverview.ts`, and frontend dashboard
consumers must route KPI cards, problem-resource rows, governed resource
labels, top-infrastructure identity, and canonical metrics-target join keys
through `/api/resources/dashboard-summary` instead of reconstructing that
shell from the paginated `/api/resources` list payload or guessing how
dashboard trend identities map onto infrastructure chart series.
37. Keep the retired compact dashboard overview route absent from that same
shared API surface. `internal/api/resources.go`,
`internal/api/router_routes_monitoring.go`, and
`frontend-modern/src/api/resources.ts` must not restore
`/api/resources/dashboard-summary`, `useDashboardOverview`, or frontend
dashboard consumers as compatibility paths for KPI cards, problem-resource
rows, governed resource labels, top-infrastructure identity, or metrics-
target join keys. New summary payloads must be owned by their product
route and pinned in the API contract there.
38. Keep mock and demo chart reads on the same canonical unified snapshot as
the rest of the API surface. `internal/api/router.go`,
`internal/api/contract_test.go`, and chart consumers must route
@@ -594,7 +593,7 @@ the canonical monitored-system blocked payload.
boundary: when
`frontend-modern/src/components/Settings/useInfrastructureInstallState.tsx`
receives a successful connected-agent lookup result, the canonical install
flow must expose direct navigation into `/dashboard` and
flow must expose direct navigation into `/infrastructure` and
`/settings/infrastructure/operations` rather than leaving the operator on a
transport-only status readout.
21. Keep the shared first-host detection contract explicit on `/api/state` as
@@ -699,7 +698,7 @@ the canonical monitored-system blocked payload.
the status payload flips `hasAuthentication` to `false`, preserves
`bootstrapTokenPath`, and allows browser-owned first-session proof to
re-enter the real setup wizard instead of silently falling back to an
authenticated dashboard state. That recovery transport may expose the
already-authenticated app state. That recovery transport may expose the
bootstrap token file path, but it must not emit the token value into
automatic runtime logs.
30. Keep shared SSO test and metadata-preview transport fail-closed: SAML
@@ -924,7 +923,7 @@ that internal grant.
That same shared API contract now also owns browser-proofed read separation.
Non-billing browser journeys such as
`tests/integration/tests/11-first-session.spec.ts`,
`tests/integration/tests/journeys/01-smoke-bootstrap-login-dashboard.spec.ts`,
`tests/integration/tests/journeys/01-smoke-bootstrap-login-infrastructure.spec.ts`,
and `tests/integration/tests/journeys/03-relay-pairing.spec.ts` may call
`/api/license/runtime-capabilities` for feature truth, but they must assert
zero browser requests to `/api/license/entitlements`. Billing activation,
@@ -1604,9 +1603,9 @@ That correlations route now reads through the canonical AI intelligence
facade first, so the handler and its payload keep the detector behind one
shared access layer instead of routing directly to Patrol-local correlation
state.
That store now also owns the dashboard load bundle used by the Patrol page,
so the page refresh path stays aligned on one store-owned orchestration layer
instead of re-encoding the AI bundle inline.
That store now also owns the Patrol page load bundle, so the page refresh path
stays aligned on one store-owned orchestration layer instead of re-encoding
the AI bundle inline.
The AI summary page now also renders the canonical
`frontend-modern/src/components/Infrastructure/ResourcePolicySummary.tsx`
card for policy posture, so sensitivity, routing, and redaction counts are
@@ -267,7 +267,7 @@ runtime gating as separate unlinked claims.
they must not turn `trial_eligible`, `trial_eligibility_reason`, or an
expired trial marker into a default Pro CTA or banner.
17. Add or change monitored-system ledger, disclosure, or admission-preview presentation through `frontend-modern/src/components/Settings/MonitoredSystemLedgerPanel.tsx`, `frontend-modern/src/components/Settings/MonitoredSystemAdmissionPreview.tsx`, `frontend-modern/src/components/Commercial/MonitoredSystemDefinitionDisclosure.tsx`, and `frontend-modern/src/utils/monitoredSystemPresentation.ts`
18. Add or change paid relay settings and pairing presentation through `frontend-modern/src/components/Settings/RelaySettingsPanel.tsx`, `frontend-modern/src/components/Settings/RelayPairingSection.tsx`, and `frontend-modern/src/components/Settings/useRelaySettingsPanelState.ts`. The Dashboard shell must not carry a Relay onboarding card or equivalent blanket upsell — relay discovery stays inside its owning settings surface.
18. Add or change paid relay settings and pairing presentation through `frontend-modern/src/components/Settings/RelaySettingsPanel.tsx`, `frontend-modern/src/components/Settings/RelayPairingSection.tsx`, and `frontend-modern/src/components/Settings/useRelaySettingsPanelState.ts`. The retired Dashboard shell must not be restored to carry a Relay onboarding card or equivalent blanket upsell — relay discovery stays inside its owning settings surface.
Public demo and other read-only presentation policy states must suppress
relay setup and upsell onboarding instead of inviting pairing or commercial
action from a governed non-manageable surface.
@@ -359,11 +359,11 @@ runtime gating as separate unlinked claims.
RBAC, SSO, audit logging, reporting, and agent profiles may remain present,
but they are secondary included value and must not displace that operator
outcome framing on owned commercial surfaces.
24. Keep public-demo dashboard bootstrap route-owned on the adjacent
24. Keep public-demo route bootstrap owned on the adjacent
commercial/runtime boundary. `frontend-modern/src/useAppRuntimeState.ts`
may prewarm shared infrastructure summary caches for non-dashboard routes,
but public-demo dashboard arrival must not front-run a broader
infrastructure-summary fetch than the route actually renders. Commercial
may prewarm shared infrastructure summary caches for route-owned surfaces,
but public-demo arrival must not front-run a broader infrastructure-summary
fetch than the route actually renders. Commercial
posture on `v6-demo` therefore stays governed by the route-owned
presentation policy and summary scope rather than by app-shell-wide
bootstrap heuristics. That same app-shell boundary must also keep the
@@ -641,7 +641,7 @@ contract, but shared billing and upgrade surfaces now hide or suppress
themselves from the resolved `presentationPolicy` payload instead of teaching
mock mode, response-header inference, or frontend-only feature flags to
bypass the real licensing model. That includes settings billing tabs,
public-demo banner and monitored-system/trial nudges, dashboard relay
public-demo banner and monitored-system/trial nudges, app-wide relay
paywalls, Patrol upgrade CTAs, and history-lock upsells. Demo readiness
therefore means presentation isolation, not a license exemption.
That same public-demo boundary now also owns route-level commercial
@@ -713,12 +713,12 @@ surfaces such as `frontend-modern/src/components/Settings/useProLicensePanelStat
may still force refresh through the same shared store when plan, activation, or
recovery actions mutate commercial truth.
Non-billing browser journeys must also stay off
`/api/license/entitlements` entirely. Dashboard, infrastructure, alerts,
`/api/license/entitlements` entirely. Infrastructure, Workloads, alerts,
first-session gated routes, and relay settings must take feature truth from
`/api/license/runtime-capabilities` or already loaded commercial posture, and
the governed browser proof in
`tests/integration/tests/11-first-session.spec.ts`,
`tests/integration/tests/journeys/01-smoke-bootstrap-login-dashboard.spec.ts`,
`tests/integration/tests/journeys/01-smoke-bootstrap-login-infrastructure.spec.ts`,
and `tests/integration/tests/journeys/03-relay-pairing.spec.ts` must continue
to assert zero browser-shell entitlement requests outside owned billing flows.
That same authenticated-shell commercial boundary may surface prerelease
@@ -1375,12 +1375,12 @@ That same authenticated route shell also owns the canonical post-auth landing
path. `frontend-modern/src/App.tsx` and
`frontend-modern/src/pages/RuntimeHome.tsx` must send authenticated `/`
through the runtime-home landing contract first: existing operators and
self-hosted sessions land on the governed dashboard route, while hosted
workspaces with no connected infrastructure forward into the canonical
infrastructure onboarding contract before the workspace normalizes back to the
single `/settings/infrastructure` shell. That same shared landing contract
must not regress into a root-only redirect straight to the infrastructure
workspace or a dashboard-only shortcut that strands first-time hosted tenants.
self-hosted sessions land on the governed Infrastructure route, while hosted
workspaces with no connected infrastructure stay on the same Infrastructure
and Add infrastructure ownership path before the workspace normalizes back to
the single `/settings/infrastructure` shell. That same shared landing contract
must not regress into a root-only settings redirect or a dashboard-only
shortcut that strands first-time hosted tenants.
That same landing contract also owns authenticated `/login`: once the browser
has a valid session, `frontend-modern/src/App.tsx` must route `/login`
through that same runtime-home landing boundary instead of leaving the
@@ -1580,10 +1580,11 @@ license state for Community users and leave discovery of paid tiers to
surfaces outside the plan page.
That same no-funnel rule extends beyond Settings -> Plan. The self-hosted
frontend must not render blanket trial/upgrade marketing to Community users
from the main dashboard shell, the setup wizard completion screen, or any
from the main app shell, the setup wizard completion screen, or any
app-wide surface that fires without the user having explicitly engaged with
a paid feature. In particular the Dashboard overview may not carry a
`RelayOnboardingCard` paywall or equivalent `Start trial` prompt, the
a paid feature. In particular the retired Dashboard overview must not be
restored to carry a `RelayOnboardingCard` paywall or equivalent `Start trial`
prompt, the
`SetupCompletionPanel` may not carry a `Monitor from Anywhere` Relay trial
block, and no time-triggered "active use" nudge such as `ActiveUseTrialNudge`
may auto-appear for Community users. Feature-gated discovery that fires only
@@ -1673,10 +1674,10 @@ owns relay config/status polling and pairing runtime, and
`frontend-modern/src/components/Settings/RelayPairingSection.tsx` owns the QR
pairing surface. Future relay settings work must extend that split instead of
pulling polling and QR-generation lifecycle back into the shell component.
The Dashboard shell must not host a Relay onboarding card or equivalent
blanket Relay upsell. Relay discovery belongs to the owning Settings surface
above; the Dashboard stays a monitoring-first view with no app-wide paywall
onboarding composed into it.
The retired Dashboard shell must not be restored to host a Relay onboarding
card or equivalent blanket Relay upsell. Relay discovery belongs to the owning
Settings surface above; Infrastructure and Workloads stay monitoring-first
views with no app-wide paywall onboarding composed into them.
That relay pairing boundary now also includes backend-owned mobile credential
lifecycle: when the settings surface generates a mobile pairing QR, it must ask
the server for a fresh scoped Pulse Mobile relay access token, fetch the
@@ -2096,7 +2097,7 @@ canonical browser bootstrap signal, and shared billing or upgrade surfaces
must hide or suppress themselves from that contract rather than teaching mock
mode, response-header inference, or frontend-only feature flags to bypass the
real licensing model. That includes settings billing tabs, public-demo banner
and monitored-system/trial nudges, dashboard relay paywalls, Patrol upgrade
and monitored-system/trial nudges, app-wide relay paywalls, Patrol upgrade
CTAs, and history-lock upsells. Demo readiness therefore means presentation
isolation, not a license exemption.
That same bootstrap owner must also avoid protected pre-auth noise. When auth
@@ -247,9 +247,10 @@ server-side update execution surfaces.
7. Keep managed-runtime first-session helpers deterministic: shared browser
helpers under `tests/integration/tests/helpers.ts` may only drive the live
setup wizard through the current managed runtime after refreshing the
canonical dev reset route, and any helper changes that rely on hot-dev
browser/backend behavior must keep a managed-runtime recovery proof updated
in the same slice.
canonical dev reset route, authenticated completion must expect the
Infrastructure landing path rather than the retired `/dashboard` route, and
any helper changes that rely on hot-dev browser/backend behavior must keep a
managed-runtime recovery proof updated in the same slice.
8. Keep root-level Playwright wrapper routing on the canonical managed browser
truth. `playwright.config.ts`, `tests/integration/playwright.config.ts`,
and `tests/integration/tests/runtime-defaults.ts` must resolve the same
@@ -690,7 +691,8 @@ requests together with `page.request` or other direct API helper calls.
Managed runtime recovery and browser bootstrap proofs therefore need to keep
helper coverage that demonstrates browser-shell request tracking remains
trustworthy when the same test also performs direct health or security-status
API probes.
API probes, and that authenticated bootstrap does not fall back to the retired
Dashboard route.
`scripts/hot-dev-bg.sh` must also supervise `scripts/hot-dev.sh` in an isolated
child session so an unexpected owner-process death cannot leave orphaned
watchers or health monitors behind. When the supervisor replaces the managed
@@ -99,10 +99,10 @@ work extends shared components instead of creating new local variants.
73. `frontend-modern/src/utils/reportableResourceTypes.ts`
74. `frontend-modern/src/utils/reportingResourceTypes.ts`
75. `frontend-modern/src/utils/problemResourcePresentation.ts`
76. `frontend-modern/src/utils/dashboardEmptyStatePresentation.ts`
77. `frontend-modern/src/utils/dashboardGuestPresentation.ts`
78. `frontend-modern/src/utils/dashboardKpiPresentation.ts`
79. `frontend-modern/src/utils/dashboardTrendPresentation.ts`
76. `frontend-modern/src/utils/workloadEmptyStatePresentation.ts`
77. `frontend-modern/src/utils/workloadGuestPresentation.ts`
78. `frontend-modern/src/utils/emptyStatePresentation.ts`
79. `frontend-modern/src/utils/semanticTonePresentation.ts`
80. `frontend-modern/src/components/Toast/Toast.tsx`
81. `frontend-modern/src/utils/toast.ts`
82. `frontend-modern/src/utils/semanticTonePresentation.ts`
@@ -194,10 +194,10 @@ work extends shared components instead of creating new local variants.
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.
Dashboard overview primitives under `frontend-modern/src/features/` 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.
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 filter chrome under `frontend-modern/src/features/infrastructure/`
must use mode-oriented labels for table presentation controls: grouped table
mode is `Grouped`, not `Cluster`, because cluster remains a
@@ -536,9 +536,8 @@ work extends shared components instead of creating new local variants.
16. 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`,
`frontend-modern/src/components/AI/Chat/index.tsx`, and
`frontend-modern/src/hooks/useDashboardActions.ts` must consume the
`frontend-modern/src/stores/aiChat.ts`, and
`frontend-modern/src/components/AI/Chat/index.tsx` must consume the
backend-owned `/api/security/status.sessionCapabilities.assistantEnabled`
fact instead of probing `/api/settings/ai` or `/api/ai/*` during ordinary
route bootstrap. Closed assistant chrome and non-AI settings panels may
@@ -674,7 +673,7 @@ work extends shared components instead of creating new local variants.
instead of importing generic commercial presentation helpers directly into
hosted settings route shells.
8. Keep first-session dashboard empty-state copy on
`frontend-modern/src/utils/dashboardEmptyStatePresentation.ts`, and make
`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.
9. Keep the live first-session wizard on the canonical three-step runtime
@@ -722,10 +721,11 @@ work extends shared components instead of creating new local variants.
and the `Add infrastructure` entry point on it, not by a second landing route,
so first-time operators and returning operators see one consistent
infrastructure surface by default.
14. Keep dashboard onboarding copy on the shared presentation owner in
`frontend-modern/src/utils/dashboardEmptyStatePresentation.ts`. Both the
infrastructure empty state and the dashboard route's no-resources state
must route first-time operators into the canonical
14. 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=pick` source picker, describe platform API
inventory and Pulse Agent telemetry as equal source strategies, and avoid
falling back to either passive “nothing here yet” wording or the retired
@@ -778,14 +778,14 @@ work extends shared components instead of creating new local variants.
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.tsx` must land `/` on the dashboard shell and let
the governed dashboard empty state route first-time operators into the
`Add infrastructure` source picker, instead of preserving a separate
root-only jump to `/infrastructure` or an agent-only install jump that
drifts from the rest of the onboarding contract.
`frontend-modern/src/App.tsx` must land `/` on the Infrastructure route and
let the governed Infrastructure empty state route first-time operators into
the `Add infrastructure` source picker, instead of preserving a separate
root-only compatibility shell or an agent-only install jump that drifts from
the rest of the onboarding contract.
The same entry-shell contract must also canonicalize authenticated
`/login`: once auth succeeds, the shared shell must resolve that route back
onto the governed dashboard landing path instead of rendering a page-local
onto the governed Infrastructure landing path instead of rendering a page-local
not-found state inside the authenticated chrome.
24. Keep relay settings shell copy on the shared presentation owner in
`frontend-modern/src/utils/relayPresentation.ts`. The route metadata in
@@ -1213,9 +1213,9 @@ 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. Dashboard workload rows and the unified-resource
detail drawer must import that shared owner instead of keeping a dashboard-local
tag badge variant or importing a feature-local path into infrastructure
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
@@ -1322,33 +1322,21 @@ metadata. The shell may expose `data-active-series-display` and
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 dashboard overview trend cards now also have an explicit shared-shell
obligation: `frontend-modern/src/features/dashboardOverview/TrendCharts.tsx`
must treat missing infrastructure history as a governed empty state rather than
as a silent blank sparkline box. Error copy and empty-history copy belong to
the feature shell, while the data path and chart-shaping logic must stay in the
owned hook/model layers that feed it.
That same dashboard overview shell also owns workload discoverability copy on
the KPI strip. `frontend-modern/src/features/dashboardOverview/KPIStrip.tsx`
and `frontend-modern/src/utils/dashboardKpiPresentation.ts` must keep the
`Workloads` card explicit that the unified workloads surface contains VMs,
containers, and pods, rather than assuming operators will infer that mapping
from the route label alone.
That same dashboard boundary now also owns the shared dashboard presentation
helpers through `frontend-modern/src/utils/dashboardEmptyStatePresentation.ts`,
`frontend-modern/src/utils/dashboardGuestPresentation.ts`,
`frontend-modern/src/utils/dashboardKpiPresentation.ts`,
`frontend-modern/src/utils/dashboardMetricPresentation.ts`, and
`frontend-modern/src/utils/dashboardTrendPresentation.ts`. Dashboard loading,
disconnect, and empty states; guest backup/disk fallback copy; KPI card
framing; status-badge, delta, percent, and action-priority formatting; and
trend palette/error copy must extend those helpers instead of being redefined
inline in route shells, guest rows, or overview cards.
That shell must also stay passive with respect to data ownership: dashboard
trend cards may render the summary-range controls and operator-facing empty or
error copy, but 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 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
@@ -1685,7 +1673,7 @@ runtime logic inline. Audit-log filter option labels must come from
label primitive instead of hard-coded title-case strings in the settings shell.
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/dashboard filters, storage source
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
@@ -1736,49 +1724,15 @@ 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 now follows that same feature-owner pattern for
its dashboard-specific summary surfaces. `frontend-modern/src/pages/Dashboard.tsx`
stays the route shell, while `frontend-modern/src/features/dashboardOverview/`
owns the dashboard-specific action, KPI, problem-resource, trend, and
customization surfaces. Lane-owned widgets like recent alerts, storage,
and recovery must continue to route through their own subsystem owners instead
of drifting back into a page-local dashboard panel cluster.
That same dashboard overview boundary owns the first-viewport estate
orientation contract for the v6 landing page. `EstateSummaryPanel.tsx` and
`estateSummaryModel.ts` in `frontend-modern/src/features/dashboardOverview/`
must derive system count, health, source coverage, and freshness from the
canonical connected-infrastructure projection, fall back only to the compact
dashboard summary that the route already owns, and keep the explicit
Infrastructure handoff above detailed problem, storage, recovery, or trend
rows without restoring platform-special navigation.
Source coverage is part of that first-viewport proof and must remain readable
at mobile and tablet shell widths; the estate summary may wrap that provider
inventory across lines, but it must not truncate the canonical source mix with
ellipsis in the default dashboard viewport.
That compact fallback must keep speaking in system terms. When the connected
projection has not arrived yet, estate orientation copy may say how many
systems are reporting or syncing, but it must not slide back to generic
resource-count language that blurs the v6 system model.
That first-viewport copy must distinguish system-level estate health from
resource, alert, storage, or recovery issues that remain elsewhere on the
dashboard, and partial/empty dashboard states must describe synchronization or
infrastructure-source onboarding in operator terms instead of exposing
implementation fallback language.
The estate summary may surface resource and alert issue counts only as
below-the-summary detail references; it must not claim the whole dashboard has
no issues when storage or recovery widgets still own independent health
signals. Those detail references must use governed dashboard section anchors so
the first viewport can move focus to Problem Resources or Alerts without
inventing separate dashboard drill-down routes.
The dashboard may add an optional Pulse Brief below that estate orientation
only when Assistant and Patrol are actually enabled and configured. That brief
must stay additive to the factual dashboard source of truth, derive its first
render from the already-owned estate, overview, action, storage, and recovery
facts, and hand off to Pulse Assistant through a structured context prompt
instead of replacing the route's canonical numbers, tables, or lane-owned
widgets with model prose. `PulseBriefPanel.tsx`, `useDashboardPulseBrief.ts`,
and `dashboardPulseBriefModel.ts` own that presentation and deterministic
first-render summary contract under `frontend-modern/src/features/dashboardOverview/`.
The dashboard overview route and its feature-owned summary surfaces are
retired. Authenticated root entry now lands on Infrastructure, so first-
viewport estate orientation belongs to the Infrastructure page and Add
infrastructure flow rather than a separate dashboard 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 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
@@ -1882,28 +1836,19 @@ 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.
That same dashboard overview boundary must consume the governed Patrol finding
presentation helpers when it surfaces Patrol findings in compact form. In
`frontend-modern/src/features/dashboardOverview/ActionRequiredPanel.tsx`,
Patrol-owned runtime findings must use the shared compact badge, title, and
primary-action/manual-control contracts from
`frontend-modern/src/utils/aiFindingPresentation.ts` rather than rendering raw
`Pulse Patrol:` titles or generic snooze/dismiss controls that the Patrol
runtime lifecycle rejects.
That same boundary must also consume the shared attention-queue ordering from
`frontend-modern/src/utils/aiFindingPresentation.ts` through
`frontend-modern/src/hooks/useDashboardActions.ts`, so Patrol-blinding runtime
issues sort ahead of same-severity infrastructure findings in the dashboard
action panel instead of inheriting arbitrary store order.
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 and dashboard recent-alerts panel
must both compose
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
`frontend-modern/src/components/Alerts/RecentAlertsPanel.tsx`.
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
@@ -2622,7 +2567,7 @@ 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/dashboardEmptyStatePresentation.ts`,
`frontend-modern/src/utils/workloadEmptyStatePresentation.ts`,
`frontend-modern/src/utils/infrastructureEmptyStatePresentation.ts`, and
adjacent setup guidance must use `Add infrastructure` as the operator-facing
first-run label for API-backed onboarding, resolve that label to the shared
@@ -2707,8 +2652,7 @@ 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`, and
`frontend-modern/src/hooks/useDashboardActions.ts` must treat
`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
@@ -322,17 +322,17 @@ sampler generation instead of regenerating or re-downsampling the same seeded
timeline on every endpoint hit. When seeded mock history is rebuilt or a live
mock tick advances, monitoring must invalidate that cache so preview charts
stay current without paying repeated per-request synthesis cost.
That same sampler-owned cache contract also covers compact dashboard summary
reads. When live mock ticks advance, monitoring must repopulate the canonical
24-hour aggregate `/api/charts/storage-summary` cache inside the sampler path
instead of leaving the first operator request after each tick to rebuild
per-pool mock storage charts on demand.
That same sampler-owned cache contract also covers compact summary reads after
the dashboard overview retirement. When live mock ticks advance, monitoring
must repopulate the canonical 24-hour aggregate `/api/charts/storage-summary`
cache inside the sampler path instead of leaving the first operator request
after each tick to rebuild per-pool mock storage charts on demand.
That same metrics-hot-path ownership also includes metric-type selection for
compact summary reads. When dashboard infrastructure or storage summary routes
request only a subset of canonical chart series, `internal/monitoring/monitor_metrics.go`
must preserve that narrowed metric set through the batch store fallback path
instead of querying every metric type for each resource and discarding most of
the payload afterward.
compact summary reads. When infrastructure or storage summary routes request
only a subset of canonical chart series,
`internal/monitoring/monitor_metrics.go` must preserve that narrowed metric
set through the batch store fallback path instead of querying every metric type
for each resource and discarding most of the payload afterward.
That same mock-runtime owner now also owns demo-scenario curation.
`internal/mock/fixture_graph.go`, `internal/mock/platform_fixtures.go`, and
`internal/mock/demo_scenarios.go` may project an authored demo estate over
@@ -9,7 +9,7 @@
"contract_file": "docs/release-control/v6/internal/subsystems/performance-and-scalability.md",
"status_file": "docs/release-control/v6/internal/status.json",
"registry_file": "docs/release-control/v6/internal/subsystems/registry.json",
"dependency_subsystem_ids": ["ai-runtime", "api-contracts", "cloud-paid", "frontend-primitives", "storage-recovery", "unified-resources"]
"dependency_subsystem_ids": ["api-contracts", "frontend-primitives"]
}
```
@@ -25,63 +25,63 @@ regression protection.
3. `pkg/metrics/store_slo_test.go`
4. `internal/api/slo.go`
5. `internal/api/slo_bench_test.go`
6. `frontend-modern/src/components/Dashboard/Dashboard.tsx`
7. `frontend-modern/src/components/Dashboard/DashboardStateCards.tsx`
8. `frontend-modern/src/components/Dashboard/DashboardStatsStrip.tsx`
9. `frontend-modern/src/components/Dashboard/DashboardWorkloadTable.tsx`
10. `frontend-modern/src/components/Dashboard/WorkloadPanel.tsx`
11. `frontend-modern/src/components/Dashboard/WorkloadTableHeader.tsx`
12. `frontend-modern/src/components/Dashboard/useDashboardState.ts`
13. `frontend-modern/src/components/Dashboard/useDashboardControlsState.ts`
14. `frontend-modern/src/components/Dashboard/useDashboardWorkloadDerivedState.ts`
15. `frontend-modern/src/components/Dashboard/useDashboardGuestMetadataState.ts`
16. `frontend-modern/src/components/Dashboard/useDashboardSelectionState.ts`
17. `frontend-modern/src/components/Dashboard/useDashboardWorkloadRouteState.ts`
18. `frontend-modern/src/components/Dashboard/useDashboardWorkloadUrlSync.ts`
19. `frontend-modern/src/components/Dashboard/DashboardFilter.tsx`
20. `frontend-modern/src/components/Dashboard/dashboardFilterModel.ts`
21. `frontend-modern/src/components/Dashboard/useDashboardFilterState.ts`
22. `frontend-modern/src/components/Dashboard/ThresholdSlider.tsx`
23. `frontend-modern/src/components/Dashboard/thresholdSliderModel.ts`
24. `frontend-modern/src/components/Dashboard/useThresholdSliderState.ts`
25. `frontend-modern/src/components/Dashboard/StackedDiskBar.tsx`
26. `frontend-modern/src/components/Dashboard/stackedDiskBarModel.ts`
27. `frontend-modern/src/components/Dashboard/useStackedDiskBarState.ts`
28. `frontend-modern/src/components/Dashboard/StackedMemoryBar.tsx`
29. `frontend-modern/src/components/Dashboard/stackedMemoryBarModel.ts`
30. `frontend-modern/src/components/Dashboard/useStackedMemoryBarState.ts`
31. `frontend-modern/src/components/Dashboard/MetricBar.tsx`
32. `frontend-modern/src/components/Dashboard/metricBarModel.ts`
33. `frontend-modern/src/components/Dashboard/useMetricBarState.ts`
34. `frontend-modern/src/components/Dashboard/EnhancedCPUBar.tsx`
35. `frontend-modern/src/components/Dashboard/enhancedCpuBarModel.ts`
36. `frontend-modern/src/components/Dashboard/useEnhancedCPUBarState.ts`
37. `frontend-modern/src/components/Dashboard/DiskList.tsx`
38. `frontend-modern/src/components/Dashboard/diskListModel.ts`
39. `frontend-modern/src/components/Dashboard/useDiskListState.ts`
40. `frontend-modern/src/components/Dashboard/GuestRow.tsx`
41. `frontend-modern/src/components/Dashboard/GuestRowCells.tsx`
42. `frontend-modern/src/components/Dashboard/guestRowModel.tsx`
43. `frontend-modern/src/components/Dashboard/useGuestRowState.ts`
44. `frontend-modern/src/components/Dashboard/GuestDrawer.tsx`
45. `frontend-modern/src/components/Dashboard/GuestDrawerOverview.tsx`
46. `frontend-modern/src/components/Dashboard/guestDrawerModel.ts`
47. `frontend-modern/src/components/Dashboard/useGuestDrawerState.ts`
48. `frontend-modern/src/components/Dashboard/useGroupedTableWindowing.ts`
49. `frontend-modern/src/components/Dashboard/workloadSelectors.ts`
50. `frontend-modern/src/components/Dashboard/workloadTopology.ts`
51. `frontend-modern/src/components/Dashboard/dashboardSelectionModel.ts`
52. `frontend-modern/src/components/Dashboard/dashboardWorkloadRouteModel.ts`
53. `frontend-modern/src/components/Dashboard/dashboardWorkloadFilterConfigModel.ts`
54. `frontend-modern/src/components/Dashboard/dashboardWorkloadRouteStateModel.ts`
55. `frontend-modern/src/components/Dashboard/dashboardWorkloadUrlSyncModel.ts`
56. `frontend-modern/src/components/Dashboard/useDashboardWorkloadFilterOptions.ts`
57. `frontend-modern/src/components/Dashboard/__tests__/dashboardSelectionModel.test.ts`
58. `frontend-modern/src/components/Dashboard/__tests__/dashboardWorkloadFilterConfigModel.test.ts`
59. `frontend-modern/src/components/Dashboard/__tests__/dashboardWorkloadRouteModel.test.ts`
60. `frontend-modern/src/components/Dashboard/__tests__/dashboardWorkloadRouteStateModel.test.ts`
61. `frontend-modern/src/components/Dashboard/__tests__/dashboardWorkloadUrlSyncModel.test.ts`
62. `frontend-modern/src/components/Dashboard/__tests__/workloadTopology.test.ts`
6. `frontend-modern/src/components/Workloads/WorkloadsSurface.tsx`
7. `frontend-modern/src/components/Workloads/WorkloadsStateCards.tsx`
8. `frontend-modern/src/components/Workloads/WorkloadsStatsStrip.tsx`
9. `frontend-modern/src/components/Workloads/WorkloadsTable.tsx`
10. `frontend-modern/src/components/Workloads/WorkloadPanel.tsx`
11. `frontend-modern/src/components/Workloads/WorkloadTableHeader.tsx`
12. `frontend-modern/src/components/Workloads/useWorkloadsState.ts`
13. `frontend-modern/src/components/Workloads/useWorkloadsControlsState.ts`
14. `frontend-modern/src/components/Workloads/useWorkloadsDerivedState.ts`
15. `frontend-modern/src/components/Workloads/useWorkloadGuestMetadataState.ts`
16. `frontend-modern/src/components/Workloads/useWorkloadSelectionState.ts`
17. `frontend-modern/src/components/Workloads/useWorkloadRouteState.ts`
18. `frontend-modern/src/components/Workloads/useWorkloadUrlSync.ts`
19. `frontend-modern/src/components/Workloads/WorkloadsFilter.tsx`
20. `frontend-modern/src/components/Workloads/workloadsFilterModel.ts`
21. `frontend-modern/src/components/Workloads/useWorkloadsFilterState.ts`
22. `frontend-modern/src/components/Workloads/ThresholdSlider.tsx`
23. `frontend-modern/src/components/Workloads/thresholdSliderModel.ts`
24. `frontend-modern/src/components/Workloads/useThresholdSliderState.ts`
25. `frontend-modern/src/components/Workloads/StackedDiskBar.tsx`
26. `frontend-modern/src/components/Workloads/stackedDiskBarModel.ts`
27. `frontend-modern/src/components/Workloads/useStackedDiskBarState.ts`
28. `frontend-modern/src/components/Workloads/StackedMemoryBar.tsx`
29. `frontend-modern/src/components/Workloads/stackedMemoryBarModel.ts`
30. `frontend-modern/src/components/Workloads/useStackedMemoryBarState.ts`
31. `frontend-modern/src/components/Workloads/MetricBar.tsx`
32. `frontend-modern/src/components/Workloads/metricBarModel.ts`
33. `frontend-modern/src/components/Workloads/useMetricBarState.ts`
34. `frontend-modern/src/components/Workloads/EnhancedCPUBar.tsx`
35. `frontend-modern/src/components/Workloads/enhancedCpuBarModel.ts`
36. `frontend-modern/src/components/Workloads/useEnhancedCPUBarState.ts`
37. `frontend-modern/src/components/Workloads/DiskList.tsx`
38. `frontend-modern/src/components/Workloads/diskListModel.ts`
39. `frontend-modern/src/components/Workloads/useDiskListState.ts`
40. `frontend-modern/src/components/Workloads/GuestRow.tsx`
41. `frontend-modern/src/components/Workloads/GuestRowCells.tsx`
42. `frontend-modern/src/components/Workloads/guestRowModel.tsx`
43. `frontend-modern/src/components/Workloads/useGuestRowState.ts`
44. `frontend-modern/src/components/Workloads/GuestDrawer.tsx`
45. `frontend-modern/src/components/Workloads/GuestDrawerOverview.tsx`
46. `frontend-modern/src/components/Workloads/guestDrawerModel.ts`
47. `frontend-modern/src/components/Workloads/useGuestDrawerState.ts`
48. `frontend-modern/src/components/Workloads/useGroupedTableWindowing.ts`
49. `frontend-modern/src/components/Workloads/workloadSelectors.ts`
50. `frontend-modern/src/components/Workloads/workloadTopology.ts`
51. `frontend-modern/src/components/Workloads/workloadSelectionModel.ts`
52. `frontend-modern/src/components/Workloads/workloadRouteModel.ts`
53. `frontend-modern/src/components/Workloads/workloadFilterConfigModel.ts`
54. `frontend-modern/src/components/Workloads/workloadRouteStateModel.ts`
55. `frontend-modern/src/components/Workloads/workloadUrlSyncModel.ts`
56. `frontend-modern/src/components/Workloads/useWorkloadFilterOptions.ts`
57. `frontend-modern/src/components/Workloads/__tests__/workloadSelectionModel.test.ts`
58. `frontend-modern/src/components/Workloads/__tests__/workloadFilterConfigModel.test.ts`
59. `frontend-modern/src/components/Workloads/__tests__/workloadRouteModel.test.ts`
60. `frontend-modern/src/components/Workloads/__tests__/workloadRouteStateModel.test.ts`
61. `frontend-modern/src/components/Workloads/__tests__/workloadUrlSyncModel.test.ts`
62. `frontend-modern/src/components/Workloads/__tests__/workloadTopology.test.ts`
63. `frontend-modern/src/components/Infrastructure/UnifiedResourceTable.tsx`
64. `frontend-modern/src/components/Infrastructure/unifiedResourceTableStateModel.ts`
65. `frontend-modern/src/components/Infrastructure/useUnifiedResourceTableState.ts`
@@ -95,33 +95,30 @@ regression protection.
73. `frontend-modern/src/components/Infrastructure/unifiedResourceTableModel.ts`
74. `frontend-modern/src/components/Infrastructure/infrastructureSelectors.ts`
75. `frontend-modern/src/components/Infrastructure/resourceDetailMappers.ts`
76. `frontend-modern/src/components/Dashboard/__tests__/Dashboard.performance.contract.test.tsx`
77. `frontend-modern/src/components/Dashboard/__tests__/DashboardFilter.test.tsx`
78. `frontend-modern/src/components/Dashboard/__tests__/useDashboardFilterState.test.ts`
79. `frontend-modern/src/components/Dashboard/__tests__/useDashboardSelectionState.test.ts`
80. `frontend-modern/src/components/Dashboard/MetricBar.test.tsx`
81. `frontend-modern/src/components/Dashboard/__tests__/useMetricBarState.test.tsx`
82. `frontend-modern/src/components/Dashboard/__tests__/EnhancedCPUBar.test.tsx`
83. `frontend-modern/src/components/Dashboard/__tests__/useEnhancedCPUBarState.test.tsx`
84. `frontend-modern/src/components/Dashboard/ThresholdSlider.test.tsx`
85. `frontend-modern/src/components/Dashboard/__tests__/useThresholdSliderState.test.ts`
86. `frontend-modern/src/components/Dashboard/__tests__/StackedDiskBar.test.tsx`
87. `frontend-modern/src/components/Dashboard/__tests__/useStackedDiskBarState.test.tsx`
88. `frontend-modern/src/components/Dashboard/StackedMemoryBar.test.tsx`
89. `frontend-modern/src/components/Dashboard/__tests__/useStackedMemoryBarState.test.tsx`
90. `frontend-modern/src/components/Dashboard/__tests__/DiskList.test.tsx`
91. `frontend-modern/src/components/Dashboard/__tests__/GuestRow.test.tsx`
92. `frontend-modern/src/components/Dashboard/GuestDrawer.test.tsx`
93. `frontend-modern/src/components/Dashboard/__tests__/useGroupedTableWindowing.test.ts`
76. `frontend-modern/src/components/Workloads/__tests__/WorkloadsSurface.performance.contract.test.tsx`
77. `frontend-modern/src/components/Workloads/__tests__/WorkloadsFilter.test.tsx`
78. `frontend-modern/src/components/Workloads/__tests__/useWorkloadsFilterState.test.ts`
79. `frontend-modern/src/components/Workloads/__tests__/useWorkloadSelectionState.test.ts`
80. `frontend-modern/src/components/Workloads/MetricBar.test.tsx`
81. `frontend-modern/src/components/Workloads/__tests__/useMetricBarState.test.tsx`
82. `frontend-modern/src/components/Workloads/__tests__/EnhancedCPUBar.test.tsx`
83. `frontend-modern/src/components/Workloads/__tests__/useEnhancedCPUBarState.test.tsx`
84. `frontend-modern/src/components/Workloads/ThresholdSlider.test.tsx`
85. `frontend-modern/src/components/Workloads/__tests__/useThresholdSliderState.test.ts`
86. `frontend-modern/src/components/Workloads/__tests__/StackedDiskBar.test.tsx`
87. `frontend-modern/src/components/Workloads/__tests__/useStackedDiskBarState.test.tsx`
88. `frontend-modern/src/components/Workloads/StackedMemoryBar.test.tsx`
89. `frontend-modern/src/components/Workloads/__tests__/useStackedMemoryBarState.test.tsx`
90. `frontend-modern/src/components/Workloads/__tests__/DiskList.test.tsx`
91. `frontend-modern/src/components/Workloads/__tests__/GuestRow.test.tsx`
92. `frontend-modern/src/components/Workloads/GuestDrawer.test.tsx`
93. `frontend-modern/src/components/Workloads/__tests__/useGroupedTableWindowing.test.ts`
94. `frontend-modern/src/components/Infrastructure/__tests__/UnifiedResourceTable.performance.contract.test.tsx`
95. `frontend-modern/src/components/Dashboard/useDashboardWorkloadViewportSync.ts`
96. `frontend-modern/src/components/Dashboard/__tests__/useDashboardWorkloadViewportSync.test.tsx`
95. `frontend-modern/src/components/Workloads/useWorkloadViewportSync.ts`
96. `frontend-modern/src/components/Workloads/__tests__/useWorkloadViewportSync.test.tsx`
97. `frontend-modern/src/components/Infrastructure/InfrastructureSummary.tsx`
98. `frontend-modern/src/components/Infrastructure/useInfrastructureSummaryState.ts`
99. `frontend-modern/src/components/Infrastructure/infrastructureSummaryModel.ts`
100. `frontend-modern/src/hooks/useDashboardTrends.ts`
101. `frontend-modern/src/hooks/__tests__/useDashboardTrends.test.ts`
102. `frontend-modern/src/components/Storage/DashboardStoragePanel.tsx`
103. `frontend-modern/src/components/Storage/StorageSummary.tsx`
104. `frontend-modern/src/utils/storageSummaryCache.ts`
105. `frontend-modern/src/pages/Workloads.tsx`
@@ -168,40 +165,36 @@ regression protection.
`/auth/trial-activate` and `POST /api/license/trial/start` from public-path
and CSRF inventories must stay as constant-time route-table absence rather
than replacing the old callback with persistence-backed router probes.
5. Extend dashboard hot-path filter, sort, grouping, and stats math through `frontend-modern/src/components/Dashboard/workloadSelectors.ts`, and extend workload identity, discovery routing, and node-topology helpers through `frontend-modern/src/components/Dashboard/workloadTopology.ts`, rather than duplicating selector or topology logic in `frontend-modern/src/components/Dashboard/Dashboard.tsx`
Dashboard landing-page estate orientation is part of that same hot-path
discipline. First-viewport system count, health, source coverage, and
freshness must be derived from app-runtime connected-infrastructure state
and the already-loaded compact dashboard summary fallback; it must not
introduce a new `useUnifiedResources()` subscription, platform-specific
fetch, or chart/history request just to make the dashboard feel oriented.
Dashboard Pulse Brief handoff belongs to that existing route state too:
`frontend-modern/src/pages/Dashboard.tsx` may open Assistant with the
already-derived compact prompt and a per-request approval-required override,
but it must not introduce extra dashboard polling, model/settings fetches,
or chart/history reads before the Assistant drawer is actually used.
6. Normalize dashboard workload view-mode aliases through `frontend-modern/src/utils/workloads.ts` instead of keeping local URL/storage parsing in `frontend-modern/src/components/Dashboard/Dashboard.tsx`
7. Deduplicate dashboard workload rows by canonical workload ID from `frontend-modern/src/utils/workloads.ts` rather than via local pass-through wrappers in `frontend-modern/src/components/Dashboard/Dashboard.tsx`
8. Render dashboard row identity directly from the shared canonical workload helper so row selection, hover, and fallback metadata lookup stay aligned with the same workload contract
5. Extend workload hot-path filter, sort, grouping, and stats math through `frontend-modern/src/components/Workloads/workloadSelectors.ts`, and extend workload identity, discovery routing, and node-topology helpers through `frontend-modern/src/components/Workloads/workloadTopology.ts`, rather than duplicating selector or topology logic in `frontend-modern/src/components/Workloads/WorkloadsSurface.tsx`
The retired dashboard overview route must not return as a hot-path
orientation shortcut. First-viewport system count, health, source coverage,
and freshness now belong to Infrastructure and Add infrastructure, derived
from their existing runtime/resource state rather than a dashboard-specific
compact summary fallback. Any future brief-style Assistant handoff needs a
newly governed owner and must not add route-global polling, model/settings
fetches, or chart/history reads to the authenticated root path.
6. Normalize workload view-mode aliases through `frontend-modern/src/utils/workloads.ts` instead of keeping local URL/storage parsing in `frontend-modern/src/components/Workloads/WorkloadsSurface.tsx`
7. Deduplicate workload rows by canonical workload ID from `frontend-modern/src/utils/workloads.ts` rather than via local pass-through wrappers in `frontend-modern/src/components/Workloads/WorkloadsSurface.tsx`
8. Render workload row identity directly from the shared canonical workload helper so row selection, hover, and fallback metadata lookup stay aligned with the same workload contract
9. Format infrastructure sensor labels through the shared `frontend-modern/src/utils/textPresentation.ts` presentation helper instead of maintaining a local title-casing implementation in `frontend-modern/src/components/Infrastructure/resourceDetailMappers.ts`
10. Extend dashboard row contract and per-row hot-path derivations through `frontend-modern/src/components/Dashboard/guestRowModel.tsx` and `frontend-modern/src/components/Dashboard/useGuestRowState.ts`, and extend tooltip-backed row cell presentation through `frontend-modern/src/components/Dashboard/GuestRowCells.tsx`, rather than rebuilding column metadata, row identity, cell tooltips, or anomaly correlation inside `frontend-modern/src/components/Dashboard/GuestRow.tsx`
11. Extend dashboard drawer derivations and runtime wiring through `frontend-modern/src/components/Dashboard/guestDrawerModel.ts` and `frontend-modern/src/components/Dashboard/useGuestDrawerState.ts`, and extend drawer overview rendering through `frontend-modern/src/components/Dashboard/GuestDrawerOverview.tsx`, rather than rebuilding canonical guest identity, discovery routing, or drawer-local normalization inside `frontend-modern/src/components/Dashboard/GuestDrawer.tsx`
12. Extend dashboard disk-list derivations and fallback runtime wiring through `frontend-modern/src/components/Dashboard/diskListModel.ts` and `frontend-modern/src/components/Dashboard/useDiskListState.ts` rather than rebuilding usage math, progress-state mapping, or tooltip fallback logic inside `frontend-modern/src/components/Dashboard/DiskList.tsx`
13. Extend dashboard guest metadata cache persistence, metadata refresh, org-scope switching, and optimistic custom-URL updates through `frontend-modern/src/components/Dashboard/useDashboardGuestMetadataState.ts` rather than rebuilding dashboard-local storage caches, event listeners, or guest metadata API wiring inside `frontend-modern/src/components/Dashboard/useDashboardState.ts`
14. Extend dashboard deep-link selection and hovered-row continuity semantics through `frontend-modern/src/components/Dashboard/dashboardSelectionModel.ts`, and extend table scroll preservation plus reactive selection state through `frontend-modern/src/components/Dashboard/useDashboardSelectionState.ts`, rather than rebuilding resource-query parsing, selected-row scroll pinning, or hovered-row invalidation inside `frontend-modern/src/components/Dashboard/useDashboardState.ts`; canonical typed workload IDs such as `app-container:<host>:<provider-id>` must remain exact route/selection keys and must not be reinterpreted into synthetic node scopes
15. Extend dashboard workload route ownership, route-driven option catalogs, and toolbar filter config through `frontend-modern/src/components/Dashboard/useDashboardWorkloadRouteState.ts`, `frontend-modern/src/components/Dashboard/useDashboardWorkloadFilterOptions.ts`, `frontend-modern/src/components/Dashboard/dashboardWorkloadRouteModel.ts`, `frontend-modern/src/components/Dashboard/dashboardWorkloadFilterConfigModel.ts`, and `frontend-modern/src/components/Dashboard/dashboardWorkloadRouteStateModel.ts`, and extend query-param synchronization plus managed workload URL semantics through `frontend-modern/src/components/Dashboard/useDashboardWorkloadUrlSync.ts` and `frontend-modern/src/components/Dashboard/dashboardWorkloadUrlSyncModel.ts`, rather than rebuilding route sync, alias parsing, option derivation, toolbar callback/config wiring, reset policy, node-selection compatibility rules, param precedence, or managed workload URLs inside `frontend-modern/src/components/Dashboard/useDashboardState.ts`
Workloads route host scopes must resolve through `frontend-modern/src/components/Dashboard/workloadTopology.ts`: app-container scopes use the canonical Docker/runtime host id before host labels or node fallbacks, while VM and system-container scopes keep the instance-node key. Route `agent` filters and option catalogs must consume that shared scope so Infrastructure related-workload links cannot drift from Workloads filtering.
16. Extend grouped dashboard workload derivation, summary fallbacks, and grouped/windowed table presentation through `frontend-modern/src/components/Dashboard/useDashboardWorkloadDerivedState.ts`, extend viewport-driven grouped table synchronization through `frontend-modern/src/components/Dashboard/useDashboardWorkloadViewportSync.ts`, and extend node parent mapping through `frontend-modern/src/components/Dashboard/workloadTopology.ts`, rather than rebuilding grouped selectors, summary snapshot math, scroll listeners, or topology lookups inside `frontend-modern/src/components/Dashboard/useDashboardState.ts`
17. Extend dashboard control defaults, persistent view preferences, keyboard reset behavior, column-visibility ownership, and tag-search flow through `frontend-modern/src/components/Dashboard/useDashboardControlsState.ts` and `frontend-modern/src/components/Dashboard/dashboardFilterModel.ts` rather than rebuilding sort/search/grouping state, reset drift, or column-toggle plumbing inside `frontend-modern/src/components/Dashboard/useDashboardState.ts`
18. Extend dashboard filter active-count, reset semantics, and mobile toolbar state through `frontend-modern/src/components/Dashboard/dashboardFilterModel.ts` and `frontend-modern/src/components/Dashboard/useDashboardFilterState.ts`, rather than rebuilding filter-local state inside `frontend-modern/src/components/Dashboard/DashboardFilter.tsx`
19. Extend threshold-slider value-position math, title/label derivation, and drag scroll-lock runtime through `frontend-modern/src/components/Dashboard/thresholdSliderModel.ts` and `frontend-modern/src/components/Dashboard/useThresholdSliderState.ts` rather than rebuilding slider-local state and pointer lifecycle inside `frontend-modern/src/components/Dashboard/ThresholdSlider.tsx`
20. Extend stacked disk-bar capacity math, segment/tooltip derivation, and resize-observer runtime through `frontend-modern/src/components/Dashboard/stackedDiskBarModel.ts` and `frontend-modern/src/components/Dashboard/useStackedDiskBarState.ts` rather than rebuilding disk-bar-local state, mode branching, and tooltip shaping inside `frontend-modern/src/components/Dashboard/StackedDiskBar.tsx`
21. Extend stacked memory-bar capacity math, balloon/swap derivation, and resize-observer runtime through `frontend-modern/src/components/Dashboard/stackedMemoryBarModel.ts` and `frontend-modern/src/components/Dashboard/useStackedMemoryBarState.ts` rather than rebuilding memory-bar-local state, tooltip shaping, and label-fit logic inside `frontend-modern/src/components/Dashboard/StackedMemoryBar.tsx`
22. Extend metric-bar width, label-fit logic, and resize-observer runtime through `frontend-modern/src/components/Dashboard/metricBarModel.ts` and `frontend-modern/src/components/Dashboard/useMetricBarState.ts` rather than rebuilding metric-local state and threshold mapping inside `frontend-modern/src/components/Dashboard/MetricBar.tsx`
23. Extend enhanced CPU bar usage/anomaly presentation and tooltip runtime through `frontend-modern/src/components/Dashboard/enhancedCpuBarModel.ts` and `frontend-modern/src/components/Dashboard/useEnhancedCPUBarState.ts` rather than rebuilding tooltip-local state and CPU-threshold formatting inside `frontend-modern/src/components/Dashboard/EnhancedCPUBar.tsx`
23. Extend grouped dashboard row windowing, reveal-index clamping, overscan math, and per-group visible-slice derivation through `frontend-modern/src/components/Dashboard/useGroupedTableWindowing.ts`, and extend viewport event wiring through `frontend-modern/src/components/Dashboard/useDashboardWorkloadViewportSync.ts` rather than rebuilding scroll handlers, mounted-row budgets, viewport listeners, or group-slice math inside `frontend-modern/src/components/Dashboard/useDashboardWorkloadDerivedState.ts`
24. Extend dashboard shell rendering through `frontend-modern/src/components/Dashboard/DashboardStateCards.tsx`, `frontend-modern/src/components/Dashboard/DashboardWorkloadTable.tsx`, and `frontend-modern/src/components/Dashboard/DashboardStatsStrip.tsx` rather than accreting loading cards, workload table markup, or stats-strip presentation back into `frontend-modern/src/components/Dashboard/Dashboard.tsx`
25. Extend dashboard workload table shell ownership through `frontend-modern/src/components/Dashboard/WorkloadTableHeader.tsx` and `frontend-modern/src/components/Dashboard/WorkloadPanel.tsx` rather than rebuilding sortable header markup, grouped node rows, row expansion, or guest-drawer rendering inside `frontend-modern/src/components/Dashboard/DashboardWorkloadTable.tsx`
10. Extend workload row contract and per-row hot-path derivations through `frontend-modern/src/components/Workloads/guestRowModel.tsx` and `frontend-modern/src/components/Workloads/useGuestRowState.ts`, and extend tooltip-backed row cell presentation through `frontend-modern/src/components/Workloads/GuestRowCells.tsx`, rather than rebuilding column metadata, row identity, cell tooltips, or anomaly correlation inside `frontend-modern/src/components/Workloads/GuestRow.tsx`
11. Extend workload drawer derivations and runtime wiring through `frontend-modern/src/components/Workloads/guestDrawerModel.ts` and `frontend-modern/src/components/Workloads/useGuestDrawerState.ts`, and extend drawer overview rendering through `frontend-modern/src/components/Workloads/GuestDrawerOverview.tsx`, rather than rebuilding canonical guest identity, discovery routing, or drawer-local normalization inside `frontend-modern/src/components/Workloads/GuestDrawer.tsx`
12. Extend workload disk-list derivations and fallback runtime wiring through `frontend-modern/src/components/Workloads/diskListModel.ts` and `frontend-modern/src/components/Workloads/useDiskListState.ts` rather than rebuilding usage math, progress-state mapping, or tooltip fallback logic inside `frontend-modern/src/components/Workloads/DiskList.tsx`
13. Extend workload guest metadata cache persistence, metadata refresh, org-scope switching, and optimistic custom-URL updates through `frontend-modern/src/components/Workloads/useWorkloadGuestMetadataState.ts` rather than rebuilding workload-local storage caches, event listeners, or guest metadata API wiring inside `frontend-modern/src/components/Workloads/useWorkloadsState.ts`
14. Extend workload deep-link selection and hovered-row continuity semantics through `frontend-modern/src/components/Workloads/workloadSelectionModel.ts`, and extend table scroll preservation plus reactive selection state through `frontend-modern/src/components/Workloads/useWorkloadSelectionState.ts`, rather than rebuilding resource-query parsing, selected-row scroll pinning, or hovered-row invalidation inside `frontend-modern/src/components/Workloads/useWorkloadsState.ts`; canonical typed workload IDs such as `app-container:<host>:<provider-id>` must remain exact route/selection keys and must not be reinterpreted into synthetic node scopes
15. Extend workload route ownership, route-driven option catalogs, and toolbar filter config through `frontend-modern/src/components/Workloads/useWorkloadRouteState.ts`, `frontend-modern/src/components/Workloads/useWorkloadFilterOptions.ts`, `frontend-modern/src/components/Workloads/workloadRouteModel.ts`, `frontend-modern/src/components/Workloads/workloadFilterConfigModel.ts`, and `frontend-modern/src/components/Workloads/workloadRouteStateModel.ts`, and extend query-param synchronization plus managed workload URL semantics through `frontend-modern/src/components/Workloads/useWorkloadUrlSync.ts` and `frontend-modern/src/components/Workloads/workloadUrlSyncModel.ts`, rather than rebuilding route sync, alias parsing, option derivation, toolbar callback/config wiring, reset policy, node-selection compatibility rules, param precedence, or managed workload URLs inside `frontend-modern/src/components/Workloads/useWorkloadsState.ts`
Workloads route host scopes must resolve through `frontend-modern/src/components/Workloads/workloadTopology.ts`: app-container scopes use the canonical Docker/runtime host id before host labels or node fallbacks, while VM and system-container scopes keep the instance-node key. Route `agent` filters and option catalogs must consume that shared scope so Infrastructure related-workload links cannot drift from Workloads filtering.
16. Extend grouped workload derivation, summary fallbacks, and grouped/windowed table presentation through `frontend-modern/src/components/Workloads/useWorkloadsDerivedState.ts`, extend viewport-driven grouped table synchronization through `frontend-modern/src/components/Workloads/useWorkloadViewportSync.ts`, and extend node parent mapping through `frontend-modern/src/components/Workloads/workloadTopology.ts`, rather than rebuilding grouped selectors, summary snapshot math, scroll listeners, or topology lookups inside `frontend-modern/src/components/Workloads/useWorkloadsState.ts`
17. Extend workload control defaults, persistent view preferences, keyboard reset behavior, column-visibility ownership, and tag-search flow through `frontend-modern/src/components/Workloads/useWorkloadsControlsState.ts` and `frontend-modern/src/components/Workloads/workloadsFilterModel.ts` rather than rebuilding sort/search/grouping state, reset drift, or column-toggle plumbing inside `frontend-modern/src/components/Workloads/useWorkloadsState.ts`
18. Extend workload filter active-count, reset semantics, and mobile toolbar state through `frontend-modern/src/components/Workloads/workloadsFilterModel.ts` and `frontend-modern/src/components/Workloads/useWorkloadsFilterState.ts`, rather than rebuilding filter-local state inside `frontend-modern/src/components/Workloads/WorkloadsFilter.tsx`
19. Extend threshold-slider value-position math, title/label derivation, and drag scroll-lock runtime through `frontend-modern/src/components/Workloads/thresholdSliderModel.ts` and `frontend-modern/src/components/Workloads/useThresholdSliderState.ts` rather than rebuilding slider-local state and pointer lifecycle inside `frontend-modern/src/components/Workloads/ThresholdSlider.tsx`
20. Extend stacked disk-bar capacity math, segment/tooltip derivation, and resize-observer runtime through `frontend-modern/src/components/Workloads/stackedDiskBarModel.ts` and `frontend-modern/src/components/Workloads/useStackedDiskBarState.ts` rather than rebuilding disk-bar-local state, mode branching, and tooltip shaping inside `frontend-modern/src/components/Workloads/StackedDiskBar.tsx`
21. Extend stacked memory-bar capacity math, balloon/swap derivation, and resize-observer runtime through `frontend-modern/src/components/Workloads/stackedMemoryBarModel.ts` and `frontend-modern/src/components/Workloads/useStackedMemoryBarState.ts` rather than rebuilding memory-bar-local state, tooltip shaping, and label-fit logic inside `frontend-modern/src/components/Workloads/StackedMemoryBar.tsx`
22. Extend metric-bar width, label-fit logic, and resize-observer runtime through `frontend-modern/src/components/Workloads/metricBarModel.ts` and `frontend-modern/src/components/Workloads/useMetricBarState.ts` rather than rebuilding metric-local state and threshold mapping inside `frontend-modern/src/components/Workloads/MetricBar.tsx`
23. Extend enhanced CPU bar usage/anomaly presentation and tooltip runtime through `frontend-modern/src/components/Workloads/enhancedCpuBarModel.ts` and `frontend-modern/src/components/Workloads/useEnhancedCPUBarState.ts` rather than rebuilding tooltip-local state and CPU-threshold formatting inside `frontend-modern/src/components/Workloads/EnhancedCPUBar.tsx`
23. Extend grouped workload row windowing, reveal-index clamping, overscan math, and per-group visible-slice derivation through `frontend-modern/src/components/Workloads/useGroupedTableWindowing.ts`, and extend viewport event wiring through `frontend-modern/src/components/Workloads/useWorkloadViewportSync.ts` rather than rebuilding scroll handlers, mounted-row budgets, viewport listeners, or group-slice math inside `frontend-modern/src/components/Workloads/useWorkloadsDerivedState.ts`
24. Extend Workloads shell rendering through `frontend-modern/src/components/Workloads/WorkloadsStateCards.tsx`, `frontend-modern/src/components/Workloads/WorkloadsTable.tsx`, and `frontend-modern/src/components/Workloads/WorkloadsStatsStrip.tsx` rather than accreting loading cards, workload table markup, or stats-strip presentation back into `frontend-modern/src/components/Workloads/WorkloadsSurface.tsx`
25. Extend workload table shell ownership through `frontend-modern/src/components/Workloads/WorkloadTableHeader.tsx` and `frontend-modern/src/components/Workloads/WorkloadPanel.tsx` rather than rebuilding sortable header markup, grouped node rows, row expansion, or guest-drawer rendering inside `frontend-modern/src/components/Workloads/WorkloadsTable.tsx`
Compact icon headers inside `WorkloadTableHeader.tsx` may stay visually
dense for responsive workload tables, but the icon must be decorative and
the column label must remain present through an `sr-only` label so the
@@ -253,14 +246,14 @@ regression protection.
show the host and service tables dropping lower-priority columns without
introducing horizontal overflow.
Dashboard workload table responsive behavior belongs to the same hot-path
owner. `frontend-modern/src/components/Dashboard/useDashboardControlsState.ts`
owner. `frontend-modern/src/components/Workloads/useWorkloadsControlsState.ts`
must derive workload table layout stages from live viewport width, and
`frontend-modern/src/components/Dashboard/guestRowModel.tsx` must own the
responsive workload column priority and width model. `DashboardWorkloadTable`,
`frontend-modern/src/components/Workloads/guestRowModel.tsx` must own the
responsive workload column priority and width model. `WorkloadsTable`,
`WorkloadTableHeader`, and `WorkloadPanel` must consume one shared
layout-visible column set so headers, colgroups, and rows stay aligned during
live resize. View-mode-specific column sets in
`frontend-modern/src/components/Dashboard/guestRowModel.tsx` must retain
`frontend-modern/src/components/Workloads/guestRowModel.tsx` must retain
core monitoring metrics when narrowing scope: app-container mode keeps CPU,
memory, disk, and the responsive Net I/O and Disk I/O columns available,
while the layout priority model decides when detail-heavy I/O columns are
@@ -287,7 +280,14 @@ regression protection.
performance work must not add filesystem reads, GitHub release lookups, or
other per-request version discovery on activation, legacy exchange, or
grant-refresh traffic just to stamp authenticated install metadata.
30. Keep dashboard summary-chart fetches scope-owned rather than page-churn-owned: `frontend-modern/src/hooks/useDashboardTrends.ts` must hydrate infrastructure and storage summaries once per org/range scope from the canonical summary caches and recompute card presentation locally as the compact dashboard overview changes, rather than refetching the infrastructure-summary transport in `frontend-modern/src/components/Infrastructure/useInfrastructureSummaryState.ts`, the dashboard storage-summary trend transport in `frontend-modern/src/utils/storageSummaryTrendCache.ts`, or the storage-page summary transport in `frontend-modern/src/utils/storageSummaryCache.ts` for every top-resource or card reshuffle on the same dashboard load. That dashboard infrastructure path must also request only the metrics it renders through the canonical infrastructure-summary route owned by `internal/api/router_routes_monitoring.go` and `internal/api/router.go`; the dashboard may not pay for disk or network summary series when it only renders CPU and memory. App-shell prewarm in `frontend-modern/src/useAppRuntimeState.ts` must not front-run that dashboard-specific route while the operator is already on the root dashboard route owned by `frontend-modern/src/App.tsx`.
30. Keep retired dashboard summary-chart paths absent rather than replacing
them with new hot-path fetches. Infrastructure summary cards must continue to
hydrate through `frontend-modern/src/components/Infrastructure/useInfrastructureSummaryState.ts`
and the canonical infrastructure-summary route owned by
`internal/api/router_routes_monitoring.go` and `internal/api/router.go`; storage
summary cards must use the storage summary cache owners. The authenticated
root path in `frontend-modern/src/App.tsx` must not prewarm a deleted
dashboard-specific overview, trend, or summary transport.
The same hot path must keep mock/demo chart identity on the canonical
unified snapshot too: when mock mode is enabled, `internal/api/router.go`
may not read `/api/charts`, `/api/charts/infrastructure`, or
@@ -320,7 +320,7 @@ regression protection.
hot-path contract: `frontend-modern/src/useAppRuntimeState.ts` may reload
the org list from the shared `organizations_changed` event, but that refresh
must stay event-driven and route-safe rather than expanding into a second
full app bootstrap, a pre-auth org probe, or a dashboard-route prewarm
full app bootstrap, a pre-auth org probe, or a deleted dashboard-route prewarm
that duplicates the canonical summary fetch path.
into another summary-fetch or org-bootstrap hot path.
The same protected hot path also owns Patrol route compatibility: if
@@ -334,7 +334,7 @@ regression protection.
work before the canonical Settings URL takes over.
Authenticated `/login` recovery belongs to that same app-shell boundary:
`frontend-modern/src/App.tsx` must redirect that route back to the
canonical dashboard landing path instead of leaving the freshly
canonical Infrastructure landing path instead of leaving the freshly
authenticated shell on a not-found page that immediately pays another cold
bootstrap.
The same protected hot path now also owns proof harness steadiness.
@@ -343,27 +343,25 @@ regression protection.
must wait for deferred metrics-store startup maintenance to quiesce before
timing steady-state reads, so one-time retention or auto-vacuum cleanup does
not masquerade as summary-route or chart-batch regression latency.
31. Keep the dashboard overview hot path compact and route-owned. `frontend-modern/src/pages/Dashboard.tsx`, `frontend-modern/src/api/resources.ts`, and `frontend-modern/src/hooks/useDashboardOverview.ts` must hydrate KPI cards, problem-resource rows, and top-infrastructure identities through the compact dashboard-summary API contract owned by the adjacent `api-contracts` and `unified-resources` surfaces, rather than booting the full unfiltered paginated unified-resource list just to derive summary cards.
Any commercial or relay-owned dashboard affordances composed into that
route must remain additive shells on top of the same compact summary
payload instead of reintroducing route-local `all-resources` fetches,
summary recomputation, or page-level layout churn that displaces the
protected overview widgets.
Estate-summary issue copy on that route must use the already-hydrated
compact overview problem-resource and alert counts; it must not introduce a
second resource scan, alert fetch, storage read, or recovery read just to
clarify first-viewport copy. In-page issue anchors must therefore remain
pure section focus links against already-rendered dashboard sections rather
than triggering new data hydration.
Dashboard Pulse Brief composition belongs to that same hot-path rule: the
first render may issue the existing non-blocking AI runtime settings
readiness read and consume existing action-state signals, but it must not
issue an LLM request, mount a second resource scan, or block estate/KPI
rendering just to produce prose. Its Assistant handoff may add a scoped
`autonomous_mode:false` flag only when the operator submits the prefilled
prompt; that request-level safety override must not become a dashboard
render dependency or a persistent settings write on the hot path.
32. Keep infrastructure summary consumers on the compact dashboard overview rather than reopening the all-resources hook. `frontend-modern/src/hooks/useDashboardTrends.ts`, `frontend-modern/src/components/Infrastructure/useInfrastructureSummaryState.ts`, and adjacent dashboard summary consumers may derive chart identity and storage presence from the overview payload they were already given, but they must not call `useResources()` or mount a second unfiltered unified-resource fetch path inside the dashboard hot path. That rule also applies to globally mounted helpers such as `frontend-modern/src/components/AI/Chat/index.tsx`: closed assistant surfaces must read the live websocket snapshot or existing unified-resource cache rather than forcing the dashboard to pay for `all-resources` just because the shell component is mounted. When that assistant shell changes presentation, `frontend-modern/src/utils/aiChatPresentation.ts` must remain the canonical owner for launcher, drawer, session-menu, and empty-state copy so hot-path consumers do not grow one-off inline strings or extra state branches alongside the mounted shell. Blocking shared dialogs must also suppress closed assistant affordances through the shared dialog runtime instead of leaving the mounted shell clickable behind another overlay.
31. Keep the retired dashboard overview route absent from the protected hot
path. `frontend-modern/src/pages/Dashboard.tsx`,
`frontend-modern/src/hooks/useDashboardOverview.ts`, and
`/api/resources/dashboard-summary` must not be restored as compatibility
surfaces for KPI cards, problem-resource rows, or top-infrastructure
identities. New summary cards must live on their owning product route and
prove their data path there.
32. Keep infrastructure and assistant consumers off deleted dashboard summary
state. `frontend-modern/src/components/Infrastructure/useInfrastructureSummaryState.ts`
and globally mounted helpers such as `frontend-modern/src/components/AI/Chat/index.tsx`
must read the live websocket snapshot or existing unified-resource cache
rather than forcing root navigation to pay for a replacement dashboard
overview transport. When the assistant shell changes presentation,
`frontend-modern/src/utils/aiChatPresentation.ts` must remain the canonical
owner for launcher, drawer, session-menu, and empty-state copy so hot-path
consumers do not grow one-off inline strings or extra state branches alongside
the mounted shell. Blocking shared dialogs must also suppress closed assistant
affordances through the shared dialog runtime instead of leaving the mounted
shell clickable behind another overlay.
Approval presentation inside that mounted assistant shell must stay
state-local to the existing drawer/session state and backend approval
endpoints. Deny/skip failure handling may preserve the pending approval
@@ -383,21 +381,19 @@ regression protection.
must not hydrate `all-resources` or recovery rollups behind a hidden node
summary; selector-owned data hooks must be explicitly visibility-gated so
`/workloads` only pays for workload-owned transports.
34. Keep dashboard page-header framing additive on the compact hot path.
`frontend-modern/src/pages/Dashboard.tsx` may render the shared
`PageHeader` for route-level shell consistency, but that header must stay
pure presentation on top of the existing compact overview, trends,
actions, and recovery/storage widget hydration. It must not introduce a
second dashboard data load, widen suspense ownership, or force dashboard
summaries back through full-resource fetch paths just to satisfy page
34. Keep the retired dashboard page-header path absent from the compact hot
path. New page headers must stay pure presentation on their owning route
and must not introduce a second data load, widen suspense ownership, or
force deleted dashboard summaries back through full-resource fetch paths
just to satisfy page
chrome.
35. Keep the dashboard workloads table CSP-safe on the hot path. The renderers
in `frontend-modern/src/components/Dashboard/DashboardWorkloadTable.tsx`,
`frontend-modern/src/components/Dashboard/WorkloadTableHeader.tsx`,
`frontend-modern/src/components/Dashboard/GuestRow.tsx`,
`frontend-modern/src/components/Dashboard/EnhancedCPUBar.tsx`,
`frontend-modern/src/components/Dashboard/StackedMemoryBar.tsx`, and
`frontend-modern/src/components/Dashboard/StackedDiskBar.tsx` may still
35. Keep the Workloads table CSP-safe on the hot path. The renderers
in `frontend-modern/src/components/Workloads/WorkloadsTable.tsx`,
`frontend-modern/src/components/Workloads/WorkloadTableHeader.tsx`,
`frontend-modern/src/components/Workloads/GuestRow.tsx`,
`frontend-modern/src/components/Workloads/EnhancedCPUBar.tsx`,
`frontend-modern/src/components/Workloads/StackedMemoryBar.tsx`, and
`frontend-modern/src/components/Workloads/StackedDiskBar.tsx` may still
use shared models plus SVG or HTML attributes for widths, offsets, and
colors, but they must not fall back to inline `style=` attributes on the
public shell just to express virtualization spacers, alert accents, or
@@ -444,7 +440,7 @@ health-state counts (offline, degraded, alerting) instead of raw online/offline
splits. `InfrastructureSummary.tsx` and `infrastructureSummaryModel.ts` add
`degraded` and `alerting` resource counts; `StorageSummary.tsx` and
`useStoragePageSummary.ts` add `poolsDegraded` and `disksFailing` indicators;
`WorkloadsSummary.tsx` and `useDashboardWorkloadDerivedState.ts` add an
`WorkloadsSummary.tsx` and `useWorkloadsDerivedState.ts` add an
alerting count derived from `activeAlerts`. These additions must remain
read-only projections from existing websocket state — they must not introduce
new polling loops or widen fetch scope on the hot-path boundary.
@@ -474,10 +470,10 @@ snapshot-owned sources; the hot path must fail closed until the monitor has
both observed an initial baseline for every active connection and rebuilt the
canonical store at or after the latest provider watermark.
The dashboard workload selector path and the dashboard runtime that consumes it
The Workloads selector path and the Workloads runtime that consumes it
are now part of the protected performance surface rather than proof-only
context. Future hot-path filter/group/sort/windowing changes must route through
the explicit dashboard performance proof policy in the subsystem registry.
the explicit Workloads performance proof policy in the subsystem registry.
Route-backed workload `resource` focus on that hot path is contextual state
only, not inferred filter state: opening or closing an inline drawer must not
invent, retain, or clear `agent` or node-scope filters unless those filters
@@ -502,15 +498,15 @@ That same hot path now also owns dashboard freshness discipline. Supported
unified-resource dashboard reads may hydrate from REST for first paint, but
once websocket `state.resources` is available they must consume that canonical
live snapshot directly instead of turning each websocket update into a second
REST fetch. Dashboard trend loading must also key off stable target identity
REST fetch. Route-owned trend loading must also key off stable target identity
and selected range only; reconciled resource snapshots that do not change the
effective target set must not trigger duplicate infrastructure or storage
chart requests.
effective target set must not trigger duplicate infrastructure or storage chart
requests.
That same protected metrics-store boundary now also owns selected-series batch
queries. Compact dashboard routes that request only CPU/memory or only
storage `used`/`avail` capacity must keep that metric-type filter all the way
through `pkg/metrics/store.go` instead of fetching every series for every
resource and discarding the extra payload in higher layers.
queries. Compact route consumers that request only CPU/memory or only storage
`used`/`avail` capacity must keep that metric-type filter all the way through
`pkg/metrics/store.go` instead of fetching every series for every resource and
discarding the extra payload in higher layers.
That same protected workload-chart hot path now also owns rendered-metric
budgeting. `internal/api/router.go` may parallelize VM, container, pod, and
docker-container workload batch reads, but `/api/charts/workloads` and
@@ -566,7 +562,7 @@ rows and infrastructure resource rows must expose summary-linked activity
through the shared `data-summary-row-active` marker and let the shared frontend
primitive render the emphasis, instead of layering lane-local row-fill classes
that diverge across pages or wash out inline metric bars.
`frontend-modern/src/components/Dashboard/useDashboardSelectionState.ts` must
`frontend-modern/src/components/Workloads/useWorkloadSelectionState.ts` must
write workload selection back into the workloads route through the shared
same-path route-state scheduler, but the actual shell-position handoff for
query-only row focus must go through `frontend-modern/src/utils/appShellScrollRestoration.ts`
@@ -619,23 +615,22 @@ must include API-backed systems such as top-level TrueNAS appliances through
the shared `isAgentFacetInfrastructureResource(...)` helper instead of a local
`resource.type` branch, so the summary poll/cache path stays on one canonical
infrastructure selector contract.
That same protected dashboard hot path now includes storage trend loading too.
`frontend-modern/src/hooks/useDashboardTrends.ts` and
`internal/api/router.go` must keep dashboard storage trends on one compact
`/api/charts/storage-summary` request backed by
`GetStorageMetricsForChartBatch(...)`, so the dashboard does not pull the full
storage-page `/api/storage-charts` payload or reopen an N+1 per-pool
`/api/metrics-store/history` fan-out just to compute the shared 24-hour
storage capacity delta.
That same dashboard shell boundary also owns empty-state action routing in
`frontend-modern/src/components/Dashboard/DashboardStateCards.tsx`. When the
dashboard has no connected infrastructure, the CTA must hand operators
That same protected hot path keeps storage trend loading route-owned after the
dashboard overview retirement. `internal/api/router.go` must continue serving
the compact `/api/charts/storage-summary` request backed by
`GetStorageMetricsForChartBatch(...)` for the surfaces that still own storage
summary presentation, but no deleted dashboard trend hook may reopen the full
storage-page `/api/storage-charts` payload or an N+1 per-pool
`/api/metrics-store/history` fan-out.
That same Workloads shell boundary also owns empty-state action routing in
`frontend-modern/src/components/Workloads/WorkloadsStateCards.tsx`. When the
Workloads route has no connected infrastructure, the CTA must hand operators
directly to the canonical infrastructure install route via
`buildInfrastructureWorkspacePath('install')` instead of bouncing through a
generic settings landing page.
That workload route now also treats readiness as a route-owned contract instead
of a raw websocket proxy signal: `frontend-modern/src/components/Dashboard/Dashboard.tsx`
and `frontend-modern/src/components/Dashboard/useDashboardState.ts` must keep
of a raw websocket proxy signal: `frontend-modern/src/components/Workloads/WorkloadsSurface.tsx`
and `frontend-modern/src/components/Workloads/useWorkloadsState.ts` must keep
filters, stats, and table visibility driven by the workload route's own
REST-backed health so websocket churn does not hide already-fetched workloads
or swap the protected hot path into a false disconnected shell. The first
@@ -657,47 +652,48 @@ served stale mixed-cadence chart shapes after the backend timeline model has
already been corrected.
stay on one ordered index scan plus Go-side bucket aggregation rather than
forcing SQLite to `GROUP BY` computed buckets through a temp B-tree on the
fleet-scale dashboard path.
fleet-scale workload path.
That runtime is now intentionally split by concern:
`frontend-modern/src/components/Dashboard/useDashboardState.ts` owns
top-level dashboard orchestration, workload loading, and composition across
the canonical dashboard owners, while
`frontend-modern/src/components/Dashboard/useDashboardControlsState.ts`
`frontend-modern/src/components/Workloads/useWorkloadsState.ts` owns
top-level Workloads orchestration, workload loading, and composition across
the canonical Workloads owners, while
`frontend-modern/src/components/Workloads/useWorkloadsControlsState.ts`
owns persistent control defaults, keyboard-reset semantics, sort/search/tag
behavior, column visibility, and summary display preferences, while
`frontend-modern/src/components/Dashboard/useDashboardWorkloadDerivedState.ts`
`frontend-modern/src/components/Workloads/useWorkloadsDerivedState.ts`
owns grouped workload derivation, summary fallbacks, parent-node mapping,
and grouped/windowed table math, while
`frontend-modern/src/components/Dashboard/useDashboardWorkloadViewportSync.ts`
`frontend-modern/src/components/Workloads/useWorkloadViewportSync.ts`
owns grouped workload viewport synchronization and the scroll/resize listener
lifecycle, while
`frontend-modern/src/components/Dashboard/useDashboardGuestMetadataState.ts`
`frontend-modern/src/components/Workloads/useWorkloadGuestMetadataState.ts`
owns guest metadata cache persistence, optimistic custom-URL updates,
org-scope switching, and metadata refresh, and
`frontend-modern/src/components/Dashboard/useDashboardWorkloadRouteState.ts`
`frontend-modern/src/components/Workloads/useWorkloadRouteState.ts`
owns workload-route synchronization, deep-link normalization, and route-scoped
filter contracts. Future dashboard hot-path changes must extend through those
owners instead of accreting back into `frontend-modern/src/components/Dashboard/Dashboard.tsx`.
filter contracts. Future workload hot-path changes must extend through those
owners within the Workloads surface, and new overview-route work must not
accrete back into `frontend-modern/src/components/Workloads/WorkloadsSurface.tsx`.
That same route-owned filter contract now also includes canonical workload
`platform` scoping for API-backed runtimes. Dashboard URL-sync, filter-option
`platform` scoping for API-backed runtimes. Workloads URL-sync, filter-option
assembly, and workload drill-down routes must preserve
`platform=<owned-source-key>` for surfaces such as TrueNAS app-containers,
while treating host or cluster identity as secondary scope instead of
collapsing those routes back to generic agent-only semantics. The derived
workload hot path must also tolerate transient async route/data gaps by
treating missing filtered-workload collections as empty until the route and
resource snapshots converge, rather than crashing the dashboard between sync
resource snapshots converge, rather than crashing Workloads between sync
phases.
That same workload hot path also owns the split between canonical
app-container routing and Docker-only actions. `frontend-modern/src/hooks/useWorkloads.ts`,
`frontend-modern/src/components/Dashboard/workloadTopology.ts`, and
`frontend-modern/src/components/Dashboard/useGuestRowState.ts` must preserve a
`frontend-modern/src/components/Workloads/workloadTopology.ts`, and
`frontend-modern/src/components/Workloads/useGuestRowState.ts` must preserve a
canonical app-container navigation path while keeping Docker runtime action
identifiers explicit. Discovery affordances on the dashboard drawer must follow
identifiers explicit. Discovery affordances on the Workloads drawer must follow
the canonical `discoveryTarget` contract, not generic app-container host
fallbacks. TrueNAS app-containers may reuse runtime metadata such as image and
runtime strings, but they must not inherit Docker-only update affordances or
agent-only Discovery tabs on the dashboard row path unless the unified
agent-only Discovery tabs on the Workloads row path unless the unified
resource contract explicitly supplies discovery ownership.
That same row-and-selection boundary also requires canonical app-container
identity to stay intact on the workload surface. `/workloads` rows, deep links,
@@ -729,50 +725,50 @@ When a row is only known through an agent or container runtime, the table must
prefer reported OS/appliance identity before falling back to Docker/runtime
capability labels.
That derived workload owner now also routes grouped row windowing through
`frontend-modern/src/components/Dashboard/useGroupedTableWindowing.ts`, which
`frontend-modern/src/components/Workloads/useGroupedTableWindowing.ts`, which
owns row-window thresholds, overscan behavior, reveal-index clamping, and
per-group visible-slice derivation. Future dashboard table windowing changes
per-group visible-slice derivation. Future Workloads table windowing changes
must extend through that hook instead of rebuilding scroll math or mounted-row
budgets inline inside `frontend-modern/src/components/Dashboard/useDashboardWorkloadDerivedState.ts`.
budgets inline inside `frontend-modern/src/components/Workloads/useWorkloadsDerivedState.ts`.
Viewport-driven grouped table synchronization now also routes through
`frontend-modern/src/components/Dashboard/useDashboardWorkloadViewportSync.ts`,
which owns the dashboard table body measurement and the scroll/resize listener
`frontend-modern/src/components/Workloads/useWorkloadViewportSync.ts`,
which owns the Workloads table body measurement and the scroll/resize listener
lifecycle. Future viewport sync changes must extend through that hook rather
than rebuilding browser-event wiring or table-body geometry reads inside
`frontend-modern/src/components/Dashboard/useDashboardWorkloadDerivedState.ts`.
The dashboard guest-row path now follows the same pattern: the render shell
stays in `frontend-modern/src/components/Dashboard/GuestRow.tsx`, tooltip-backed
cell presentation lives in `frontend-modern/src/components/Dashboard/GuestRowCells.tsx`,
`frontend-modern/src/components/Workloads/useWorkloadsDerivedState.ts`.
The workload guest-row path now follows the same pattern: the render shell
stays in `frontend-modern/src/components/Workloads/GuestRow.tsx`, tooltip-backed
cell presentation lives in `frontend-modern/src/components/Workloads/GuestRowCells.tsx`,
and the canonical row contract and per-row hot-path derivations live in
`frontend-modern/src/components/Dashboard/guestRowModel.tsx` and
`frontend-modern/src/components/Dashboard/useGuestRowState.ts`. Future row
`frontend-modern/src/components/Workloads/guestRowModel.tsx` and
`frontend-modern/src/components/Workloads/useGuestRowState.ts`. Future row
identity, column, cell-tooltip, anomaly-correlation, and link-state changes
must extend through those owners instead of rebuilding row-local state inside
the shell.
That per-row link state now also consumes the shared
`frontend-modern/src/routing/resourceLinks.ts` workload-to-infrastructure
helper instead of a dashboard-local routing shim. Future infrastructure-link
changes for dashboard rows must extend through the shared routing owner rather
helper instead of a workload-local routing shim. Future infrastructure-link
changes for workload rows must extend through the shared routing owner rather
than recreating feature-local path builders.
That shell now also routes tag-dot rendering through the shared
`frontend-modern/src/components/shared/TagBadges.tsx` primitive instead of
keeping a dashboard-local badge helper. Future guest-row tag presentation
keeping a workload-local badge helper. Future guest-row tag presentation
changes must extend through that shared owner rather than reintroducing a
dashboard-only tag-badge variant.
The dashboard guest drawer now follows that same ownership rule: the shell
stays in `frontend-modern/src/components/Dashboard/GuestDrawer.tsx`, the
workload-only tag-badge variant.
The Workloads guest drawer now follows that same ownership rule: the shell
stays in `frontend-modern/src/components/Workloads/GuestDrawer.tsx`, the
overview card surface lives in
`frontend-modern/src/components/Dashboard/GuestDrawerOverview.tsx`, and
`frontend-modern/src/components/Workloads/GuestDrawerOverview.tsx`, and
drawer-local normalization, backup/tag formatting, discovery identity wiring,
and workload-derived navigation state live in
`frontend-modern/src/components/Dashboard/guestDrawerModel.ts` and
`frontend-modern/src/components/Dashboard/useGuestDrawerState.ts`. Future
`frontend-modern/src/components/Workloads/guestDrawerModel.ts` and
`frontend-modern/src/components/Workloads/useGuestDrawerState.ts`. Future
drawer runtime and overview-surface changes must extend through those owners
instead of adding more mixed state and helper drift back into the shell.
That drawer state now also consumes the same shared
`frontend-modern/src/routing/resourceLinks.ts` workload-to-infrastructure
helper, so row and drawer navigation stay aligned without a second
dashboard-local link-mapping file.
workload-local link-mapping file.
The shared infrastructure mapper hot path now stays intentionally narrow:
`frontend-modern/src/components/Infrastructure/resourceDetailMappers.ts`
continues to own sensor-label presentation and hot-path host/agent projection,
@@ -790,86 +786,86 @@ service-model helpers, while
stays a thin render shell. Future running-task visibility changes must extend
through the shared model and accepted performance proof file instead of adding
per-row PBS status parsing or inline job scans in the table render path.
The dashboard disk list now follows the same pattern: the shell stays in
`frontend-modern/src/components/Dashboard/DiskList.tsx`, while disk-row
The Workloads disk list now follows the same pattern: the shell stays in
`frontend-modern/src/components/Workloads/DiskList.tsx`, while disk-row
presentation derivations and fallback tooltip/runtime wiring live in
`frontend-modern/src/components/Dashboard/diskListModel.ts` and
`frontend-modern/src/components/Dashboard/useDiskListState.ts`. Future disk
`frontend-modern/src/components/Workloads/diskListModel.ts` and
`frontend-modern/src/components/Workloads/useDiskListState.ts`. Future disk
usage math, threshold-color routing, and fallback handling must extend
through those owners instead of accreting back into the shell.
The dashboard filter now follows that same ownership rule: the shell
stays in `frontend-modern/src/components/Dashboard/DashboardFilter.tsx`, while
The Workloads filter now follows that same ownership rule: the shell
stays in `frontend-modern/src/components/Workloads/WorkloadsFilter.tsx`, while
toolbar defaults, active-filter counting, and reset semantics live in
`frontend-modern/src/components/Dashboard/dashboardFilterModel.ts` and
`frontend-modern/src/components/Dashboard/useDashboardFilterState.ts`.
Dashboard table-mode controls must also keep their accessible group name aligned
`frontend-modern/src/components/Workloads/workloadsFilterModel.ts` and
`frontend-modern/src/components/Workloads/useWorkloadsFilterState.ts`.
Workloads table-mode controls must also keep their accessible group name aligned
with the shared table presentation contract by using `Group by` for the
grouped/list selector instead of reintroducing local `Group By` casing or
platform-specific cluster wording. Dense workload toolbar variants must keep
that same row wrap-capable so optional runtime, chart, column, and reset
controls remain reachable on desktop instead of forcing a single no-wrap row
that clips trailing actions.
The dashboard-owned filter-config assembly now lives in
`frontend-modern/src/components/Dashboard/useDashboardState.ts`, so future
The Workloads-owned filter-config assembly now lives in
`frontend-modern/src/components/Workloads/useWorkloadsState.ts`, so future
filter runtime changes must extend through those owners instead of
reintroducing dashboard-local state, reset drift, or inline config assembly
reintroducing workload-local state, reset drift, or inline config assembly
into the shell.
Workload type option labels are part of that filter-config model ownership:
`DashboardFilter.tsx` must render the exported workload type option catalog
`WorkloadsFilter.tsx` must render the exported workload type option catalog
instead of embedding its own `System containers` / `App containers` wording in
the shell.
The dashboard threshold slider now follows that same pattern: the shell stays
in `frontend-modern/src/components/Dashboard/ThresholdSlider.tsx`, while
in `frontend-modern/src/components/Workloads/ThresholdSlider.tsx`, while
metric-type text and fill presentation live in
`frontend-modern/src/utils/thresholdSliderPresentation.ts`, while
slider bounds math, thumb-position transforms, and title/label derivations
live in `frontend-modern/src/components/Dashboard/thresholdSliderModel.ts`
live in `frontend-modern/src/components/Workloads/thresholdSliderModel.ts`
and drag scroll-lock lifecycle lives in
`frontend-modern/src/components/Dashboard/useThresholdSliderState.ts`.
`frontend-modern/src/components/Workloads/useThresholdSliderState.ts`.
Future slider runtime changes must extend through those owners instead of
reintroducing mixed drag state, type-color formatting, and presentation logic
into the shell.
The dashboard stacked disk bar now follows that same pattern: the shell stays
in `frontend-modern/src/components/Dashboard/StackedDiskBar.tsx`, while
in `frontend-modern/src/components/Workloads/StackedDiskBar.tsx`, while
disk-capacity math, segment and tooltip derivation, max-disk labeling, and
mode-specific presentation live in
`frontend-modern/src/components/Dashboard/stackedDiskBarModel.ts` and
`frontend-modern/src/components/Workloads/stackedDiskBarModel.ts` and
resize-observer plus tooltip lifecycle live in
`frontend-modern/src/components/Dashboard/useStackedDiskBarState.ts`.
`frontend-modern/src/components/Workloads/useStackedDiskBarState.ts`.
Future disk-bar runtime changes must extend through those owners instead of
reintroducing mixed resize state and presentation branching into the shell.
The dashboard stacked memory bar now follows that same pattern: the shell
stays in `frontend-modern/src/components/Dashboard/StackedMemoryBar.tsx`,
stays in `frontend-modern/src/components/Workloads/StackedMemoryBar.tsx`,
while memory-capacity math, balloon/swap tooltip derivation, anomaly label
presentation, and sublabel-fit logic live in
`frontend-modern/src/components/Dashboard/stackedMemoryBarModel.ts` and
`frontend-modern/src/components/Workloads/stackedMemoryBarModel.ts` and
resize-observer plus tooltip lifecycle live in
`frontend-modern/src/components/Dashboard/useStackedMemoryBarState.ts`.
`frontend-modern/src/components/Workloads/useStackedMemoryBarState.ts`.
Future memory-bar runtime changes must extend through those owners instead of
reintroducing mixed resize state, balloon branching, and tooltip shaping into
the shell.
The dashboard metric bar now follows that same pattern: the shell stays in
`frontend-modern/src/components/Dashboard/MetricBar.tsx`, while width,
`frontend-modern/src/components/Workloads/MetricBar.tsx`, while width,
show-label, sublabel-fit, and threshold-color derivation live in
`frontend-modern/src/components/Dashboard/metricBarModel.ts` and
`frontend-modern/src/components/Workloads/metricBarModel.ts` and
resize-observer lifecycle lives in
`frontend-modern/src/components/Dashboard/useMetricBarState.ts`. Future
`frontend-modern/src/components/Workloads/useMetricBarState.ts`. Future
metric-bar runtime changes must extend through those owners instead of
reintroducing mixed resize state and label-fit logic into the shell.
The dashboard enhanced CPU bar now follows that same pattern: the shell stays
in `frontend-modern/src/components/Dashboard/EnhancedCPUBar.tsx`, while usage
in `frontend-modern/src/components/Workloads/EnhancedCPUBar.tsx`, while usage
formatting, anomaly presentation, tooltip load-average formatting, and
threshold-driven label state live in
`frontend-modern/src/components/Dashboard/enhancedCpuBarModel.ts` and
`frontend-modern/src/components/Workloads/enhancedCpuBarModel.ts` and
tooltip lifecycle lives in
`frontend-modern/src/components/Dashboard/useEnhancedCPUBarState.ts`. Future
`frontend-modern/src/components/Workloads/useEnhancedCPUBarState.ts`. Future
CPU-bar runtime changes must extend through those owners instead of
reintroducing mixed tooltip state and formatting logic into the shell.
The unified resource table hot path is now also governed as explicit
performance-owned runtime, with shared ownership against the unified-resource
consumer boundary. The remaining performance work is no longer top-level
ownership ambiguity on the main dashboard or infrastructure tables.
ownership ambiguity on the main Infrastructure or Workloads tables.
The table's reactive runtime, grouping, and row-windowing owner now lives in
`frontend-modern/src/components/Infrastructure/useUnifiedResourceTableState.ts`,
while pure table-state derivation, service sorting, sort-cycle policy, and
@@ -975,7 +971,7 @@ The shared workloads-link helper used by the resource drawer and table now
also routes its Kubernetes-cluster fallback through the same preferred
resource display contract, so navigation context does not leak raw
`displayName` values for governed clusters.
That same workloads-link path and the dashboard workload projection now also
That same workloads-link path and the workload projection now also
share the canonical cluster-name helpers in the shared agent-resource layer,
so route labels, pod grouping, and cluster-name fetch keys keep using the
same source of truth instead of rebuilding the `clusterName`/`context`/
@@ -1084,7 +1080,7 @@ doing range selection and fallback. Feature-local polling loops or browser-side
disk ring buffers are forbidden because they duplicate live sampling work and
drift out of sync with the governed history timeline.
That same hot-path ownership now also requires lazy-load-safe websocket
consumption. `frontend-modern/src/components/Dashboard/useDashboardState.ts`
consumption. `frontend-modern/src/components/Workloads/useWorkloadsState.ts`
may read connection and alert state only through
`frontend-modern/src/contexts/appRuntime.ts`; it must not import `@/App` or
create a reverse dependency into the root shell just to read websocket state.
@@ -1093,15 +1089,15 @@ Shared shells may consume the small `/api/license/runtime-capabilities`
contract for feature truth, but commercial demo routes stay hidden and the
browser must not keep retrying `/api/license/commercial-posture`,
`/api/license/entitlements`, `/auth/license-purchase-start`, or other hidden
commercial endpoints from performance-sensitive settings or dashboard shells.
Dashboard and infrastructure summary consumers now also keep null-tolerant read
commercial endpoints from performance-sensitive settings or route shells.
Workloads and infrastructure summary consumers now also keep null-tolerant read
models on the shared hot path. Guest rows, stacked bars, anomaly summaries, and
resource detail mappers may accept partial platform metadata or undefined
ratios, but they must normalize those values once in the shared model layer
instead of scattering non-null assertions or per-component count coercion
through the dashboard runtime.
through the Workloads runtime.
That same workload-table hot path now also owns a single width contract.
`frontend-modern/src/components/Dashboard/DashboardWorkloadTable.tsx`,
`frontend-modern/src/components/Workloads/WorkloadsTable.tsx`,
`WorkloadTableHeader.tsx`, `GuestRow.tsx`, and `guestRowModel.tsx` must remain
the canonical owners for desktop and mobile workload column sizing. Global CSS
must not reintroduce competing `.workload-table [data-workload-col=…]` width
@@ -1320,7 +1320,6 @@
"exact_files": [
"frontend-modern/src/components/Alerts/__tests__/BulkEditDialog.test.tsx",
"frontend-modern/src/components/Alerts/__tests__/InvestigateAlertButton.test.tsx",
"frontend-modern/src/components/Alerts/__tests__/RecentAlertsPanel.test.tsx",
"frontend-modern/src/components/Alerts/__tests__/ThresholdsTable.test.tsx",
"frontend-modern/src/components/Alerts/EmailProviderSelect.test.tsx",
"frontend-modern/src/components/Alerts/ResourceTable.test.tsx",
@@ -2990,27 +2989,11 @@
"frontend-modern/src/components/SetupWizard/SetupCompletionPreview.tsx",
"frontend-modern/src/components/SetupWizard/SetupWizard.tsx",
"frontend-modern/src/components/Toast/Toast.tsx",
"frontend-modern/src/features/dashboardOverview/ActionRequiredPanel.tsx",
"frontend-modern/src/features/dashboardOverview/DashboardCustomizer.tsx",
"frontend-modern/src/features/dashboardOverview/dashboardPulseBriefModel.ts",
"frontend-modern/src/features/dashboardOverview/estateSummaryModel.ts",
"frontend-modern/src/features/dashboardOverview/EstateSummaryPanel.tsx",
"frontend-modern/src/features/dashboardOverview/index.ts",
"frontend-modern/src/features/dashboardOverview/KPIStrip.tsx",
"frontend-modern/src/features/dashboardOverview/ProblemResourcesTable.tsx",
"frontend-modern/src/features/dashboardOverview/PulseBriefPanel.tsx",
"frontend-modern/src/features/dashboardOverview/TrendCharts.tsx",
"frontend-modern/src/features/dashboardOverview/useDashboardPulseBrief.ts",
"frontend-modern/src/hooks/createNonSuspendingQuery.ts",
"frontend-modern/src/index.css",
"frontend-modern/src/pages/Operations.tsx",
"frontend-modern/src/stores/aiChat.ts",
"frontend-modern/src/utils/aiSettingsPresentation.ts",
"frontend-modern/src/utils/dashboardEmptyStatePresentation.ts",
"frontend-modern/src/utils/dashboardGuestPresentation.ts",
"frontend-modern/src/utils/dashboardKpiPresentation.ts",
"frontend-modern/src/utils/dashboardMetricPresentation.ts",
"frontend-modern/src/utils/dashboardTrendPresentation.ts",
"frontend-modern/src/utils/diagnosticsPresentation.ts",
"frontend-modern/src/utils/discoveryPresentation.ts",
"frontend-modern/src/utils/emptyStatePresentation.ts",
@@ -3032,7 +3015,9 @@
"frontend-modern/src/utils/toast.ts",
"frontend-modern/src/utils/typeColumnPresentation.ts",
"frontend-modern/src/utils/updatesPresentation.ts",
"frontend-modern/src/utils/upgradeNavigation.ts"
"frontend-modern/src/utils/upgradeNavigation.ts",
"frontend-modern/src/utils/workloadEmptyStatePresentation.ts",
"frontend-modern/src/utils/workloadGuestPresentation.ts"
],
"verification": {
"allow_same_subsystem_tests": false,
@@ -3214,24 +3199,18 @@
]
},
{
"id": "dashboard-presentation-helpers",
"label": "dashboard presentation helper proof",
"id": "workload-presentation-helpers",
"label": "workload presentation helper proof",
"match_prefixes": [],
"match_files": [
"frontend-modern/src/utils/dashboardEmptyStatePresentation.ts",
"frontend-modern/src/utils/dashboardGuestPresentation.ts",
"frontend-modern/src/utils/dashboardKpiPresentation.ts",
"frontend-modern/src/utils/dashboardMetricPresentation.ts",
"frontend-modern/src/utils/dashboardTrendPresentation.ts"
"frontend-modern/src/utils/workloadEmptyStatePresentation.ts",
"frontend-modern/src/utils/workloadGuestPresentation.ts"
],
"allow_same_subsystem_tests": false,
"test_prefixes": [],
"exact_files": [
"frontend-modern/src/utils/__tests__/dashboardEmptyStatePresentation.test.ts",
"frontend-modern/src/utils/__tests__/dashboardGuestPresentation.test.ts",
"frontend-modern/src/utils/__tests__/dashboardKpiPresentation.test.ts",
"frontend-modern/src/utils/__tests__/dashboardMetricPresentation.test.ts",
"frontend-modern/src/utils/__tests__/dashboardTrendPresentation.test.ts"
"frontend-modern/src/utils/__tests__/workloadEmptyStatePresentation.test.ts",
"frontend-modern/src/utils/__tests__/workloadGuestPresentation.test.ts"
]
},
{
@@ -3256,6 +3235,7 @@
"match_files": [
"frontend-modern/src/components/Toast/Toast.tsx",
"frontend-modern/src/utils/emptyStatePresentation.ts",
"frontend-modern/src/utils/problemResourcePresentation.ts",
"frontend-modern/src/utils/semanticTonePresentation.ts",
"frontend-modern/src/utils/toast.ts",
"frontend-modern/src/utils/typeColumnPresentation.ts"
@@ -3266,6 +3246,7 @@
"frontend-modern/src/components/shared/TypeColumn.guardrails.test.ts",
"frontend-modern/src/components/Toast/__tests__/Toast.test.tsx",
"frontend-modern/src/utils/__tests__/emptyStatePresentation.test.ts",
"frontend-modern/src/utils/__tests__/problemResourcePresentation.test.ts",
"frontend-modern/src/utils/__tests__/semanticTonePresentation.test.ts",
"frontend-modern/src/utils/__tests__/typeColumnPresentation.test.ts"
]
@@ -3393,37 +3374,6 @@
"frontend-modern/src/components/Infrastructure/__tests__/ResourceDetailDrawer.history.test.tsx"
]
},
{
"id": "dashboard-overview-feature-surface",
"label": "dashboard overview feature proof",
"match_prefixes": [],
"match_files": [
"frontend-modern/src/features/dashboardOverview/ActionRequiredPanel.tsx",
"frontend-modern/src/features/dashboardOverview/DashboardCustomizer.tsx",
"frontend-modern/src/features/dashboardOverview/dashboardPulseBriefModel.ts",
"frontend-modern/src/features/dashboardOverview/estateSummaryModel.ts",
"frontend-modern/src/features/dashboardOverview/EstateSummaryPanel.tsx",
"frontend-modern/src/features/dashboardOverview/index.ts",
"frontend-modern/src/features/dashboardOverview/KPIStrip.tsx",
"frontend-modern/src/features/dashboardOverview/ProblemResourcesTable.tsx",
"frontend-modern/src/features/dashboardOverview/PulseBriefPanel.tsx",
"frontend-modern/src/features/dashboardOverview/TrendCharts.tsx",
"frontend-modern/src/features/dashboardOverview/useDashboardPulseBrief.ts",
"frontend-modern/src/utils/problemResourcePresentation.ts"
],
"allow_same_subsystem_tests": false,
"test_prefixes": [],
"exact_files": [
"frontend-modern/src/features/dashboardOverview/__tests__/dashboardPulseBriefModel.test.ts",
"frontend-modern/src/features/dashboardOverview/__tests__/KPIStrip.test.tsx",
"frontend-modern/src/features/dashboardOverview/__tests__/ProblemResourcesTable.test.tsx",
"frontend-modern/src/features/dashboardOverview/__tests__/TrendCharts.test.tsx",
"frontend-modern/src/pages/__tests__/DashboardPage.test.tsx",
"frontend-modern/src/utils/__tests__/frontendResourceTypeBoundaries.test.ts",
"frontend-modern/src/utils/__tests__/problemResourcePresentation.test.ts",
"frontend-modern/src/utils/__tests__/typeColumnPresentation.test.ts"
]
},
{
"id": "first-session-runtime-and-preview",
"label": "first-session runtime and preview proof",
@@ -4155,58 +4105,6 @@
"pkg/metrics/"
],
"owned_files": [
"frontend-modern/src/components/Dashboard/Dashboard.tsx",
"frontend-modern/src/components/Dashboard/DashboardFilter.tsx",
"frontend-modern/src/components/Dashboard/dashboardFilterModel.ts",
"frontend-modern/src/components/Dashboard/dashboardSelectionModel.ts",
"frontend-modern/src/components/Dashboard/DashboardStateCards.tsx",
"frontend-modern/src/components/Dashboard/DashboardStatsStrip.tsx",
"frontend-modern/src/components/Dashboard/dashboardWorkloadFilterConfigModel.ts",
"frontend-modern/src/components/Dashboard/dashboardWorkloadRouteModel.ts",
"frontend-modern/src/components/Dashboard/dashboardWorkloadRouteStateModel.ts",
"frontend-modern/src/components/Dashboard/DashboardWorkloadTable.tsx",
"frontend-modern/src/components/Dashboard/dashboardWorkloadUrlSyncModel.ts",
"frontend-modern/src/components/Dashboard/DiskList.tsx",
"frontend-modern/src/components/Dashboard/diskListModel.ts",
"frontend-modern/src/components/Dashboard/EnhancedCPUBar.tsx",
"frontend-modern/src/components/Dashboard/enhancedCpuBarModel.ts",
"frontend-modern/src/components/Dashboard/GuestDrawer.tsx",
"frontend-modern/src/components/Dashboard/guestDrawerModel.ts",
"frontend-modern/src/components/Dashboard/GuestDrawerOverview.tsx",
"frontend-modern/src/components/Dashboard/GuestRow.tsx",
"frontend-modern/src/components/Dashboard/GuestRowCells.tsx",
"frontend-modern/src/components/Dashboard/guestRowModel.tsx",
"frontend-modern/src/components/Dashboard/MetricBar.tsx",
"frontend-modern/src/components/Dashboard/metricBarModel.ts",
"frontend-modern/src/components/Dashboard/StackedDiskBar.tsx",
"frontend-modern/src/components/Dashboard/stackedDiskBarModel.ts",
"frontend-modern/src/components/Dashboard/StackedMemoryBar.tsx",
"frontend-modern/src/components/Dashboard/stackedMemoryBarModel.ts",
"frontend-modern/src/components/Dashboard/ThresholdSlider.tsx",
"frontend-modern/src/components/Dashboard/thresholdSliderModel.ts",
"frontend-modern/src/components/Dashboard/useDashboardControlsState.ts",
"frontend-modern/src/components/Dashboard/useDashboardFilterState.ts",
"frontend-modern/src/components/Dashboard/useDashboardGuestMetadataState.ts",
"frontend-modern/src/components/Dashboard/useDashboardSelectionState.ts",
"frontend-modern/src/components/Dashboard/useDashboardState.ts",
"frontend-modern/src/components/Dashboard/useDashboardWorkloadDerivedState.ts",
"frontend-modern/src/components/Dashboard/useDashboardWorkloadFilterOptions.ts",
"frontend-modern/src/components/Dashboard/useDashboardWorkloadRouteState.ts",
"frontend-modern/src/components/Dashboard/useDashboardWorkloadUrlSync.ts",
"frontend-modern/src/components/Dashboard/useDashboardWorkloadViewportSync.ts",
"frontend-modern/src/components/Dashboard/useDiskListState.ts",
"frontend-modern/src/components/Dashboard/useEnhancedCPUBarState.ts",
"frontend-modern/src/components/Dashboard/useGroupedTableWindowing.ts",
"frontend-modern/src/components/Dashboard/useGuestDrawerState.ts",
"frontend-modern/src/components/Dashboard/useGuestRowState.ts",
"frontend-modern/src/components/Dashboard/useMetricBarState.ts",
"frontend-modern/src/components/Dashboard/useStackedDiskBarState.ts",
"frontend-modern/src/components/Dashboard/useStackedMemoryBarState.ts",
"frontend-modern/src/components/Dashboard/useThresholdSliderState.ts",
"frontend-modern/src/components/Dashboard/WorkloadPanel.tsx",
"frontend-modern/src/components/Dashboard/workloadSelectors.ts",
"frontend-modern/src/components/Dashboard/WorkloadTableHeader.tsx",
"frontend-modern/src/components/Dashboard/workloadTopology.ts",
"frontend-modern/src/components/Infrastructure/infrastructureSelectors.ts",
"frontend-modern/src/components/Infrastructure/InfrastructureSummary.tsx",
"frontend-modern/src/components/Infrastructure/infrastructureSummaryModel.ts",
@@ -4221,7 +4119,59 @@
"frontend-modern/src/components/Infrastructure/useInfrastructureSummaryState.ts",
"frontend-modern/src/components/Infrastructure/useUnifiedResourceTableState.ts",
"frontend-modern/src/components/Infrastructure/useUnifiedResourceTableViewportSync.ts",
"frontend-modern/src/components/Workloads/DiskList.tsx",
"frontend-modern/src/components/Workloads/diskListModel.ts",
"frontend-modern/src/components/Workloads/EnhancedCPUBar.tsx",
"frontend-modern/src/components/Workloads/enhancedCpuBarModel.ts",
"frontend-modern/src/components/Workloads/GuestDrawer.tsx",
"frontend-modern/src/components/Workloads/guestDrawerModel.ts",
"frontend-modern/src/components/Workloads/GuestDrawerOverview.tsx",
"frontend-modern/src/components/Workloads/GuestRow.tsx",
"frontend-modern/src/components/Workloads/GuestRowCells.tsx",
"frontend-modern/src/components/Workloads/guestRowModel.tsx",
"frontend-modern/src/components/Workloads/MetricBar.tsx",
"frontend-modern/src/components/Workloads/metricBarModel.ts",
"frontend-modern/src/components/Workloads/StackedDiskBar.tsx",
"frontend-modern/src/components/Workloads/stackedDiskBarModel.ts",
"frontend-modern/src/components/Workloads/StackedMemoryBar.tsx",
"frontend-modern/src/components/Workloads/stackedMemoryBarModel.ts",
"frontend-modern/src/components/Workloads/ThresholdSlider.tsx",
"frontend-modern/src/components/Workloads/thresholdSliderModel.ts",
"frontend-modern/src/components/Workloads/useDiskListState.ts",
"frontend-modern/src/components/Workloads/useEnhancedCPUBarState.ts",
"frontend-modern/src/components/Workloads/useGroupedTableWindowing.ts",
"frontend-modern/src/components/Workloads/useGuestDrawerState.ts",
"frontend-modern/src/components/Workloads/useGuestRowState.ts",
"frontend-modern/src/components/Workloads/useMetricBarState.ts",
"frontend-modern/src/components/Workloads/useStackedDiskBarState.ts",
"frontend-modern/src/components/Workloads/useStackedMemoryBarState.ts",
"frontend-modern/src/components/Workloads/useThresholdSliderState.ts",
"frontend-modern/src/components/Workloads/useWorkloadFilterOptions.ts",
"frontend-modern/src/components/Workloads/useWorkloadGuestMetadataState.ts",
"frontend-modern/src/components/Workloads/useWorkloadRouteState.ts",
"frontend-modern/src/components/Workloads/useWorkloadsControlsState.ts",
"frontend-modern/src/components/Workloads/useWorkloadsDerivedState.ts",
"frontend-modern/src/components/Workloads/useWorkloadSelectionState.ts",
"frontend-modern/src/components/Workloads/useWorkloadsFilterState.ts",
"frontend-modern/src/components/Workloads/useWorkloadsState.ts",
"frontend-modern/src/components/Workloads/useWorkloadUrlSync.ts",
"frontend-modern/src/components/Workloads/useWorkloadViewportSync.ts",
"frontend-modern/src/components/Workloads/workloadFilterConfigModel.ts",
"frontend-modern/src/components/Workloads/WorkloadPanel.tsx",
"frontend-modern/src/components/Workloads/workloadRouteModel.ts",
"frontend-modern/src/components/Workloads/workloadRouteStateModel.ts",
"frontend-modern/src/components/Workloads/workloadSelectionModel.ts",
"frontend-modern/src/components/Workloads/workloadSelectors.ts",
"frontend-modern/src/components/Workloads/WorkloadsFilter.tsx",
"frontend-modern/src/components/Workloads/workloadsFilterModel.ts",
"frontend-modern/src/components/Workloads/WorkloadsStateCards.tsx",
"frontend-modern/src/components/Workloads/WorkloadsStatsStrip.tsx",
"frontend-modern/src/components/Workloads/WorkloadsSummary.tsx",
"frontend-modern/src/components/Workloads/WorkloadsSurface.tsx",
"frontend-modern/src/components/Workloads/WorkloadsTable.tsx",
"frontend-modern/src/components/Workloads/WorkloadTableHeader.tsx",
"frontend-modern/src/components/Workloads/workloadTopology.ts",
"frontend-modern/src/components/Workloads/workloadUrlSyncModel.ts",
"frontend-modern/src/pages/Workloads.tsx",
"frontend-modern/src/utils/thresholdSliderPresentation.ts",
"frontend-modern/src/utils/throughputPresentation.ts",
@@ -4233,28 +4183,28 @@
"allow_same_subsystem_tests": false,
"test_prefixes": [],
"exact_files": [
"frontend-modern/src/components/Dashboard/__tests__/Dashboard.performance.contract.test.tsx",
"frontend-modern/src/components/Dashboard/__tests__/DashboardFilter.test.tsx",
"frontend-modern/src/components/Dashboard/__tests__/dashboardSelectionModel.test.ts",
"frontend-modern/src/components/Dashboard/__tests__/dashboardWorkloadFilterConfigModel.test.ts",
"frontend-modern/src/components/Dashboard/__tests__/dashboardWorkloadRouteModel.test.ts",
"frontend-modern/src/components/Dashboard/__tests__/dashboardWorkloadRouteStateModel.test.ts",
"frontend-modern/src/components/Dashboard/__tests__/dashboardWorkloadUrlSyncModel.test.ts",
"frontend-modern/src/components/Dashboard/__tests__/DiskList.test.tsx",
"frontend-modern/src/components/Dashboard/__tests__/EnhancedCPUBar.test.tsx",
"frontend-modern/src/components/Dashboard/__tests__/StackedDiskBar.test.tsx",
"frontend-modern/src/components/Dashboard/__tests__/useDashboardFilterState.test.ts",
"frontend-modern/src/components/Dashboard/__tests__/useDashboardSelectionState.test.ts",
"frontend-modern/src/components/Dashboard/__tests__/useDashboardWorkloadViewportSync.test.tsx",
"frontend-modern/src/components/Dashboard/__tests__/useEnhancedCPUBarState.test.tsx",
"frontend-modern/src/components/Dashboard/__tests__/useGroupedTableWindowing.test.ts",
"frontend-modern/src/components/Dashboard/__tests__/useMetricBarState.test.tsx",
"frontend-modern/src/components/Dashboard/__tests__/useStackedDiskBarState.test.tsx",
"frontend-modern/src/components/Dashboard/__tests__/useStackedMemoryBarState.test.tsx",
"frontend-modern/src/components/Dashboard/__tests__/useThresholdSliderState.test.ts",
"frontend-modern/src/components/Dashboard/MetricBar.test.tsx",
"frontend-modern/src/components/Dashboard/StackedMemoryBar.test.tsx",
"frontend-modern/src/components/Dashboard/ThresholdSlider.test.tsx",
"frontend-modern/src/components/Workloads/__tests__/DiskList.test.tsx",
"frontend-modern/src/components/Workloads/__tests__/EnhancedCPUBar.test.tsx",
"frontend-modern/src/components/Workloads/__tests__/StackedDiskBar.test.tsx",
"frontend-modern/src/components/Workloads/__tests__/useEnhancedCPUBarState.test.tsx",
"frontend-modern/src/components/Workloads/__tests__/useGroupedTableWindowing.test.ts",
"frontend-modern/src/components/Workloads/__tests__/useMetricBarState.test.tsx",
"frontend-modern/src/components/Workloads/__tests__/useStackedDiskBarState.test.tsx",
"frontend-modern/src/components/Workloads/__tests__/useStackedMemoryBarState.test.tsx",
"frontend-modern/src/components/Workloads/__tests__/useThresholdSliderState.test.ts",
"frontend-modern/src/components/Workloads/__tests__/useWorkloadSelectionState.test.ts",
"frontend-modern/src/components/Workloads/__tests__/useWorkloadsFilterState.test.ts",
"frontend-modern/src/components/Workloads/__tests__/useWorkloadViewportSync.test.tsx",
"frontend-modern/src/components/Workloads/__tests__/workloadFilterConfigModel.test.ts",
"frontend-modern/src/components/Workloads/__tests__/workloadRouteModel.test.ts",
"frontend-modern/src/components/Workloads/__tests__/workloadRouteStateModel.test.ts",
"frontend-modern/src/components/Workloads/__tests__/workloadSelectionModel.test.ts",
"frontend-modern/src/components/Workloads/__tests__/WorkloadsFilter.test.tsx",
"frontend-modern/src/components/Workloads/__tests__/WorkloadsSurface.performance.contract.test.tsx",
"frontend-modern/src/components/Workloads/__tests__/workloadUrlSyncModel.test.ts",
"frontend-modern/src/components/Workloads/MetricBar.test.tsx",
"frontend-modern/src/components/Workloads/StackedMemoryBar.test.tsx",
"frontend-modern/src/components/Workloads/ThresholdSlider.test.tsx",
"internal/api/router_bench_test.go",
"internal/api/slo_bench_test.go",
"pkg/metrics/store_bench_test.go",
@@ -4280,62 +4230,10 @@
]
},
{
"id": "dashboard-workload-hot-path",
"label": "dashboard workload selector proof",
"id": "workloads-hot-path",
"label": "Workloads hot-path proof",
"match_prefixes": [],
"match_files": [
"frontend-modern/src/components/Dashboard/Dashboard.tsx",
"frontend-modern/src/components/Dashboard/DashboardFilter.tsx",
"frontend-modern/src/components/Dashboard/dashboardFilterModel.ts",
"frontend-modern/src/components/Dashboard/dashboardSelectionModel.ts",
"frontend-modern/src/components/Dashboard/DashboardStateCards.tsx",
"frontend-modern/src/components/Dashboard/DashboardStatsStrip.tsx",
"frontend-modern/src/components/Dashboard/dashboardWorkloadFilterConfigModel.ts",
"frontend-modern/src/components/Dashboard/dashboardWorkloadRouteModel.ts",
"frontend-modern/src/components/Dashboard/dashboardWorkloadRouteStateModel.ts",
"frontend-modern/src/components/Dashboard/DashboardWorkloadTable.tsx",
"frontend-modern/src/components/Dashboard/dashboardWorkloadUrlSyncModel.ts",
"frontend-modern/src/components/Dashboard/DiskList.tsx",
"frontend-modern/src/components/Dashboard/diskListModel.ts",
"frontend-modern/src/components/Dashboard/EnhancedCPUBar.tsx",
"frontend-modern/src/components/Dashboard/enhancedCpuBarModel.ts",
"frontend-modern/src/components/Dashboard/GuestDrawer.tsx",
"frontend-modern/src/components/Dashboard/guestDrawerModel.ts",
"frontend-modern/src/components/Dashboard/GuestDrawerOverview.tsx",
"frontend-modern/src/components/Dashboard/GuestRow.tsx",
"frontend-modern/src/components/Dashboard/GuestRowCells.tsx",
"frontend-modern/src/components/Dashboard/guestRowModel.tsx",
"frontend-modern/src/components/Dashboard/MetricBar.tsx",
"frontend-modern/src/components/Dashboard/metricBarModel.ts",
"frontend-modern/src/components/Dashboard/StackedDiskBar.tsx",
"frontend-modern/src/components/Dashboard/stackedDiskBarModel.ts",
"frontend-modern/src/components/Dashboard/StackedMemoryBar.tsx",
"frontend-modern/src/components/Dashboard/stackedMemoryBarModel.ts",
"frontend-modern/src/components/Dashboard/ThresholdSlider.tsx",
"frontend-modern/src/components/Dashboard/thresholdSliderModel.ts",
"frontend-modern/src/components/Dashboard/useDashboardControlsState.ts",
"frontend-modern/src/components/Dashboard/useDashboardFilterState.ts",
"frontend-modern/src/components/Dashboard/useDashboardGuestMetadataState.ts",
"frontend-modern/src/components/Dashboard/useDashboardSelectionState.ts",
"frontend-modern/src/components/Dashboard/useDashboardState.ts",
"frontend-modern/src/components/Dashboard/useDashboardWorkloadDerivedState.ts",
"frontend-modern/src/components/Dashboard/useDashboardWorkloadFilterOptions.ts",
"frontend-modern/src/components/Dashboard/useDashboardWorkloadRouteState.ts",
"frontend-modern/src/components/Dashboard/useDashboardWorkloadUrlSync.ts",
"frontend-modern/src/components/Dashboard/useDashboardWorkloadViewportSync.ts",
"frontend-modern/src/components/Dashboard/useDiskListState.ts",
"frontend-modern/src/components/Dashboard/useEnhancedCPUBarState.ts",
"frontend-modern/src/components/Dashboard/useGroupedTableWindowing.ts",
"frontend-modern/src/components/Dashboard/useGuestDrawerState.ts",
"frontend-modern/src/components/Dashboard/useGuestRowState.ts",
"frontend-modern/src/components/Dashboard/useMetricBarState.ts",
"frontend-modern/src/components/Dashboard/useStackedDiskBarState.ts",
"frontend-modern/src/components/Dashboard/useStackedMemoryBarState.ts",
"frontend-modern/src/components/Dashboard/useThresholdSliderState.ts",
"frontend-modern/src/components/Dashboard/WorkloadPanel.tsx",
"frontend-modern/src/components/Dashboard/workloadSelectors.ts",
"frontend-modern/src/components/Dashboard/WorkloadTableHeader.tsx",
"frontend-modern/src/components/Dashboard/workloadTopology.ts",
"frontend-modern/src/components/Infrastructure/infrastructureSelectors.ts",
"frontend-modern/src/components/Infrastructure/InfrastructureSummary.tsx",
"frontend-modern/src/components/Infrastructure/infrastructureSummaryModel.ts",
@@ -4350,42 +4248,94 @@
"frontend-modern/src/components/Infrastructure/useInfrastructureSummaryState.ts",
"frontend-modern/src/components/Infrastructure/useUnifiedResourceTableState.ts",
"frontend-modern/src/components/Infrastructure/useUnifiedResourceTableViewportSync.ts",
"frontend-modern/src/components/Workloads/DiskList.tsx",
"frontend-modern/src/components/Workloads/diskListModel.ts",
"frontend-modern/src/components/Workloads/EnhancedCPUBar.tsx",
"frontend-modern/src/components/Workloads/enhancedCpuBarModel.ts",
"frontend-modern/src/components/Workloads/GuestDrawer.tsx",
"frontend-modern/src/components/Workloads/guestDrawerModel.ts",
"frontend-modern/src/components/Workloads/GuestDrawerOverview.tsx",
"frontend-modern/src/components/Workloads/GuestRow.tsx",
"frontend-modern/src/components/Workloads/GuestRowCells.tsx",
"frontend-modern/src/components/Workloads/guestRowModel.tsx",
"frontend-modern/src/components/Workloads/MetricBar.tsx",
"frontend-modern/src/components/Workloads/metricBarModel.ts",
"frontend-modern/src/components/Workloads/StackedDiskBar.tsx",
"frontend-modern/src/components/Workloads/stackedDiskBarModel.ts",
"frontend-modern/src/components/Workloads/StackedMemoryBar.tsx",
"frontend-modern/src/components/Workloads/stackedMemoryBarModel.ts",
"frontend-modern/src/components/Workloads/ThresholdSlider.tsx",
"frontend-modern/src/components/Workloads/thresholdSliderModel.ts",
"frontend-modern/src/components/Workloads/useDiskListState.ts",
"frontend-modern/src/components/Workloads/useEnhancedCPUBarState.ts",
"frontend-modern/src/components/Workloads/useGroupedTableWindowing.ts",
"frontend-modern/src/components/Workloads/useGuestDrawerState.ts",
"frontend-modern/src/components/Workloads/useGuestRowState.ts",
"frontend-modern/src/components/Workloads/useMetricBarState.ts",
"frontend-modern/src/components/Workloads/useStackedDiskBarState.ts",
"frontend-modern/src/components/Workloads/useStackedMemoryBarState.ts",
"frontend-modern/src/components/Workloads/useThresholdSliderState.ts",
"frontend-modern/src/components/Workloads/useWorkloadFilterOptions.ts",
"frontend-modern/src/components/Workloads/useWorkloadGuestMetadataState.ts",
"frontend-modern/src/components/Workloads/useWorkloadRouteState.ts",
"frontend-modern/src/components/Workloads/useWorkloadsControlsState.ts",
"frontend-modern/src/components/Workloads/useWorkloadsDerivedState.ts",
"frontend-modern/src/components/Workloads/useWorkloadSelectionState.ts",
"frontend-modern/src/components/Workloads/useWorkloadsFilterState.ts",
"frontend-modern/src/components/Workloads/useWorkloadsState.ts",
"frontend-modern/src/components/Workloads/useWorkloadUrlSync.ts",
"frontend-modern/src/components/Workloads/useWorkloadViewportSync.ts",
"frontend-modern/src/components/Workloads/workloadFilterConfigModel.ts",
"frontend-modern/src/components/Workloads/WorkloadPanel.tsx",
"frontend-modern/src/components/Workloads/workloadRouteModel.ts",
"frontend-modern/src/components/Workloads/workloadRouteStateModel.ts",
"frontend-modern/src/components/Workloads/workloadSelectionModel.ts",
"frontend-modern/src/components/Workloads/workloadSelectors.ts",
"frontend-modern/src/components/Workloads/WorkloadsFilter.tsx",
"frontend-modern/src/components/Workloads/workloadsFilterModel.ts",
"frontend-modern/src/components/Workloads/WorkloadsStateCards.tsx",
"frontend-modern/src/components/Workloads/WorkloadsStatsStrip.tsx",
"frontend-modern/src/components/Workloads/WorkloadsSummary.tsx",
"frontend-modern/src/components/Workloads/WorkloadsSurface.tsx",
"frontend-modern/src/components/Workloads/WorkloadsTable.tsx",
"frontend-modern/src/components/Workloads/WorkloadTableHeader.tsx",
"frontend-modern/src/components/Workloads/workloadTopology.ts",
"frontend-modern/src/components/Workloads/workloadUrlSyncModel.ts",
"frontend-modern/src/pages/Workloads.tsx",
"frontend-modern/src/utils/thresholdSliderPresentation.ts"
],
"allow_same_subsystem_tests": false,
"test_prefixes": [],
"exact_files": [
"frontend-modern/src/components/Dashboard/__tests__/Dashboard.performance.contract.test.tsx",
"frontend-modern/src/components/Dashboard/__tests__/DashboardFilter.test.tsx",
"frontend-modern/src/components/Dashboard/__tests__/dashboardSelectionModel.test.ts",
"frontend-modern/src/components/Dashboard/__tests__/dashboardWorkloadFilterConfigModel.test.ts",
"frontend-modern/src/components/Dashboard/__tests__/dashboardWorkloadRouteModel.test.ts",
"frontend-modern/src/components/Dashboard/__tests__/dashboardWorkloadRouteStateModel.test.ts",
"frontend-modern/src/components/Dashboard/__tests__/dashboardWorkloadUrlSyncModel.test.ts",
"frontend-modern/src/components/Dashboard/__tests__/DiskList.test.tsx",
"frontend-modern/src/components/Dashboard/__tests__/EnhancedCPUBar.test.tsx",
"frontend-modern/src/components/Dashboard/__tests__/GuestRow.test.tsx",
"frontend-modern/src/components/Dashboard/__tests__/StackedDiskBar.test.tsx",
"frontend-modern/src/components/Dashboard/__tests__/useDashboardFilterState.test.ts",
"frontend-modern/src/components/Dashboard/__tests__/useDashboardSelectionState.test.ts",
"frontend-modern/src/components/Dashboard/__tests__/useDashboardWorkloadViewportSync.test.tsx",
"frontend-modern/src/components/Dashboard/__tests__/useEnhancedCPUBarState.test.tsx",
"frontend-modern/src/components/Dashboard/__tests__/useGroupedTableWindowing.test.ts",
"frontend-modern/src/components/Dashboard/__tests__/useMetricBarState.test.tsx",
"frontend-modern/src/components/Dashboard/__tests__/useStackedDiskBarState.test.tsx",
"frontend-modern/src/components/Dashboard/__tests__/useStackedMemoryBarState.test.tsx",
"frontend-modern/src/components/Dashboard/__tests__/useThresholdSliderState.test.ts",
"frontend-modern/src/components/Dashboard/__tests__/workloadSelectors.test.ts",
"frontend-modern/src/components/Dashboard/__tests__/workloadTopology.test.ts",
"frontend-modern/src/components/Dashboard/GuestDrawer.test.tsx",
"frontend-modern/src/components/Dashboard/MetricBar.test.tsx",
"frontend-modern/src/components/Dashboard/StackedMemoryBar.test.tsx",
"frontend-modern/src/components/Dashboard/ThresholdSlider.test.tsx",
"frontend-modern/src/components/Infrastructure/__tests__/infrastructureSummaryModel.test.ts",
"frontend-modern/src/components/Infrastructure/__tests__/UnifiedResourceTable.performance.contract.test.tsx",
"frontend-modern/src/components/Infrastructure/__tests__/unifiedResourceTableStateModel.test.ts",
"frontend-modern/src/components/Workloads/__tests__/DiskList.test.tsx",
"frontend-modern/src/components/Workloads/__tests__/EnhancedCPUBar.test.tsx",
"frontend-modern/src/components/Workloads/__tests__/GuestRow.test.tsx",
"frontend-modern/src/components/Workloads/__tests__/StackedDiskBar.test.tsx",
"frontend-modern/src/components/Workloads/__tests__/useEnhancedCPUBarState.test.tsx",
"frontend-modern/src/components/Workloads/__tests__/useGroupedTableWindowing.test.ts",
"frontend-modern/src/components/Workloads/__tests__/useMetricBarState.test.tsx",
"frontend-modern/src/components/Workloads/__tests__/useStackedDiskBarState.test.tsx",
"frontend-modern/src/components/Workloads/__tests__/useStackedMemoryBarState.test.tsx",
"frontend-modern/src/components/Workloads/__tests__/useThresholdSliderState.test.ts",
"frontend-modern/src/components/Workloads/__tests__/useWorkloadSelectionState.test.ts",
"frontend-modern/src/components/Workloads/__tests__/useWorkloadsFilterState.test.ts",
"frontend-modern/src/components/Workloads/__tests__/useWorkloadViewportSync.test.tsx",
"frontend-modern/src/components/Workloads/__tests__/workloadFilterConfigModel.test.ts",
"frontend-modern/src/components/Workloads/__tests__/workloadRouteModel.test.ts",
"frontend-modern/src/components/Workloads/__tests__/workloadRouteStateModel.test.ts",
"frontend-modern/src/components/Workloads/__tests__/workloadSelectionModel.test.ts",
"frontend-modern/src/components/Workloads/__tests__/workloadSelectors.test.ts",
"frontend-modern/src/components/Workloads/__tests__/WorkloadsFilter.test.tsx",
"frontend-modern/src/components/Workloads/__tests__/WorkloadsSurface.performance.contract.test.tsx",
"frontend-modern/src/components/Workloads/__tests__/workloadTopology.test.ts",
"frontend-modern/src/components/Workloads/__tests__/workloadUrlSyncModel.test.ts",
"frontend-modern/src/components/Workloads/GuestDrawer.test.tsx",
"frontend-modern/src/components/Workloads/MetricBar.test.tsx",
"frontend-modern/src/components/Workloads/StackedMemoryBar.test.tsx",
"frontend-modern/src/components/Workloads/ThresholdSlider.test.tsx",
"frontend-modern/src/components/Workloads/WorkloadsSummary.test.tsx",
"frontend-modern/src/pages/__tests__/Workloads.helpers.test.ts",
"frontend-modern/src/utils/__tests__/thresholdSliderPresentation.test.ts"
@@ -4731,21 +4681,14 @@
"internal/recovery/"
],
"owned_files": [
"frontend-modern/src/components/Recovery/DashboardRecoveryStatusPanel.tsx",
"frontend-modern/src/components/Storage/DashboardStoragePanel.tsx",
"frontend-modern/src/features/dashboardOverview/dashboardWidgets.ts",
"frontend-modern/src/features/recovery/useRecoverySurfaceState.ts",
"frontend-modern/src/hooks/useDashboardRecovery.ts",
"frontend-modern/src/hooks/useRecoveryPoints.ts",
"frontend-modern/src/hooks/useRecoveryPointsFacets.ts",
"frontend-modern/src/hooks/useRecoveryPointsSeries.ts",
"frontend-modern/src/hooks/useRecoveryRollups.ts",
"frontend-modern/src/pages/Dashboard.tsx",
"frontend-modern/src/pages/Recovery.tsx",
"frontend-modern/src/pages/Storage.tsx",
"frontend-modern/src/types/recovery.ts",
"frontend-modern/src/utils/dashboardRecoveryPresentation.ts",
"frontend-modern/src/utils/dashboardStoragePresentation.ts",
"frontend-modern/src/utils/recoveryActionPresentation.ts",
"frontend-modern/src/utils/recoveryArtifactModePresentation.ts",
"frontend-modern/src/utils/recoveryDatePresentation.ts",
@@ -4769,7 +4712,6 @@
"frontend-modern/src/components/Recovery/RecoverySummary.test.tsx",
"frontend-modern/src/components/Storage/code_standards.test.ts",
"frontend-modern/src/pages/__tests__/Recovery.test.tsx",
"frontend-modern/src/utils/__tests__/dashboardRecoveryPresentation.test.ts",
"internal/recovery/recovery_test.go"
],
"require_explicit_path_policy_coverage": true,
@@ -4793,20 +4735,6 @@
"internal/recovery/store/store_test.go"
]
},
{
"id": "dashboard-storage-recovery-surface",
"label": "dashboard storage and recovery surface proof",
"match_prefixes": [],
"match_files": [
"frontend-modern/src/features/dashboardOverview/dashboardWidgets.ts",
"frontend-modern/src/pages/Dashboard.tsx"
],
"allow_same_subsystem_tests": false,
"test_prefixes": [],
"exact_files": [
"frontend-modern/src/pages/__tests__/DashboardPage.test.tsx"
]
},
{
"id": "recovery-product-surface",
"label": "recovery product surface proof",
@@ -4814,16 +4742,13 @@
"frontend-modern/src/components/Recovery/"
],
"match_files": [
"frontend-modern/src/components/Recovery/DashboardRecoveryStatusPanel.tsx",
"frontend-modern/src/features/recovery/useRecoverySurfaceState.ts",
"frontend-modern/src/hooks/useDashboardRecovery.ts",
"frontend-modern/src/hooks/useRecoveryPoints.ts",
"frontend-modern/src/hooks/useRecoveryPointsFacets.ts",
"frontend-modern/src/hooks/useRecoveryPointsSeries.ts",
"frontend-modern/src/hooks/useRecoveryRollups.ts",
"frontend-modern/src/pages/Recovery.tsx",
"frontend-modern/src/types/recovery.ts",
"frontend-modern/src/utils/dashboardRecoveryPresentation.ts",
"frontend-modern/src/utils/recoveryActionPresentation.ts",
"frontend-modern/src/utils/recoveryArtifactModePresentation.ts",
"frontend-modern/src/utils/recoveryDatePresentation.ts",
@@ -4845,7 +4770,6 @@
"frontend-modern/src/components/Recovery/__tests__/Recovery.test.tsx",
"frontend-modern/src/components/Recovery/RecoverySummary.test.tsx",
"frontend-modern/src/pages/__tests__/Recovery.test.tsx",
"frontend-modern/src/utils/__tests__/dashboardRecoveryPresentation.test.ts",
"frontend-modern/src/utils/__tests__/frontendResourceTypeBoundaries.test.ts",
"tests/integration/tests/17-recovery-layout.spec.ts"
]
@@ -4858,14 +4782,11 @@
"frontend-modern/src/features/storageBackups/"
],
"match_files": [
"frontend-modern/src/components/Storage/DashboardStoragePanel.tsx",
"frontend-modern/src/pages/Storage.tsx",
"frontend-modern/src/utils/dashboardStoragePresentation.ts"
"frontend-modern/src/pages/Storage.tsx"
],
"allow_same_subsystem_tests": false,
"test_prefixes": [],
"exact_files": [
"frontend-modern/src/components/Storage/__tests__/DashboardStoragePanel.test.tsx",
"frontend-modern/src/components/Storage/__tests__/DiskList.test.tsx",
"frontend-modern/src/components/Storage/__tests__/Storage.test.tsx",
"frontend-modern/src/components/Storage/__tests__/StorageControls.test.tsx",
@@ -4881,7 +4802,6 @@
"frontend-modern/src/features/storageBackups/__tests__/storagePagePresentation.test.ts",
"frontend-modern/src/features/storageBackups/__tests__/storagePoolsTablePresentation.test.ts",
"frontend-modern/src/pages/__tests__/Storage.helpers.test.ts",
"frontend-modern/src/utils/__tests__/dashboardStoragePresentation.test.ts",
"frontend-modern/src/utils/__tests__/frontendResourceTypeBoundaries.test.ts"
]
}
@@ -4935,7 +4855,6 @@
"frontend-modern/src/features/infrastructure/InfrastructurePageSurface.tsx",
"frontend-modern/src/features/infrastructure/useInfrastructurePageRouteState.ts",
"frontend-modern/src/features/infrastructure/useInfrastructurePageState.ts",
"frontend-modern/src/hooks/useDashboardTrends.ts",
"frontend-modern/src/hooks/useUnifiedResources.ts",
"frontend-modern/src/pages/Infrastructure.tsx",
"frontend-modern/src/routing/resourceLinks.ts",
@@ -5054,7 +4973,6 @@
"frontend-modern/src/features/infrastructure/InfrastructurePageSurface.tsx",
"frontend-modern/src/features/infrastructure/useInfrastructurePageRouteState.ts",
"frontend-modern/src/features/infrastructure/useInfrastructurePageState.ts",
"frontend-modern/src/hooks/useDashboardTrends.ts",
"frontend-modern/src/hooks/useUnifiedResources.ts",
"frontend-modern/src/pages/Infrastructure.tsx",
"frontend-modern/src/routing/resourceLinks.ts",
@@ -5080,7 +4998,6 @@
"frontend-modern/src/components/Infrastructure/__tests__/unifiedResourceTableStateModel.test.ts",
"frontend-modern/src/features/infrastructure/__tests__/infrastructurePageModel.test.ts",
"frontend-modern/src/features/infrastructure/__tests__/InfrastructurePageSurface.guardrails.test.ts",
"frontend-modern/src/hooks/__tests__/useDashboardTrends.test.ts",
"frontend-modern/src/hooks/__tests__/useUnifiedResources.test.ts",
"frontend-modern/src/pages/__tests__/Infrastructure.empty-state.test.tsx",
"frontend-modern/src/pages/__tests__/Infrastructure.pbs-pmg.test.tsx",
@@ -155,6 +155,11 @@ Security-facing settings remain intentionally shared with `frontend-primitives`
because shell framing and presentation consistency still belong there, but the
meaning of those surfaces now lives here so auth posture, token controls, and
privacy toggles stop borrowing their governance only from adjacent lanes.
That settings presentation boundary also owns trust-sensitive vocabulary around
operator access. `frontend-modern/src/components/Settings/GeneralSettingsPanel.tsx`
and `frontend-modern/src/components/Settings/apiTokenManagerModel.ts` must use
monitoring/workspace wording for tours and read-only token presets instead of
reviving Dashboard-specific labels after the Dashboard route has been retired.
The Data Handling settings surface extends that trust boundary to resource
policy posture. It may expose the canonical sensitivity, handling-boundary,
and redaction counts that Pulse already applies to resources, but it must stay
@@ -47,11 +47,7 @@ querying, and the operator-facing storage health presentation layer.
20. `frontend-modern/src/hooks/useRecoveryPointsSeries.ts`
21. `frontend-modern/src/pages/Recovery.tsx`
22. `frontend-modern/src/routing/resourceLinks.ts`
23. `frontend-modern/src/pages/Dashboard.tsx`
24. `frontend-modern/src/features/dashboardOverview/dashboardWidgets.ts`
25. `frontend-modern/src/components/Recovery/DashboardRecoveryStatusPanel.tsx`
26. `frontend-modern/src/components/Storage/DashboardStoragePanel.tsx`
27. `frontend-modern/src/types/recovery.ts`
23. `frontend-modern/src/types/recovery.ts`
28. `frontend-modern/src/utils/recoverySummaryPresentation.ts`
29. `frontend-modern/src/utils/recoveryTablePresentation.ts`
30. `frontend-modern/src/utils/recoveryItemTypePresentation.ts`
@@ -84,17 +80,11 @@ querying, and the operator-facing storage health presentation layer.
protected-inventory-only state so hidden filters cannot make valid history
look empty.
3. Add or change storage page UX through `frontend-modern/src/pages/Storage.tsx`, `frontend-modern/src/components/Storage/`, `frontend-modern/src/features/storageBackups/`, and the shared storage-source contract in `frontend-modern/src/utils/storageSources.ts`
Dashboard route composition may place storage and recovery widgets beside
the estate summary, but estate-summary copy must not present storage or
recovery health as resolved. Storage and recovery readiness claims remain
owned by `DashboardStoragePanel` and `DashboardRecoveryStatusPanel`; estate
summary anchors may focus the dashboard alerts or problem-resource sections
but must not redirect into storage or recovery ownership. Optional dashboard
Pulse Brief copy may summarize storage and recovery facts that are already on
the route, but it must not become the owner of storage capacity, storage
health, protected-item, or recovery-outcome readiness claims. The Assistant
handoff safety mode for that brief remains an AI runtime/API contract concern
and must not move storage or recovery readiness truth into model prose.
The retired dashboard route must not reintroduce storage or recovery
widgets as compatibility panels. Storage capacity, storage health,
protected-item, and recovery-outcome readiness claims belong on the Storage
and Recovery surfaces or their shared summary components, not in a restored
dashboard panel cluster or Assistant brief.
4. Route transport changes for storage and recovery endpoints through `internal/api/` and the owning `api-contracts` proof routes
That same adjacent API/security boundary owns CSRF replacement-token
concurrency for browser mutations. Storage and recovery forms may benefit
@@ -348,24 +338,13 @@ querying, and the operator-facing storage health presentation layer.
In mock mode, that same compact route must stay aggregate-only and
sampler-prewarmed; storage and recovery must not trigger per-pool chart
reconstruction on the first dashboard request after each mock refresh.
34. Keep storage and recovery websocket reads on the neutral app-runtime boundary. `frontend-modern/src/components/Recovery/RecoveryPointDetails.tsx`, `frontend-modern/src/components/Storage/useStoragePageResources.ts`, and storage/recovery-adjacent dashboard composition may consume live websocket state only through `frontend-modern/src/contexts/appRuntime.ts`, not by importing `frontend-modern/src/App.tsx` or rebuilding shell-local providers.
That same dashboard composition boundary also owns compact summary reads.
When `frontend-modern/src/pages/Dashboard.tsx` renders storage and
recovery-adjacent cards from `/api/resources/dashboard-summary` and
`/api/charts/storage-summary`, those cards may reuse the compact payloads
they were already given, but they must not reopen paginated
`useUnifiedResources()` transport or reintroduce per-pool
`/api/metrics-store/history` fan-out under the dashboard hot path.
Any route-owned dashboard additions composed above those cards must remain
adjacent composition only: they must not replace the recovery/storage
dashboard panels, suppress the governed no-resources handoff, or move
storage/recovery summary ownership out of the compact dashboard route.
Dashboard estate-orientation additions follow that same rule: they may
compose above the storage/recovery cards to prove Pulse sees the connected
estate, but they must consume `appRuntime` connected-infrastructure state
and the existing compact dashboard summary fallback rather than adding new
storage/recovery fetches or reclassifying storage/recovery widgets as
dashboard-owned summary panels.
34. Keep storage and recovery websocket reads on the neutral app-runtime boundary. `frontend-modern/src/components/Recovery/RecoveryPointDetails.tsx`, `frontend-modern/src/components/Storage/useStoragePageResources.ts`, and adjacent summary/detail composition may consume live websocket state only through `frontend-modern/src/contexts/appRuntime.ts`, not by importing `frontend-modern/src/App.tsx` or rebuilding shell-local providers.
That same dashboard composition boundary is now a retired negative space.
Storage and recovery-adjacent summaries may reuse
`/api/charts/storage-summary` through their owning summary components, but
they must not restore `/api/resources/dashboard-summary`, paginated
`useUnifiedResources()` transport, or per-pool `/api/metrics-store/history`
fan-out under a dashboard hot path.
35. Keep shared `frontend-modern/src/App.tsx` public-route ownership explicit by
surface. Storage/recovery preview entrypoints such as
`/preview/setup-complete` may remain public app-shell routes, but unrelated
@@ -387,7 +366,7 @@ querying, and the operator-facing storage health presentation layer.
plane boundary.
Authenticated `/login` must follow that same shared app-shell contract:
once login succeeds, `frontend-modern/src/App.tsx` must hand the browser
back to the governed dashboard landing route instead of leaving
back to the governed Infrastructure landing route instead of leaving
storage/recovery-adjacent authenticated shells on a page-local not-found
route.
Authenticated-shell demo organization suppression on `frontend-modern/src/App.tsx`
@@ -453,18 +432,17 @@ querying, and the operator-facing storage health presentation layer.
runtime as populated mock inventory, but they must not expose
`demo_fixtures`, billing identity, or alternate entitlement semantics as
recovery-local transport or operator-facing storage metadata.
37. Keep dashboard storage summary fetches scope-owned on the shared summary
caches. `frontend-modern/src/components/Storage/StorageSummary.tsx`,
37. Keep storage summary fetches scope-owned on the shared summary caches.
`frontend-modern/src/components/Storage/StorageSummary.tsx`,
`frontend-modern/src/utils/storageSummaryCache.ts`,
`frontend-modern/src/utils/storageSummaryTrendCache.ts`, and
`frontend-modern/src/pages/Dashboard.tsx` may reuse cached org/range
storage summaries for first paint, but they must not refetch the full
`/api/storage-charts` payload once per additional dashboard resource page
or invent a dashboard-only storage summary transport path outside the
and `frontend-modern/src/utils/storageSummaryTrendCache.ts` may reuse
cached org/range storage summaries for first paint, but they must not
refetch the full `/api/storage-charts` payload once per adjacent summary
card or invent a dashboard-only storage summary transport path outside the
canonical cache owners.
38. Keep storage and recovery route framing additive and owner-neutral.
`frontend-modern/src/components/Storage/Storage.tsx` and storage/recovery-
adjacent dashboard composition may use the shared `PageHeader` shell for
adjacent route composition may use the shared `PageHeader` shell for
top-level route framing, but that header must stay additive on top of the
canonical storage page model, recovery presenters, and shared summary
caches. Header chrome must not become a second owner for storage filters,
@@ -528,19 +506,14 @@ querying, and the operator-facing storage health presentation layer.
widening it into a broad short-name collapse across distinct FQDNs.
5. Keep recovery history table width budgeting derived from the canonical column specs in `frontend-modern/src/utils/recoveryTablePresentation.ts`, not from raw visible-column counts, so normalized subject labels and optional column sets cannot drift the right-edge badges and controls off-screen
6. Keep at least one browser-level desktop recovery proof in the governed `recovery-product-surface` policy so right-edge column visibility and wrapper-fit regressions are caught at rendered layout time instead of only through unit-level width math
7. Keep the dashboard route's no-resources state on an explicit first-session
handoff. `frontend-modern/src/pages/Dashboard.tsx` may stay storage/recovery-
owned for dashboard composition, but when no resources have reported yet it
must route operators to `/settings/infrastructure/install` instead of
leaving the dashboard as a passive dead end.
8. Keep dashboard storage/recovery composition on the shared all-resources
snapshot. `frontend-modern/src/pages/Dashboard.tsx` may stay
storage/recovery-owned for dashboard widget composition, but it must reuse
the canonical `all-resources` cache key from
`frontend-modern/src/hooks/useUnifiedResources.ts` so storage/recovery
widgets do not fork a dashboard-only resource snapshot and force adjacent
infrastructure handoffs back through transient loading shells once the
broader dashboard data is already warm.
7. Keep the retired dashboard route from becoming a passive no-resources
compatibility shell. First-session handoff now belongs to Infrastructure
and Add infrastructure, and storage/recovery must not restore dashboard
composition just to route operators to setup.
8. Keep storage/recovery summaries on their owning snapshots after dashboard
retirement. They may reuse the canonical `all-resources` cache key from
`frontend-modern/src/hooks/useUnifiedResources.ts` only through their owned
page or shared drawer surfaces, not as a dashboard-only resource snapshot.
9. Keep shared OIDC/SAML callback redirects on the canonical local-target
helper contract when storage- or recovery-adjacent routes inherit shared
auth browser handoff through `internal/api/`, so adjacent surfaces do not
@@ -1520,42 +1493,13 @@ boundary: raw compatibility fields such as `provider` / `providers` may be
accepted from older `/api/recovery/*` payloads, but the runtime values they
return to the rest of the recovery UI must be canonical `platform` /
`platforms` models.
That same rule applies to the dashboard recovery entry points too:
`frontend-modern/src/hooks/useDashboardRecovery.ts`,
`frontend-modern/src/pages/Dashboard.tsx`,
`frontend-modern/src/features/dashboardOverview/dashboardWidgets.ts`,
`frontend-modern/src/components/Recovery/DashboardRecoveryStatusPanel.tsx`, and
`frontend-modern/src/utils/dashboardRecoveryPresentation.ts` must stay on
explicit direct dashboard/recovery proof routing instead of inheriting
coverage only through the full recovery route or broader dashboard shells.
The storage dashboard entry point must be treated the same way on the storage
side: `frontend-modern/src/pages/Dashboard.tsx`,
`frontend-modern/src/features/dashboardOverview/dashboardWidgets.ts`, and
`frontend-modern/src/components/Storage/DashboardStoragePanel.tsx` and
`frontend-modern/src/utils/dashboardStoragePresentation.ts` must stay on
explicit direct dashboard/storage proof routing instead of borrowing release-
control coverage only from the broader storage page, component, and model
surfaces.
That same dashboard storage surface must also render its capacity meter through
the shared `frontend-modern/src/components/shared/ProgressBar.tsx` primitive
and attribute-driven fill geometry instead of owner-local inline width styles,
so the live dashboard remains CSP-safe while still reporting the canonical
storage summary.
That route shell now also composes the recent-alerts widget directly from the
alert-owned `frontend-modern/src/components/Alerts/RecentAlertsPanel.tsx`
surface instead of via a dashboard-panels-local alert implementation, so the
dashboard route stays storage/recovery-owned while alert widget runtime remains
owned by the alerts subsystem. That route handoff must stay thin: the dashboard
page should pass the live alert list into `RecentAlertsPanel` and let the
alert-owned surface derive its own summary and acknowledgement state instead of
rebuilding alert summary counts or alert-action runtime inside the
storage/recovery-governed dashboard route.
That same route shell also owns the top-of-dashboard scan order. When
`frontend-modern/src/pages/Dashboard.tsx` renders both the KPI strip and the
problem-resources table, the route must keep the operator snapshot above the
detail table so the page reads as action queue, summary, then problem detail
rather than dropping operators straight into a long exception list before the
dashboard baseline is visible.
The retired dashboard recovery and storage entry points must stay removed:
`useDashboardRecovery`, `DashboardRecoveryStatusPanel`,
`DashboardStoragePanel`, dashboard storage/recovery presentation helpers, and
dashboard widget orchestration must not return as direct proof surfaces. New
storage or recovery summary proof must attach to the owning Storage,
Recovery, Infrastructure drawer, or shared summary component instead of
borrowing coverage through a broader dashboard shell.
The shared recovery type contract must be pinned the same way:
`frontend-modern/src/types/recovery.ts` must stay on the explicit
`recovery-product-surface` proof path instead of riding indirectly on route or
@@ -2624,13 +2568,12 @@ collector, agent-id/device fallback stream, or separate "real-time" history
store once monitoring and `/api/metrics-store/history` already own the disk
timeline.
The same shell/runtime split now applies to websocket consumers:
`frontend-modern/src/components/Recovery/RecoveryPointDetails.tsx`,
`frontend-modern/src/components/Storage/useStoragePageResources.ts`, and
`frontend-modern/src/pages/Dashboard.tsx` may consume websocket state only
through `frontend-modern/src/contexts/appRuntime.ts`. They must not import
`@/App` or create storage/recovery-local shell coupling, because provider
placement remains app-shell-owned and storage/recovery surfaces must stay
lazy-load safe.
`frontend-modern/src/components/Recovery/RecoveryPointDetails.tsx` and
`frontend-modern/src/components/Storage/useStoragePageResources.ts` may consume
websocket state only through `frontend-modern/src/contexts/appRuntime.ts`.
They must not import `@/App` or create storage/recovery-local shell coupling,
because provider placement remains app-shell-owned and storage/recovery
surfaces must stay lazy-load safe.
That same adjacent `internal/api/` boundary now also governs public-demo
commercial redaction for storage and recovery viewers. Shared storage/recovery
surfaces may run beside a demo runtime that has real internal entitlements,
@@ -100,8 +100,7 @@ cross-source deduplication.
74. `frontend-modern/src/components/PMG/ServiceHealthBadge.tsx`
75. `frontend-modern/src/utils/resourceIdentity.ts`
76. `frontend-modern/src/components/Infrastructure/resourceDetailDrawerIdentityModel.ts`
77. `frontend-modern/src/hooks/useDashboardTrends.ts`
78. `frontend-modern/src/hooks/useUnifiedResources.ts`
77. `frontend-modern/src/hooks/useUnifiedResources.ts`
79. `frontend-modern/src/types/resource.ts`
80. `frontend-modern/src/utils/sourcePlatforms.ts`
81. `frontend-modern/src/utils/platformSupportManifest.generated.ts`
@@ -228,7 +227,7 @@ AI-only summary payloads, or page-local heuristics.
canonicalization boundary: REST may hydrate the initial snapshot and
unsupported filtered queries, but supported snapshot freshness must come
from websocket `state.resources` instead of layering confirmatory
dashboard/infrastructure REST refetch loops over already-owned resource
route-local REST refetch loops over already-owned resource
updates.
That shared store/adapter/hook path must also preserve canonical row shape
across transport boundaries: thinner realtime `state.resources` payloads
@@ -243,20 +242,19 @@ AI-only summary payloads, or page-local heuristics.
explicit cluster identity such as Kubernetes context or platform cluster
labels; standalone resource names must never be repurposed as synthetic
`clusterId` values.
10. Keep the dashboard overview shell on the compact governed summary route
rather than the unfiltered list transport. `frontend-modern/src/pages/Dashboard.tsx`
and `frontend-modern/src/hooks/useDashboardOverview.ts` may consume the
canonical `/api/resources/dashboard-summary` payload for KPI cards,
problem-resource rows, and top-resource identity, but they must not
recreate those summaries by mounting `useUnifiedResources()` just to count
or rank resources on the dashboard shell.
10. Keep the retired dashboard overview shell absent from unified-resource
consumers. `frontend-modern/src/pages/Dashboard.tsx`,
`frontend-modern/src/hooks/useDashboardOverview.ts`, and
`/api/resources/dashboard-summary` must not be restored as compatibility
readers for KPI cards, problem-resource rows, or top-resource identity.
Infrastructure and Workloads must consume their owning unified-resource
projections directly.
11. Keep summary consumers on the payload they were already given.
`frontend-modern/src/hooks/useDashboardTrends.ts` and
`frontend-modern/src/components/Infrastructure/useInfrastructureSummaryState.ts`
may derive chart identity, storage presence, and infrastructure rollups
from the compact dashboard overview or resource snapshot they already own,
but they must not reopen `useResources()` or start a second unfiltered
unified-resource fetch path under the dashboard summary surface.
from the resource snapshot it already owns, but it must not reopen
`useResources()` or start a second unfiltered unified-resource fetch path
as a replacement dashboard summary surface.
12. Keep shared selector hydration visibility-bound. Reusable shells such as
`frontend-modern/src/components/shared/useInfrastructureSelectorState.ts`
may consume `frontend-modern/src/hooks/useUnifiedResources.ts`, but hidden
@@ -420,13 +418,13 @@ AI-only summary payloads, or page-local heuristics.
through the canonical frontend-to-`ResourceType` resolver before slicing
the snapshot, so compatibility values such as `disk` / `physical_disk`
stay on one cache truth instead of falling back to ad hoc filter aliases.
18. Keep dashboard storage trend target selection on canonical unified-resource
truth. `frontend-modern/src/hooks/useDashboardTrends.ts` may detect
storage presence from canonical `isStorage(...)` resources and their
shared metrics-target IDs, but once storage exists it must reuse the owned
compact `/api/charts/storage-summary` contract instead of rebuilding
page-local per-resource storage history fetches, storage-type aliases, or
full storage-page `/api/storage-charts` fetches.
18. Keep storage summary target selection on canonical unified-resource truth.
Storage-summary consumers may detect storage presence from canonical
`isStorage(...)` resources and their shared metrics-target IDs, but once
storage exists they must reuse the owned compact
`/api/charts/storage-summary` contract instead of rebuilding page-local
per-resource storage history fetches, storage-type aliases, or full
storage-page `/api/storage-charts` fetches.
19. Keep infrastructure page-header framing presentation-only on the page
shell. `frontend-modern/src/features/infrastructure/InfrastructurePageSurface.tsx`
may render the shared `PageHeader`, but canonical source/status/search
@@ -584,8 +582,8 @@ Seeded unified-resource snapshots are part of that same registry ownership.
`internal/unifiedresources/registry.go` must rebuild the canonical `bySource`
index from source-owned facets already present on seeded unified resources
before manual links, overlays, or metrics-target resolution run, so
`/api/resources/dashboard-summary`, `/api/resources`, dashboard trend joins,
and other chart consumers keep resolving history through canonical source
`/api/resources`, infrastructure summary joins, storage summary joins, and
other chart consumers keep resolving history through canonical source
coordinates instead of silently falling back to hashed unified resource IDs.
That same registry/view boundary now also applies to provider-backed storage.
`internal/unifiedresources/registry.go` must attach the resolved
@@ -836,20 +834,14 @@ shape: `InfrastructureSummary.tsx` is the render shell,
org-scope lifecycle, and focused-summary state, and
`infrastructureSummaryModel.ts` owns chart matching, focused-summary display
selection, empty-state wording, and summary-series/metric derivation.
The dashboard overview trend hook now follows that same canonical consumer
contract for infrastructure sparklines: `frontend-modern/src/hooks/useDashboardTrends.ts`
must consume the infrastructure summary chart cache and shared unified-resource
series matching logic instead of issuing bespoke per-resource
`/api/metrics-store/history` fetches for top-CPU and top-memory cards. That
keeps dashboard summary sparklines aligned with canonical resource identity
matching, agent-facet fallback behavior, and first-sample empty-state semantics
already owned by the infrastructure summary surface.
The same contract now also treats dashboard top-card sparkline hydration as a
summary projection concern, not a per-resource history concern: top-CPU and
top-memory selections may still come from dashboard overview ranking, but the
series attached to those selections must be resolved through the same
resource-to-chart matching path used by infrastructure summary cards so agent
fallback and canonical identity aliases do not drift between the two surfaces.
The retired dashboard overview trend hook must not return as a second
infrastructure sparkline consumer. Infrastructure summary surfaces must consume
the infrastructure summary chart cache and shared unified-resource series
matching logic instead of issuing bespoke per-resource
`/api/metrics-store/history` fetches for overview cards. That keeps summary
sparklines aligned with canonical resource identity matching, agent-facet
fallback behavior, and first-sample empty-state semantics already owned by the
infrastructure summary surface.
The backend AI and Patrol context renderers now derive their canonical change
kind, source type, source adapter, actor, reason, and related-resource
fragments from `internal/unifiedresources/change_presentation.go`, so the
@@ -943,7 +935,7 @@ card, so canonical timeline wording and ordering stay governed by one
frontend feed instead of separate page-local loops.
The unified-resource detail drawer now also routes resource-tag presentation
through the shared `frontend-modern/src/components/shared/TagBadges.tsx`
primitive instead of importing a dashboard-local badge helper into the
primitive instead of importing a workload-local badge helper into the
resource surface. Future drawer tag presentation changes must extend through
that shared primitive boundary rather than recreating tag-dot logic inside the
drawer or pulling feature-local badge components across subsystem lines.
@@ -1033,9 +1025,9 @@ drawer, which keeps the presentation surface aligned with the governed API
contract instead of rebuilding the relationship and timeline inline.
The canonical routing owner now also lives in
`frontend-modern/src/routing/resourceLinks.ts`, including the
workload-to-infrastructure href builder used by dashboard row and drawer
workload-to-infrastructure href builder used by Workloads row and drawer
consumers. Future workload-to-resource navigation changes must extend through
that shared routing contract instead of reintroducing dashboard-local path
that shared routing contract instead of reintroducing workload-local path
builders.
That same shared routing boundary now also owns the canonical Patrol
destination used by cross-surface findings and drill-down links. Shared
@@ -1418,15 +1410,15 @@ mock-history paths, so cluster, node, pod, and deployment charts all bind to
one runtime key family instead of each layer rebuilding Kubernetes IDs from
cluster name, namespace, or surface-local aliases.
The drawer's discovery mapper also reuses that helper for pod fallback agent
IDs, so the resource-detail path and the dashboard path stay aligned on the
IDs, so the resource-detail path and the Workloads path stay aligned on the
same cluster-name source of truth.
The dashboard workload projection and workloads-link route helpers also share
The workload projection and workloads-link route helpers also share
the same Kubernetes context prefix helper in the shared agent-resource
layer, so pod grouping and cluster navigation keep the same cluster-context
prefix before any surface-specific display fallback is applied.
The unified-resource projection also reuses that same prefix helper for
projected Kubernetes `clusterId`, so the shared resource store stays aligned
with the dashboard and detail surfaces on the same canonical cluster-context
with the Workloads and detail surfaces on the same canonical cluster-context
source of truth.
That same contract also owns the canonical resource display-name fallback, so
name-or-ID presentation stays consistent between the unified AI adapter, the
@@ -1796,8 +1788,8 @@ Docker-routed control target.
That same contract applies to the frontend workload state boundary:
`frontend-modern/src/hooks/useWorkloads.ts`,
`frontend-modern/src/utils/workloads.ts`,
`frontend-modern/src/components/Dashboard/workloadTopology.ts`, and
`frontend-modern/src/components/Dashboard/useGuestRowState.ts` may reuse
`frontend-modern/src/components/Workloads/workloadTopology.ts`, and
`frontend-modern/src/components/Workloads/useGuestRowState.ts` may reuse
shared `DockerData` for runtime metadata, but they must keep Docker-only
action identifiers and update affordances scoped to Docker-managed
app-containers. TrueNAS-backed `app-container` workloads must still navigate
@@ -61,10 +61,8 @@ const ALLOWLIST = new Set([
'src/utils/agentCapabilityPresentation.ts',
'src/utils/unifiedAgentInventoryPresentation.ts',
'src/utils/unifiedAgentStatusPresentation.ts',
'src/utils/dashboardAlertPresentation.ts',
'src/utils/dashboardEmptyStatePresentation.ts',
'src/utils/dashboardStoragePresentation.ts',
'src/utils/dashboardRecoveryPresentation.ts',
'src/utils/alertOverviewPresentation.ts',
'src/utils/workloadEmptyStatePresentation.ts',
'src/utils/temperature.ts',
'src/utils/licensePresentation.ts',
'src/utils/k8sStatusPresentation.ts',
@@ -142,10 +140,7 @@ const ALLOWLIST = new Set([
'src/utils/k8sDeploymentPresentation.ts',
'src/utils/k8sNamespacePresentation.ts',
'src/components/shared/EnvironmentLockBadge.tsx',
'src/utils/dashboardCompositionPresentation.ts',
'src/utils/dashboardGuestPresentation.ts',
'src/utils/dashboardMetricPresentation.ts',
'src/utils/dashboardTrendPresentation.ts',
'src/utils/workloadGuestPresentation.ts',
'src/utils/deployFlowPresentation.ts',
'src/utils/infrastructureEmptyStatePresentation.ts',
]);
@@ -335,11 +330,11 @@ const HELPER_RULES = [
'Do not define local infrastructure empty-state copy in page code. Use @/utils/infrastructureEmptyStatePresentation instead.',
},
{
rule: 'canonical-dashboard/no-local-dashboard-empty-state-copy',
rule: 'canonical-workloads/no-local-workload-empty-state-copy',
regex:
/No infrastructure hosts connected|No guests found|Install the Pulse agent to connect a host and unlock v6 infrastructure data, or add a Proxmox connection in Settings → Infrastructure → Proxmox\.|No guests match your current filters|No guests match your search |Loading dashboard data\.\.\.|Connecting to monitoring service|Reconnecting to monitoring service…|Real-time data is currently unavailable\. Showing last-known state\.|Real-time data is reconnecting\. Showing last-known state\.|Dashboard unavailable|Real-time dashboard data is currently unavailable\. Reconnect to try again\.|No resources yet|Once connected platforms report resources, your dashboard overview will appear here\./g,
message:
'Do not define local dashboard empty-state copy in component code. Use @/utils/dashboardEmptyStatePresentation instead.',
'Do not define local Workloads empty-state copy in component code. Use @/utils/workloadEmptyStatePresentation instead.',
},
{
rule: 'canonical-shared/no-local-empty-state-tone-maps',
@@ -530,16 +525,16 @@ const HELPER_RULES = [
'Do not define local recovery chart range selected-button classes in page code. Use the shared segmented button contract instead.',
},
{
rule: 'canonical-dashboard/no-local-trend-range-selected-classes',
rule: 'canonical-overview/no-local-trend-range-selected-classes',
regex: /\bselectedRange\(\)\s*===\s*range\b[\s\S]{0,260}bg-blue-600\s+text-white/g,
message:
'Do not define local dashboard trend range selected-button classes in page code. Use the shared segmented button contract instead.',
'Do not define local overview trend range selected-button classes in page code. Use the shared segmented button contract instead.',
},
{
rule: 'canonical-dashboard/no-local-trend-error-copy',
rule: 'canonical-overview/no-local-trend-error-copy',
regex: /Unable to load trends/g,
message:
'Do not define local dashboard trend error copy in panel code. Use @/utils/dashboardTrendPresentation instead.',
'Do not define local overview trend error copy in panel code. Use the governed route presentation helper instead.',
},
{
rule: 'canonical-ai/no-local-findings-filter-selected-classes',
@@ -644,30 +639,30 @@ const HELPER_RULES = [
'Do not define local unified-agent lookup status helpers in component code. Use @/utils/unifiedAgentStatusPresentation instead.',
},
{
rule: 'canonical-dashboard/no-local-problem-status-variant',
rule: 'canonical-overview/no-local-problem-status-variant',
regex: /\b(?:const|function)\s+statusVariant\s*\(\s*pr\s*:\s*ProblemResource\s*\)/g,
message:
'Do not define local dashboard problem-resource status helpers in page code. Use @/utils/problemResourcePresentation instead.',
'Do not define local problem-resource status helpers in page code. Use @/utils/problemResourcePresentation instead.',
},
{
rule: 'canonical-dashboard/no-local-alerts-tone',
rule: 'canonical-alerts/no-local-alerts-tone',
regex: /\b(?:const|function)\s+alertsTone\b/g,
message:
'Do not define local dashboard alert summary tone helpers in page code. Use @/utils/dashboardAlertPresentation instead.',
'Do not define local alert summary tone helpers in page code. Use @/utils/alertOverviewPresentation instead.',
},
{
rule: 'canonical-dashboard/no-local-storage-recovery-or-alert-copy',
rule: 'canonical-overview/no-local-storage-recovery-or-alert-copy',
regex:
/No active alerts|No storage resources|No recovery data available|Last recovery point over 24 hours ago/g,
message:
'Do not define local dashboard storage, recovery, or alert empty-state/staleness copy in page code. Use the shared dashboard presentation utilities instead.',
'Do not define local storage, recovery, or alert empty-state/staleness copy in page code. Use the owning shared presentation utilities instead.',
},
{
rule: 'canonical-dashboard/no-local-composition-or-ai-empty-state-copy',
rule: 'canonical-overview/no-local-composition-or-ai-empty-state-copy',
regex:
/No resources detected|Loading usage…|No usage data yet\.|No daily USD trend yet\.|No daily token trend yet\.|No issues found/g,
message:
'Do not define local dashboard or AI empty-state copy in component code. Use the shared presentation utilities instead.',
'Do not define local overview or AI empty-state copy in component code. Use the shared presentation utilities instead.',
},
{
rule: 'canonical-ai/no-local-chat-empty-state-copy',
@@ -789,7 +784,7 @@ const HELPER_RULES = [
'Do not define local ResourcePicker filter labels or hardcoded filter lists in component code. Use @/utils/reportableResourceTypes instead.',
},
{
rule: 'canonical-dashboard/no-local-threshold-slider-color-map',
rule: 'canonical-shared/no-local-threshold-slider-color-map',
regex:
/\bconst\s+colorMap\s*:\s*Record<ThresholdSliderProps\['type'\],\s*string>\s*=\s*\{[\s\S]{0,220}cpu:[\s\S]{0,220}memory:[\s\S]{0,220}disk:[\s\S]{0,220}temperature:/g,
message:
@@ -1027,24 +1022,24 @@ const HELPER_RULES = [
'Do not define local recovery timeline chart presentation in component code. Use @/utils/recoveryTimelineChartPresentation instead.',
},
{
rule: 'canonical-dashboard/no-local-dashboard-helper-implementations',
rule: 'canonical-overview/no-local-helper-implementations',
regex:
/\bexport function statusBadgeClass\b|\bexport function priorityBadgeClass\b|\bexport function deltaColorClass\b/g,
message:
'Do not define dashboard metric presentation functions in page-local helpers. Use @/utils/dashboardMetricPresentation instead.',
'Do not define metric presentation functions in page-local helpers. Use the governed route presentation helper instead.',
},
{
rule: 'canonical-dashboard/no-local-composition-icon-map',
rule: 'canonical-overview/no-local-composition-icon-map',
regex: /\bconst\s+TYPE_ICONS\s*:\s*Record<string,\s*any>\s*=/g,
message:
'Do not define local dashboard composition icon maps in page code. Use @/utils/dashboardCompositionPresentation instead.',
'Do not define local composition icon maps in page code. Use the governed route presentation helper instead.',
},
{
rule: 'canonical-dashboard/no-local-guest-backup-status-map',
rule: 'canonical-workloads/no-local-guest-backup-status-map',
regex:
/\bconst\s+BACKUP_STATUS_CONFIG\s*:\s*Record<|No backup found|No IP assigned|No filesystems found\. VM may be booting or using a Live ISO\./g,
message:
'Do not define local dashboard guest fallback copy or backup status maps in component code. Use @/utils/dashboardGuestPresentation instead.',
'Do not define local Workloads guest fallback copy or backup status maps in component code. Use @/utils/workloadGuestPresentation instead.',
},
{
rule: 'canonical-infrastructure/no-local-deploy-flow-copy',
-4
View File
@@ -23,7 +23,6 @@ import { aiChatStore } from './stores/aiChat';
import { useKeyboardShortcuts } from './hooks/useKeyboardShortcuts';
import { useKioskMode } from '@/hooks/useKioskMode';
import {
DASHBOARD_PATH,
PATROL_PATH,
buildRecoveryPath,
buildInfrastructurePath,
@@ -54,7 +53,6 @@ const AlertsPage = lazy(() =>
);
const SettingsPage = lazy(() => import('./components/Settings/Settings'));
const InfrastructurePage = lazy(() => import('./pages/Infrastructure'));
const DashboardPage = lazy(() => import('./pages/Dashboard'));
const WorkloadsPage = lazy(() => import('./pages/Workloads'));
const AIIntelligencePage = lazy(() =>
import('./pages/AIIntelligence').then((module) => ({ default: module.AIIntelligence })),
@@ -67,7 +65,6 @@ const SetupCompletionPreviewPage = lazy(() =>
default: module.SetupCompletionPreview,
})),
);
const ROOT_DASHBOARD_PATH = DASHBOARD_PATH;
const INFRASTRUCTURE_ROUTE_PATH = buildInfrastructurePath();
const ROOT_WORKLOADS_PATH = buildWorkloadsPath();
const STORAGE_PATH = buildStoragePath();
@@ -455,7 +452,6 @@ function App() {
<Router root={RootLayout}>
<Route path="/pricing" component={PricingHandoffPage} />
<Route path="/preview/setup-complete" component={SetupCompletionPreviewPage} />
<Route path={ROOT_DASHBOARD_PATH} component={DashboardPage} />
<Route path="/login" component={RuntimeHomePage} />
<Route path="/" component={RuntimeHomePage} />
<Route path={ROOT_WORKLOADS_PATH} component={WorkloadsPage} />
+2 -15
View File
@@ -2,7 +2,6 @@ import { For, Show, Suspense, createEffect, createMemo, createSignal, onCleanup
import type { JSX } from 'solid-js';
import { useLocation, useNavigate } from '@solidjs/router';
import BoxesIcon from 'lucide-solid/icons/boxes';
import LayoutDashboardIcon from 'lucide-solid/icons/layout-dashboard';
import ServerIcon from 'lucide-solid/icons/server';
import HardDriveIcon from 'lucide-solid/icons/hard-drive';
import ArchiveIcon from 'lucide-solid/icons/archive';
@@ -31,7 +30,6 @@ import { preloadRouteModule } from '@/routing/routePreload';
import {
buildInfrastructurePath,
buildWorkloadsPath,
DASHBOARD_PATH,
} from '@/routing/resourceLinks';
import { buildStorageRecoveryTabSpecs } from '@/routing/platformTabs';
import { getKioskModePreference, setKioskMode } from '@/utils/url';
@@ -229,8 +227,8 @@ export function AppLayout(props: AppLayoutProps) {
!normalizedPath.startsWith('/alerts/overview/') &&
!normalizedPath.startsWith('/alerts/history/');
if ((isBlocked || isAlertConfigTab) && normalizedPath !== DASHBOARD_PATH) {
navigate(DASHBOARD_PATH, { replace: true });
if ((isBlocked || isAlertConfigTab) && normalizedPath !== ROOT_INFRASTRUCTURE_PATH) {
navigate(ROOT_INFRASTRUCTURE_PATH, { replace: true });
}
});
@@ -268,17 +266,6 @@ export function AppLayout(props: AppLayoutProps) {
const platformTabs = createMemo<PlatformTab[]>(() => {
const allPlatforms: PlatformTab[] = [
{
id: 'dashboard',
label: 'Dashboard',
route: DASHBOARD_PATH,
settingsRoute: '/settings',
tooltip: 'Environment overview and command center',
enabled: true,
live: true,
icon: LayoutDashboardIcon,
alwaysShow: true,
},
{
id: 'infrastructure',
label: 'Infrastructure',
@@ -11,7 +11,6 @@ const appStylesSource = readFileSync(join(process.cwd(), 'src/index.css'), 'utf8
describe('App architecture', () => {
it('keeps App as the entry shell that delegates runtime and chrome ownership', () => {
expect(appSource).toContain('DASHBOARD_PATH,');
expect(appSource).toContain("import { AppLayout } from '@/AppLayout';");
expect(appSource).toContain("import { aiChatStore } from './stores/aiChat';");
expect(appSource).toContain(
@@ -28,7 +27,7 @@ describe('App architecture', () => {
expect(appSource).toContain('setAppScrollShellRef');
expect(appSource).toContain('readPendingAppShellRestoreTop');
expect(appSource).toContain('clearPendingAppShellRestoreTop');
expect(appSource).toContain('const ROOT_DASHBOARD_PATH = DASHBOARD_PATH;');
expect(appSource).toContain('const INFRASTRUCTURE_ROUTE_PATH = buildInfrastructurePath();');
expect(appSource).toContain('const ROOT_PATROL_PATH = PATROL_PATH;');
expect(appSource).toContain("import { preloadRouteModule } from '@/routing/routePreload';");
expect(appSource).toContain('const APP_SHELL_ROUTE_PRELOAD_PATHS = [');
@@ -37,7 +36,10 @@ describe('App architecture', () => {
expect(appSource).toContain('await preloadRouteModule(route);');
expect(appSource).toContain('const timeoutId = window.setTimeout(() => {');
expect(appSource).toContain('void preloadAppShellRoutes();');
expect(appSource).toContain('<Route path={ROOT_DASHBOARD_PATH} component={DashboardPage} />');
expect(appSource).toContain(
'<Route path={INFRASTRUCTURE_ROUTE_PATH} component={InfrastructurePage} />',
);
expect(appSource).not.toContain('DashboardPage');
expect(appSource).toContain("import RuntimeHomePage from '@/pages/RuntimeHome';");
expect(appSource).toContain('<Route path="/login" component={RuntimeHomePage} />');
expect(appSource).toContain('<Route path="/" component={RuntimeHomePage} />');
@@ -67,7 +69,7 @@ describe('App architecture', () => {
"const StorageComponent = lazy(() => import('./components/Storage/Storage'));",
);
expect(appSource).not.toContain(
"const WorkloadsView = lazy(() => import('./components/Dashboard/Dashboard'));",
"const WorkloadsView = lazy(() => import('./components/Workloads/WorkloadsSurface'));",
);
expect(appSource).not.toContain(
"const RecoveryRoute = lazy(() => import('./pages/RecoveryRoute'));",
@@ -244,8 +246,10 @@ describe('App architecture', () => {
expect(appRuntimeStateSource).toContain(
"eventBus.on('websocket_reconnected', handleWebSocketReconnected);",
);
expect(appRuntimeStateSource).toContain("const ROOT_DASHBOARD_PATH = '/dashboard';");
expect(appRuntimeStateSource).toContain('if (pathname === ROOT_DASHBOARD_PATH) return false;');
expect(appRuntimeStateSource).toContain(
'const ROOT_INFRASTRUCTURE_PATH = buildInfrastructurePath();',
);
expect(appRuntimeStateSource).not.toContain("const ROOT_DASHBOARD_PATH = '/dashboard';");
expect(appRuntimeStateSource).not.toContain(
"import { startMetricsCollector } from '@/stores/metricsCollector';",
);
@@ -257,9 +261,10 @@ describe('App architecture', () => {
expect(routePreloadSource).toContain(
'const routePreloadCache = new Map<string, Promise<void>>();',
);
expect(routePreloadSource).toContain("import('@/pages/Infrastructure')");
expect(routePreloadSource).toContain("import('@/pages/Workloads')");
expect(routePreloadSource).toContain("import('@/pages/Recovery')");
expect(routePreloadSource).not.toContain("import('@/components/Dashboard/Dashboard')");
expect(routePreloadSource).not.toContain("import('@/components/Workloads/WorkloadsSurface')");
expect(routePreloadSource).not.toContain("import('@/pages/RecoveryRoute')");
expect(appRuntimeContextSource).toContain(
"import { createContext, useContext } from 'solid-js';",
@@ -9,7 +9,7 @@ HTMLElement.prototype.scrollIntoView = vi.fn();
describe('AppLayout navigation icons', () => {
beforeEach(() => {
window.history.replaceState({}, '', '/dashboard');
window.history.replaceState({}, '', '/infrastructure');
aiChatStore.close();
aiChatStore.setEnabled(true);
});
@@ -24,7 +24,7 @@ describe('AppLayout navigation icons', () => {
render(() => (
<Router>
<Route
path="/dashboard"
path="/infrastructure"
component={() => (
<AppLayout
connectionStatus={() => ({
@@ -58,7 +58,7 @@ describe('AppLayout navigation icons', () => {
showOrgSwitcher={() => false}
onSwitchOrg={() => {}}
>
<div>Dashboard body</div>
<div>Infrastructure body</div>
</AppLayout>
)}
/>
@@ -96,7 +96,7 @@ describe('AppLayout navigation icons', () => {
const mobilePatrolTab = within(mobileTablist).getByRole('button', { name: 'Patrol' });
expect(mobilePatrolTab.querySelector('svg')).toBeTruthy();
expect(container).toHaveTextContent('Dashboard body');
expect(container).toHaveTextContent('Infrastructure body');
});
it('keeps connected brand motion on the logo while the wordmark stays static', () => {
@@ -11,54 +11,6 @@ describe('ResourceAPI', () => {
vi.clearAllMocks();
});
it('fetches the compact dashboard summary payload from the dashboard-summary endpoint', async () => {
vi.mocked(apiFetchJSON).mockResolvedValueOnce({
health: { totalResources: 4, byStatus: { online: 3, degraded: 1 } },
infrastructure: {
total: 2,
byStatus: { online: 2 },
byType: { agent: 1, 'docker-host': 1 },
topCPU: [
{
id: 'infra-a',
name: 'Infra A',
percent: 91,
metricsTarget: { resourceType: 'agent', resourceId: 'host-a' },
},
],
topMemory: [
{
id: 'infra-a',
name: 'Infra A',
percent: 82,
metricsTarget: { resourceType: 'agent', resourceId: 'host-a' },
},
],
},
workloads: { total: 1, running: 1, stopped: 0, byType: { vm: 1 } },
storage: {
total: 1,
totalCapacity: 1_000,
totalUsed: 850,
warningCount: 1,
criticalCount: 0,
},
problemResources: [],
} as any);
const result = await ResourceAPI.getDashboardSummary();
expect(apiFetchJSON).toHaveBeenCalledWith('/api/resources/dashboard-summary', {
cache: 'no-store',
});
expect(result.health.totalResources).toBe(4);
expect(result.infrastructure.topCPU[0]?.id).toBe('infra-a');
expect(result.infrastructure.topCPU[0]?.metricsTarget).toEqual({
resourceType: 'agent',
resourceId: 'host-a',
});
});
it('fetches the resource history bundle from the facet endpoint', async () => {
vi.mocked(apiFetchJSON).mockResolvedValueOnce({
resourceId: 'vm:42',
-61
View File
@@ -6,8 +6,6 @@ import type {
ResourceChangeSourceAdapter,
ResourceChangeSourceType,
ResourceFacetCounts,
ResourceMetricsTarget,
ResourcePolicy,
ResourceRelationship,
} from '@/types/resource';
@@ -32,61 +30,6 @@ export interface ResourceFacetBundle {
counts: ResourceFacetCounts;
}
export interface DashboardOverviewSummaryTopResource {
id: string;
name: string;
percent: number;
metricsTarget?: ResourceMetricsTarget;
}
export interface DashboardOverviewSummaryProblemResource {
id: string;
type: string;
name: string;
status: string;
lastSeen?: string;
sources?: string[];
aiSafeSummary?: string;
policy?: ResourcePolicy;
canonicalIdentity?: {
displayName?: string;
hostname?: string;
platformId?: string;
primaryId?: string;
aliases?: string[];
};
problems: string[];
worstValue: number;
}
export interface DashboardOverviewSummaryResponse {
health: {
totalResources: number;
byStatus: Record<string, number>;
};
infrastructure: {
total: number;
byStatus: Record<string, number>;
byType: Record<string, number>;
topCPU: DashboardOverviewSummaryTopResource[];
topMemory: DashboardOverviewSummaryTopResource[];
};
workloads: {
total: number;
running: number;
stopped: number;
byType: Record<string, number>;
};
storage: {
total: number;
totalCapacity: number;
totalUsed: number;
warningCount: number;
criticalCount: number;
};
problemResources: DashboardOverviewSummaryProblemResource[];
}
const normalizeResourceId = (resourceId: string): string => resourceId.trim();
const buildFacetPath = (resourceId: string, suffix: string): string =>
@@ -122,10 +65,6 @@ const fetchFacet = async <T>(url: string): Promise<T> =>
});
export class ResourceAPI {
static async getDashboardSummary(): Promise<DashboardOverviewSummaryResponse> {
return fetchFacet<DashboardOverviewSummaryResponse>('/api/resources/dashboard-summary');
}
static async getTimeline(
resourceId: string,
options?: ResourceTimelineQueryOptions,
@@ -3,7 +3,7 @@ import RotateCcw from 'lucide-solid/icons/rotate-ccw';
import { TogglePrimitive } from '@/components/shared/Toggle';
import { StatusBadge } from '@/components/shared/StatusBadge';
import { ThresholdSlider } from '@/components/Dashboard/ThresholdSlider';
import { ThresholdSlider } from '@/components/Workloads/ThresholdSlider';
import { TableCell, TableRow } from '@/components/shared/Table';
import {
getAlertResourceTableCustomBadgeLabel,
@@ -1,6 +1,6 @@
import { Show, For, createSignal, createEffect } from 'solid-js';
import { Dialog } from '../shared/Dialog';
import { ThresholdSlider } from '../Dashboard/ThresholdSlider';
import { ThresholdSlider } from '../Workloads/ThresholdSlider';
import {
ALERT_BULK_EDIT_CANCEL_LABEL,
ALERT_BULK_EDIT_CLEAR_LABEL,
@@ -1,178 +0,0 @@
import { For, Show, createMemo } from 'solid-js';
import { Card } from '@/components/shared/Card';
import { ALERTS_OVERVIEW_PATH, AI_PATROL_PATH } from '@/routing/resourceLinks';
import { formatRelativeTime } from '@/utils/format';
import {
ALERTS_EMPTY_STATE,
getDashboardAlertSummaryText,
} from '@/utils/alertOverviewPresentation';
import {
getAlertSeverityBadgeClass,
getAlertSeverityCompactLabel,
} from '@/utils/alertSeverityPresentation';
import type { Alert } from '@/types/api';
import { getCanonicalAlertId } from '@/features/alerts/identity';
import { useAlertAcknowledgementState } from '@/features/alerts/useAlertAcknowledgementState';
import BellIcon from 'lucide-solid/icons/bell';
const MAX_SHOWN = 8;
interface RecentAlertsPanelProps {
alerts: Alert[];
}
// "State-only" alerts describe a state the operator typically chose (a VM that
// is stopped, a container that is powered off). Mixed alongside structural
// health signals like "ZFS device /dev/sda4 has errors" they drown out the
// signal that actually requires attention. We still surface them below
// structural warnings of the same level so the first screen leads with real
// problems.
const STATE_ONLY_MESSAGE_PATTERNS: RegExp[] = [/is powered off/i, /is stopped/i, /is paused/i];
function isStateOnlyAlert(alert: Alert): boolean {
const message = alert.message ?? '';
return STATE_ONLY_MESSAGE_PATTERNS.some((pattern) => pattern.test(message));
}
function alertRank(alert: Alert): number {
// Lower rank = higher priority in the list.
if (alert.level === 'critical') return 0;
if (alert.level === 'warning') {
return isStateOnlyAlert(alert) ? 3 : 1;
}
return 2;
}
function sortAlertsForDashboard(alerts: Alert[]): Alert[] {
const sorted = [...alerts];
sorted.sort((a, b) => {
const rankDelta = alertRank(a) - alertRank(b);
if (rankDelta !== 0) return rankDelta;
const aMs = Date.parse(a.startTime) || 0;
const bMs = Date.parse(b.startTime) || 0;
return bMs - aMs;
});
return sorted;
}
export function RecentAlertsPanel(props: RecentAlertsPanelProps) {
const {
effectiveAlerts,
unacknowledgedAlerts,
processingAlerts,
bulkAckProcessing,
handleAlertAcknowledgement,
handleBulkAcknowledge,
} = useAlertAcknowledgementState({
alerts: () => props.alerts,
});
const recent = createMemo(() => sortAlertsForDashboard(effectiveAlerts()).slice(0, MAX_SHOWN));
const activeCriticalCount = createMemo(
() =>
effectiveAlerts().filter((alert) => !alert.acknowledged && alert.level === 'critical').length,
);
const activeWarningCount = createMemo(
() =>
effectiveAlerts().filter((alert) => !alert.acknowledged && alert.level === 'warning').length,
);
const alertSummaryText = createMemo(() =>
getDashboardAlertSummaryText({
activeCritical: activeCriticalCount(),
activeWarning: activeWarningCount(),
}),
);
return (
<Card padding="none" tone="default" class="border-border-subtle overflow-hidden">
<div class="px-4 py-3 flex items-center justify-between gap-2">
<div class="flex items-center gap-2">
<BellIcon class="w-4 h-4 text-muted" aria-hidden="true" />
<h2 class="text-sm font-semibold text-base-content">Alerts</h2>
</div>
<div class="flex items-center gap-2">
<Show when={unacknowledgedAlerts().length > 1}>
<button
type="button"
onClick={() => {
void handleBulkAcknowledge();
}}
disabled={bulkAckProcessing()}
class="text-[10px] font-medium text-base-content bg-surface-alt hover:bg-surface-hover disabled:opacity-50 px-2 py-0.5 rounded transition-colors"
>
{bulkAckProcessing() ? 'Acking...' : 'Ack All'}
</button>
</Show>
<a
href={ALERTS_OVERVIEW_PATH}
aria-label="View all alerts"
class="text-xs font-medium text-blue-600 hover:text-blue-700 dark:text-blue-400 dark:hover:text-blue-300"
>
All →
</a>
<a
href={AI_PATROL_PATH}
aria-label="View findings"
class="text-xs font-medium text-blue-600 hover:text-blue-700 dark:text-blue-400 dark:hover:text-blue-300"
>
Findings →
</a>
</div>
</div>
<div class="px-4 pb-3">
<Show
when={props.alerts.length > 0}
fallback={
<p class="text-xs font-medium text-emerald-700 dark:text-emerald-300 pt-0.5">
{ALERTS_EMPTY_STATE}
</p>
}
>
<p class="text-xs text-muted mb-1.5">{alertSummaryText()}</p>
<ul class="space-y-0.5" role="list">
<For each={recent()}>
{(alert) => (
<li class="flex items-center gap-2 -mx-1 px-1 py-1 rounded hover:bg-surface-hover transition-colors">
<span class={getAlertSeverityBadgeClass(alert.level)}>
{getAlertSeverityCompactLabel(alert.level)}
</span>
<p class="min-w-0 text-xs text-base-content truncate">{alert.resourceName}</p>
<p class="min-w-0 text-xs text-muted truncate flex-1">{alert.message}</p>
<span class="shrink-0 ml-auto text-[10px] font-mono text-slate-400">
{formatRelativeTime(alert.startTime, { compact: true })}
</span>
<Show when={!alert.acknowledged}>
<button
type="button"
onClick={() => {
void handleAlertAcknowledgement(alert);
}}
disabled={processingAlerts().has(getCanonicalAlertId(alert))}
class="shrink-0 px-1.5 py-0.5 text-[10px] font-medium text-base-content bg-surface-alt hover:bg-surface-hover disabled:opacity-50 rounded transition-colors"
>
{processingAlerts().has(getCanonicalAlertId(alert)) ? '...' : 'Ack'}
</button>
</Show>
</li>
)}
</For>
</ul>
<Show when={props.alerts.length > MAX_SHOWN}>
<p class="mt-1.5 text-[11px] text-muted">
<a
href={ALERTS_OVERVIEW_PATH}
class="text-blue-600 hover:underline dark:text-blue-400"
>
+{props.alerts.length - MAX_SHOWN} more
</a>
</p>
</Show>
</Show>
</div>
</Card>
);
}
export default RecentAlertsPanel;
@@ -92,7 +92,7 @@ vi.mock('@/components/shared/SectionHeader', () => ({
SectionHeader: (props: any) => <h3 data-testid="section-header">{props.title}</h3>,
}));
vi.mock('@/components/Dashboard/ThresholdSlider', () => ({
vi.mock('@/components/Workloads/ThresholdSlider', () => ({
ThresholdSlider: (props: any) => (
<input
data-testid="threshold-slider"
@@ -5,7 +5,7 @@ import { createSignal } from 'solid-js';
import { BulkEditDialog, normalizeMetricKey } from '../BulkEditDialog';
// Mock ThresholdSlider — renders a range input that calls onChange on input events
vi.mock('../../Dashboard/ThresholdSlider', () => ({
vi.mock('../../Workloads/ThresholdSlider', () => ({
ThresholdSlider: (props: {
type: string;
min: number;
@@ -1,136 +0,0 @@
import { fireEvent, render, screen, waitFor } from '@solidjs/testing-library';
import { beforeEach, describe, expect, it, vi } from 'vitest';
import { AlertsAPI } from '@/api/alerts';
import { notificationStore } from '@/stores/notifications';
import { RecentAlertsPanel } from '../RecentAlertsPanel';
import type { Alert } from '@/types/api';
vi.mock('@/api/alerts', () => ({
AlertsAPI: {
acknowledge: vi.fn(),
bulkAcknowledge: vi.fn(),
},
}));
vi.mock('@/stores/notifications', () => ({
notificationStore: {
error: vi.fn(),
success: vi.fn(),
},
}));
vi.mock('@/utils/logger', () => ({
logger: {
error: vi.fn(),
},
}));
function makeAlert(overrides: Partial<Alert> = {}): Alert {
return {
id: 'alert-1',
level: 'critical',
resourceName: 'tower',
message: 'Disk failure detected',
acknowledged: false,
startTime: '2026-03-08T10:00:00Z',
type: 'disk-health',
resourceId: 'host-1',
resourceType: 'agent',
source: 'agent',
...overrides,
} as Alert;
}
describe('RecentAlertsPanel', () => {
beforeEach(() => {
vi.mocked(AlertsAPI.acknowledge).mockReset();
vi.mocked(AlertsAPI.bulkAcknowledge).mockReset();
vi.mocked(notificationStore.success).mockReset();
vi.mocked(notificationStore.error).mockReset();
});
it('renders summary counts when alerts exist', () => {
render(() => (
<RecentAlertsPanel alerts={[makeAlert(), makeAlert({ id: 'alert-2', level: 'warning' })]} />
));
expect(screen.getByText(/critical ·/)).toBeInTheDocument();
expect(screen.getAllByText('tower')).toHaveLength(2);
});
it('renders empty state when there are no active alerts', () => {
render(() => <RecentAlertsPanel alerts={[]} />);
expect(screen.getByText('No active alerts')).toBeInTheDocument();
});
it('keeps structural warnings above state-only warnings of the same severity', () => {
render(() => (
<RecentAlertsPanel
alerts={[
makeAlert({
id: 'state-only',
level: 'warning',
resourceName: 'tails-anon',
message: "VM 'tails-anon' is powered off",
startTime: '2026-03-08T12:00:00Z',
}),
makeAlert({
id: 'structural',
level: 'warning',
resourceName: 'local-zfs',
message: 'ZFS device /dev/sda4 has errors',
startTime: '2026-03-08T09:00:00Z',
}),
]}
/>
));
const structuralWarning = screen.getByText('ZFS device /dev/sda4 has errors');
const stateOnlyWarning = screen.getByText("VM 'tails-anon' is powered off");
expect(structuralWarning.compareDocumentPosition(stateOnlyWarning)).toBe(
Node.DOCUMENT_POSITION_FOLLOWING,
);
});
it('routes single acknowledge actions through the shared alert acknowledgement owner', async () => {
vi.mocked(AlertsAPI.acknowledge).mockResolvedValue(undefined as never);
render(() => (
<RecentAlertsPanel
alerts={[makeAlert(), makeAlert({ id: 'alert-2', message: 'Memory high' })]}
/>
));
await fireEvent.click(screen.getAllByText('Ack')[0]);
await waitFor(() => {
expect(AlertsAPI.acknowledge).toHaveBeenCalledWith('alert-1');
});
expect(notificationStore.success).toHaveBeenCalledWith('Alert acknowledged');
});
it('routes bulk acknowledge actions through the shared alert acknowledgement owner', async () => {
vi.mocked(AlertsAPI.bulkAcknowledge).mockResolvedValue({
results: [
{ alertIdentifier: 'alert-1', success: true },
{ alertIdentifier: 'alert-2', success: true },
],
} as never);
render(() => (
<RecentAlertsPanel
alerts={[makeAlert(), makeAlert({ id: 'alert-2', message: 'Memory high' })]}
/>
));
await fireEvent.click(screen.getByText('Ack All'));
await waitFor(() => {
expect(AlertsAPI.bulkAcknowledge).toHaveBeenCalledWith(['alert-1', 'alert-2']);
});
expect(notificationStore.success).toHaveBeenCalledWith('Acknowledged 2 alerts.');
});
});
@@ -1,218 +0,0 @@
import { describe, expect, it } from 'vitest';
import type { Alert } from '@/types/api';
import type { Resource } from '@/types/resource';
import { computeDashboardOverview } from '@/hooks/useDashboardOverview';
function createResource(overrides: Partial<Resource> = {}): Resource {
return {
id: 'resource-1',
type: 'agent',
name: 'resource-1',
displayName: 'Resource 1',
platformId: 'platform-1',
platformType: 'agent',
sourceType: 'agent',
status: 'online',
lastSeen: Date.now(),
...overrides,
};
}
describe('Dashboard panels data contract', () => {
it('computes infrastructure and workloads summary data for dashboard panels', () => {
const resources: Resource[] = [
createResource({
id: 'infra-1',
type: 'agent',
name: 'host-alpha',
displayName: 'Host Alpha',
status: 'online',
cpu: { current: 95 },
}),
createResource({
id: 'infra-2',
type: 'agent',
name: 'node-beta',
displayName: 'Node Beta',
status: 'offline',
cpu: { current: 92 },
}),
createResource({
id: 'infra-3',
type: 'k8s-node',
name: 'k8s-gamma',
displayName: 'K8s Gamma',
status: 'online',
cpu: { current: 84 },
}),
createResource({
id: 'infra-4',
type: 'docker-host',
name: 'docker-delta',
displayName: 'Docker Delta',
status: 'online',
cpu: { current: 78 },
}),
createResource({
id: 'infra-5',
type: 'agent',
name: 'truenas-epsilon',
displayName: 'TrueNAS Epsilon',
platformType: 'truenas',
status: 'degraded',
cpu: { current: 65 },
}),
createResource({
id: 'infra-6',
type: 'k8s-cluster',
name: 'cluster-zeta',
displayName: 'Cluster Zeta',
status: 'online',
cpu: { current: 51 },
}),
createResource({
id: 'work-1',
type: 'vm',
status: 'running',
}),
createResource({
id: 'work-2',
type: 'system-container',
status: 'online',
}),
createResource({
id: 'work-3',
type: 'app-container',
status: 'stopped',
}),
createResource({
id: 'work-4',
type: 'pod',
status: 'offline',
}),
];
const overview = computeDashboardOverview(resources, []);
expect(overview.infrastructure.total).toBe(6);
expect(overview.infrastructure.byStatus.online).toBe(4);
expect(overview.infrastructure.byStatus.offline).toBe(1);
expect(overview.infrastructure.topCPU).toHaveLength(5);
expect(overview.infrastructure.topCPU.map((entry) => entry.id)).toEqual([
'infra-1',
'infra-2',
'infra-3',
'infra-4',
'infra-5',
]);
expect(overview.infrastructure.topCPU.map((entry) => entry.percent)).toEqual([
95, 92, 84, 78, 65,
]);
expect(overview.workloads.total).toBe(4);
expect(overview.workloads.running).toBe(2);
expect(overview.workloads.stopped).toBe(2);
expect(overview.workloads.byType).toEqual({
vm: 1,
'system-container': 1,
'app-container': 1,
pod: 1,
});
});
it('computes storage summary data for dashboard panels', () => {
const resources: Resource[] = [
createResource({
id: 'store-1',
type: 'storage',
status: 'online',
disk: { current: 40, total: 1000, used: 400 },
}),
createResource({
id: 'store-2',
type: 'datastore',
status: 'online',
disk: { current: 85, total: 1000, used: 850 },
}),
createResource({
id: 'store-3',
type: 'pool',
status: 'online',
disk: { current: 90, total: 1000, used: 900 },
}),
createResource({
id: 'store-4',
type: 'dataset',
status: 'online',
disk: { current: 92, total: 1000, used: 920 },
}),
createResource({
id: 'infra-1',
type: 'agent',
status: 'online',
disk: { current: 99, total: 1000, used: 990 },
}),
];
const overview = computeDashboardOverview(resources, []);
expect(overview.storage.total).toBe(4);
expect(overview.storage.totalCapacity).toBe(4000);
expect(overview.storage.totalUsed).toBe(3070);
expect(overview.storage.warningCount).toBe(2);
expect(overview.storage.criticalCount).toBe(1);
});
it('computes alerts summary data for dashboard panels', () => {
const alerts: Alert[] = [
{
id: 'a-1',
type: 'cpu',
level: 'critical',
resourceId: 'r1',
resourceName: 'Host 1',
node: 'n1',
instance: 'i1',
message: 'CPU critical',
value: 95,
threshold: 90,
startTime: '2026-02-01T00:00:00Z',
acknowledged: false,
},
{
id: 'a-2',
type: 'memory',
level: 'warning',
resourceId: 'r2',
resourceName: 'Host 2',
node: 'n2',
instance: 'i2',
message: 'Memory high',
value: 85,
threshold: 80,
startTime: '2026-02-01T00:00:00Z',
acknowledged: false,
},
{
id: 'a-3',
type: 'disk',
level: 'critical',
resourceId: 'r3',
resourceName: 'Host 3',
node: 'n3',
instance: 'i3',
message: 'Disk full',
value: 96,
threshold: 90,
startTime: '2026-02-01T00:00:00Z',
acknowledged: false,
},
];
const overview = computeDashboardOverview([], alerts);
expect(overview.alerts.activeCritical).toBe(2);
expect(overview.alerts.activeWarning).toBe(1);
expect(overview.alerts.total).toBe(3);
});
});
@@ -1,89 +0,0 @@
import type { ColumnDef } from '@/hooks/useColumnVisibility';
import type { ViewMode, WorkloadGuest } from '@/types/workloads';
import type { JSX } from 'solid-js';
export type DashboardStatusMode = 'all' | 'running' | 'degraded' | 'stopped';
export type DashboardGroupingMode = 'grouped' | 'flat';
export type DashboardSortKey = keyof WorkloadGuest | 'diskIo' | 'netIo';
export interface DashboardFilterSelectOption {
value: string;
label: string;
}
export interface DashboardToolbarFilterConfig {
id?: string;
label?: string;
value: string;
options: DashboardFilterSelectOption[];
onChange: (value: string) => void;
}
export interface DashboardFilterProps {
search: () => string;
setSearch: (value: string) => void;
viewMode: () => ViewMode;
setViewMode: (value: ViewMode) => void;
statusMode: () => DashboardStatusMode;
setStatusMode: (value: DashboardStatusMode) => void;
groupingMode: () => DashboardGroupingMode;
setGroupingMode: (value: DashboardGroupingMode) => void;
setSortKey: (value: DashboardSortKey) => void;
setSortDirection: (value: 'asc' | 'desc') => void;
onBeforeAutoFocus?: () => boolean;
columnVisibility?: {
availableColumns: ColumnDef[];
isColumnHidden: (id: string) => boolean;
onColumnToggle: (id: string) => void;
onColumnReset?: () => void;
};
hostFilter?: DashboardToolbarFilterConfig;
platformFilter?: DashboardToolbarFilterConfig;
namespaceFilter?: DashboardToolbarFilterConfig;
containerRuntimeFilter?: DashboardToolbarFilterConfig;
chartsCollapsed?: () => boolean;
onChartsToggle?: () => void;
searchTrailing?: JSX.Element;
utilityActions?: JSX.Element;
mobileTrailing?: JSX.Element;
}
export interface CountActiveDashboardFiltersOptions {
search: string;
viewMode: ViewMode;
statusMode: DashboardStatusMode;
hostFilterValue?: string;
platformFilterValue?: string;
namespaceFilterValue?: string;
}
export interface HasActiveDashboardFiltersOptions extends CountActiveDashboardFiltersOptions {
groupingMode: DashboardGroupingMode;
}
export const DEFAULT_DASHBOARD_SORT_KEY: DashboardSortKey = 'type';
export const DEFAULT_DASHBOARD_SORT_DIRECTION = 'asc';
export const DEFAULT_DASHBOARD_VIEW_MODE: ViewMode = 'all';
export const DEFAULT_DASHBOARD_STATUS_MODE: DashboardStatusMode = 'all';
export const DEFAULT_DASHBOARD_GROUPING_MODE: DashboardGroupingMode = 'grouped';
export const countActiveDashboardFilters = (
options: CountActiveDashboardFiltersOptions,
): number => {
let count = 0;
if (options.search.trim() !== '') count++;
if (options.viewMode !== DEFAULT_DASHBOARD_VIEW_MODE) count++;
if (options.statusMode !== DEFAULT_DASHBOARD_STATUS_MODE) count++;
if ((options.hostFilterValue ?? '') !== '') count++;
if ((options.platformFilterValue ?? '') !== '') count++;
if ((options.namespaceFilterValue ?? '') !== '') count++;
return count;
};
export const hasActiveDashboardFilters = (
options: HasActiveDashboardFiltersOptions,
): boolean =>
countActiveDashboardFilters(options) > 0 ||
options.groupingMode !== DEFAULT_DASHBOARD_GROUPING_MODE;
@@ -1,71 +0,0 @@
import type { Disk } from '@/types/api';
import { formatBytes } from '@/utils/format';
import { getMetricColorClass } from '@/utils/metricThresholds';
export interface DiskListProps {
disks: Disk[];
diskStatusReason?: string;
}
export interface DashboardDiskPresentation {
key: string;
label: string;
labelTitle?: string;
progressClass: string;
progressWidth: string;
typeLabel: string;
usageText: string;
usagePercentLabel: string;
}
export const hasDashboardDiskCapacity = (disk: Disk): boolean =>
typeof disk.total === 'number' && disk.total > 0;
export const getDashboardDiskUsagePercent = (disk: Disk): number => {
if (!hasDashboardDiskCapacity(disk)) {
return 0;
}
return (disk.used / disk.total) * 100;
};
export const getDashboardDiskLabel = (disk: Disk): string =>
disk.mountpoint || disk.device || 'Unknown';
export const getDashboardDiskLabelTitle = (label: string): string | undefined =>
label !== 'Unknown' ? label : undefined;
export const getDashboardDiskUsageText = (disk: Disk): string =>
hasDashboardDiskCapacity(disk)
? `${formatBytes(disk.used)}/${formatBytes(disk.total)}`
: 'Usage unavailable';
export const getDashboardDiskUsagePercentLabel = (disk: Disk): string =>
hasDashboardDiskCapacity(disk) ? `${getDashboardDiskUsagePercent(disk).toFixed(0)}%` : '—';
export const getDashboardDiskProgressClass = (disk: Disk): string =>
getMetricColorClass(getDashboardDiskUsagePercent(disk), 'disk');
export const getDashboardDiskProgressWidth = (disk: Disk): string =>
`${Math.min(getDashboardDiskUsagePercent(disk), 100)}%`;
export const getDashboardDiskTypeLabel = (disk: Disk): string => disk.type?.toUpperCase() ?? '';
export const buildDashboardDiskPresentation = (
disk: Disk,
index: number,
): DashboardDiskPresentation => {
const label = getDashboardDiskLabel(disk);
return {
key: `${disk.mountpoint ?? ''}:${disk.device ?? ''}:${index}`,
label,
labelTitle: getDashboardDiskLabelTitle(label),
progressClass: getDashboardDiskProgressClass(disk),
progressWidth: getDashboardDiskProgressWidth(disk),
typeLabel: getDashboardDiskTypeLabel(disk),
usageText: getDashboardDiskUsageText(disk),
usagePercentLabel: getDashboardDiskUsagePercentLabel(disk),
};
};
@@ -21,8 +21,8 @@ import {
} from '@/components/shared/Table';
import { StatusDot } from '@/components/shared/StatusDot';
import { ResponsiveMetricCell } from '@/components/shared/responsive';
import { StackedDiskBar } from '@/components/Dashboard/StackedDiskBar';
import { StackedMemoryBar } from '@/components/Dashboard/StackedMemoryBar';
import { StackedDiskBar } from '@/components/Workloads/StackedDiskBar';
import { StackedMemoryBar } from '@/components/Workloads/StackedMemoryBar';
import { buildMetricKeyForUnifiedResource } from '@/utils/metricsKeys';
import {
dedupeResourceBadges,
@@ -144,7 +144,7 @@ describe('ResourceDetailDrawer change history section', () => {
expect(resourceDetailDrawerOverviewSource).not.toContain("'All sources'");
expect(resourceDetailDrawerOverviewSource).not.toContain("'All adapters'");
expect(resourceDetailDrawerOverviewSource).not.toContain(
"from '@/components/Dashboard/TagBadges'",
"from '@/components/Workloads/TagBadges'",
);
expect(resourceDetailDrawerShellSource).toContain("from './ResourceDetailDrawerOverviewTab'");
expect(resourceDetailDrawerShellSource).toContain("from './ResourceDetailDrawerDebugTab'");
@@ -1,89 +0,0 @@
import { For, Show, createMemo } from 'solid-js';
import { Card } from '@/components/shared/Card';
import { buildRecoveryPath } from '@/routing/resourceLinks';
import { formatRelativeTime } from '@/utils/format';
import {
RECOVERY_OUTCOMES,
getRecoveryOutcomeBadgeClass,
} from '@/utils/recoveryOutcomePresentation';
import {
DASHBOARD_RECOVERY_EMPTY_STATE,
DASHBOARD_RECOVERY_STALE_MESSAGE,
} from '@/utils/dashboardRecoveryPresentation';
import type { DashboardRecoverySummary } from '@/hooks/useDashboardRecovery';
interface DashboardRecoveryStatusPanelProps {
recovery: DashboardRecoverySummary;
}
export function DashboardRecoveryStatusPanel(props: DashboardRecoveryStatusPanelProps) {
const latestAgeMs = createMemo(() => {
const ts = props.recovery.latestEventTimestamp;
if (typeof ts !== 'number' || !Number.isFinite(ts)) return null;
return Date.now() - ts;
});
const isStale = createMemo(() => (latestAgeMs() ?? 0) > 24 * 60 * 60_000);
return (
<Card padding="none" class="px-4 py-3.5">
<div class="flex items-center justify-between gap-3">
<h2 class="text-sm font-semibold text-base-content">Recovery Status</h2>
<a
href={buildRecoveryPath()}
aria-label="View all recovery"
class="text-xs font-medium text-blue-600 hover:text-blue-700 dark:text-blue-400 dark:hover:text-blue-300"
>
View all →
</a>
</div>
<Show
when={props.recovery.hasData}
fallback={<p class="text-xs text-muted mt-1">{DASHBOARD_RECOVERY_EMPTY_STATE}</p>}
>
<div class="space-y-1.5">
<div class="flex items-baseline justify-between gap-4">
<p class="text-xs text-muted">
<span class="font-mono font-semibold text-base text-base-content">
{props.recovery.totalProtected}
</span>{' '}
total
</p>
<p class="text-xs text-muted">
Last:{' '}
<span class="font-mono font-medium text-base-content">
{formatRelativeTime(props.recovery.latestEventTimestamp ?? undefined, {
compact: true,
}) || '—'}
</span>
</p>
</div>
<Show when={isStale()}>
<p class="text-sm font-medium text-amber-700 dark:text-amber-300">
{DASHBOARD_RECOVERY_STALE_MESSAGE}
</p>
</Show>
<div class="flex flex-wrap gap-2">
<For each={RECOVERY_OUTCOMES}>
{(outcome) => {
const count = () => props.recovery.byOutcome[outcome] ?? 0;
return (
<Show when={count() > 0}>
<span class={getRecoveryOutcomeBadgeClass(outcome)}>
<span class="font-mono">{count()}</span>
<span class="ml-1">{outcome}</span>
</span>
</Show>
);
}}
</For>
</div>
</div>
</Show>
</Card>
);
}
export default DashboardRecoveryStatusPanel;
@@ -1,37 +0,0 @@
import { render, screen } from '@solidjs/testing-library';
import { describe, expect, it } from 'vitest';
import { DashboardRecoveryStatusPanel } from '@/components/Recovery/DashboardRecoveryStatusPanel';
import type { DashboardRecoverySummary } from '@/hooks/useDashboardRecovery';
describe('DashboardRecoveryStatusPanel', () => {
it('renders the empty state when no recovery data exists', () => {
const recovery: DashboardRecoverySummary = {
totalProtected: 0,
byOutcome: {},
latestEventTimestamp: null,
hasData: false,
};
render(() => <DashboardRecoveryStatusPanel recovery={recovery} />);
expect(screen.getByText('No recovery data available')).toBeInTheDocument();
});
it('renders summary counts and stale messaging for old recovery activity', () => {
const recovery: DashboardRecoverySummary = {
totalProtected: 3,
byOutcome: { success: 2, failed: 1 },
latestEventTimestamp: Date.now() - 25 * 60 * 60_000,
hasData: true,
};
render(() => <DashboardRecoveryStatusPanel recovery={recovery} />);
expect(screen.getByRole('heading', { name: 'Recovery Status' })).toBeInTheDocument();
expect(screen.getByText('3')).toBeInTheDocument();
expect(screen.getByText('2')).toBeInTheDocument();
expect(screen.getByText('success')).toBeInTheDocument();
expect(screen.getByText('failed')).toBeInTheDocument();
expect(screen.getByText('Last recovery point over 24 hours ago')).toBeInTheDocument();
});
});
@@ -182,8 +182,8 @@ export const GeneralSettingsPanel: Component<GeneralSettingsPanelProps> = (props
<div class="text-sm text-muted min-w-0">
<p class="font-medium text-base-content truncate">Navigation guide</p>
<p class="text-xs text-muted line-clamp-2">
Replay the five-stop walkthrough of Dashboard, Infrastructure, Workloads, Storage,
and Recovery.
Replay the four-stop walkthrough of Infrastructure, Workloads, Storage, and
Recovery.
</p>
</div>
</div>
@@ -613,8 +613,8 @@ export const InfrastructureInstallerSection: Component = () => {
>
{isConnected()
? state.lookupWasAutoDetected()
? `${agent().displayName || agent().hostname} started reporting and Pulse detected it automatically. Open the dashboard to verify your first overview, or open Inventory to inspect this host and add more infrastructure.`
: `${agent().displayName || agent().hostname} is reporting live telemetry to Pulse. Open the dashboard to verify your first overview, or open Inventory to inspect this host and add more infrastructure.`
? `${agent().displayName || agent().hostname} started reporting and Pulse detected it automatically. Open Infrastructure to inspect this host and add more infrastructure.`
: `${agent().displayName || agent().hostname} is reporting live telemetry to Pulse. Open Infrastructure to inspect this host and add more infrastructure.`
: `${agent().displayName || agent().hostname} has been found, but Pulse is not receiving a live check-in yet. Keep the installer running on that machine and check again.`}
</p>
</div>
@@ -650,10 +650,10 @@ export const InfrastructureInstallerSection: Component = () => {
<div class="flex flex-col gap-2 sm:flex-row">
<button
type="button"
onClick={state.openDashboard}
onClick={state.openInfrastructure}
class="inline-flex items-center justify-center rounded-md bg-emerald-600 px-3 py-2 text-sm font-medium text-white transition-colors hover:bg-emerald-700"
>
Open dashboard
Open infrastructure
</button>
<button
type="button"
@@ -458,7 +458,7 @@ export const InfrastructureSourceManager: Component<InfrastructureSourceManagerP
<p class="mt-1 text-sm leading-5 text-muted">
This is the source map Pulse uses to understand the estate. Confirm the API inventory,
agent telemetry, scan scope, and any pending discovery candidates before relying on
dashboards or automation.
monitoring views or automation.
</p>
</div>
@@ -47,7 +47,7 @@ export const NetworkBoundarySettingsSection: Component<NetworkBoundarySettingsSe
Public URL
</h4>
<div class="space-y-2">
<label class="text-sm font-medium text-base-content">Dashboard URL for Notifications</label>
<label class="text-sm font-medium text-base-content">Pulse URL for Notifications</label>
<p class="text-xs text-muted">
The URL included in email alerts to link back to Pulse. Required for Docker deployments
with custom ports.
@@ -148,7 +148,7 @@ export const NetworkBoundarySettingsSection: Component<NetworkBoundarySettingsSe
Embedding
</h4>
<p class="text-xs text-muted">
Allow Pulse to be embedded in iframes (e.g., Homepage dashboard)
Allow Pulse to be embedded in iframes (e.g., Homepage)
</p>
<div class="space-y-3">
<div class="flex items-center gap-2">
@@ -183,11 +183,11 @@ export const NetworkBoundarySettingsSection: Component<NetworkBoundarySettingsSe
props.setAllowedEmbedOrigins(e.currentTarget.value);
props.setHasUnsavedChanges(true);
}}
placeholder="https://my.domain, https://dashboard.example.com"
placeholder="https://my.domain, https://pulse.example.com"
class="w-full min-h-10 sm:min-h-10 px-3 py-2.5 text-sm border rounded-md border-border bg-surface"
/>
<p class="text-xs text-muted">
Example: If Pulse is at <code>pulse.my.domain</code> and your dashboard is at{' '}
Example: If Pulse is at <code>pulse.my.domain</code> and your homepage is at{' '}
<code>my.domain</code>, add <code>https://my.domain</code> here.
</p>
</div>
@@ -34,7 +34,7 @@ export const NodeModalBasicInfoSection: Component<NodeModalBasicInfoSectionProps
type="text"
value={state.formData().name}
onInput={(event) => state.updateField('name', event.currentTarget.value)}
placeholder="Pulse uses this label across dashboards"
placeholder="Pulse uses this label across monitoring views"
required
class={controlClass()}
/>
@@ -75,7 +75,7 @@ export const NodeModalMonitoringSection: Component<NodeModalMonitoringSectionPro
/>
<p class="text-xs text-muted">
Control which Proxmox VE resources Pulse ingests. Disable individual collectors to
reduce API load or trim dashboard noise.
reduce API load or trim workload noise.
</p>
<label class="flex items-start gap-2 text-sm text-base-content">
@@ -216,7 +216,7 @@ export const NodeModalMonitoringSection: Component<NodeModalMonitoringSectionPro
</div>
<Show when={!state.temperatureMonitoringEnabledValue()}>
<p class="mt-3 rounded border border-blue-200 bg-blue-50 p-2 text-xs text-blue-700 dark:border-blue-700 dark:bg-blue-900 dark:text-blue-200">
Pulse will skip SSH temperature polling for this node. Existing dashboard readings
Pulse will skip SSH temperature polling for this node. Existing monitoring readings
will stop refreshing.
</p>
</Show>
@@ -64,7 +64,7 @@ export const RecoverySettingsPanel: Component<RecoverySettingsPanelProps> = (pro
<div>
<p class="text-sm font-medium text-base-content">Enable backup polling</p>
<p class="text-xs text-muted">
Required for dashboard backup status, storage snapshots, and alerting.
Required for workload backup status, storage snapshots, and alerting.
</p>
</div>
<label class="relative inline-flex items-center cursor-pointer">
@@ -347,7 +347,7 @@ describe('APITokenManager', () => {
fireEvent.input(screen.getByPlaceholderText('e.g. Container pipeline'), {
target: { value: 'Blocked token' },
});
fireEvent.click(screen.getByRole('button', { name: 'Kiosk / Dashboard' }));
fireEvent.click(screen.getByRole('button', { name: 'Kiosk / Monitoring' }));
fireEvent.click(screen.getByRole('button', { name: 'Generate' }));
await waitFor(() => {
@@ -206,6 +206,19 @@ describe('infrastructure operations model', () => {
);
});
it('keeps first-host completion handoff on Infrastructure instead of the retired dashboard', async () => {
const installStateSource = await import('../useInfrastructureInstallState.tsx?raw').then(
(mod) => (mod as { default: string }).default,
);
expect(infrastructureInstallerSectionSource).toContain('Open infrastructure');
expect(infrastructureInstallerSectionSource).not.toContain('Open dashboard');
expect(installStateSource).toContain('const openInfrastructure = () => {');
expect(installStateSource).toContain('navigate(buildInfrastructureWorkspacePath())');
expect(installStateSource).not.toContain('openDashboard');
expect(installStateSource).not.toContain("navigate('/dashboard')");
});
it('does not reintroduce the retired reporting state hook on the operations state', async () => {
const operationsStateSource = await import(
'../useInfrastructureOperationsState?raw'
@@ -32,6 +32,8 @@ import connectionEditorSource from '../ConnectionEditor/ConnectionEditor.tsx?raw
import addressProbeStepSource from '../ConnectionEditor/AddressProbeStep.tsx?raw';
import connectionEditorStateSource from '../ConnectionEditor/useConnectionEditor.ts?raw';
import nodeCredentialSlotSource from '../ConnectionEditor/CredentialSlots/NodeCredentialSlot.tsx?raw';
import networkBoundarySettingsSectionSource from '../NetworkBoundarySettingsSection.tsx?raw';
import nodeModalBasicInfoSectionSource from '../NodeModalBasicInfoSection.tsx?raw';
import nodeModalAuthenticationSectionSource from '../NodeModalAuthenticationSection.tsx?raw';
import nodeModalMonitoringSectionSource from '../NodeModalMonitoringSection.tsx?raw';
import nodeModalSetupGuideSectionSource from '../NodeModalSetupGuideSection.tsx?raw';
@@ -231,6 +233,22 @@ describe('settings architecture guardrails', () => {
}
});
it('keeps settings copy aligned with Infrastructure as the default workspace', () => {
expect(generalSettingsPanelSource).toContain(
'Replay the four-stop walkthrough of Infrastructure, Workloads, Storage, and',
);
expect(generalSettingsPanelSource).not.toContain('Dashboard, Infrastructure');
expect(networkBoundarySettingsSectionSource).toContain('Pulse URL for Notifications');
expect(networkBoundarySettingsSectionSource).not.toContain('Dashboard URL for Notifications');
expect(nodeModalBasicInfoSectionSource).toContain('monitoring views');
expect(nodeModalBasicInfoSectionSource).not.toContain('dashboards');
expect(nodeModalMonitoringSectionSource).toContain('trim workload noise');
expect(nodeModalMonitoringSectionSource).toContain('Existing monitoring readings');
expect(nodeModalMonitoringSectionSource).not.toContain('dashboard readings');
expect(recoverySettingsPanelSource).toContain('Required for workload backup status');
expect(recoverySettingsPanelSource).not.toContain('dashboard backup status');
});
it('keeps system AI model catalogs on the shared searchable picker boundary', () => {
expect(aiModelSelectionSectionSource).toContain(
"import { AIModelPicker } from '@/components/shared/AIModelPicker';",
@@ -40,7 +40,7 @@ const API_TOKEN_SCOPE_GROUP_ORDER: ScopeGroup[] = ['Monitoring', 'Agents', 'Sett
export const getAPITokenScopePresets = (): APITokenPreset[] => [
{
label: 'Kiosk / Dashboard',
label: 'Kiosk / Monitoring',
scopes: [MONITORING_READ_SCOPE],
description:
'Read-only access for wall displays. Use ?token=xxx&kiosk=1 in the URL to hide navigation and filters.',
@@ -473,8 +473,8 @@ Pulse prepares the first-host install token from setup so you can move straight
}));
});
const openDashboard = () => {
navigate('/dashboard');
const openInfrastructure = () => {
navigate(buildInfrastructureWorkspacePath());
};
const openInfrastructureInventory = () => {
@@ -515,7 +515,7 @@ Pulse prepares the first-host install token from setup so you can move straight
lookupResult,
lookupValue,
lookupWasAutoDetected,
openDashboard,
openInfrastructure,
openInfrastructureInventory,
requiresToken,
selectedAgentUrl,
@@ -165,7 +165,7 @@ Keep these credentials secure!
props.onComplete(AGENT_INSTALL_PATH);
};
const handleGoToDashboard = () => {
const handleOpenInfrastructure = () => {
props.onComplete('/');
};
@@ -441,14 +441,14 @@ Keep these credentials secure!
<div class="mt-4 flex flex-col gap-3 sm:flex-row">
<button
onClick={() =>
completionViewModel().primaryAction === 'dashboard'
? handleGoToDashboard()
completionViewModel().primaryAction === 'infrastructure'
? handleOpenInfrastructure()
: handleOpenAddInfrastructure()
}
class="inline-flex items-center justify-center gap-2 rounded-md bg-blue-600 px-4 py-3 text-sm font-semibold text-white transition-colors hover:bg-blue-700"
>
{completionViewModel().primaryAction === 'dashboard'
? 'Go to Dashboard'
{completionViewModel().primaryAction === 'infrastructure'
? 'Open Infrastructure'
: 'Add infrastructure'}
</button>
<Show when={completionViewModel().showAddInfrastructureAction}>
@@ -100,13 +100,13 @@ describe('SetupCompletionPanel guardrails', () => {
);
expect(setupCompletionPanelSource).toContain('Recommended next step');
expect(setupCompletionPanelSource).toContain('aria-label="Setup next step"');
expect(setupCompletionPanelSource).toContain('Go to Dashboard');
expect(setupCompletionPanelSource).toContain('Open Infrastructure');
expect(setupCompletionModelSource).toContain("heroTitle: 'First monitored system connected'");
expect(setupCompletionModelSource).toContain(
"heroTitle: 'Choose your first infrastructure source'",
);
expect(setupCompletionPanelSource).toContain(
"completionViewModel().primaryAction === 'dashboard'",
"completionViewModel().primaryAction === 'infrastructure'",
);
expect(setupCompletionPanelSource).toContain(
'completionViewModel().showAddInfrastructureAction',
@@ -176,7 +176,7 @@ describe('SetupCompletionPanel', () => {
expect(screen.queryByText('password')).not.toBeInTheDocument();
});
it('still shows connected systems from polled resources and allows dashboard handoff', async () => {
it('still shows connected systems from polled resources and allows Infrastructure handoff', async () => {
const onComplete = vi.fn();
apiFetchJSONMock.mockResolvedValue({
resources: [
@@ -199,18 +199,18 @@ describe('SetupCompletionPanel', () => {
expect(screen.getByText('First monitored system connected')).toBeInTheDocument();
expect(
screen.getByText(
'Your admin account is ready and Pulse is already receiving telemetry. Open the dashboard to verify the first overview, then return to Add infrastructure when you want another Pulse Agent or platform API source.',
'Your admin account is ready and Pulse is already receiving telemetry. Open Infrastructure to inspect the first system, then return to Add infrastructure when you want another Pulse Agent or platform API source.',
),
).toBeInTheDocument();
expect(
screen.getByText('Open the dashboard to review your first connected system.'),
screen.getByText('Open Infrastructure to review your first connected system.'),
).toBeInTheDocument();
expect(
screen.getByText(
'Add infrastructure stays available for more Pulse Agent systems or platform API inventory when a platform manages the estate.',
),
).toBeInTheDocument();
expect(screen.getAllByRole('button', { name: 'Go to Dashboard' }).length).toBeGreaterThan(0);
expect(screen.getAllByRole('button', { name: 'Open Infrastructure' }).length).toBeGreaterThan(0);
expect(screen.getAllByRole('button', { name: 'Add infrastructure' }).length).toBeGreaterThan(0);
expect(screen.queryByRole('button', { name: 'Install Pulse Agent' })).not.toBeInTheDocument();
expect(trackAgentFirstConnectedMock).toHaveBeenCalledWith(
@@ -218,12 +218,12 @@ describe('SetupCompletionPanel', () => {
'first_agent',
);
const nextStepHeading = screen.getByRole('heading', { name: 'Open your first dashboard view' });
const nextStepHeading = screen.getByRole('heading', { name: 'Open Infrastructure' });
const nextStepCard = nextStepHeading.closest('[aria-label="Setup next step"]');
expect(nextStepCard).not.toBeNull();
fireEvent.click(
within(nextStepCard as HTMLElement).getByRole('button', { name: 'Go to Dashboard' }),
within(nextStepCard as HTMLElement).getByRole('button', { name: 'Open Infrastructure' }),
);
expect(onComplete).toHaveBeenCalledWith('/');
});
@@ -260,7 +260,7 @@ describe('SetupCompletionPanel', () => {
expect(screen.getByText('First monitored system connected')).toBeInTheDocument();
expect(
screen.getByText(
'Your admin account is ready and Pulse is already receiving telemetry. Open the dashboard to verify the first overview, then return to Add infrastructure when you want another platform API or Pulse Agent source.',
'Your admin account is ready and Pulse is already receiving telemetry. Open Infrastructure to inspect the first system, then return to Add infrastructure when you want another platform API or Pulse Agent source.',
),
).toBeInTheDocument();
expect(
@@ -268,12 +268,12 @@ describe('SetupCompletionPanel', () => {
'Add infrastructure stays available for more API-backed systems or Pulse Agent telemetry when a system needs node-local coverage.',
),
).toBeInTheDocument();
expect(screen.getByRole('button', { name: 'Go to Dashboard' })).toBeInTheDocument();
expect(screen.getByRole('button', { name: 'Open Infrastructure' })).toBeInTheDocument();
expect(screen.getByRole('button', { name: 'Add infrastructure' })).toBeInTheDocument();
expect(screen.queryByRole('button', { name: 'Install Pulse Agent' })).not.toBeInTheDocument();
expect(trackAgentFirstConnectedMock).not.toHaveBeenCalled();
const nextStepHeading = screen.getByRole('heading', { name: 'Open your first dashboard view' });
const nextStepHeading = screen.getByRole('heading', { name: 'Open Infrastructure' });
const nextStepCard = nextStepHeading.closest('[aria-label="Setup next step"]');
expect(nextStepCard).not.toBeNull();
@@ -316,7 +316,7 @@ describe('SetupCompletionPanel', () => {
expect(screen.getByText('VMware vSphere')).toBeInTheDocument();
expect(
screen.getByText(
'Your admin account is ready and Pulse is already receiving telemetry. Open the dashboard to verify the first overview, then return to Add infrastructure when you want another platform API or Pulse Agent source.',
'Your admin account is ready and Pulse is already receiving telemetry. Open Infrastructure to inspect the first system, then return to Add infrastructure when you want another platform API or Pulse Agent source.',
),
).toBeInTheDocument();
expect(screen.getByRole('button', { name: 'Add infrastructure' })).toBeInTheDocument();
@@ -365,7 +365,7 @@ describe('SetupCompletionPanel', () => {
expect(
screen.getByText(
'Your admin account is ready and Pulse is already receiving telemetry. Open the dashboard to verify the first overview, then return to Add infrastructure when you want another platform API or Agent source.',
'Your admin account is ready and Pulse is already receiving telemetry. Open Infrastructure to inspect the first system, then return to Add infrastructure when you want another platform API or Agent source.',
),
).toBeInTheDocument();
expect(
@@ -15,7 +15,7 @@ import {
} from '@/utils/resourceIdentity';
export type SetupCompletionConnectionPath = 'agent' | 'api';
export type SetupCompletionPrimaryAction = 'dashboard' | 'sources';
export type SetupCompletionPrimaryAction = 'infrastructure' | 'sources';
export interface ConnectedSetupSystem {
id: string;
@@ -149,7 +149,7 @@ const buildConnectedHeroDescription = (
hasApiConnectedSystems: boolean,
): string => {
const prefix =
'Your admin account is ready and Pulse is already receiving telemetry. Open the dashboard to verify the first overview';
'Your admin account is ready and Pulse is already receiving telemetry. Open Infrastructure to inspect the first system';
if (hasAgentConnectedSystems && hasApiConnectedSystems) {
return `${prefix}, then return to Add infrastructure when you want another platform API or Agent source.`;
@@ -164,7 +164,7 @@ const buildCredentialsContinuationText = (
_hasAgentConnectedSystems: boolean,
_hasApiConnectedSystems: boolean,
): string => {
return 'the dashboard or Add infrastructure.';
return 'Infrastructure or Add infrastructure.';
};
const buildConnectedNextStepDetail = (
@@ -192,8 +192,8 @@ export function buildSetupCompletionViewModel(
const connectedSystemNoun = connectedSystems.length === 1 ? 'system' : 'systems';
const nextStepSummary =
connectedSystems.length === 1
? 'Open the dashboard to review your first connected system.'
: 'Open the dashboard to review your connected systems.';
? 'Open Infrastructure to review your first connected system.'
: 'Open Infrastructure to review your connected systems.';
if (!hasConnectedSystems) {
return {
@@ -231,8 +231,8 @@ export function buildSetupCompletionViewModel(
heroTitle: 'First monitored system connected',
nextStepDetail: buildConnectedNextStepDetail(hasAgentConnectedSystems, hasApiConnectedSystems),
nextStepSummary,
nextStepTitle: 'Open your first dashboard view',
primaryAction: 'dashboard',
nextStepTitle: 'Open Infrastructure',
primaryAction: 'infrastructure',
showAddInfrastructureAction: hasConnectedSystems,
showAgentInstallAction: false,
};
@@ -117,7 +117,7 @@ export const SecurityStep: Component<SecurityStepProps> = (props) => {
<div class="max-w-lg mx-auto bg-surface border border-border overflow-hidden animate-fade-in relative rounded-md">
{' '}
<div class="p-8 border-b border-border relative z-10 text-center">
<h2 class="text-3xl font-bold tracking-tight text-base-content">Secure Your Dashboard</h2>
<h2 class="text-3xl font-bold tracking-tight text-base-content">Secure Pulse</h2>
<p class="text-slate-500 dark:text-blue-200 mt-2 font-light">
Create your root administrator account, then choose the first infrastructure source.
</p>
@@ -1,86 +0,0 @@
import { Match, Switch } from 'solid-js';
import { ProgressBar } from '@/components/shared/ProgressBar';
import { Card } from '@/components/shared/Card';
import { buildStoragePath } from '@/routing/resourceLinks';
import {
formatDashboardDelta,
formatDashboardPercent,
getDashboardDeltaColorClass,
} from '@/utils/dashboardMetricPresentation';
import { formatBytes } from '@/utils/format';
import type { DashboardOverview } from '@/hooks/useDashboardOverview';
import type { TrendData } from '@/hooks/useDashboardTrends';
import {
computeDashboardStorageCapacityPercent,
DASHBOARD_STORAGE_EMPTY_STATE,
getDashboardStorageCapacityBarClass,
getDashboardStorageIssueBadges,
} from '@/utils/dashboardStoragePresentation';
interface DashboardStoragePanelProps {
storage: DashboardOverview['storage'];
storageTrend: TrendData | null;
loading: boolean;
}
export function DashboardStoragePanel(props: DashboardStoragePanelProps) {
const capacityPercent = () =>
computeDashboardStorageCapacityPercent(props.storage.totalUsed, props.storage.totalCapacity);
const hasTrend = () => !!props.storageTrend && props.storageTrend.points.length >= 2;
return (
<Card padding="none" class="px-4 py-3.5" aria-busy={props.loading}>
<div class="flex items-center justify-between gap-3">
<h2 class="text-sm font-semibold text-base-content">
Storage <span class="font-normal text-xs text-muted">({props.storage.total} pools)</span>
</h2>
<a
href={buildStoragePath()}
aria-label="View all storage"
class="text-xs font-medium text-blue-600 hover:text-blue-700 dark:text-blue-400 dark:hover:text-blue-300"
>
View all →
</a>
</div>
<Switch>
<Match when={props.storage.total === 0}>
<p class="text-xs text-muted mt-1">{DASHBOARD_STORAGE_EMPTY_STATE}</p>
</Match>
<Match when={props.storage.total > 0}>
<div class="mt-1.5 space-y-1.5">
<div class="flex items-center justify-between gap-3">
<p class="text-xs text-muted">
{formatBytes(props.storage.totalUsed)} / {formatBytes(props.storage.totalCapacity)}
</p>
<span class="text-xs font-mono font-semibold text-base-content">
{formatDashboardPercent(capacityPercent())}
</span>
</div>
<ProgressBar
value={capacityPercent()}
class="h-2"
fillClass={getDashboardStorageCapacityBarClass(capacityPercent())}
/>
<div class="flex flex-wrap items-center gap-3 text-xs">
{getDashboardStorageIssueBadges(props.storage).map((badge) => (
<span class={badge.className}>{badge.label}</span>
))}
{hasTrend() && (
<span
class={`font-mono font-medium ${getDashboardDeltaColorClass(props.storageTrend?.delta ?? null)}`}
>
24h: {formatDashboardDelta(props.storageTrend?.delta ?? null) ?? '—'}
</span>
)}
</div>
</div>
</Match>
</Switch>
</Card>
);
}
export default DashboardStoragePanel;
@@ -1,67 +0,0 @@
import { render, screen } from '@solidjs/testing-library';
import { describe, expect, it } from 'vitest';
import { DashboardStoragePanel } from '@/components/Storage/DashboardStoragePanel';
import type { DashboardOverview } from '@/hooks/useDashboardOverview';
import type { TrendData } from '@/hooks/useDashboardTrends';
describe('DashboardStoragePanel', () => {
it('renders the empty state when no storage resources exist', () => {
const storage: DashboardOverview['storage'] = {
total: 0,
totalCapacity: 0,
totalUsed: 0,
warningCount: 0,
criticalCount: 0,
};
render(() => <DashboardStoragePanel storage={storage} storageTrend={null} loading={false} />);
expect(screen.getByText('No storage resources')).toBeInTheDocument();
});
it('renders storage usage, issue badges, and the 24h delta', () => {
const storage: DashboardOverview['storage'] = {
total: 2,
totalCapacity: 4000,
totalUsed: 2000,
warningCount: 1,
criticalCount: 1,
};
const storageTrend: TrendData = {
points: [
{ timestamp: 1, value: 40 },
{ timestamp: 2, value: 50 },
],
delta: 10,
currentValue: 50,
};
render(() => (
<DashboardStoragePanel storage={storage} storageTrend={storageTrend} loading={false} />
));
expect(screen.getByRole('heading', { name: /Storage/i })).toBeInTheDocument();
expect(screen.getByText('50%')).toBeInTheDocument();
expect(screen.getByText('1 warnings')).toBeInTheDocument();
expect(screen.getByText('1 critical')).toBeInTheDocument();
expect(screen.getByText('24h: +10.0%')).toBeInTheDocument();
});
it('renders the capacity bar through the shared progress primitive without inline styles', () => {
const storage: DashboardOverview['storage'] = {
total: 1,
totalCapacity: 4000,
totalUsed: 1540,
warningCount: 0,
criticalCount: 0,
};
const result = render(() => (
<DashboardStoragePanel storage={storage} storageTrend={null} loading={false} />
));
const fill = result.container.querySelector('[data-progress-fill="true"]');
expect(fill).toHaveAttribute('width', '38.5');
expect(result.container.querySelector('[style]')).toBeNull();
});
});
@@ -16,14 +16,14 @@ import {
import { StatusDot } from '@/components/shared/StatusDot';
import { TagBadges } from '@/components/shared/TagBadges';
import { resolveWorkloadType } from '@/utils/workloads';
import { EnhancedCPUBar } from '@/components/Dashboard/EnhancedCPUBar';
import { EnhancedCPUBar } from '@/components/Workloads/EnhancedCPUBar';
import { UpdateButton } from '@/components/shared/ContainerUpdateBadge';
import {
buildSummaryDisclosureControlsId,
createSummaryInteractiveRowPreviewHandlers,
} from '@/components/shared/summaryInteractionA11y';
import { SummaryRowActionButton } from '@/components/shared/SummaryRowActionButton';
import { getDashboardGuestDiskStatusMessage } from '@/utils/dashboardGuestPresentation';
import { getWorkloadGuestDiskStatusMessage } from '@/utils/workloadGuestPresentation';
import type { GuestRowProps } from './guestRowModel';
import { useGuestRowState } from './useGuestRowState';
@@ -95,7 +95,7 @@ export function GuestRow(props: GuestRowProps) {
if (!isVM(props.guest)) return 'Disk stats unavailable';
const vm = props.guest as VM;
return getDashboardGuestDiskStatusMessage(vm.diskStatusReason);
return getWorkloadGuestDiskStatusMessage(vm.diskStatusReason);
};
const interactiveRowHandlers =
props.onClick || props.onHoverChange
@@ -6,10 +6,10 @@ import { useAlertsActivation } from '@/stores/alertsActivation';
import type { GuestNetworkInterface } from '@/types/api';
import { formatBytes, getBackupInfo } from '@/utils/format';
import {
getDashboardGuestBackupStatusPresentation,
getDashboardGuestBackupTooltip,
getDashboardGuestNetworkEmptyState,
} from '@/utils/dashboardGuestPresentation';
getWorkloadsGuestBackupStatusPresentation,
getWorkloadsGuestBackupTooltip,
getWorkloadsGuestNetworkEmptyState,
} from '@/utils/workloadGuestPresentation';
function BackupIndicator(props: {
lastBackup: string | number | null | undefined;
@@ -21,7 +21,7 @@ function BackupIndicator(props: {
const backupInfo = createMemo(() =>
getBackupInfo(props.lastBackup, alertsActivation.getBackupThresholds()),
);
const config = createMemo(() => getDashboardGuestBackupStatusPresentation(backupInfo().status));
const config = createMemo(() => getWorkloadsGuestBackupStatusPresentation(backupInfo().status));
const shouldShow = createMemo(() => {
const status = backupInfo().status;
@@ -30,7 +30,7 @@ function BackupIndicator(props: {
const tooltipText = createMemo(() => {
const info = backupInfo();
return getDashboardGuestBackupTooltip(info.status, info.ageFormatted);
return getWorkloadsGuestBackupTooltip(info.status, info.ageFormatted);
});
return (
@@ -139,7 +139,7 @@ function NetworkInfoCell(props: {
</Show>
<Show when={!iface.addresses || iface.addresses.length === 0}>
<span class="text-slate-500 text-[9px]">
{getDashboardGuestNetworkEmptyState()}
{getWorkloadsGuestNetworkEmptyState()}
</span>
</Show>
<Show when={(iface.rxBytes || 0) > 0 || (iface.txBytes || 0) > 0}>
@@ -275,7 +275,7 @@ function BackupStatusCell(props: { lastBackup: string | number | null | undefine
const info = createMemo(() =>
getBackupInfo(props.lastBackup, alertsActivation.getBackupThresholds()),
);
const config = createMemo(() => getDashboardGuestBackupStatusPresentation(info().status));
const config = createMemo(() => getWorkloadsGuestBackupStatusPresentation(info().status));
return (
<>
@@ -21,10 +21,10 @@ import {
import { GuestDrawer } from './GuestDrawer';
import { GuestRow } from './GuestRow';
import { buildWorkloadSummaryGroupScope } from './workloadSelectors';
import type { DashboardState } from './useDashboardState';
import type { WorkloadsState } from './useWorkloadsState';
type WorkloadPanelProps = Pick<
DashboardState,
WorkloadsState,
| 'activeAlerts'
| 'alertsEnabled'
| 'bottomSpacerHeight'
@@ -3,10 +3,10 @@ import { For } from 'solid-js';
import { TableHead, TableHeader, TableRow } from '@/components/shared/Table';
import { getGuestColumnStyle } from './guestRowModel';
import type { DashboardState, WorkloadSortKey } from './useDashboardState';
import type { WorkloadsState, WorkloadSortKey } from './useWorkloadsState';
type WorkloadTableHeaderProps = Pick<
DashboardState,
WorkloadsState,
| 'handleSort'
| 'isMobile'
| 'sortDirection'
@@ -5,11 +5,11 @@ import { GroupedTableModeSegmentedControl } from '@/components/shared/GroupedTab
import { PageControls } from '@/components/shared/PageControls';
import { SearchInput } from '@/components/shared/SearchInput';
import { STORAGE_KEYS } from '@/utils/localStorage';
import type { DashboardFilterProps } from './dashboardFilterModel';
import { DASHBOARD_WORKLOAD_TYPE_OPTIONS } from './dashboardWorkloadFilterConfigModel';
import { useDashboardFilterState } from './useDashboardFilterState';
import type { WorkloadsFilterProps } from './workloadsFilterModel';
import { WORKLOAD_TYPE_OPTIONS } from './workloadFilterConfigModel';
import { useWorkloadsFilterState } from './useWorkloadsFilterState';
export const DashboardFilter: Component<DashboardFilterProps> = (props) => {
export const WorkloadsFilter: Component<WorkloadsFilterProps> = (props) => {
const {
activeFilterCount,
isMobile,
@@ -18,10 +18,10 @@ export const DashboardFilter: Component<DashboardFilterProps> = (props) => {
showReset,
showToolbarFilters,
toggleFilters,
} = useDashboardFilterState(props);
} = useWorkloadsFilterState(props);
return (
<Card class="dashboard-filter mb-4" padding="sm">
<Card class="workloads-filter mb-4" padding="sm">
<PageControls
search={
<SearchInput
@@ -31,7 +31,7 @@ export const DashboardFilter: Component<DashboardFilterProps> = (props) => {
class="w-full"
onBeforeAutoFocus={props.onBeforeAutoFocus}
typeToSearch
history={{ storageKey: STORAGE_KEYS.DASHBOARD_SEARCH_HISTORY }}
history={{ storageKey: STORAGE_KEYS.WORKLOADS_SEARCH_HISTORY }}
/>
}
searchTrailing={props.searchTrailing}
@@ -68,7 +68,7 @@ export const DashboardFilter: Component<DashboardFilterProps> = (props) => {
<Show when={props.hostFilter}>
{(hostFilter) => (
<LabeledFilterSelect
id={hostFilter().id ?? 'dashboard-host-filter'}
id={hostFilter().id ?? 'workloads-host-filter'}
label={hostFilter().label ?? 'Agent'}
value={hostFilter().value}
onChange={(e) => hostFilter().onChange(e.currentTarget.value)}
@@ -84,7 +84,7 @@ export const DashboardFilter: Component<DashboardFilterProps> = (props) => {
<Show when={props.platformFilter}>
{(platformFilter) => (
<LabeledFilterSelect
id={platformFilter().id ?? 'dashboard-platform-filter'}
id={platformFilter().id ?? 'workloads-platform-filter'}
label={platformFilter().label ?? 'Platform'}
value={platformFilter().value}
onChange={(e) => platformFilter().onChange(e.currentTarget.value)}
@@ -100,7 +100,7 @@ export const DashboardFilter: Component<DashboardFilterProps> = (props) => {
<Show when={props.namespaceFilter}>
{(namespaceFilter) => (
<LabeledFilterSelect
id={namespaceFilter().id ?? 'dashboard-namespace-filter'}
id={namespaceFilter().id ?? 'workloads-namespace-filter'}
label={namespaceFilter().label ?? 'Namespace'}
value={namespaceFilter().value}
onChange={(e) => namespaceFilter().onChange(e.currentTarget.value)}
@@ -118,7 +118,7 @@ export const DashboardFilter: Component<DashboardFilterProps> = (props) => {
>
{(runtimeFilter) => (
<LabeledFilterSelect
id={runtimeFilter().id ?? 'dashboard-runtime-filter'}
id={runtimeFilter().id ?? 'workloads-runtime-filter'}
label={runtimeFilter().label ?? 'Runtime'}
value={runtimeFilter().value}
onChange={(e) => runtimeFilter().onChange(e.currentTarget.value)}
@@ -132,7 +132,7 @@ export const DashboardFilter: Component<DashboardFilterProps> = (props) => {
</Show>
<LabeledFilterSelect
id="dashboard-type-filter"
id="workloads-type-filter"
label="Type"
value={props.viewMode()}
onChange={(event) =>
@@ -147,13 +147,13 @@ export const DashboardFilter: Component<DashboardFilterProps> = (props) => {
}
selectClass="min-w-[7rem]"
>
<For each={DASHBOARD_WORKLOAD_TYPE_OPTIONS}>
<For each={WORKLOAD_TYPE_OPTIONS}>
{(option) => <option value={option.value}>{option.label}</option>}
</For>
</LabeledFilterSelect>
<LabeledFilterSelect
id="dashboard-status-filter"
id="workloads-status-filter"
label="Status"
value={props.statusMode()}
onChange={(event) =>
@@ -4,16 +4,16 @@ import { buildInfrastructureOnboardingPath } from '@/components/Settings/infrast
import { Card } from '@/components/shared/Card';
import { EmptyState } from '@/components/shared/EmptyState';
import type { DashboardState } from './useDashboardState';
import type { WorkloadsState } from './useWorkloadsState';
type DashboardStateCardsProps = Pick<
DashboardState,
type WorkloadsStateCardsProps = Pick<
WorkloadsState,
| 'allGuests'
| 'connected'
| 'dashboardDisconnectedState'
| 'dashboardGuestsEmptyState'
| 'dashboardInfrastructureEmptyState'
| 'dashboardLoadingState'
| 'workloadsDisconnectedState'
| 'workloadsGuestsEmptyState'
| 'workloadsInfrastructureEmptyState'
| 'workloadsLoadingState'
| 'filteredGuests'
| 'initialDataReceived'
| 'kioskMode'
@@ -24,7 +24,7 @@ type DashboardStateCardsProps = Pick<
nodeCount: number;
};
export function DashboardStateCards(props: DashboardStateCardsProps) {
export function WorkloadsStateCards(props: WorkloadsStateCardsProps) {
return (
<>
<Show when={props.connected() && !props.initialDataReceived()}>
@@ -51,8 +51,8 @@ export function DashboardStateCards(props: DashboardStateCardsProps) {
/>
</svg>
}
title={props.dashboardLoadingState().title}
description={props.dashboardLoadingState().description}
title={props.workloadsLoadingState().title}
description={props.workloadsLoadingState().description}
/>
</Card>
</Show>
@@ -83,8 +83,8 @@ export function DashboardStateCards(props: DashboardStateCardsProps) {
/>
</svg>
}
title={props.dashboardInfrastructureEmptyState().title}
description={props.dashboardInfrastructureEmptyState().description}
title={props.workloadsInfrastructureEmptyState().title}
description={props.workloadsInfrastructureEmptyState().description}
actions={
!props.kioskMode() ? (
<button
@@ -92,7 +92,7 @@ export function DashboardStateCards(props: DashboardStateCardsProps) {
onClick={() => props.navigate(buildInfrastructureOnboardingPath('pick'))}
class="inline-flex items-center px-3 py-1.5 border border-transparent text-xs font-medium rounded-md text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500"
>
{props.dashboardInfrastructureEmptyState().actionLabel}
{props.workloadsInfrastructureEmptyState().actionLabel}
</button>
) : undefined
}
@@ -118,16 +118,16 @@ export function DashboardStateCards(props: DashboardStateCardsProps) {
/>
</svg>
}
title={props.dashboardDisconnectedState().title}
description={props.dashboardDisconnectedState().description}
title={props.workloadsDisconnectedState().title}
description={props.workloadsDisconnectedState().description}
tone="danger"
actions={
props.dashboardDisconnectedState().actionLabel ? (
props.workloadsDisconnectedState().actionLabel ? (
<button
onClick={() => props.reconnect()}
class="mt-2 inline-flex items-center px-4 py-2 text-xs font-medium rounded bg-red-600 text-white hover:bg-red-700 transition-colors"
>
{props.dashboardDisconnectedState().actionLabel}
{props.workloadsDisconnectedState().actionLabel}
</button>
) : undefined
}
@@ -160,8 +160,8 @@ export function DashboardStateCards(props: DashboardStateCardsProps) {
/>
</svg>
}
title={props.dashboardGuestsEmptyState().title}
description={props.dashboardGuestsEmptyState().description}
title={props.workloadsGuestsEmptyState().title}
description={props.workloadsGuestsEmptyState().description}
/>
</Card>
</Show>
@@ -1,13 +1,13 @@
import { Show } from 'solid-js';
import type { DashboardState } from './useDashboardState';
import type { WorkloadsState } from './useWorkloadsState';
type DashboardStatsStripProps = Pick<
DashboardState,
type WorkloadsStatsStripProps = Pick<
WorkloadsState,
'connected' | 'initialDataReceived' | 'totalStats'
>;
export function DashboardStatsStrip(props: DashboardStatsStripProps) {
export function WorkloadsStatsStrip(props: WorkloadsStatsStripProps) {
return (
<Show when={props.connected() && props.initialDataReceived()}>
<div class="mb-4">
@@ -1,17 +1,17 @@
import { Show } from 'solid-js';
import { InfrastructureSelector } from '@/components/shared/InfrastructureSelector';
import { PageHeader } from '@/components/shared/PageHeader';
import { DashboardFilter } from './DashboardFilter';
import { WorkloadsFilter } from './WorkloadsFilter';
import { WorkloadsSummary } from '@/components/Workloads/WorkloadsSummary';
import { ScrollToTopButton } from '@/components/shared/ScrollToTopButton';
import { StickySummarySection } from '@/components/shared/StickySummarySection';
import { DashboardStateCards } from './DashboardStateCards';
import { DashboardStatsStrip } from './DashboardStatsStrip';
import { DashboardWorkloadTable } from './DashboardWorkloadTable';
import { useDashboardState, type DashboardProps } from './useDashboardState';
import { WorkloadsStateCards } from './WorkloadsStateCards';
import { WorkloadsStatsStrip } from './WorkloadsStatsStrip';
import { WorkloadsTable } from './WorkloadsTable';
import { useWorkloadsState, type WorkloadsSurfaceProps } from './useWorkloadsState';
export function Dashboard(props: DashboardProps) {
const state = useDashboardState(props);
export function WorkloadsSurface(props: WorkloadsSurfaceProps) {
const state = useWorkloadsState(props);
return (
<div
@@ -46,7 +46,7 @@ export function Dashboard(props: DashboardProps) {
</Show>
<InfrastructureSelector
currentTab="dashboard"
currentTab="workloads"
globalTemperatureMonitoringEnabled={state.ws.state.temperatureMonitoringEnabled}
onNodeSelect={state.handleNodeSelect}
nodes={props.nodes}
@@ -54,13 +54,13 @@ export function Dashboard(props: DashboardProps) {
showNodeSummary={!state.isWorkloadsRoute()}
/>
<DashboardStateCards
<WorkloadsStateCards
allGuests={state.allGuests}
connected={state.surfaceConnected}
dashboardDisconnectedState={state.dashboardDisconnectedState}
dashboardGuestsEmptyState={state.dashboardGuestsEmptyState}
dashboardInfrastructureEmptyState={state.dashboardInfrastructureEmptyState}
dashboardLoadingState={state.dashboardLoadingState}
workloadsDisconnectedState={state.workloadsDisconnectedState}
workloadsGuestsEmptyState={state.workloadsGuestsEmptyState}
workloadsInfrastructureEmptyState={state.workloadsInfrastructureEmptyState}
workloadsLoadingState={state.workloadsLoadingState}
filteredGuests={state.filteredGuests}
initialDataReceived={state.surfaceInitialDataReceived}
kioskMode={state.kioskMode}
@@ -80,7 +80,7 @@ export function Dashboard(props: DashboardProps) {
}
>
<div data-summary-clear-ignore>
<DashboardFilter
<WorkloadsFilter
search={state.search}
setSearch={state.setSearch}
viewMode={state.viewMode}
@@ -92,7 +92,7 @@ export function Dashboard(props: DashboardProps) {
setSortKey={state.setSortKey}
setSortDirection={state.setSortDirection}
onBeforeAutoFocus={state.handleBeforeAutoFocus}
columnVisibility={state.dashboardFilterColumnVisibility()}
columnVisibility={state.workloadsFilterColumnVisibility()}
chartsCollapsed={state.isWorkloadsRoute() ? state.workloadsSummaryCollapsed : undefined}
onChartsToggle={
state.isWorkloadsRoute()
@@ -114,7 +114,7 @@ export function Dashboard(props: DashboardProps) {
state.filteredGuests().length > 0
}
>
<DashboardWorkloadTable
<WorkloadsTable
activeAlerts={state.activeAlerts}
alertsEnabled={state.alertsEnabled}
bottomSpacerHeight={state.bottomSpacerHeight}
@@ -159,7 +159,7 @@ export function Dashboard(props: DashboardProps) {
</Show>
</div>
<DashboardStatsStrip
<WorkloadsStatsStrip
connected={state.surfaceConnected}
initialDataReceived={state.surfaceInitialDataReceived}
totalStats={state.totalStats}
@@ -6,12 +6,12 @@ import { Table } from '@/components/shared/Table';
import { TableCard } from '@/components/shared/TableCard';
import { getGuestColumnWidthStyle } from './guestRowModel';
import type { DashboardState } from './useDashboardState';
import type { WorkloadsState } from './useWorkloadsState';
import { WorkloadPanel } from './WorkloadPanel';
import { WorkloadTableHeader } from './WorkloadTableHeader';
type DashboardWorkloadTableProps = Pick<
DashboardState,
type WorkloadsTableProps = Pick<
WorkloadsState,
| 'activeAlerts'
| 'alertsEnabled'
| 'bottomSpacerHeight'
@@ -54,7 +54,7 @@ type DashboardWorkloadTableProps = Pick<
| 'workloadTableVisibleColumns'
>;
export function DashboardWorkloadTable(props: DashboardWorkloadTableProps) {
export function WorkloadsTable(props: WorkloadsTableProps) {
const showClearSelection = () =>
Boolean(props.selectedGuestId() || props.focusedSummaryWorkloadGroupId());
@@ -2,8 +2,8 @@ import { describe, it, expect, afterEach } from 'vitest';
import { render, screen, cleanup } from '@solidjs/testing-library';
import { DiskList } from '../DiskList';
import {
buildDashboardDiskPresentation,
getDashboardDiskUsagePercent,
buildWorkloadsDiskPresentation,
getWorkloadsDiskUsagePercent,
} from '../diskListModel';
import type { Disk } from '@/types/api';
@@ -135,7 +135,7 @@ describe('DiskList', () => {
describe('usage calculation', () => {
it('derives usage percent through the canonical disk-list model', () => {
const disk = makeDisk({ used: 26843545600, total: 107374182400 });
expect(getDashboardDiskUsagePercent(disk)).toBe(25);
expect(getWorkloadsDiskUsagePercent(disk)).toBe(25);
});
it('calculates usage percent correctly', () => {
@@ -282,7 +282,7 @@ describe('DiskList', () => {
describe('disk type display', () => {
it('builds disk presentation through the canonical disk-list model', () => {
const presentation = buildDashboardDiskPresentation(
const presentation = buildWorkloadsDiskPresentation(
makeDisk({ mountpoint: undefined, device: undefined, type: 'ext4' }),
0,
);
@@ -66,7 +66,7 @@ vi.mock('@/components/shared/StatusDot', () => ({
),
}));
vi.mock('@/components/Dashboard/EnhancedCPUBar', () => ({
vi.mock('@/components/Workloads/EnhancedCPUBar', () => ({
EnhancedCPUBar: (props: { usage: number; cores?: number }) => (
<div data-testid="cpu-bar" data-usage={props.usage} data-cores={props.cores} />
),
@@ -1,11 +1,11 @@
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
import { render, screen, cleanup, fireEvent } from '@solidjs/testing-library';
import { createSignal } from 'solid-js';
import { DashboardFilter } from '../DashboardFilter';
import { WorkloadsFilter } from '../WorkloadsFilter';
import {
DEFAULT_DASHBOARD_SORT_KEY,
type DashboardFilterProps,
} from '../dashboardFilterModel';
DEFAULT_WORKLOADS_SORT_KEY,
type WorkloadsFilterProps,
} from '../workloadsFilterModel';
// Mock useBreakpoint — hoist so tests can control isMobile
const { isMobileMock } = vi.hoisted(() => {
@@ -42,7 +42,7 @@ vi.mock('@/components/shared/ColumnPicker', () => ({
}));
/** Helper to build default required props, with optional overrides. */
function makeProps(overrides: Partial<DashboardFilterProps> = {}): DashboardFilterProps {
function makeProps(overrides: Partial<WorkloadsFilterProps> = {}): WorkloadsFilterProps {
return {
search: vi.fn(() => ''),
setSearch: vi.fn(),
@@ -58,7 +58,7 @@ function makeProps(overrides: Partial<DashboardFilterProps> = {}): DashboardFilt
};
}
describe('DashboardFilter', () => {
describe('WorkloadsFilter', () => {
beforeEach(() => {
vi.clearAllMocks();
isMobileMock.mockReturnValue(false);
@@ -73,13 +73,13 @@ describe('DashboardFilter', () => {
describe('rendering', () => {
it('renders the search input', () => {
const props = makeProps();
render(() => <DashboardFilter {...props} />);
render(() => <WorkloadsFilter {...props} />);
expect(screen.getByTestId('search-input')).toBeInTheDocument();
});
it('renders the Type filter select with all options', () => {
const props = makeProps();
render(() => <DashboardFilter {...props} />);
render(() => <WorkloadsFilter {...props} />);
const typeSelect = screen.getByLabelText('Type');
expect(typeSelect).toBeInTheDocument();
expect(typeSelect).toHaveValue('all');
@@ -93,7 +93,7 @@ describe('DashboardFilter', () => {
it('renders the Status filter select with all options', () => {
const props = makeProps();
render(() => <DashboardFilter {...props} />);
render(() => <WorkloadsFilter {...props} />);
const statusSelect = screen.getByLabelText('Status');
expect(statusSelect).toBeInTheDocument();
expect(statusSelect).toHaveValue('all');
@@ -105,7 +105,7 @@ describe('DashboardFilter', () => {
it('renders Grouped and List buttons', () => {
const props = makeProps();
render(() => <DashboardFilter {...props} />);
render(() => <WorkloadsFilter {...props} />);
expect(screen.getByLabelText('Group by')).toBeInTheDocument();
expect(screen.getByText('Grouped')).toBeInTheDocument();
expect(screen.getByText('List')).toBeInTheDocument();
@@ -140,11 +140,11 @@ describe('DashboardFilter', () => {
chartsCollapsed: vi.fn(() => false),
onChartsToggle: vi.fn(),
});
const { container } = render(() => <DashboardFilter {...props} />);
const { container } = render(() => <WorkloadsFilter {...props} />);
expect(screen.getByTestId('column-picker')).toBeInTheDocument();
expect(
Array.from(container.querySelectorAll('.dashboard-filter *')).some((element) =>
Array.from(container.querySelectorAll('.workloads-filter *')).some((element) =>
(element.getAttribute('class') ?? '').includes('lg:flex-nowrap'),
),
).toBe(false);
@@ -152,7 +152,7 @@ describe('DashboardFilter', () => {
it('does not show the Reset button when all filters are at defaults', () => {
const props = makeProps();
render(() => <DashboardFilter {...props} />);
render(() => <WorkloadsFilter {...props} />);
expect(screen.queryByText('Reset')).not.toBeInTheDocument();
});
});
@@ -162,7 +162,7 @@ describe('DashboardFilter', () => {
describe('type filter', () => {
it('calls setViewMode when a different type is selected', () => {
const props = makeProps();
render(() => <DashboardFilter {...props} />);
render(() => <WorkloadsFilter {...props} />);
const typeSelect = screen.getByLabelText('Type');
fireEvent.change(typeSelect, { target: { value: 'vm' } });
expect(props.setViewMode).toHaveBeenCalledWith('vm');
@@ -174,7 +174,7 @@ describe('DashboardFilter', () => {
describe('status filter', () => {
it('calls setStatusMode when a different status is selected', () => {
const props = makeProps();
render(() => <DashboardFilter {...props} />);
render(() => <WorkloadsFilter {...props} />);
const statusSelect = screen.getByLabelText('Status');
fireEvent.change(statusSelect, { target: { value: 'running' } });
expect(props.setStatusMode).toHaveBeenCalledWith('running');
@@ -186,7 +186,7 @@ describe('DashboardFilter', () => {
describe('grouping mode', () => {
it('uses the canonical grouped/list table-mode titles', () => {
const props = makeProps();
render(() => <DashboardFilter {...props} />);
render(() => <WorkloadsFilter {...props} />);
expect(screen.getByRole('button', { name: 'Grouped' })).toHaveAttribute(
'title',
'Grouped table view',
@@ -199,14 +199,14 @@ describe('DashboardFilter', () => {
it('calls setGroupingMode("flat") when List button is clicked', () => {
const props = makeProps();
render(() => <DashboardFilter {...props} />);
render(() => <WorkloadsFilter {...props} />);
fireEvent.click(screen.getByText('List'));
expect(props.setGroupingMode).toHaveBeenCalledWith('flat');
});
it('calls setGroupingMode("grouped") when Grouped button is clicked', () => {
const props = makeProps({ groupingMode: vi.fn(() => 'flat' as const) });
render(() => <DashboardFilter {...props} />);
render(() => <WorkloadsFilter {...props} />);
fireEvent.click(screen.getByText('Grouped'));
expect(props.setGroupingMode).toHaveBeenCalledWith('grouped');
});
@@ -217,25 +217,25 @@ describe('DashboardFilter', () => {
describe('reset button', () => {
it('shows when search is non-empty', () => {
const props = makeProps({ search: vi.fn(() => 'hello') });
render(() => <DashboardFilter {...props} />);
render(() => <WorkloadsFilter {...props} />);
expect(screen.getByText('Reset')).toBeInTheDocument();
});
it('shows when viewMode is not "all"', () => {
const props = makeProps({ viewMode: vi.fn(() => 'vm' as const) });
render(() => <DashboardFilter {...props} />);
render(() => <WorkloadsFilter {...props} />);
expect(screen.getByText('Reset')).toBeInTheDocument();
});
it('shows when statusMode is not "all"', () => {
const props = makeProps({ statusMode: vi.fn(() => 'running' as const) });
render(() => <DashboardFilter {...props} />);
render(() => <WorkloadsFilter {...props} />);
expect(screen.getByText('Reset')).toBeInTheDocument();
});
it('shows when groupingMode is "flat"', () => {
const props = makeProps({ groupingMode: vi.fn(() => 'flat' as const) });
render(() => <DashboardFilter {...props} />);
render(() => <WorkloadsFilter {...props} />);
expect(screen.getByText('Reset')).toBeInTheDocument();
});
@@ -250,7 +250,7 @@ describe('DashboardFilter', () => {
onChange: vi.fn(),
},
});
render(() => <DashboardFilter {...props} />);
render(() => <WorkloadsFilter {...props} />);
expect(screen.getByText('Reset')).toBeInTheDocument();
});
@@ -288,12 +288,12 @@ describe('DashboardFilter', () => {
onChange: namespaceOnChange,
},
});
render(() => <DashboardFilter {...props} />);
render(() => <WorkloadsFilter {...props} />);
fireEvent.click(screen.getByText('Reset'));
expect(props.setSearch).toHaveBeenCalledWith('');
expect(props.setSortKey).toHaveBeenCalledWith(DEFAULT_DASHBOARD_SORT_KEY);
expect(props.setSortKey).toHaveBeenCalledWith(DEFAULT_WORKLOADS_SORT_KEY);
expect(props.setSortDirection).toHaveBeenCalledWith('asc');
expect(props.setViewMode).toHaveBeenCalledWith('all');
expect(props.setStatusMode).toHaveBeenCalledWith('all');
@@ -317,7 +317,7 @@ describe('DashboardFilter', () => {
onChange: runtimeOnChange,
},
});
render(() => <DashboardFilter {...props} />);
render(() => <WorkloadsFilter {...props} />);
fireEvent.click(screen.getByText('Reset'));
@@ -336,7 +336,7 @@ describe('DashboardFilter', () => {
onChange: vi.fn(),
},
});
render(() => <DashboardFilter {...props} />);
render(() => <WorkloadsFilter {...props} />);
expect(screen.getByText('Reset')).toBeInTheDocument();
});
});
@@ -355,13 +355,13 @@ describe('DashboardFilter', () => {
onChange: vi.fn(),
},
});
render(() => <DashboardFilter {...props} />);
render(() => <WorkloadsFilter {...props} />);
expect(screen.getByLabelText('Agent')).toBeInTheDocument();
});
it('does not render when hostFilter prop is absent', () => {
const props = makeProps();
render(() => <DashboardFilter {...props} />);
render(() => <WorkloadsFilter {...props} />);
expect(screen.queryByLabelText('Agent')).not.toBeInTheDocument();
});
@@ -377,7 +377,7 @@ describe('DashboardFilter', () => {
onChange,
},
});
render(() => <DashboardFilter {...props} />);
render(() => <WorkloadsFilter {...props} />);
fireEvent.change(screen.getByLabelText('Agent'), { target: { value: 'host-1' } });
expect(onChange).toHaveBeenCalledWith('host-1');
});
@@ -391,7 +391,7 @@ describe('DashboardFilter', () => {
onChange: vi.fn(),
},
});
render(() => <DashboardFilter {...props} />);
render(() => <WorkloadsFilter {...props} />);
expect(screen.getByText('Node')).toBeInTheDocument();
});
});
@@ -408,7 +408,7 @@ describe('DashboardFilter', () => {
onChange: vi.fn(),
},
});
render(() => <DashboardFilter {...props} />);
render(() => <WorkloadsFilter {...props} />);
expect(screen.getByLabelText('Platform')).toBeInTheDocument();
});
@@ -424,7 +424,7 @@ describe('DashboardFilter', () => {
onChange,
},
});
render(() => <DashboardFilter {...props} />);
render(() => <WorkloadsFilter {...props} />);
fireEvent.change(screen.getByLabelText('Platform'), { target: { value: 'truenas' } });
expect(onChange).toHaveBeenCalledWith('truenas');
});
@@ -436,7 +436,7 @@ describe('DashboardFilter', () => {
]);
render(() => (
<DashboardFilter
<WorkloadsFilter
{...baseProps}
platformFilter={{
value: 'truenas',
@@ -470,7 +470,7 @@ describe('DashboardFilter', () => {
onChange: vi.fn(),
},
});
render(() => <DashboardFilter {...props} />);
render(() => <WorkloadsFilter {...props} />);
expect(screen.getByLabelText('Namespace')).toBeInTheDocument();
});
@@ -486,7 +486,7 @@ describe('DashboardFilter', () => {
onChange,
},
});
render(() => <DashboardFilter {...props} />);
render(() => <WorkloadsFilter {...props} />);
fireEvent.change(screen.getByLabelText('Namespace'), { target: { value: 'kube-system' } });
expect(onChange).toHaveBeenCalledWith('kube-system');
});
@@ -507,7 +507,7 @@ describe('DashboardFilter', () => {
onChange: vi.fn(),
},
});
render(() => <DashboardFilter {...props} />);
render(() => <WorkloadsFilter {...props} />);
expect(screen.getByLabelText('Runtime')).toBeInTheDocument();
});
@@ -523,7 +523,7 @@ describe('DashboardFilter', () => {
onChange: vi.fn(),
},
});
render(() => <DashboardFilter {...props} />);
render(() => <WorkloadsFilter {...props} />);
expect(screen.queryByLabelText('Runtime')).not.toBeInTheDocument();
});
@@ -540,7 +540,7 @@ describe('DashboardFilter', () => {
onChange,
},
});
render(() => <DashboardFilter {...props} />);
render(() => <WorkloadsFilter {...props} />);
fireEvent.change(screen.getByLabelText('Runtime'), { target: { value: 'podman' } });
expect(onChange).toHaveBeenCalledWith('podman');
});
@@ -554,13 +554,13 @@ describe('DashboardFilter', () => {
chartsCollapsed: vi.fn(() => false),
onChartsToggle: vi.fn(),
});
render(() => <DashboardFilter {...props} />);
render(() => <WorkloadsFilter {...props} />);
expect(screen.getByText('Charts')).toBeInTheDocument();
});
it('does not render Charts button when onChartsToggle is not provided', () => {
const props = makeProps();
render(() => <DashboardFilter {...props} />);
render(() => <WorkloadsFilter {...props} />);
expect(screen.queryByText('Charts')).not.toBeInTheDocument();
});
@@ -570,7 +570,7 @@ describe('DashboardFilter', () => {
chartsCollapsed: vi.fn(() => false),
onChartsToggle,
});
render(() => <DashboardFilter {...props} />);
render(() => <WorkloadsFilter {...props} />);
fireEvent.click(screen.getByText('Charts'));
expect(onChartsToggle).toHaveBeenCalled();
});
@@ -587,13 +587,13 @@ describe('DashboardFilter', () => {
onColumnToggle: vi.fn(),
},
});
render(() => <DashboardFilter {...props} />);
render(() => <WorkloadsFilter {...props} />);
expect(screen.getByTestId('column-picker')).toBeInTheDocument();
});
it('does not render ColumnPicker when column props are missing', () => {
const props = makeProps();
render(() => <DashboardFilter {...props} />);
render(() => <WorkloadsFilter {...props} />);
expect(screen.queryByTestId('column-picker')).not.toBeInTheDocument();
});
});
@@ -604,14 +604,14 @@ describe('DashboardFilter', () => {
it('shows the Filters toggle button on mobile', () => {
isMobileMock.mockReturnValue(true);
const props = makeProps();
render(() => <DashboardFilter {...props} />);
render(() => <WorkloadsFilter {...props} />);
expect(screen.getByText('Filters')).toBeInTheDocument();
});
it('hides filter controls on mobile by default', () => {
isMobileMock.mockReturnValue(true);
const props = makeProps();
render(() => <DashboardFilter {...props} />);
render(() => <WorkloadsFilter {...props} />);
// The Type filter should not be visible until Filters is toggled
expect(screen.queryByLabelText('Type')).not.toBeInTheDocument();
});
@@ -619,7 +619,7 @@ describe('DashboardFilter', () => {
it('shows filter controls when Filters toggle is clicked on mobile', () => {
isMobileMock.mockReturnValue(true);
const props = makeProps();
render(() => <DashboardFilter {...props} />);
render(() => <WorkloadsFilter {...props} />);
fireEvent.click(screen.getByText('Filters'));
expect(screen.getByLabelText('Type')).toBeInTheDocument();
expect(screen.getByLabelText('Status')).toBeInTheDocument();
@@ -628,7 +628,7 @@ describe('DashboardFilter', () => {
it('hides filter controls when Filters toggle is clicked again', () => {
isMobileMock.mockReturnValue(true);
const props = makeProps();
render(() => <DashboardFilter {...props} />);
render(() => <WorkloadsFilter {...props} />);
const toggle = screen.getByText('Filters');
fireEvent.click(toggle);
expect(screen.getByLabelText('Type')).toBeInTheDocument();
@@ -642,7 +642,7 @@ describe('DashboardFilter', () => {
search: vi.fn(() => 'test'),
viewMode: vi.fn(() => 'vm' as const),
});
render(() => <DashboardFilter {...props} />);
render(() => <WorkloadsFilter {...props} />);
// The badge should show "2" (search + viewMode)
expect(screen.getByText('2')).toBeInTheDocument();
});
@@ -650,7 +650,7 @@ describe('DashboardFilter', () => {
it('does not show filter count badge when no filters are active', () => {
isMobileMock.mockReturnValue(true);
const props = makeProps();
render(() => <DashboardFilter {...props} />);
render(() => <WorkloadsFilter {...props} />);
// The badge is a span inside the Filters button — when count is 0, no badge renders.
// Verify no numeric badge appears near the Filters button.
const filtersButton = screen.getByText('Filters');
@@ -685,7 +685,7 @@ describe('DashboardFilter', () => {
onChange: vi.fn(),
},
});
render(() => <DashboardFilter {...props} />);
render(() => <WorkloadsFilter {...props} />);
// 5 active: search, viewMode, statusMode, host, namespace
expect(screen.getByText('5')).toBeInTheDocument();
});
@@ -703,14 +703,14 @@ describe('DashboardFilter', () => {
onChange: vi.fn(),
},
});
render(() => <DashboardFilter {...props} />);
render(() => <WorkloadsFilter {...props} />);
expect(screen.getByText('1')).toBeInTheDocument();
});
it('does not count whitespace-only search as active', () => {
isMobileMock.mockReturnValue(true);
const props = makeProps({ search: vi.fn(() => ' ') });
render(() => <DashboardFilter {...props} />);
render(() => <WorkloadsFilter {...props} />);
expect(screen.queryByText('1')).not.toBeInTheDocument();
});
});
@@ -1,32 +1,32 @@
import { fireEvent, render, screen } from '@solidjs/testing-library';
import { describe, expect, it, vi } from 'vitest';
import { DashboardStateCards } from '../DashboardStateCards';
import { WorkloadsStateCards } from '../WorkloadsStateCards';
describe('DashboardStateCards', () => {
describe('WorkloadsStateCards', () => {
it('routes the empty-state action directly to infrastructure setup', () => {
const navigate = vi.fn();
render(() => (
<DashboardStateCards
<WorkloadsStateCards
allGuests={() => []}
connected={() => true}
dashboardDisconnectedState={() => ({
workloadsDisconnectedState={() => ({
title: 'Connection lost',
description: 'Unable to connect to the backend server',
actionLabel: 'Reconnect now',
})}
dashboardGuestsEmptyState={() => ({
workloadsGuestsEmptyState={() => ({
title: 'No guests found',
description: 'No guests match your current filters',
})}
dashboardInfrastructureEmptyState={() => ({
workloadsInfrastructureEmptyState={() => ({
title: 'No infrastructure sources connected',
description:
'Start in Settings → Infrastructure by choosing a source strategy. Connect a platform API for inventory and health, install Pulse Agent for host telemetry, or use both when you want full coverage.',
actionLabel: 'Add infrastructure source',
})}
dashboardLoadingState={() => ({
title: 'Loading dashboard data...',
workloadsLoadingState={() => ({
title: 'Loading workloads...',
description: 'Connecting to monitoring service',
})}
filteredGuests={() => []}
@@ -1,7 +1,7 @@
import { beforeEach, describe, expect, it, vi } from 'vitest';
import { render, waitFor } from '@solidjs/testing-library';
import { createEffect } from 'solid-js';
import { Dashboard } from '../Dashboard';
import { WorkloadsSurface } from '../WorkloadsSurface';
import type { State } from '@/types/api';
const mockWebSocketState: State = {
@@ -127,13 +127,13 @@ vi.mock('@/components/shared/InfrastructureSelector', () => ({
),
}));
vi.mock('../DashboardFilter', () => ({
DashboardFilter: (props: { hostFilter?: HostFilterMock }) => {
vi.mock('../WorkloadsFilter', () => ({
WorkloadsFilter: (props: { hostFilter?: HostFilterMock }) => {
createEffect(() => {
lastHostFilter = props.hostFilter;
});
return (
<div data-testid="dashboard-filter">
<div data-testid="workloads-filter">
{props.hostFilter ? 'host-filter-enabled' : 'host-filter-disabled'}
</div>
);
@@ -171,7 +171,7 @@ vi.mock('../GuestRow', () => {
};
});
describe('Dashboard pod workloads integration', () => {
describe('Workloads pod workloads integration', () => {
beforeEach(() => {
navigateSpy.mockReset();
localStorage.clear();
@@ -209,14 +209,14 @@ describe('Dashboard pod workloads integration', () => {
];
mockLocationSearch = '?type=pod';
const { getByText, getByTestId } = render(() => (
<Dashboard vms={[]} containers={[]} nodes={[]} useWorkloads />
<WorkloadsSurface vms={[]} containers={[]} nodes={[]} useWorkloads />
));
await waitFor(() => {
expect(getByText('api-6c4d8')).toBeInTheDocument();
});
expect(getByTestId('dashboard-filter')).toHaveTextContent('host-filter-enabled');
expect(getByTestId('workloads-filter')).toHaveTextContent('host-filter-enabled');
});
it('does not let preselected host filtering suppress pod workloads', async () => {
@@ -251,7 +251,7 @@ describe('Dashboard pod workloads integration', () => {
mockLocationSearch = '?type=pod&resource=legacy:pve1:101';
const { getByText } = render(() => (
<Dashboard vms={[]} containers={[]} nodes={[]} useWorkloads />
<WorkloadsSurface vms={[]} containers={[]} nodes={[]} useWorkloads />
));
await waitFor(() => {
@@ -291,7 +291,7 @@ describe('Dashboard pod workloads integration', () => {
];
const { getByText, queryByText } = render(() => (
<Dashboard vms={[]} containers={[]} nodes={[]} useWorkloads />
<WorkloadsSurface vms={[]} containers={[]} nodes={[]} useWorkloads />
));
await waitFor(() => {
@@ -360,7 +360,7 @@ describe('Dashboard pod workloads integration', () => {
];
const { getByText, queryByText } = render(() => (
<Dashboard vms={[]} containers={[]} nodes={[]} useWorkloads />
<WorkloadsSurface vms={[]} containers={[]} nodes={[]} useWorkloads />
));
await waitFor(() => {
@@ -440,7 +440,7 @@ describe('Dashboard pod workloads integration', () => {
];
const { getByText, queryByText } = render(() => (
<Dashboard vms={[]} containers={[]} nodes={[]} useWorkloads />
<WorkloadsSurface vms={[]} containers={[]} nodes={[]} useWorkloads />
));
await waitFor(() => {
@@ -504,7 +504,7 @@ describe('Dashboard pod workloads integration', () => {
];
const { getByText, queryByText } = render(() => (
<Dashboard vms={[]} containers={[]} nodes={[]} useWorkloads />
<WorkloadsSurface vms={[]} containers={[]} nodes={[]} useWorkloads />
));
await waitFor(() => {
@@ -596,7 +596,7 @@ describe('Dashboard pod workloads integration', () => {
];
const { queryByText, getByTestId } = render(() => (
<Dashboard vms={[]} containers={[]} nodes={[]} useWorkloads />
<WorkloadsSurface vms={[]} containers={[]} nodes={[]} useWorkloads />
));
await waitFor(() => {
@@ -668,7 +668,7 @@ describe('Dashboard pod workloads integration', () => {
];
const { getByText, getAllByText } = render(() => (
<Dashboard vms={[]} containers={[]} nodes={[]} useWorkloads />
<WorkloadsSurface vms={[]} containers={[]} nodes={[]} useWorkloads />
));
await waitFor(() => {
@@ -685,7 +685,7 @@ describe('Dashboard pod workloads integration', () => {
mockLocationSearch = '?type=all';
mockWorkloads = [];
render(() => <Dashboard vms={[]} containers={[]} nodes={[]} useWorkloads />);
render(() => <WorkloadsSurface vms={[]} containers={[]} nodes={[]} useWorkloads />);
await waitFor(() => {
expect(navigateSpy).toHaveBeenCalled();
@@ -701,7 +701,7 @@ describe('Dashboard pod workloads integration', () => {
mockLocationSearch = '?type=all&context=cluster-a';
mockWorkloads = [];
render(() => <Dashboard vms={[]} containers={[]} nodes={[]} useWorkloads />);
render(() => <WorkloadsSurface vms={[]} containers={[]} nodes={[]} useWorkloads />);
await waitFor(() => {
expect(navigateSpy).toHaveBeenCalled();
@@ -2,30 +2,30 @@ import { beforeEach, describe, expect, it, vi } from 'vitest';
import { render, waitFor } from '@solidjs/testing-library';
import { createSignal, onCleanup, onMount } from 'solid-js';
import type { Resource } from '@/types/resource';
import { Dashboard } from '../Dashboard';
import dashboardSource from '../Dashboard.tsx?raw';
import dashboardStateCardsSource from '../DashboardStateCards.tsx?raw';
import dashboardStatsStripSource from '../DashboardStatsStrip.tsx?raw';
import dashboardFilterSource from '../DashboardFilter.tsx?raw';
import dashboardWorkloadTableSource from '../DashboardWorkloadTable.tsx?raw';
import { WorkloadsSurface } from '../WorkloadsSurface';
import workloadsSource from '../WorkloadsSurface.tsx?raw';
import workloadsStateCardsSource from '../WorkloadsStateCards.tsx?raw';
import workloadsStatsStripSource from '../WorkloadsStatsStrip.tsx?raw';
import workloadsFilterSource from '../WorkloadsFilter.tsx?raw';
import workloadsWorkloadTableSource from '../WorkloadsTable.tsx?raw';
import workloadPanelSource from '../WorkloadPanel.tsx?raw';
import workloadTableHeaderSource from '../WorkloadTableHeader.tsx?raw';
import dashboardFilterModelSource from '../dashboardFilterModel.ts?raw';
import dashboardControlsStateSource from '../useDashboardControlsState.ts?raw';
import dashboardGuestMetadataStateSource from '../useDashboardGuestMetadataState.ts?raw';
import dashboardSelectionModelSource from '../dashboardSelectionModel.ts?raw';
import dashboardSelectionStateSource from '../useDashboardSelectionState.ts?raw';
import dashboardWorkloadDerivedStateSource from '../useDashboardWorkloadDerivedState.ts?raw';
import dashboardWorkloadFilterOptionsSource from '../useDashboardWorkloadFilterOptions.ts?raw';
import dashboardWorkloadFilterConfigModelSource from '../dashboardWorkloadFilterConfigModel.ts?raw';
import dashboardWorkloadRouteModelSource from '../dashboardWorkloadRouteModel.ts?raw';
import dashboardWorkloadRouteStateModelSource from '../dashboardWorkloadRouteStateModel.ts?raw';
import dashboardWorkloadUrlSyncModelSource from '../dashboardWorkloadUrlSyncModel.ts?raw';
import dashboardWorkloadViewportSyncSource from '../useDashboardWorkloadViewportSync.ts?raw';
import dashboardWorkloadRouteStateSource from '../useDashboardWorkloadRouteState.ts?raw';
import dashboardWorkloadUrlSyncSource from '../useDashboardWorkloadUrlSync.ts?raw';
import dashboardStateSource from '../useDashboardState.ts?raw';
import dashboardFilterStateSource from '../useDashboardFilterState.ts?raw';
import workloadsFilterModelSource from '../workloadsFilterModel.ts?raw';
import workloadsControlsStateSource from '../useWorkloadsControlsState.ts?raw';
import workloadsGuestMetadataStateSource from '../useWorkloadGuestMetadataState.ts?raw';
import workloadSelectionModelSource from '../workloadSelectionModel.ts?raw';
import workloadsSelectionStateSource from '../useWorkloadSelectionState.ts?raw';
import workloadsWorkloadDerivedStateSource from '../useWorkloadsDerivedState.ts?raw';
import workloadsWorkloadFilterOptionsSource from '../useWorkloadFilterOptions.ts?raw';
import workloadFilterConfigModelSource from '../workloadFilterConfigModel.ts?raw';
import workloadRouteModelSource from '../workloadRouteModel.ts?raw';
import workloadRouteStateModelSource from '../workloadRouteStateModel.ts?raw';
import workloadUrlSyncModelSource from '../workloadUrlSyncModel.ts?raw';
import workloadsWorkloadViewportSyncSource from '../useWorkloadViewportSync.ts?raw';
import workloadsWorkloadRouteStateSource from '../useWorkloadRouteState.ts?raw';
import workloadsWorkloadUrlSyncSource from '../useWorkloadUrlSync.ts?raw';
import workloadsStateSource from '../useWorkloadsState.ts?raw';
import workloadsFilterStateSource from '../useWorkloadsFilterState.ts?raw';
import groupedTableWindowingSource from '../useGroupedTableWindowing.ts?raw';
import thresholdSliderSource from '../ThresholdSlider.tsx?raw';
import thresholdSliderModelSource from '../thresholdSliderModel.ts?raw';
@@ -188,8 +188,8 @@ vi.mock('@/components/shared/InfrastructureSelector', () => ({
),
}));
vi.mock('../DashboardFilter', () => ({
DashboardFilter: () => <div data-testid="dashboard-filter">filter</div>,
vi.mock('../WorkloadsFilter', () => ({
WorkloadsFilter: () => <div data-testid="workloads-filter">filter</div>,
}));
vi.mock('../GuestDrawer', () => ({
@@ -313,7 +313,7 @@ const flushEffects = async () => {
await Promise.resolve();
};
describe('Dashboard performance contract', () => {
describe('Workloads performance contract', () => {
beforeEach(() => {
vi.clearAllMocks();
localStorage.clear();
@@ -343,12 +343,12 @@ describe('Dashboard performance contract', () => {
});
describe('Baseline structural contracts', () => {
it('renders Profile S dashboard table and guest rows', async () => {
it('renders Profile S workloads table and guest rows', async () => {
mockLocationSearch = '?type=all';
mockWorkloads = makeGuests(PROFILES.S);
const { container, getByTestId } = render(() => (
<Dashboard vms={[]} containers={[]} nodes={[]} useWorkloads />
<WorkloadsSurface vms={[]} containers={[]} nodes={[]} useWorkloads />
));
await waitFor(() => {
@@ -369,10 +369,10 @@ describe('Dashboard performance contract', () => {
wsReconnecting = true;
mockWorkloads = [makeGuest(1, { name: 'route-owned-workload' })];
render(() => <Dashboard vms={[]} containers={[]} nodes={[]} useWorkloads />);
render(() => <WorkloadsSurface vms={[]} containers={[]} nodes={[]} useWorkloads />);
await waitFor(() => {
expect(document.querySelector('[data-testid="dashboard-filter"]')).toBeInTheDocument();
expect(document.querySelector('[data-testid="workloads-filter"]')).toBeInTheDocument();
});
await waitFor(() => {
expect(
@@ -412,7 +412,7 @@ describe('Dashboard performance contract', () => {
mockWorkloads = [makeGuest(1, { name: 'first-connect-workload' })];
const { container } = render(() => (
<Dashboard vms={[]} containers={[]} nodes={[]} useWorkloads />
<WorkloadsSurface vms={[]} containers={[]} nodes={[]} useWorkloads />
));
await waitFor(() => {
@@ -431,7 +431,7 @@ describe('Dashboard performance contract', () => {
mockWorkloads = [makeGuest(1, { name: 'reconnect-workload' })];
const { container } = render(() => (
<Dashboard vms={[]} containers={[]} nodes={[]} useWorkloads />
<WorkloadsSurface vms={[]} containers={[]} nodes={[]} useWorkloads />
));
await waitFor(() => {
@@ -455,7 +455,7 @@ describe('Dashboard performance contract', () => {
mockWorkloads = makeGuests(PROFILES.M);
const { container } = render(() => (
<Dashboard vms={[]} containers={[]} nodes={[]} useWorkloads />
<WorkloadsSurface vms={[]} containers={[]} nodes={[]} useWorkloads />
));
await waitFor(() => {
@@ -473,7 +473,7 @@ describe('Dashboard performance contract', () => {
mockWorkloads = makeGuests(PROFILES.L);
const { container } = render(() => (
<Dashboard vms={[]} containers={[]} nodes={[]} useWorkloads />
<WorkloadsSurface vms={[]} containers={[]} nodes={[]} useWorkloads />
));
await waitFor(() => {
@@ -494,7 +494,7 @@ describe('Dashboard performance contract', () => {
mockWorkloads = makeGuests(PROFILES.S);
const { container } = render(() => (
<Dashboard vms={[]} containers={[]} nodes={[]} useWorkloads />
<WorkloadsSurface vms={[]} containers={[]} nodes={[]} useWorkloads />
));
await waitFor(() => {
@@ -508,7 +508,7 @@ describe('Dashboard performance contract', () => {
mockWorkloads = guests;
const { container } = render(() => (
<Dashboard vms={[]} containers={[]} nodes={[]} useWorkloads />
<WorkloadsSurface vms={[]} containers={[]} nodes={[]} useWorkloads />
));
await waitFor(() => {
@@ -545,7 +545,7 @@ describe('Dashboard performance contract', () => {
mockWorkloads = profileGuests;
const { container, unmount } = render(() => (
<Dashboard vms={[]} containers={[]} nodes={[]} useWorkloads />
<WorkloadsSurface vms={[]} containers={[]} nodes={[]} useWorkloads />
));
await waitFor(() => {
@@ -558,7 +558,7 @@ describe('Dashboard performance contract', () => {
});
describe('Workload derivation contracts', () => {
it('normalizes dashboard view mode aliases through the shared workload helper', () => {
it('normalizes workloads view mode aliases through the shared workload helper', () => {
expect(normalizeWorkloadViewModeParam('all')).toBe('all');
expect(normalizeWorkloadViewModeParam('docker')).toBe('app-container');
expect(normalizeWorkloadViewModeParam('Kubernetes')).toBe('pod');
@@ -576,7 +576,7 @@ describe('Dashboard performance contract', () => {
);
const { container } = render(() => (
<Dashboard vms={[]} containers={[]} nodes={[]} useWorkloads />
<WorkloadsSurface vms={[]} containers={[]} nodes={[]} useWorkloads />
));
await waitFor(() => {
@@ -589,202 +589,202 @@ describe('Dashboard performance contract', () => {
});
it('routes org scope normalization through the shared helper', () => {
expect(dashboardSource).toContain('useDashboardState');
expect(dashboardStateSource).toContain('useDashboardGuestMetadataState');
expect(dashboardGuestMetadataStateSource).toContain('normalizeOrgScope(getOrgID())');
expect(dashboardStateSource).not.toContain("const DEFAULT_ORG_SCOPE = 'default'");
expect(dashboardStateSource).not.toContain('const normalizeOrgScope =');
expect(workloadsSource).toContain('useWorkloadsState');
expect(workloadsStateSource).toContain('useWorkloadGuestMetadataState');
expect(workloadsGuestMetadataStateSource).toContain('normalizeOrgScope(getOrgID())');
expect(workloadsStateSource).not.toContain("const DEFAULT_ORG_SCOPE = 'default'");
expect(workloadsStateSource).not.toContain('const normalizeOrgScope =');
});
it('keeps hot-path dashboard state in the shared dashboard state owner', () => {
expect(dashboardSource).toContain('useDashboardState');
expect(dashboardSource).toContain('DashboardStateCards');
expect(dashboardSource).toContain('DashboardStatsStrip');
expect(dashboardSource).toContain('DashboardWorkloadTable');
expect(dashboardSource).not.toContain('const [search, setSearch] = createSignal(');
expect(dashboardStateSource).toContain('useDashboardControlsState');
expect(dashboardStateSource).toContain('useDashboardGuestMetadataState');
expect(dashboardStateSource).toContain('useDashboardSelectionState');
expect(dashboardStateSource).toContain('useDashboardWorkloadDerivedState');
expect(dashboardStateSource).toContain('useDashboardWorkloadRouteState');
expect(dashboardStateSource).toContain('createWorkloadSortComparator');
expect(dashboardStateSource).toContain('filterWorkloads(params)');
expect(dashboardStateSource).not.toContain('useBreakpoint');
expect(dashboardStateSource).not.toContain('useColumnVisibility');
expect(dashboardStateSource).not.toContain('blurFocusedTypeToSearch');
expect(dashboardStateSource).not.toContain("from './GuestRow'");
expect(dashboardStateSource).not.toContain('GuestMetadataAPI.getAllMetadata()');
expect(dashboardStateSource).not.toContain('buildWorkloadsPath({');
expect(dashboardStateSource).not.toContain('parseWorkloadsLinkSearch');
expect(dashboardStateSource).not.toContain('const [selectedGuestId, setSelectedGuestIdRaw]');
expect(dashboardStateSource).not.toContain('const [hoveredWorkloadId, setHoveredWorkloadId]');
expect(dashboardStateSource).not.toContain('groupWorkloads(');
expect(dashboardStateSource).not.toContain('computeWorkloadStats(');
expect(dashboardStateSource).not.toContain('computeWorkloadIOEmphasis(');
expect(dashboardStateSource).not.toContain('buildNodeByInstance(');
expect(dashboardStateSource).not.toContain('buildGuestParentNodeMap(');
expect(dashboardStateSource).not.toContain('useGroupedTableWindowing');
expect(dashboardGuestMetadataStateSource).toContain('GuestMetadataAPI.getAllMetadata()');
expect(dashboardGuestMetadataStateSource).toContain("eventBus.on('org_switched'");
expect(dashboardGuestMetadataStateSource).toContain(
it('keeps hot-path workloads state in the shared workloads state owner', () => {
expect(workloadsSource).toContain('useWorkloadsState');
expect(workloadsSource).toContain('WorkloadsStateCards');
expect(workloadsSource).toContain('WorkloadsStatsStrip');
expect(workloadsSource).toContain('WorkloadsTable');
expect(workloadsSource).not.toContain('const [search, setSearch] = createSignal(');
expect(workloadsStateSource).toContain('useWorkloadsControlsState');
expect(workloadsStateSource).toContain('useWorkloadGuestMetadataState');
expect(workloadsStateSource).toContain('useWorkloadSelectionState');
expect(workloadsStateSource).toContain('useWorkloadsDerivedState');
expect(workloadsStateSource).toContain('useWorkloadRouteState');
expect(workloadsStateSource).toContain('createWorkloadSortComparator');
expect(workloadsStateSource).toContain('filterWorkloads(params)');
expect(workloadsStateSource).not.toContain('useBreakpoint');
expect(workloadsStateSource).not.toContain('useColumnVisibility');
expect(workloadsStateSource).not.toContain('blurFocusedTypeToSearch');
expect(workloadsStateSource).not.toContain("from './GuestRow'");
expect(workloadsStateSource).not.toContain('GuestMetadataAPI.getAllMetadata()');
expect(workloadsStateSource).not.toContain('buildWorkloadsPath({');
expect(workloadsStateSource).not.toContain('parseWorkloadsLinkSearch');
expect(workloadsStateSource).not.toContain('const [selectedGuestId, setSelectedGuestIdRaw]');
expect(workloadsStateSource).not.toContain('const [hoveredWorkloadId, setHoveredWorkloadId]');
expect(workloadsStateSource).not.toContain('groupWorkloads(');
expect(workloadsStateSource).not.toContain('computeWorkloadStats(');
expect(workloadsStateSource).not.toContain('computeWorkloadIOEmphasis(');
expect(workloadsStateSource).not.toContain('buildNodeByInstance(');
expect(workloadsStateSource).not.toContain('buildGuestParentNodeMap(');
expect(workloadsStateSource).not.toContain('useGroupedTableWindowing');
expect(workloadsGuestMetadataStateSource).toContain('GuestMetadataAPI.getAllMetadata()');
expect(workloadsGuestMetadataStateSource).toContain("eventBus.on('org_switched'");
expect(workloadsGuestMetadataStateSource).toContain(
"window.addEventListener('pulse:metadata-changed'",
);
expect(dashboardWorkloadRouteStateSource).toContain('useDashboardWorkloadUrlSync');
expect(dashboardWorkloadRouteStateSource).toContain('useDashboardWorkloadFilterOptions');
expect(dashboardWorkloadRouteStateSource).not.toContain('buildWorkloadsPath({');
expect(dashboardWorkloadRouteStateSource).not.toContain('normalizeWorkloadViewModeParam');
expect(dashboardWorkloadRouteStateSource).not.toContain(
expect(workloadsWorkloadRouteStateSource).toContain('useWorkloadUrlSync');
expect(workloadsWorkloadRouteStateSource).toContain('useWorkloadFilterOptions');
expect(workloadsWorkloadRouteStateSource).not.toContain('buildWorkloadsPath({');
expect(workloadsWorkloadRouteStateSource).not.toContain('normalizeWorkloadViewModeParam');
expect(workloadsWorkloadRouteStateSource).not.toContain(
'const [handledTypeParam, setHandledTypeParam]',
);
expect(dashboardWorkloadRouteStateSource).not.toContain(
expect(workloadsWorkloadRouteStateSource).not.toContain(
'const [workloadsRouteActive, setWorkloadsRouteActive] = createSignal(false)',
);
expect(dashboardWorkloadRouteStateSource).not.toContain(
expect(workloadsWorkloadRouteStateSource).not.toContain(
'const workloadNodeOptions = createMemo',
);
expect(dashboardWorkloadRouteStateSource).not.toContain(
expect(workloadsWorkloadRouteStateSource).not.toContain(
'const containerRuntimeFilterConfig = createMemo',
);
expect(dashboardWorkloadRouteStateSource).toContain(
"from './dashboardWorkloadRouteStateModel'",
expect(workloadsWorkloadRouteStateSource).toContain(
"from './workloadRouteStateModel'",
);
expect(dashboardWorkloadRouteStateSource).toContain(
'resolveDashboardWorkloadNodeSelection({',
expect(workloadsWorkloadRouteStateSource).toContain(
'resolveWorkloadsWorkloadNodeSelection({',
);
expect(dashboardWorkloadRouteStateSource).toContain('DASHBOARD_WORKLOAD_ROUTE_RESET_STATE');
expect(dashboardWorkloadRouteStateSource).toContain('isWorkloadsRoute,');
expect(dashboardWorkloadFilterOptionsSource).toContain(
"from './dashboardWorkloadFilterConfigModel'",
expect(workloadsWorkloadRouteStateSource).toContain('WORKLOADS_WORKLOAD_ROUTE_RESET_STATE');
expect(workloadsWorkloadRouteStateSource).toContain('isWorkloadsRoute,');
expect(workloadsWorkloadFilterOptionsSource).toContain(
"from './workloadFilterConfigModel'",
);
expect(dashboardWorkloadFilterOptionsSource).toContain(
'buildDashboardWorkloadNodeOptions(options.allGuests())',
expect(workloadsWorkloadFilterOptionsSource).toContain(
'buildWorkloadNodeOptions(options.allGuests())',
);
expect(dashboardWorkloadFilterOptionsSource).not.toContain(
expect(workloadsWorkloadFilterOptionsSource).not.toContain(
'const onContextChange = (value: string) =>',
);
expect(dashboardWorkloadFilterOptionsSource).toContain(
'buildDashboardContainerRuntimeFilterConfig({',
expect(workloadsWorkloadFilterOptionsSource).toContain(
'buildWorkloadsContainerRuntimeFilterConfig({',
);
expect(dashboardWorkloadFilterOptionsSource).toContain('buildDashboardHostFilterConfig({');
expect(dashboardWorkloadFilterOptionsSource).toContain(
'buildDashboardNamespaceFilterConfig({',
expect(workloadsWorkloadFilterOptionsSource).toContain('buildWorkloadsHostFilterConfig({');
expect(workloadsWorkloadFilterOptionsSource).toContain(
'buildWorkloadsNamespaceFilterConfig({',
);
expect(dashboardWorkloadFilterConfigModelSource).toContain(
'export const buildDashboardContainerRuntimeFilterConfig',
expect(workloadFilterConfigModelSource).toContain(
'export const buildWorkloadsContainerRuntimeFilterConfig',
);
expect(dashboardWorkloadFilterConfigModelSource).toContain(
'export const buildDashboardHostFilterConfig',
expect(workloadFilterConfigModelSource).toContain(
'export const buildWorkloadsHostFilterConfig',
);
expect(dashboardWorkloadFilterConfigModelSource).toContain(
"DASHBOARD_KUBERNETES_CONTEXT_FILTER_LABEL = 'K8s cluster'",
expect(workloadFilterConfigModelSource).toContain(
"WORKLOADS_KUBERNETES_CONTEXT_FILTER_LABEL = 'K8s cluster'",
);
expect(dashboardWorkloadFilterConfigModelSource).toContain(
expect(workloadFilterConfigModelSource).toContain(
"getAllFilterOptionLabel('K8s clusters')",
);
expect(dashboardWorkloadFilterConfigModelSource).toContain(
'DASHBOARD_WORKLOAD_TYPE_OPTIONS',
expect(workloadFilterConfigModelSource).toContain(
'WORKLOAD_TYPE_OPTIONS',
);
expect(dashboardWorkloadFilterConfigModelSource).toContain(
'export const buildDashboardNamespaceFilterConfig',
expect(workloadFilterConfigModelSource).toContain(
'export const buildWorkloadsNamespaceFilterConfig',
);
expect(dashboardWorkloadRouteModelSource).toContain(
'export const deserializeDashboardWorkloadViewMode',
expect(workloadRouteModelSource).toContain(
'export const deserializeWorkloadViewMode',
);
expect(dashboardWorkloadRouteModelSource).toContain(
expect(workloadRouteModelSource).toContain(
"normalizeWorkloadViewModeParam(raw) ?? 'all'",
);
expect(dashboardWorkloadRouteModelSource).not.toContain(
'export const buildDashboardContainerRuntimeFilterConfig',
expect(workloadRouteModelSource).not.toContain(
'export const buildWorkloadsContainerRuntimeFilterConfig',
);
expect(dashboardWorkloadRouteModelSource).not.toContain(
'export const buildDashboardHostFilterConfig',
expect(workloadRouteModelSource).not.toContain(
'export const buildWorkloadsHostFilterConfig',
);
expect(dashboardWorkloadRouteModelSource).not.toContain(
'export const buildDashboardNamespaceFilterConfig',
expect(workloadRouteModelSource).not.toContain(
'export const buildWorkloadsNamespaceFilterConfig',
);
expect(dashboardWorkloadRouteStateModelSource).toContain(
'export const DASHBOARD_WORKLOAD_ROUTE_RESET_STATE',
expect(workloadRouteStateModelSource).toContain(
'export const WORKLOADS_WORKLOAD_ROUTE_RESET_STATE',
);
expect(dashboardWorkloadRouteStateModelSource).toContain(
'export const resolveDashboardWorkloadNodeSelection',
expect(workloadRouteStateModelSource).toContain(
'export const resolveWorkloadsWorkloadNodeSelection',
);
expect(dashboardWorkloadRouteStateModelSource).toContain(
'export const deserializeDashboardContainerRuntime',
expect(workloadRouteStateModelSource).toContain(
'export const deserializeWorkloadsContainerRuntime',
);
expect(dashboardWorkloadUrlSyncSource).not.toContain('buildWorkloadsPath({');
expect(dashboardWorkloadUrlSyncSource).not.toContain('normalizeWorkloadViewModeParam');
expect(dashboardWorkloadUrlSyncSource).not.toContain('parseWorkloadsLinkSearch');
expect(dashboardWorkloadUrlSyncSource).toContain("from './dashboardWorkloadRouteModel'");
expect(dashboardWorkloadUrlSyncSource).toContain("from './dashboardWorkloadUrlSyncModel'");
expect(dashboardWorkloadUrlSyncSource).toContain(
expect(workloadsWorkloadUrlSyncSource).not.toContain('buildWorkloadsPath({');
expect(workloadsWorkloadUrlSyncSource).not.toContain('normalizeWorkloadViewModeParam');
expect(workloadsWorkloadUrlSyncSource).not.toContain('parseWorkloadsLinkSearch');
expect(workloadsWorkloadUrlSyncSource).toContain("from './workloadRouteModel'");
expect(workloadsWorkloadUrlSyncSource).toContain("from './workloadUrlSyncModel'");
expect(workloadsWorkloadUrlSyncSource).toContain(
'const [handledTypeParam, setHandledTypeParam]',
);
expect(dashboardWorkloadUrlSyncSource).toContain(
expect(workloadsWorkloadUrlSyncSource).toContain(
'if (normalized === handledPlatformParam()) return;',
);
expect(dashboardWorkloadUrlSyncSource).toContain(
expect(workloadsWorkloadUrlSyncSource).toContain(
'const currentSelected = untrack(() => options.selectedPlatform());',
);
expect(dashboardWorkloadUrlSyncSource).toContain('parseDashboardWorkloadUrlParams');
expect(dashboardWorkloadUrlSyncSource).toContain(
'resolveDashboardManagedWorkloadsNavigateTarget({',
expect(workloadsWorkloadUrlSyncSource).toContain('parseWorkloadsWorkloadUrlParams');
expect(workloadsWorkloadUrlSyncSource).toContain(
'resolveWorkloadsManagedWorkloadsNavigateTarget({',
);
expect(dashboardWorkloadUrlSyncModelSource).toContain('parseWorkloadsLinkSearch(search)');
expect(dashboardWorkloadUrlSyncModelSource).toContain('buildWorkloadsPath({');
expect(dashboardWorkloadUrlSyncModelSource).toContain(
'resolveDashboardManagedWorkloadsNavigateTarget',
expect(workloadUrlSyncModelSource).toContain('parseWorkloadsLinkSearch(search)');
expect(workloadUrlSyncModelSource).toContain('buildWorkloadsPath({');
expect(workloadUrlSyncModelSource).toContain(
'resolveWorkloadsManagedWorkloadsNavigateTarget',
);
expect(dashboardWorkloadUrlSyncModelSource).toContain('resolveDashboardWorkloadRuntimeParam');
expect(dashboardWorkloadUrlSyncModelSource).toContain(
expect(workloadUrlSyncModelSource).toContain('resolveWorkloadsWorkloadRuntimeParam');
expect(workloadUrlSyncModelSource).toContain(
'normalizeWorkloadViewModeParam(params.type)',
);
expect(dashboardControlsStateSource).toContain('useBreakpoint');
expect(dashboardControlsStateSource).toContain('useColumnVisibility');
expect(dashboardControlsStateSource).toContain('usePersistentSignal');
expect(dashboardControlsStateSource).toContain('blurFocusedTypeToSearch');
expect(dashboardControlsStateSource).toContain('DEFAULT_DASHBOARD_SORT_KEY');
expect(dashboardWorkloadDerivedStateSource).toContain('groupWorkloads(');
expect(dashboardWorkloadDerivedStateSource).toContain('computeWorkloadStats(');
expect(dashboardWorkloadDerivedStateSource).toContain('computeWorkloadIOEmphasis(');
expect(dashboardWorkloadDerivedStateSource).toContain("from './workloadTopology'");
expect(dashboardWorkloadDerivedStateSource).toContain('buildNodeByInstance(');
expect(dashboardWorkloadDerivedStateSource).toContain('buildGuestParentNodeMap(');
expect(dashboardWorkloadDerivedStateSource).toContain('useGroupedTableWindowing');
expect(dashboardWorkloadDerivedStateSource).toContain('useDashboardWorkloadViewportSync');
expect(dashboardWorkloadDerivedStateSource).not.toContain('window.addEventListener');
expect(dashboardWorkloadDerivedStateSource).not.toContain('getBoundingClientRect');
expect(dashboardWorkloadViewportSyncSource).toContain('window.addEventListener');
expect(dashboardWorkloadViewportSyncSource).toContain('window.removeEventListener');
expect(dashboardWorkloadViewportSyncSource).toContain('getBoundingClientRect');
expect(dashboardWorkloadViewportSyncSource).toContain('groupedWindowing.onScroll');
expect(dashboardWorkloadRouteStateSource).not.toContain("from './workloadTopology'");
expect(dashboardWorkloadRouteModelSource).toContain("from './workloadTopology'");
expect(dashboardWorkloadRouteModelSource).toContain('workloadHostScopeId');
expect(dashboardWorkloadRouteModelSource).toContain('getKubernetesContextKey');
expect(dashboardSelectionStateSource).toContain(
expect(workloadsControlsStateSource).toContain('useBreakpoint');
expect(workloadsControlsStateSource).toContain('useColumnVisibility');
expect(workloadsControlsStateSource).toContain('usePersistentSignal');
expect(workloadsControlsStateSource).toContain('blurFocusedTypeToSearch');
expect(workloadsControlsStateSource).toContain('DEFAULT_WORKLOADS_SORT_KEY');
expect(workloadsWorkloadDerivedStateSource).toContain('groupWorkloads(');
expect(workloadsWorkloadDerivedStateSource).toContain('computeWorkloadStats(');
expect(workloadsWorkloadDerivedStateSource).toContain('computeWorkloadIOEmphasis(');
expect(workloadsWorkloadDerivedStateSource).toContain("from './workloadTopology'");
expect(workloadsWorkloadDerivedStateSource).toContain('buildNodeByInstance(');
expect(workloadsWorkloadDerivedStateSource).toContain('buildGuestParentNodeMap(');
expect(workloadsWorkloadDerivedStateSource).toContain('useGroupedTableWindowing');
expect(workloadsWorkloadDerivedStateSource).toContain('useWorkloadViewportSync');
expect(workloadsWorkloadDerivedStateSource).not.toContain('window.addEventListener');
expect(workloadsWorkloadDerivedStateSource).not.toContain('getBoundingClientRect');
expect(workloadsWorkloadViewportSyncSource).toContain('window.addEventListener');
expect(workloadsWorkloadViewportSyncSource).toContain('window.removeEventListener');
expect(workloadsWorkloadViewportSyncSource).toContain('getBoundingClientRect');
expect(workloadsWorkloadViewportSyncSource).toContain('groupedWindowing.onScroll');
expect(workloadsWorkloadRouteStateSource).not.toContain("from './workloadTopology'");
expect(workloadRouteModelSource).toContain("from './workloadTopology'");
expect(workloadRouteModelSource).toContain('workloadHostScopeId');
expect(workloadRouteModelSource).toContain('getKubernetesContextKey');
expect(workloadsSelectionStateSource).toContain(
'const [selectedGuestId, setSelectedGuestIdRaw]',
);
expect(dashboardSelectionStateSource).toContain(
expect(workloadsSelectionStateSource).toContain(
'const [hoveredWorkloadId, setHoveredWorkloadId]',
);
expect(dashboardSelectionStateSource).toContain('setHandledResourceId(null)');
expect(dashboardSelectionStateSource).toContain("from './dashboardSelectionModel'");
expect(dashboardSelectionStateSource).not.toContain('parseWorkloadsLinkSearch');
expect(dashboardSelectionStateSource).not.toContain('getCanonicalWorkloadId');
expect(dashboardSelectionModelSource).toContain('parseWorkloadsLinkSearch(search)');
expect(dashboardSelectionModelSource).toContain('getCanonicalWorkloadId');
expect(dashboardSelectionModelSource).toContain('resolveDashboardResourceSelection');
expect(dashboardSelectionModelSource).toContain('dashboardHasHoveredWorkload');
expect(workloadsSelectionStateSource).toContain('setHandledResourceId(null)');
expect(workloadsSelectionStateSource).toContain("from './workloadSelectionModel'");
expect(workloadsSelectionStateSource).not.toContain('parseWorkloadsLinkSearch');
expect(workloadsSelectionStateSource).not.toContain('getCanonicalWorkloadId');
expect(workloadSelectionModelSource).toContain('parseWorkloadsLinkSearch(search)');
expect(workloadSelectionModelSource).toContain('getCanonicalWorkloadId');
expect(workloadSelectionModelSource).toContain('resolveWorkloadResourceSelection');
expect(workloadSelectionModelSource).toContain('workloadsHasHoveredWorkload');
expect(groupedTableWindowingSource).toContain('DEFAULT_WINDOW_SIZE');
expect(groupedTableWindowingSource).toContain('DEFAULT_ENABLE_THRESHOLD');
expect(groupedTableWindowingSource).toContain('DEFAULT_OVERSCAN_ROWS');
expect(groupedTableWindowingSource).toContain('getVisibleSlice');
expect(groupedTableWindowingSource).toContain('onScroll');
expect(groupedTableWindowingSource).toContain('revealIndex');
expect(dashboardStateSource).not.toContain('const DEFAULT_WINDOW_SIZE =');
expect(dashboardStateSource).not.toContain('const DEFAULT_ENABLE_THRESHOLD =');
expect(dashboardStateSource).not.toContain('const DEFAULT_OVERSCAN_ROWS =');
expect(dashboardSource).not.toContain('createMemo(() => getCanonicalWorkloadId(guest()))');
expect(workloadsStateSource).not.toContain('const DEFAULT_WINDOW_SIZE =');
expect(workloadsStateSource).not.toContain('const DEFAULT_ENABLE_THRESHOLD =');
expect(workloadsStateSource).not.toContain('const DEFAULT_OVERSCAN_ROWS =');
expect(workloadsSource).not.toContain('createMemo(() => getCanonicalWorkloadId(guest()))');
expect(workloadPanelSource).toContain('createMemo(() => getCanonicalWorkloadId(guest()))');
expect(workloadPanelSource).toContain('buildWorkloadSummaryGroupScope');
expect(workloadPanelSource).toContain('data-summary-group-id');
@@ -792,56 +792,56 @@ describe('Dashboard performance contract', () => {
expect(workloadPanelSource).toContain('getInteractiveGroupedTableRowClass');
expect(workloadPanelSource).toContain('getGroupedTableRowCellClass');
expect(workloadPanelSource).not.toContain('cursor-pointer bg-surface-alt');
expect(dashboardWorkloadTableSource).not.toContain(
expect(workloadsWorkloadTableSource).not.toContain(
'createMemo(() => getCanonicalWorkloadId(guest()))',
);
expect(dashboardStateSource).not.toContain('const guestId = () => {');
expect(workloadsStateSource).not.toContain('const guestId = () => {');
});
it('keeps dashboard filter state in canonical dashboard filter owners', () => {
expect(dashboardFilterSource).toContain('useDashboardFilterState');
expect(dashboardSource).not.toContain('SummaryScopeBar');
expect(dashboardSource).not.toContain('searchTrailing={pinnedScopeFallback()}');
expect(dashboardSource).not.toContain('mobileTrailing={pinnedScopeFallback()}');
expect(dashboardSource).toContain('setClearSurfaceRootRef');
expect(dashboardSource).toContain('setTableRootRef={state.setTableRootRef}');
expect(dashboardSource).toContain('data-testid="workloads-interaction-surface"');
expect(dashboardSource).toContain('data-summary-clear-ignore');
expect(dashboardSource).toContain('clearPinnedSummaryScope={state.clearPinnedSummaryScope}');
expect(dashboardWorkloadTableSource).toContain('data-summary-clear-surface');
expect(dashboardWorkloadTableSource).toContain('data-testid="workloads-table-surface"');
expect(dashboardWorkloadTableSource).toContain('TableCard');
expect(dashboardWorkloadTableSource).toContain('TableCardHeader');
expect(dashboardWorkloadTableSource).toContain('showClearSelection');
expect(dashboardWorkloadTableSource).toContain('clearPinnedSummaryScope');
expect(dashboardFilterSource).not.toContain('const [filtersOpen, setFiltersOpen] =');
expect(dashboardFilterSource).not.toContain('useBreakpoint');
expect(dashboardFilterSource).not.toContain("props.setSortKey('name')");
expect(dashboardFilterSource).toContain('searchTrailing={props.searchTrailing}');
expect(dashboardFilterSource).toContain('mobileTrailing={props.mobileTrailing}');
expect(dashboardFilterStateSource).toContain('countActiveDashboardFilters');
expect(dashboardFilterStateSource).not.toContain('props.containerRuntimeFilter?.onChange');
expect(dashboardFilterStateSource).toContain('useBreakpoint');
expect(dashboardFilterStateSource).toContain('DEFAULT_DASHBOARD_SORT_KEY');
expect(dashboardFilterModelSource).toContain('export const countActiveDashboardFilters');
expect(dashboardFilterModelSource).toContain('export const hasActiveDashboardFilters');
expect(dashboardFilterModelSource).toContain(
"DEFAULT_DASHBOARD_SORT_KEY: DashboardSortKey = 'type'",
it('keeps workloads filter state in canonical workloads filter owners', () => {
expect(workloadsFilterSource).toContain('useWorkloadsFilterState');
expect(workloadsSource).not.toContain('SummaryScopeBar');
expect(workloadsSource).not.toContain('searchTrailing={pinnedScopeFallback()}');
expect(workloadsSource).not.toContain('mobileTrailing={pinnedScopeFallback()}');
expect(workloadsSource).toContain('setClearSurfaceRootRef');
expect(workloadsSource).toContain('setTableRootRef={state.setTableRootRef}');
expect(workloadsSource).toContain('data-testid="workloads-interaction-surface"');
expect(workloadsSource).toContain('data-summary-clear-ignore');
expect(workloadsSource).toContain('clearPinnedSummaryScope={state.clearPinnedSummaryScope}');
expect(workloadsWorkloadTableSource).toContain('data-summary-clear-surface');
expect(workloadsWorkloadTableSource).toContain('data-testid="workloads-table-surface"');
expect(workloadsWorkloadTableSource).toContain('TableCard');
expect(workloadsWorkloadTableSource).toContain('TableCardHeader');
expect(workloadsWorkloadTableSource).toContain('showClearSelection');
expect(workloadsWorkloadTableSource).toContain('clearPinnedSummaryScope');
expect(workloadsFilterSource).not.toContain('const [filtersOpen, setFiltersOpen] =');
expect(workloadsFilterSource).not.toContain('useBreakpoint');
expect(workloadsFilterSource).not.toContain("props.setSortKey('name')");
expect(workloadsFilterSource).toContain('searchTrailing={props.searchTrailing}');
expect(workloadsFilterSource).toContain('mobileTrailing={props.mobileTrailing}');
expect(workloadsFilterStateSource).toContain('countActiveWorkloadsFilters');
expect(workloadsFilterStateSource).not.toContain('props.containerRuntimeFilter?.onChange');
expect(workloadsFilterStateSource).toContain('useBreakpoint');
expect(workloadsFilterStateSource).toContain('DEFAULT_WORKLOADS_SORT_KEY');
expect(workloadsFilterModelSource).toContain('export const countActiveWorkloadsFilters');
expect(workloadsFilterModelSource).toContain('export const hasActiveWorkloadsFilters');
expect(workloadsFilterModelSource).toContain(
"DEFAULT_WORKLOADS_SORT_KEY: WorkloadsSortKey = 'type'",
);
expect(dashboardFilterModelSource).toContain('searchTrailing?: JSX.Element;');
expect(dashboardFilterModelSource).toContain('utilityActions?: JSX.Element;');
expect(dashboardFilterModelSource).toContain('mobileTrailing?: JSX.Element;');
expect(dashboardStateSource).toContain('useDashboardWorkloadRouteState');
expect(dashboardStateSource).toContain('filterWorkloads(params)');
expect(dashboardStateSource).not.toContain('const containerRuntimeFilterConfig = createMemo');
expect(dashboardStateSource).not.toContain('useGroupedTableWindowing');
expect(dashboardWorkloadRouteStateSource).toContain('useDashboardWorkloadUrlSync');
expect(dashboardWorkloadRouteStateSource).toContain('useDashboardWorkloadFilterOptions');
expect(dashboardWorkloadRouteStateSource).toContain('containerRuntimeFilterConfig');
expect(dashboardWorkloadRouteStateSource).toContain('hostFilterConfig');
expect(dashboardWorkloadRouteStateSource).toContain('namespaceFilterConfig');
expect(dashboardWorkloadDerivedStateSource).toContain('useGroupedTableWindowing');
expect(dashboardWorkloadDerivedStateSource).toContain('useDashboardWorkloadViewportSync');
expect(workloadsFilterModelSource).toContain('searchTrailing?: JSX.Element;');
expect(workloadsFilterModelSource).toContain('utilityActions?: JSX.Element;');
expect(workloadsFilterModelSource).toContain('mobileTrailing?: JSX.Element;');
expect(workloadsStateSource).toContain('useWorkloadRouteState');
expect(workloadsStateSource).toContain('filterWorkloads(params)');
expect(workloadsStateSource).not.toContain('const containerRuntimeFilterConfig = createMemo');
expect(workloadsStateSource).not.toContain('useGroupedTableWindowing');
expect(workloadsWorkloadRouteStateSource).toContain('useWorkloadUrlSync');
expect(workloadsWorkloadRouteStateSource).toContain('useWorkloadFilterOptions');
expect(workloadsWorkloadRouteStateSource).toContain('containerRuntimeFilterConfig');
expect(workloadsWorkloadRouteStateSource).toContain('hostFilterConfig');
expect(workloadsWorkloadRouteStateSource).toContain('namespaceFilterConfig');
expect(workloadsWorkloadDerivedStateSource).toContain('useGroupedTableWindowing');
expect(workloadsWorkloadDerivedStateSource).toContain('useWorkloadViewportSync');
});
it('keeps threshold slider runtime and derivations in canonical slider owners', () => {
@@ -970,22 +970,22 @@ describe('Dashboard performance contract', () => {
expect(workloadTopologySource).toContain('export const buildGuestParentNodeMap');
});
it('keeps dashboard shell rendering in canonical section owners', () => {
expect(dashboardSource).toContain('DashboardStateCards');
expect(dashboardSource).toContain('DashboardStatsStrip');
expect(dashboardSource).toContain('DashboardWorkloadTable');
expect(dashboardSource).not.toContain('TableHeader');
expect(dashboardSource).not.toContain('NodeGroupHeader');
expect(dashboardWorkloadTableSource).toContain('WorkloadTableHeader');
expect(dashboardWorkloadTableSource).toContain('WorkloadPanel');
expect(dashboardWorkloadTableSource).not.toContain('style={{');
expect(dashboardWorkloadTableSource).toContain('style={getGuestColumnWidthStyle(');
expect(dashboardWorkloadTableSource).toContain('props.workloadTableLayoutMode()');
expect(dashboardWorkloadTableSource).toContain('props.workloadTableVisibleColumnIds()');
expect(dashboardWorkloadTableSource).toContain('<colgroup>');
expect(dashboardWorkloadTableSource).not.toContain('<TableHead');
expect(dashboardWorkloadTableSource).not.toContain('NodeGroupHeader');
expect(dashboardWorkloadTableSource).not.toContain('GuestDrawer');
it('keeps workloads shell rendering in canonical section owners', () => {
expect(workloadsSource).toContain('WorkloadsStateCards');
expect(workloadsSource).toContain('WorkloadsStatsStrip');
expect(workloadsSource).toContain('WorkloadsTable');
expect(workloadsSource).not.toContain('TableHeader');
expect(workloadsSource).not.toContain('NodeGroupHeader');
expect(workloadsWorkloadTableSource).toContain('WorkloadTableHeader');
expect(workloadsWorkloadTableSource).toContain('WorkloadPanel');
expect(workloadsWorkloadTableSource).not.toContain('style={{');
expect(workloadsWorkloadTableSource).toContain('style={getGuestColumnWidthStyle(');
expect(workloadsWorkloadTableSource).toContain('props.workloadTableLayoutMode()');
expect(workloadsWorkloadTableSource).toContain('props.workloadTableVisibleColumnIds()');
expect(workloadsWorkloadTableSource).toContain('<colgroup>');
expect(workloadsWorkloadTableSource).not.toContain('<TableHead');
expect(workloadsWorkloadTableSource).not.toContain('NodeGroupHeader');
expect(workloadsWorkloadTableSource).not.toContain('GuestDrawer');
expect(workloadTableHeaderSource).toContain('TableHead');
expect(workloadTableHeaderSource).toContain('col.sortKey as WorkloadSortKey');
expect(workloadTableHeaderSource).toContain('style={getGuestColumnStyle(');
@@ -1006,32 +1006,32 @@ describe('Dashboard performance contract', () => {
expect(workloadPanelSource).not.toContain('style={');
expect(workloadPanelSource).not.toContain('kind="scope"');
expect(workloadPanelSource).not.toContain('leadingAction={');
expect(dashboardSelectionStateSource).toContain('activeSummaryWorkloadGroupScope');
expect(dashboardSelectionStateSource).toContain('focusedSummaryWorkloadGroupScope');
expect(dashboardSelectionStateSource).toContain('setHoveredWorkloadGroupScope');
expect(dashboardWorkloadTableSource).toContain('focusedSummaryWorkloadGroupScope');
expect(dashboardWorkloadTableSource).toContain('hoveredSummaryWorkloadGroupScope');
expect(dashboardSource).toContain(
expect(workloadsSelectionStateSource).toContain('activeSummaryWorkloadGroupScope');
expect(workloadsSelectionStateSource).toContain('focusedSummaryWorkloadGroupScope');
expect(workloadsSelectionStateSource).toContain('setHoveredWorkloadGroupScope');
expect(workloadsWorkloadTableSource).toContain('focusedSummaryWorkloadGroupScope');
expect(workloadsWorkloadTableSource).toContain('hoveredSummaryWorkloadGroupScope');
expect(workloadsSource).toContain(
'hoveredGroupScope={state.hoveredSummaryWorkloadGroupScope()}',
);
expect(dashboardSource).toContain(
expect(workloadsSource).toContain(
'focusedGroupScope={state.focusedSummaryWorkloadGroupScope()}',
);
expect(workloadPanelSource).not.toContain('TableHead');
expect(dashboardStateCardsSource).toContain('dashboardInfrastructureEmptyState().title');
expect(dashboardStateCardsSource).toContain('dashboardDisconnectedState().actionLabel');
expect(dashboardStateCardsSource).toContain("buildInfrastructureOnboardingPath('pick')");
expect(dashboardStatsStripSource).toContain('totalStats().running');
expect(dashboardStatsStripSource).toContain('totalStats().stopped');
expect(workloadsStateCardsSource).toContain('workloadsInfrastructureEmptyState().title');
expect(workloadsStateCardsSource).toContain('workloadsDisconnectedState().actionLabel');
expect(workloadsStateCardsSource).toContain("buildInfrastructureOnboardingPath('pick')");
expect(workloadsStatsStripSource).toContain('totalStats().running');
expect(workloadsStatsStripSource).toContain('totalStats().stopped');
});
it('keeps disk-list runtime and derivations in canonical disk-list owners', () => {
expect(diskListSource).toContain('useDiskListState');
expect(diskListSource).not.toContain('const getUsagePercent =');
expect(diskListSource).not.toContain('const getDiskStatusTooltip =');
expect(diskListStateSource).toContain('getDashboardGuestDiskStatusMessage');
expect(diskListModelSource).toContain('export const buildDashboardDiskPresentation');
expect(diskListModelSource).toContain('export const getDashboardDiskUsagePercent');
expect(diskListStateSource).toContain('getWorkloadGuestDiskStatusMessage');
expect(diskListModelSource).toContain('export const buildWorkloadsDiskPresentation');
expect(diskListModelSource).toContain('export const getWorkloadsDiskUsagePercent');
});
it('filterWorkloads returns all guests when no filters active', () => {
@@ -1,6 +1,6 @@
import { afterEach, describe, expect, it } from 'vitest';
import { cleanup, render } from '@solidjs/testing-library';
import { useEnhancedCPUBarState } from '@/components/Dashboard/useEnhancedCPUBarState';
import { useEnhancedCPUBarState } from '@/components/Workloads/useEnhancedCPUBarState';
afterEach(() => {
cleanup();
@@ -1,6 +1,6 @@
import { afterEach, describe, expect, it, vi } from 'vitest';
import { cleanup, render } from '@solidjs/testing-library';
import { useMetricBarState } from '@/components/Dashboard/useMetricBarState';
import { useMetricBarState } from '@/components/Workloads/useMetricBarState';
class MockResizeObserver {
callback: ResizeObserverCallback;
@@ -1,7 +1,7 @@
import { afterEach, describe, expect, it, vi } from 'vitest';
import { cleanup, render } from '@solidjs/testing-library';
import type { Disk } from '@/types/api';
import { useStackedDiskBarState } from '@/components/Dashboard/useStackedDiskBarState';
import { useStackedDiskBarState } from '@/components/Workloads/useStackedDiskBarState';
class MockResizeObserver {
callback: ResizeObserverCallback;
@@ -1,6 +1,6 @@
import { afterEach, describe, expect, it, vi } from 'vitest';
import { cleanup, render } from '@solidjs/testing-library';
import { useStackedMemoryBarState } from '@/components/Dashboard/useStackedMemoryBarState';
import { useStackedMemoryBarState } from '@/components/Workloads/useStackedMemoryBarState';
class MockResizeObserver {
callback: ResizeObserverCallback;
@@ -1,6 +1,6 @@
import { cleanup, renderHook } from '@solidjs/testing-library';
import { describe, expect, it, vi } from 'vitest';
import { useThresholdSliderState } from '@/components/Dashboard/useThresholdSliderState';
import { useThresholdSliderState } from '@/components/Workloads/useThresholdSliderState';
vi.mock('@/utils/temperature', () => ({
formatTemperature: (value: number) => `${value}°C`,
@@ -6,8 +6,8 @@ import type { SummarySeriesGroupScope } from '@/components/shared/summaryCardInt
import type { WorkloadGuest } from '@/types/workloads';
import { ROUTE_STATE_REPLACE_OPTIONS } from '@/utils/routeStateNavigation';
import { resolveDashboardResourceSelection } from '../dashboardSelectionModel';
import { useDashboardSelectionState } from '../useDashboardSelectionState';
import { resolveWorkloadResourceSelection } from '../workloadSelectionModel';
import { useWorkloadSelectionState } from '../useWorkloadSelectionState';
let locationSearch = '?resource=cluster-a:node-1:101';
const navigateSpy = vi.fn();
@@ -23,7 +23,7 @@ vi.mock('@solidjs/router', () => ({
useNavigate: () => navigateSpy,
}));
describe('useDashboardSelectionState', () => {
describe('useWorkloadSelectionState', () => {
beforeEach(() => {
locationSearch = '?resource=cluster-a:node-1:101';
navigateSpy.mockReset();
@@ -41,18 +41,18 @@ describe('useDashboardSelectionState', () => {
vi.unstubAllGlobals();
});
it('owns dashboard resource deep-link selection without mutating workload filters', () => {
it('owns workloads resource deep-link selection without mutating workload filters', () => {
const [filteredGuests] = createSignal<WorkloadGuest[]>([]);
const { result } = renderHook(() =>
useDashboardSelectionState({
useWorkloadSelectionState({
filteredGuests,
summaryGroupScopes: emptySummaryGroupScopes,
}),
);
expect(result.selectedGuestId()).toBe('cluster-a:node-1:101');
expect(resolveDashboardResourceSelection(locationSearch)).toEqual({
expect(resolveWorkloadResourceSelection(locationSearch)).toEqual({
resourceId: 'cluster-a:node-1:101',
summaryGroupId: null,
});
@@ -70,7 +70,7 @@ describe('useDashboardSelectionState', () => {
const [filteredGuests, setFilteredGuests] = createSignal<WorkloadGuest[]>([guest]);
const { result } = renderHook(() =>
useDashboardSelectionState({
useWorkloadSelectionState({
filteredGuests,
summaryGroupScopes: emptySummaryGroupScopes,
}),
@@ -88,14 +88,14 @@ describe('useDashboardSelectionState', () => {
const [filteredGuests] = createSignal<WorkloadGuest[]>([]);
const { result } = renderHook(() =>
useDashboardSelectionState({
useWorkloadSelectionState({
filteredGuests,
summaryGroupScopes: emptySummaryGroupScopes,
}),
);
expect(result.selectedGuestId()).toBe('app-container:truenas-main:nextcloud');
expect(resolveDashboardResourceSelection(locationSearch)).toEqual({
expect(resolveWorkloadResourceSelection(locationSearch)).toEqual({
resourceId: 'app-container:truenas-main:nextcloud',
summaryGroupId: null,
});
@@ -106,7 +106,7 @@ describe('useDashboardSelectionState', () => {
const [filteredGuests] = createSignal<WorkloadGuest[]>([]);
const { result } = renderHook(() =>
useDashboardSelectionState({
useWorkloadSelectionState({
filteredGuests,
summaryGroupScopes: emptySummaryGroupScopes,
}),
@@ -138,7 +138,7 @@ describe('useDashboardSelectionState', () => {
]);
const { result } = renderHook(() =>
useDashboardSelectionState({
useWorkloadSelectionState({
filteredGuests,
summaryGroupScopes: summaryScopes,
}),
@@ -160,7 +160,7 @@ describe('useDashboardSelectionState', () => {
const clearAdditionalPageStateOnEscape = vi.fn();
renderHook(() =>
useDashboardSelectionState({
useWorkloadSelectionState({
clearAdditionalPageStateOnEscape,
filteredGuests,
summaryGroupScopes: emptySummaryGroupScopes,
@@ -182,7 +182,7 @@ describe('useDashboardSelectionState', () => {
const [filteredGuests] = createSignal<WorkloadGuest[]>([]);
const { result } = renderHook(() =>
useDashboardSelectionState({
useWorkloadSelectionState({
filteredGuests,
summaryGroupScopes: emptySummaryGroupScopes,
}),
@@ -230,7 +230,7 @@ describe('useDashboardSelectionState', () => {
]);
const { result } = renderHook(() =>
useDashboardSelectionState({
useWorkloadSelectionState({
filteredGuests,
summaryGroupScopes: emptySummaryGroupScopes,
}),
@@ -288,7 +288,7 @@ describe('useDashboardSelectionState', () => {
window.scrollTo = scrollToSpy as typeof window.scrollTo;
const { result } = renderHook(() =>
useDashboardSelectionState({
useWorkloadSelectionState({
filteredGuests,
summaryGroupScopes: emptySummaryGroupScopes,
}),
@@ -369,7 +369,7 @@ describe('useDashboardSelectionState', () => {
};
const { result } = renderHook(() =>
useDashboardSelectionState({
useWorkloadSelectionState({
filteredGuests,
summaryGroupScopes: emptySummaryGroupScopes,
}),
@@ -408,7 +408,7 @@ describe('useDashboardSelectionState', () => {
let top = 120;
const { result } = renderHook(() =>
useDashboardSelectionState({
useWorkloadSelectionState({
filteredGuests,
summaryGroupScopes: summaryScopes,
}),
@@ -441,7 +441,7 @@ describe('useDashboardSelectionState', () => {
const [filteredGuests] = createSignal<WorkloadGuest[]>([]);
const { result } = renderHook(() =>
useDashboardSelectionState({
useWorkloadSelectionState({
filteredGuests,
summaryGroupScopes: emptySummaryGroupScopes,
}),
@@ -2,15 +2,15 @@ import { afterEach, describe, expect, it, vi } from 'vitest';
import { cleanup, render, waitFor } from '@solidjs/testing-library';
import { createSignal } from 'solid-js';
import { useDashboardWorkloadViewportSync } from '@/components/Dashboard/useDashboardWorkloadViewportSync';
import type { UseGroupedTableWindowingResult } from '@/components/Dashboard/useGroupedTableWindowing';
import { useWorkloadViewportSync } from '@/components/Workloads/useWorkloadViewportSync';
import type { UseGroupedTableWindowingResult } from '@/components/Workloads/useGroupedTableWindowing';
afterEach(() => {
cleanup();
vi.restoreAllMocks();
});
describe('useDashboardWorkloadViewportSync', () => {
describe('useWorkloadViewportSync', () => {
it('owns grouped workload viewport sync and listener cleanup', async () => {
const onScroll = vi.fn();
const addEventListenerSpy = vi.spyOn(window, 'addEventListener');
@@ -28,7 +28,7 @@ describe('useDashboardWorkloadViewportSync', () => {
const Harness = () => {
const [bodyRef, setBodyRef] = createSignal<HTMLTableSectionElement | null>(null);
useDashboardWorkloadViewportSync({
useWorkloadViewportSync({
filteredGuestCount: () => 640,
groupedWindowing,
rowHeight: 32,
@@ -1,7 +1,7 @@
import { renderHook } from '@solidjs/testing-library';
import { createSignal } from 'solid-js';
import { describe, expect, it, vi } from 'vitest';
import { useDashboardFilterState } from '@/components/Dashboard/useDashboardFilterState';
import { useWorkloadsFilterState } from '@/components/Workloads/useWorkloadsFilterState';
vi.mock('@/hooks/useBreakpoint', () => ({
useBreakpoint: () => ({
@@ -9,8 +9,8 @@ vi.mock('@/hooks/useBreakpoint', () => ({
}),
}));
describe('useDashboardFilterState', () => {
it('centralizes dashboard filter state and reset behavior', () => {
describe('useWorkloadsFilterState', () => {
it('centralizes workloads filter state and reset behavior', () => {
const [search, setSearch] = createSignal('query');
const [viewMode, setViewMode] = createSignal<
'all' | 'vm' | 'system-container' | 'app-container' | 'pod'
@@ -27,7 +27,7 @@ describe('useDashboardFilterState', () => {
const runtimeOnChange = vi.fn();
const { result } = renderHook(() =>
useDashboardFilterState({
useWorkloadsFilterState({
search,
setSearch,
viewMode,
@@ -1,26 +1,26 @@
import { describe, expect, it, vi } from 'vitest';
import {
buildDashboardContainerRuntimeFilterConfig,
buildDashboardHostFilterConfig,
buildDashboardNamespaceFilterConfig,
buildDashboardPlatformFilterConfig,
buildDashboardPlatformFilterOptions,
DASHBOARD_CONTAINER_RUNTIME_ALL_OPTION_LABEL,
DASHBOARD_KUBERNETES_CONTEXT_ALL_OPTION_LABEL,
DASHBOARD_KUBERNETES_CONTEXT_FILTER_LABEL,
DASHBOARD_NAMESPACE_ALL_OPTION_LABEL,
DASHBOARD_NODE_ALL_OPTION_LABEL,
DASHBOARD_PLATFORM_ALL_OPTION_LABEL,
DASHBOARD_WORKLOAD_TYPE_OPTIONS,
} from '../dashboardWorkloadFilterConfigModel';
buildWorkloadsContainerRuntimeFilterConfig,
buildWorkloadsHostFilterConfig,
buildWorkloadsNamespaceFilterConfig,
buildWorkloadsPlatformFilterConfig,
buildWorkloadsPlatformFilterOptions,
WORKLOADS_CONTAINER_RUNTIME_ALL_OPTION_LABEL,
WORKLOADS_KUBERNETES_CONTEXT_ALL_OPTION_LABEL,
WORKLOADS_KUBERNETES_CONTEXT_FILTER_LABEL,
WORKLOADS_NAMESPACE_ALL_OPTION_LABEL,
WORKLOADS_NODE_ALL_OPTION_LABEL,
WORKLOADS_PLATFORM_ALL_OPTION_LABEL,
WORKLOAD_TYPE_OPTIONS,
} from '../workloadFilterConfigModel';
describe('dashboardWorkloadFilterConfigModel', () => {
describe('workloadFilterConfigModel', () => {
it('builds container runtime config only for app-container workload routes', () => {
const onChange = vi.fn();
expect(
buildDashboardContainerRuntimeFilterConfig({
buildWorkloadsContainerRuntimeFilterConfig({
isWorkloadsRoute: true,
viewMode: 'app-container',
containerRuntime: 'docker',
@@ -32,14 +32,14 @@ describe('dashboardWorkloadFilterConfigModel', () => {
label: 'Runtime',
value: 'docker',
options: [
{ value: '', label: DASHBOARD_CONTAINER_RUNTIME_ALL_OPTION_LABEL },
{ value: '', label: WORKLOADS_CONTAINER_RUNTIME_ALL_OPTION_LABEL },
{ value: 'containerd', label: 'containerd' },
{ value: 'docker', label: 'docker' },
],
});
expect(
buildDashboardContainerRuntimeFilterConfig({
buildWorkloadsContainerRuntimeFilterConfig({
isWorkloadsRoute: true,
viewMode: 'vm',
containerRuntime: 'docker',
@@ -54,7 +54,7 @@ describe('dashboardWorkloadFilterConfigModel', () => {
const onNodeChange = vi.fn();
expect(
buildDashboardHostFilterConfig({
buildWorkloadsHostFilterConfig({
isWorkloadsRoute: true,
viewMode: 'pod',
selectedKubernetesContext: 'prod',
@@ -66,17 +66,17 @@ describe('dashboardWorkloadFilterConfigModel', () => {
}),
).toMatchObject({
id: 'workloads-k8s-context-filter',
label: DASHBOARD_KUBERNETES_CONTEXT_FILTER_LABEL,
label: WORKLOADS_KUBERNETES_CONTEXT_FILTER_LABEL,
value: 'prod',
options: [
{ value: '', label: DASHBOARD_KUBERNETES_CONTEXT_ALL_OPTION_LABEL },
{ value: '', label: WORKLOADS_KUBERNETES_CONTEXT_ALL_OPTION_LABEL },
{ value: 'prod', label: 'prod' },
{ value: 'stage', label: 'stage' },
],
});
expect(
buildDashboardHostFilterConfig({
buildWorkloadsHostFilterConfig({
isWorkloadsRoute: true,
viewMode: 'vm',
selectedKubernetesContext: null,
@@ -91,7 +91,7 @@ describe('dashboardWorkloadFilterConfigModel', () => {
label: 'Node',
value: 'cluster-a-node-a',
options: [
{ value: '', label: DASHBOARD_NODE_ALL_OPTION_LABEL },
{ value: '', label: WORKLOADS_NODE_ALL_OPTION_LABEL },
{ value: 'cluster-a-node-a', label: 'node-a' },
],
});
@@ -101,7 +101,7 @@ describe('dashboardWorkloadFilterConfigModel', () => {
const onChange = vi.fn();
expect(
buildDashboardNamespaceFilterConfig({
buildWorkloadsNamespaceFilterConfig({
isWorkloadsRoute: true,
viewMode: 'pod',
selectedNamespace: 'default',
@@ -113,14 +113,14 @@ describe('dashboardWorkloadFilterConfigModel', () => {
label: 'Namespace',
value: 'default',
options: [
{ value: '', label: DASHBOARD_NAMESPACE_ALL_OPTION_LABEL },
{ value: '', label: WORKLOADS_NAMESPACE_ALL_OPTION_LABEL },
{ value: 'default', label: 'default' },
{ value: 'kube-system', label: 'kube-system' },
],
});
expect(
buildDashboardNamespaceFilterConfig({
buildWorkloadsNamespaceFilterConfig({
isWorkloadsRoute: true,
viewMode: 'pod',
selectedNamespace: 'default',
@@ -134,7 +134,7 @@ describe('dashboardWorkloadFilterConfigModel', () => {
const onChange = vi.fn();
expect(
buildDashboardPlatformFilterConfig({
buildWorkloadsPlatformFilterConfig({
isWorkloadsRoute: true,
selectedPlatform: 'truenas',
platformOptions: [
@@ -148,7 +148,7 @@ describe('dashboardWorkloadFilterConfigModel', () => {
label: 'Platform',
value: 'truenas',
options: [
{ value: '', label: DASHBOARD_PLATFORM_ALL_OPTION_LABEL },
{ value: '', label: WORKLOADS_PLATFORM_ALL_OPTION_LABEL },
{ value: 'docker', label: 'Docker' },
{ value: 'truenas', label: 'TrueNAS' },
],
@@ -157,12 +157,12 @@ describe('dashboardWorkloadFilterConfigModel', () => {
it('keeps the selected platform selectable when the current type narrows available options', () => {
expect(
buildDashboardPlatformFilterOptions('proxmox-pve', [
buildWorkloadsPlatformFilterOptions('proxmox-pve', [
{ value: 'truenas', label: 'TrueNAS' },
{ value: 'docker', label: 'Docker / Podman' },
]),
).toEqual([
{ value: '', label: DASHBOARD_PLATFORM_ALL_OPTION_LABEL },
{ value: '', label: WORKLOADS_PLATFORM_ALL_OPTION_LABEL },
{ value: 'proxmox-pve', label: 'PVE' },
{ value: 'truenas', label: 'TrueNAS' },
{ value: 'docker', label: 'Docker / Podman' },
@@ -170,7 +170,7 @@ describe('dashboardWorkloadFilterConfigModel', () => {
});
it('keeps workload type filter labels in the presentation model', () => {
expect(DASHBOARD_WORKLOAD_TYPE_OPTIONS).toEqual([
expect(WORKLOAD_TYPE_OPTIONS).toEqual([
{ value: 'all', label: 'All' },
{ value: 'vm', label: 'VMs' },
{ value: 'system-container', label: 'System containers' },
@@ -2,13 +2,13 @@ import { describe, expect, it } from 'vitest';
import type { WorkloadGuest } from '@/types/workloads';
import {
buildDashboardContainerRuntimeOptions,
buildDashboardKubernetesContextOptions,
buildDashboardKubernetesNamespaceOptions,
buildDashboardPlatformOptions,
buildDashboardWorkloadNodeOptions,
deserializeDashboardWorkloadViewMode,
} from '../dashboardWorkloadRouteModel';
buildWorkloadsContainerRuntimeOptions,
buildWorkloadsKubernetesContextOptions,
buildWorkloadsKubernetesNamespaceOptions,
buildWorkloadsPlatformOptions,
buildWorkloadNodeOptions,
deserializeWorkloadViewMode,
} from '../workloadRouteModel';
const makeGuest = (overrides?: Partial<WorkloadGuest>): WorkloadGuest =>
({
@@ -37,17 +37,17 @@ const makeGuest = (overrides?: Partial<WorkloadGuest>): WorkloadGuest =>
...overrides,
}) as WorkloadGuest;
describe('dashboardWorkloadRouteModel', () => {
describe('workloadRouteModel', () => {
it('deserializes persisted workload view-mode aliases through the canonical helper', () => {
expect(deserializeDashboardWorkloadViewMode('all')).toBe('all');
expect(deserializeDashboardWorkloadViewMode('docker')).toBe('app-container');
expect(deserializeDashboardWorkloadViewMode('Kubernetes')).toBe('pod');
expect(deserializeDashboardWorkloadViewMode('invalid')).toBe('all');
expect(deserializeDashboardWorkloadViewMode(null)).toBe('all');
expect(deserializeWorkloadViewMode('all')).toBe('all');
expect(deserializeWorkloadViewMode('docker')).toBe('app-container');
expect(deserializeWorkloadViewMode('Kubernetes')).toBe('pod');
expect(deserializeWorkloadViewMode('invalid')).toBe('all');
expect(deserializeWorkloadViewMode(null)).toBe('all');
});
it('builds workload node options with instance disambiguation only when needed', () => {
const options = buildDashboardWorkloadNodeOptions([
const options = buildWorkloadNodeOptions([
makeGuest({ id: 'vm-a', node: 'node-a', instance: 'cluster-a' }),
makeGuest({ id: 'vm-b', node: 'node-a', instance: 'cluster-b' }),
makeGuest({ id: 'vm-c', node: 'node-z', instance: 'cluster-a' }),
@@ -62,7 +62,7 @@ describe('dashboardWorkloadRouteModel', () => {
});
it('builds app-container host options from Docker host ids and host labels', () => {
const options = buildDashboardWorkloadNodeOptions([
const options = buildWorkloadNodeOptions([
makeGuest({
id: 'docker-a',
type: 'app-container',
@@ -124,15 +124,15 @@ describe('dashboardWorkloadRouteModel', () => {
}),
];
expect(buildDashboardKubernetesContextOptions(guests)).toEqual(['prod', 'stage']);
expect(buildDashboardKubernetesNamespaceOptions(guests, 'prod')).toEqual([
expect(buildWorkloadsKubernetesContextOptions(guests)).toEqual(['prod', 'stage']);
expect(buildWorkloadsKubernetesNamespaceOptions(guests, 'prod')).toEqual([
'default',
'kube-system',
]);
});
it('builds container runtime options from canonical app-container guests', () => {
expect(
buildDashboardContainerRuntimeOptions([
buildWorkloadsContainerRuntimeOptions([
makeGuest({
id: 'docker-a',
type: 'app-container',
@@ -157,7 +157,7 @@ describe('dashboardWorkloadRouteModel', () => {
it('builds canonical platform options from visible workload platforms', () => {
expect(
buildDashboardPlatformOptions(
buildWorkloadsPlatformOptions(
[
makeGuest({
id: 'app-a',
@@ -211,10 +211,10 @@ describe('dashboardWorkloadRouteModel', () => {
}),
];
expect(buildDashboardKubernetesNamespaceOptions(guests, 'prod')).toEqual([
expect(buildWorkloadsKubernetesNamespaceOptions(guests, 'prod')).toEqual([
'default',
'kube-system',
]);
expect(buildDashboardKubernetesNamespaceOptions(guests, 'stage')).toEqual(['observability']);
expect(buildWorkloadsKubernetesNamespaceOptions(guests, 'stage')).toEqual(['observability']);
});
});
@@ -1,21 +1,21 @@
import { describe, expect, it } from 'vitest';
import {
DASHBOARD_WORKLOAD_ROUTE_RESET_STATE,
deserializeDashboardContainerRuntime,
resolveDashboardWorkloadNodeSelection,
} from '../dashboardWorkloadRouteStateModel';
WORKLOADS_WORKLOAD_ROUTE_RESET_STATE,
deserializeWorkloadsContainerRuntime,
resolveWorkloadsWorkloadNodeSelection,
} from '../workloadRouteStateModel';
describe('dashboardWorkloadRouteStateModel', () => {
describe('workloadRouteStateModel', () => {
it('deserializes persisted container runtime values through the canonical helper', () => {
expect(deserializeDashboardContainerRuntime('docker')).toBe('docker');
expect(deserializeDashboardContainerRuntime('')).toBe('');
expect(deserializeDashboardContainerRuntime(null)).toBe('');
expect(deserializeDashboardContainerRuntime(42)).toBe('');
expect(deserializeWorkloadsContainerRuntime('docker')).toBe('docker');
expect(deserializeWorkloadsContainerRuntime('')).toBe('');
expect(deserializeWorkloadsContainerRuntime(null)).toBe('');
expect(deserializeWorkloadsContainerRuntime(42)).toBe('');
});
it('exports the canonical workload-route reset state', () => {
expect(DASHBOARD_WORKLOAD_ROUTE_RESET_STATE).toEqual({
expect(WORKLOADS_WORKLOAD_ROUTE_RESET_STATE).toEqual({
selectedNode: null,
selectedHostHint: null,
selectedPlatform: null,
@@ -28,7 +28,7 @@ describe('dashboardWorkloadRouteStateModel', () => {
it('applies host-node selection only for pve or neutral route contexts', () => {
expect(
resolveDashboardWorkloadNodeSelection({
resolveWorkloadsWorkloadNodeSelection({
nodeId: 'cluster-a-node-a',
nodeType: 'pve',
showFilters: false,
@@ -41,7 +41,7 @@ describe('dashboardWorkloadRouteStateModel', () => {
});
expect(
resolveDashboardWorkloadNodeSelection({
resolveWorkloadsWorkloadNodeSelection({
nodeId: 'cluster-a-node-a',
nodeType: 'pbs',
showFilters: false,
@@ -3,19 +3,19 @@ import { describe, expect, it } from 'vitest';
import type { WorkloadGuest } from '@/types/workloads';
import {
dashboardHasHoveredWorkload,
resolveDashboardResourceSelection,
resolveDashboardSelectionNavigateTarget,
} from '../dashboardSelectionModel';
workloadsHasHoveredWorkload,
resolveWorkloadResourceSelection,
resolveWorkloadsSelectionNavigateTarget,
} from '../workloadSelectionModel';
describe('dashboardSelectionModel', () => {
it('resolves dashboard resource deep links into focused guest ids without inventing filters', () => {
expect(resolveDashboardResourceSelection('?resource=cluster-a:node-1:101')).toEqual({
describe('workloadSelectionModel', () => {
it('resolves workloads resource deep links into focused guest ids without inventing filters', () => {
expect(resolveWorkloadResourceSelection('?resource=cluster-a:node-1:101')).toEqual({
resourceId: 'cluster-a:node-1:101',
summaryGroupId: null,
});
expect(
resolveDashboardResourceSelection(
resolveWorkloadResourceSelection(
'?type=app-container&resource=app-container:truenas-main:nextcloud',
),
).toEqual({
@@ -23,16 +23,16 @@ describe('dashboardSelectionModel', () => {
summaryGroupId: null,
});
expect(
resolveDashboardResourceSelection('?resource=app-container:docker-main:container-123'),
resolveWorkloadResourceSelection('?resource=app-container:docker-main:container-123'),
).toEqual({
resourceId: 'app-container:docker-main:container-123',
summaryGroupId: null,
});
expect(resolveDashboardResourceSelection('?resource=guest-1')).toEqual({
expect(resolveWorkloadResourceSelection('?resource=guest-1')).toEqual({
resourceId: 'guest-1',
summaryGroupId: null,
});
expect(resolveDashboardResourceSelection('')).toBeNull();
expect(resolveWorkloadResourceSelection('')).toBeNull();
});
it('checks hovered workload continuity against canonical workload ids', () => {
@@ -47,13 +47,13 @@ describe('dashboardSelectionModel', () => {
} as unknown as WorkloadGuest,
];
expect(dashboardHasHoveredWorkload(guests, 'cluster-a:node-1:101')).toBe(true);
expect(dashboardHasHoveredWorkload(guests, 'cluster-a:node-1:102')).toBe(false);
expect(workloadsHasHoveredWorkload(guests, 'cluster-a:node-1:101')).toBe(true);
expect(workloadsHasHoveredWorkload(guests, 'cluster-a:node-1:102')).toBe(false);
});
it('builds route-backed workload selection targets without dropping other filters', () => {
expect(
resolveDashboardSelectionNavigateTarget({
resolveWorkloadsSelectionNavigateTarget({
pathname: '/workloads',
search: '?type=app-container&platform=truenas&agent=truenas-main',
resourceId: 'app-container:truenas-main:nextcloud',
@@ -64,7 +64,7 @@ describe('dashboardSelectionModel', () => {
);
expect(
resolveDashboardSelectionNavigateTarget({
resolveWorkloadsSelectionNavigateTarget({
pathname: '/workloads',
search:
'?type=app-container&platform=truenas&agent=truenas-main&resource=app-container%3Atruenas-main%3Anextcloud',
@@ -74,7 +74,7 @@ describe('dashboardSelectionModel', () => {
).toBe('/workloads?type=app-container&platform=truenas&agent=truenas-main');
expect(
resolveDashboardSelectionNavigateTarget({
resolveWorkloadsSelectionNavigateTarget({
pathname: '/workloads',
search:
'?type=app-container&platform=truenas&agent=truenas-main&resource=app-container%3Atruenas-main%3Anextcloud',
@@ -84,7 +84,7 @@ describe('dashboardSelectionModel', () => {
).toBeNull();
expect(
resolveDashboardSelectionNavigateTarget({
resolveWorkloadsSelectionNavigateTarget({
pathname: '/workloads',
search: '?type=app-container&platform=truenas&agent=truenas-main',
resourceId: null,
@@ -95,7 +95,7 @@ describe('dashboardSelectionModel', () => {
);
expect(
resolveDashboardSelectionNavigateTarget({
resolveWorkloadsSelectionNavigateTarget({
pathname: '/workloads',
search:
'?type=app-container&platform=truenas&agent=truenas-main&summaryGroup=docker-host%3Atruenas-main',
@@ -8,8 +8,8 @@ import {
getDiskUsagePercent,
getWorkloadGroupKey,
groupWorkloads,
} from '@/components/Dashboard/workloadSelectors';
import { workloadNodeScopeId } from '@/components/Dashboard/workloadTopology';
} from '@/components/Workloads/workloadSelectors';
import { workloadNodeScopeId } from '@/components/Workloads/workloadTopology';
const makeGuest = (i: number, overrides?: Partial<WorkloadGuest>): WorkloadGuest => ({
id: `guest-${i}`,

Some files were not shown because too many files have changed in this diff Show More