// Package stats implements the read-only /api/v1/stats aggregation API. It // bundles six queries into one round-trip and fills deterministic time windows // (daily, heatmap, hour-of-day) so missing slots appear as zeros on the wire. package stats import "time" // DailyWindow returns days+1 unix seconds (at UTC midnight), oldest first, // ending today. Example: days=7 returns 8 entries — today plus the 7 prior // days. Each pair is exactly 86400 seconds apart. // // The boundary rule: time.Unix(now,0).UTC().Truncate(24h) gives today's // midnight in UTC. func DailyWindow(now int64, days int) []int64 { today := time.Unix(now, 0).UTC().Truncate(24 * time.Hour).Unix() out := make([]int64, days+1) for i := 0; i <= days; i++ { // oldest first: index 0 is `days` days before today out[i] = today - int64(days-i)*86400 } return out } // FillDaily left-joins rows onto the window. Each window slot appears exactly // once in the output (oldest first). Missing slots get PerTool: map[string]int64{} // (empty map, not nil — so JSON encodes as {}). Rows whose DateUnix does not // match any window slot are silently dropped (they are outside the range). func FillDaily(window []int64, rows []DailyBucket) []DailyBucket { // Build a lookup map from the rows. rowByDate := make(map[int64]DailyBucket, len(rows)) for _, r := range rows { rowByDate[r.DateUnix] = r } out := make([]DailyBucket, len(window)) for i, ts := range window { if row, ok := rowByDate[ts]; ok { out[i] = row } else { out[i] = DailyBucket{DateUnix: ts, PerTool: map[string]int64{}} } } return out } // HeatmapWindow returns 84 unix seconds (at UTC midnight), oldest first, // ending today — 12 weeks × 7 days. The length is fixed regardless of the // request's range. func HeatmapWindow(now int64) []int64 { const cells = 84 // 12 weeks × 7 days today := time.Unix(now, 0).UTC().Truncate(24 * time.Hour).Unix() out := make([]int64, cells) for i := 0; i < cells; i++ { out[i] = today - int64(cells-1-i)*86400 } return out } // HourWindow returns 24 zero-initialised HourBucket values, one per hour // (Hour 0..23), in ascending order. func HourWindow() []HourBucket { out := make([]HourBucket, 24) for i := range out { out[i] = HourBucket{Hour: i, Count: 0} } return out }