compw00ter — Build-Plan Board: Kanban Redesign
Date: 2026-07-04
Status: Design — approved ("no holds barred"), ready for implementation plan.
Supersedes the UI of: 2026-07-04-compw00ter-build-plan-tracker-design.md
(the data-layer foundations from that spec — PlanRepository, plan_phases/plan_tasks,
the seeded 9-phase roadmap — are kept and evolved, not rebuilt).
Reshape the existing build-plan board from a vertical list-of-phases into a tabbed, drag-and-drop kanban: one tab per phase, each phase shown as four columns with a fixed goal, cards dragged between columns to change status.
Ground rule change
JavaScript is required. The prior "works with JS off / progressive
enhancement" constraint is explicitly dropped for compw00ter (user directive).
No no-JS fallbacks. The other house rules still hold: vanilla PHP backend, PDO
prepared statements, POST + CSRF on every write, e() output escaping, dark
theme using the :root CSS vars, XHTML-1.1-valid server markup.
What changes vs. the current board
| Area | Now | After |
|---|---|---|
| Statuses | todo/doing/done/blocked |
todo/in_play/ready_test/tested (blocked dropped) |
| Layout | vertical list, all phases stacked | tabs (one per phase), one phase visible |
| Task status control | click chip to cycle | drag card between columns (fetch + CSRF) |
| Phase target | none | a goal per phase, pinned at the bottom, editable |
| Progress | % of tasks done |
% of tasks tested |
| Page reloads | every action full-reloads | drag & mutations are fetch; board never full-reloads |
Data model
Statuses (config)
includes/config.php:
const W00T_PLAN_STATUSES = ['todo', 'in_play', 'ready_test', 'tested'];
Human labels (used by page + JS): Undone / In play / Ready for testing / Tested.
Schema
plan_phasesgainsgoal TEXT NOT NULL DEFAULT ''(added idempotently, like the existingaddColumnIfMissingpattern inDatabase).plan_tasks.statuskeeps itsTEXTcolumn; only the allowed values change.
Migration (idempotent, runs on the live DB in Database::migrate())
Map any legacy statuses in place, once:
done → tested,doing → in_play,blocked → todo.todounchanged. Run as a guardedUPDATE ... WHERE status IN (...). Safe to re-run (no legacy rows left after the first pass).
Seed (sql/plan_seed.sql)
- Add a one-line
goalto each of the 9 phases. - Phase 1's tasks seed as
tested(weredone); all otherstodo. - Task count stays 32; "complete" now means Tested.
Goals (fixed target per phase):
- Hero Builder & tooling — "Heroes, vocab, specs and map tools all usable."
- Content lock — "Every hero, incident and meter finalised in the DB."
- Engine core — "Players connect to the Node server and a round clock ticks."
- The w00t engine — "Attendance fills the w00t; empty chairs drain it; you can win or lose."
- Meters, traits & incidents — "The full self-management loop runs end to end."
- Player client — "A player can see their state, move rooms, and act on incidents."
- Live admin panel — "The host can watch and steer a running match."
- Bosses & richer layers — "Named bosses and depth systems are in play."
- Playtest & tune — "The game is fun and survives a full LAN dry-run."
PlanRepository changes
Keep the class; evolve it:
allPhasesWithTasks()— each phase gainsgoal;pct= round(tested/total*100); keep returning alltasks(the page buckets them by status).setTaskStatus($id, $status)— validates against the newW00T_PLAN_STATUSES(already does; enum just changes).- New
moveTask($id, $status, $sortOrder)OR extend reorder: dragging sets both the task'sstatus(which column) and itssort_order(position in that column). Chosen approach:setTaskPosition(int $id, string $status, int $sortOrder): void— one call updates status + sort_order in a transaction; the page/JS sends the target column and the index it was dropped at, and the repo renumbers that column. updatePhase($id, $name, $blurb, $goal)— extended to persist the goal.- Drop
cycleTaskStatus(no longer used — cards drag, not cycle). Remove its test. - Keep
movePhasefor tab reordering; keepaddPhase/addTask/updateTask/deleteTask/deletePhase.
addPhase gains an optional goal param (default '').
Page: plan.php
Server renders the full board (all phases + tasks) as data; the layout + all interaction is JS. Structure:
- Tab bar — one tab per phase in
sort_order, each showing the phase name and a minipct%. A trailing+tab adds a phase. Active phase comes from?phase=<id>(default: first). Clicking a tab is handled by JS (show/hide panels, update the URL viahistory.pushState) — no reload. - Phase panel (one per phase, only the active shown):
- 4 columns — Undone / In play / Ready for testing / Tested — each a drop zone with a header (label + live count) and its cards.
- Card —
draggable="true", carriesdata-id; shows title, optional note, an inline edit affordance, and a delete control.data-statusreflects its column. - Add-task box at the foot of the Undone column (adds there).
- Goal banner pinned at the bottom of the panel — shows the phase goal, editable inline; also a small phase toolbar (edit name/blurb, reorder tab ◀ ▶, delete phase).
- A hidden CSRF token (meta tag or
data-attr) for JSfetchcalls.
All dynamic values e()-escaped; markup XHTML-1.1-valid.
Interaction: assets/plan.js (new, vanilla, no deps)
- Tabs: click → show that phase panel, hide others, set
?phase=viahistory.pushState; on load, open the phase from the URL. - Drag: HTML5 DnD.
dragstartstamps the task id; columns + card gaps are drop targets showing an insertion indicator; ondrop, compute target column + index, optimistically move the card, thenfetchPOSTset_position(task id, status, sort_order, CSRF). On non-OK response, reload to resync. Update the source/target column counts and the phasepct%live after a successful move. - Mutations without drag (add/edit/delete task, add/edit/delete phase, edit goal):
fetchPOST toplan_save.php, then patch the DOM (or re-fetch that phase's data). Every request carries the CSRF token. - Small, readable, one file; no framework.
Handler: plan_save.php
- Keep POST +
csrf_check()+ the action switch. - Responds to
fetch: on success return a tiny JSON{"ok":true}(and, where useful, the new computedpct) withContent-Type: application/json; on bad input return a 400 with{"ok":false,"error":"…"}. (No more PRG redirect for the JS path; a non-JS direct hit can still 302 toplan.php, harmless.) - Actions:
set_position(new: id + status + sort_order),add_task,edit_task,delete_task,add_phase,edit_phase(name+blurb+goal),delete_phase,move_phase. Dropcycle_status.
Styling: assets/style.css
Replace the .plan-* block with kanban styles (dark theme, :root vars):
tab bar, active-tab state, 4-column responsive grid (columns scroll on narrow
screens), card styling, drag-over/insertion indicators, column count badges,
goal banner, per-status accent colours (Undone grey, In play blue, Ready-for-test
amber, Tested green).
Testing
- Data layer
tests/plan_smoke.php— update for the new statuses and goal: seeded board sanity (9 phases, each with a goal; Phase 1 = 100% viatested);setTaskPositionmoves a task's column + order;pctreflectstested;updatePhasepersists goal; cascade delete still holds. Remove the cycle test. - Migration — a check that no legacy
doing/done/blockedremain after load. - UI —
php -S+ curl forplan.php(renders tabs, columns, goal) and afetch-shaped POST toplan_save.php?action=set_positionreturning{"ok":true}with the DB reflecting the move. (Drag itself is JS; the server contract is what we assert.) - Error log lives two levels up:
../../logs/playground.crisprain.co.uk.error.log.
Out of scope (YAGNI)
- Multi-user realtime sync / websockets for the board (it's a single host).
- Card assignees, due dates, comments/history (title + note + status is enough).
- Drag to reorder phases (tabs reorder via ◀ ▶ buttons; dragging tabs is not needed).
- Animations beyond a simple drag-over highlight.