- 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>
198 lines
8.3 KiB
Markdown
198 lines
8.3 KiB
Markdown
# 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`](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
|
|
|
|
```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 <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:
|
|
|
|
```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.
|