|
@@ -3,6 +3,12 @@
|
|
|
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.
|
|
|
|
|
|
|
|
|
|
+> **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.
|
|
|
|
|
+
|
|
|
## 1. What this is
|
|
## 1. What this is
|
|
|
|
|
|
|
|
A web replacement for an Excel sprint-planning workbook used by a ~15-person
|
|
A web replacement for an Excel sprint-planning workbook used by a ~15-person
|
|
@@ -18,36 +24,45 @@ per-cell audit trail.
|
|
|
|
|
|
|
|
## 2. Tech stack (non-negotiable)
|
|
## 2. Tech stack (non-negotiable)
|
|
|
|
|
|
|
|
-- Runtime: Docker, single container, `php:8.3-apache` base.
|
|
|
|
|
|
|
+- Runtime: Docker, two-stage build, `node:20-alpine` for CSS + `php:8.3-apache` for runtime.
|
|
|
- Language: PHP 8.3, strict types, PSR-12.
|
|
- Language: PHP 8.3, strict types, PSR-12.
|
|
|
- Database: SQLite via PDO, file at `/var/www/data/app.sqlite` (mounted volume).
|
|
- Database: SQLite via PDO, file at `/var/www/data/app.sqlite` (mounted volume).
|
|
|
-- Front end: server-rendered PHP templates + Tailwind (Play CDN, slated for
|
|
|
|
|
- vendoring in Phase 11) + jQuery 3.x + jQuery UI 1.13 (CDN). No build step yet.
|
|
|
|
|
|
|
+- Front end: server-rendered PHP templates + Tailwind CSS (vendored, compiled
|
|
|
|
|
+ by the Node stage) + jQuery 3.x + jQuery UI 1.13 (from code.jquery.com CDN).
|
|
|
- Auth: Microsoft Entra ID via OpenID Connect (Authorization Code + PKCE),
|
|
- Auth: Microsoft Entra ID via OpenID Connect (Authorization Code + PKCE),
|
|
|
plus an optional env-configured "local admin" fallback for dev / on-prem.
|
|
plus an optional env-configured "local admin" fallback for dev / on-prem.
|
|
|
- Composer deps: `jumbojett/openid-connect-php`, `vlucas/phpdotenv`,
|
|
- Composer deps: `jumbojett/openid-connect-php`, `vlucas/phpdotenv`,
|
|
|
`phpunit/phpunit` (dev).
|
|
`phpunit/phpunit` (dev).
|
|
|
|
|
+- npm deps: `tailwindcss` (build-time only).
|
|
|
|
|
|
|
|
## 3. Directory layout
|
|
## 3. Directory layout
|
|
|
|
|
|
|
|
```
|
|
```
|
|
|
-├── Dockerfile
|
|
|
|
|
|
|
+├── Dockerfile # multi-stage: css-builder + php:8.3-apache
|
|
|
├── docker-compose.yml
|
|
├── docker-compose.yml
|
|
|
|
|
+├── .dockerignore
|
|
|
├── .env.example
|
|
├── .env.example
|
|
|
├── composer.json / composer.lock
|
|
├── composer.json / composer.lock
|
|
|
|
|
+├── package.json / package-lock.json
|
|
|
|
|
+├── tailwind.config.js
|
|
|
├── phpunit.xml
|
|
├── phpunit.xml
|
|
|
-├── ACCEPTANCE.md # §10 manual checklist walkthrough
|
|
|
|
|
|
|
+├── ACCEPTANCE.md # spec §10 manual checklist walkthrough
|
|
|
|
|
+├── HANDOFF.md # this file
|
|
|
|
|
+├── assets/
|
|
|
|
|
+│ └── css/input.css # Tailwind entry, compiled into public/assets/css/app.css
|
|
|
├── public/
|
|
├── public/
|
|
|
-│ ├── index.php # front controller + router wiring + security headers
|
|
|
|
|
-│ ├── .htaccess # belt-and-suspenders rewrite
|
|
|
|
|
|
|
+│ ├── index.php # front controller + router wiring + security headers
|
|
|
|
|
+│ ├── .htaccess # belt-and-suspenders rewrite
|
|
|
│ └── assets/
|
|
│ └── assets/
|
|
|
|
|
+│ ├── css/app.css # GENERATED at image-build time (gitignored)
|
|
|
│ └── js/
|
|
│ └── js/
|
|
|
-│ ├── sprint-planner.js # /sprints/{id} Arbeitstage + task list
|
|
|
|
|
-│ └── sprint-settings.js # /sprints/{id}/settings
|
|
|
|
|
|
|
+│ ├── app.js # site-wide; data-href click handler
|
|
|
|
|
+│ ├── sprint-planner.js # /sprints/{id} Arbeitstage + task list
|
|
|
|
|
+│ └── sprint-settings.js # /sprints/{id}/settings
|
|
|
├── src/
|
|
├── src/
|
|
|
│ ├── Auth/ LocalAdmin, OidcClient, SessionGuard
|
|
│ ├── Auth/ LocalAdmin, OidcClient, SessionGuard
|
|
|
│ ├── Controllers/ AuthController, WorkerController, SprintController,
|
|
│ ├── Controllers/ AuthController, WorkerController, SprintController,
|
|
|
-│ │ TaskController, AuditController
|
|
|
|
|
|
|
+│ │ TaskController, AuditController, UserController
|
|
|
│ ├── Db/ Connection, Migrator
|
|
│ ├── Db/ Connection, Migrator
|
|
|
│ ├── Domain/ User, Worker, Sprint, SprintWeek, SprintWorker,
|
|
│ ├── Domain/ User, Worker, Sprint, SprintWeek, SprintWorker,
|
|
|
│ │ SprintWorkerDay, Task, TaskAssignment
|
|
│ │ SprintWorkerDay, Task, TaskAssignment
|
|
@@ -59,10 +74,10 @@ per-cell audit trail.
|
|
|
│ └── Services/ AuditLogger, CapacityCalculator
|
|
│ └── Services/ AuditLogger, CapacityCalculator
|
|
|
├── migrations/ 001_init.sql (full schema per spec §3)
|
|
├── migrations/ 001_init.sql (full schema per spec §3)
|
|
|
├── views/ layout.php, home.php, auth/local.php,
|
|
├── views/ layout.php, home.php, auth/local.php,
|
|
|
-│ workers/index.php,
|
|
|
|
|
|
|
+│ workers/index.php, users/index.php,
|
|
|
│ sprints/{new,show,settings}.php,
|
|
│ sprints/{new,show,settings}.php,
|
|
|
│ audit/index.php
|
|
│ audit/index.php
|
|
|
-├── tests/ TestCase.php + Services/ + Repositories/
|
|
|
|
|
|
|
+├── tests/ TestCase + Services/ + Repositories/ + Controllers/ + Cascade/
|
|
|
└── data/ SQLite + sessions directory (volume-mounted, gitignored)
|
|
└── data/ SQLite + sessions directory (volume-mounted, gitignored)
|
|
|
```
|
|
```
|
|
|
|
|
|
|
@@ -81,7 +96,8 @@ Value constraints enforced in PHP (not SQL):
|
|
|
- `task_assignments.days` ≥ 0, no hard upper bound.
|
|
- `task_assignments.days` ≥ 0, no hard upper bound.
|
|
|
- `reserve_fraction`, `rtb` ∈ [0, 1].
|
|
- `reserve_fraction`, `rtb` ∈ [0, 1].
|
|
|
|
|
|
|
|
-FK cascades (important for audit-integrity work in Phase 8):
|
|
|
|
|
|
|
+FK cascades (every cascade path now snapshot-audits before the parent delete
|
|
|
|
|
+runs — Phase 8):
|
|
|
- `sprint_weeks.sprint_id → sprints(id)` ON DELETE CASCADE
|
|
- `sprint_weeks.sprint_id → sprints(id)` ON DELETE CASCADE
|
|
|
- `sprint_workers.sprint_id → sprints(id)` ON DELETE CASCADE
|
|
- `sprint_workers.sprint_id → sprints(id)` ON DELETE CASCADE
|
|
|
- `sprint_workers.worker_id → workers(id)` ON DELETE RESTRICT
|
|
- `sprint_workers.worker_id → workers(id)` ON DELETE RESTRICT
|
|
@@ -123,6 +139,8 @@ Pages (HTML):
|
|
|
| GET | `/workers` | admin |
|
|
| GET | `/workers` | admin |
|
|
|
| POST | `/workers` | admin |
|
|
| POST | `/workers` | admin |
|
|
|
| POST | `/workers/{id}` | admin |
|
|
| POST | `/workers/{id}` | admin |
|
|
|
|
|
+| GET | `/users` | admin |
|
|
|
|
|
+| POST | `/users/{id}` | admin |
|
|
|
| GET | `/sprints/new` | admin |
|
|
| GET | `/sprints/new` | admin |
|
|
|
| POST | `/sprints` | admin |
|
|
| POST | `/sprints` | admin |
|
|
|
| GET | `/sprints/{id}` | signed-in |
|
|
| GET | `/sprints/{id}` | signed-in |
|
|
@@ -135,7 +153,7 @@ JSON (admin-only, CSRF via `X-CSRF-Token` header; envelope per spec §7):
|
|
|
| PATCH | `/sprints/{id}` | name/dates/reserve |
|
|
| PATCH | `/sprints/{id}` | name/dates/reserve |
|
|
|
| POST | `/sprints/{id}/weeks` | resize week set |
|
|
| POST | `/sprints/{id}/weeks` | resize week set |
|
|
|
| POST | `/sprints/{id}/workers` | add sprint worker |
|
|
| POST | `/sprints/{id}/workers` | add sprint worker |
|
|
|
-| DELETE | `/sprints/{id}/workers/{sw_id}` | remove sprint worker |
|
|
|
|
|
|
|
+| DELETE | `/sprints/{id}/workers/{sw_id}` | remove sprint worker (audits cascaded children) |
|
|
|
| POST | `/sprints/{id}/workers/reorder` | reorder sprint workers |
|
|
| POST | `/sprints/{id}/workers/reorder` | reorder sprint workers |
|
|
|
| PATCH | `/sprints/{id}/workers/{sw_id}` | set rtb |
|
|
| PATCH | `/sprints/{id}/workers/{sw_id}` | set rtb |
|
|
|
| PATCH | `/sprints/{id}/week-cells` | batch day cells |
|
|
| PATCH | `/sprints/{id}/week-cells` | batch day cells |
|
|
@@ -161,7 +179,10 @@ is called inside the same transaction as the DB change. Controllers prefer
|
|
|
- Bulk operations (batch cell save) → one row per changed cell.
|
|
- Bulk operations (batch cell save) → one row per changed cell.
|
|
|
- A no-op UPDATE (canonical-JSON-equal before/after) → no row.
|
|
- A no-op UPDATE (canonical-JSON-equal before/after) → no row.
|
|
|
- FK-cascading deletes must be audited by the controller BEFORE calling the
|
|
- FK-cascading deletes must be audited by the controller BEFORE calling the
|
|
|
- cascading delete. See `TaskController::delete()` as the reference.
|
|
|
|
|
|
|
+ cascading delete. References:
|
|
|
|
|
+ - `TaskController::delete()` — task → task_assignments
|
|
|
|
|
+ - `SprintController::removeWorker()` — sprint_worker → sprint_worker_days + task_assignments
|
|
|
|
|
+ - `SprintController::replaceWeeks()` — sprint_week → sprint_worker_days (on shrink)
|
|
|
- Non-mutation events (LOGIN, LOGOUT, LOGIN_FAILED, BOOTSTRAP_ADMIN) → always
|
|
- Non-mutation events (LOGIN, LOGOUT, LOGIN_FAILED, BOOTSTRAP_ADMIN) → always
|
|
|
one row.
|
|
one row.
|
|
|
|
|
|
|
@@ -208,23 +229,32 @@ with a `BOOTSTRAP_ADMIN` audit row.
|
|
|
- [x] **Phase 7 — Audit viewer + polish** (`21d0c4a`): `/audit` admin page
|
|
- [x] **Phase 7 — Audit viewer + polish** (`21d0c4a`): `/audit` admin page
|
|
|
with filters + pagination + collapsible diffs, security headers +
|
|
with filters + pagination + collapsible diffs, security headers +
|
|
|
strict-ish CSP, CSRF audit (18/18 mutations), PHPUnit harness with
|
|
strict-ish CSP, CSRF audit (18/18 mutations), PHPUnit harness with
|
|
|
- 59 tests covering CapacityCalculator, day-value validation,
|
|
|
|
|
- AuditLogger, and OIDC bootstrap. [ACCEPTANCE.md](ACCEPTANCE.md)
|
|
|
|
|
- captures the spec §10 manual walkthrough.
|
|
|
|
|
|
|
+ 59 tests. [ACCEPTANCE.md](ACCEPTANCE.md) captures the spec §10
|
|
|
|
|
+ manual walkthrough.
|
|
|
|
|
+- [x] **Phase 8 — Cascade audit integrity** (`dd158f3`): three FK cascade
|
|
|
|
|
+ paths (sprint_worker → sprint_worker_days, sprint_worker →
|
|
|
|
|
+ task_assignments, sprint_week → sprint_worker_days on shrink) now
|
|
|
|
|
+ snapshot-audit before the parent delete fires. +4 tests, +2 repo
|
|
|
|
|
+ lookup methods.
|
|
|
|
|
+- [x] **Phase 9 — Users management** (`f7f5db5`): `GET /users` + `POST
|
|
|
|
|
+ /users/{id}` with self-demote and last-admin guardrails. Pure static
|
|
|
|
|
+ `UserController::demoteGuardrail` is testable with no PDO/session
|
|
|
|
|
+ setup. +6 tests.
|
|
|
|
|
+- [x] **Phase 10 — Task list polish** (`c35a934`): multi-select owner
|
|
|
|
|
+ filter (checkbox dropdown) and column-visibility toggle, both
|
|
|
|
|
+ pure client-side with localStorage persistence per sprint.
|
|
|
|
|
+- [x] **Phase 11 — CSP hardening** (`ab9430b`): vendored Tailwind via
|
|
|
|
|
+ a Node css-builder Docker stage; inline onclick replaced by
|
|
|
|
|
+ `data-href` + `app.js`; CSP dropped `'unsafe-inline'` and the
|
|
|
|
|
+ Tailwind CDN host. Strict CSP now in effect.
|
|
|
|
|
|
|
|
### Upcoming
|
|
### Upcoming
|
|
|
|
|
|
|
|
-Planned in §14. Short version:
|
|
|
|
|
-
|
|
|
|
|
-- [ ] **Phase 8 — Cascade audit integrity**
|
|
|
|
|
-- [ ] **Phase 9 — Users management**
|
|
|
|
|
-- [ ] **Phase 10 — Task list polish** (column visibility + multi-select owner filter)
|
|
|
|
|
-- [ ] **Phase 11 — CSP hardening** (vendor Tailwind + drop inline onclick)
|
|
|
|
|
|
|
+Nothing scheduled. The next time real work lands, follow the maintenance
|
|
|
|
|
+rule (§14) and add the entry here.
|
|
|
|
|
|
|
|
## 10. Residual known gaps / deferred items
|
|
## 10. Residual known gaps / deferred items
|
|
|
|
|
|
|
|
-Items moved into Phases 8–11 are NOT listed here — see §14.
|
|
|
|
|
-
|
|
|
|
|
- **Worker reorder on `/sprints/{id}` reloads the page** after drag so the
|
|
- **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
|
|
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). Not
|
|
user has a lot of edits in flight (they're all saved by then). Not
|
|
@@ -237,7 +267,7 @@ Items moved into Phases 8–11 are NOT listed here — see §14.
|
|
|
release.
|
|
release.
|
|
|
- **Manual acceptance walkthrough** ([ACCEPTANCE.md](ACCEPTANCE.md))
|
|
- **Manual acceptance walkthrough** ([ACCEPTANCE.md](ACCEPTANCE.md))
|
|
|
hasn't been executed end-to-end by a human yet — it's a documentary
|
|
hasn't been executed end-to-end by a human yet — it's a documentary
|
|
|
- follow-up once Phase 8 lands.
|
|
|
|
|
|
|
+ follow-up that should happen in the running container.
|
|
|
|
|
|
|
|
## 11. Running locally
|
|
## 11. Running locally
|
|
|
|
|
|
|
@@ -248,11 +278,17 @@ docker compose up --build
|
|
|
# open http://localhost:8080
|
|
# open http://localhost:8080
|
|
|
```
|
|
```
|
|
|
|
|
|
|
|
-Rebuild when the Dockerfile / composer manifest changes:
|
|
|
|
|
|
|
+Rebuild when the Dockerfile / composer manifest / Tailwind sources change:
|
|
|
```bash
|
|
```bash
|
|
|
docker compose build --no-cache && docker compose up
|
|
docker compose build --no-cache && docker compose up
|
|
|
```
|
|
```
|
|
|
|
|
|
|
|
|
|
+For local CSS dev without Docker:
|
|
|
|
|
+```bash
|
|
|
|
|
+npm install
|
|
|
|
|
+npm run watch:css # rebuilds public/assets/css/app.css on change
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
The SQLite file lives at `./data/app.sqlite` on the host; nuking it resets
|
|
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).
|
|
the app to a blank slate (migrations run on the next request).
|
|
|
|
|
|
|
@@ -264,7 +300,7 @@ for f in $(git ls-files '*.php'); do php -l "$f" | tail -1 | sed "s|^|$f: |"; do
|
|
|
Run the test suite:
|
|
Run the test suite:
|
|
|
```bash
|
|
```bash
|
|
|
vendor/bin/phpunit
|
|
vendor/bin/phpunit
|
|
|
-# → OK (59 tests, 90 assertions)
|
|
|
|
|
|
|
+# → OK (74 tests, 138 assertions)
|
|
|
```
|
|
```
|
|
|
|
|
|
|
|
## 12. How to resume in a fresh Claude session
|
|
## 12. How to resume in a fresh Claude session
|
|
@@ -272,11 +308,12 @@ 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–7 are
|
|
|
|
|
-> shipped (see §9). Start **Phase 8 — Cascade audit integrity**. Follow
|
|
|
|
|
-> §14 for the full plan of Phases 8–11. Do one phase at a time, commit
|
|
|
|
|
-> after each, and update `HANDOFF.md` §9 to move each phase from
|
|
|
|
|
-> "Upcoming" to "Shipped" with its commit SHA.
|
|
|
|
|
|
|
+> Read `HANDOFF.md`, the git log, and `ACCEPTANCE.md`. All 11 phases are
|
|
|
|
|
+> shipped (see §9). There is no scheduled coding work. The only
|
|
|
|
|
+> outstanding items are in §10 (mostly a manual acceptance walkthrough
|
|
|
|
|
+> in the running container). If I ask for new work, follow the
|
|
|
|
|
+> maintenance rule in §14 — commit code, then commit a HANDOFF.md
|
|
|
|
|
+> update separately.
|
|
|
|
|
|
|
|
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.
|
|
@@ -284,6 +321,11 @@ before acting — nothing here is load-bearing once it grows stale.
|
|
|
## 13. Git history (as of this handoff)
|
|
## 13. Git history (as of this handoff)
|
|
|
|
|
|
|
|
```
|
|
```
|
|
|
|
|
+ab9430b Phase 11: vendor Tailwind + drop inline onclick + tighten CSP
|
|
|
|
|
+c35a934 Phase 10: multi-select owner filter + column visibility toggle
|
|
|
|
|
+f7f5db5 Phase 9: users management page (promote / demote admin)
|
|
|
|
|
+dd158f3 Phase 8: audit rows for FK-cascaded deletes
|
|
|
|
|
+8537fc2 Plan Phases 8–11 in HANDOFF.md
|
|
|
afa9e4f Ignore PHPUnit cache directory
|
|
afa9e4f Ignore PHPUnit cache directory
|
|
|
21d0c4a Phase 7: audit viewer + security headers + PHPUnit
|
|
21d0c4a Phase 7: audit viewer + security headers + PHPUnit
|
|
|
09b67f3 Add HANDOFF.md for resuming work in a fresh session
|
|
09b67f3 Add HANDOFF.md for resuming work in a fresh session
|
|
@@ -304,226 +346,33 @@ summary here.
|
|
|
|
|
|
|
|
---
|
|
---
|
|
|
|
|
|
|
|
-## 14. Upcoming phases (plan)
|
|
|
|
|
-
|
|
|
|
|
-Four phases close out every remaining item from the original §10 "Known
|
|
|
|
|
-gaps". Ordered by value and independent enough to ship one at a time —
|
|
|
|
|
-**do not batch them into one commit**.
|
|
|
|
|
-
|
|
|
|
|
-### Phase 8 — Cascade audit integrity
|
|
|
|
|
-
|
|
|
|
|
-**Why.** `task_assignments` deletes correctly snapshot-audit their
|
|
|
|
|
-children before the FK cascade (see `TaskController::delete`). Three
|
|
|
|
|
-other cascade paths don't:
|
|
|
|
|
-
|
|
|
|
|
-| Parent delete | Cascaded rows | Where gap lives |
|
|
|
|
|
-|-------------------------------------|----------------------------|-------------------------------|
|
|
|
|
|
-| `sprint_workers` row removed | `sprint_worker_days` | `SprintController::removeWorker` |
|
|
|
|
|
-| `sprint_workers` row removed | `task_assignments` | `SprintController::removeWorker` |
|
|
|
|
|
-| `sprint_weeks` row removed (shrink) | `sprint_worker_days` | `SprintController::replaceWeeks` / `SprintWeekRepository::syncCount` |
|
|
|
|
|
-
|
|
|
|
|
-Spec §5 is explicit: every DELETE on a domain table produces exactly one
|
|
|
|
|
-audit row. Cascades violate that.
|
|
|
|
|
-
|
|
|
|
|
-**Deliverables.**
|
|
|
|
|
-- `SprintWorkerDayRepository::allForSprintWorker(int $swId): list<SprintWorkerDay>`
|
|
|
|
|
-- `SprintWorkerDayRepository::allForSprintWeek(int $weekId): list<SprintWorkerDay>`
|
|
|
|
|
-- `TaskAssignmentRepository::allForSprintWorker(int $swId): list<TaskAssignment>`
|
|
|
|
|
-- `SprintController::removeWorker` — inside the tx, fetch both child sets
|
|
|
|
|
- for the target `sprint_worker`, emit DELETE audit rows for each, THEN
|
|
|
|
|
- call `$sprintWorkers->remove($swId)` so the FK cascade runs cleanly.
|
|
|
|
|
-- `SprintController::replaceWeeks` — for each week in
|
|
|
|
|
- `$weeks->syncCount(...)->removed`, fetch `sprint_worker_days` for that
|
|
|
|
|
- week and emit DELETE audit rows BEFORE the inner DELETE (move the
|
|
|
|
|
- fetch into the repo or do it in the controller before calling
|
|
|
|
|
- `syncCount`). Simpler shape: change `syncCount` to return the
|
|
|
|
|
- orphaned `sprint_worker_days` alongside `removed` weeks.
|
|
|
|
|
-- PHPUnit tests covering each of the three paths: set up a tiny sprint
|
|
|
|
|
- with one worker + one week + one day cell + one task + one assignment,
|
|
|
|
|
- trigger the delete, assert one audit row per cascaded child.
|
|
|
|
|
-
|
|
|
|
|
-**Commit message stub.**
|
|
|
|
|
-> `Phase 8: audit rows for FK-cascaded deletes`
|
|
|
|
|
-> - Three cascade paths now emit DELETE audit rows before the parent
|
|
|
|
|
-> delete runs, matching the TaskController::delete pattern from Phase 6.
|
|
|
|
|
-> - Repos gain by-parent lookup helpers.
|
|
|
|
|
-> - Regression tests.
|
|
|
|
|
-
|
|
|
|
|
-**Estimated size.** ~150 LOC + 3 test classes.
|
|
|
|
|
-
|
|
|
|
|
----
|
|
|
|
|
-
|
|
|
|
|
-### Phase 9 — Users management
|
|
|
|
|
-
|
|
|
|
|
-**Why.** The first login promotes a single user to admin. If that admin
|
|
|
|
|
-demotes themselves or wants to grant admin to someone else, there is no
|
|
|
|
|
-UI — they have to hand-edit SQLite or re-log in via local admin.
|
|
|
|
|
-
|
|
|
|
|
-**Design.**
|
|
|
|
|
-- New page `GET /users` (admin-only): table of every user with columns
|
|
|
|
|
- email, display name, is_admin (checkbox), last_login_at.
|
|
|
|
|
-- New endpoint `POST /users/{id}` (admin-only form, CSRF) that toggles
|
|
|
|
|
- `is_admin`. Form-based not JSON, to match `/workers/{id}`.
|
|
|
|
|
-- Guardrails (both enforced server-side with 422 on violation):
|
|
|
|
|
- - You cannot demote yourself.
|
|
|
|
|
- - You cannot demote the last remaining admin.
|
|
|
|
|
-- Every toggle writes an UPDATE audit row on `user`.
|
|
|
|
|
-
|
|
|
|
|
-**Deliverables.**
|
|
|
|
|
-- `UserRepository`:
|
|
|
|
|
- - `all(): list<User>` — sorted by email.
|
|
|
|
|
- - `countAdmins(): int`.
|
|
|
|
|
- - `setAdmin(int $id, bool $isAdmin): array{before: User, after: User}`.
|
|
|
|
|
-- `UserController` (new):
|
|
|
|
|
- - `index(Request): Response` — renders `views/users/index.php`.
|
|
|
|
|
- - `updateAdmin(Request, array $params): Response` — form POST handler.
|
|
|
|
|
-- `views/users/index.php` — table + inline toggle forms + flash banner.
|
|
|
|
|
-- Route wiring + admin nav link ("Users" next to Workers / New sprint /
|
|
|
|
|
- Audit log).
|
|
|
|
|
-- PHPUnit:
|
|
|
|
|
- - Cannot demote self (return 422, no DB change, no audit).
|
|
|
|
|
- - Cannot demote last admin (same).
|
|
|
|
|
- - Admin→non-admin and back: UPDATE audit written with proper
|
|
|
|
|
- before/after.
|
|
|
|
|
-
|
|
|
|
|
-**Commit message stub.**
|
|
|
|
|
-> `Phase 9: users management page (promote / demote admin)`
|
|
|
|
|
-
|
|
|
|
|
-**Estimated size.** ~200 LOC + tests + 1 view.
|
|
|
|
|
-
|
|
|
|
|
----
|
|
|
|
|
-
|
|
|
|
|
-### Phase 10 — Task list polish
|
|
|
|
|
-
|
|
|
|
|
-**Why.** Spec §6.4 calls out a column-visibility toggle and a
|
|
|
|
|
-multi-select owner filter; Phase 6 shipped the task list with a
|
|
|
|
|
-single-select filter and no visibility toggle.
|
|
|
|
|
-
|
|
|
|
|
-**Deliverables — purely client-side.**
|
|
|
|
|
-
|
|
|
|
|
-1. **Multi-select owner filter** in the task toolbar.
|
|
|
|
|
- - Replace the `<select data-owner-filter>` with a "trigger button →
|
|
|
|
|
- checkbox dropdown" pattern. Options: `(No owner)` + every worker.
|
|
|
|
|
- - State is a `Set<string>` of selected values. Empty set == show all.
|
|
|
|
|
- - Click outside closes the dropdown.
|
|
|
|
|
- - Filter logic: row visible iff the set is empty OR the row's owner
|
|
|
|
|
- id is in the set.
|
|
|
|
|
- - Persist in `localStorage` keyed by `sprint-{id}-owner-filter`.
|
|
|
|
|
-
|
|
|
|
|
-2. **Column-visibility toggle** in the task toolbar.
|
|
|
|
|
- - "Columns" button opens a checkbox list: Owner, Prio, Tot, and one
|
|
|
|
|
- entry per sprint worker column. Task column is always visible.
|
|
|
|
|
- - Hidden columns get a `hidden` class on BOTH the `<th>` and every
|
|
|
|
|
- corresponding `<td>`.
|
|
|
|
|
- - Persist in `localStorage` keyed by `sprint-{id}-column-visibility`.
|
|
|
|
|
- - Restoring a hidden state on page load runs BEFORE first sort /
|
|
|
|
|
- filter so layout doesn't flash.
|
|
|
|
|
-
|
|
|
|
|
-3. Wire `recomputeAllCapacity()` to still work when some columns are
|
|
|
|
|
- hidden (it reads from DOM, so values stay correct — but verify).
|
|
|
|
|
-
|
|
|
|
|
-4. Light "sort/filter don't touch hidden rows differently" check: sort
|
|
|
|
|
- sees hidden rows the same as visible rows (correct). Filter and
|
|
|
|
|
- visibility are independent dimensions.
|
|
|
|
|
-
|
|
|
|
|
-**Deliverables — no backend changes.** No repo, no controller, no route,
|
|
|
|
|
-no audit. All in `sprint-planner.js` + `views/sprints/show.php`
|
|
|
|
|
-(replace the `<select>`, add a `"Columns"` button, add the dropdown
|
|
|
|
|
-containers).
|
|
|
|
|
-
|
|
|
|
|
-**Commit message stub.**
|
|
|
|
|
-> `Phase 10: multi-select owner filter + column visibility toggle`
|
|
|
|
|
-
|
|
|
|
|
-**Estimated size.** ~250 LOC of JS + small view changes. No tests
|
|
|
|
|
-required (behaviour is pure DOM manipulation); manual acceptance
|
|
|
|
|
-covers it.
|
|
|
|
|
-
|
|
|
|
|
----
|
|
|
|
|
-
|
|
|
|
|
-### Phase 11 — CSP hardening (vendor Tailwind + drop inline onclick)
|
|
|
|
|
-
|
|
|
|
|
-**Why.** Current CSP:
|
|
|
|
|
-```
|
|
|
|
|
-script-src 'self' https://cdn.tailwindcss.com https://code.jquery.com 'unsafe-inline'
|
|
|
|
|
-style-src 'self' https://code.jquery.com 'unsafe-inline'
|
|
|
|
|
-```
|
|
|
|
|
-
|
|
|
|
|
-- `cdn.tailwindcss.com` is a dev-only CDN that runs a JIT compiler in
|
|
|
|
|
- the browser — not acceptable for production.
|
|
|
|
|
-- `'unsafe-inline'` in script-src is there for the one inline
|
|
|
|
|
- `onclick="location.href=…"` in `views/home.php`.
|
|
|
|
|
-- `'unsafe-inline'` in style-src is there because Tailwind's runtime JIT
|
|
|
|
|
- injects `<style>` blocks.
|
|
|
|
|
-
|
|
|
|
|
-Once Tailwind is pre-compiled and the inline onclick is gone, we can
|
|
|
|
|
-drop both `'unsafe-inline'` directives.
|
|
|
|
|
-
|
|
|
|
|
-**Deliverables.**
|
|
|
|
|
-
|
|
|
|
|
-1. **Vendor Tailwind via a multi-stage Docker build.**
|
|
|
|
|
- - `package.json` with `tailwindcss@^3` (v3 — simpler than v4).
|
|
|
|
|
- - `tailwind.config.js` scanning `views/**/*.php`, `src/**/*.php`,
|
|
|
|
|
- `public/assets/js/**/*.js`.
|
|
|
|
|
- - `assets/css/input.css` (NOT under `public/`) with `@tailwind base;
|
|
|
|
|
- components; utilities;`.
|
|
|
|
|
- - `Dockerfile` gains a build stage:
|
|
|
|
|
- ```
|
|
|
|
|
- FROM node:20-alpine AS css-builder
|
|
|
|
|
- WORKDIR /build
|
|
|
|
|
- COPY package.json package-lock.json* ./
|
|
|
|
|
- RUN npm ci
|
|
|
|
|
- COPY tailwind.config.js ./
|
|
|
|
|
- COPY assets/css/input.css ./assets/css/input.css
|
|
|
|
|
- COPY views/ ./views/
|
|
|
|
|
- COPY src/ ./src/
|
|
|
|
|
- COPY public/assets/js/ ./public/assets/js/
|
|
|
|
|
- RUN npx tailwindcss -i ./assets/css/input.css -o /build/app.css --minify
|
|
|
|
|
- ```
|
|
|
|
|
- Final stage `COPY --from=css-builder /build/app.css
|
|
|
|
|
- /var/www/html/public/assets/css/app.css`.
|
|
|
|
|
- - `.gitignore` gains `/node_modules/` (already there).
|
|
|
|
|
- - `views/layout.php`: replace `<script src="https://cdn.tailwindcss.com">`
|
|
|
|
|
- with `<link rel="stylesheet" href="/assets/css/app.css">`.
|
|
|
|
|
-
|
|
|
|
|
-2. **Move the inline `onclick` out of `home.php`.**
|
|
|
|
|
- - Change each sprint-list row to `data-href="/sprints/{id}"` and
|
|
|
|
|
- drop the `onclick` attribute.
|
|
|
|
|
- - New tiny `public/assets/js/app.js` (loaded in `layout.php`) wires a
|
|
|
|
|
- delegated `click` handler on any `[data-href]` and navigates.
|
|
|
|
|
-
|
|
|
|
|
-3. **Tighten CSP in `public/index.php`.**
|
|
|
|
|
- ```
|
|
|
|
|
- default-src 'self';
|
|
|
|
|
- script-src 'self' https://code.jquery.com;
|
|
|
|
|
- style-src 'self' https://code.jquery.com;
|
|
|
|
|
- img-src 'self' data:;
|
|
|
|
|
- font-src 'self' data: https://code.jquery.com;
|
|
|
|
|
- connect-src 'self';
|
|
|
|
|
- frame-ancestors 'none';
|
|
|
|
|
- base-uri 'self';
|
|
|
|
|
- form-action 'self' https://login.microsoftonline.com;
|
|
|
|
|
- ```
|
|
|
|
|
- No more `'unsafe-inline'`, no more `cdn.tailwindcss.com`.
|
|
|
|
|
-
|
|
|
|
|
-4. **Verification checklist (manual).**
|
|
|
|
|
- - Pages render correctly after the rebuild (`docker compose build
|
|
|
|
|
- --no-cache && docker compose up`).
|
|
|
|
|
- - Browser DevTools → Console: zero CSP violations on every page
|
|
|
|
|
- (home, sprint, settings, workers, audit, local sign-in form).
|
|
|
|
|
- - `curl -I http://localhost:8080/` shows the tightened CSP.
|
|
|
|
|
- - Sprint-row click on the home page still navigates.
|
|
|
|
|
-
|
|
|
|
|
-**Commit message stub.**
|
|
|
|
|
-> `Phase 11: vendor Tailwind + drop inline onclick + tighten CSP`
|
|
|
|
|
-> - Multi-stage Docker build produces public/assets/css/app.css.
|
|
|
|
|
-> - home.php uses data-href instead of inline onclick; app.js handles nav.
|
|
|
|
|
-> - CSP drops 'unsafe-inline' and the Tailwind CDN.
|
|
|
|
|
-
|
|
|
|
|
-**Estimated size.** Infrastructure changes (Dockerfile, package.json,
|
|
|
|
|
-tailwind.config.js, input.css, app.js, layout.php, index.php header
|
|
|
|
|
-builder). ~120 LOC net + the dep manifest. Manual verification
|
|
|
|
|
-dominates the cost.
|
|
|
|
|
-
|
|
|
|
|
-**Rollback plan.** If Tailwind build breaks production, reverting this
|
|
|
|
|
-commit is a single `git revert` and the app returns to the Play-CDN
|
|
|
|
|
-state. No schema or data touched.
|
|
|
|
|
|
|
+## 14. Maintenance contract
|
|
|
|
|
+
|
|
|
|
|
+The previous §14 was the plan for Phases 8–11. All four shipped, so the
|
|
|
|
|
+plan moved into §9. This section now codifies the rule that produced
|
|
|
|
|
+this file in the first place — **don't lose it on a context reset.**
|
|
|
|
|
+
|
|
|
|
|
+After every commit that completes a phase or substantive change:
|
|
|
|
|
+
|
|
|
|
|
+1. **Commit the code first.** A commit message that captures *what*
|
|
|
|
|
+ changed, *why*, and *how it was verified* is the canonical record.
|
|
|
|
|
+2. **Update §9.** If the work matched a planned phase, move it from
|
|
|
|
|
+ Upcoming → Shipped with the SHA. If it didn't match a planned phase
|
|
|
|
|
+ (hotfix, infra fix, etc.), insert it inline with the SHA.
|
|
|
|
|
+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
|
|
|
|
|
+ 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
|
|
|
|
|
+ phase" or a deferred-work item gets resolved), update that too.
|
|
|
|
|
+
|
|
|
|
|
+Why: a fresh Claude session starts by reading this file. Stale status
|
|
|
|
|
+here is the single biggest source of duplicated or wrong work. Keeping
|
|
|
|
|
+the file truthful costs ~2 minutes per phase; recovering from a stale
|
|
|
|
|
+file costs more.
|
|
|
|
|
+
|
|
|
|
|
+If you skip these steps, the next session will eventually notice and
|
|
|
|
|
+have to rebuild the picture from `git log` and the codebase. That's
|
|
|
|
|
+recoverable, but a regression from why this file exists.
|