Skip to content

Commit 17a3883

Browse files
committed
fix: improve quota and timestamp handling
1 parent a685b45 commit 17a3883

6 files changed

Lines changed: 88 additions & 15 deletions

File tree

src/components/quota/quotaConfigs.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -307,6 +307,7 @@ const normalizeClaudePlanTypeFromCache = (value: unknown): string | null => {
307307
if (!normalized) return null;
308308
if (normalized === 'max' || normalized === 'plan_max') return 'plan_max';
309309
if (normalized === 'pro' || normalized === 'plan_pro') return 'plan_pro';
310+
if (normalized === 'team' || normalized === 'plan_team') return 'plan_team';
310311
if (normalized === 'free' || normalized === 'plan_free') return 'plan_free';
311312
return normalized.startsWith('plan_') ? normalized : null;
312313
};
@@ -1310,6 +1311,13 @@ const resolveClaudePlanType = (profile: ClaudeProfileResponse | null): string |
13101311
const hasClaudePro = normalizeFlagValue(profile.account?.has_claude_pro);
13111312
if (hasClaudePro) return 'plan_pro';
13121313

1314+
const organizationType = normalizeStringValue(profile.organization?.organization_type)?.toLowerCase();
1315+
const subscriptionStatus = normalizeStringValue(profile.organization?.subscription_status)?.toLowerCase();
1316+
1317+
if (organizationType === 'claude_team' && subscriptionStatus === 'active') {
1318+
return 'plan_team';
1319+
}
1320+
13131321
if (hasClaudeMax === false && hasClaudePro === false) return 'plan_free';
13141322

13151323
return null;

src/components/usage/RequestEventsDetailsCard.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import type { GeminiKeyConfig, ProviderKeyConfig, OpenAIProviderConfig } from '@
99
import type { AuthFileItem } from '@/types/authFile';
1010
import type { CredentialInfo } from '@/types/sourceInfo';
1111
import { buildSourceInfoMap, resolveSourceDisplay } from '@/utils/sourceResolver';
12+
import { parseTimestampMs } from '@/utils/timestamp';
1213
import {
1314
collectUsageDetails,
1415
extractLatencyMs,
@@ -188,7 +189,7 @@ export function RequestEventsDetailsCard({
188189
const timestampMs =
189190
typeof detail.__timestampMs === 'number' && detail.__timestampMs > 0
190191
? detail.__timestampMs
191-
: Date.parse(timestamp);
192+
: parseTimestampMs(timestamp);
192193
const date = Number.isNaN(timestampMs) ? null : new Date(timestampMs);
193194
const sourceRaw = String(detail.source ?? '').trim();
194195
const authIndexRaw = detail.auth_index as unknown;

src/pages/hooks/useTraceResolver.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { USAGE_STATS_STALE_TIME_MS, useUsageStatsStore } from '@/stores';
55
import type { AuthFileItem, Config } from '@/types';
66
import type { CredentialInfo, SourceInfo } from '@/types/sourceInfo';
77
import { buildSourceInfoMap, resolveSourceDisplay } from '@/utils/sourceResolver';
8+
import { parseTimestampMs } from '@/utils/timestamp';
89
import {
910
collectUsageDetailsWithEndpoint,
1011
normalizeAuthIndex,
@@ -183,7 +184,7 @@ export function useTraceResolver(options: UseTraceResolverOptions): UseTraceReso
183184
if (!logPath) return [];
184185

185186
const logTimestampMs = traceLogLine.timestamp
186-
? Date.parse(traceLogLine.timestamp)
187+
? parseTimestampMs(traceLogLine.timestamp)
187188
: Number.NaN;
188189

189190
// Step 1: filter by path match

src/services/api/authFiles.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import type {
1212
AuthFilesResponse,
1313
} from '@/types/authFile';
1414
import type { OAuthModelAliasEntry } from '@/types';
15+
import { parseTimestampMs } from '@/utils/timestamp';
1516

1617
type StatusError = { status?: number };
1718
type AuthFileStatusResponse = { status: string; disabled: boolean };
@@ -239,7 +240,7 @@ const readDateField = (entry: AuthFileEntry): number => {
239240
if (Number.isFinite(asNumber)) {
240241
return asNumber < 1e12 ? asNumber * 1000 : asNumber;
241242
}
242-
const parsed = Date.parse(trimmed);
243+
const parsed = parseTimestampMs(trimmed);
243244
if (!Number.isNaN(parsed)) {
244245
return parsed;
245246
}

src/utils/timestamp.ts

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
const RFC3339_HIGH_PRECISION_REGEX =
2+
/^(\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2})(\.(\d+))?(Z|[+-]\d{2}:\d{2})?$/i;
3+
4+
/**
5+
* Some browsers mis-handle RFC3339 timestamps that include sub-millisecond
6+
* precision. Normalize them to millisecond precision before parsing.
7+
*/
8+
export function normalizeTimestampForDateParse(value: string): string {
9+
const trimmed = value.trim();
10+
if (!trimmed) return '';
11+
12+
const match = trimmed.match(RFC3339_HIGH_PRECISION_REGEX);
13+
if (!match) return trimmed;
14+
15+
const [, base, , fractionDigits = '', timezone = ''] = match;
16+
if (fractionDigits.length <= 3) {
17+
return trimmed;
18+
}
19+
20+
return `${base}.${fractionDigits.slice(0, 3)}${timezone}`;
21+
}
22+
23+
export function parseTimestampMs(value: unknown): number {
24+
if (typeof value === 'number' && Number.isFinite(value)) {
25+
return value;
26+
}
27+
if (value instanceof Date) {
28+
return value.getTime();
29+
}
30+
if (typeof value !== 'string') {
31+
return Number.NaN;
32+
}
33+
34+
const trimmed = value.trim();
35+
if (!trimmed) {
36+
return Number.NaN;
37+
}
38+
39+
const normalized = normalizeTimestampForDateParse(trimmed);
40+
const normalizedParsed = Date.parse(normalized);
41+
if (!Number.isNaN(normalizedParsed)) {
42+
return normalizedParsed;
43+
}
44+
45+
if (normalized !== trimmed) {
46+
const originalParsed = Date.parse(trimmed);
47+
if (!Number.isNaN(originalParsed)) {
48+
return originalParsed;
49+
}
50+
}
51+
52+
return Number.NaN;
53+
}
54+
55+
export function parseTimestamp(value: unknown): Date | null {
56+
const timestampMs = parseTimestampMs(value);
57+
if (!Number.isFinite(timestampMs)) {
58+
return null;
59+
}
60+
return new Date(timestampMs);
61+
}

src/utils/usage.ts

Lines changed: 13 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import {
1313
finalizeLatencyStats,
1414
} from './usage/latency';
1515
import { maskApiKey } from './format';
16+
import { parseTimestampMs } from './timestamp';
1617

1718
export type { DurationFormatOptions, LatencyStats } from './usage/latency';
1819
export {
@@ -202,7 +203,7 @@ export function filterUsageByTimeRange<T>(
202203
if (!detailRecord || typeof detailRecord.timestamp !== 'string') {
203204
return;
204205
}
205-
const timestamp = Date.parse(detailRecord.timestamp);
206+
const timestamp = parseTimestampMs(detailRecord.timestamp);
206207
if (Number.isNaN(timestamp) || timestamp < windowStart || timestamp > nowMs) {
207208
return;
208209
}
@@ -578,7 +579,7 @@ export function collectUsageDetails(usageData: unknown): UsageDetail[] {
578579
modelDetails.forEach((detailRaw) => {
579580
if (!isRecord(detailRaw) || typeof detailRaw.timestamp !== 'string') return;
580581
const timestamp = detailRaw.timestamp;
581-
const timestampMs = Date.parse(timestamp);
582+
const timestampMs = parseTimestampMs(timestamp);
582583
const tokensRaw = isRecord(detailRaw.tokens) ? detailRaw.tokens : {};
583584
const latencyMs = extractLatencyMs(detailRaw);
584585
const firstByteLatencyMs =
@@ -663,7 +664,7 @@ export function collectUsageDetailsWithEndpoint(usageData: unknown): UsageDetail
663664
modelDetails.forEach((detailRaw) => {
664665
if (!isRecord(detailRaw) || typeof detailRaw.timestamp !== 'string') return;
665666
const timestamp = detailRaw.timestamp;
666-
const timestampMs = Date.parse(timestamp);
667+
const timestampMs = parseTimestampMs(timestamp);
667668
const tokensRaw = isRecord(detailRaw.tokens) ? detailRaw.tokens : {};
668669
const latencyMs = extractLatencyMs(detailRaw);
669670
const firstByteLatencyMs =
@@ -776,7 +777,7 @@ export function calculateRecentPerMinuteRates(
776777
const timestamp =
777778
typeof detail.__timestampMs === 'number'
778779
? detail.__timestampMs
779-
: Date.parse(detail.timestamp);
780+
: parseTimestampMs(detail.timestamp);
780781
if (!Number.isFinite(timestamp) || timestamp < windowStart || timestamp > now) {
781782
return;
782783
}
@@ -1217,7 +1218,7 @@ export function buildHourlySeriesByModel(
12171218
const timestamp =
12181219
typeof detail.__timestampMs === 'number'
12191220
? detail.__timestampMs
1220-
: Date.parse(detail.timestamp);
1221+
: parseTimestampMs(detail.timestamp);
12211222
if (!Number.isFinite(timestamp) || timestamp <= 0) {
12221223
return;
12231224
}
@@ -1276,7 +1277,7 @@ export function buildDailySeriesByModel(
12761277
const timestamp =
12771278
typeof detail.__timestampMs === 'number'
12781279
? detail.__timestampMs
1279-
: Date.parse(detail.timestamp);
1280+
: parseTimestampMs(detail.timestamp);
12801281
if (!Number.isFinite(timestamp) || timestamp <= 0) {
12811282
return;
12821283
}
@@ -1502,7 +1503,7 @@ export function calculateStatusBarData(
15021503
const timestamp =
15031504
typeof detail.__timestampMs === 'number'
15041505
? detail.__timestampMs
1505-
: Date.parse(detail.timestamp);
1506+
: parseTimestampMs(detail.timestamp);
15061507
if (
15071508
!Number.isFinite(timestamp) ||
15081509
timestamp <= 0 ||
@@ -1610,7 +1611,7 @@ export function calculateServiceHealthData(usageDetails: UsageDetail[]): Service
16101611
const timestamp =
16111612
typeof detail.__timestampMs === 'number'
16121613
? detail.__timestampMs
1613-
: Date.parse(detail.timestamp);
1614+
: parseTimestampMs(detail.timestamp);
16141615
if (
16151616
!Number.isFinite(timestamp) ||
16161617
timestamp <= 0 ||
@@ -1820,7 +1821,7 @@ export function buildHourlyTokenBreakdown(
18201821
const timestamp =
18211822
typeof detail.__timestampMs === 'number'
18221823
? detail.__timestampMs
1823-
: Date.parse(detail.timestamp);
1824+
: parseTimestampMs(detail.timestamp);
18241825
if (!Number.isFinite(timestamp) || timestamp <= 0) return;
18251826
const normalized = new Date(timestamp);
18261827
normalized.setMinutes(0, 0, 0);
@@ -1862,7 +1863,7 @@ export function buildDailyTokenBreakdown(usageData: unknown): TokenBreakdownSeri
18621863
const timestamp =
18631864
typeof detail.__timestampMs === 'number'
18641865
? detail.__timestampMs
1865-
: Date.parse(detail.timestamp);
1866+
: parseTimestampMs(detail.timestamp);
18661867
if (!Number.isFinite(timestamp) || timestamp <= 0) return;
18671868
const dayLabel = formatDayLabel(new Date(timestamp));
18681869
if (!dayLabel) return;
@@ -1939,7 +1940,7 @@ export function buildHourlyCostSeries(
19391940
const timestamp =
19401941
typeof detail.__timestampMs === 'number'
19411942
? detail.__timestampMs
1942-
: Date.parse(detail.timestamp);
1943+
: parseTimestampMs(detail.timestamp);
19431944
if (!Number.isFinite(timestamp) || timestamp <= 0) return;
19441945
const normalized = new Date(timestamp);
19451946
normalized.setMinutes(0, 0, 0);
@@ -1974,7 +1975,7 @@ export function buildDailyCostSeries(
19741975
const timestamp =
19751976
typeof detail.__timestampMs === 'number'
19761977
? detail.__timestampMs
1977-
: Date.parse(detail.timestamp);
1978+
: parseTimestampMs(detail.timestamp);
19781979
if (!Number.isFinite(timestamp) || timestamp <= 0) return;
19791980
const dayLabel = formatDayLabel(new Date(timestamp));
19801981
if (!dayLabel) return;

0 commit comments

Comments
 (0)