Browse Source

HANDOFF.md: mark Phase 15 shipped

Maintenance rule (§14). Moves Phase 15 from Upcoming → Shipped with
its SHA (d1dda4f), resets §9 Upcoming to the "Nothing scheduled"
template (matches the shape used after Phases 12 / 13 / 14 shipped),
updates §3 layout to list sprints/{new,show,settings,present}.php,
adds the new GET /sprints/{id}/present row to §6 Routes, updates the
§12 resume prompt to "1–15 shipped, no scheduled work," and appends
d1dda4f (Phase 15 code) and 48c56b7 (Phase 15 plan) plus the missing
d59120c (Phase 14 shipped) / bfb93fc (gitignore tweak) / a30cb0b
(Phase 13 hotfix note) lines to the §13 git history block so it
matches `git log`.

Test count unchanged at 88 / 208 — Phase 15 kept the controller-level
sanity test out of scope (the existing tests/Controllers/ harness is
pure-static; a full controller integration test would need PDO +
session wiring). §11 already reflects that.

No code changes.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
achiappa 2 weeks ago
parent
commit
c70e4424b4
1 changed files with 56 additions and 207 deletions
  1. 56 207
      HANDOFF.md

+ 56 - 207
HANDOFF.md

@@ -76,7 +76,7 @@ per-cell audit trail.
 │                        002_sprint_week_active_days.sql (Phase 12 — mask column)
 ├── views/               layout.php, home.php, auth/local.php,
 │                        workers/index.php, users/index.php,
-│                        sprints/{new,show,settings}.php,
+│                        sprints/{new,show,settings,present}.php,
 │                        audit/index.php
 ├── tests/               TestCase + Services/ + Repositories/ + Controllers/ + Cascade/
 │                                 + Domain/ + Db/
@@ -155,6 +155,7 @@ Pages (HTML):
 | GET    | `/sprints/new`              | admin          |
 | POST   | `/sprints`                  | admin          |
 | GET    | `/sprints/{id}`             | signed-in      |
+| GET    | `/sprints/{id}/present`     | signed-in      |
 | GET    | `/sprints/{id}/settings`    | admin          |
 | GET    | `/audit`                    | admin          |
 
@@ -310,206 +311,51 @@ with a `BOOTSTRAP_ADMIN` audit row.
       88 / 208 holds. ACCEPTANCE.md gains a "Phase 14 — Hamburger
       menu" section with the four manual scenarios from the plan.
 
+- [x] **Phase 15 — Big-screen (beamer) task viewer at
+      `/sprints/{id}/present`** (`d1dda4f`). New signed-in route
+      renders a stripped-down view: no shared layout chrome, no
+      Arbeitstage matrix, no capacity summary — just a thin top
+      bar (sprint name + dates + Close) and the task-list toolbar
+      + table. `SprintController::show()` keeps its behaviour; the
+      shared data fan-out is extracted into a private
+      `loadSprintPage(int $id): ?array` helper that both `show()`
+      and the new `present()` method call, returning `null` for a
+      missing sprint so each caller renders its own 404.
+      `views/sprints/present.php` emits its own `<!doctype html>`
+      (rendered with `layout=null`) reusing `/assets/css/app.css`
+      + the jQuery / jQuery UI CDN tags from layout.php +
+      `/assets/js/sprint-planner.js defer`. The root `<main>`
+      carries `beamer-root` + `data-sprint-root` + `data-sprint-
+      id` + `data-csrf` + `data-reserve-fraction` + `data-
+      beamer="1"`. `sprint-planner.js` detects the beamer flag,
+      namespaces its three localStorage keys with a `:beamer`
+      suffix (so presentation filters don't clobber the user's
+      `/sprints/{id}` workflow), seeds `["owner","prio","tot"]`
+      into `hiddenCols:beamer` on first load (before the first
+      `applyColumnVisibility()` so nothing flashes), and after
+      the boot `applyFilters()` measures `table.scrollWidth >
+      container.clientWidth`; if it overflows, adds
+      `.beamer-vertical-headers` (rotates sw column headers 90°);
+      if it still overflows, `console.warn`s and falls through to
+      horizontal scroll — never a hang. Strict CSP unchanged.
+      CSS scoping block lives under `@layer components` in
+      `assets/css/input.css` (`.beamer-root table` typography +
+      padding, `.handle` + `[data-delete-task]` hidden, vertical-
+      header rule). Entry point is a new "Present" anchor next to
+      Settings in `views/sprints/show.php`, `target="_blank"` for
+      all signed-in users. Tests unchanged at 88 / 208 —
+      refactor is a pure extraction and the sanity test the plan
+      allowed was skipped because the existing `tests/
+      Controllers/` harness only runs pure statics; a full
+      controller integration test would need PDO + session wiring
+      out of scope for this phase. ACCEPTANCE.md gains a
+      "Phase 15 — Big-screen viewer" section with the six manual
+      scenarios from the plan.
+
 ### Upcoming
 
-- [ ] **Phase 15 — Big-screen (beamer) task viewer**
-
-      **Problem.** Sprint-planning stand-ups and retro discussions happen
-      around a projector / TV at roughly 1920×1080 or 3840×2160. The
-      regular `/sprints/{id}` page is tuned for editing from a laptop:
-      `max-w-7xl` centred on the page, full header chrome, the
-      Arbeitstage matrix and capacity summary stacked on top of the
-      task list. On a beamer this wastes 40%+ of the screen width
-      and forces horizontal scroll once a sprint has more than ~7
-      workers — exactly the moment when everyone needs to see every
-      column at once.
-
-      **Goal.** A dedicated **presentation route** that strips the
-      chrome, dumps the planning sections the group isn't discussing,
-      and renders only the task list at full-viewport width with all
-      worker columns visible without horizontal scroll. Filters,
-      focus, and column-visibility reuse the existing Phase 10/13
-      plumbing as-is. Admins can still edit cells in-place (discussion
-      edits persist instantly); viewers see read-only text, same as
-      today.
-
-      **Scope (plan).**
-
-      1. **Route + controller.**
-         - New route in `public/index.php`:
-           `GET /sprints/{id}/present` → `SprintController::present(...)`.
-           Auth: `SessionGuard::requireAuth($this->users)` — anyone
-           signed-in can view, same as `show()`.
-         - New `SprintController::present(Request $req, array $params)`
-           method. Its body is ~90% a copy of `show()` — load the
-           sprint, weeks, sprint workers, day grid, tasks, task grid,
-           capacity, owner choices — but renders a different view.
-           To avoid drift with `show()`, extract the data-loading
-           fan-out into a private `loadSprintPage(int $id): ?array`
-           helper that both methods call. Return `null` when the
-           sprint is missing; let each caller render the appropriate
-           404. (This is the only controller-level refactor; audit
-           and route wiring are unchanged.)
-
-      2. **View — `views/sprints/present.php`.**
-         - Does **not** extend the shared `views/layout.php`. Instead
-           the view emits its own `<!doctype html>` with a minimal
-           `<head>` that reuses the same compiled
-           `/assets/css/app.css`, the same jQuery + jQuery UI CDN
-           tags from `layout.php`, and `/assets/js/sprint-planner.js`
-           with `defer`. No nav header, no page padding — the entire
-           viewport belongs to the task table.
-         - Root wrapper:
-           `<body class="bg-white"><main class="min-h-screen w-screen overflow-hidden beamer-root">...`.
-           `.beamer-root` is a new scoping class (see §3 below).
-         - Mount the existing `[data-sprint-root]` + `data-sprint-id`
-           + `data-csrf` + `data-reserve-fraction` so
-           `sprint-planner.js` auto-wires filters, focus, reset,
-           column visibility, and cell-save debounce without a
-           single line of new JS.
-         - Render **only** the `<section data-task-section>` block
-           (toolbar + table). Drop the Arbeitstage matrix, the
-           capacity summary, and the footer hint. A thin top bar
-           shows the sprint name + date range + a "Close" link back
-           to `/sprints/{id}` (so a viewer can escape without
-           `history.back`). Include the CSRF meta and the status chip
-           so error flashes still surface.
-         - Leave the toolbar as-is: search, prio filter, owner
-           multi-select, focus, columns, reset, + add-task (admin
-           only). In presentation mode, tasks often need live
-           captures — we keep the button.
-
-      3. **CSS — `assets/css/input.css`.**
-         - Add a `beamer-root` scoping class under `@layer
-           components` (keeps Tailwind's JIT scan happy without
-           scattering one-off classes across the view). Inside the
-           scope:
-             * `table { table-layout: fixed; font-size: clamp(0.75rem, 0.95vw, 1.05rem); }`
-             * Smaller cell padding (`td, th { padding: 0.25rem 0.35rem; }`).
-             * Hide non-discussion columns **by default** via extra
-               `data-col` values on the toolbar's hidden-columns
-               set: on first page load, seed
-               `localStorage['sp:{sprintId}:hiddenCols']` with
-               `["owner","prio","tot"]` if the key has never been
-               set in presentation mode. Use a second key —
-               `sp:{sprintId}:hiddenCols:beamer` — so the regular
-               `/sprints/{id}` view's hidden-columns pick isn't
-               clobbered. Implementing this cleanly needs one tiny
-               JS touch: see §4.
-             * `.handle { display: none; }` — drag-reorder is wrong
-               for a shared screen; also hides the delete buttons
-               (pattern: add `.px-1.py-1.text-right button[data-delete-task] { display: none; }`).
-         - "No horizontal scroll" rule: if the rendered table is
-           wider than the viewport, rotate worker-column headers
-           vertically. Detection is pure CSS — use a container
-           query on `.beamer-root section[data-task-section]`
-           (`@container (max-width: calc(100vw))`). Too clever;
-           fall back to a JS-side switch that measures
-           `table.scrollWidth > containerWidth` once on boot and
-           toggles a `.beamer-vertical-headers` class on the root.
-           In the CSS, `.beamer-vertical-headers thead th[data-sort-col^="sw-"] { writing-mode: vertical-rl; transform: rotate(180deg); padding: 0.5rem 0.25rem; }`.
-           See §4 — 10 lines of new JS.
-
-      4. **JS — `public/assets/js/sprint-planner.js` (minimal touch).**
-         - Detect the beamer root on boot. If
-           `$root.closest('.beamer-root').length > 0` (or a new
-           `$root.data('beamer') === 1`), use the `:beamer`
-           localStorage namespace for `hiddenCols` (and optionally
-           `focusWorker` + `ownerFilterSet`). Cleanest: take the
-           storage-key prefix as a variable derived from a single
-           boolean `isBeamer`.
-         - On boot in beamer mode, if
-           `localStorage[beamerHiddenColsKey]` is missing, seed with
-           `["owner","prio","tot"]`, persist, then call
-           `applyColumnVisibility()`. Respects the user's later
-           overrides.
-         - After `applyFilters()` completes for the first time in
-           beamer mode, measure `table.scrollWidth` vs
-           `container.clientWidth`. If it still overflows, add
-           `.beamer-vertical-headers` to the root and re-measure; if
-           it still overflows (very wide sprint), log a console
-           warning but leave horizontal scroll available — hard
-           visual failure is worse than a small scroll.
-         - Every other handler (cell save, focus, reset) keeps
-           working unchanged.
-
-      5. **Entry point — `views/sprints/show.php`.**
-         - Next to the "Settings" button in the sprint header, add an
-           anchor:
-           `<a href="/sprints/{id}/present" target="_blank" rel="noopener"
-           class="inline-flex items-center gap-2 rounded-md border
-           border-slate-300 bg-white text-slate-700 px-3 py-2 text-sm
-           hover:bg-slate-100">Present</a>`.
-           Visible to every signed-in user, not just admins.
-
-      6. **A11y / polish.**
-         - The vertical-text rotation applies `aria-label` stays the
-           plain text, so screen readers read it horizontally.
-         - Dark mode / high-contrast theme is deferred (see out of
-           scope).
-         - Focus outline stays visible so keyboard users can still
-           navigate.
-
-      7. **Edge cases.**
-         - **Archived sprints** render fine — read-only for
-           non-admins via the same `$currentUser->isAdmin` gate.
-         - **Zero workers / zero tasks** renders the existing
-           empty-state banner inside the viewport.
-         - **Sprint ID mismatch** — 404 via the same path `show()`
-           uses.
-         - **Printing** — out of scope; press `Ctrl+P` if curious,
-           the CSS doesn't have `@media print` rules yet.
-
-      8. **Tests.**
-         - No PHPUnit additions expected — the new method is mostly
-           a re-use of existing repositories. If the extraction of
-           `loadSprintPage()` is done, add **one** controller-level
-           sanity test that `present` renders HTTP 200 for a seeded
-           sprint and 404 for an unknown one. This keeps the count
-           climbing only if refactoring benefits from a guard.
-           Target: **88 → 90** if the helper is extracted; stay at
-           **88** if it isn't.
-         - `ACCEPTANCE.md` gains a "Phase 15 — Big-screen viewer"
-           section with six scenarios:
-             (a) open `/sprints/{id}/present` on an admin account:
-                 no horizontal scroll at 1920×1080; all worker
-                 columns visible;
-             (b) same, but at 3840×2160 (HiDPI beamer);
-             (c) apply focus + owner filter + search: all still
-                 work and column auto-hide still fires;
-             (d) admin edits a cell in present mode; reload
-                 `/sprints/{id}`; value persisted;
-             (e) non-admin user opens the URL: read-only, same
-                 layout;
-             (f) very wide sprint (12+ workers): vertical header
-                 rotation kicks in, table still fits.
-
-      **Out of scope.**
-      - TV-optimised high-contrast / dark theme. Separate concern;
-        revisit if a user complains about legibility in a dim room.
-      - Auto-scroll / ticker mode. Group discussions steer
-        themselves; no one has asked.
-      - Remote-control navigation (keyboard arrows to cycle
-        tasks/filters). Keep the filter toolbar as the single
-        interaction surface.
-      - Live-update push / websockets. If one admin edits a cell
-        while the beamer is projected, a manual reload picks it up
-        — acceptable for a stand-up cadence.
-      - Mobile layout variant. The presentation view is for large
-        screens; on phones, `/sprints/{id}` is already the right
-        shape.
-
-      **Spec alignment.**
-      - §3 layout: add `views/sprints/present.php` to the `views/`
-        line.
-      - §4 / §5 / §7: no changes — no new schema, math, or audit
-        surface.
-      - §6 Routes: add the new `GET /sprints/{id}/present` row
-        (signed-in, any auth level).
-      - §11 phpunit count: 88 → 90 if the controller refactor
-        adds tests, else stays 88.
-      - Strict CSP (§ Phase 11) stays intact — no inline handlers,
-        no new external hosts.
+- Nothing scheduled. When a new phase is ready, drop its plan
+  here and flip the checkbox when it ships (see §14).
 
 ## 10. Residual known gaps / deferred items
 
@@ -566,14 +412,12 @@ vendor/bin/phpunit
 Tell Claude:
 
 > Working on `/Users/achiappa/Development/claude_code_private/sprint_planer_web`.
-> Read `HANDOFF.md`, the git log, and `ACCEPTANCE.md`. Phases 1–14
-> are shipped (see §9); Phase 15 (big-screen / beamer task viewer at
-> `/sprints/{id}/present`) is planned but not implemented — the
-> scope is in §9 under Upcoming. Outstanding items are in §10
-> (mostly a human-run acceptance walkthrough in the running
-> container). If I ask you to work Phase 15, follow the maintenance
-> rule in §14 — commit code, then commit a HANDOFF.md update
-> separately that moves the entry from Upcoming
+> Read `HANDOFF.md`, the git log, and `ACCEPTANCE.md`. Phases 1–15
+> are shipped (see §9); no scheduled work — §9 Upcoming is empty.
+> Outstanding items are in §10 (mostly a human-run acceptance
+> walkthrough in the running container). If a new phase comes up,
+> follow the maintenance rule in §14 — commit code, then commit a
+> HANDOFF.md update separately that moves the entry from Upcoming
 > → Shipped with its SHA.
 
 Claude should verify what's described here against actual repo state
@@ -582,8 +426,13 @@ before acting — nothing here is load-bearing once it grows stale.
 ## 13. Git history (as of this handoff)
 
 ```
+d1dda4f Phase 15: big-screen (beamer) task viewer at /sprints/{id}/present
+48c56b7 HANDOFF.md: add Phase 15 plan (big-screen task viewer)
+d59120c HANDOFF.md: mark Phase 14 shipped
 101cc57 Phase 14: hamburger menu groups admin utilities + Sign out
 15695ab HANDOFF.md: add Phase 14 plan (hamburger menu)
+bfb93fc gitignore: exclude .claude/ (Claude Code agent runtime scratch)
+a30cb0b HANDOFF.md: note buildTaskRow data-col hotfix on Phase 13
 23ab365 Fix: stamp data-col on JS-built task row cells
 d0fdf53 HANDOFF.md: mark Phase 13 shipped
 b027c5d Phase 13: Focus filter + Reset in the task list