|
|
@@ -0,0 +1,294 @@
|
|
|
+# Sprint Planner — Handoff
|
|
|
+
|
|
|
+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.
|
|
|
+
|
|
|
+## 1. What this is
|
|
|
+
|
|
|
+A web replacement for an Excel sprint-planning workbook used by a ~15-person
|
|
|
+ops/dev team. One sheet per sprint in the original; per sheet:
|
|
|
+
|
|
|
+- **Arbeitstage matrix** (top): max working days per week + per-worker
|
|
|
+ availability per week.
|
|
|
+- **Task list** (bottom): one row per task with priority, owner, total days,
|
|
|
+ and a per-worker days-allocated cell for each sprint worker.
|
|
|
+
|
|
|
+The web app reproduces that workflow with proper auth, database, and
|
|
|
+per-cell audit trail.
|
|
|
+
|
|
|
+## 2. Tech stack (non-negotiable)
|
|
|
+
|
|
|
+- Runtime: Docker, single container, `php:8.3-apache` base.
|
|
|
+- Language: PHP 8.3, strict types, PSR-12.
|
|
|
+- Database: SQLite via PDO, file at `/var/www/data/app.sqlite` (mounted volume).
|
|
|
+- Front end: server-rendered PHP templates + Tailwind (Play CDN) + jQuery 3.x
|
|
|
+ + jQuery UI 1.13 (CDN). No build step.
|
|
|
+- Auth: Microsoft Entra ID via OpenID Connect (Authorization Code + PKCE),
|
|
|
+ plus an optional env-configured "local admin" fallback for dev / on-prem.
|
|
|
+- Composer deps: `jumbojett/openid-connect-php`, `vlucas/phpdotenv`,
|
|
|
+ `phpunit/phpunit` (dev).
|
|
|
+
|
|
|
+## 3. Directory layout
|
|
|
+
|
|
|
+```
|
|
|
+├── Dockerfile
|
|
|
+├── docker-compose.yml
|
|
|
+├── .env.example
|
|
|
+├── composer.json / composer.lock
|
|
|
+├── public/
|
|
|
+│ ├── index.php # front controller + router wiring
|
|
|
+│ ├── .htaccess # belt-and-suspenders rewrite
|
|
|
+│ └── assets/
|
|
|
+│ └── js/
|
|
|
+│ ├── sprint-planner.js # main planning view (/sprints/{id})
|
|
|
+│ └── sprint-settings.js # settings page
|
|
|
+├── src/
|
|
|
+│ ├── Auth/ LocalAdmin, OidcClient, SessionGuard
|
|
|
+│ ├── Controllers/ AuthController, WorkerController, SprintController, TaskController
|
|
|
+│ ├── Db/ Connection, Migrator
|
|
|
+│ ├── Domain/ User, Worker, Sprint, SprintWeek, SprintWorker, SprintWorkerDay, Task, TaskAssignment
|
|
|
+│ ├── Http/ Request, Response, Router, View (+ e() helper)
|
|
|
+│ ├── Repositories/ UserRepository, WorkerRepository, SprintRepository, SprintWeekRepository,
|
|
|
+│ │ SprintWorkerRepository, SprintWorkerDayRepository,
|
|
|
+│ │ TaskRepository, TaskAssignmentRepository
|
|
|
+│ └── Services/ AuditLogger, CapacityCalculator
|
|
|
+├── migrations/ 001_init.sql (full schema per spec §3)
|
|
|
+├── views/ layout.php, home.php, auth/local.php,
|
|
|
+│ workers/index.php, sprints/{new,show,settings}.php
|
|
|
+├── data/ SQLite + sessions directory (volume-mounted, gitignored)
|
|
|
+└── tests/ (empty; PHPUnit setup pending in Phase 7)
|
|
|
+```
|
|
|
+
|
|
|
+## 4. Schema (migrations/001_init.sql)
|
|
|
+
|
|
|
+Tables (already applied): `users`, `workers`, `sprints`, `sprint_weeks`,
|
|
|
+`sprint_workers`, `sprint_worker_days`, `tasks`, `task_assignments`,
|
|
|
+`audit_log`, plus the `schema_version` tracking table.
|
|
|
+
|
|
|
+Indexes: `idx_audit_occurred_at`, `idx_audit_entity`, `idx_tasks_sprint`,
|
|
|
+`idx_sw_sprint`.
|
|
|
+
|
|
|
+Value constraints enforced in PHP (not SQL):
|
|
|
+- All `days` fields: non-negative multiple of 0.5.
|
|
|
+- `max_working_days`, `sprint_worker_days.days` ∈ {0, 0.5, …, 5}.
|
|
|
+- `task_assignments.days` ≥ 0, no hard upper bound.
|
|
|
+- `reserve_fraction`, `rtb` ∈ [0, 1].
|
|
|
+
|
|
|
+## 5. Capacity math (spec §6.5)
|
|
|
+
|
|
|
+Runs identically in `App\Services\CapacityCalculator` (PHP) and in
|
|
|
+`sprint-planner.js` (JS). Any edit must touch both.
|
|
|
+
|
|
|
+```
|
|
|
+round_half(x) = round(x * 2) / 2
|
|
|
+ressourcen = Σ sprint_worker_days.days per sprint worker
|
|
|
+after_reserves = round_half(ressourcen * (1 − sprint.reserve_fraction))
|
|
|
+committed_p1 = Σ task_assignments.days where task.priority = 1
|
|
|
+available = after_reserves − committed_p1
|
|
|
+```
|
|
|
+
|
|
|
+Priority-2 assignments do NOT consume capacity (they're "nice to have").
|
|
|
+A negative `available` turns the cell red but is not blocked.
|
|
|
+
|
|
|
+## 6. Routes
|
|
|
+
|
|
|
+Pages (HTML):
|
|
|
+| Method | Path | Auth |
|
|
|
+|--------|-----------------------------|----------------|
|
|
|
+| GET | `/` | any (anon → sign-in CTA) |
|
|
|
+| GET | `/healthz` | — |
|
|
|
+| GET | `/auth/login` | — |
|
|
|
+| GET | `/auth/callback` | — |
|
|
|
+| GET | `/auth/local` | — (404 if disabled) |
|
|
|
+| POST | `/auth/local` | — (404 if disabled) |
|
|
|
+| POST | `/auth/logout` | signed-in |
|
|
|
+| GET | `/workers` | admin |
|
|
|
+| POST | `/workers` | admin |
|
|
|
+| POST | `/workers/{id}` | admin |
|
|
|
+| GET | `/sprints/new` | admin |
|
|
|
+| POST | `/sprints` | admin |
|
|
|
+| GET | `/sprints/{id}` | signed-in |
|
|
|
+| GET | `/sprints/{id}/settings` | admin |
|
|
|
+
|
|
|
+JSON (admin-only, CSRF via `X-CSRF-Token` header; envelope per spec §7):
|
|
|
+| Method | Path | What |
|
|
|
+|--------|----------------------------------------------|---------------|
|
|
|
+| PATCH | `/sprints/{id}` | name/dates/reserve |
|
|
|
+| POST | `/sprints/{id}/weeks` | resize week set |
|
|
|
+| POST | `/sprints/{id}/workers` | add sprint worker |
|
|
|
+| DELETE | `/sprints/{id}/workers/{sw_id}` | remove sprint worker |
|
|
|
+| POST | `/sprints/{id}/workers/reorder` | reorder sprint workers |
|
|
|
+| PATCH | `/sprints/{id}/workers/{sw_id}` | set rtb |
|
|
|
+| PATCH | `/sprints/{id}/week-cells` | batch day cells |
|
|
|
+| PATCH | `/sprints/{id}/week/{week_id}` | set max_working_days |
|
|
|
+| POST | `/sprints/{id}/tasks` | create task |
|
|
|
+| POST | `/sprints/{id}/tasks/reorder` | reorder tasks |
|
|
|
+| PATCH | `/tasks/{id}` | title/owner/priority |
|
|
|
+| DELETE | `/tasks/{id}` | delete task (audits cascaded assignments) |
|
|
|
+| PATCH | `/tasks/{id}/assignments` | batch assignment cells |
|
|
|
+
|
|
|
+Response envelope:
|
|
|
+- Success: `{"ok": true, "data": …}`
|
|
|
+- Failure: `{"ok": false, "error": {"code", "message", "details?"}}`
|
|
|
+- Validation errors: HTTP 422.
|
|
|
+
|
|
|
+## 7. Audit logging rules (cross-cutting)
|
|
|
+
|
|
|
+`App\Services\AuditLogger::record(action, entityType, entityId, before, after, userId, userEmail, ip, ua)`
|
|
|
+is called inside the same transaction as the DB change. Controllers prefer
|
|
|
+`recordForRequest(..., Request, ?User)` to drop the repeated plumbing.
|
|
|
+
|
|
|
+- Every CREATE / UPDATE / DELETE on a domain table → exactly one row.
|
|
|
+- Bulk operations (batch cell save) → one row per changed cell.
|
|
|
+- A no-op UPDATE (canonical-JSON-equal before/after) → no row.
|
|
|
+- FK-cascading deletes must be audited by the controller BEFORE calling the
|
|
|
+ cascading delete. See `TaskController::delete()` as the reference.
|
|
|
+- Non-mutation events (LOGIN, LOGOUT, LOGIN_FAILED, BOOTSTRAP_ADMIN) → always
|
|
|
+ one row.
|
|
|
+
|
|
|
+## 8. Env (.env.example)
|
|
|
+
|
|
|
+```
|
|
|
+ENTRA_TENANT_ID=
|
|
|
+ENTRA_CLIENT_ID=
|
|
|
+ENTRA_CLIENT_SECRET=
|
|
|
+APP_BASE_URL=http://localhost:8080
|
|
|
+SESSION_SECRET=
|
|
|
+DB_PATH=/var/www/data/app.sqlite
|
|
|
+SESSION_PATH=/var/www/data/sessions
|
|
|
+APP_ENV=production
|
|
|
+
|
|
|
+# Optional local admin fallback (disables when blank).
|
|
|
+# Password is compared verbatim (not hashed) — .env must be file-permissions
|
|
|
+# protected. The resulting user is entra_oid="local:<email>", is_admin=1.
|
|
|
+LOCAL_ADMIN_EMAIL=
|
|
|
+LOCAL_ADMIN_PASSWORD=
|
|
|
+LOCAL_ADMIN_NAME=Local Admin
|
|
|
+```
|
|
|
+
|
|
|
+First-login bootstrap: when the `users` table is empty at the moment of
|
|
|
+successful login (either OIDC or local), that user is promoted to `is_admin=1`
|
|
|
+with a `BOOTSTRAP_ADMIN` audit row.
|
|
|
+
|
|
|
+## 9. Build phases — status
|
|
|
+
|
|
|
+- [x] **Phase 1 — Skeleton** (`58a6b30`): Dockerfile, docker-compose, composer,
|
|
|
+ front controller, router, migrator, hello home, `/healthz`.
|
|
|
+- [x] **Phase 2 — Auth** (`be193d2`, hotfix `83493d0`): Entra OIDC with PKCE,
|
|
|
+ session + CSRF, first-user-is-admin bootstrap, login/logout/LOGIN_FAILED/
|
|
|
+ BOOTSTRAP_ADMIN audit, local-admin fallback, scalar-safe Request.
|
|
|
+- [x] **Apache routing fix** (`82ddc98`): replaced sed-based docroot hack with
|
|
|
+ explicit site config using `FallbackResource /index.php`.
|
|
|
+- [x] **Phase 3 — Workers + sprints + audit** (`f189e7d`): workers CRUD,
|
|
|
+ sprints create, generic AuditLogger wired into repositories, home shows
|
|
|
+ sprint list.
|
|
|
+- [x] **Phase 4 — Sprint settings** (`38ba151`): meta edit, weeks resize,
|
|
|
+ worker membership add/remove/reorder (jQuery UI sortable), per-row RTB.
|
|
|
+- [x] **Phase 5 — Arbeitstage grid** (`515d7d0`): editable matrix,
|
|
|
+ CapacityCalculator (server + client), per-cell persistence with audit.
|
|
|
+- [x] **Phase 6 — Task list** (`ad78283`): task CRUD, assignments grid,
|
|
|
+ client-side sort/filter/search, drag-reorder, capacity updates with
|
|
|
+ committed prio-1.
|
|
|
+- [x] **SRI fix** (`927b708`): dropped hand-typed SRI hashes, guarded
|
|
|
+ `.sortable()` calls so missing jQuery UI fails soft.
|
|
|
+- [ ] **Phase 7 — Audit viewer + polish** (pending). Required deliverables:
|
|
|
+ - [ ] `/audit` admin page: reverse-chronological, paginated (50/page),
|
|
|
+ filters (user / action / entity_type / date range / entity_id search),
|
|
|
+ pretty-printed collapsible before/after JSON.
|
|
|
+ - [ ] Security headers: `X-Content-Type-Options: nosniff`,
|
|
|
+ `X-Frame-Options: DENY`, `Referrer-Policy: strict-origin-when-cross-origin`,
|
|
|
+ a strict-ish CSP allowing `cdn.tailwindcss.com` and `code.jquery.com`.
|
|
|
+ - [ ] Double-check CSRF on every mutation (mostly in place — verify).
|
|
|
+ - [ ] PHPUnit tests from spec §10:
|
|
|
+ - `CapacityCalculator`: half-step rounding, prio-2 exclusion, empty-sprint edges.
|
|
|
+ - Day-value validation: rejects 0.3, 1.7, negative.
|
|
|
+ - `AuditLogger`: no-op update → 0 rows; real update → 1 row; DELETE
|
|
|
+ carries `before_json` + null `after_json`.
|
|
|
+ - OIDC bootstrap: first user becomes admin; subsequent users do not.
|
|
|
+ - [ ] Walk through the manual acceptance checklist in spec §10.
|
|
|
+
|
|
|
+## 10. Known gaps / deferred items
|
|
|
+
|
|
|
+- **Phase 4 removeWorker cascade is un-audited.** When a sprint_worker is
|
|
|
+ removed, the FK on `sprint_worker_days` cascade-deletes rows that are
|
|
|
+ NOT reflected in `audit_log`. TaskController::delete does the correct
|
|
|
+ snapshot-then-delete; SprintController::removeWorker should be brought
|
|
|
+ to parity. Fix sketch: in a tx, pre-read all `sprint_worker_days` rows
|
|
|
+ for that sprint_worker, write DELETE audit for each, then call
|
|
|
+ `SprintWorkerRepository::remove()`.
|
|
|
+- **removeWorker also cascades `task_assignments`** (via FK on sprint_worker)
|
|
|
+ — same audit gap.
|
|
|
+- **PHPUnit test harness not set up** (no `phpunit.xml` yet, tests/ is
|
|
|
+ empty). Phase 7 work.
|
|
|
+- **No users management page**. Admin bootstrap on first login is the only
|
|
|
+ way to gain admin rights. If you demote yourself, there's no UI to fix
|
|
|
+ it — you'd need to hand-edit the DB (or re-log-in via local admin with
|
|
|
+ `forceAdmin=true`).
|
|
|
+- **Column-visibility toggle** on the task list (spec §6.4 mentions it)
|
|
|
+ was deferred; not implemented. Horizontal scroll handles wide worker
|
|
|
+ counts for now.
|
|
|
+- **Multi-select owner filter** (spec §6.4) is a single-select in the UI.
|
|
|
+- **Worker reorder on `/sprints/{id}` reloads the page** after drag so the
|
|
|
+ task list's worker columns stay in sync. Acceptable, but noisy if the
|
|
|
+ user has a lot of edits in flight (they're all saved by then).
|
|
|
+
|
|
|
+## 11. Running locally
|
|
|
+
|
|
|
+```bash
|
|
|
+cp .env.example .env
|
|
|
+# Fill Entra vars, OR set LOCAL_ADMIN_EMAIL + LOCAL_ADMIN_PASSWORD
|
|
|
+docker compose up --build
|
|
|
+# open http://localhost:8080
|
|
|
+```
|
|
|
+
|
|
|
+Rebuild when the Dockerfile / composer manifest changes:
|
|
|
+```bash
|
|
|
+docker compose build --no-cache && docker compose up
|
|
|
+```
|
|
|
+
|
|
|
+The SQLite file lives at `./data/app.sqlite` on the host; nuking it resets
|
|
|
+the app to a blank slate (migrations run on the next request).
|
|
|
+
|
|
|
+Syntax-check PHP without Docker:
|
|
|
+```bash
|
|
|
+for f in $(git ls-files '*.php'); do php -l "$f" | tail -1 | sed "s|^|$f: |"; done
|
|
|
+```
|
|
|
+
|
|
|
+Run the in-session smoke tests (used throughout to verify each phase):
|
|
|
+these live as ad-hoc `php -r '…'` scripts in the commit history and the
|
|
|
+handoff conversation. There's no formal test runner until Phase 7 lands
|
|
|
+PHPUnit.
|
|
|
+
|
|
|
+## 12. How to resume in a fresh Claude session
|
|
|
+
|
|
|
+Tell Claude:
|
|
|
+
|
|
|
+> Working on `/Users/achiappa/Development/claude_code_private/sprint_planer_web`.
|
|
|
+> Read `HANDOFF.md`, `CLAUDE.md` if present, the git log, and
|
|
|
+> `.planning/*` if present. Phases 1–6 are complete (including hotfixes).
|
|
|
+> Start **Phase 7 — Audit viewer + polish**: /audit page with filters and
|
|
|
+> pagination, security headers, strict-ish CSP allowing the two CDNs,
|
|
|
+> PHPUnit test harness with the four test groups listed in spec §10, and
|
|
|
+> walk through the manual acceptance checklist. Do it one deliverable at a
|
|
|
+> time, commit between each, keep the existing audit-logging conventions.
|
|
|
+
|
|
|
+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)
|
|
|
+
|
|
|
+```
|
|
|
+927b708 Fix: drop unreliable SRI hashes + guard sortable() calls
|
|
|
+ad78283 Phase 6: task list, assignments, client-side sort/filter/search
|
|
|
+515d7d0 Phase 5: Arbeitstage grid + capacity calculator + cell persistence
|
|
|
+38ba151 Phase 4: sprint settings — meta, weeks, workers, reorder, RTB
|
|
|
+f189e7d Phase 3: workers + sprints + generic audit wiring
|
|
|
+82ddc98 Route all URLs to the front controller via FallbackResource
|
|
|
+83493d0 Phase 2 hotfix: scalar-safe Request + local admin login
|
|
|
+be193d2 Phase 2: Entra OIDC auth + session + audit log
|
|
|
+58a6b30 Phase 1: skeleton
|
|
|
+```
|
|
|
+
|
|
|
+Each commit message captures the deliverables, rationale, and the smoke
|
|
|
+tests that were run to validate it. Read those in preference to any
|
|
|
+summary here.
|