Skip to content

Commit 1e8262d

Browse files
committed
Add 30d range and sync health grid
1 parent ca108e2 commit 1e8262d

8 files changed

Lines changed: 31 additions & 12 deletions

File tree

README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -92,7 +92,7 @@ All endpoints are mounted under `<APP_BASE_PATH>/api/v1`. Protected endpoints re
9292
| POST | `/auth/logout` | clear cookie |
9393
| GET | `/status` | drain status (last pop, errors, totals) |
9494
| POST | `/sync` | trigger metadata refresh |
95-
| GET | `/usage/overview` | summary + hourly + daily + 30×96 health grid |
95+
| GET | `/usage/overview` | summary + hourly + daily + range-sized 15-minute health grid |
9696
| GET | `/usage/analysis` | aggregations by API / model / both |
9797
| GET | `/usage/events` | paginated raw events |
9898
| GET | `/usage/events/filters` | distinct models + sources |
@@ -109,7 +109,7 @@ All endpoints are mounted under `<APP_BASE_PATH>/api/v1`. Protected endpoints re
109109
| GET | `/aliases/export` | JSON dump of all aliases |
110110
| POST | `/aliases/import` | bulk merge / replace |
111111

112-
Common query params: `range=all|today|4h|8h|12h|24h|7d|custom`, `start`, `end`, `model` (repeatable), `source` (repeatable), `auth_index`, `result=success|failed`, `page`, `page_size`.
112+
Common query params: `range=all|today|4h|8h|12h|24h|7d|30d|custom`, `start`, `end`, `model` (repeatable), `source` (repeatable), `auth_index`, `result=success|failed`, `page`, `page_size`.
113113

114114
## Architecture
115115

internal/storage/sqlite/usage.go

Lines changed: 17 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -432,7 +432,7 @@ func costFromTotals(model string, input, output, cached int64, prices map[string
432432
return computeCost(model, input, output, cached, prices)
433433
}
434434

435-
// BuildUsageOverview returns the summary, hourly+daily series and a 30×96 health grid.
435+
// BuildUsageOverview returns the summary, hourly+daily series and a range-sized 15-minute health grid.
436436
func (s *Store) BuildUsageOverview(ctx context.Context, f storage.UsageFilter, prices map[string]storage.ModelPriceSetting) (*storage.UsageOverview, error) {
437437
now := time.Now().UTC()
438438

@@ -581,8 +581,16 @@ func (s *Store) bucketSeriesDaily(ctx context.Context, f storage.UsageFilter, no
581581
func (s *Store) healthGrid(ctx context.Context, f storage.UsageFilter, now time.Time) ([][]storage.HealthCell, error) {
582582
healthFilter := f
583583
end := startOfDay(now).Add(24 * time.Hour)
584-
healthFilter.End = end
585-
healthFilter.Start = end.Add(-30 * 24 * time.Hour)
584+
if healthFilter.HasRange() {
585+
healthFilter.Start = startOfDay(healthFilter.Start)
586+
healthFilter.End = startOfDay(healthFilter.End.Add(-time.Nanosecond)).Add(24 * time.Hour)
587+
if healthFilter.End.Sub(healthFilter.Start) > 30*24*time.Hour {
588+
healthFilter.Start = healthFilter.End.Add(-30 * 24 * time.Hour)
589+
}
590+
} else {
591+
healthFilter.End = end
592+
healthFilter.Start = end.Add(-30 * 24 * time.Hour)
593+
}
586594
type row struct {
587595
Bucket string
588596
Total int64
@@ -606,8 +614,12 @@ func (s *Store) healthGrid(ctx context.Context, f storage.UsageFilter, now time.
606614
bucketMap[t.UTC()] = storage.HealthCell{Bucket: t.UTC(), Total: r.Total, Failed: r.Failed}
607615
}
608616

609-
grid := make([][]storage.HealthCell, 30)
610-
for d := 0; d < 30; d++ {
617+
days := int(healthFilter.End.Sub(healthFilter.Start).Hours() / 24)
618+
if days < 1 {
619+
days = 1
620+
}
621+
grid := make([][]storage.HealthCell, days)
622+
for d := 0; d < days; d++ {
611623
row := make([]storage.HealthCell, 96)
612624
dayStart := healthFilter.Start.Add(time.Duration(d) * 24 * time.Hour)
613625
for c := 0; c < 96; c++ {

internal/storage/types.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -91,7 +91,7 @@ type APIKeyOverview struct {
9191

9292
// UsageFilter parameterizes all aggregation/listing queries.
9393
type UsageFilter struct {
94-
Range string // all | today | 4h | 8h | 12h | 24h | 7d | custom
94+
Range string // all | today | 4h | 8h | 12h | 24h | 7d | 30d | custom
9595
Start time.Time
9696
End time.Time
9797
Models []string
@@ -226,7 +226,7 @@ type UsageBucket struct {
226226
Cost float64 `json:"cost"`
227227
}
228228

229-
// HealthCell is one cell of the 30-day health heatmap (15-minute spans by default).
229+
// HealthCell is one cell of the range-sized health heatmap (15-minute spans by default).
230230
type HealthCell struct {
231231
Bucket time.Time `json:"bucket"`
232232
Total int64 `json:"total"`

internal/usage/service.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,10 @@ func ParseFilter(rangeKey string, startStr, endStr string, models, sources, apiK
9595
end := startOfDay(now).Add(24 * time.Hour)
9696
f.End = end
9797
f.Start = end.Add(-7 * 24 * time.Hour)
98+
case "30d":
99+
end := startOfDay(now).Add(24 * time.Hour)
100+
f.End = end
101+
f.Start = end.Add(-30 * 24 * time.Hour)
98102
case "custom":
99103
if strings.TrimSpace(startStr) == "" || strings.TrimSpace(endStr) == "" {
100104
return f, errors.New("custom range requires start and end")

web/src/api/types.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ export type RangeKey =
1414
| "12h"
1515
| "24h"
1616
| "7d"
17+
| "30d"
1718
| "custom";
1819

1920
export type ResultFilter = "" | "success" | "failed";

web/src/components/FilterBar.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ const RANGE_PRESETS: { key: RangeKey; label: string }[] = [
1010
{ key: "12h", label: "12h" },
1111
{ key: "24h", label: "24h" },
1212
{ key: "7d", label: "7d" },
13+
{ key: "30d", label: "30d" },
1314
{ key: "all", label: "All" },
1415
{ key: "custom", label: "Custom" },
1516
];

web/src/components/HealthGrid.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,11 +33,12 @@ export default function HealthGrid({ grid }: Props) {
3333
hours: hourlyCells(day),
3434
}));
3535
const maxTotal = Math.max(0, ...days.flatMap((day) => day.hours.map((cell) => cell.total)));
36+
const title = `${days.length}-day request health by hour`;
3637

3738
return (
3839
<div className="bg-panel border border-border rounded-lg p-4">
3940
<div className="flex items-center justify-between mb-3">
40-
<h3 className="text-sm font-medium">30-day request health by hour</h3>
41+
<h3 className="text-sm font-medium">{title}</h3>
4142
<Legend />
4243
</div>
4344
<div className="overflow-x-auto pb-1">

web/src/pages/Overview.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ export default function Overview() {
3737
}, [filter, tick]);
3838

3939
const summary = data?.summary;
40-
const useDaily = filter.range === "7d" || filter.range === "all";
40+
const useDaily = filter.range === "7d" || filter.range === "30d" || filter.range === "all";
4141
const series = useDaily ? data?.daily_series || [] : data?.hourly_series || [];
4242

4343
return (
@@ -104,7 +104,7 @@ export default function Overview() {
104104
</div>
105105

106106
<div>
107-
<h2 className="text-sm uppercase tracking-wider text-muted mb-2">30-day health</h2>
107+
<h2 className="text-sm uppercase tracking-wider text-muted mb-2">Health</h2>
108108
{data && <HealthGrid grid={data.health_grid || []} />}
109109
</div>
110110
</div>

0 commit comments

Comments
 (0)