feat: show available reset credits
This commit is contained in:
@@ -8,6 +8,7 @@ import (
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
@@ -229,6 +230,7 @@ func (a *App) dashboardAPI(w http.ResponseWriter, r *http.Request) {
|
||||
func publicDashboard(d Dashboard) Dashboard {
|
||||
d.Account.Email = nil
|
||||
d.Account.AuthMode = nil
|
||||
d.ResetCredits = nil
|
||||
d.LastError = ""
|
||||
return d
|
||||
}
|
||||
@@ -489,10 +491,12 @@ func (a *App) syncAccount(ctx context.Context, id int64) error {
|
||||
d.Account.AuthMode = &ar.Account.Type
|
||||
}
|
||||
var lr struct {
|
||||
RateLimits *rawLimit `json:"rateLimits"`
|
||||
By map[string]rawLimit `json:"rateLimitsByLimitId"`
|
||||
RateLimits *rawLimit `json:"rateLimits"`
|
||||
By map[string]rawLimit `json:"rateLimitsByLimitId"`
|
||||
ResetCredits *rawResetCredits `json:"rateLimitResetCredits"`
|
||||
}
|
||||
if e := rt.client.Call(ctx, "account/rateLimits/read", map[string]any{}, &lr); e == nil {
|
||||
d.ResetCredits = normalizeResetCredits(lr.ResetCredits)
|
||||
if len(lr.By) > 0 {
|
||||
for _, x := range lr.By {
|
||||
d.Limits = append(d.Limits, flattenLimit(x)...)
|
||||
@@ -605,6 +609,35 @@ type rawLimit struct {
|
||||
Secondary *LimitWindow `json:"secondary"`
|
||||
}
|
||||
|
||||
type rawResetCredits struct {
|
||||
AvailableCount int `json:"availableCount"`
|
||||
Credits []rawResetCredit `json:"credits"`
|
||||
}
|
||||
|
||||
type rawResetCredit struct {
|
||||
ExpiresAt *int64 `json:"expiresAt"`
|
||||
}
|
||||
|
||||
func normalizeResetCredits(raw *rawResetCredits) *ResetCreditsSummary {
|
||||
if raw == nil || raw.AvailableCount <= 0 {
|
||||
return nil
|
||||
}
|
||||
expiresAt := make([]int64, 0, len(raw.Credits))
|
||||
for _, credit := range raw.Credits {
|
||||
if credit.ExpiresAt != nil && *credit.ExpiresAt > 0 {
|
||||
expiresAt = append(expiresAt, *credit.ExpiresAt)
|
||||
}
|
||||
}
|
||||
sort.Slice(expiresAt, func(i, j int) bool { return expiresAt[i] < expiresAt[j] })
|
||||
uniqueExpiresAt := expiresAt[:0]
|
||||
for _, value := range expiresAt {
|
||||
if len(uniqueExpiresAt) == 0 || uniqueExpiresAt[len(uniqueExpiresAt)-1] != value {
|
||||
uniqueExpiresAt = append(uniqueExpiresAt, value)
|
||||
}
|
||||
}
|
||||
return &ResetCreditsSummary{AvailableCount: raw.AvailableCount, ExpiresAt: uniqueExpiresAt}
|
||||
}
|
||||
|
||||
func currentTokenCycle(limits []LimitBucket, usage []UsagePoint, fetchedAt int64) *TokenCycle {
|
||||
current := LimitBucket{}
|
||||
found := false
|
||||
|
||||
@@ -6,6 +6,7 @@ import (
|
||||
"errors"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"reflect"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
@@ -93,12 +94,13 @@ func TestAnonymousOverviewIsReadOnly(t *testing.T) {
|
||||
}
|
||||
a.runtimes[1] = &accountRuntime{
|
||||
dash: Dashboard{
|
||||
AccountID: 1,
|
||||
DisplayName: "默认账号",
|
||||
Account: AccountView{Email: &email, PlanType: &plan, Connected: true},
|
||||
Limits: []LimitBucket{},
|
||||
Usage: []UsagePoint{},
|
||||
FetchedAt: time.Now().Unix(),
|
||||
AccountID: 1,
|
||||
DisplayName: "默认账号",
|
||||
Account: AccountView{Email: &email, PlanType: &plan, Connected: true},
|
||||
Limits: []LimitBucket{},
|
||||
Usage: []UsagePoint{},
|
||||
ResetCredits: &ResetCreditsSummary{AvailableCount: 2, ExpiresAt: []int64{1784246400}},
|
||||
FetchedAt: time.Now().Unix(),
|
||||
},
|
||||
}
|
||||
|
||||
@@ -132,6 +134,9 @@ func TestAnonymousOverviewIsReadOnly(t *testing.T) {
|
||||
if publicDashboardBody.Account.Email != nil || publicDashboardBody.Account.AuthMode != nil {
|
||||
t.Fatalf("anonymous dashboard account = %#v; identity fields were not redacted", publicDashboardBody.Account)
|
||||
}
|
||||
if publicDashboardBody.ResetCredits != nil {
|
||||
t.Fatalf("anonymous dashboard reset credits = %#v; entitlement data was not redacted", publicDashboardBody.ResetCredits)
|
||||
}
|
||||
|
||||
for _, path := range []string{"/api/v1/settings/general", "/api/v1/accounts", "/api/v1/accounts/1/sync"} {
|
||||
recorder := httptest.NewRecorder()
|
||||
@@ -163,6 +168,9 @@ func TestAnonymousOverviewIsReadOnly(t *testing.T) {
|
||||
if privateDashboardBody.Account.Email == nil || *privateDashboardBody.Account.Email != email {
|
||||
t.Fatalf("authenticated dashboard email = %v; want %q", privateDashboardBody.Account.Email, email)
|
||||
}
|
||||
if privateDashboardBody.ResetCredits == nil || privateDashboardBody.ResetCredits.AvailableCount != 2 {
|
||||
t.Fatalf("authenticated dashboard reset credits = %#v; want two credits", privateDashboardBody.ResetCredits)
|
||||
}
|
||||
configRecorder := httptest.NewRecorder()
|
||||
configRequest := httptest.NewRequest(http.MethodPut, "/api/v1/settings/general", strings.NewReader(`{"timezone":"UTC","theme":"system","syncMinutes":5,"retentionDays":90,"beforeMinutes":30,"notifyBefore":true,"notifyAfter":true}`))
|
||||
configRequest.AddCookie(&http.Cookie{Name: "session", Value: session})
|
||||
@@ -320,6 +328,29 @@ func TestCurrentTokenCycleUsesLongestWindowAndFiltersDailyUsage(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestNormalizeResetCreditsKeepsAvailableCountAndUniqueExpiryTimes(t *testing.T) {
|
||||
earlier := int64(1781654400)
|
||||
later := int64(1784246400)
|
||||
credits := normalizeResetCredits(&rawResetCredits{
|
||||
AvailableCount: 3,
|
||||
Credits: []rawResetCredit{
|
||||
{ExpiresAt: &later},
|
||||
{ExpiresAt: nil},
|
||||
{ExpiresAt: &earlier},
|
||||
{ExpiresAt: &later},
|
||||
},
|
||||
})
|
||||
if credits == nil || credits.AvailableCount != 3 {
|
||||
t.Fatalf("reset credits = %#v; want three available credits", credits)
|
||||
}
|
||||
if want := []int64{earlier, later}; !reflect.DeepEqual(credits.ExpiresAt, want) {
|
||||
t.Fatalf("expiry times = %v; want %v", credits.ExpiresAt, want)
|
||||
}
|
||||
if normalizeResetCredits(&rawResetCredits{}) != nil {
|
||||
t.Fatal("zero available credits should be hidden")
|
||||
}
|
||||
}
|
||||
|
||||
func TestCurrentTokenCycleRequiresAValidFutureResetWindow(t *testing.T) {
|
||||
cycle := currentTokenCycle(
|
||||
[]LimitBucket{{WindowDurationMinutes: 0, ResetsAt: 1}},
|
||||
|
||||
@@ -81,17 +81,22 @@ type TokenCycle struct {
|
||||
ResetsAt int64 `json:"resetsAt"`
|
||||
TotalTokens int64 `json:"totalTokens"`
|
||||
}
|
||||
type ResetCreditsSummary struct {
|
||||
AvailableCount int `json:"availableCount"`
|
||||
ExpiresAt []int64 `json:"expiresAt"`
|
||||
}
|
||||
type Dashboard struct {
|
||||
AccountID int64 `json:"accountId"`
|
||||
DisplayName string `json:"displayName"`
|
||||
Account AccountView `json:"account"`
|
||||
Limits []LimitBucket `json:"limits"`
|
||||
Summary UsageSummary `json:"summary"`
|
||||
Usage []UsagePoint `json:"usage"`
|
||||
CurrentCycle *TokenCycle `json:"currentCycle,omitempty"`
|
||||
FetchedAt int64 `json:"fetchedAt"`
|
||||
Stale bool `json:"stale"`
|
||||
LastError string `json:"lastError,omitempty"`
|
||||
AccountID int64 `json:"accountId"`
|
||||
DisplayName string `json:"displayName"`
|
||||
Account AccountView `json:"account"`
|
||||
Limits []LimitBucket `json:"limits"`
|
||||
Summary UsageSummary `json:"summary"`
|
||||
Usage []UsagePoint `json:"usage"`
|
||||
CurrentCycle *TokenCycle `json:"currentCycle,omitempty"`
|
||||
ResetCredits *ResetCreditsSummary `json:"resetCredits,omitempty"`
|
||||
FetchedAt int64 `json:"fetchedAt"`
|
||||
Stale bool `json:"stale"`
|
||||
LastError string `json:"lastError,omitempty"`
|
||||
}
|
||||
|
||||
func defaults() GeneralSettings {
|
||||
|
||||
Reference in New Issue
Block a user