# 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 **future** events are written: a past lesson is history, not a plan. - Which events qualify is visible before anything is written: ```bash 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:]`. 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 ```bash 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) ``` ## Verification ```bash 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.