Feature: Nuxt UI shell foundation and router guards As the maintainer of RenewCMS I want the SPA to run on Nuxt UI instead of PrimeVue, with AuthShell and DashboardShell layouts, an AppBoot skeleton, and a router guard that resolves and caches the session once per app load So that every later screen slice can build on a themed, guarded shell instead of an unguarded router with no layout Background: Given web/src/main.ts wires the app through the @nuxt/ui/vite plugin, and no longer imports primevue, @primeuix/themes, primeicons or tailwindcss-primeui And web/src/theme/ui.config.ts and web/src/assets/main.css hold the token files copied from docs/design/source/tokens/ (ui.config.ts, renewcms.css), and public/favicon.svg is the file copied from docs/design/source/brand/favicon.svg And the router declares route meta.auth as one of "public", "session", "verified" or "any", and meta.title for every verified route And a global router.beforeEach guard runs on every navigation, and on the first navigation of the app it resolves the session by calling GET /v1/auth/me once and caches the result (session or "no session") in a session Pinia store; later navigations in the same app load reuse that cached result instead of calling GET /v1/auth/me again And /home, /article and /integration currently render only a trivial placeholder view inside DashboardShell; later screen slices (Home, Articles, Integration) replace that placeholder content with the real screens Scenario: A public route never triggers a session check Given the target route is /login, whose meta.auth is "public" When the router navigates to it as the first navigation of the app Then GET /v1/auth/me is not called And AuthShell renders immediately, with no AppBoot skeleton shown Scenario: The first navigation to a non-public route resolves the session and shows AppBoot meanwhile Given the target route is /home, whose meta.auth is "verified" And GET /v1/auth/me has not resolved yet When the router navigates to /home as the first navigation of the app Then DashboardShell renders with the AppBoot skeleton in place of the page content When GET /v1/auth/me then resolves 200 with is_verified true Then the AppBoot skeleton is replaced by the routed page's content And the navigation is not redirected Scenario: A second navigation reuses the cached session instead of calling GET /v1/auth/me again Given GET /v1/auth/me has already resolved 200 with is_verified true during the first navigation When the router navigates from /home to /article, whose meta.auth is also "verified" Then GET /v1/auth/me is not called a second time And no AppBoot skeleton is shown for this navigation Scenario: A verified-only route admits a verified session Given the target route is /article, whose meta.auth is "verified" And GET /v1/auth/me resolves 200 with is_verified true When the router navigates to /article Then the navigation completes to /article And there is no redirect Scenario: A verified-only route redirects an unverified session to the pending page Given the target route is /article, whose meta.auth is "verified" And GET /v1/auth/me resolves 200 with is_verified false When the router navigates to /article Then the router redirects to /register/pending And the navigation to /article does not complete Scenario: A session-only route admits an unverified session Given the target route is /register/pending, whose meta.auth is "session" And GET /v1/auth/me resolves 200 with is_verified false When the router navigates to /register/pending Then the navigation completes to /register/pending And there is no redirect Scenario: No session redirects to login with the original path and an expired reason Given the target route is /article/12/edit, whose meta.auth is "verified" And GET /v1/auth/me resolves 401 When the router navigates to /article/12/edit Then the router redirects to "/login?redirect=/article/12/edit&reason=expired" And the navigation to /article/12/edit does not complete Scenario Outline: The root path resolves to /home, /login or /register/pending depending on the session Given GET /v1/auth/me resolves When the router navigates to / Then the router redirects to Examples: | response | destination | | 200 with is_verified true | /home | | 401 | /login | | 200 with is_verified false | /register/pending | Scenario: A session check that fails with a server error renders the Generic error view instead of redirecting Given the target route is /, whose meta.auth is "any" And GET /v1/auth/me fails with a 500 response When the router navigates to / Then the router does not redirect anywhere And the Generic error view renders in place, with breadcrumb "Home", title "Something went wrong", text "This page couldn't load. Your saved work is safe. Try again, and if it keeps happening, restart the RenewCMS server.", a primary action "Try again" and a secondary action "Go to Home" When "Try again" is activated Then GET /v1/auth/me is called again to re-run the session check Scenario: A session check that fails with a network error on a non-root verified route also renders the Generic error view Given the target route is /article, whose meta.auth is "verified" And GET /v1/auth/me fails with a network error When the router navigates to /article Then the router does not redirect to /login or /register/pending And DashboardShell renders the Generic error view in place of the AppBoot skeleton and the page content, with title "Something went wrong" and a primary action "Try again" When "Try again" is activated Then GET /v1/auth/me is called again to re-run the session check Scenario Outline: Each verified placeholder route renders inside DashboardShell with its own heading, breadcrumb and active nav item Given GET /v1/auth/me resolves 200 with is_verified true When the router navigates to Then the route renders inside DashboardShell And the page heading is And the breadcrumb is <title> And the nav item for <title> has aria-current="page" Examples: | path | title | | /home | "Home" | | /article | "Articles" | | /integration | "Integration" | Scenario: AuthShell renders its title as the page heading and sets the document title Given a screen renders AuthShell with title "Log in" and no description When AuthShell mounts Then AuthShell renders an h1 containing "Log in" And document.title is "Log in ยท RenewCMS" Scenario: AuthShell renders its notice and footer slots Given a screen renders AuthShell with a "notice" slot containing "Your session expired." and a "footer" slot containing a "Forgot password?" link When AuthShell mounts Then the alert above the title contains "Your session expired." And the footer area outside the card contains a "Forgot password?" link Scenario: DashboardShell renders the five navigation destinations Given the current route is /article When DashboardShell mounts Then the sidebar navigation lists exactly five destinations in this order: "Home" linking to /home, "Articles" linking to /article, "Users" linking to /users, "Integration" linking to /integration, and "Settings" linking to /settings And the "Articles" destination has aria-current="page" And none of "Home", "Users", "Integration" and "Settings" has aria-current set Scenario: DashboardShell shows the offline banner when the browser reports offline Given navigator.onLine is false When DashboardShell mounts Then an offline banner is shown When navigator.onLine becomes true Then the offline banner is hidden