Przeglądaj źródła

Docs: rename HANDOFF.md to SPEC.md, add admin manual, refresh README

- Rename HANDOFF.md → SPEC.md (git mv preserves history); rewrite the
  prose self-references in the file so "this is the handoff doc" reads
  as "this is the spec." Git-log block left intact as historical record.
- Add doc/admin-manual.md: prerequisites, .env walkthrough (Entra +
  local-admin fallback), running containers, day-to-day ops (admin
  promotion, backups, audit log, reset, upgrades), troubleshooting.
- Rewrite README.md: corrected stack description (multi-stage build,
  vendored Tailwind, dark mode), expanded Quick setup section with
  correct host port (8088, not the stale 8080), bootstrap-admin note,
  pointers to admin manual / SPEC / ACCEPTANCE, tests one-liner.
chiappa 3 dni temu
rodzic
commit
c5eef6ada1
3 zmienionych plików z 411 dodań i 24 usunięć
  1. 60 16
      README.md
  2. 10 8
      SPEC.md
  3. 341 0
      doc/admin-manual.md

+ 60 - 16
README.md

@@ -1,27 +1,59 @@
 # Sprint Planner
 
-Web replacement for the Excel-based sprint planning tool.
+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, single container)
-- SQLite (file on mounted volume)
-- Server-rendered PHP templates + Tailwind CSS + jQuery / jQuery UI (all via CDN)
-- Microsoft Entra ID (OpenID Connect) for auth
+- 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 start
+## 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
-# fill in ENTRA_TENANT_ID, ENTRA_CLIENT_ID, ENTRA_CLIENT_SECRET, SESSION_SECRET
-docker compose up --build
+
+# 3. Edit .env. The minimum to sign in without Entra is:
+#      SESSION_SECRET=<random 32+ bytes, e.g. `openssl rand -hex 32`>
+#      LOCAL_ADMIN_EMAIL=you@example.com
+#      LOCAL_ADMIN_PASSWORD=<a long passphrase>
+#    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 8088 → container 80)
+docker compose up -d --build
+
+# 5. Open the app
+xdg-open http://localhost:8088   # or just visit it in a browser
 ```
 
-Then open http://localhost:8080.
+The first user to sign in is automatically promoted to administrator
+(audit-logged as `BOOTSTRAP_ADMIN`). Subsequent admin promotions happen
+through the **Users** page in the hamburger menu.
 
-The SQLite database lives at `./data/app.sqlite` (mounted into the container at
-`/var/www/data/app.sqlite`). Migrations run automatically on the first request
-after container start.
+The SQLite database is created on first request at `./data/app.sqlite` on
+the host (mounted into the container at `/var/www/data/app.sqlite`).
+Migrations run automatically on container start. 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).
 
 ## Layout
 
@@ -30,11 +62,23 @@ 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/      static JS / CSS
+assets/      Tailwind source (compiled into public/assets/css/app.css)
 data/        SQLite + sessions (volume-mounted; gitignored)
+doc/         operator-facing documentation (admin manual)
 ```
 
-## Build phases
+## 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
 
-This repo is built incrementally. See the spec for phase definitions. Current
-phase: **Phase 1 — skeleton**.
+```bash
+vendor/bin/phpunit
+# → OK (88 tests, 208 assertions)
+```

+ 10 - 8
HANDOFF.md → SPEC.md

@@ -1,4 +1,4 @@
-# Sprint Planner — Handoff
+# Sprint Planner — Specification
 
 Single source of truth to resume work in a fresh session. Keep this file in
 sync with the code; if something here disagrees with the repo, trust the repo.
@@ -6,8 +6,8 @@ sync with the code; if something here disagrees with the repo, trust the repo.
 > **Maintenance rule (read first, then keep doing it).**
 > After every commit that completes a phase or substantive change, update
 > §9 (move the entry from Upcoming → Shipped with the SHA) and §13 (git
-> history). If new files were added, refresh §3. Commit the HANDOFF
-> update separately. See §14.
+> history). If new files were added, refresh §3. Commit the SPEC update
+> separately. See §14.
 
 ## 1. What this is
 
@@ -47,7 +47,9 @@ per-cell audit trail.
 ├── tailwind.config.js
 ├── phpunit.xml
 ├── ACCEPTANCE.md               # spec §10 manual checklist walkthrough
-├── HANDOFF.md                  # this file
+├── SPEC.md                     # this file
+├── doc/
+│   └── admin-manual.md         # operator-facing setup + run guide
 ├── assets/
 │   └── css/input.css           # Tailwind entry, compiled into public/assets/css/app.css
 ├── public/
@@ -653,18 +655,18 @@ vendor/bin/phpunit
 Tell Claude:
 
 > Working on `/Users/achiappa/Development/claude_code_private/sprint_planer_web`.
-> Read `HANDOFF.md`, the git log, and `ACCEPTANCE.md`. Phases 1–17
+> Read `SPEC.md`, the git log, and `ACCEPTANCE.md`. Phases 1–17
 > are shipped (see §9) — nothing is currently scheduled. Outstanding
 > items are in §10 (mostly a human-run acceptance walkthrough in the
 > running container, plus the jQuery UI dark-mode cosmetic gap noted
 > there). If I ask you to plan or work a new phase, follow the
-> maintenance rule in §14 — commit code, then commit a HANDOFF.md
+> maintenance rule in §14 — commit code, then commit a SPEC.md
 > update separately that marks the new work shipped with its SHA.
 
 Claude should verify what's described here against actual repo state
 before acting — nothing here is load-bearing once it grows stale.
 
-## 13. Git history (as of this handoff)
+## 13. Git history (as of this writing)
 
 ```
 e93df6b Fix: stepper close via direct element listeners (not doc delegation)
@@ -746,7 +748,7 @@ After every commit that completes a phase or substantive change:
 3. **Update §13.** Append the new SHA to the git history block.
 4. **If new files / directories were added or moved, refresh §3.**
 5. **If the test count changed, update §11's expected count.**
-6. **Commit the HANDOFF.md update as its own commit.** Keeping it
+6. **Commit the SPEC.md update as its own commit.** Keeping it
    separate means a `git revert` of the code revert leaves the doc
    honest by reverting alongside.
 7. If the change affects the resume prompt in §12 (e.g. a new "next

+ 341 - 0
doc/admin-manual.md

@@ -0,0 +1,341 @@
+# Sprint Planner — Administrator Manual
+
+This guide walks an administrator through the initial configuration and the
+day-to-day operation of the Sprint Planner web application. It assumes a
+Linux host with Docker and Docker Compose v2 installed.
+
+For architecture, schema, and feature history, see [SPEC.md](../SPEC.md) in
+the repository root.
+
+---
+
+## 1. Prerequisites
+
+- A Linux (or macOS / Windows with WSL2) host with:
+  - Docker Engine 24+ and the `docker compose` plugin.
+  - Outbound HTTPS access during build (Composer + npm pull dependencies).
+- One of the following for sign-in:
+  - **Microsoft Entra ID tenant** with permission to register an application
+    (preferred for production), or
+  - **Local admin credentials** (email + password) — useful for initial
+    setup, on-prem-only deployments, or development.
+- A directory on the host to hold the persistent SQLite database and PHP
+  session files. The compose file mounts `./data` next to
+  `docker-compose.yml`; nothing else is needed for storage.
+
+---
+
+## 2. Getting the code
+
+```bash
+git clone <repository-url> sprint_planer_web
+cd sprint_planer_web
+```
+
+All commands in the rest of this manual are run from the repository root.
+
+---
+
+## 3. Initial configuration of `.env`
+
+The application reads its configuration from a single `.env` file in the
+repository root. Start by copying the template:
+
+```bash
+cp .env.example .env
+```
+
+Open `.env` in an editor and fill in the values described below. The file
+must be readable by the user that runs `docker compose`; on a shared host
+restrict it with `chmod 600 .env`.
+
+### 3.1 Entra ID (Microsoft 365) — production sign-in
+
+If your team will sign in with their Microsoft 365 / Entra accounts:
+
+| Variable              | What to put there |
+|-----------------------|-------------------|
+| `ENTRA_TENANT_ID`     | Directory (tenant) ID — a GUID, visible on the Entra "Overview" page. |
+| `ENTRA_CLIENT_ID`     | Application (client) ID of the app registration. |
+| `ENTRA_CLIENT_SECRET` | A client secret you generated for the app registration. Treat as sensitive. |
+
+In the Entra app registration, configure:
+
+- **Redirect URI** (Web platform): `{APP_BASE_URL}/auth/callback`,
+  e.g. `https://sprint.example.com/auth/callback`.
+- **ID tokens** issued from the auth endpoint: enabled.
+- **Supported account types**: usually "Accounts in this organisational
+  directory only".
+- **API permissions**: `openid`, `profile`, `email` (delegated, default for
+  OIDC). Grant admin consent if your tenant requires it.
+
+Leave Entra fields blank if you only intend to use the local-admin
+fallback. The OIDC sign-in button still appears, but clicking it will fail
+until the variables are populated — keep `LOCAL_ADMIN_EMAIL` /
+`LOCAL_ADMIN_PASSWORD` filled if you skip Entra entirely.
+
+### 3.2 Application URL
+
+```
+APP_BASE_URL=https://sprint.example.com
+```
+
+The base URL the application is reachable at, **without** trailing slash.
+This is used to construct the OIDC redirect URI and must match exactly
+what is registered in Entra. For local testing the default
+`http://localhost:8080` is fine — but the compose file ships the app on
+port `8088`, so use `http://localhost:8088` if you have not edited
+`docker-compose.yml`.
+
+### 3.3 Session secret
+
+```
+SESSION_SECRET=<random string, at least 32 bytes>
+```
+
+Used to derive the session cookie name and CSRF tokens. Generate one with
+either of:
+
+```bash
+openssl rand -hex 32
+# or
+head -c 48 /dev/urandom | base64
+```
+
+Rotating this value invalidates all active sessions — users will need to
+sign in again, but no data is lost.
+
+### 3.4 Database and session storage paths
+
+```
+DB_PATH=/var/www/data/app.sqlite
+SESSION_PATH=/var/www/data/sessions
+```
+
+Leave the defaults unless you are also remapping the volume. The parent
+directory `/var/www/data` is the volume mount point inside the container
+and corresponds to `./data/` on the host.
+
+### 3.5 Environment mode
+
+```
+APP_ENV=production
+```
+
+`production` silences verbose PHP errors. Any other value (e.g. `dev`)
+turns them on — useful when troubleshooting in a non-public install.
+
+### 3.6 Local admin fallback (optional)
+
+```
+LOCAL_ADMIN_EMAIL=admin@example.com
+LOCAL_ADMIN_PASSWORD=<a long passphrase>
+LOCAL_ADMIN_NAME=Local Admin
+```
+
+If both `LOCAL_ADMIN_EMAIL` and `LOCAL_ADMIN_PASSWORD` are set, a second
+sign-in form appears at `/auth/local`. The password is **compared in
+plain text** against the env value — so:
+
+- Pick a long, unique passphrase you do not reuse anywhere else.
+- Make sure `.env` is `chmod 600` and owned by a single trusted user.
+- Rotate the passphrase by editing `.env` and restarting the container.
+
+The local-admin user is recorded in the database as
+`entra_oid = "local:<email>"` with `is_admin = 1`. If the `users` table
+is empty at the moment of the first successful login (OIDC or local),
+that user is auto-promoted to admin and an audit entry of type
+`BOOTSTRAP_ADMIN` is recorded. This is how the very first administrator
+is created — there is no separate "create admin" step.
+
+To disable the fallback later, blank both variables and restart.
+
+---
+
+## 4. Running the containers
+
+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.
+
+### 4.1 First start
+
+```bash
+docker compose up --build
+```
+
+What happens:
+
+1. Compose builds the image. The Node stage compiles
+   `assets/css/input.css` into `public/assets/css/app.css`. The PHP
+   stage installs Composer dependencies and copies the application
+   source into `/var/www/html`.
+2. The container starts Apache on port 80, exposed on the host as
+   port **8088** (see `docker-compose.yml`).
+3. On the first request, the migration runner creates `app.sqlite`
+   inside the volume and applies all migrations under `migrations/`.
+
+Open `http://<host>:8088`. If you used the local-admin fallback, sign
+in at `/auth/local`. Otherwise click the Entra sign-in CTA on `/`.
+
+### 4.2 Running detached (recommended for production)
+
+```bash
+docker compose up -d --build
+```
+
+Logs are then read with:
+
+```bash
+docker compose logs -f
+```
+
+Stopping the stack:
+
+```bash
+docker compose down
+```
+
+`down` keeps the `./data` volume intact — your database and sessions
+survive. To wipe and start over, stop the stack and remove `./data`
+manually (see §5.4).
+
+### 4.3 Rebuilding after changes
+
+You only need to rebuild the image when the build inputs change:
+
+- `Dockerfile`, `composer.json` / `composer.lock`, or `package.json` /
+  `package-lock.json`
+- Tailwind sources under `assets/css/` or any view file that introduces
+  new utility classes (the css-builder stage scans the views).
+
+```bash
+docker compose build --no-cache && docker compose up -d
+```
+
+For pure PHP / view edits without a rebuild, restart the container:
+
+```bash
+docker compose restart
+```
+
+### 4.4 Health check
+
+The application exposes an unauthenticated `GET /healthz` route that
+returns `200 OK` with a small JSON body. Use it from a load balancer or
+uptime monitor:
+
+```bash
+curl -fsS http://<host>:8088/healthz
+```
+
+### 4.5 Changing the host port
+
+Edit the `ports` line in `docker-compose.yml`:
+
+```yaml
+ports:
+  - "8088:80"
+```
+
+Change `8088` to any free port on the host. After editing, `docker
+compose up -d` is enough — no rebuild required. Update `APP_BASE_URL`
+in `.env` and the redirect URI in Entra to match.
+
+---
+
+## 5. Day-to-day administration
+
+### 5.1 Promoting and demoting administrators
+
+Once the bootstrap admin is signed in, additional administrators are
+managed through the web UI:
+
+1. Open the hamburger menu → **Users**.
+2. Toggle the admin checkbox next to a user and confirm.
+
+Guardrails enforced server-side:
+
+- An administrator cannot demote themselves while signed in (avoids
+  locking yourself out).
+- The last remaining administrator cannot be demoted (avoids locking
+  the org out).
+
+If both guardrails fire at once — i.e. you are the only admin — the
+only safe escape is to sign in via the local-admin fallback (if
+configured) and promote a second account first.
+
+### 5.2 Backups
+
+Everything that needs a backup lives under `./data/`:
+
+- `app.sqlite` — the database, including audit log.
+- `sessions/` — active PHP session files. Losing these only forces
+  re-login, no data loss.
+
+A safe nightly backup is a straight file copy while the container is
+running. SQLite handles concurrent reads fine; for a guaranteed
+consistent snapshot, prefer `sqlite3` `.backup`:
+
+```bash
+docker compose exec app sqlite3 /var/www/data/app.sqlite \
+  ".backup /var/www/data/app.sqlite.bak"
+cp ./data/app.sqlite.bak /your/offsite/backup/location/
+```
+
+### 5.3 Inspecting the audit log
+
+Every create / update / delete on a domain entity is recorded, plus
+sign-in events. Admins can browse and filter the log via the hamburger
+menu → **Audit log**, with filters by entity type, action, user, and
+date range, and collapsible JSON diffs per row.
+
+### 5.4 Resetting the application
+
+To wipe all data and start from a blank slate:
+
+```bash
+docker compose down
+rm -rf ./data
+docker compose up -d
+```
+
+Migrations run again on the next request and you are back to "no users,
+first sign-in becomes admin".
+
+### 5.5 Updating to a new release
+
+```bash
+git pull
+docker compose build --no-cache
+docker compose up -d
+```
+
+Schema migrations under `migrations/` run automatically on the next
+request after restart. Always take a backup of `./data/app.sqlite`
+before pulling.
+
+---
+
+## 6. Troubleshooting
+
+| Symptom | Likely cause | Fix |
+|---|---|---|
+| `/auth/login` returns "redirect URI mismatch" | The redirect URI registered in Entra does not equal `{APP_BASE_URL}/auth/callback`. | Update either `APP_BASE_URL` in `.env` or the Entra app registration. |
+| `/auth/local` returns 404 | `LOCAL_ADMIN_EMAIL` or `LOCAL_ADMIN_PASSWORD` is blank. | Set both, then `docker compose restart`. |
+| Sign-in succeeds but every page shows "not authorised" | The user has no `is_admin` flag and is trying to access an admin-only page. | Promote them via Users (logged in as another admin). |
+| Container restarts in a loop | Most often a malformed `.env` line or a permission problem on `./data`. | `docker compose logs` will show the PHP fatal. Check that `./data` is writable by the `www-data` user inside the container (uid 33 on the Apache image). |
+| CSS looks broken / unstyled | The Node build stage was skipped or used a stale layer. | `docker compose build --no-cache` and restart. |
+| OIDC works but PHP 8.4 deprecation warnings appear in logs | Known upstream issue in `jumbojett/openid-connect-php`. | Harmless on PHP 8.3 (the shipped runtime); ignore until upstream releases a fix. |
+
+For anything else, check the audit log first — most user-facing errors
+also leave a trail there with the exact request that failed.
+
+---
+
+## 7. Where to go next
+
+- [SPEC.md](../SPEC.md) — full specification, schema, route list, and
+  build phase history. Read this if you intend to modify the code.
+- [ACCEPTANCE.md](../ACCEPTANCE.md) — manual acceptance checklist used
+  to validate releases.