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:
@@ -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
|
||||||
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user