# User journeys — __APP_NAME__

> The source of truth for end-to-end tests. Every journey below maps to a spec in
> `tests/e2e/*.spec.js`. Draft the journey here first, then implement its test.
> Generated by @dreamtree-org/ai-builder — extend as you add modules/features.

Legend: **Actor** — Anonymous · Member (limited perms) · Admin (tenant-admin) ·
External (API-token client). Each journey: _Given → When → Then_.

---

## A. Authentication
| ID | Journey | Expected |
| --- | --- | --- |
| A1 | Anonymous opens any protected route | redirected to `/login` |
| A2 | Login with valid credentials | lands on `/`, nav + user menu visible |
| A3 | Login with invalid credentials | inline error, stays on `/login` |
| A4 | Logout | session cleared, back to `/login`; protected routes blocked again |
| A5 | Refresh after login | stays authenticated (persisted profile + httpOnly cookie) |

## B. RBAC — navigation & route guards
| ID | Journey | Expected |
| --- | --- | --- |
| B1 | Admin views nav | sees **all** module items (Sidebar on desktop / FootNav on mobile) |
| B2 | Member views nav | sees **only** items for modules they hold `view` on |
| B3 | Member deep-links to an unpermitted route | redirected to `/unauthorized` |
| B4 | Member opens a permitted list | page renders with the model's data |

## C. CRUD + action gating
| ID | Journey | Expected |
| --- | --- | --- |
| C1 | Admin opens a model list | Table renders; **New** button visible |
| C2 | Member with `view` but not `create` | Table renders; **New** button hidden (`<Can>`) |
| C3 | Unauthenticated CRUD API call | `401` |

## D. Row-level (instance) access — default-deny
| ID | Journey | Expected |
| --- | --- | --- |
| D1 | Member with module `view` but no instance grant lists an instance-scoped model | **0 rows** (default-deny) |
| D2 | After being granted a specific row | sees **exactly** that row |
| D3 | Admin lists the same instance-scoped model | sees **all** rows (bypass) |

## E. Per-tenant API tokens (external access)
| ID | Journey | Expected |
| --- | --- | --- |
| E1 | Admin issues a scoped token | plaintext returned **once**, prefix stored |
| E2 | External call with token, within scope | `200` |
| E3 | External call out of scope (action/module) | `403` |
| E4 | Bogus / revoked token | `401` |
| E5 | Token cannot issue/escalate | `403` (no `manage_tokens`) |
| E6 | Admin rotates token | old token → `401`, new token → `200` |
| E7 | Token from another tenant used here | `401` (not found in this tenant's DB) |

## F. Theming
| ID | Journey | Expected |
| --- | --- | --- |
| F1 | Toggle dark mode | `data-theme="dark"` set; persists across reload |
| F2 | Brand color applied | primary buttons use the configured `--color-primary-*` |

## G. PWA
| ID | Journey | Expected |
| --- | --- | --- |
| G1 | Manifest present + linked | `manifest.webmanifest` served, `display: standalone` |
| G2 | Service worker registers | SW controls the page; app shell precached |
| G3 | Offline app-shell | shell loads offline (API calls degrade gracefully) |

## H. Responsive (all screen sizes)
| ID | Journey | Expected |
| --- | --- | --- |
| H1 | Phone viewport (375px) | bottom **FootNav** visible, Sidebar hidden, no horizontal scroll |
| H2 | Desktop viewport (1440px) | **Sidebar** visible, FootNav hidden |
| H3 | Ultrawide (2560px) | content max-width capped + centered (readable) |
