## Summary - Add a History API sync layer (`useUrlSync`) that maps shell state to `/nodes//...` paths without react-router. - Gate URL normalization on permissions and license readiness; preserve the current URL on authz read failures (fail closed). - Add stack-list frozen error UI with retry, transition-specific dirty guards for popstate, and user docs at `docs/features/deep-links.mdx`. - Fix cold-load/refresh regressions: skip dashboard reset on initial node mount; defer URL writer until hydrated `activeView` matches the route. - Mobile URL contract: `/dashboard` opens Home (content surface); `/stacks` opens the stack list. List surface always writes `/stacks` even when `activeView` is Fleet or Resources. - Mobile stack deep links: hydrate `pendingDetailStack`, use `loadFileForRoute` + `routeDetailError` for compose failures (frozen URL, `MobileDetailError` + Retry). - **QA follow-up (`20b35df7`):** Bootstrap `activeView`/tabs from URL via `readUrlRouteState` so Fleet/Security/Resources cold-load correctly; settle route phase when state already matches (fixes in-app URL writes); normalize unknown view segments; open Monaco editor tabs from `/compose`/`/env`/`/files` deep links. ## Test plan - [x] `cd frontend && npx tsc -b --noEmit` - [x] Unit: `readUrlRouteState`, `useUrlSync`, `senchoRoute`, `useViewNavigationState` (52+ tests) - [x] E2E routing spec expanded (shell view content assertions, mobile cases) — re-run on CI / fresh preview - [ ] Manual QA on rebuilt preview image: - [ ] Cold load `/nodes/local/fleet`, `/security/images`, `/resources` (correct view, not Dashboard) - [ ] Dashboard → Fleet → Security updates address bar - [ ] `/stacks//env` opens Monaco env tab (not Anatomy-only) - [ ] `/nodes/local/not-a-view` normalizes to dashboard - [ ] Mobile stack deep link + compose failure retry (from prior commit) ## Notes - Internal architecture docs are local-only (gitignored), not in this PR. - Anatomy panel sub-tabs remain URL-opaque; only Monaco editor tabs update the path. - Remote node slug suffix (`name-id`) is existing `nodeSlug` behavior; not changed in this PR.