Phase 6b/7b: per-child decoded periods, separate brief blocks, idempotent calendar (AUD-119)
- Parse each child's own "Today" day widget for the timetable (FR-16); drop the school-wide week calendar's lesson grid and clean the leaked routine rows. - Add the editable codes map (FR-17): subjects, rooms, teachers and period labels, seeded, an unknown code falls back to the raw code; `school codes` CLI. - Brief: Finn's block first, then Harry's, four sections always present (FR-9); periods decoded into plain English; the undated tail is one counted line (FR-18). - Calendar: write periods + work due + work overdue + major events, per child, on every pass. Identity is the stable per-item key, so a re-run never re-inserts (FR-8). morning-brief.sh now runs scan -> calendar push -> notify. - Day-widget labels are 12-hour: 1-6 read as PM, fixing afternoon periods. Proven live on .13: image 0.6.2-phase6b7b; both children's briefs decoded and separate; 35 events in calendar.school; runs 2 and 3 of the scan added nothing. Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
@@ -10,6 +10,7 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"gitea.lab.audasmedia.com.au/sam/compass_teams_nhs/internal/period"
|
||||
"gitea.lab.audasmedia.com.au/sam/compass_teams_nhs/internal/store"
|
||||
)
|
||||
|
||||
@@ -26,6 +27,13 @@ type Buckets struct {
|
||||
// so the summary can count them and nothing is silently dropped.
|
||||
Later []store.Item
|
||||
Done []store.Item
|
||||
// PeriodsToday is the child's own timetable for today (FR-16/FR-17).
|
||||
PeriodsToday []store.Item
|
||||
// EventsToday is today's major school events (concerts, excursions, exams).
|
||||
EventsToday []store.Item
|
||||
// FutureWork is the outstanding work due today or later. The morning brief
|
||||
// lists it under "DUE IN FUTURE" (FR-9).
|
||||
FutureWork []store.Item
|
||||
}
|
||||
|
||||
// Actionable is the number of task items that still need doing.
|
||||
@@ -78,6 +86,11 @@ func Classify(items []store.Item, now time.Time, loc *time.Location) Buckets {
|
||||
switch {
|
||||
case !due.Before(startToday) && due.Before(endToday):
|
||||
b.DueToday = append(b.DueToday, it)
|
||||
if isPeriodItem(it) {
|
||||
b.PeriodsToday = append(b.PeriodsToday, it)
|
||||
} else {
|
||||
b.EventsToday = append(b.EventsToday, it)
|
||||
}
|
||||
case !due.Before(endToday) && due.Before(endWeek):
|
||||
b.DueWeek = append(b.DueWeek, it)
|
||||
case !due.Before(endWeek):
|
||||
@@ -106,14 +119,17 @@ func Classify(items []store.Item, now time.Time, loc *time.Location) Buckets {
|
||||
b.Overdue = append(b.Overdue, it)
|
||||
case due.Before(endToday):
|
||||
b.DueToday = append(b.DueToday, it)
|
||||
b.FutureWork = append(b.FutureWork, it)
|
||||
case due.Before(endWeek):
|
||||
b.DueWeek = append(b.DueWeek, it)
|
||||
b.FutureWork = append(b.FutureWork, it)
|
||||
default:
|
||||
b.Later = append(b.Later, it)
|
||||
b.FutureWork = append(b.FutureWork, it)
|
||||
}
|
||||
}
|
||||
|
||||
for _, s := range [][]store.Item{b.Overdue, b.DueToday, b.DueWeek, b.NoDue, b.Later, b.Done} {
|
||||
for _, s := range [][]store.Item{b.Overdue, b.DueToday, b.DueWeek, b.NoDue, b.Later, b.Done, b.PeriodsToday, b.EventsToday, b.FutureWork} {
|
||||
sort.SliceStable(s, func(i, j int) bool { return less(s[i], s[j]) })
|
||||
}
|
||||
sort.SliceStable(b.Messages, func(i, j int) bool {
|
||||
@@ -171,8 +187,20 @@ var submittedStatuses = map[string]bool{
|
||||
"completed": true,
|
||||
}
|
||||
|
||||
// IsSubmittedStatus reports whether a task status means the work no longer
|
||||
// needs doing, so it is excluded from the calendar's work lists.
|
||||
func IsSubmittedStatus(status string) bool { return isSubmitted(status) }
|
||||
|
||||
func isSubmitted(status string) bool { return submittedStatuses[status] }
|
||||
|
||||
// isPeriodItem reports whether an event is a routine class period from the
|
||||
// child's own timetable rather than a school event. Compass writes a lesson as
|
||||
// "1 - 7SCF - H14LAB - KNI" (FR-16/FR-17).
|
||||
func isPeriodItem(it store.Item) bool {
|
||||
parts := period.SplitDash(it.Title)
|
||||
return len(parts) > 0 && period.IsPeriodLabel(parts[0])
|
||||
}
|
||||
|
||||
func startOfDay(t time.Time, loc *time.Location) time.Time {
|
||||
t = t.In(loc)
|
||||
return time.Date(t.Year(), t.Month(), t.Day(), 0, 0, 0, 0, loc)
|
||||
@@ -363,3 +391,113 @@ func line(it store.Item, loc *time.Location) string {
|
||||
}
|
||||
return title + due + mark
|
||||
}
|
||||
|
||||
// CombinedBrief is one child's block of the combined brief, in the required
|
||||
// order: Finn's block first, then Harry's (FR-9).
|
||||
type CombinedBrief struct {
|
||||
Child string
|
||||
Buckets Buckets
|
||||
}
|
||||
|
||||
// RenderBrief writes one child's block in the required Phase 7 shape (FR-9,
|
||||
// FR-16, FR-17, FR-18):
|
||||
//
|
||||
// === FINN ===
|
||||
// PERIODS TODAY
|
||||
// EVENTS TODAY
|
||||
// OVERDUE
|
||||
// DUE IN FUTURE
|
||||
//
|
||||
// An empty section is printed with "none"; it is never omitted silently.
|
||||
// decode turns a raw lesson title into plain English (FR-17); pass nil to
|
||||
// render the raw codes.
|
||||
func RenderBrief(child string, b Buckets, decode func(string) string, loc *time.Location) string {
|
||||
if loc == nil {
|
||||
loc = time.UTC
|
||||
}
|
||||
var sb strings.Builder
|
||||
fmt.Fprintf(&sb, "=== %s ===\n", strings.ToUpper(strings.TrimSpace(child)))
|
||||
|
||||
writeBriefPeriods(&sb, b.PeriodsToday, decode, loc)
|
||||
writeBriefEvents(&sb, b.EventsToday, loc)
|
||||
writeBriefWork(&sb, "OVERDUE", b.Overdue, loc)
|
||||
writeBriefFuture(&sb, b, loc)
|
||||
return sb.String()
|
||||
}
|
||||
|
||||
// RenderCombinedBrief writes the single brief for Jo and Sam (FR-14) as two
|
||||
// separate blocks — Finn first, then Harry — and never interleaves them (FR-9).
|
||||
func RenderCombinedBrief(parts []CombinedBrief, decode func(string) string, loc *time.Location) string {
|
||||
blocks := make([]string, 0, len(parts))
|
||||
for _, p := range parts {
|
||||
blocks = append(blocks, RenderBrief(p.Child, p.Buckets, decode, loc))
|
||||
}
|
||||
return strings.Join(blocks, "\n")
|
||||
}
|
||||
|
||||
// writeBriefPeriods lists the child's own periods for today, decoded (FR-17).
|
||||
func writeBriefPeriods(sb *strings.Builder, items []store.Item, decode func(string) string, loc *time.Location) {
|
||||
sb.WriteString("\nPERIODS TODAY\n")
|
||||
if len(items) == 0 {
|
||||
sb.WriteString("- none\n")
|
||||
return
|
||||
}
|
||||
for _, it := range items {
|
||||
title := strings.TrimSpace(it.Title)
|
||||
if decode != nil {
|
||||
title = decode(title)
|
||||
}
|
||||
when := ""
|
||||
if it.DueAt != nil {
|
||||
when = " (" + it.DueAt.In(loc).Format("15:04") + ")"
|
||||
}
|
||||
fmt.Fprintf(sb, "- %s%s\n", title, when)
|
||||
}
|
||||
}
|
||||
|
||||
// writeBriefEvents lists today's major school events (FR-9b).
|
||||
func writeBriefEvents(sb *strings.Builder, items []store.Item, loc *time.Location) {
|
||||
sb.WriteString("\nEVENTS TODAY\n")
|
||||
if len(items) == 0 {
|
||||
sb.WriteString("- none\n")
|
||||
return
|
||||
}
|
||||
for _, it := range items {
|
||||
when := ""
|
||||
if it.DueAt != nil {
|
||||
when = " (" + it.DueAt.In(loc).Format("15:04") + ")"
|
||||
}
|
||||
fmt.Fprintf(sb, "- %s%s\n", strings.TrimSpace(it.Title), when)
|
||||
}
|
||||
}
|
||||
|
||||
// writeBriefWork lists outstanding work (FR-9c).
|
||||
func writeBriefWork(sb *strings.Builder, title string, items []store.Item, loc *time.Location) {
|
||||
fmt.Fprintf(sb, "\n%s\n", title)
|
||||
if len(items) == 0 {
|
||||
sb.WriteString("- none\n")
|
||||
return
|
||||
}
|
||||
for _, it := range items {
|
||||
fmt.Fprintf(sb, "- %s\n", line(it, loc))
|
||||
}
|
||||
}
|
||||
|
||||
// writeBriefFuture lists work due today or later (FR-9d). The undated tail is
|
||||
// never dumped: it is one line with a count and a pointer to the dashboard
|
||||
// (FR-18).
|
||||
func writeBriefFuture(sb *strings.Builder, b Buckets, loc *time.Location) {
|
||||
sb.WriteString("\nDUE IN FUTURE\n")
|
||||
wrote := false
|
||||
for _, it := range b.FutureWork {
|
||||
fmt.Fprintf(sb, "- %s\n", line(it, loc))
|
||||
wrote = true
|
||||
}
|
||||
if n := len(b.NoDue); n > 0 {
|
||||
fmt.Fprintf(sb, "- %d task(s) with no due date (see the dashboard)\n", n)
|
||||
wrote = true
|
||||
}
|
||||
if !wrote {
|
||||
sb.WriteString("- none\n")
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user