|
|
@@ -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
|
|
|
|