Files
Sam Rolfe 207a4978a9 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>
2026-10-15 17:40:52 +11:00

91 lines
2.4 KiB
Go

package main
import (
"context"
"flag"
"fmt"
"os"
"gitea.lab.audasmedia.com.au/sam/compass_teams_nhs/internal/config"
"gitea.lab.audasmedia.com.au/sam/compass_teams_nhs/internal/store"
)
// scanCommand is the `school scan` entry point. It only records the outcome of a
// scan; the scan itself runs the patchright fetcher on the host and then
// `school parse` (AGENTS.md §11 Phase 4).
//
// `school scan record` exists so a fetch failure is written to the append-only
// `scans` table with ok = 0 (FR-12/FR-13): the dashboard can then show an
// explicit failure instead of an empty day.
func scanCommand(args []string) {
if len(args) == 0 || args[0] != "record" {
fmt.Fprintln(os.Stderr, "usage: school scan record --child NAME [--ok] [--pages N] [--items N] --message TEXT")
os.Exit(2)
}
fs := flag.NewFlagSet("scan record", flag.ContinueOnError)
var (
child string
pages int
items int
message string
ok bool
)
fs.StringVar(&child, "child", "", "child name (Harry or Finn)")
fs.BoolVar(&ok, "ok", false, "true when the scan succeeded")
fs.IntVar(&pages, "pages", 0, "pages fetched")
fs.IntVar(&items, "items", 0, "items found")
fs.StringVar(&message, "message", "", "failure message (required when --ok=false)")
_ = fs.Parse(args[1:])
if child == "" {
fmt.Fprintln(os.Stderr, "school scan record: --child is required")
os.Exit(2)
}
if !ok && message == "" {
fmt.Fprintln(os.Stderr, "school scan record: --message is required for a failed scan")
os.Exit(2)
}
if err := runScanRecord(child, pages, items, ok, message); err != nil {
fmt.Fprintln(os.Stderr, "school scan:", err)
os.Exit(1)
}
}
func runScanRecord(child string, pages, items int, ok bool, message string) error {
ctx := context.Background()
cfg, err := config.LoadDB()
if err != nil {
return err
}
st, err := store.Open(cfg.DBPath)
if err != nil {
return err
}
defer st.Close()
if err := st.Migrate(ctx); err != nil {
return err
}
if err := st.SeedChildren(ctx, seedChildren...); err != nil {
return err
}
c, err := st.ChildByName(ctx, child)
if err != nil {
return err
}
if c == nil {
return fmt.Errorf("unknown child %q", child)
}
id, err := st.RecordScan(ctx, &c.ID, pages, items, ok, message)
if err != nil {
return err
}
status := "ok"
if !ok {
status = "failed"
}
fmt.Printf("scan=%d child=%s status=%s pages=%d items=%d\n", id, c.Name, status, pages, items)
return nil
}