- 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>
4.5 KiB
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):
- Overdue — overdue first, with a red marker.
- Due today.
- Due this week — the next seven days.
- No due date — only when the child has such tasks.
- 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.
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)
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, thenSCAN FAILEDwith the error, thenLast 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.