# 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///` (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.