diff --git a/docs/release-control/v6/internal/subsystems/frontend-primitives.md b/docs/release-control/v6/internal/subsystems/frontend-primitives.md
index e0a3bd443..98e010e3c 100644
--- a/docs/release-control/v6/internal/subsystems/frontend-primitives.md
+++ b/docs/release-control/v6/internal/subsystems/frontend-primitives.md
@@ -1307,14 +1307,16 @@ pushing listener lifecycle or trigger policy back into the shared shell.
The shared what's-new modal now follows that same owner split.
`frontend-modern/src/components/shared/WhatsNewModal.tsx` stays the render
shell, `frontend-modern/src/components/shared/useWhatsNewModalState.ts` owns
-local-storage dismissal, session dismissal, and close behavior, and
+local-storage dismissal, session dismissal, step progression, spotlight target
+resolution, and overlay placement/runtime behavior, and
`frontend-modern/src/components/shared/whatsNewModalModel.ts` owns the feature
-card catalog, telemetry copy, labels, and canonical docs/privacy links. Future
+tour catalog, telemetry copy, labels, and canonical docs/privacy links. Future
what's-new work should extend those owners instead of pushing dismissal state,
-product copy, or external links back into the shared shell. Internal product
-navigation from that shell should still route through canonical shared helpers
-such as `frontend-modern/src/routing/resourceLinks.ts` rather than freezing raw
-`/recovery?...` route strings into the modal itself.
+spotlight runtime, product copy, or external links back into the shared shell.
+The v6 welcome surface is one guided spotlight tour, not a modal plus a second
+dashboard-only migration hint: it must dim the live app, glow the real
+primary-navigation target being described, and keep route-orientation copy on
+the existing welcome flow instead of layering a duplicate in-product banner.
That state owner now also owns public-demo suppression: the modal must stay
closed until `sessionPresentationPolicyResolved()` is true and must fail closed
when `presentationPolicyIsDemoMode()` resolves true, so the public demo does
@@ -1419,14 +1421,6 @@ 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 route shell also owns the v5-to-v6 navigation continuity
-notice. When the populated dashboard needs to orient operators who still
-expect the old Proxmox, Docker, or Hosts tabs, `frontend-modern/src/pages/Dashboard.tsx`
-must render the shared `MigrationNoticeBanner` with a dismissible local-storage
-backing key, route the learn-more affordance to the shipped
-`/docs/MIGRATION_UNIFIED_NAV.md` guide, and keep the mapping explicit that
-Infrastructure now holds platform roots while Workloads now holds VMs,
-containers, pods, and container update status.
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
diff --git a/docs/release-control/v6/internal/subsystems/performance-and-scalability.md b/docs/release-control/v6/internal/subsystems/performance-and-scalability.md
index cb1cbed3a..d9eca073f 100644
--- a/docs/release-control/v6/internal/subsystems/performance-and-scalability.md
+++ b/docs/release-control/v6/internal/subsystems/performance-and-scalability.md
@@ -244,12 +244,6 @@ regression protection.
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.
-32. Keep dashboard migration continuity on the same hot path passive. When
- `frontend-modern/src/pages/Dashboard.tsx` renders the dismissible v5-to-v6
- navigation notice, that surface must read and persist only a local
- dismissal flag and shipped-doc link state; it must not introduce another
- dashboard fetch, route-blocking readiness check, or legacy topology scan
- just to decide whether the notice appears.
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,
@@ -364,12 +358,6 @@ page-local “selected row” overlays on top of already downsampled summary
history. Hovering a sparkline or density map for one entity must promote that
entity into the shared active series so sibling cards highlight the same object
at once rather than maintaining chart-local hover state, and the synchronized
-The dashboard route shell may now also carry a dismissible migration notice for
-operators coming from v5 navigation, but that continuity affordance stays on
-the same protected hot path. The notice must remain a static route-shell
-projection layered above the existing action/KPI widgets, with dismissal backed
-by local storage only; it must not fork a second dashboard readiness state,
-delay KPI hydration, or displace the governed summary-before-detail scan order.
hover timestamp must remain visible across those sibling cards even when the
active entity has no samples for one metric in the current range. Those
sibling cards should expose the synchronized value through one compact
diff --git a/docs/release-control/v6/internal/subsystems/storage-recovery.md b/docs/release-control/v6/internal/subsystems/storage-recovery.md
index 8cc200113..9615cd180 100644
--- a/docs/release-control/v6/internal/subsystems/storage-recovery.md
+++ b/docs/release-control/v6/internal/subsystems/storage-recovery.md
@@ -1292,14 +1292,6 @@ 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.
-That same dashboard route shell may also surface a dismissible migration
-continuity notice for operators coming from v5 Proxmox, Docker, and Hosts
-tabs, but the notice must preserve the same scan order: continuity notice
-first when present, action-required summary next, KPI strip after that, and
-problem detail below the snapshot layer. The notice may explain that platform
-roots live under Infrastructure and workloads plus container update status live
-under Workloads, but it must stay read-only and must not push storage/recovery
-widgets or the KPI summary below the problem-resources table again.
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
diff --git a/frontend-modern/public/docs/MIGRATION_UNIFIED_NAV.md b/frontend-modern/public/docs/MIGRATION_UNIFIED_NAV.md
new file mode 100644
index 000000000..fcb781d40
--- /dev/null
+++ b/frontend-modern/public/docs/MIGRATION_UNIFIED_NAV.md
@@ -0,0 +1,63 @@
+# Migration Guide: Unified Navigation
+
+This guide explains what changed in unified navigation and where legacy pages moved in v6.
+
+## What Changed
+- Navigation is now organized by **task** (Infrastructure, Workloads, Storage, Recovery) instead of by platform.
+- Legacy pages (Proxmox Overview, Hosts, Docker, Services, Kubernetes) were replaced by unified views.
+- Global search and keyboard shortcuts make navigation faster across all resources.
+- Kubernetes is now split by intent:
+ - **Infrastructure** shows Kubernetes clusters and nodes.
+ - **Workloads** shows Kubernetes pods with the same filters/grouping as VMs and containers.
+
+## Why This Change
+- A unified resource model enables one inventory and one search across platforms.
+- Filters, drawers, and workflows stay consistent, instead of being re-implemented per platform page.
+- New integrations can be added without expanding the top-level navigation indefinitely.
+
+## Legacy Aliases and Redirects
+- Legacy aliases have been fully removed; update bookmarks and runbooks to canonical routes.
+- Optional migration aid: enable the **Classic shortcuts** bar in the main navigation (Settings → System → General).
+- Plan automation/bookmarks to use canonical routes now:
+ - `/infrastructure?source=pmg`
+ - `/workloads?type=k8s`
+
+## Where Old Pages Moved
+
+| Legacy Page | New Location |
+|------------|--------------|
+| Proxmox Overview | `/infrastructure` |
+| Hosts | `/infrastructure` |
+| Docker | `/workloads` (containers) + `/infrastructure` (hosts) |
+| Proxmox Storage | `/storage` |
+| Proxmox Backups | `/recovery` |
+| Proxmox Replication | `/recovery?view=events&mode=remote` |
+| Proxmox Ceph | `/ceph` (summary also visible in Storage) |
+| Proxmox Mail Gateway | `/infrastructure?source=pmg` |
+| Services | `/infrastructure?source=pmg` |
+| Kubernetes | `/workloads?type=k8s` |
+
+## New Features to Know
+
+### Global Search
+- Press `/` to focus search.
+- Search by name, node, type, tags, or status.
+- Results navigate directly to the relevant view.
+ - Use `Cmd/Ctrl+K` for the command palette.
+
+### Keyboard Shortcuts
+- `g i` → Infrastructure
+- `g w` → Workloads
+- `g s` → Storage
+- `g b` → Recovery
+- `g a` → Alerts
+- `g t` → Settings
+- `?` → Shortcut help
+
+### Debug Drawer (Optional)
+- Enable with localStorage key `pulse_debug_mode` for raw JSON in resource drawer.
+
+## Tips
+- If you used Docker and Hosts pages before, start with **Infrastructure** (hosts) and **Workloads** (containers).
+- If you used the Kubernetes page before, use **Infrastructure** for cluster/node health and **Workloads** for pod-level operations.
+- The new pages support unified filters, tags, and search across all sources.
diff --git a/frontend-modern/scripts/sync-public-docs.mjs b/frontend-modern/scripts/sync-public-docs.mjs
index 45371fd53..361dac3f5 100644
--- a/frontend-modern/scripts/sync-public-docs.mjs
+++ b/frontend-modern/scripts/sync-public-docs.mjs
@@ -10,6 +10,7 @@ const targetDocsDir = path.join(frontendRoot, 'public', 'docs');
const shippedDocs = [
{ source: path.join(repoRoot, 'docs', 'README.md'), target: 'README.md' },
+ { source: path.join(repoRoot, 'docs', 'MIGRATION_UNIFIED_NAV.md'), target: 'MIGRATION_UNIFIED_NAV.md' },
{ source: path.join(repoRoot, 'docs', 'PRIVACY.md'), target: 'PRIVACY.md' },
{ source: path.join(repoRoot, 'docs', 'CONFIGURATION.md'), target: 'CONFIGURATION.md' },
{ source: path.join(repoRoot, 'docs', 'PROXY_AUTH.md'), target: 'PROXY_AUTH.md' },
diff --git a/frontend-modern/src/components/shared/SharedPrimitives.guardrails.test.ts b/frontend-modern/src/components/shared/SharedPrimitives.guardrails.test.ts
index 9442086d1..14c06d746 100644
--- a/frontend-modern/src/components/shared/SharedPrimitives.guardrails.test.ts
+++ b/frontend-modern/src/components/shared/SharedPrimitives.guardrails.test.ts
@@ -1120,11 +1120,13 @@ describe('shared primitive guardrails', () => {
it('keeps whats new modal on shell, runtime, and model owners', () => {
expect(whatsNewModalSource).toContain('useWhatsNewModalState');
+ expect(whatsNewModalSource).toContain('useDialogState');
expect(whatsNewModalSource).toContain('WHATS_NEW_FEATURE_CARDS');
+ expect(whatsNewModalSource).toContain('Portal');
expect(whatsNewModalSource).not.toContain('createLocalStorageBooleanSignal');
expect(whatsNewModalSource).not.toContain('createSignal');
expect(whatsNewModalSource).not.toContain('WHATS_NEW_NAV_V2_SHOWN');
- expect(whatsNewModalSource).not.toContain('Documentation');
+ expect(whatsNewModalSource).not.toContain('Migration guide');
expect(whatsNewModalSource).not.toContain(
'https://github.com/rcourtman/Pulse/blob/main/docs/PRIVACY.md',
);
@@ -1132,17 +1134,21 @@ describe('shared primitive guardrails', () => {
expect(whatsNewModalStateSource).toContain('export function useWhatsNewModalState');
expect(whatsNewModalStateSource).toContain('createLocalStorageBooleanSignal');
expect(whatsNewModalStateSource).toContain('createSignal');
+ expect(whatsNewModalStateSource).toContain('createMemo');
expect(whatsNewModalStateSource).toContain('STORAGE_KEYS.WHATS_NEW_NAV_V2_SHOWN');
expect(whatsNewModalStateSource).toContain('sessionPresentationPolicyResolved');
expect(whatsNewModalStateSource).toContain('presentationPolicyIsDemoMode');
expect(whatsNewModalStateSource).toContain('handleClose');
+ expect(whatsNewModalStateSource).toContain('handleNext');
+ expect(whatsNewModalStateSource).toContain('spotlightStyle');
expect(whatsNewModalModelSource).toContain('WHATS_NEW_FEATURE_CARDS');
expect(whatsNewModalModelSource).toContain('WHATS_NEW_TELEMETRY_TITLE');
expect(whatsNewModalModelSource).toContain('WHATS_NEW_DOCS_URL');
expect(whatsNewModalModelSource).toContain('WHATS_NEW_PRIVACY_URL');
expect(whatsNewModalModelSource).toContain('WHATS_NEW_DOCS_LABEL');
- expect(whatsNewModalModelSource).toContain("title: 'Infrastructure'");
+ expect(whatsNewModalModelSource).toContain('MIGRATION_GUIDE_DOC_URL');
+ expect(whatsNewModalModelSource).toContain("title: 'Dashboard'");
});
it('keeps dialog stack visibility in the shared dialog runtime', () => {
diff --git a/frontend-modern/src/components/shared/WhatsNewModal.tsx b/frontend-modern/src/components/shared/WhatsNewModal.tsx
index 5c5183d1f..17baa270d 100644
--- a/frontend-modern/src/components/shared/WhatsNewModal.tsx
+++ b/frontend-modern/src/components/shared/WhatsNewModal.tsx
@@ -1,5 +1,6 @@
-import { For } from 'solid-js';
-import { Dialog } from '@/components/shared/Dialog';
+import { For, Show } from 'solid-js';
+import { Portal } from 'solid-js/web';
+import LayoutDashboardIcon from 'lucide-solid/icons/layout-dashboard';
import ServerIcon from 'lucide-solid/icons/server';
import BoxesIcon from 'lucide-solid/icons/boxes';
import HardDriveIcon from 'lucide-solid/icons/hard-drive';
@@ -7,16 +8,17 @@ import ShieldCheckIcon from 'lucide-solid/icons/shield-check';
import ChartBarIcon from 'lucide-solid/icons/chart-bar';
import ExternalLinkIcon from 'lucide-solid/icons/external-link';
import XIcon from 'lucide-solid/icons/x';
-import { buildRecoveryPath } from '@/routing/resourceLinks';
import {
+ WHATS_NEW_BACK_LABEL,
WHATS_NEW_CLOSE_LABEL,
WHATS_NEW_DOCS_LABEL,
WHATS_NEW_DOCS_URL,
WHATS_NEW_DO_NOT_SHOW_LABEL,
WHATS_NEW_FEATURE_CARDS,
+ WHATS_NEW_NEXT_LABEL,
WHATS_NEW_PRIMARY_ACTION_LABEL,
WHATS_NEW_PRIVACY_URL,
- WHATS_NEW_RECOVERY_LINK_LABEL,
+ WHATS_NEW_SKIP_LABEL,
WHATS_NEW_SUBTITLE,
WHATS_NEW_TELEMETRY_COPY,
WHATS_NEW_TELEMETRY_ENV_VAR,
@@ -26,10 +28,13 @@ import {
WHATS_NEW_TITLE,
type WhatsNewFeatureCard,
} from './whatsNewModalModel';
+import { useDialogState } from './useDialogState';
import { useWhatsNewModalState } from './useWhatsNewModalState';
function WhatsNewFeatureIcon(props: { card: WhatsNewFeatureCard }) {
switch (props.card.icon) {
+ case 'dashboard':
+ return ;
case 'infrastructure':
return ;
case 'workloads':
@@ -43,112 +48,172 @@ function WhatsNewFeatureIcon(props: { card: WhatsNewFeatureCard }) {
export function WhatsNewModal() {
const state = useWhatsNewModalState();
- const recoveryEventsHref = buildRecoveryPath({ view: 'events', mode: 'remote' });
+ const dialogState = useDialogState({
+ get isOpen() {
+ return state.isOpen();
+ },
+ onClose: state.handleClose,
+ });
+ const step = () => state.currentStep();
+ const setPanelRef = (element: HTMLDivElement) => {
+ state.setPanelRef(element);
+ dialogState.setPanelRef(element);
+ };
return (
-