Phase 5: per-child dashboard (overdue first), item detail, delete

- internal/dashboard: classify items into overdue / due today / due this week /
  no due date / messages; render the plain-text brief (FR-6, FR-7, FR-9).
- internal/store: ItemsForChild, ItemByID, DeleteItemByID, ScanSummaryForChild
  and the stored-brief CRUD helpers (FR-10).
- internal/web: routes /?child=, /items/{id}, POST /items/{id}/delete,
  POST /summaries/build, POST /summaries/{id}/delete, plus a per-child scan
  banner (FR-12) and a phone-first server-rendered template.
- Calendar events are never "overdue": a past lesson is history, not work.
- cmd/school brief build|list; scripts/screenshot-phase5.mjs for phone evidence.
- Tests: bucket classification, stored-brief round-trip, and HTTP routes
  including the NO SCAN / SCAN FAILED / healthy banner behaviour.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
2026-10-15 17:50:29 +11:00
parent 207a4978a9
commit 37d82b1a0a
16 changed files with 2223 additions and 65 deletions

View File

@@ -0,0 +1,114 @@
# Phase 5 — the per-child dashboard
Status: **done.** Sam can see, per child and on a phone, what is overdue, what is
due today, what is due this week, and the messages. See AUD-109.
## What the dashboard shows
One tab per child (Harry, Finn). The selected tab shows, in order (FR-7):
1. **Overdue** — overdue first, with a red marker.
2. **Due today**.
3. **Due this week** — the next seven days.
4. **No due date** — only when the child has such tasks.
5. **Messages** — the Compass news feed, newest first.
A task is **overdue** when Compass says so (`Status: Overdue` from the JSON
service, `Submitted: Overdue` from the grid), or when it is not submitted and its
due date is before today. **Submitted** work (On Time, Received Late) is not
"due", so it stays out of the lists; the count is shown in the panel footer.
A **calendar event** is never overdue: a past lesson is history, not work to do.
Events appear under due today and due this week.
The page is server-rendered HTML with HTMX. It is phone-first: one column, 44px
touch targets, and the Delete button steps below the title under 520px.
## Routes
| Method | Path | Shows |
|---|---|---|
| `GET` | `/?child=<id>` | the per-child dashboard (first child when `child` is absent) |
| `GET` | `/items/{id}` | item detail: subject, teacher, due date, link, full text (§5 screen 3) |
| `POST` | `/items/{id}/delete` | the Delete action on an item (FR-10) |
| `POST` | `/summaries/build` | build a draft brief for one child (FR-10 summary half) |
| `POST` | `/summaries/{id}/delete` | the Delete action on a stored brief (FR-10) |
| `GET` | `/partials/scan-status` | the scan banner fragment (polled every 60s) |
Every `POST` checks the session CSRF token. An HTMX request gets `204 No
Content`, so the row is removed in place; a plain form post gets `303 See Other`
back to the dashboard, so the page works without JavaScript.
A deleted item returns on the next parse if the school still lists it. A deleted
brief is gone.
## Fail loudly (FR-12)
The banner reads the scan log. It is never an empty day:
- **NO SCAN HAS RUN YET** — amber. A child with no scan shows
"Unknown — the last scan did not complete." instead of "Nothing overdue.".
- **SCAN FAILED** — red, with the recorded error. The child panel repeats the
warning.
- **Last scan completed** — green. Only then may an empty section say
"Nothing overdue.".
The banner is per child (`ScanSummaryForChild`), so a failed scan for Finn is
never hidden by a successful scan for Harry.
## Where the items come from
No new scan code: Phase 3/4 store the items. Phase 5 adds the reads and the
classification (`internal/dashboard`).
- `internal/dashboard/dashboard.go` — classify items into buckets; render the
plain-text brief.
- `internal/store/dashboard.go` — `ItemsForChild`, `ItemByID`, `DeleteItemByID`,
`ScanSummaryForChild`, and the summary create/list/get/delete helpers.
- `internal/web/server.go` — the routes and handlers.
- `internal/web/templates/dashboard.html`, `item.html` — the screens.
## The stored brief (FR-10, delete half)
A brief is a row in `notifications` (AGENTS.md §7). `sent_at` stays NULL for a
draft. Building a brief stores the text; **sending** it is Phase 7.
```bash
docker exec compass-teams-nhs /app/school brief build --child Harry
docker exec compass-teams-nhs /app/school brief list
```
## Verification (2026-10-15, live)
```bash
make test # go test ./...
```
The tests cover the buckets, the brief text, the stored brief round-trip, and the
HTTP routes. `internal/web/server_test.go` logs in and proves:
- the selected child's items render and the other child's do not,
- the overdue item renders before the due-today item,
- the item detail carries subject, teacher and body,
- delete removes the row and a tokenless delete is refused (403),
- the banner shows `NO SCAN HAS RUN YET`, then `SCAN FAILED` with the error, then
`Last scan completed`, and only a healthy scan allows "Nothing overdue.".
Live counts on `.13` (Compass data of 2026-10-15):
| Child | Overdue | Due today | This week | No due date | Messages |
|---|---|---|---|---|---|
| Harry | 6 | 8 | 7 | 18 | 1 |
| Finn | 2 | 0 | 0 | 8 | 1 |
The overdue counts match the home-page reconciliation from Phase 4a (Harry 6,
Finn 2).
Phone-width screenshots (390 × 844) are attached to AUD-109:
`scripts/screenshot-phase5.mjs`.
## Known gaps
- Teams (FR-15) is a separate source; the dashboard shows Compass only.
- Sending a brief is Phase 7. Phase 5 stores and deletes a draft.
- The calendar write (Phase 6) and the Hub API (Phase 8) are not built yet.