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

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. 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.

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.

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.

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.

# 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.

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.

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

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:

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

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.
Description
Scan Compass and Teams for the boys school schedule and homework.
Readme 1.3 MiB
Languages
Go 80.3%
JavaScript 6.2%
Shell 5.6%
HTML 3.6%
CSS 3.3%
Other 0.9%