Parcourir la source

Docs: dev/prod compose split + /check skill (SPEC §2 / §3 / §11, README, admin-manual)

SPEC.md
  * §2 — "two-stage build" replaced with the four named targets
    (css-builder / css-watcher / runtime / tests) and what each is for.
  * §3 — directory layout adds docker-compose.dev.yml, Makefile,
    .claude/{agents,skills}, bin/dev-css-watcher.sh; Dockerfile comment
    updated to reflect the new stages.
  * §11 — full rewrite. Opens with a dev/prod/check decision table,
    then 5 subsections:
       11.1 first-time setup
       11.2 dev iteration loop (with edit-cycle table)
       11.3 tests + lint via `make` (replaces the bespoke
            `docker run -v $(pwd):/app …` invocation that's now
            obsolete)
       11.4 the /check skill
       11.5 why two compose files (overlay rationale)
    The stale `OK (252 tests, 683 assertions)` line is dropped — the
    count drifted (README said 88, SPEC said 252, current run reports
    340) and pinning a number means re-editing every time the suite
    grows. Pointing at `make test` doesn't.

README.md
  * Quick setup: notes `make prod` as a shortcut for the existing
    `docker compose up -d --build` line.
  * New "Local development" section between Quick setup and Layout
    covering `make dev` (and the long form for users without make).
  * Tests section rewritten: `make check` / `make test` / `make lint`
    are primary; /check skill mentioned; host `vendor/bin/phpunit`
    documented as fallback.

doc/admin-manual.md
  * §4 gets a small operator-vs-developer banner pointing dev users
    at SPEC §11. The rest of the operator manual stays focused on
    bare `docker compose` prod-ops commands — operators don't need
    `make` or the dev overlay.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
ClaudePriv@chiappa.zhdk.ch il y a 1 jour
Parent
commit
14b0068d35
3 fichiers modifiés avec 196 ajouts et 40 suppressions
  1. 44 2
      README.md
  2. 144 38
      SPEC.md
  3. 8 0
      doc/admin-manual.md

+ 44 - 2
README.md

@@ -53,6 +53,7 @@ chmod 600 .env
 
 # 4. Build and start the container (compose maps host port 8088 → container 80)
 docker compose up -d --build
+#    Equivalent shortcut once `make` is installed: `make prod`
 
 # 5. Open the app
 xdg-open http://localhost:8088   # or just visit it in a browser
@@ -78,6 +79,31 @@ returns 503 Service Unavailable when something is pending. Wiping
 For everything else — Entra app registration, backups, troubleshooting,
 upgrades — see [`doc/admin-manual.md`](doc/admin-manual.md).
 
+## Local development
+
+For iterating on code (vs. running the prod-shaped image you set up
+above), use the dev compose overlay. Source dirs are bind-mounted from
+the host and a sidecar runs `tailwindcss --watch`, so Twig / PHP edits
+are picked up on the next request and CSS changes appear without a
+container rebuild:
+
+```bash
+make dev          # foreground; equivalent to:
+                  # HOST_UID=$(id -u) HOST_GID=$(id -g) \
+                  # docker compose -f docker-compose.yml -f docker-compose.dev.yml up
+```
+
+Rebuild the dev images only when `Dockerfile`, `composer.json/.lock`, or
+`package.json/.lock` change:
+
+```bash
+make dev-build
+```
+
+`make help` lists every target. Full architecture (overlay rationale,
+dev edit-cycle table, `tests` Docker stage) lives in
+[`SPEC.md`](SPEC.md) §11.
+
 ## Layout
 
 ```
@@ -101,7 +127,23 @@ doc/         operator-facing documentation (admin manual)
 
 ## Tests
 
+The preferred path is the `tests` Docker stage — same PHP version,
+extensions, and composer state as prod, plus the dev composer deps
+layered in:
+
 ```bash
-vendor/bin/phpunit
-# → OK (88 tests, 208 assertions)
+make check        # lint + phpunit, in a one-shot --rm container
+make test         # phpunit only
+make lint         # php -l on src/ + tests/
 ```
+
+First run of any of these cold-builds the `tests` image (~30–60s);
+subsequent runs reuse it. No host PHP / composer needed. From inside
+Claude Code, the **`/check`** slash command runs the same pipeline via
+a Haiku-powered subagent that returns only the pass/fail summary —
+verbose phpunit output stays out of the conversation context.
+
+Direct `vendor/bin/phpunit` on the host still works if you have PHP +
+composer locally and want the fastest possible loop, but the Phase 20
+parser tests then auto-skip on hosts missing `ext-gd` / `ext-zip` /
+`ext-xmlreader` etc. See [`SPEC.md`](SPEC.md) §11.3 for the full table.

+ 144 - 38
SPEC.md

@@ -24,11 +24,20 @@ per-cell audit trail.
 
 ## 2. Tech stack (non-negotiable)
 
-- Runtime: Docker, two-stage build, `node:20-alpine` for CSS + JS-vendor
-  copy + `php:8.3-apache` for runtime. The runtime stage installs
-  `pdo_sqlite`, plus `zip` and `gd` (Phase 20 — required by
-  PhpSpreadsheet); `dom`, `xml`, `xmlreader`, `xmlwriter`, `simplexml`,
-  `mbstring`, `fileinfo` ship with the base image.
+- Runtime: Docker, four-stage build:
+  1. **`css-builder`** (`node:20-alpine`) — one-shot Tailwind compile +
+     vendor JS copy, output baked into the runtime image.
+  2. **`css-watcher`** (`node:20-alpine`, dev only) — runs
+     `tailwindcss --watch` against host bind-mounted source via the
+     `docker-compose.dev.yml` overlay; never built for prod.
+  3. **`runtime`** (`php:8.3-apache`) — production image. Installs
+     `pdo_sqlite`, plus `zip` and `gd` (Phase 20 — required by
+     PhpSpreadsheet); `dom`, `xml`, `xmlreader`, `xmlwriter`,
+     `simplexml`, `mbstring`, `fileinfo` ship with the base image.
+  4. **`tests`** (`FROM runtime`) — re-runs `composer install` to layer
+     in dev deps (phpunit, etc.) and ships a `composer test` CMD. Built
+     on demand by `make test` / `make check` via the dev compose's
+     `tests` profile, never started by `make dev` or `make prod`.
 - Language: PHP 8.3, strict types, PSR-12.
 - Database: SQLite via PDO, file at `/var/www/data/app.sqlite` (mounted volume).
 - Front end (Phase 19):
@@ -60,14 +69,24 @@ per-cell audit trail.
 ## 3. Directory layout
 
 ```
-├── Dockerfile                  # multi-stage: css-builder + php:8.3-apache
-├── docker-compose.yml
+├── Dockerfile                  # 4 stages: css-builder, css-watcher (dev),
+│                               # runtime (prod), tests (phpunit)
+├── docker-compose.yml          # prod stack — builds target=runtime
+├── docker-compose.dev.yml      # dev overlay — adds bind mounts, css-watcher
+│                               # sidecar, tests profile (see §11)
+├── Makefile                    # dev/prod/check wrappers; HOST_UID/GID export
 ├── .dockerignore
 ├── .env.example
 ├── composer.json / composer.lock
 ├── package.json / package-lock.json
 ├── tailwind.config.js
 ├── phpunit.xml
+├── .claude/                    # ships the `/check` skill + container-tester
+│   ├── agents/                 # Haiku-powered subagent (model: haiku, terse
+│   │                           # output policy — failures + 1-line summary)
+│   └── skills/                 # `/check[ lint|test]` slash command, delegates
+│                               # to the agent so verbose output stays out of
+│                               # the main session context.
 ├── ACCEPTANCE.md               # spec §10 manual checklist walkthrough
 ├── SPEC.md                     # this file
 ├── doc/
@@ -90,9 +109,13 @@ per-cell audit trail.
 │               ├── htmx.min.js         # htmx.org
 │               └── sortable.min.js     # SortableJS
 ├── bin/
-│   └── audit.sh                # R01-N16 — wraps `composer audit --locked`
-│                               # inside the runtime image; honours
-│                               # SPRINT_PLANER_IMAGE for non-default tags
+│   ├── audit.sh                # R01-N16 — wraps `composer audit --locked`
+│   │                           # inside the runtime image; honours
+│   │                           # SPRINT_PLANER_IMAGE for non-default tags
+│   ├── docker-entrypoint.sh    # migrations + session GC; stage 3 entrypoint
+│   ├── migrate.php             # invoked by docker-entrypoint.sh
+│   └── dev-css-watcher.sh      # CMD for the css-watcher dev container —
+│                               # seeds vendor JS, then `tailwindcss --watch`
 ├── src/
 │   ├── Auth/            BootstrapAdmin, LocalAdmin, OidcClaims (R01-N18),
 │   │                    OidcClient, SessionGuard
@@ -1652,23 +1675,42 @@ Nothing scheduled.
 
 ## 11. Running locally
 
-```bash
-cp .env.example .env
-# Fill Entra vars, OR set LOCAL_ADMIN_EMAIL + LOCAL_ADMIN_PASSWORD_HASH
-# (see README's Quick setup for the password_hash() one-liner)
-docker compose up --build
-# open http://localhost:8080
-```
+The project ships **two compose configurations** plus a Makefile that
+wraps the long `docker compose -f … -f …` invocations. Pick the one
+that matches what you're doing:
+
+| Goal | Command | What runs |
+|---|---|---|
+| Run prod build (or operate it) | `make prod` | `docker-compose.yml` only — the `runtime` image with baked CSS |
+| Iterate on code locally | `make dev` | `docker-compose.yml` + `docker-compose.dev.yml` — adds source bind mounts, `APP_ENV=development`, and a `css-watcher` sidecar that runs `tailwindcss --watch` against the host |
+| Lint + tests, one-shot | `make check` | Builds the `tests` Dockerfile target on demand and runs `php -l` + PHPUnit in a `--rm` container; doesn't require `make dev` to be running |
+
+Without `make` installed (`sudo apt-get install -y make` on Debian),
+the same commands written out:
 
-Rebuild when the Dockerfile / composer manifest / Tailwind sources change:
 ```bash
-docker compose build --no-cache && docker compose up
+# Dev (foreground; sidecar regenerates CSS on save)
+HOST_UID=$(id -u) HOST_GID=$(id -g) \
+  docker compose -f docker-compose.yml -f docker-compose.dev.yml up
+
+# Prod (detached)
+docker compose up -d
+
+# Tests / lint, one-shot
+docker compose -f docker-compose.yml -f docker-compose.dev.yml \
+  --profile test run --rm tests
 ```
 
-For local CSS dev without Docker:
+`make help` lists all targets; the rest of this section is the *why*.
+
+### 11.1 First-time setup
+
 ```bash
-npm install
-npm run watch:css   # rebuilds public/assets/css/app.css on change
+cp .env.example .env
+# Fill Entra vars, OR set LOCAL_ADMIN_EMAIL + LOCAL_ADMIN_PASSWORD_HASH
+# (see README's Quick setup for the password_hash() one-liner)
+make prod   # or `docker compose up --build`
+# open http://localhost:8088
 ```
 
 The SQLite file lives at `./data/app.sqlite` on the host; nuking it resets
@@ -1676,29 +1718,93 @@ the app to a blank slate (migrations run from the Docker entrypoint —
 `bin/docker-entrypoint.sh` → `php bin/migrate.php` — before Apache starts;
 the request path only checks and 503s if pending, never auto-migrates).
 
-Syntax-check PHP without Docker:
-```bash
-for f in $(git ls-files '*.php'); do php -l "$f" | tail -1 | sed "s|^|$f: |"; done
-```
+### 11.2 Dev iteration loop
+
+`make dev` (or the long form above) starts two services:
+
+- **`app`** — `runtime` image with `src/`, `views/`, `public/`,
+  `assets/`, `migrations/`, `bin/`, `tailwind.config.js`,
+  `composer.json/.lock`, and `phpunit.xml` bind-mounted from the host.
+  `APP_ENV=development` flips `View.php`'s Twig `auto_reload` on, so
+  template edits are picked up on the next request without a rebuild.
+  `vendor/` is **not** mounted — composer-installed packages stay in
+  the image.
+- **`css-watcher`** — `node:20-alpine` sidecar running
+  `bin/dev-css-watcher.sh`: seeds `public/assets/js/vendor/*` from
+  `node_modules` once, then `tailwindcss --watch` regenerates
+  `public/assets/css/app.css` on every save under `views/`, `src/`, or
+  `public/assets/js/`. Runs as `${HOST_UID}:${HOST_GID}` (exported by
+  the Makefile) so files written into the bind-mounted host paths
+  land with normal ownership and don't need a `sudo chown` later.
+
+Edit cycle:
+
+| Change | Visible after |
+|---|---|
+| `.twig` template, `.php` source | next browser request (no rebuild) |
+| `.css` / new Tailwind class in a view or `.js` | watcher regenerates `app.css` in <1s; F5 |
+| `composer.json/.lock` or `package.json/.lock` | `make dev-build` |
+| `Dockerfile` | `make dev-build` |
+
+### 11.3 Tests + lint
+
+The preferred path is one of:
 
-Run the test suite:
 ```bash
-vendor/bin/phpunit
-# → OK (252 tests, 683 assertions)
+make check        # lint + phpunit, in a one-shot tests container
+make test         # phpunit only
+make lint         # php -l on src/ + tests/
 ```
 
+All three use the dev compose's `tests` service (profile `test`), which
+builds stage 4 of the Dockerfile (`FROM runtime` + `composer install`
+without `--no-dev`). The first run cold-builds that image (~30–60s);
+subsequent runs reuse it.
+
 The Phase 20 parser tests need `ext-dom`, `ext-zip`, `ext-xmlreader`,
-`ext-simplexml`, and `ext-gd` (PhpSpreadsheet's hard requires); on hosts
-that don't have all of them the parser tests auto-skip via
-`extension_loaded()` in `setUp()`. Run inside the Docker image when
-the host PHP is thin:
+`ext-simplexml`, and `ext-gd` — all present in the runtime image, so
+running them via `make test` is always green. On hosts where you'd run
+PHPUnit directly without Docker, those parser tests auto-skip via
+`extension_loaded()` in `setUp()`.
+
+If you have host PHP and want to bypass Docker for a quick syntax check
+during writing:
+
 ```bash
-docker compose build
-docker run --rm -v "$(pwd):/app" -w /app sprint_planer_web-app:latest \
-    sh -c "git config --global --add safe.directory /app \
-        && composer install --no-interaction --no-progress \
-        && vendor/bin/phpunit --colors=never"
+for f in $(git ls-files '*.php'); do php -l "$f" | tail -1 | sed "s|^|$f: |"; done
+```
+
+…but `make lint` does the same thing inside the container with the
+known-good extension set.
+
+### 11.4 Running checks via the `/check` Claude Code skill
+
+A project-level skill at `.claude/skills/check/SKILL.md` invokes a
+Haiku-powered subagent (`.claude/agents/container-tester.md`) that runs
+`make check` and reports back **only** failures + a one-line summary.
+Verbose phpunit / docker build output is consumed by the agent and
+never enters the main session context.
+
+Inside Claude Code:
+
 ```
+/check          # lint + tests
+/check lint     # lint only
+/check test     # tests only
+```
+
+The skill needs no setup beyond a Claude Code session restart after
+checkout (skills + agents are scanned at session start).
+
+### 11.5 Why both compose files
+
+A single file with a runtime env-var switch can swap a build target but
+can't conditionally add bind mounts or extra services. The overlay
+pattern keeps `docker-compose.yml` prod-shaped (anyone who runs plain
+`docker compose up` on a server gets prod) while the explicit
+`-f docker-compose.dev.yml` opt-in adds dev mounts + the watcher.
+Neither file auto-loads the other — there is no `compose.override.yml`
+in the tree, so accidental dev-on-prod is structurally impossible.
 
 ## 12. How to resume in a fresh Claude session
 

+ 8 - 0
doc/admin-manual.md

@@ -268,6 +268,14 @@ The project ships a multi-stage Dockerfile (Node-based Tailwind build →
 PHP 8.3 + Apache runtime) and a `docker-compose.yml` that wires the
 container to the host.
 
+> **Operators vs. developers.** Everything in this section is the
+> production deployment path: the bare `docker compose` commands
+> targeting `docker-compose.yml` only. Developers iterating on code
+> use a separate `docker-compose.dev.yml` overlay (bind mounts +
+> `tailwindcss --watch` sidecar) plus a `Makefile` of wrappers; that
+> story lives in `SPEC.md` §11 and is not relevant to operating a
+> deployment.
+
 ### 4.1 First start
 
 ```bash