Phase 4: per-child sessions, scan both children, items separated

Fetch and parse Harry and Finn with a stored Compass session per child and
store every item against the right children.id.

- cmd/school/session.go + internal/store/session.go: read and write the
  per-child harness session state in the SQLite `session` table. `show`
  prints metadata only; the state is never printed.
- scripts/compass-fetch-patchright.mjs: reuse the stored session, refresh it
  after login, resolve the child's userId from the parent page by name, and
  keep the shared calendar out of the per-child run (it would leak the
  eldest's timetable into the younger child).
- scripts/scan-compass.sh + make scan: scan one child or both; restore the
  session, fetch, persist the session, then parse. A failing child is
  recorded in `scans` with ok = 0 and the script exits non-zero (FR-12).
- cmd/school/scan.go: `school scan record` writes a fetch failure to the
  scans table. cmd/school/items.go: `school items purge` removes rows that a
  shared page stored against the wrong child.
- internal/store/store_test.go: prove items and sessions are separated by
  child. docs/phase-4-both-children-notes.md; README + image tag 0.3.0-phase4.

Live evidence: Harry 70 tasks / 1 message / 35 events (6 overdue); Finn
42 tasks / 1 message / 0 events (2 overdue). Finn's subjects are Year 7,
Harry's are Year 11. Both sessions stored.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
2026-10-15 17:40:52 +11:00
parent 5d263987dd
commit 207a4978a9
12 changed files with 964 additions and 14 deletions

View File

@@ -0,0 +1,93 @@
# Phase 4 — both children, per-child sessions
Status: **done.** The Compass scan now fetches and parses **Harry and Finn**
with a stored session per child, and every item is stored against the right
`children.id`. See AUD-108.
## What runs
```bash
# Both children, with the stored session per child.
make scan
# One child only (a scan can target one child).
make scan SCAN_CHILD=Finn
# Same thing without make.
scripts/scan-compass.sh
scripts/scan-compass.sh --child Harry --child Finn --classify none
```
`scripts/scan-compass.sh` runs the patchright fetcher on the host and the
`school` subcommands in a one-off container. It never restarts the service.
## Per-child session (FR-2, §7 `session` table)
The Compass login is one parent account, but each child gets its own browser
session state so a scan is independent and targetable.
| Step | Command | Notes |
|---|---|---|
| Restore | `school session get --child NAME --out FILE` | Reads `session.state`; writes a 0600 file. No row means "log in fresh". |
| Fetch | `node scripts/compass-fetch-patchright.mjs --child NAME --state FILE` | Loads the state; logs in only when Compass rejects it; saves the refreshed state. |
| Persist | `school session put --child NAME --file FILE` | Upserts `session.state` for that child. |
| Inspect | `school session show [--child NAME] [--json]` | Prints bytes, cookie and origin counts, and `updated_at`. **Never the state.** |
The session state is a secret. It lives in the database volume only. No command
prints it, and the handoff file exists only inside the run's scratch directory.
## What each run stores
One run directory per child under `var/raw/<child>/<runId>/` (git-ignored):
| Page | Source | Becomes |
|---|---|---|
| `home` | parent landing page | `message` rows (news feed) |
| `learning-tasks-all` | `POST /Services/LearningTasks.svc/GetAllLearningTasksByUserId` | `task` rows (authoritative per-child list) |
| `child-profile` | `/Records/User.aspx?userId=N` | no grid; used only when it carries the legacy grid |
| `learning-tasks` | `/Records/UserNew.aspx?userId=N#learningTasks` | `task` rows (legacy grid fallback) |
The per-child Compass `userId` is discovered from the parent landing page by
child name (`Harrison` → Harry, `Finnegan` → Finn) and recorded in the manifest.
## Why the calendar is not fetched per child
The parent calendar page renders only the calendars that the parent has enabled.
In this account only Harry's calendar is enabled; Finn's is hidden. Fetching that
page inside Finn's run therefore stored Harry's Year 11 timetable against Finn —
a leak between children.
Until per-child calendar attribution exists, the shared calendar stays out of the
per-child loop. Harry's events remain from Phase 3. A caller can still fetch it
explicitly with the fetcher's `--pages` option. The `school items purge` command
removes rows that a shared page stored against the wrong child.
## Fail loudly (FR-12)
- A fetch failure records `scans.ok = 0` with the message through
`school scan record --child NAME --ok=false --message TEXT`.
- A parse failure records its own failed scan inside `school parse`.
- The scan script finishes every child, prints each failure by name, and exits
non-zero. A failed child is never shown as an empty day.
## Evidence (2026-10-15, live)
```bash
scripts/scan-compass.sh --classify auto
```
| Child | tasks | messages | events | overdue (home-page recon.) |
|---|---|---|---|---|
| Harry | 70 | 1 | 35 | 6 |
| Finn | 42 | 1 | 0 | 2 |
Rows are separated by `child_id`. A check for items with no matching child
returns 0. Finn's task subjects are all Year 7 (`7MA`, `7DRA`, `7DES`); Harry's
are all Year 11 (`11MGMA`, `11HLEG`, `11EENG`).
## Known gaps
- `children.compass_id` is not populated yet. The manifest holds the `userId` per
run; a later phase can copy it into the `children` table.
- Per-child calendar attribution is deferred (see above).
- Teams (FR-15) is a separate source and is not scanned here.