Files
compass_teams_nhs/docs/phase-7-notify-notes.md
Sam Rolfe 047af0f31d 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>
2026-10-16 10:25:55 +11:00

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 (207 on the endpoint).
  • A delivery that fails is recorded with ok = 0, no sent_at, and the redacted Apprise error; the dashboard shows SEND FAILED and 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

  1. Channels are Email and WhatsApp (AGENTS.md §13 decision 8). Both are configured in the house Apprise configuration and both delivered.
  2. /json/urls/{key} shows callmebot:// for the WhatsApp entries. That is Apprise's privacy mode masking the credentials, not an empty destination: the two callmebot:// entries per person carry a phone number and a CallMeBot API key, and Apprise logged CallMeBot SUCCESS for each.
  3. 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.sh runs both steps.
  4. The n8n workflow is provided, not imported — n8n is a shared service and the sibling email_agent project set the same precedent.
  5. Teams (FR-15) is still out of scope for this phase; the brief uses the Compass items stored so far.