Pārlūkot izejas kodu

Release v0.26.0: tooling/dev-stack split + R02-N01..N03 simplifications

CHANGELOG: lift the [Unreleased] appctl entry into a full [0.26.0]
section dated 2026-05-09; cover the Dockerfile 4-target split, the
docker-compose.dev.yml overlay, the /check skill + container-tester
subagent, the HTTP_PORT env var, the present-view sprint switcher and
brand logo, the REVIEW_02 audit landing, the three R02 fixes
(JS-side capacity arithmetic, task-row Twig template, requireAdminForm),
the htmx-CSP indicator-style fix, the weekday-only pre-check, and the
two dev-stack regressions (css-watcher restart loop, readonly-DB 500).
Compare-link footer adds v0.26.0 and re-points [Unreleased] at it.

Meta::VERSION → 0.26.0 so the admin-only Runtime panel on / lines up
with the git tag and the CHANGELOG heading.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
ClaudePriv@chiappa.zhdk.ch 17 stundas atpakaļ
vecāks
revīzija
c5fafcfb47
2 mainītis faili ar 177 papildinājumiem un 11 dzēšanām
  1. 176 10
      CHANGELOG.md
  2. 1 1
      src/Meta.php

+ 176 - 10
CHANGELOG.md

@@ -6,23 +6,188 @@ adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
 
 ## [Unreleased]
 
+## [0.26.0] — 2026-05-09
+
+Tooling- and dev-stack-heavy release on top of `v0.25.0`. Most of the
+work lives outside the running app: the Dockerfile splits into four
+named build targets (`css-builder` / `css-watcher` / `runtime` /
+`tests`), a `docker-compose.dev.yml` overlay adds a Tailwind `--watch`
+sidecar with edit-without-rebuild bind mounts, and a hand-written
+`bin/appctl` Bash wrapper replaces the Makefile so the project no
+longer needs `make`. A new `/check` Claude Code skill runs lint +
+PHPUnit inside the `tests` stage via a Haiku-powered subagent, keeping
+verbose phpunit output out of the main session. Inside the app, three
+REVIEW_02 simplifications fired: the JS-side capacity arithmetic is
+gone (server response is now authoritative), the task-row markup
+collapses into a single `_task_row.twig` partial cloned from a hidden
+`<template>` instead of being rebuilt by hand, and the form-endpoint
+admin/CSRF triple-line gets folded into a single
+`SessionGuard::requireAdminForm` helper. Smaller items: the present
+view gains a sprint-switcher dropdown and the brand logo, htmx no
+longer fights strict CSP by injecting an inline indicator stylesheet,
+the new-sprint settings page only pre-checks weekdays whose calendar
+date falls inside the chosen `[start, end]` range, the published host
+port becomes configurable via `HTTP_PORT`, and two dev-stack
+regressions (a css-watcher restart loop and a host-bind-mount
+read-only-database 500) get fixed.
+
+### Added
+
+- **`/check` Claude Code skill + `container-tester` (Haiku) subagent.**
+  New project-level slash command at `.claude/skills/check/SKILL.md`
+  delegates to a Haiku subagent (`.claude/agents/container-tester.md`,
+  Bash + Read only) that runs lint + PHPUnit inside the new `tests`
+  Dockerfile target and surfaces only failures + a one-line summary.
+  Verbose phpunit / docker build output stays inside the subagent and
+  never reaches the main session. Subcommands: `/check`,
+  `/check lint`, `/check test`, `/check <free text>`. `.gitignore`
+  flips from a blanket `/.claude/` ignore to `/.claude/* + !agents/ +
+  !skills/` so the two committed files ship with the repo while
+  session scratch / worktrees stay ignored.
+- **`HTTP_PORT` env var for the published host port.** `docker-compose.yml`
+  switches from a hard-coded `8088:80` to `${HTTP_PORT:-8080}:80` so
+  operators set the host port in `.env` alongside `APP_BASE_URL`
+  instead of editing the compose file. Default moves from `8088` to
+  `8080` so the compose default and the `APP_BASE_URL` default
+  finally agree. Docs: SPEC §8 env block, README quick-setup, and a
+  new admin-manual §3.3 "Host port".
+- **Present view: sprint switcher dropdown next to Close.** Header
+  `<select>` (visible to every signed-in user, hidden when only one
+  sprint exists) lists all sprints newest-start-first; on change the
+  page navigates to the chosen sprint's `/sprints/{id}/present`. The
+  change handler lives next to the existing `data-close-present`
+  block in `sprint-planner.js` so strict CSP stays intact.
+- **Present view: brand logo in the header.** Mirrors the cycle SVG
+  used in the main page header so the projection / beamer view
+  carries the same brand mark.
+- **`doc/REVIEW_02.md` — simplification + maintainability audit.**
+  Sister document to `REVIEW_01.md` (security). Catalogs over-complicated
+  patterns that can be rewritten more simply with no functional change;
+  same `R02-Nxx / Severity / Status` format so the `/loop` workflow
+  plugs in directly. The bold preamble requires re-reading SPEC.md
+  before raising any finding (the spec is the contract these refactors
+  must preserve, and §9's build-phase log explains why "complicated"
+  patterns landed that way). Three HIGH findings (R02-N01..N03) all
+  landed in this release.
+
 ### Changed
 
-- **Tooling: `make` → `appctl`.** The `Makefile` is gone; a hand-written
-  bash wrapper at `bin/appctl` (with a top-level `./appctl` symlink)
-  takes over. Subcommand surface follows the verb-then-action pattern:
+- **Build: split `Dockerfile` into 4 targets + dev compose overlay.**
+  The single Dockerfile now exposes `css-builder` (one-shot Tailwind
+  for prod, existing behaviour), `css-watcher` (node sidecar running
+  `tailwindcss --watch`, dev only), `runtime` (PHP/Apache prod image,
+  renamed but otherwise unchanged), and `tests` (`FROM runtime` +
+  `composer install` w/ dev deps for phpunit). `docker-compose.yml`
+  stays prod-shaped (now with explicit `target: runtime`).
+  `docker-compose.dev.yml` is an explicit overlay (no auto-load) that
+  flips `APP_ENV=development` (Twig `auto_reload`), bind-mounts the
+  source over the runtime image, and runs the css-watcher sidecar as
+  `${HOST_UID}:${HOST_GID}` so files written into bind-mounted host
+  paths land with normal ownership instead of root. `bin/dev-css-watcher.sh`
+  seeds vendor JS bundles (alpine-csp, htmx, sortable) into the
+  host-mounted `public/assets/js/vendor` on first start, then execs
+  `tailwindcss --watch` directly (skipping `npx`, which would want to
+  write `$HOME` under a non-root user). `tests` runs under
+  `profiles: [test]` for one-shot `--rm` invocations.
+- **Tooling: `make` → `appctl`.** A hand-written Bash wrapper at
+  `bin/appctl` (with a top-level `./appctl` symlink) replaces the
+  short-lived `Makefile` so the project no longer needs `make`.
+  Subcommand surface follows the verb-then-action pattern:
   `./appctl dev start|stop|build|shell|logs`,
   `./appctl prod start|stop|build`, and the unchanged check trio
-  `./appctl lint|test|check`. `HOST_UID` / `HOST_GID` are still exported
-  for the css-watcher's bind-mount ownership; the long
+  `./appctl lint|test|check`. `HOST_UID` / `HOST_GID` are still
+  exported for the css-watcher's bind-mount ownership; the long
   `docker compose -f docker-compose.yml -f docker-compose.dev.yml …`
-  invocation is still the one being wrapped. Bash completion ships at
+  invocation is what's being wrapped. Bash completion ships at
   `bin/appctl-completion.bash`; the first interactive `./appctl`
   invocation offers to add a `source` line to `~/.bashrc`, with a
   marker file under `~/.config/appctl/` so the prompt only appears
-  once. Updated SPEC §3 / §11, README, `doc/admin-manual.md`, the
-  `/check` Claude Code skill, and the `container-tester` agent so
-  `make check` no longer appears anywhere.
+  once (`APPCTL_NO_COMPLETION_PROMPT=1` silences it for CI). SPEC §3
+  / §11, README, `doc/admin-manual.md`, the `/check` skill, and the
+  `container-tester` agent all updated so `make check` no longer
+  appears anywhere.
+- **Sprint create: pre-check only weekdays inside the date range.**
+  `materializeWeeks` now takes `endDate` and seeds each week's
+  `active_days_mask` with the Mo–Fr bits whose calendar dates fall
+  inside `[start, end]` (`max_working_days = popcount(mask)`). The
+  settings page still renders all five checkboxes per week; only the
+  in-range ones are pre-checked, and editing/saving works as before.
+  The Weeks-table Start column also gains `dd.mm.YYYY` formatting,
+  matching the audit page's date convention.
+- **CSP: stop htmx from injecting its inline indicator `<style>`.**
+  Strict `style-src 'self'` was blocking htmx's built-in indicator
+  stylesheet on every page load, flooding the audit log with
+  `csp_violation` rows. Disable the auto-injection via
+  `<meta name="htmx-config" content='{"includeIndicatorStyles": false}'>`
+  and ship the equivalent `.htmx-indicator` / `.htmx-request` rules
+  from `app.css` instead.
+- **Fix R02-N01: drop JS-side capacity arithmetic — server is
+  authoritative.** The capacity formula was reproduced in
+  `sprint-planner.js` (~30 lines of `roundHalf` / `after_reserves` /
+  `committed_p1` chained math) so on-screen totals updated during
+  typing without the 400 ms PATCH round-trip. The PHP
+  `CapacityCalculator` is the spec's contract — every PATCH response
+  already returns the freshly computed `per_worker` values that
+  `applyServerCapacity()` applies, so the JS copy was just a
+  typing-feel optimisation that doubled the maintenance cost of any
+  future change. Path B from the finding: keep an immediate visual
+  update for the Ressourcen sum (a plain DOM sum of `[data-day]`
+  inputs) but drop the reserve arithmetic. Available / Reserves now
+  move on the server response. Removed: `capacity()`,
+  `committedPrio1FromDom()`, `recomputeAllCapacity()`, and the
+  `data-reserve-fraction` attribute on `show.twig` /
+  `present.twig`. `POST /tasks/{id}/move` now also returns
+  `per_worker` for the source sprint so the move-out path can call
+  `applyServerCapacity()` instead of recomputing locally. SPEC §5 /
+  §9 / §13 refreshed; the "any edit must touch both" line in §5 goes
+  away.
+- **Fix R02-N02: clone `<template data-task-row-template>` instead
+  of mirroring `_task_list` in JS.** Extracted the task-row markup
+  into `views/sprints/_task_row.twig` and call it twice from
+  `_task_list.twig` — once inside the for-loop, once inside a hidden
+  `<template data-task-row-template>` at the bottom (admin-only).
+  `sprint-planner.js`'s `buildTaskRow` now clones the template and
+  populates the few fields that vary; ~150 lines of hand-rolled DOM
+  construction are gone, along with the `ownerChoices()` /
+  `sprintWorkerHeaders()` helpers that only existed to feed it.
+  Server-rendered rows and JS-built rows share a single Twig source,
+  so the historical drift incidents (hotfixes 7c298d3, 23ab365, and
+  the whitespace-nowrap re-mirror in f204611) can no longer happen.
+  TwigViewTest pins both directions: a marker on the template body
+  for the admin path, and the absence of `<template
+  data-task-row-template>` for read-only users.
+- **Fix R02-N03: extract `SessionGuard::requireAdminForm` + drop
+  `SprintController::gateJsonAdmin`.** Form-handling controllers used
+  to open with the same three-line block (`requireAdmin` + Response
+  early-return + `verifyCsrf`) at nine sites: `SprintController::create`
+  / `::delete`, `WorkerController::create` / `::update`,
+  `UserController::update` / `::tombstone`, `SettingsController::update`,
+  `ImportController::upload` / `::commit`. New
+  `SessionGuard::requireAdminForm(Request, UserRepository): User|Response`
+  mirrors the existing `requireAdminJson` but speaks the form dialect
+  (redirect to `/auth/login` when anonymous, 403 text on non-admin,
+  403 text on CSRF mismatch). All nine sites collapse to a single
+  call. `SprintController::gateJsonAdmin` was an identical local twin
+  of `requireAdminJson`; its eight call sites switch to the
+  `SessionGuard` helper and the private method goes away. Net: +37
+  -64 lines; tests unchanged at 340 / 340.
+
+### Fixed
+
+- **Dev stack: css-watcher restart loop + readonly-DB 500.** Two
+  unrelated `appctl dev start` regressions caused by the bind-mount
+  overlay clashing with the upstream images. The css-watcher
+  restarted forever because Tailwind's `--watch` listens on stdin and
+  exits on EOF; `compose up` closes stdin by default, so
+  `restart: unless-stopped` looped it. Fixed by adding
+  `stdin_open` + `tty` to keep chokidar alive. The host bind mount at
+  `/var/www/data` was masking the Dockerfile's `chown www-data`,
+  leaving `app.sqlite` (root:1000 644) and no `sessions/` dir, so the
+  first request 500'd with `attempt to write a readonly database`.
+  Self-heal in `bin/docker-entrypoint.sh` by `mkdir`-ing the data +
+  session dirs and `chown`-ing to `www-data` on every start (twice —
+  migrate runs as root and would otherwise leave new SQLite/WAL
+  files root-owned).
 
 ## [0.25.0] — 2026-05-07
 
@@ -417,6 +582,7 @@ R01-N09 (`SameSite=Lax` retained — `Strict` would block the OIDC
 callback), R01-N17 (concurrent-tab OIDC clobber is correct
 RFC behaviour), R01-N29, R01-N30, R01-N32, R01-N33, R01-N34.
 
-[Unreleased]: https://github.com/chiappa/sprint_planer_web/compare/v0.23.0...HEAD
+[Unreleased]: https://github.com/chiappa/sprint_planer_web/compare/v0.26.0...HEAD
+[0.26.0]: https://github.com/chiappa/sprint_planer_web/compare/v0.25.0...v0.26.0
 [0.23.0]: https://github.com/chiappa/sprint_planer_web/compare/v0.22.0...v0.23.0
 [0.22.0]: https://github.com/chiappa/sprint_planer_web/releases/tag/v0.22.0

+ 1 - 1
src/Meta.php

@@ -20,6 +20,6 @@ namespace App;
  */
 final class Meta
 {
-    public const VERSION = '0.25.0';
+    public const VERSION = '0.26.0';
     public const CREATOR = 'Alessandro Chiapparini';
 }