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