|
@@ -76,7 +76,7 @@ per-cell audit trail.
|
|
|
│ 002_sprint_week_active_days.sql (Phase 12 — mask column)
|
|
│ 002_sprint_week_active_days.sql (Phase 12 — mask column)
|
|
|
├── views/ layout.php, home.php, auth/local.php,
|
|
├── views/ layout.php, home.php, auth/local.php,
|
|
|
│ workers/index.php, users/index.php,
|
|
│ workers/index.php, users/index.php,
|
|
|
-│ sprints/{new,show,settings}.php,
|
|
|
|
|
|
|
+│ sprints/{new,show,settings,present}.php,
|
|
|
│ audit/index.php
|
|
│ audit/index.php
|
|
|
├── tests/ TestCase + Services/ + Repositories/ + Controllers/ + Cascade/
|
|
├── tests/ TestCase + Services/ + Repositories/ + Controllers/ + Cascade/
|
|
|
│ + Domain/ + Db/
|
|
│ + Domain/ + Db/
|
|
@@ -155,6 +155,7 @@ Pages (HTML):
|
|
|
| GET | `/sprints/new` | admin |
|
|
| GET | `/sprints/new` | admin |
|
|
|
| POST | `/sprints` | admin |
|
|
| POST | `/sprints` | admin |
|
|
|
| GET | `/sprints/{id}` | signed-in |
|
|
| GET | `/sprints/{id}` | signed-in |
|
|
|
|
|
+| GET | `/sprints/{id}/present` | signed-in |
|
|
|
| GET | `/sprints/{id}/settings` | admin |
|
|
| GET | `/sprints/{id}/settings` | admin |
|
|
|
| GET | `/audit` | admin |
|
|
| GET | `/audit` | admin |
|
|
|
|
|
|
|
@@ -310,206 +311,51 @@ with a `BOOTSTRAP_ADMIN` audit row.
|
|
|
88 / 208 holds. ACCEPTANCE.md gains a "Phase 14 — Hamburger
|
|
88 / 208 holds. ACCEPTANCE.md gains a "Phase 14 — Hamburger
|
|
|
menu" section with the four manual scenarios from the plan.
|
|
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
|
|
### 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
|
|
## 10. Residual known gaps / deferred items
|
|
|
|
|
|
|
@@ -566,14 +412,12 @@ vendor/bin/phpunit
|
|
|
Tell Claude:
|
|
Tell Claude:
|
|
|
|
|
|
|
|
> Working on `/Users/achiappa/Development/claude_code_private/sprint_planer_web`.
|
|
> 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.
|
|
> → Shipped with its SHA.
|
|
|
|
|
|
|
|
Claude should verify what's described here against actual repo state
|
|
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)
|
|
## 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
|
|
101cc57 Phase 14: hamburger menu groups admin utilities + Sign out
|
|
|
15695ab HANDOFF.md: add Phase 14 plan (hamburger menu)
|
|
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
|
|
23ab365 Fix: stamp data-col on JS-built task row cells
|
|
|
d0fdf53 HANDOFF.md: mark Phase 13 shipped
|
|
d0fdf53 HANDOFF.md: mark Phase 13 shipped
|
|
|
b027c5d Phase 13: Focus filter + Reset in the task list
|
|
b027c5d Phase 13: Focus filter + Reset in the task list
|