The first live run against the new calendar.school entity failed loudly twice, and both causes are fixed here. - The Local Calendar rejects `return_response` on calendar.create_event. The client retries the call without it and ignores the changed-states array that Home Assistant returns instead. - Home Assistant then reports no event id. The item keeps the description marker ([compass-nhs:<id>]) as its reference, so it leaves the candidate set and is never written twice. The live retry proved it: both events already in the calendar were adopted by their markers, with no duplicate. - calendar.get_events reports `start` / `end` (a date-time or a bare date), not `start_date_time`; the reader now accepts all three shapes. A model must no longer classify events. The calendar write depends on the school-event rule, and the LLM labelled every event "normal"; events now keep the deterministic rule under both classifiers. Also fix the Phase 5 screenshot script: the calendar banner shares the `.scan` class, so select each banner by its labelled section. Docs record the accepted decision: Sam chose a Home Assistant Local Calendar, so the target is calendar.school. Co-Authored-By: Paperclip <noreply@paperclip.ing>
5.4 KiB
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 6 (the calendar write). See §11 of the spec for the phase order.
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): notifications and the Hub API. The calendar write is Phase 6. The service itself does not scan the school; n8n triggers the scan later. 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/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, deployment notes, verify script
scripts/ Compass fetchers, the parse wrapper and 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.4.0-phase6 gen-secret
docker run --rm --entrypoint /app/school compass-teams-nhs/school:0.4.0-phase6 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.