# Phase 7 — Notify and send (Apprise: Email + WhatsApp) 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 `_email` and `_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 ```bash # 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 ``` 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: ```bash 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.