# 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.
