Phase 7: notify and send via Apprise (Email + WhatsApp)
The morning briefs go out through the house Apprise API on `.35:8210`: one brief per boy and one combined brief to Jo and Sam (FR-9, FR-14). The service names an Apprise tag (`harry_email`, `joanna_whatsapp`, ...) and never holds an address, a phone number or an SMTP password (AGENTS.md §10 rule 3). - internal/apprise: keyed and stateless client, tag filtering, and error redaction so nothing sensitive reaches a log, a row or a screen. - internal/brief: builds the per-boy and the combined brief, delivers on email and WhatsApp, records every attempt in `notifications` (migration 004 adds scope, channel, tag, ok and error) and fails loudly. - Send panel (§5 screen 4): recipients, channels, an exact preview, Send now, plus the Send action on a stored draft (FR-10). The dashboard shows a Morning briefs banner and the Sends list. - POST /notify/morning: the bearer-token endpoint the 6:00am n8n schedule calls. 207 when a channel or a scan failed; `dry_run=1` checks the wiring without messaging the family. - scripts/morning-brief.sh: scan both children, then send. It is the n8n job; deploy/n8n-morning-brief.json is the ready-to-import workflow (06:00 daily). - FR-12: a failed scan is never "nothing due" -- the brief says so, and the run exits non-zero. A failed delivery is recorded with no sent_at. - The long no-due-date tail is capped in the sent brief so it stays brief (§10 rule 6); overdue work is never capped. Verified live: 8 deliveries recorded (4 email, 4 WhatsApp) with HTTP 200 and sent_at, and the endpoint answers 401/200/207 as documented. Evidence in docs/phase-7-notify-notes.md. Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
192
docs/phase-7-notify-notes.md
Normal file
192
docs/phase-7-notify-notes.md
Normal file
@@ -0,0 +1,192 @@
|
||||
# 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 `<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.
|
||||
Reference in New Issue
Block a user