Sam Rolfe 3e25011399 Phase 6b/7b: dashboard codes editor, decode events, human-readable briefs (AUD-115)
- New /codes dashboard page (behind the session login, linked in the top bar):
  lists every mapping by kind with Add, inline Edit+Save and Delete. Sam can put
  real teacher surnames in without a rebuild (FR-17).
- Decode timetable EVENTS through the same map as periods, so an ensemble reads
  'JAZZ ENSEMBLE - Ensemble Room - Mr KWO' instead of raw codes.
- teacherName splits comma- or space-separated codes ('HHL, OFG').
- SeedCodes is a one-time bootstrap while the table is empty, so a correction or
  a delete is never overwritten or restored.
- Tests: decode Label/Event, the /codes page (save, refuse, csrf, delete, auth),
  store re-seed behaviour. go test ./internal/... passes.
2026-10-16 12:50:31 +11:00

Compass / NHS

A school service for Harry and Finn: Compass gives the daily schedule, the homework and the notices. It stores what is due per child, puts overdue work first, and tells the family before work is late.

Teams (FR-15) is DEFERRED. No Teams section is scanned. Compass answers what is due and overdue. This is a known gap, not a defect.

Build spec: AGENTS.md. This repository is at Phase 9 (documentation). See §11 of the spec for the phase order.

Status — Phase 9

Phase 9 publishes the project documentation across five places.

Layer Artifact Location
Technical README This file the Gitea repo
Architecture source docs/architecture.mmd repo
Rendered diagram docs/diagrams/architecture.png repo
Interactive map docs/architecture-map.html https://maps.lab.audasmedia.com.au/compass_teams_nhs/docs/
Human note Obsidian 010 inbox/ on .27 filed by the sorter
AI context docs/outline/ Outline collection Compass / NHS (School)
Tasks docs/vikunja-tasks.json Vikunja project id 21

The daily trigger is 06:00 in Australia/Melbourne (n8n). The briefs go out on Email and WhatsApp through Apprise on .35:8210: one brief per boy, plus one combined brief to Jo and Sam. Each boy's block comes separately, Finn first, with his own decoded periods, today's events, overdue work and due work. Every pass also writes that child's periods, due work, overdue work and the major school events to the Home Assistant Local Calendar calendar.school, once and never twice. The Hub reads items through the read-only POST /ask contract.

The accepted scraper is patchright (headless Playwright), not the pi-browser-harness: the harness cannot pass the Cloudflare managed challenge on the Compass login POST. The Compass parent login required no two-factor code during the Phase 2 proof.

Architecture

flowchart LR
    N8N["n8n on .13<br/>06:00 daily trigger"]
    SAM["Sam<br/>browser dashboard"]
    CADDY["Caddy on .35<br/>school.lab.audasmedia.com.au"]
    SVC["Compass / NHS service<br/>Go + SQLite :8792"]
    PATCH["patchright scraper<br/>headless Playwright"]
    COMPASS["Compass NHS<br/>read-only login + pages"]
    OMNI["OmniRoute<br/>importance classification"]
    HA["Home Assistant calendar<br/>calendar.school"]
    APPRISE["Apprise on .35:8210<br/>Email + WhatsApp"]
    BOYS["Harry and Finn<br/>per-boy morning brief"]
    PARENTS["Jo and Sam<br/>combined morning brief"]
    HUB["Hub :8788<br/>read-only /ask"]

    N8N -->|"scripts/morning-brief.sh"| SVC
    SAM -->|HTTPS| CADDY
    CADDY --> SVC
    SVC -->|scan| PATCH
    PATCH -->|"login + fetch pages"| COMPASS
    SVC -.->|"important only"| OMNI
    SVC -->|"create event"| HA
    SVC -->|briefs| APPRISE
    APPRISE -->|"email + WhatsApp"| BOYS
    APPRISE -->|"email + WhatsApp"| PARENTS
    HUB -->|"Bearer /ask"| SVC

Interactive map: https://maps.lab.audasmedia.com.au/compass_teams_nhs/docs/

Status — Phase 8

Phase 8 exposes the project side of the frozen Hub contract (central_ai_llm_master/docs/ask-contract.md) on this service's own port (FR-11):

Method Path Notes
GET /capabilities {"project":"compass_nhs","topics":[…],"people":[…]}, matching the Hub's routing.md row.
POST /ask Read-only. Answers from the stored Compass items in one short sentence; returns the contract's answer: null + reason when it cannot answer. A failed or stale scan is stated in the answer, never as "nothing due" (FR-12).
POST /notify Accepts the contract's {"from","level","message","person"} and delivers it through Apprise.

All three require Authorization: Bearer $SCHOOL_HUB_TOKEN. With no token set they answer 503. See docs/phase-8-hub-notes.md.

make verify-phase8                 # /health ok, /capabilities, /ask, /notify auth
deploy/verify-phase8.sh --send     # also delivers one real /notify message to Sam

# The Hub's own client shape, against the live service:
curl -sS -H "Authorization: Bearer $SCHOOL_HUB_TOKEN" -H 'Content-Type: application/json' \
  -d '{"question":"what does Harry have on today at school?","asker":"sam","person":"harry"}' \
  http://127.0.0.1:8792/ask

The /ask answers are deterministic (no model on the path), so the Hub's 3 second cap is never the reason an answer is missing.

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.

Each child's block is separate and Finn comes first (FR-9). The four headings are always present and say none when empty:

=== FINN ===
PERIODS TODAY      <- decoded from the child's own day widget (FR-16/FR-17)
EVENTS TODAY
OVERDUE
DUE IN FUTURE

The undated tail is never dumped: it is one counted line with a pointer to the dashboard (FR-18).

make notify                       # send the briefs from the latest scan
make preview                      # print the briefs, send nothing
make morning-brief                # scan, write the calendar, then send

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

The decode map (FR-17)

Periods read as plain English from an editable codes table. Seed rows ship for the captures (for example 7SCF -> Year 7 Science, H14LAB -> Room H14LAB, KNI -> Mr KNI); an unknown code renders as the raw code, never blank. Correct or add a mapping without a rebuild:

docker exec compass-teams-nhs /app/school codes list
docker exec compass-teams-nhs /app/school codes set --kind teacher --code KNI --name 'Mr Knight'
docker exec compass-teams-nhs /app/school codes delete --kind room --code Z9

Status — Phase 6

Phase 6 writes, for each child separately, that day's periods, the work due (any future date), the work overdue, and the major school events to Sam's Home Assistant calendar through the Home Assistant REST API (FR-8). It is idempotent: identity is the stable per-item Compass key, so a re-run checks the item and never re-inserts it — a piece of work due in two weeks appears on every pass until it passes. 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 periods/work/events
scripts/calendar-push.sh events --past 120 --days 60   # read the calendar back
scripts/calendar-push.sh events --past 120 --days 60 --json
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: Teams (FR-15). It is deferred on purpose. The service itself does not scan the school; n8n triggers the scan.

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/hub/               the Hub contract: capabilities + read-only answers (FR-11)
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)
docs/                       phase notes, the architecture source and map, the Outline set, Vikunja tasks

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.6.2-phase6b7b gen-secret
docker run --rm --entrypoint /app/school compass-teams-nhs/school:0.6.2-phase6b7b 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    # container, /health, login, the Caddy route
make verify-phase8    # the Hub API: /capabilities, /ask, /notify

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%