compw00tergame designer
2026-07-04 2026-07-04-compw00ter-plan-board-kanban-redesign.md

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_phases gains goal TEXT NOT NULL DEFAULT '' (added idempotently, like the existing addColumnIfMissing pattern in Database).
  • plan_tasks.status keeps its TEXT column; 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. todo unchanged. Run as a guarded UPDATE ... WHERE status IN (...). Safe to re-run (no legacy rows left after the first pass).

Seed (sql/plan_seed.sql)

  • Add a one-line goal to each of the 9 phases.
  • Phase 1's tasks seed as tested (were done); all others todo.
  • Task count stays 32; "complete" now means Tested.

Goals (fixed target per phase):

  1. Hero Builder & tooling — "Heroes, vocab, specs and map tools all usable."
  2. Content lock — "Every hero, incident and meter finalised in the DB."
  3. Engine core — "Players connect to the Node server and a round clock ticks."
  4. The w00t engine — "Attendance fills the w00t; empty chairs drain it; you can win or lose."
  5. Meters, traits & incidents — "The full self-management loop runs end to end."
  6. Player client — "A player can see their state, move rooms, and act on incidents."
  7. Live admin panel — "The host can watch and steer a running match."
  8. Bosses & richer layers — "Named bosses and depth systems are in play."
  9. Playtest & tune — "The game is fun and survives a full LAN dry-run."

PlanRepository changes

Keep the class; evolve it:

  • allPhasesWithTasks() — each phase gains goal; pct = round(tested/total*100); keep returning all tasks (the page buckets them by status).
  • setTaskStatus($id, $status) — validates against the new W00T_PLAN_STATUSES (already does; enum just changes).
  • New moveTask($id, $status, $sortOrder) OR extend reorder: dragging sets both the task's status (which column) and its sort_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 movePhase for tab reordering; keep addPhase/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 mini pct%. 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 via history.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.
    • Carddraggable="true", carries data-id; shows title, optional note, an inline edit affordance, and a delete control. data-status reflects 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 JS fetch calls.

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= via history.pushState; on load, open the phase from the URL.
  • Drag: HTML5 DnD. dragstart stamps the task id; columns + card gaps are drop targets showing an insertion indicator; on drop, compute target column + index, optimistically move the card, then fetch POST set_position (task id, status, sort_order, CSRF). On non-OK response, reload to resync. Update the source/target column counts and the phase pct% live after a successful move.
  • Mutations without drag (add/edit/delete task, add/edit/delete phase, edit goal): fetch POST to plan_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 computed pct) with Content-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 to plan.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. Drop cycle_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% via tested); setTaskPosition moves a task's column + order; pct reflects tested; updatePhase persists goal; cascade delete still holds. Remove the cycle test.
  • Migration — a check that no legacy doing/done/blocked remain after load.
  • UIphp -S + curl for plan.php (renders tabs, columns, goal) and a fetch-shaped POST to plan_save.php?action=set_position returning {"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.