# Sprint Planner

Web replacement for an Excel-based sprint planning workbook used by a
~15-person ops/dev team. One sprint per page: an Arbeitstage matrix
(working-days availability per worker per week) on top, a task list with
per-worker day allocations on the bottom. Proper auth, database, and a
per-cell audit trail.

## Stack

- PHP 8.3 + Apache (Docker, multi-stage build)
- SQLite via PDO (file on a mounted volume)
- Server-rendered PHP templates + Tailwind CSS (vendored, compiled by a
  Node build stage) + jQuery / jQuery UI (CDN)
- Microsoft Entra ID (OpenID Connect, Authorization Code + PKCE), with an
  optional local-admin fallback for dev / on-prem deployments
- Dark mode (manual toggle, no FOUC), strict CSP

## Quick setup

For a development or pilot install on a single host:

```bash
# 1. Clone and enter the repo
git clone <repository-url> sprint_planer_web
cd sprint_planer_web

# 2. Create the .env file from the template
cp .env.example .env

# 3. Generate a hash for the local-admin password. The app verifies sign-ins
#    with password_verify() against LOCAL_ADMIN_PASSWORD_HASH, so the
#    plaintext password never lands on disk. Pick the snippet that matches
#    what you have installed:
#
#    a) Host PHP 8 (any minor):
#       php -r 'echo password_hash(readline("Password: "), PASSWORD_DEFAULT), PHP_EOL;'
#
#    b) No host PHP — use the Docker image you already build with:
#       docker run --rm -it php:8.3-cli php -r \
#         'echo password_hash(readline("Password: "), PASSWORD_DEFAULT), PHP_EOL;'
#
#    Both prompt for the password (no echo on most TTYs) and print a
#    bcrypt string starting with `$2y$`. Paste it into .env as:
#       LOCAL_ADMIN_EMAIL='you@example.com'
#       LOCAL_ADMIN_PASSWORD_HASH='$2y$...'
#    Use single quotes — the `$` in the hash will otherwise be eaten by the
#    shell on `docker compose up`.
#
#    For Entra-based sign-in, fill ENTRA_TENANT_ID / ENTRA_CLIENT_ID /
#    ENTRA_CLIENT_SECRET instead (or in addition).
chmod 600 .env

# 4. Build and start the container (compose maps host port HTTP_PORT
#    from .env → container 80; default 8080, change in .env to taste)
docker compose up -d --build
#    Equivalent shortcut: `./appctl prod start`

# 5. Open the app
xdg-open http://localhost:8080   # substitute your HTTP_PORT if changed
```

If you signed in via the local-admin fallback you are immediately the
administrator (audit-logged as `BOOTSTRAP_ADMIN` with `via=local`). For
an OIDC-only deploy, nominate the first admin up front via
`BOOTSTRAP_ADMIN_OID` or `BOOTSTRAP_ADMIN_EMAIL` in `.env` — without one
of them set the OIDC path will not auto-promote anyone (this closes
finding R01-N03 in `doc/REVIEW_01.md`). Subsequent admin promotions
happen through the **Users** page in the hamburger menu.

The SQLite database is created at `./data/app.sqlite` on the host
(mounted into the container at `/var/www/data/app.sqlite`). Migrations
run from the Docker entrypoint (`bin/docker-entrypoint.sh` → `php
bin/migrate.php`) BEFORE Apache binds the port — a failed migration
aborts container start instead of leaving a half-applied schema
(R01-N22). The web request path only checks `schema_version` and
returns 503 Service Unavailable when something is pending. Wiping
`./data/` resets the application to a blank slate.

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
./appctl dev start   # 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
./appctl dev build
```

`./appctl help` lists every command. The first `./appctl` invocation
in an interactive shell offers to wire up bash completion (a `source`
line into `~/.bashrc`); answer `n` to skip — re-asking is suppressed
via a marker in `~/.config/appctl/`. Full architecture (overlay
rationale, dev edit-cycle table, `tests` Docker stage) lives in
[`SPEC.md`](SPEC.md) §11.

## Layout

```
public/      front controller (index.php) + web root
src/         application code (App\ namespace, PSR-4)
views/       PHP templates
migrations/  numbered .sql files applied by Migrator
assets/      Tailwind source (compiled into public/assets/css/app.css)
data/        SQLite + sessions (volume-mounted; gitignored)
doc/         operator-facing documentation (admin manual)
```

## Documentation

- [`doc/admin-manual.md`](doc/admin-manual.md) — administrator setup,
  configuration, and operations guide.
- [`SPEC.md`](SPEC.md) — full specification: schema, routes, capacity
  math, build phase history. Read this if you intend to modify the code.
- [`ACCEPTANCE.md`](ACCEPTANCE.md) — manual acceptance checklist used to
  validate releases.

## 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
./appctl check    # lint + phpunit, in a one-shot --rm container
./appctl test     # phpunit only
./appctl 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.
