Explorar el Código

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 hace 3 días
padre
commit
c5eef6ada1
Se han modificado 3 ficheros con 411 adiciones y 24 borrados
  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
 # 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
 ## 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
 ```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
 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
 ## Layout
 
 
@@ -30,11 +62,23 @@ public/      front controller (index.php) + web root
 src/         application code (App\ namespace, PSR-4)
 src/         application code (App\ namespace, PSR-4)
 views/       PHP templates
 views/       PHP templates
 migrations/  numbered .sql files applied by Migrator
 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)
 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
 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.
 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).**
 > **Maintenance rule (read first, then keep doing it).**
 > After every commit that completes a phase or substantive change, update
 > After every commit that completes a phase or substantive change, update
 > §9 (move the entry from Upcoming → Shipped with the SHA) and §13 (git
 > §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
 ## 1. What this is
 
 
@@ -47,7 +47,9 @@ per-cell audit trail.
 ├── tailwind.config.js
 ├── tailwind.config.js
 ├── phpunit.xml
 ├── phpunit.xml
 ├── ACCEPTANCE.md               # spec §10 manual checklist walkthrough
 ├── 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/
 ├── assets/
 │   └── css/input.css           # Tailwind entry, compiled into public/assets/css/app.css
 │   └── css/input.css           # Tailwind entry, compiled into public/assets/css/app.css
 ├── public/
 ├── public/
@@ -653,18 +655,18 @@ vendor/bin/phpunit
 Tell Claude:
 Tell Claude:
 
 
 > Working on `/Users/achiappa/Development/claude_code_private/sprint_planer_web`.
 > 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
 > are shipped (see §9) — nothing is currently scheduled. Outstanding
 > items are in §10 (mostly a human-run acceptance walkthrough in the
 > items are in §10 (mostly a human-run acceptance walkthrough in the
 > running container, plus the jQuery UI dark-mode cosmetic gap noted
 > 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
 > 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.
 > update separately that marks the new work shipped with its SHA.
 
 
 Claude should verify what's described here against actual repo state
 Claude should verify what's described here against actual repo state
 before acting — nothing here is load-bearing once it grows stale.
 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)
 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.
 3. **Update §13.** Append the new SHA to the git history block.
 4. **If new files / directories were added or moved, refresh §3.**
 4. **If new files / directories were added or moved, refresh §3.**
 5. **If the test count changed, update §11's expected count.**
 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
    separate means a `git revert` of the code revert leaves the doc
    honest by reverting alongside.
    honest by reverting alongside.
 7. If the change affects the resume prompt in §12 (e.g. a new "next
 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.