Phase 6b/7b: per-child decoded periods, separate brief blocks, idempotent calendar (AUD-119)

- Parse each child's own "Today" day widget for the timetable (FR-16); drop the
  school-wide week calendar's lesson grid and clean the leaked routine rows.
- Add the editable codes map (FR-17): subjects, rooms, teachers and period
  labels, seeded, an unknown code falls back to the raw code; `school codes` CLI.
- Brief: Finn's block first, then Harry's, four sections always present (FR-9);
  periods decoded into plain English; the undated tail is one counted line (FR-18).
- Calendar: write periods + work due + work overdue + major events, per child,
  on every pass. Identity is the stable per-item key, so a re-run never re-inserts
  (FR-8). morning-brief.sh now runs scan -> calendar push -> notify.
- Day-widget labels are 12-hour: 1-6 read as PM, fixing afternoon periods.

Proven live on .13: image 0.6.2-phase6b7b; both children's briefs decoded and
separate; 35 events in calendar.school; runs 2 and 3 of the scan added nothing.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
2026-10-16 10:25:55 +11:00
parent 4950755b3d
commit 047af0f31d
40 changed files with 2049 additions and 160 deletions

View File

@@ -26,8 +26,11 @@ Phase 9 publishes the project documentation across five places.
The daily trigger is **06:00** in `Australia/Melbourne` (n8n). The briefs go out
on **Email and WhatsApp** through Apprise on `.35:8210`: one brief per boy, plus
one combined brief to Jo and Sam. Important school events go to the Home
Assistant Local Calendar `calendar.school`. The Hub reads items through the
one combined brief to Jo and Sam. Each boy's block comes **separately, Finn
first**, with his own **decoded** periods, today's events, overdue work and due
work. Every pass also writes that child's periods, due work, overdue work and
the major school events to the Home Assistant Local Calendar
`calendar.school`, once and never twice. The Hub reads items through the
read-only `POST /ask` contract.
The accepted scraper is **patchright** (headless Playwright), not the
@@ -104,10 +107,24 @@ shown on the dashboard, so a failed send is loud rather than silent (FR-12).
`POST /notify/morning` is the endpoint the 6:00am n8n schedule calls. See
[`docs/phase-7-notify-notes.md`](docs/phase-7-notify-notes.md).
Each child's block is separate and **Finn comes first** (FR-9). The four
headings are always present and say `none` when empty:
```
=== FINN ===
PERIODS TODAY <- decoded from the child's own day widget (FR-16/FR-17)
EVENTS TODAY
OVERDUE
DUE IN FUTURE
```
The undated tail is never dumped: it is one counted line with a pointer to the
dashboard (FR-18).
```bash
make notify # send the briefs from the latest scan
make preview # print the briefs, send nothing
make morning-brief # scan both children, then send (the n8n job)
make morning-brief # scan, write the calendar, then send
# Delivery log: per recipient per channel, plus the scan state.
docker exec compass-teams-nhs /app/school notify status
@@ -116,12 +133,28 @@ docker exec compass-teams-nhs /app/school notify status
The destinations live in the Apprise configuration, not here: this service only
names a tag such as `harry_email` (AGENTS.md §10 rule 3).
### The decode map (FR-17)
Periods read as plain English from an editable `codes` table. Seed rows ship for
the captures (for example `7SCF` -> `Year 7 Science`, `H14LAB` -> `Room H14LAB`,
`KNI` -> `Mr KNI`); an unknown code renders as the **raw code**, never blank.
Correct or add a mapping without a rebuild:
```bash
docker exec compass-teams-nhs /app/school codes list
docker exec compass-teams-nhs /app/school codes set --kind teacher --code KNI --name 'Mr Knight'
docker exec compass-teams-nhs /app/school codes delete --kind room --code Z9
```
## Status — Phase 6
Phase 6 writes the **important** school events to **Sam's Home Assistant
calendar** through the Home Assistant REST API (FR-8). It is idempotent (a
re-run never duplicates an event) and it fails loudly (a failed write is shown
on the dashboard and recorded in `calendar_pushes`). See
Phase 6 writes, for **each child separately**, that day's **periods**, the
**work due** (any future date), the **work overdue**, and the **major school
events** to **Sam's Home Assistant calendar** through the Home Assistant REST
API (FR-8). It is idempotent: identity is the stable per-item Compass key, so a
re-run checks the item and never re-inserts it — a piece of work due in two
weeks appears on every pass until it passes. A failed write is shown on the
dashboard and recorded in `calendar_pushes`. See
[`docs/phase-6-calendar-notes.md`](docs/phase-6-calendar-notes.md).
The target is a **Home Assistant Local Calendar**, `calendar.school` (added
@@ -133,8 +166,9 @@ Assistant had). The entity stays configurable with
scripts/calendar-push.sh entities # list the writable calendar entities
scripts/calendar-push.sh status # target, reachability, pending, last push
scripts/calendar-push.sh push --dry-run
scripts/calendar-push.sh push # write the pending important events
scripts/calendar-push.sh events # read the calendar back
scripts/calendar-push.sh push # write the pending periods/work/events
scripts/calendar-push.sh events --past 120 --days 60 # read the calendar back
scripts/calendar-push.sh events --past 120 --days 60 --json
make calendar CALENDAR_ARGS='push --dry-run'
```
@@ -229,8 +263,8 @@ docker compose up -d --build
Generate secrets with the built image:
```bash
docker run --rm --entrypoint /app/school compass-teams-nhs/school:0.6.1-phase8 gen-secret
docker run --rm --entrypoint /app/school compass-teams-nhs/school:0.6.1-phase8 hash-password 'the-password'
docker run --rm --entrypoint /app/school compass-teams-nhs/school:0.6.2-phase6b7b gen-secret
docker run --rm --entrypoint /app/school compass-teams-nhs/school:0.6.2-phase6b7b hash-password 'the-password'
```
Note: Docker Compose interpolates `$VAR` inside `env_file`. A bcrypt hash