- 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>
8.3 KiB
Phase 7 — Notify and send (Apprise: Email + WhatsApp)
Updated by Phase 7b (AUD-119): the brief now sends Finn's block first, then Harry's, never interleaved; periods are decoded into plain English; and the undated tail is one counted line, not a dump. See
phase-6b7b-notes.md.
Status: done (2026-10-15). A brief was delivered to real recipients on both channels. See Evidence at the end.
Spec: AGENTS.md §4 FR-9/FR-10/FR-13/FR-14, §5 screen 4 (Send panel), §8
(integrations), §10 (rules), §11 Phase 7.
What the phase does
One morning run sends:
| Brief | Recipients | Channels |
|---|---|---|
| Harry | Harry | email, WhatsApp |
| Finn | Finn | email, WhatsApp |
| Combined (FR-14) | Jo and Sam | email, WhatsApp |
Every attempt is a row in notifications: one row per recipient per channel,
with the Apprise tag, the HTTP status, sent_at, and the redacted error when it
failed. The dashboard shows a Morning briefs banner and a Sends list, so
a failed send is visible instead of silent (FR-12).
How the destination is kept out of this repo
The Apprise API on .35:8210 holds every destination in one stored
configuration. This service only names a tag:
SCHOOL_APPRISE_TAGS=Harry=harry,Finn=finn,Jo=joanna,Sam=sam
A channel tag is <tag base>_email and <tag base>_whatsapp, which is the
naming the house configuration already uses (harry_email, joanna_whatsapp, …).
So no email address, phone number or SMTP password is stored in this repo, in
the database, in a log or on the dashboard (AGENTS.md §10 rule 3).
The Apprise configuration key is a secret and lives only in
environment.d/school-service.env (gitignored). Environment variables:
| Variable | Meaning |
|---|---|
SCHOOL_APPRISE_URL |
API base, default http://192.168.20.35:8210 |
SCHOOL_APPRISE_KEY |
stored configuration key (required to send) |
SCHOOL_APPRISE_TOKEN |
optional bearer token for the API |
SCHOOL_APPRISE_TIMEOUT |
per-request timeout, default 15s |
SCHOOL_APPRISE_CHANNELS |
email,whatsapp |
SCHOOL_APPRISE_TAGS |
recipients as Name=tag-base |
SCHOOL_APPRISE_PARENTS |
who receives the combined brief, Jo,Sam |
SCHOOL_NOTIFY_TOKEN |
bearer token for POST /notify/morning |
Commands
# Build and send everything (the 6:00am job): scan, then send.
scripts/morning-brief.sh
# Send from the latest scan, no school access.
scripts/morning-brief.sh --no-scan
# Print the briefs and send nothing.
scripts/morning-brief.sh --no-scan --dry-run
make preview
# Delivery log: status per recipient per channel, plus the scan state.
docker exec compass-teams-nhs /app/school notify status
# One brief only, or one channel only.
docker exec compass-teams-nhs /app/school notify run --only boys
docker exec compass-teams-nhs /app/school notify run --channel email
# Rebuild and send one stored draft.
docker exec compass-teams-nhs /app/school notify send --summary 12
Send panel (AGENTS.md §5 screen 4)
The dashboard has one card per brief: recipients, channels, a preview that is exactly what is sent, and Send now. A tick box per recipient is the recipient selection; leaving every box clear falls back to the scope default (the boy for a child brief, Jo and Sam for the combined brief).
POST /summaries/{id}/send is the Send action on a stored draft (FR-10). The
draft is stamped sent_at only when every channel delivered.
The 6:00am trigger (n8n)
The endpoint n8n calls:
POST http://192.168.20.13:8792/notify/morning
Authorization: Bearer <SCHOOL_NOTIFY_TOKEN>
Optional query parameters: channels=email,whatsapp and dry_run=1.
| Response | Meaning |
|---|---|
200 |
every channel delivered |
207 |
a channel failed, or a scan did not complete — the body names both |
401 |
missing or bad bearer token |
503 |
SCHOOL_NOTIFY_TOKEN is not set, so the endpoint is off |
500 |
an internal error |
deploy/n8n-morning-brief.json is a ready-to-import workflow:
Schedule Trigger 0 6 * * * (timezone Australia/Melbourne) → Execute Command
ssh remote 'cd /home/sam/paseo/projects/compass_teams_nhs && scripts/scan-compass.sh'
→ HTTP Request POST /notify/morning.
The scan runs on the host, not in the service container: patchright and its
browser are installed on .13 (Phase 2). One-command alternative for the
Execute Command node:
ssh remote 'cd /home/sam/paseo/projects/compass_teams_nhs && scripts/morning-brief.sh'
The workflow is not imported yet. n8n is a shared service, so importing it
is Sam's call. To import: n8n → Workflows → Import from file →
deploy/n8n-morning-brief.json, set SCHOOL_NOTIFY_TOKEN in the n8n
environment (or replace the expression with the literal token), then activate.
Fail loudly (FR-12)
- A child whose last scan failed or never ran gets a brief that opens with
SCAN FAILED … Due work is UNKNOWN, not "nothing due". The combined brief carries the same warning, and the run exits non-zero (207on the endpoint). - A delivery that fails is recorded with
ok = 0, nosent_at, and the redacted Apprise error; the dashboard showsSEND FAILEDand the CLI exits non-zero. - Nothing is retried automatically: a failure is reported, not hidden.
Brief content (FR-9, §10 rule 6)
Overdue first, then due today, due this week, no due date, then messages. The
long no-due-date tail is capped at five entries in the sent brief with
… and N more with no due date (see the dashboard); overdue work is never
capped. The dashboard and the Send panel preview show the full lists.
Evidence (2026-10-15, 08:24 UTC / 19:24 AEDT)
scripts/morning-brief.sh --no-scan delivered all eight attempts (HTTP 200
from the Apprise API, CallMeBot SUCCESS and Sent Email to … in the Apprise
log). Addresses and numbers are redacted here by design.
notifications (8 rows)
id scope recipient channel tag ok sent_at bytes
1 Harry Harry email harry_email 1 2026-10-15T08:24:24Z 2161
2 Harry Harry whatsapp harry_whatsapp 1 2026-10-15T08:24:24Z 2161
3 Finn Finn email finn_email 1 2026-10-15T08:24:31Z 1582
4 Finn Finn whatsapp finn_whatsapp 1 2026-10-15T08:24:31Z 1582
5 combined Jo email joanna_email 1 2026-10-15T08:24:36Z 3645
6 combined Jo whatsapp joanna_whatsapp 1 2026-10-15T08:24:36Z 3645
7 combined Sam email sam_email 1 2026-10-15T08:24:36Z 3645
8 combined Sam whatsapp sam_whatsapp 1 2026-10-15T08:24:36Z 3645
scans holds the run the briefs were built from (2026-10-15 07:58,
ok = 1, 78 items for Finn and 106 for Harry), and PRAGMA user_version = 4
confirms migration 004.
The endpoint was checked live against the deployed container:
POST /notify/morning -> 401 (no token)
POST /notify/morning?dry_run=1 (real token) -> 200, 3 sends, 0 scans failed
scope=Harry recipients=['Harry'] text 2161 bytes
scope=Finn recipients=['Finn'] text 1582 bytes
scope=combined recipients=['Jo', 'Sam'] text 3645 bytes
A live sending call to the endpoint was not repeated, to avoid a second round
of eight messages five minutes after the first. The endpoint calls the same
brief.Service.Morning that delivered the eight rows above, and the delivery
path is covered by tests.
Decisions and notes
- Channels are Email and WhatsApp (AGENTS.md §13 decision 8). Both are configured in the house Apprise configuration and both delivered.
/json/urls/{key}showscallmebot://for the WhatsApp entries. That is Apprise's privacy mode masking the credentials, not an empty destination: the twocallmebot://entries per person carry a phone number and a CallMeBot API key, and Apprise loggedCallMeBot SUCCESSfor each.- The endpoint does not scan. The Compass scan needs patchright on
.13, so it stays a host step (Phase 2/4) and the endpoint builds and sends what the last scan stored.scripts/morning-brief.shruns both steps. - The n8n workflow is provided, not imported — n8n is a shared service and
the sibling
email_agentproject set the same precedent. - Teams (FR-15) is still out of scope for this phase; the brief uses the Compass items stored so far.