package parse import ( "fmt" "strings" "time" ) // This file parses the per-child day widget that Compass renders on a child's // own pages ("/Records/User.aspx?userId=..." and the Learning Tasks page). The // widget is labelled "Today" and its events are that child's own periods: // // 8:55: 1 - 7SCF - H14LAB - KNI // // The week calendar is school-wide (it carries the eldest child's lesson grid), // so it must never be the source of a child's timetable (AGENTS.md ยง4 FR-16). // This is the per-child source. // dayWidgetBaseHour is unused: the widget's grid start moves with the day's // first event (a page whose first event is 7:45 starts at 07:00, one whose // first event is 8:55 starts at 06:00), so the printed clock is the reliable // source and the grid offset is not. // parseDayWidget returns the periods in one child's day widget. found reports // whether the page carried a day widget at all. func parseDayWidget(child string, page Page, loc *time.Location, ref time.Time) (items []Item, found bool, err error) { root := parseHTML(page.HTML) events := findAll(root, func(n *node) bool { return n.Tag == "div" && n.hasClassPart("day-bd-evt-") && n.hasClassPart("calendardaywidget-") }) if len(events) == 0 { return nil, false, nil } day := ref.In(loc) if day.IsZero() { day = time.Now().In(loc) } date := time.Date(day.Year(), day.Month(), day.Day(), 0, 0, 0, 0, loc) for _, ev := range events { labelSpan := find(ev, func(n *node) bool { return n.hasClass("ext-evt-bd") }) if labelSpan == nil { continue } label := collapse(labelSpan.Text()) if label == "" { continue } cm := clockPrefix.FindStringSubmatch(label) if cm == nil { return nil, true, fmt.Errorf("parse: %s: day-widget event %q has no start time", page.Slug, label) } at, err := dayWidgetTime(cm, date) if err != nil { return nil, true, fmt.Errorf("parse: %s: day-widget event %q: %w", page.Slug, label, err) } title := strings.TrimSpace(cm[3]) if title == "" { title = label } subject, teacher, body := splitEvent(label, title) url := page.URL if url == "" { url = compassHost + "/Organise/Calendar/" } items = append(items, Item{ Child: child, Source: SourceCompass, Type: TypeEvent, Subject: subject, Teacher: teacher, Title: title, Body: body, DueAt: &at, URL: url, SourcePage: page.Slug, ExternalRef: dedupeKey("day-period", at.Format(time.RFC3339), title), Importance: importanceNormal, }) } return items, true, nil } // dayWidgetTime reads an event's start from the printed clock. Compass prints a // 12-hour clock with no AM/PM marker ("1:26" is the afternoon period), so the // school-day convention applies: 1-6 are PM, 7-11 are AM, 12 is noon. func dayWidgetTime(clock []string, date time.Time) (time.Time, error) { hour, minute := atoiSafe(clock[1]), atoiSafe(clock[2]) if hour < 0 || hour > 23 || minute < 0 || minute > 59 { return time.Time{}, fmt.Errorf("bad time %02d:%02d", hour, minute) } if hour >= 1 && hour <= 6 { // 1:26 means 1:26 PM in the Compass day grid hour += 12 } return date.Add(time.Duration(hour)*time.Hour + time.Duration(minute)*time.Minute), nil } // atoiSafe parses a small decimal number, returning -1 when it is not one. func atoiSafe(s string) int { n := 0 if s == "" { return -1 } for _, r := range s { if r < '0' || r > '9' { return -1 } n = n*10 + int(r-'0') } return n }