Skip to content

Commit cb8a876

Browse files
committed
feat: MiniMax weekly quota integration across all views
Parse weekly limit fields from MiniMax API (current_weekly_total_count, current_weekly_usage_count, weekly_start_time, weekly_end_time, weekly_remains_time) - only available for accounts purchased from 2026-03-23 onwards. - Add weekly fields to MiniMaxModelRemain, MiniMaxModelQuota types - DB migration adds 7 weekly columns to minimax_model_values - Store persists and reads weekly data alongside interval data - Dashboard shows weekly quota cards (3 per row, 2 rows for 6 quotas) - Menubar shows compact weekly KPIs (Wkly M*, Wkly Img, Wkly Spch) - Usage graphs include weekly series as separate chart lines - Logging history includes weekly columns - Cycle overview cross-quota entries include weekly data - Session detail shows weekly peak, start, delta - Usage insights add 5-Hour vs Weekly ratio insight - Graceful degradation for pre-March-23 accounts (no weekly data)
1 parent 4a6b491 commit cb8a876

File tree

9 files changed

+415
-93
lines changed

9 files changed

+415
-93
lines changed

CLAUDE.md

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,9 +33,12 @@ internal/
3333

3434
## Operations
3535

36+
**Always use `app.sh` for build and test - never run `go build` or `go test` directly.**
37+
3638
```bash
37-
./app.sh --build # Build before running
38-
./app.sh --test # go test -race -cover ./...
39+
./app.sh --build # Build production binary
40+
./app.sh --test # Run all tests with race detection and coverage
41+
./app.sh --smoke # Quick validation: vet + build check + short tests
3942
go test -race ./... && go vet ./... # Pre-commit (mandatory)
4043
```
4144

VERSION

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
2.11.35
1+
2.11.36

internal/api/minimax_types.go

Lines changed: 69 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,13 @@ type MiniMaxModelRemain struct {
2222
CurrentIntervalTotalCount int `json:"current_interval_total_count"`
2323
// Despite the field name, this endpoint returns remaining requests.
2424
CurrentIntervalUsageCount int `json:"current_interval_usage_count"`
25+
26+
// Weekly quota fields - only present for accounts purchased from 2026-03-23 onwards.
27+
CurrentWeeklyTotalCount int `json:"current_weekly_total_count"`
28+
CurrentWeeklyUsageCount int `json:"current_weekly_usage_count"`
29+
WeeklyStartTime interface{} `json:"weekly_start_time"`
30+
WeeklyEndTime interface{} `json:"weekly_end_time"`
31+
WeeklyRemainsTime int64 `json:"weekly_remains_time"`
2532
}
2633

2734
// MiniMaxRemainsResponse is the full API response.
@@ -41,6 +48,17 @@ type MiniMaxModelQuota struct {
4148
WindowStart *time.Time
4249
WindowEnd *time.Time
4350
TimeUntilReset time.Duration
51+
52+
// Weekly quota - zero values when not available (pre-March-23 accounts).
53+
WeeklyTotal int
54+
WeeklyRemain int
55+
WeeklyUsed int
56+
WeeklyUsedPercent float64
57+
WeeklyResetAt *time.Time
58+
WeeklyWindowStart *time.Time
59+
WeeklyWindowEnd *time.Time
60+
WeeklyTimeUntilReset time.Duration
61+
HasWeeklyQuota bool
4462
}
4563

4664
// MiniMaxSnapshot is a point-in-time capture.
@@ -97,15 +115,24 @@ func (s *MiniMaxSnapshot) MergedQuota() *MiniMaxModelQuota {
97115
}
98116
first := s.Models[0]
99117
return &MiniMaxModelQuota{
100-
ModelName: "MiniMax Coding Plan",
101-
Total: first.Total,
102-
Remain: first.Remain,
103-
Used: first.Used,
104-
UsedPercent: first.UsedPercent,
105-
ResetAt: first.ResetAt,
106-
WindowStart: first.WindowStart,
107-
WindowEnd: first.WindowEnd,
108-
TimeUntilReset: first.TimeUntilReset,
118+
ModelName: "MiniMax Coding Plan",
119+
Total: first.Total,
120+
Remain: first.Remain,
121+
Used: first.Used,
122+
UsedPercent: first.UsedPercent,
123+
ResetAt: first.ResetAt,
124+
WindowStart: first.WindowStart,
125+
WindowEnd: first.WindowEnd,
126+
TimeUntilReset: first.TimeUntilReset,
127+
HasWeeklyQuota: first.HasWeeklyQuota,
128+
WeeklyTotal: first.WeeklyTotal,
129+
WeeklyRemain: first.WeeklyRemain,
130+
WeeklyUsed: first.WeeklyUsed,
131+
WeeklyUsedPercent: first.WeeklyUsedPercent,
132+
WeeklyResetAt: first.WeeklyResetAt,
133+
WeeklyWindowStart: first.WeeklyWindowStart,
134+
WeeklyWindowEnd: first.WeeklyWindowEnd,
135+
WeeklyTimeUntilReset: first.WeeklyTimeUntilReset,
109136
}
110137
}
111138

@@ -229,7 +256,7 @@ func (r MiniMaxRemainsResponse) ToSnapshot(capturedAt time.Time) *MiniMaxSnapsho
229256
usedPercent = (float64(used) / float64(total)) * 100
230257
}
231258

232-
snapshot.Models = append(snapshot.Models, MiniMaxModelQuota{
259+
quota := MiniMaxModelQuota{
233260
ModelName: model.ModelName,
234261
Total: total,
235262
Remain: remain,
@@ -239,7 +266,38 @@ func (r MiniMaxRemainsResponse) ToSnapshot(capturedAt time.Time) *MiniMaxSnapsho
239266
WindowStart: windowStart,
240267
WindowEnd: windowEnd,
241268
TimeUntilReset: untilReset,
242-
})
269+
}
270+
271+
// Parse weekly quota fields if present.
272+
if model.CurrentWeeklyTotalCount > 0 || model.CurrentWeeklyUsageCount > 0 {
273+
quota.HasWeeklyQuota = true
274+
quota.WeeklyTotal = model.CurrentWeeklyTotalCount
275+
// Same naming quirk: current_weekly_usage_count is actually remaining.
276+
quota.WeeklyRemain = model.CurrentWeeklyUsageCount
277+
quota.WeeklyUsed = quota.WeeklyTotal - quota.WeeklyRemain
278+
if quota.WeeklyUsed < 0 {
279+
quota.WeeklyUsed = 0
280+
}
281+
if quota.WeeklyTotal > 0 {
282+
quota.WeeklyUsedPercent = (float64(quota.WeeklyUsed) / float64(quota.WeeklyTotal)) * 100
283+
}
284+
quota.WeeklyWindowStart = parseMiniMaxTimestamp(model.WeeklyStartTime)
285+
quota.WeeklyWindowEnd = parseMiniMaxTimestamp(model.WeeklyEndTime)
286+
if model.WeeklyRemainsTime > 0 {
287+
d := time.Duration(model.WeeklyRemainsTime) * time.Millisecond
288+
wr := snapshot.CapturedAt.Add(d)
289+
quota.WeeklyResetAt = &wr
290+
quota.WeeklyTimeUntilReset = d
291+
} else if quota.WeeklyWindowEnd != nil {
292+
quota.WeeklyResetAt = quota.WeeklyWindowEnd
293+
quota.WeeklyTimeUntilReset = quota.WeeklyWindowEnd.Sub(snapshot.CapturedAt)
294+
if quota.WeeklyTimeUntilReset < 0 {
295+
quota.WeeklyTimeUntilReset = 0
296+
}
297+
}
298+
}
299+
300+
snapshot.Models = append(snapshot.Models, quota)
243301
}
244302

245303
if raw, err := json.Marshal(r); err == nil {

internal/store/minimax_store.go

Lines changed: 57 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -66,10 +66,22 @@ func (s *Store) InsertMiniMaxSnapshot(snapshot *api.MiniMaxSnapshot, accountID i
6666
windowEndVal = m.WindowEnd.Format(time.RFC3339Nano)
6767
}
6868

69+
var weeklyResetAtVal, weeklyWindowStartVal, weeklyWindowEndVal interface{}
70+
if m.WeeklyResetAt != nil {
71+
weeklyResetAtVal = m.WeeklyResetAt.Format(time.RFC3339Nano)
72+
}
73+
if m.WeeklyWindowStart != nil {
74+
weeklyWindowStartVal = m.WeeklyWindowStart.Format(time.RFC3339Nano)
75+
}
76+
if m.WeeklyWindowEnd != nil {
77+
weeklyWindowEndVal = m.WeeklyWindowEnd.Format(time.RFC3339Nano)
78+
}
79+
6980
_, err := tx.Exec(
7081
`INSERT INTO minimax_model_values
71-
(snapshot_id, model_name, total, remain, used, used_percent, reset_at, window_start, window_end)
72-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`,
82+
(snapshot_id, model_name, total, remain, used, used_percent, reset_at, window_start, window_end,
83+
weekly_total, weekly_remain, weekly_used, weekly_used_percent, weekly_reset_at, weekly_window_start, weekly_window_end)
84+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
7385
snapshotID,
7486
m.ModelName,
7587
m.Total,
@@ -79,6 +91,13 @@ func (s *Store) InsertMiniMaxSnapshot(snapshot *api.MiniMaxSnapshot, accountID i
7991
resetAtVal,
8092
windowStartVal,
8193
windowEndVal,
94+
m.WeeklyTotal,
95+
m.WeeklyRemain,
96+
m.WeeklyUsed,
97+
m.WeeklyUsedPercent,
98+
weeklyResetAtVal,
99+
weeklyWindowStartVal,
100+
weeklyWindowEndVal,
82101
)
83102
if err != nil {
84103
return 0, fmt.Errorf("failed to insert minimax model value %s: %w", m.ModelName, err)
@@ -181,7 +200,8 @@ func (s *Store) QueryMiniMaxRange(start, end time.Time, accountID int64, limit .
181200

182201
func (s *Store) queryMiniMaxModelValues(snapshotID int64) ([]api.MiniMaxModelQuota, error) {
183202
rows, err := s.db.Query(
184-
`SELECT model_name, total, remain, used, used_percent, reset_at, window_start, window_end
203+
`SELECT model_name, total, remain, used, used_percent, reset_at, window_start, window_end,
204+
weekly_total, weekly_remain, weekly_used, weekly_used_percent, weekly_reset_at, weekly_window_start, weekly_window_end
185205
FROM minimax_model_values WHERE snapshot_id = ? ORDER BY model_name`,
186206
snapshotID,
187207
)
@@ -194,7 +214,9 @@ func (s *Store) queryMiniMaxModelValues(snapshotID int64) ([]api.MiniMaxModelQuo
194214
for rows.Next() {
195215
var m api.MiniMaxModelQuota
196216
var resetAt, windowStart, windowEnd sql.NullString
197-
if err := rows.Scan(&m.ModelName, &m.Total, &m.Remain, &m.Used, &m.UsedPercent, &resetAt, &windowStart, &windowEnd); err != nil {
217+
var weeklyResetAt, weeklyWindowStart, weeklyWindowEnd sql.NullString
218+
if err := rows.Scan(&m.ModelName, &m.Total, &m.Remain, &m.Used, &m.UsedPercent, &resetAt, &windowStart, &windowEnd,
219+
&m.WeeklyTotal, &m.WeeklyRemain, &m.WeeklyUsed, &m.WeeklyUsedPercent, &weeklyResetAt, &weeklyWindowStart, &weeklyWindowEnd); err != nil {
198220
return nil, fmt.Errorf("failed to scan minimax model value: %w", err)
199221
}
200222
if resetAt.Valid {
@@ -213,6 +235,26 @@ func (s *Store) queryMiniMaxModelValues(snapshotID int64) ([]api.MiniMaxModelQuo
213235
t, _ := time.Parse(time.RFC3339Nano, windowEnd.String)
214236
m.WindowEnd = &t
215237
}
238+
// Weekly quota fields.
239+
if m.WeeklyTotal > 0 || m.WeeklyUsed > 0 {
240+
m.HasWeeklyQuota = true
241+
}
242+
if weeklyResetAt.Valid {
243+
t, _ := time.Parse(time.RFC3339Nano, weeklyResetAt.String)
244+
m.WeeklyResetAt = &t
245+
m.WeeklyTimeUntilReset = time.Until(t)
246+
if m.WeeklyTimeUntilReset < 0 {
247+
m.WeeklyTimeUntilReset = 0
248+
}
249+
}
250+
if weeklyWindowStart.Valid {
251+
t, _ := time.Parse(time.RFC3339Nano, weeklyWindowStart.String)
252+
m.WeeklyWindowStart = &t
253+
}
254+
if weeklyWindowEnd.Valid {
255+
t, _ := time.Parse(time.RFC3339Nano, weeklyWindowEnd.String)
256+
m.WeeklyWindowEnd = &t
257+
}
216258
models = append(models, m)
217259
}
218260
return models, rows.Err()
@@ -525,7 +567,7 @@ func (s *Store) minimaxCrossQuotasAt(referenceTime time.Time, accountID int64) (
525567
return nil, nil
526568
}
527569

528-
entries := make([]CrossQuotaEntry, 0, len(snap.Models))
570+
entries := make([]CrossQuotaEntry, 0, len(snap.Models)*2)
529571
for _, model := range snap.Models {
530572
entries = append(entries, CrossQuotaEntry{
531573
Name: model.ModelName,
@@ -535,6 +577,16 @@ func (s *Store) minimaxCrossQuotasAt(referenceTime time.Time, accountID int64) (
535577
StartPercent: 0,
536578
Delta: model.UsedPercent,
537579
})
580+
if model.HasWeeklyQuota && (model.WeeklyTotal > 0 || model.WeeklyUsed > 0) {
581+
entries = append(entries, CrossQuotaEntry{
582+
Name: "weekly_" + model.ModelName,
583+
Value: float64(model.WeeklyUsed),
584+
Limit: float64(model.WeeklyTotal),
585+
Percent: model.WeeklyUsedPercent,
586+
StartPercent: 0,
587+
Delta: model.WeeklyUsedPercent,
588+
})
589+
}
538590
}
539591
sort.Slice(entries, func(i, j int) bool { return entries[i].Name < entries[j].Name })
540592
return entries, nil

internal/store/store.go

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -868,6 +868,25 @@ func (s *Store) migrateSchema() error {
868868
}
869869
}
870870

871+
// Add weekly quota columns to minimax_model_values.
872+
// Only accounts purchased from 2026-03-23 onwards have weekly limits.
873+
for _, col := range []string{
874+
"weekly_total INTEGER NOT NULL DEFAULT 0",
875+
"weekly_remain INTEGER NOT NULL DEFAULT 0",
876+
"weekly_used INTEGER NOT NULL DEFAULT 0",
877+
"weekly_used_percent REAL NOT NULL DEFAULT 0",
878+
"weekly_reset_at TEXT",
879+
"weekly_window_start TEXT",
880+
"weekly_window_end TEXT",
881+
} {
882+
if _, err := s.db.Exec(`ALTER TABLE minimax_model_values ADD COLUMN ` + col); err != nil {
883+
if !strings.Contains(err.Error(), "duplicate column name") &&
884+
!strings.Contains(err.Error(), "no such table") {
885+
return fmt.Errorf("failed to add weekly column to minimax_model_values: %w", err)
886+
}
887+
}
888+
}
889+
871890
return nil
872891
}
873892

0 commit comments

Comments
 (0)