The project side of the frozen Hub contract
(central_ai_llm_master/docs/ask-contract.md), on this service's own port:
GET /capabilities the routing topics the Hub reads, project "compass_nhs"
POST /ask one short, read-only answer about a child's school work
POST /notify accept the contract's notification shape, deliver via Apprise
All three require the shared bearer token SCHOOL_HUB_TOKEN; with no token they
answer 503. GET /health now also carries "ok": true and "hub_api".
/ask answers deterministically (no model on the path) from the stored Compass
items, reusing dashboard.Classify so the answer and the dashboard never
disagree. It is one short sentence, for example "Harry has 8 school events
today from 8:55am to 6:00pm and 6 overdue tasks", with the buckets in `data`.
It reads only, enforced by a Reader interface with no write method and by
TestHubAskReadsOnly.
Fail loudly (FR-12): a missing or failed scan is never an empty day. /ask says
due work is unknown, and an answer older than 36 hours carries a stale warning.
Permission follows the contract: a parent may ask about any child; anyone else
only about themselves.
/notify follows the house interpretation already used by internet_use_control:
the contract shape is accepted and delivered to the named person, recorded in
notifications with scope "hub". An unknown person is refused, not misdelivered.
Live proof on .13 (deploy/verify-phase8.sh):
/health 200, ok true
/capabilities 200, project compass_nhs; unsigned 401
/ask 200, the sentence above, duration 1ms
/notify 400 on a bad level (auth proven, nothing sent), then 200 with
a real delivery to Sam on email and WhatsApp
The live Hub registration (HUB_PROJECTS + HUB_PROJECT_COMPASS_NHS_TOKEN +
hub restart) is a change to a running family service and is pending approval.
Co-Authored-By: Paperclip <noreply@paperclip.ing>
194 lines
8.1 KiB
Markdown
194 lines
8.1 KiB
Markdown
# 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`](AGENTS.md). This repository is at **Phase 8** (the
|
|
> Hub API). See §11 of the spec for the phase order.
|
|
|
|
## 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`](docs/phase-8-hub-notes.md).
|
|
|
|
```bash
|
|
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`](docs/phase-7-notify-notes.md).
|
|
|
|
```bash
|
|
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`](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`.
|
|
|
|
```bash
|
|
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`](docs/phase-5-dashboard-notes.md).
|
|
|
|
```bash
|
|
# 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`](docs/phase-4-both-children-notes.md).
|
|
|
|
```bash
|
|
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`](docs/phase-3-parse-notes.md).
|
|
|
|
```bash
|
|
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): Teams (FR-15) and the Phase 9 documentation set.
|
|
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)
|
|
```
|
|
|
|
## Run
|
|
|
|
```bash
|
|
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:
|
|
|
|
```bash
|
|
docker run --rm --entrypoint /app/school compass-teams-nhs/school:0.6.0-phase8 gen-secret
|
|
docker run --rm --entrypoint /app/school compass-teams-nhs/school:0.6.0-phase8 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
|
|
|
|
```bash
|
|
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.
|