Files
compass_teams_nhs/docs/phase-6-calendar-notes.md
Sam Rolfe f56dbb46ac Phase 6 follow-up: today-and-future events, one write per school-wide event
Two corrections found while preparing the first live run:

- A scan that runs late in the day still records today's school events; the
  candidate window now starts at the local start of day instead of "now".
- The Compass week calendar is school-wide, so one event is stored against each
  child. Write it once, with no child prefix in the title, and record the same
  Home Assistant event id on every child's item. A per-child event still keeps
  the child's name.

Also report pending events as events, not rows, in `school calendar status`.
Tests cover the window and the de-duplication.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-10-15 18:05:33 +11:00

6.6 KiB
Raw Blame History

Phase 6 — the calendar write (Sam's Home Assistant calendar)

Status: code complete and tested; the calendar target needs Sam's decision (AUD-110). The service now writes important school events through the Home Assistant REST API. Nothing is written until a calendar entity is chosen.

What it does

school calendar push selects the important school events that are not in the calendar yet and writes them with the Home Assistant service calendar.create_event. The school is never touched (AGENTS.md §10 rule 1); the only write is to the calendar (rule 7 — no approval needed).

  • Important is the Phase 3 classifier: internal/parse/importance.go marks an event high when it is a school event, and normal when it is a routine class period. A Compass week grid writes a class as 5 - 11HLEGB5 - D8 - LHT and an event as End of Year Senior Music Concert - HALL (Senior Music Concert) - KSH, CFR. The first part of a lesson is a period label (5, C, RLE GROUP D).
  • Only today and future events are written: a scan that runs late in the day still records today's school events, but yesterday is history.
  • The Compass week calendar is school-wide, so the same event is stored against each child. A school-wide event is written once, with no child prefix in its title, and every child's item records the same event id. A per-child event keeps the child's name in its title (Harry — …).
  • Which events qualify is visible before anything is written:
scripts/calendar-push.sh push --dry-run

Idempotency (a re-run never duplicates)

Two independent guards:

  1. The item stores the Home Assistant event id (items.ha_event_uid). An item that already has one is not a candidate again.
  2. The event description carries a marker, source: [compass-nhs:<item id>]. A run lists the calendar first and adopts any event that already carries the marker for that item, even when Home Assistant reports no event id.

items.ha_pushed_at records when the item was written.

Fail loudly (FR-12)

A failed calendar write is never swallowed.

  • Every attempt is appended to calendar_pushes (ok = 0 plus the error).
  • The dashboard shows a Calendar banner: CALENDAR NOT CONFIGURED, CALENDAR WRITE FAILED with the Home Assistant reason, or the healthy state.
  • The item screen shows the failure in place of a confirmation.
  • scripts/calendar-push.sh push exits non-zero when any write failed.

A failed write leaves the item as a candidate, so the next run retries it.

Configuration (names only — never the values)

Variable Meaning
SCHOOL_HA_URL Home Assistant base URL, for example http://192.168.20.30:8123
SCHOOL_HA_TOKEN long-lived access token (secret)
SCHOOL_HA_CALENDAR_ENTITY the calendar entity to write to, for example calendar.school
SCHOOL_HA_INSECURE true to skip TLS verification for the internal .lab certificate
SCHOOL_HA_TIMEOUT per-request timeout (default 20s)

The values live in environment.d/school-service.env, which is gitignored (AGENTS.md §10 rule 3). The token is never logged, never printed by the CLI and never stored in SQLite.

Commands

scripts/calendar-push.sh entities     # list calendar.* entities and whether they are writable
scripts/calendar-push.sh status       # target, reachability, pending events, last push
scripts/calendar-push.sh push         # write the pending important events
scripts/calendar-push.sh events       # read the calendar back (evidence)
scripts/calendar-push.sh push --dry-run --json

make calendar CALENDAR_ARGS='push --dry-run' runs the same thing.

The item screen has an Add to calendar button for an important future event, and shows in the calendar with the Home Assistant event id once it is written.

The open decision: which calendar entity?

The Home Assistant instance (192.168.20.30, HA 2026.9.4) has three calendar entities, and only two are writable:

Entity Writable Backing
calendar.samuelrolfe_googlemail_com yes Google, synced into Home Assistant
calendar.birthdays yes Google
calendar.holidays_in_australia no Google

The brief says Sam's Home Assistant calendar (not Google). Today Home Assistant has no non-Google writable calendar: the local_calendar integration is not installed, and no CalDAV/remote calendar is configured.

So the target must be chosen before the event is written:

  1. Add a Local Calendar to Home Assistant (recommended). One Home Assistant config-flow entry creates calendar.school. It is Home-Assistant-local, never touches Google, and is entirely additive. The service then uses SCHOOL_HA_CALENDAR_ENTITY=calendar.school.
  2. Use calendar.samuelrolfe_googlemail_com. No Home Assistant change, but every school event also appears in Sam's personal Google Calendar.

The question is posted on AUD-110. The code needs only the configuration line changed once the answer is in.

Where the code lives

internal/calendar/ha.go      Home Assistant REST client (states, create, get, delete)
internal/calendar/sync.go    selection, idempotency, the push log
internal/store/calendar.go   candidates, mark-synced, the append-only push log
internal/store/migrations/003_calendar.sql
cmd/school/calendar.go       `school calendar entities|status|push|events`
internal/web/server.go       the item action + the fail-loud calendar banner
scripts/calendar-push.sh     one-off container runner (no service restart)

Found while building (worth a decision later)

The Compass week-calendar page looks school-wide, not child-filtered: Finn's page carries Year 11 Assembly and Tour to China photo exactly as Harry's does, while their lesson grids differ. Phase 6 handles that honestly — a shared event is written once and both children's items record the same event id — but the per-child attribution of a school-wide event is a Phase 3/4 data question, not a calendar one. It matters for the Phase 7 per-boy brief.

The week grid also stops at Thursday in the stored pages (Monday–Thursday have lessons, Friday–Sunday do not), so a Friday scan may be the first to carry future school events.

Verification

make test    # go test ./...

The tests cover the Home Assistant client against a fake Home Assistant (create, read-back, error mapping), the sync run (write once, adopt by marker, dry run, loud failure, refuse-to-write-blind), the store (candidates, the push log) and the web surface (the action, the CSRF guard, the failure banner).

Not tested here: the live Home Assistant write. That needs the entity decision.