Files
compass_teams_nhs/README.md
Sam Rolfe 09639c5125 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>
2026-10-15 19:28:05 +11:00

164 lines
6.5 KiB
Markdown

# Compass / NHS
A school service for Harry and Finn: **Compass** for the daily schedule and
notices, **Teams** for homework and assignments. It stores what is due per child,
puts overdue work first, and tells the family before work is late.
> Build spec: [`AGENTS.md`](AGENTS.md). This repository is at **Phase 7** (notify
> and send via Apprise). See §11 of the spec for the phase order.
## Status — Phase 7
Phase 7 builds and sends the morning briefs through the house **Apprise** API on
`.35:8210` (FR-9, FR-14): one brief per boy, plus one combined brief to Jo and
Sam, on **Email and WhatsApp**. Every attempt is recorded in `notifications` and
shown on the dashboard, so a failed send is loud rather than silent (FR-12).
`POST /notify/morning` is the endpoint the 6:00am n8n schedule calls. See
[`docs/phase-7-notify-notes.md`](docs/phase-7-notify-notes.md).
```bash
make notify # send the briefs from the latest scan
make preview # print the briefs, send nothing
make morning-brief # scan both children, then send (the n8n job)
# Delivery log: per recipient per channel, plus the scan state.
docker exec compass-teams-nhs /app/school notify status
```
The destinations live in the Apprise configuration, not here: this service only
names a tag such as `harry_email` (AGENTS.md §10 rule 3).
## Status — Phase 6
Phase 6 writes the **important** school events to **Sam's Home Assistant
calendar** through the Home Assistant REST API (FR-8). It is idempotent (a
re-run never duplicates an event) and it fails loudly (a failed write is shown
on the dashboard and recorded in `calendar_pushes`). See
[`docs/phase-6-calendar-notes.md`](docs/phase-6-calendar-notes.md).
The target is a **Home Assistant Local Calendar**, `calendar.school` (added
2026-10-15 after Sam chose it over the Google-backed entities that Home
Assistant had). The entity stays configurable with
`SCHOOL_HA_CALENDAR_ENTITY`.
```bash
scripts/calendar-push.sh entities # list the writable calendar entities
scripts/calendar-push.sh status # target, reachability, pending, last push
scripts/calendar-push.sh push --dry-run
scripts/calendar-push.sh push # write the pending important events
scripts/calendar-push.sh events # read the calendar back
make calendar CALENDAR_ARGS='push --dry-run'
```
## Status — Phase 5
Phase 5 adds the per-child dashboard: one tab per child, **overdue first**, then
due today, due this week and messages, plus the item detail screen and the
Delete actions. See [`docs/phase-5-dashboard-notes.md`](docs/phase-5-dashboard-notes.md).
```bash
# Phone-width evidence screenshots (writes under var/, git-ignored).
SCHOOL_USER=sam SCHOOL_PASS=... node scripts/screenshot-phase5.mjs
# Build a draft brief for one child.
docker exec compass-teams-nhs /app/school brief build --child Harry
```
## Status — Phase 4
Phase 4 fetches and parses **Harry and Finn** with a stored session per child,
and stores every item against the right `children.id`. See
[`docs/phase-4-both-children-notes.md`](docs/phase-4-both-children-notes.md).
```bash
make scan # both children
make scan SCAN_CHILD=Finn # one child
school session show # session state per child (metadata only, no secrets)
```
## Status — Phase 3
Phase 3 parses the raw pages stored by Phase 2 into the SQLite `items` table
(tasks, messages, events). See [`docs/phase-3-parse-notes.md`](docs/phase-3-parse-notes.md).
```bash
scripts/parse-compass.sh --child Harry --run latest --classify auto
```
## Status — Phase 1
Built and deployed on `.13`:
- Go service, SQLite schema for `children`, `items`, `scans`, `notifications`,
`session` (`internal/store/migrations/001_init.sql`).
- The service's **own session login** — a signed cookie, not SSO.
- Server-rendered **HTMX** shell using the Family Home Lab design tokens
(`internal/web/static/tokens.css`), phone-friendly with dark mode.
- `GET /health`.
- Its own container and port **8792**, behind Caddy at
`school.lab.audasmedia.com.au`.
Not built yet (later phases): the Hub API. The service itself does **not** scan
the school; n8n triggers the scan. Teams (FR-15) is a separate source.
### Fail loudly (FR-12)
A missing or failed scan is shown as an explicit warning or error, never as an
empty day. The dashboard reads the `scans` table and shows one of:
- **NO SCAN HAS RUN YET** — amber banner.
- **SCAN FAILED** — red banner with the failure on record.
- **Last scan completed** — green banner.
## Layout
```
cmd/school/ service entry point (run, parse, session, scan, items, hash-password, gen-secret)
internal/config/ environment configuration
internal/store/ SQLite: schema, migrations, queries
internal/parse/ HTML -> structured items (tasks, messages, events)
internal/llm/ OmniRoute client (importance classification only)
internal/apprise/ Apprise API client (tag-based, redacts errors)
internal/brief/ build + deliver the morning briefs (FR-9, FR-14)
internal/calendar/ Home Assistant calendar client + the idempotent sync
internal/auth/ session login (signed cookies, CSRF)
internal/web/ HTTP handlers, templates, static assets
deploy/ Caddy snippet, the n8n workflow, deployment notes, verify script
scripts/ Compass fetchers, the parse and morning-brief wrappers, the calendar push
environment.d/ env example (the real file is gitignored)
```
## Run
```bash
cp environment.d/school-service.env.example environment.d/school-service.env
chmod 600 environment.d/school-service.env # then fill in the secrets
docker compose up -d --build
```
Generate secrets with the built image:
```bash
docker run --rm --entrypoint /app/school compass-teams-nhs/school:0.5.0-phase7 gen-secret
docker run --rm --entrypoint /app/school compass-teams-nhs/school:0.5.0-phase7 hash-password 'the-password'
```
Note: Docker Compose interpolates `$VAR` inside `env_file`. A bcrypt hash
contains `$`, so escape each one as `$$` in the env file.
## Verify
```bash
make verify-phase1
```
It checks the container is healthy, `GET /health` returns 200, the login page
renders and a session login succeeds, and Caddy serves the public URL.
## Rules
- **Read-only against the school.** Never post, message, or submit anything.
- A failed scan must be loud — never report an empty day as "nothing due".
- School credentials and harness session state are secrets; never commit them.
- This project has its own folder, container, and port.