Forráskód Böngészése

Add HANDOFF.md for resuming work in a fresh session

Single markdown file capturing: tech stack, schema, routes, capacity
math, audit rules, env vars, per-phase completion status with commit
SHAs, known gaps (Phase 4 removeWorker cascade not audited; no users
page; column visibility deferred; multi-select owner filter deferred),
run/rebuild instructions, the verbatim prompt to resume with a clean
session, and the git log.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
achiappa 2 hete
szülő
commit
09b67f379c
1 módosított fájl, 294 hozzáadás és 0 törlés
  1. 294 0
      HANDOFF.md

+ 294 - 0
HANDOFF.md

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