- README: Phase 9 status, the architecture diagram + map link, and the explicit "Teams (FR-15) is DEFERRED" statement. - docs/architecture.mmd + docs/diagrams/architecture.png (mmdc). - docs/archify/architecture.architecture.json -> docs/architecture-map.html (Archify showcase, 9/9 checks). - docs/outline/: the five AI-context documents (STE style). - docs/vikunja-tasks.json: the live Vikunja board (project 21). Co-Authored-By: Paperclip <noreply@paperclip.ing>
255 lines
11 KiB
Markdown
255 lines
11 KiB
Markdown
# 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`](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`](docs/architecture.mmd) | repo |
|
|
| Rendered diagram | [`docs/diagrams/architecture.png`](docs/diagrams/architecture.png) | repo |
|
|
| Interactive map | [`docs/architecture-map.html`](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/`](docs/outline/) | Outline collection *Compass / NHS (School)* |
|
|
| Tasks | [`docs/vikunja-tasks.json`](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. Important school events go to the Home
|
|
Assistant Local Calendar `calendar.school`. 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
|
|
|
|
```mermaid
|
|
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`](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: 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
|
|
|
|
```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.1-phase8 gen-secret
|
|
docker run --rm --entrypoint /app/school compass-teams-nhs/school:0.6.1-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.
|