// Package activity reads the autotrack staging log and calendar events for // display in the grid's ACTIVITY column. All parsing is tolerant: a bad line, // missing file, or missing helper never fails the view. package activity import ( "bufio" "encoding/json" "os" "path/filepath" "time" ) // Segment is one recorded activity span from the daily jsonl log. type Segment struct { Start, End time.Time App, Title, URL, Domain string Idle bool } // rawSegment mirrors one jsonl line; timestamps stay strings for tolerant // parsing (secs is derivable and unused by the reader). type rawSegment struct { Start string `json:"start"` End string `json:"end"` App string `json:"app"` Title string `json:"title"` URL string `json:"url"` Domain string `json:"domain"` Idle bool `json:"idle"` } // stampLayout matches the daemon's isoformat(timespec="seconds") local stamps. const stampLayout = "2006-01-02T15:04:05" // LoadDay reads activity-.jsonl under dataDir. Malformed lines are // skipped — one bad row never fails the view. A missing file (or empty // dataDir) yields an empty slice with no error. func LoadDay(dataDir, date string) ([]Segment, error) { if dataDir == "" { return nil, nil } f, err := os.Open(filepath.Join(dataDir, "activity-"+date+".jsonl")) if err != nil { return nil, nil } defer f.Close() var segs []Segment sc := bufio.NewScanner(f) sc.Buffer(make([]byte, 0, 64*1024), 1024*1024) // long window titles/URLs for sc.Scan() { var r rawSegment if err := json.Unmarshal(sc.Bytes(), &r); err != nil { continue } start, err := time.ParseInLocation(stampLayout, r.Start, time.Local) if err != nil { continue } end, err := time.ParseInLocation(stampLayout, r.End, time.Local) if err != nil { continue } segs = append(segs, Segment{ Start: start, End: end, App: r.App, Title: r.Title, URL: r.URL, Domain: r.Domain, Idle: r.Idle, }) } return segs, nil }