Files
compass_teams_nhs/docs/phase-4a-overdue-tasks.md
Sam Rolfe 5d263987dd Phase 4a: fetch and parse the authoritative Compass learning tasks
The Learning Tasks grid is capped at 20 rows and, on the parent page, mixes
both children, so it cannot answer "what is overdue". The parent home page
reported 6 overdue tasks for Harry while the stored grid held 1.

Fetch the per-child task list (read-only) from
POST /Services/LearningTasks.svc/GetAllLearningTasksByUserId, parse it into
task rows, and refuse to store it when the overdue count disagrees with the
home page summary (FR-12).

- fetcher: store learning-tasks-all.json + meta (userId, status, task count)
- parse: new learning-tasks-all page, JSON parser, home-page reconciliation
- store: drop superseded grid-derived task rows before the upsert
- docs: docs/phase-4a-overdue-tasks.md; close the Phase 3 known gap

Verified with live runs: Harry 70 tasks / 6 overdue, Finn 42 tasks / 2 overdue,
both equal to the home page; a tampered home page stops the parse.

Note: the fetcher and store.go files also carry the per-child session work
that was already uncommitted in this shared workspace (AUD-108).

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-10-15 17:39:11 +11:00

113 lines
4.2 KiB
Markdown

# Phase 4a — the overdue-filtered learning-tasks view
Status: **done.** The authoritative per-child learning-task list is fetched and
parsed, and the parser fails loudly when it disagrees with the home page. See
AUD-114.
## Problem
The Learning Tasks grid is capped at the most recent 20 records
(`Compass.attributes.LearningTask_Default_Records_Count = 20`). On the parent
account the same grid mixes both children. The parent landing page reported
**6 overdue learning tasks** for Harry, but the stored grid held 1 `Overdue` row
for him. The dashboard's "overdue" column cannot be built from that page.
## The endpoint
The grid's ExtJS store reads one service:
```
POST /Services/LearningTasks.svc/GetAllLearningTasksByUserId
Content-Type: application/json
{ "userId": 27038, "page": 1, "start": 0, "limit": 1000 }
```
- **No `?sessionstate=readonly` query.** Adding it makes the service answer 500.
- The request must run **inside the logged-in page** (`fetch`, `credentials:
include`). A request from the browser context's own API client is answered 403
by the Compass request capture.
- The response is `{"d":{"data":[...],"total":n}}`. `total` is the number of
tasks for that child across the current and past enrolments.
The service returns **every** learning task for the child, with the submission
status, so the overdue count can be checked against the home page.
## Status codes
`Compass.enums.LearningTasksSubmissionStatus`:
| Code | Label |
|---|---|
| 1 | Pending |
| 2 | Overdue |
| 3 | On Time |
| 4 | Received Late |
`LearningTasks_OverdueShowAsPending = false` on the parent account, so an
`Overdue` row stays `Overdue`.
## Due dates
Most tasks carry `dueDateTimestamp` at the task level (`2026-10-09T05:00:00Z`,
UTC). A few carry it on the student row instead, so the parser reads both and
falls back to the student value.
## Fetcher
`scripts/compass-fetch-patchright.mjs` stores the response as
`learning-tasks-all.json` plus `learning-tasks-all.meta.json`. The meta records
the `userId`, the HTTP status, the byte count, the SHA-256, and the task count,
so the Go parser can tie the list to the child and to the home page. A non-200
response or a non-JSON body is an explicit fetch error (FR-12).
```bash
cd scripts && npm install
COMPASS_USERNAME=... COMPASS_PASSWORD=... \
node scripts/compass-fetch-patchright.mjs --child Harry --user-id 27038
```
Read-only against the school.
## Parser
`internal/parse/tasks_json.go` turns the JSON into `task` rows:
- one row per task for the child's `userId` (a sibling's rows are skipped),
- `external_ref = learning-task|<taskId>` (stable; the task id is unique),
- the submission status and due date are written into the item body, so the
offline importance rule marks an overdue task `high`.
When a run holds `learning-tasks-all`, the parser **ignores the legacy HTML
grid** (`learning-tasks` and `child-profile`): the grid is capped and would store
the same task a second time. `school parse` then deletes the child's old
grid-derived task rows before the upsert.
## Fail loudly (FR-12)
`parse.Parse` reads the child's overdue count from the home page (the
`N overdue learning tasks` link that carries the child's `userId`) and compares
it with the number of `Overdue` rows in the JSON list.
- They must be equal. A disagreement stops the parse, records a failed scan
(`ok = 0`), and stores nothing.
- A home page with no record of the child also stops the parse; a missing
summary is never read as zero.
## Verification (2026-10-15)
| Child | Compass id | Tasks | Overdue | Home page |
|---|---|---|---|---|
| Harry | 27038 | 70 | 6 | 6 overdue |
| Finn | 31460 | 42 | 2 | 2 overdue |
- `school parse --child Harry --classify none` →
`overdue 6 (reconciled against the home page)`, `Harry task 70`.
- `school parse --child Finn --classify none` →
`overdue 2 (reconciled against the home page)`, `Finn task 42`.
- Tampered home page (6 → 5): `parse: Harry: home page reports 5 overdue
learning task(s) but the stored learning-task list has 6; refusing to store a
partial list (FR-12)` and exit 1.
Raw pages stay under `var/raw/<child>/<runId>/` and are git-ignored. The service
response holds the family's personal data; it is never committed.