# Acceptance checklist (spec §10) — walkthrough This is the checklist from the spec. Run it against a fresh container after Phase 7 lands. Expected outcome is noted next to each step; edit this file (and open a bug) if any step deviates. ## Setup ```bash docker compose down && rm -rf data/app.sqlite docker compose up --build ``` `.env` should have EITHER valid `ENTRA_*` values OR `LOCAL_ADMIN_EMAIL` + `LOCAL_ADMIN_PASSWORD` set. Below, "sign in" means whichever flow you configured. ## Steps 1. **Fresh container, empty DB → sign in → you're the admin.** - Open http://localhost:8080. - If OIDC configured, click **Sign in with Microsoft** → complete Entra round-trip. If local admin configured, click **Sign in as local admin** and enter the env credentials. - Expected: header shows your display name + `admin` badge. `SELECT is_admin FROM users LIMIT 1` in `data/app.sqlite` returns 1. `audit_log` has rows: `CREATE user / BOOTSTRAP_ADMIN user / LOGIN user`. 2. **Create a sprint with 4 weeks.** - Header → **New sprint** → name "Sprint 1", reasonable dates, reserve 20%, weeks = 4 → Create sprint. - Expected: redirect to `/sprints/1`; empty-state banner says "No workers". Clicking **Settings** opens the settings page. The weeks table shows 4 rows with max_working_days 5.0 each and ISO week numbers computed from the start date. 3. **Add 6 workers, reorder by dragging — reload persists order.** - Header → **Workers** → add Alice, Bob, Carol, Dan, Eve, Frank. - Back to `/sprints/1/settings` → add all 6 to the sprint via **Add →** buttons. - Drag the "≡" handles to reorder (e.g. reverse order). - Reload the page. - Expected: the reordered sequence persists. `audit_log` shows `UPDATE sprint_worker` rows for every moved row (unchanged rows emit nothing thanks to the no-op rule). 4. **Fill Arbeitstage; Ressourcen / Reserven / Available update live.** - Open `/sprints/1` → edit a day cell (0.5-step input). - Blur the input. - Expected: Σ column updates immediately, the capacity strip updates (`Ressourcen`, `− Reserven`, `Available`). Status pill flashes "Saved N cell(s)". `audit_log` has one `CREATE sprint_worker_days` row per edited cell. 5. **Add tasks, assign days — Tot updates; over-commit turns Available red but still saves.** - In Section B: click **+ Add task** → focus jumps to the title input → type "Report for Q1", set priority 1, assign 10 days to one worker whose capacity is 8. - Expected: `Tot` cell shows 10; that worker's `Available` in the capacity strip goes negative and turns red. The assignment is persisted (reload still shows 10); `audit_log` has `CREATE task` + `CREATE task_assignment`. 6. **Sort task table by Owner, then by a worker column; clear → original drag order returns.** - Click the `Owner` header — rows sort asc, header shows "↑". - Click again — desc, "↓". - Click any worker column header — sorts by that cell's days. - Click the same header a third time (or any other until cleared) — the rows return to `data-sort-order` (the drag-persisted order). - Expected: no 500s, no layout shift, drag is disabled while a sort is active. 7. **Filter tasks: Prio=1, Owner=X, free-text "report".** - Set prio filter to "Prio 1 only", owner to a specific worker, type "report" into the search box. - Expected: only matching rows stay visible. The "No tasks match the current filters" banner appears when every row is hidden. 8. **Rename a worker in `/workers` — reflected everywhere.** - Header → **Workers** → change "Alice" to "Alice Cooper" → Save. - Open `/sprints/1` in another tab. - Expected: Arbeitstage row label, task list column header, and the Owner dropdowns all show the new name. `audit_log` has an `UPDATE worker` row. 9. **/audit shows one row per change with diffs visible.** - Header → **Audit log**. - Expected: reverse-chronological table with 50/page pagination. Every change you made is there. Each row's "before / after" `
` opens to show pretty-printed JSON snapshots. - Filter by user / action / entity type / date range / entity_id substring — narrow the list. 10. **Sign out; unauthenticated → redirect to /auth/login.** - Click **Sign out**. - Expected: session cleared, `audit_log` has a `LOGOUT user` row. - Try visiting `/workers` or `/sprints/1/settings` while anonymous → redirected to `/auth/login` (and then Entra, or the local form). ## Spot-check (spec §12) If you have `Tool_Sprint_PlanningSample.xlsx` handy: - Pick a worker with known weekly availability (e.g. 4-4-5-5-2). `Ressourcen` should match Excel: sum across weeks. - `− Reserven` should match: `round_to_0.5(Ressourcen * 0.8)` with reserve = 20 %. - For a prio-1 task totalling N days on that worker, `Available` should be `(Ressourcen − Reserven) − N`, and turn red when negative. ## Security headers (spec §9) With the app running, `curl -I http://localhost:8080/` should report: ``` X-Content-Type-Options: nosniff X-Frame-Options: DENY Referrer-Policy: strict-origin-when-cross-origin Content-Security-Policy: default-src 'self'; script-src 'self' …; … ``` `Strict-Transport-Security` appears only when `APP_BASE_URL` uses `https://`. ## Phase 13 — Focus filter + reset Runs against a sprint with at least 4 workers and a handful of tasks — set up via steps 3–5 above if starting from scratch. All scenarios below are pure client-side UI; no audit rows are written and no API requests should fire (verify with the Network panel open). 1. **Focus on a worker with assignments → only their tasks + only their non-zero columns remain.** - Pick a sprint worker (say **Bob**) who is the assignee on two tasks out of five. - Toolbar → **Focus** select → choose **Bob**. - Expected: - Only the two tasks where Bob's cell is > 0 stay visible. - Any worker column where every visible row is 0 collapses. Bob's own column stays visible (by definition > 0). - The **Reset** button appears to the left of the search box. - Reloading the page preserves the focus (localStorage key `sp:{sprintId}:focusWorker`). 2. **Focus on a worker with no assignments → empty-filter banner + every sw column collapses.** - Add a new worker to the sprint who has no task assignments (or pick an existing one — e.g. **Frank** — and zero out all their cells first). - Toolbar → **Focus** → choose that worker. - Expected: - Task rows all hidden → the "No tasks match the current filters" banner shows (NOT the "No tasks yet" empty-state row). - Every worker column in the task-list header is collapsed (no sw column has a non-zero visible row). - Title / Owner / Prio / Tot columns stay visible. 3. **Stacked filters AND together.** - Type "report" in **Search**, set **Prio** to "Prio 1 only", check one owner in **Owners**, and pick a **Focus** worker. - Expected: only rows that satisfy *every* predicate show — title matches, prio = 1, owner matches, focus worker's assignment > 0. Flipping any one filter off broadens the set as expected. 4. **Reset clears everything and the button hides.** - With all four filters from (3) active (plus at least one column hidden via the **Columns** dropdown), click **Reset**. - Expected: - Search empties, Prio resets to "All prios", Owners count clears, Focus returns to "All workers", every column re-appears in the Columns dropdown (all checkboxes re-checked), every row visible. - The **Reset** button itself disappears. - Reload the page — nothing returns (all five localStorage keys cleared/reset to empty state). ## Phase 14 — Hamburger menu Runs with any signed-in user. The header primary links (Sprints, New sprint) and the user badge stay inline; Workers / Users / Audit log / Sign out live behind the new hamburger button on the right. 1. **Signed-in admin: dropdown contains Workers / Users / Audit log / Sign out.** - Sign in as an admin (see Setup). - Click the hamburger button on the right of the header. - Expected: panel opens immediately below the button with four rows in order: **Workers**, **Users**, **Audit log**, a thin `
` divider, **Sign out**. `aria-expanded="true"` on the trigger. Each row has `role="menuitem"`; the panel has `role="menu"`. Clicking **Workers** navigates to `/workers` and closes the menu. 2. **Signed-in non-admin: dropdown contains only Sign out.** - Sign in as a non-admin user (demote yourself from `/users` while logged in as a second admin, or seed a user with `is_admin=0` directly). - Click the hamburger. - Expected: panel contains a single **Sign out** row — no Workers / Users / Audit log, no `
` divider. The primary **New sprint** link is already hidden for non-admins (admin gate lives on the anchor itself). 3. **Outside-click and Escape close the menu; focus returns to trigger on Escape.** - Open the menu (hamburger click). - Click anywhere in the page outside the panel (e.g. the Sprint Planner wordmark or the main content). Expected: panel closes, `aria-expanded="false"`. - Re-open the menu. Press **Escape**. Expected: panel closes AND the hamburger button regains keyboard focus (visible via the focus ring). 4. **Sign out from the menu still posts with CSRF and logs out.** - Open the menu → click **Sign out**. - Expected: browser POSTs to `/auth/logout` (Network panel shows a 302 with `_csrf` in the form payload), session is cleared, and the page redirects to `/auth/login` (or the public home with a "Sign in" CTA). `audit_log` has a new `LOGOUT user` row. No JS-driven POST — the native `
` carries the `_csrf` hidden input and submits the usual way.