diff --git a/docs/e2e/feature-matrix.md b/docs/e2e/feature-matrix.md index 545466ede..44448ba1f 100644 --- a/docs/e2e/feature-matrix.md +++ b/docs/e2e/feature-matrix.md @@ -97,7 +97,7 @@ DASH-001 note: `dashboard.e2e.ts` verifies the default owner dashboard route, Ov DASH-002 note: `dashboard.e2e.ts` verifies customize mode, the Block library, adding the built-in AI usage widget, server-backed `dashboard-layout` preference persistence, reload restoration, grid drag move, right-edge resize, drag-to-library removal, final reload absence, and 390px customize/library containment. DEF-20260623-DASH002-01 fixed invalid nested buttons in Block library live previews by rendering preview widget chrome in edit mode. -DASH-003 note: `dashboard.e2e.ts` runs in a dedicated post-setup, pre-persona project and verifies onboarding progress from genuinely clean E2E state (1/5: identity complete; framework in progress; first page, plugin, and team not started), all five step labels/actions, the Settings modal action, workspace routes for New page/Browse plugins/Add members, 390px mobile containment, and server-backed dismiss persistence through `dashboard-layout`. +DASH-003 note: `dashboard.e2e.ts` runs in a dedicated post-setup, pre-persona project and verifies onboarding progress from genuinely clean E2E state (1/5: identity complete; framework in progress; new page, plugin, and team not started), all five step labels/actions, the Settings modal action, workspace routes for New page/Browse plugins/Add members, 390px mobile containment, and server-backed dismiss persistence through `dashboard-layout`. ## Capabilities And Access Control diff --git a/docs/e2e/feature-validation.tsv b/docs/e2e/feature-validation.tsv index 176c1e3a1..e3c628c8d 100644 --- a/docs/e2e/feature-validation.tsv +++ b/docs/e2e/feature-validation.tsv @@ -24,7 +24,7 @@ ADMIN-005 Workspace layout, panels, toasts, and error recovery As an admin user, CAP-001 Workspace isolation and limited admin navigation As an owner or admin, I want limited users to reach only the admin workspaces their role grants so protected workspace data and actions stay hidden. Custom roles expose workspaces through capability checks in src/admin/access.ts. A user with site.read and media.read can reach Site, Media, and self-targeted Account; disallowed workspace links are hidden and direct disallowed admin routes redirect away without rendering protected screens or data. At 390px, the limited toolbar remains contained, the granted Media affordance and account trigger remain reachable, and denied Content/Users affordances remain absent. Users with no dashboard access fall through to the first accessible workspace; Account remains available to every authenticated user; role changes can leave stale client navigation until the current user refreshes; direct denied URLs must not flash protected content; background plugin work must not run without plugin workspace access; narrow toolbar must not reintroduce hidden denied workspaces or overflow. Workspace visibility is derived from canAccessWorkspace and firstAccessibleWorkspace; role capabilities are normalized by the Users role editor and enforced again by server route capability gates; denied direct navigation must not render protected headings or tables; mobile checks assert viewport size, document overflow, toolbar containment, granted Media link containment, and account trigger containment. src/admin/access.ts; src/admin/AuthenticatedAdmin.tsx; src/admin/pages/site/toolbar/Toolbar.module.css; tests/e2e/users.e2e.ts; src/__tests__/admin/capabilityAwareAdmin.test.tsx; server/auth/authz.ts The Playwright scenarios use disposable owner sessions to create View site + Browse media roles and users; Account reachability is code-defined self-service behavior; mobile assertions target the shared toolbar containment for restricted workspace sets. Happy: limited Site + Media user logs in and sees Media while staying out of Users/Content. Error: direct denied /admin/users navigation redirects away. Boundary: no dashboard capability falls through to Site. Invalid: stale or unknown workspace path must not expose protected UI. Permission: server APIs still require their own capabilities. Performance: no background plugin fetch/SSE for non-plugin roles. Mobile: limited user signs in at 390x844, sees Site and Media without denied Content/Users affordances, toolbar has no document-level horizontal overflow, Media and account controls remain contained, and Account menu opens with Account & security reachable. Focused CAP-001 desktop and mobile Playwright regressions passed 2026-06-23 0 None Verification: `bun run test:e2e -- --project=e2e tests/e2e/users.e2e.ts -g "CAP-001"` passed 3/3 including setup. The desktop scenario creates a custom View site + Browse media role, creates a limited user, signs in from a clean context, verifies Media is visible while Content and Users are absent, then opens `/admin/users` directly and verifies the route redirects away and the All Users heading is not rendered. The mobile scenario creates the same restricted persona, signs in at 390x844, verifies Site and Media are present while Content/Users are absent, verifies the toolbar and account trigger are viewport-contained without page-level horizontal overflow, opens Account menu, and verifies Account & security remains reachable. Related access rules live in `src/admin/access.ts`; Account remains self-targeted for every authenticated user by `canAccessWorkspace`. Run logs: docs/e2e/runs/2026-06-23-cap001-workspace-isolation.md; docs/e2e/runs/2026-06-23-cap001-mobile-limited-navigation.md. 2026-06-23 DASH-001 Dashboard widgets and metrics As an admin user, I want a dashboard with site metrics and quick status widgets so I can understand CMS state. /admin/dashboard renders the dashboard page with greeting/actions, onboarding facts, Overview header, Today/7d/30d/All range state, Customize/Add block controls, and the default nine first-party widgets: storage, pages, posts, media, status, activity, publish lineup, plugins, and domain. Dynamic widgets fetch their own dashboard endpoint and leave aria-busy once loaded; status and domain render static operational rows; the mobile Overview header stacks/wraps controls. Empty site and zero counts; per-widget API failure leaves that widget loading instead of crashing the dashboard; missing plugin widget definitions render skeleton placeholders; no saved layout preference falls back to the default layout; first-party range tabs are local state today; narrow viewports can overflow if header controls do not wrap. Dashboard workspace requires dashboard.read through admin access gates; widget endpoints require authenticated user or domain capability (media.read, plugins.read, audit.read); client responses are TypeBox-validated through apiRequest; dashboard layout preferences are schema-validated and recovered from invalid stored values. src/admin/pages/dashboard/DashboardPage.tsx; src/admin/pages/dashboard/DashboardPage.module.css; src/admin/pages/dashboard/widgets/index.ts; src/admin/pages/dashboard/hooks/useDashboardStats.ts; src/admin/pages/dashboard/hooks/useDashboardLayout.ts; server/handlers/cms/dashboard; tests/e2e/dashboard.e2e.ts The default E2E owner has dashboard, media, plugin, and audit capabilities; E2E storage label is SQLite; status/domain widget values are currently static placeholders; plugin-contributed widgets are outside the default clean-install grid. Happy: owner opens dashboard and sees all default first-party widgets with loaded data. Error: widget endpoint failure should not blank the page. Boundary: clean install with zero media/posts/plugins. Invalid: invalid saved layout falls back. Permission: limited users without widget capabilities do not receive protected widget data. Performance: widgets load progressively. Mobile: at 390px the Overview header, range tabs, Customize, and Add block controls fit without horizontal overflow. Focused DASH-001 Playwright regression passed 2026-06-23 after mobile header overflow fix 0 None DEF-20260623-DASH001-01 resolved: at 390px the Overview header kept title, range tabs, Customize, and Add block in one row; the Time range tablist right edge reached 451.5px in a 390px viewport. Root cause: DashboardPage.module.css gridHeader stayed row-oriented with a nowrap right control cluster. Fix: stack the dashboard header and wrap the right controls below 760px. Verification: bun run test:e2e -- --project=e2e tests/e2e/dashboard.e2e.ts -g "DASH-001". Run log: docs/e2e/runs/2026-06-23-dash001-dashboard-widgets.md. 2026-06-23 DASH-002 Dashboard customize grid As an admin user, I want to customize dashboard widgets so the dashboard fits my workflow. /admin/dashboard customize mode keeps the grid mounted, switches the header action to Done, widens grid gutters, exposes resize handles, shows the add tile, and opens the bottom Block library. The library shows only widgets not already on the dashboard; clicking AI usage appends it below existing widgets, debounced PUT saves the server-backed dashboard-layout user preference, reload restores it, dragging moves it to an empty cell, right-edge resize increases its span, dragging to the library pill removes it, and a final reload keeps it removed. At 390px the customize controls and Block library stay viewport-contained. Duplicate add is a no-op; explicit occupied drops are resolved down to the nearest empty row before commit; move to an occupied destination is rejected by the layout model; missing widget definitions reserve skeleton slots; all placed widgets show an Every block status; corrupted stored preferences log and keep the default layout; library height is clamped; block preview chrome must not create nested interactive elements. Layout constraints use MAX_COLS, MIN_COLS, MIN_ROWS, MAX_ROWS, snapToCell, hasOverlapAt, and TypeBox-validated dashboard-layout preferences; unknown preference keys are rejected; dashboard access requires dashboard.read; all preference responses are parsed through schema validation in the E2E test. src/admin/pages/dashboard/DashboardPage.tsx; src/admin/pages/dashboard/components/DashboardGrid.tsx; src/admin/pages/dashboard/components/BlockLibrary.tsx; src/admin/pages/dashboard/hooks/useDashboardLayout.ts; src/core/persistence/userPreferences.ts; src/ui/components/Widget/Widget.tsx; tests/e2e/dashboard.e2e.ts The clean E2E owner has the nine default dashboard widgets and the built-in AI usage widget registered but not placed; plugin-provided widgets are outside this clean-install regression; drag behaviour needs real browser coverage and uses pointer gestures. Happy: add AI usage from the Block library, verify saved preference payload, reload restored widget, drag move, resize, drag-to-library remove, and reload absence. Error: live previews must not emit nested-button console errors. Boundary: no available widgets after adding AI usage. Invalid: corrupted layout fallback remains lower-level/manual. Permission: dashboard workspace access required. Performance: debounced preference saves coalesce gestures. Mobile: at 390px customize controls, page width, library dialog, search, and add action stay contained. Focused DASH-002 Playwright regression passed 2026-06-23 after block-library nested-button fix 0 None DEF-20260623-DASH002-01 resolved: Block library live previews rendered widgets in view mode inside an Add widget button, so Widget added its own options button and React logged invalid nested button markup when AI usage appeared in the library. Root cause: LibraryItem used Render editing=false for preview chrome. Fix: render previews with editing=true so Widget emits a non-interactive drag handle inside the add/drag surface. Verification: bun run test:e2e -- --project=e2e tests/e2e/dashboard.e2e.ts -g "DASH-002"; run log docs/e2e/runs/2026-06-23-dash002-dashboard-customize.md. 2026-06-23 -DASH-003 Onboarding panel As a new admin, I want onboarding tasks that reflect setup progress so I know what to do next. /admin/dashboard defers onboarding rendering until setup facts load. When dashboard-layout onboardingDismissed is false, OnboardingPanel renders five tasks from useOnboardingState: identity (site name changed from Untitled Site or favicon), framework (site.settings.framework), first page (site.pages.length >= 2), plugin (installed plugin count), and team (users.length > 1). Clean E2E state shows 1 of 5 steps complete because only site identity is complete; framework is in progress and first page/plugin/team are not started. Task actions either open Settings or route to /admin/site, /admin/plugins, and /admin/users. Dismiss saves dashboard-layout with onboardingDismissed true and reload keeps the panel hidden. At 390px the panel and Add members action stay contained. Facts endpoints can fail individually and Promise.allSettled falls back to defaults instead of bricking the dashboard; all tasks may be complete; dismissed panel stays hidden; corrupted dashboard-layout preference falls back through layout recovery; later test-created pages must not pollute clean-install facts; target workspaces may still be access-gated by route capabilities. Client API responses are validated through cmsAdapter, listCmsPlugins, and listCmsUsers; dashboard-layout preference schema includes onboardingDismissed boolean; dashboard route requires dashboard.read; target routes enforce their workspace capabilities. src/admin/pages/dashboard/DashboardPage.tsx; src/admin/pages/dashboard/components/OnboardingPanel.tsx; src/admin/pages/dashboard/hooks/useOnboardingState.ts; src/admin/pages/dashboard/hooks/useDashboardLayout.ts; src/core/persistence/userPreferences.ts; tests/e2e/dashboard.e2e.ts Clean E2E site name is Automated E2E Site; setup seeds only Home, so firstPage remains not started; owner has settings, site, plugins, and users access; no plugins or extra team members exist in clean E2E state. Happy: verify 1/5 progress, five task labels/states/actions, Settings modal action, workspace routes, server-backed dismiss save, and reload-hidden persistence. Error: facts API soft failure is covered by implementation inspection; focused browser regression does not stub each failed endpoint. Boundary: dismissed panel remains absent after reload. Invalid: corrupted preference fallback remains lower-level/manual. Permission: target routes remain behind workspace gates. Performance: panel waits for facts before rendering. Mobile: at 390px the panel and Add members action stay contained. Dashboard preflight Playwright regression passed 2026-07-11 0 None Full-suite release gating exposed the stale fixture assumption: setup seeds only Home, so firstPage is not started and clean progress is 1/5. `dashboard-preflight` now runs all dashboard scenarios immediately after setup and before persona/plugin mutations; the focused 2026-07-11 run passed DASH-001, DASH-002, and DASH-003 before the dependent persona project. 2026-07-11 +DASH-003 Onboarding panel As a new admin, I want onboarding tasks that reflect setup progress so I know what to do next. /admin/dashboard defers onboarding rendering until setup facts load. When dashboard-layout onboardingDismissed is false, OnboardingPanel renders five tasks from useOnboardingState: identity (site name changed from Untitled Site or favicon), framework (site.settings.framework), new page (site.pages.length >= 2), plugin (installed plugin count), and team (users.length > 1). Clean E2E state shows 1 of 5 steps complete because only site identity is complete; framework is in progress and new page/plugin/team are not started. Task actions either open Settings or route to /admin/site, /admin/plugins, and /admin/users. Dismiss saves dashboard-layout with onboardingDismissed true and reload keeps the panel hidden. At 390px the panel and Add members action stay contained. Facts endpoints can fail individually and Promise.allSettled falls back to defaults instead of bricking the dashboard; all tasks may be complete; dismissed panel stays hidden; corrupted dashboard-layout preference falls back through layout recovery; later test-created pages must not pollute clean-install facts; target workspaces may still be access-gated by route capabilities. Client API responses are validated through cmsAdapter, listCmsPlugins, and listCmsUsers; dashboard-layout preference schema includes onboardingDismissed boolean; dashboard route requires dashboard.read; target routes enforce their workspace capabilities. src/admin/pages/dashboard/DashboardPage.tsx; src/admin/pages/dashboard/components/OnboardingPanel.tsx; src/admin/pages/dashboard/hooks/useOnboardingState.ts; src/admin/pages/dashboard/hooks/useDashboardLayout.ts; src/core/persistence/userPreferences.ts; tests/e2e/dashboard.e2e.ts Clean E2E site name is Automated E2E Site; setup seeds only Home, so firstPage remains not started; owner has settings, site, plugins, and users access; no plugins or extra team members exist in clean E2E state. Happy: verify 1/5 progress, five task labels/states/actions, Settings modal action, workspace routes, server-backed dismiss save, and reload-hidden persistence. Error: facts API soft failure is covered by implementation inspection; focused browser regression does not stub each failed endpoint. Boundary: dismissed panel remains absent after reload. Invalid: corrupted preference fallback remains lower-level/manual. Permission: target routes remain behind workspace gates. Performance: panel waits for facts before rendering. Mobile: at 390px the panel and Add members action stay contained. Dashboard preflight Playwright regression passed 2026-07-11 0 None Full-suite release gating exposed the stale fixture assumption: setup seeds only Home, so firstPage is not started and clean progress is 1/5. `dashboard-preflight` now runs all dashboard scenarios immediately after setup and before persona/plugin mutations; the focused 2026-07-11 run passed DASH-001, DASH-002, and DASH-003 before the dependent persona project. 2026-07-11 SPOT-001 Spotlight open/search/navigation As an admin user, I want a command palette so I can quickly navigate and run actions. Cmd/Ctrl+K opens Spotlight, focuses input, searches built-in and provider commands, Enter runs navigation/action, Esc closes and restores focus. No matches; repeated open; route changes while open; disabled workspace command; keyboard order. Commands are registered through commandRegistry/scopes; actions check capabilities; provider results validated with schemas. src/admin/spotlight; src/admin/spotlight/builtinCommands.ts; src/admin/spotlight/providerRunner.ts Browser keyboard events differ by OS; tests use platform-neutral shortcuts where possible. Happy: open, search, navigate. Error: provider failure shown/ignored. Boundary: empty query and no-match query. Invalid: unknown command id. Permission: inaccessible commands hidden/disabled. Performance: search responsive with providers. Mobile: keyboard/accessibility focus works. SPOT-010 async skeleton full command-palette regression passed 2026-06-22 after stale async provider state fix 0 None SPOT-010 promoted: hold the Content provider search request, verify the Content skeleton group is busy, release the request, and verify the Async Skeleton Probe result replaces the skeleton. Defect E2E-20260622-SPOT010-01 fixed: stale empty asyncResults from the palette's initial empty query suppressed the later skeleton for the same provider; `SET_QUERY` now clears asyncResults/loadingProviders. Prior promotions remain covered for SPOT-009 nested panel focus, SPOT-011 keyboard-only execution, SPOT-012 reduced motion, and SPOT-013 high contrast. Verification: TSV and diff guards passed; focused reducer red/green passed; focused SPOT-010 E2E passed; full command-palette E2E passed 14/14; bun run lint passed; bun run build passed; bun test passed 5496/5496. 2026-06-22 SPOT-002 Spotlight scoped commands and pending actions As an admin user, I want scoped palette flows for pages, layers, breakpoints, data, media, users, and plugins so actions happen in context. Selecting a scope pushes subcommands; some commands set pending actions consumed by destination workspace after navigation; editor commands mutate active editor state. Context missing selected node/page; destination loads slowly; pending action consumed once; stale pending action after failed nav. Scopes and pending actions are typed in spotlight files; command capabilities gate actions; destructive commands use confirmation flow. src/admin/spotlight/scopes; src/admin/spotlight/commands; src/admin/spotlight/pendingAction.ts; src/admin/pages/site/SitePage.tsx Some commands require hydrated editor store. Happy: create page/VC from palette. Error: missing context command disabled. Boundary: pending action survives nav load. Invalid: malformed args ignored. Permission: role cannot run disallowed command. Performance: provider lookup bounded. Mobile: palette remains usable. SPOT-007 selected-layer context-ranking regression and full command-palette E2E file passed 2026-06-22; broader pending-action exploratory pending 0 None SPOT-007 promoted in `command-palette.e2e.ts`: Duplicate layer is absent outside editor context, a newly inserted text layer exposes selected-layer controls, and Duplicate layer appears in the top five empty-palette options. Verification: focused selected-layer E2E, full `tests/e2e/command-palette.e2e.ts`, and `bun run lint` passed. 2026-06-22 SPOT-003 Spotlight recents and destructive confirmation As an admin user, I want recent commands and protection for destructive commands so speed does not compromise safety. Recent commands persist and appear on reopen; destructive commands require two Enter confirmations and timeout collapse after configured duration. Timer expiry; command disappears after state change; repeated Enter double-fire; recents dedupe. Recent store validates local storage; destructive command state owned in Spotlight state; no native confirm dialogs. src/admin/spotlight/recentStore.ts; src/admin/spotlight/state.ts; src/admin/spotlight/SpotlightResults.tsx Confirm timeout is now Playwright-covered; OS accessibility variants remain observational. Happy: recent command appears. Error: destructive command cancel/timeout no mutation. Boundary: confirmation at exactly timeout. Invalid: corrupted recent store. Permission: destructive command still capability gated. Performance: recents load instantly. Mobile: confirm row visible. SPOT-005 destructive confirm-timeout Playwright regression and full command-palette E2E file passed 2026-06-22; broader context-ranking and accessibility exploratory pending 0 None SPOT-005 promoted in `command-palette.e2e.ts`: first Enter arms Delete current page, timeout clears the prompt, and the throwaway page remains. Verification: focused timeout E2E, full `tests/e2e/command-palette.e2e.ts`, and `bun run lint` passed. 2026-06-22 diff --git a/docs/features/dashboard.md b/docs/features/dashboard.md index 029e3bf3f..21ccd683c 100644 --- a/docs/features/dashboard.md +++ b/docs/features/dashboard.md @@ -265,11 +265,11 @@ Each CMS hook fetches on mount through `useAsyncResource` + `apiRequest`, valida - [ ] Set site identity - [ ] Choose Core Framework import -- [ ] Create your first page +- [ ] Create a new page - [ ] Install a plugin - [ ] Invite your team -State lives in `useOnboardingState(...)`. It reads the current site, installed plugins, and users concurrently. The seed Home page does not satisfy "Create your first page"; that step flips done when the site has at least two pages. Framework import defaults to `active` until the user picks a framework mode. +State lives in `useOnboardingState(...)`. It reads the current site, installed plugins, and users concurrently. The seed Home page does not satisfy "Create a new page"; that step flips done when the site has at least two pages. Framework import defaults to `active` until the user picks a framework mode. The panel is dismissible per-user and persisted with the dashboard layout preference (`dashboard-layout`). `useDashboardLayout.restoreOnboarding()` flips the same preference flag back to visible. diff --git a/src/admin/pages/dashboard/components/OnboardingPanel.tsx b/src/admin/pages/dashboard/components/OnboardingPanel.tsx index 709f18988..56af33fe6 100644 --- a/src/admin/pages/dashboard/components/OnboardingPanel.tsx +++ b/src/admin/pages/dashboard/components/OnboardingPanel.tsx @@ -83,9 +83,9 @@ const STEPS: readonly StepDef[] = [ }, { id: 'firstPage', - title: 'Create your first page', + title: 'Create a new page', desc: - 'Start from a blank canvas, a starter layout, or import HTML and we will scaffold a tree.', + 'Import HTML, a starter layout, or create a brand new page.', cta: 'New page', icon: FileTextSolidIcon, action: { kind: 'navigate', to: '/admin/site' }, diff --git a/src/admin/pages/dashboard/hooks/useOnboardingState.ts b/src/admin/pages/dashboard/hooks/useOnboardingState.ts index 166545067..69eaf8a52 100644 --- a/src/admin/pages/dashboard/hooks/useOnboardingState.ts +++ b/src/admin/pages/dashboard/hooks/useOnboardingState.ts @@ -7,8 +7,9 @@ * • Framework import — derived from `site.settings.framework` being * populated. Defaults to `'active'` so the user is nudged to make a * deliberate decision; once they pick a mode the step flips to done. - * • First page — done when ≥ 2 pages exist (the seed Home page - * doesn't count). + * • New page — done when ≥ 2 pages exist. Setup seeds a Home page + * nobody authored, so it does not count toward the step; the panel + * labels this one "Create a new page" for that reason. * • First plugin — done when any plugin is installed. * • Team — done when more than the owner is in the users table. * diff --git a/tests/e2e/dashboard.e2e.ts b/tests/e2e/dashboard.e2e.ts index 1f98bfae7..4149140a4 100644 --- a/tests/e2e/dashboard.e2e.ts +++ b/tests/e2e/dashboard.e2e.ts @@ -212,7 +212,7 @@ test.describe('dashboard', () => { await expectOnboardingStep(panel, 'Set site identity', 'Completed', 'Open settings') await expectOnboardingStep(panel, 'Choose Core Framework import', 'In progress', 'Import') - await expectOnboardingStep(panel, 'Create your first page', 'Not started', 'New page') + await expectOnboardingStep(panel, 'Create a new page', 'Not started', 'New page') await expectOnboardingStep(panel, 'Install a plugin', 'Not started', 'Browse plugins') await expectOnboardingStep(panel, 'Invite your team', 'Not started', 'Add members')