feat: add live activity aggregation with today-hourly and last-hour bucketing

Add data layer support for real-time usage visualization:

- MinuteStats type: holds token counts for 5-minute buckets, enabling
  granular recent-activity views (12 buckets covering the last hour).

- AggregateTodayHourly(): computes 24 hourly token buckets for the
  current local day by filtering sessions to today's date boundary and
  slotting each into the correct hour index. Tracks prompts, sessions,
  and total tokens per hour.

- AggregateLastHour(): computes 12 five-minute token buckets for the
  last 60 minutes using reverse-offset bucketing (bucket 11 = most
  recent 5 minutes, bucket 0 = 55-60 minutes ago). Bounds-clamped to
  prevent off-by-one at the edges.

Both functions filter on StartTime locality and skip zero-time sessions,
consistent with existing aggregation patterns in the pipeline package.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
teernisse
2026-02-23 09:33:11 -05:00
parent 35fae37ba4
commit 5b9edc7702
2 changed files with 65 additions and 0 deletions

View File

@@ -92,3 +92,9 @@ type PeriodComparison struct {
Current SummaryStats Current SummaryStats
Previous SummaryStats Previous SummaryStats
} }
// MinuteStats holds token counts for a 5-minute bucket.
type MinuteStats struct {
Minute int // 0-11 (bucket index within the hour)
Tokens int64
}

View File

@@ -271,3 +271,62 @@ func FilterByModel(sessions []model.SessionStats, modelFilter string) []model.Se
func containsIgnoreCase(s, substr string) bool { func containsIgnoreCase(s, substr string) bool {
return strings.Contains(strings.ToLower(s), strings.ToLower(substr)) return strings.Contains(strings.ToLower(s), strings.ToLower(substr))
} }
// AggregateTodayHourly computes 24 hourly token buckets for today (local time).
func AggregateTodayHourly(sessions []model.SessionStats) []model.HourlyStats {
now := time.Now()
todayStart := time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, time.Local)
todayEnd := todayStart.Add(24 * time.Hour)
hours := make([]model.HourlyStats, 24)
for i := range hours {
hours[i].Hour = i
}
for _, s := range sessions {
if s.StartTime.IsZero() {
continue
}
local := s.StartTime.Local()
if local.Before(todayStart) || !local.Before(todayEnd) {
continue
}
h := local.Hour()
hours[h].Prompts += s.UserMessages
hours[h].Sessions++
hours[h].Tokens += s.InputTokens + s.OutputTokens
}
return hours
}
// AggregateLastHour computes 12 five-minute token buckets for the last 60 minutes.
func AggregateLastHour(sessions []model.SessionStats) []model.MinuteStats {
now := time.Now()
hourAgo := now.Add(-1 * time.Hour)
buckets := make([]model.MinuteStats, 12)
for i := range buckets {
buckets[i].Minute = i
}
for _, s := range sessions {
if s.StartTime.IsZero() {
continue
}
local := s.StartTime.Local()
if local.Before(hourAgo) || !local.Before(now) {
continue
}
// Compute which 5-minute bucket (0-11) this falls into
minutesAgo := int(now.Sub(local).Minutes())
bucketIdx := 11 - (minutesAgo / 5) // 11 = most recent, 0 = oldest
if bucketIdx < 0 {
bucketIdx = 0
}
if bucketIdx > 11 {
bucketIdx = 11
}
buckets[bucketIdx].Tokens += s.InputTokens + s.OutputTokens
}
return buckets
}