diff --git a/docs/superpowers/plans/2026-07-06-persistence-editors-ui.md b/docs/superpowers/plans/2026-07-06-persistence-editors-ui.md new file mode 100644 index 0000000..a50eb63 --- /dev/null +++ b/docs/superpowers/plans/2026-07-06-persistence-editors-ui.md @@ -0,0 +1,1818 @@ +# Plan 3b: Editor Handlers, Views, and Front Controller Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Build the six editor HTTP handlers, six view templates, the real front controller, and the full-flow integration test that complete the Plan 3 web layer. + +**Architecture:** A front controller (`public/index.php`) dispatches every request to a `Http\Router` that routes by HTTP method + path to one of eight `Http\Handlers\*` classes. Handlers parse the request through a `Http\Request` value object, do their work through `Application\*` services (which know the `Domain`), and return a `Http\Response` rendered by `Views\*` PHP templates. CSRF and per-session upload tokens are issued by the front controller and validated by the handlers. + +**Tech Stack:** PHP 8.3, PHPUnit 11, PHPStan level 6, PHP_CodeSniffer PSR-12, plain `php -S` for local dev. + +## Global Constraints + +These apply to every task and are copied verbatim from the design spec (which is a delta on the Plan 3 spec). + +- Every state-changing form or request uses and verifies a CSRF token before mutation. Forms use a hidden `_csrf` field; fetch uses an `X-CSRF-Token` header. +- All rendered output is escaped for its output context via the `Escape::html`, `Escape::attr`, and `Escape::url` helpers. Templates do not interpolate user input without one of these helpers. +- All uploaded images are content-validated by reading the first 12 bytes and checking the magic number against an allowlist (PNG, JPEG, WebP, GIF), then `getimagesize()` confirms dimensions are ≤ 512×512, then size is checked at ≤ 2 MB. The original filename and declared MIME are discarded after validation. +- `var/uploads/` is denied by `.htaccess` (Apache) and `index.php` (built-in server). +- The dev server is the built-in `php -S 0.0.0.0:8000 -t public` (documented in README). +- PHP follows the repository PHPCS rules and passes PHPStan at level 6. +- Composer configuration passes `composer validate --strict`. +- Lint, static analysis, unit and integration tests, the end-to-end smoke test, and Composer validation are required CI checks. +- The 4 `@phpstan-ignore function.alreadyNarrowedType` annotations from Plan 1+2+3a reference "Plan 4" in their comments; the actual JSON sources land in Plan 3. The Plan 3b front controller does not introduce a new annotation; the front controller sits below the threshold and the handlers and templates are pure routing and rendering. +- Plan 3b-specific: the front controller's `userToken` validation in `GetAssets` uses a strict format regex (`/^[a-f0-9]{32,}$/`) to harden the path-traversal surface. The `BATTLEFORGE_SECRET` env var takes precedence over the per-install file `var/secret.key` (a 32-byte binary file created on first run with `random_bytes(32)` and `chmod 0600`; git-ignored). +- Domain code has no dependency on HTTP, sessions, storage, or browser code (unchanged from Plans 1, 2, 3a). + +## File Structure + +```text +src/Http/Handlers/ +├── GetTeamEditor.php (NEW) — renders the team editor form +├── PostTeamEditor.php (NEW) — processes the team editor form POST +├── GetBattlefieldEditor.php (NEW) — renders the battlefield editor page +├── PostBattlefieldEditor.php (NEW) — processes the JSON body +├── PostImageUpload.php (NEW) — multipart upload, returns {url} JSON +└── PostStartMatch.php (NEW) — JSON body, returns initial MatchState + +src/Views/ +├── layout.php (NEW) — shared chrome +├── home.php (NEW) — the home page (currently inline in GetHomePage) +├── team-editor.php (NEW) — the team editor form +├── battlefield-editor.php (NEW) — the battlefield editor with the empty grid +├── match-stub.php (NEW) — the Plan 4 placeholder +└── upload-result.php (NEW) — iframe-style response for the upload form + +public/ +├── index.php (MODIFY) — replace the Task 1 stub with the real front controller +└── assets/ + └── (placeholders land in Plan 3c) + +tests/Integration/ +├── GetTeamEditorTest.php +├── PostTeamEditorTest.php +├── GetBattlefieldEditorTest.php +├── PostBattlefieldEditorTest.php +├── PostImageUploadTest.php +├── PostStartMatchTest.php +└── FullFlowTest.php +``` + +## Delivery Sequence + +Plan 3b is the fourth of four plans, picking up where Plan 3a left off. It is the last plan in the original "Plan 3" deliverable. + +1. View templates — establish the rendering foundation (Tasks 1-3). +2. Editor handlers — wire the templates to the domain (Tasks 4-6). +3. Front controller — wire the handlers to the URL space (Task 7). +4. Full-flow integration test — exercise the whole stack (Task 8). +5. CI verification and final review (Tasks 9-10). + +## Execution Preflight + +Execute this plan in an isolated worktree on `feature/editor-handlers-and-views`, branched from `develop`. The implementation branch will be submitted as a pull request into `develop` only after every completion check is green. + +### Task 1: Shared `layout.php` chrome + +**Files:** +- Create: `src/Views/layout.php` + +**Interfaces:** +- Produces the shared template that all other templates `require` to set up the document chrome. The layout is invoked by a template that has already started buffering its body content; the layout's signature is `render_layout(callable $body, string $csrf, string $title, string $extraHead = ''): void`. + +The layout prints: +- The HTML5 doctype. +- ``. +- `
` with charset, viewport, `No saved scenarios yet.
= $e($error) ?>
Scenario: = $e($scenarioId) ?>
+ + + Escape::html($v); +$matchInfo = $matchJson !== null ? "Match loaded." : "No match in progress."; +?> += $e($matchInfo) ?>
+ += $e($matchJson) ?>+
Battle interface coming in Plan 4.
+ + Escape::html($v); +$json = json_encode(['url' => $url], JSON_THROW_ON_ERROR); +?> +Uploaded: = $e($url) ?>