diff --git a/src/app/(dashboard)/dashboard/analytics/ComboHealthTab.tsx b/src/app/(dashboard)/dashboard/analytics/ComboHealthTab.tsx index 6096b6c8f9..3455c296c4 100644 --- a/src/app/(dashboard)/dashboard/analytics/ComboHealthTab.tsx +++ b/src/app/(dashboard)/dashboard/analytics/ComboHealthTab.tsx @@ -295,51 +295,15 @@ export default function ComboHealthTab() { const [error, setError] = useState(null); const [retrying, setRetrying] = useState(false); - useEffect(() => { - const controller = new AbortController(); - - async function fetchComboHealth() { - setLoading(true); + const fetchData = useCallback( + async (controller: AbortController, isRetry = false) => { + if (isRetry) { + setRetrying(true); + } else { + setLoading(true); + } setError(null); - try { - const response = await fetch(`/api/usage/combo-health?range=${range}`, { - signal: controller.signal, - }); - - if (!response.ok) { - throw new Error("Failed to fetch combo health data"); - } - - const result = (await response.json()) as ComboHealthResponse; - setData(result); - } catch (fetchError) { - if ((fetchError as Error).name === "AbortError") { - return; - } - - setError(fetchError instanceof Error ? fetchError.message : "Unknown error"); - setData(null); - } finally { - if (!controller.signal.aborted) { - setLoading(false); - } - } - } - - fetchComboHealth(); - - return () => controller.abort(); - }, [range]); - - const combos = data?.combos ?? []; - - const handleRetry = useCallback(() => { - setRetrying(true); - setError(null); - const controller = new AbortController(); - - async function retryFetch() { try { const response = await fetch(`/api/usage/combo-health?range=${range}`, { signal: controller.signal, @@ -357,18 +321,29 @@ export default function ComboHealthTab() { return; } setError(fetchError instanceof Error ? fetchError.message : "Unknown error"); + if (!isRetry) setData(null); } finally { if (!controller.signal.aborted) { setLoading(false); - setRetrying(false); + if (isRetry) setRetrying(false); } } - } - - retryFetch(); + }, + [range] + ); + useEffect(() => { + const controller = new AbortController(); + fetchData(controller, false); return () => controller.abort(); - }, [range]); + }, [fetchData]); + + const combos = data?.combos ?? []; + + const handleRetry = useCallback(() => { + const controller = new AbortController(); + fetchData(controller, true); + }, [fetchData]); return (
diff --git a/src/lib/db/migrations/012_quota_snapshots.sql b/src/lib/db/migrations/013_quota_snapshots.sql similarity index 96% rename from src/lib/db/migrations/012_quota_snapshots.sql rename to src/lib/db/migrations/013_quota_snapshots.sql index d655405e26..0843b42cde 100644 --- a/src/lib/db/migrations/012_quota_snapshots.sql +++ b/src/lib/db/migrations/013_quota_snapshots.sql @@ -1,4 +1,4 @@ --- 012_quota_snapshots.sql +-- 013_quota_snapshots.sql -- Quota snapshots for subscription utilization tracking. -- Stores periodic snapshots of quota state for historical analysis and time-series queries. diff --git a/src/lib/db/quotaSnapshots.ts b/src/lib/db/quotaSnapshots.ts index 09d0203e13..49cfd8a582 100644 --- a/src/lib/db/quotaSnapshots.ts +++ b/src/lib/db/quotaSnapshots.ts @@ -87,7 +87,11 @@ export function getAggregatedSnapshots(opts: { params.push(opts.until); } - const bucketSeconds = opts.bucketMinutes * 60; + const bucketSeconds = Number(opts.bucketMinutes) * 60; + if (!Number.isFinite(bucketSeconds) || bucketSeconds <= 0) { + throw new Error("Invalid bucket size"); + } + const sql = ` SELECT datetime((strftime('%s', created_at) / ${bucketSeconds}) * ${bucketSeconds}, 'unixepoch') as bucket, diff --git a/src/shared/components/analytics/TimeRangeSelector.tsx b/src/shared/components/analytics/TimeRangeSelector.tsx index 49588b3238..0dcba97437 100644 --- a/src/shared/components/analytics/TimeRangeSelector.tsx +++ b/src/shared/components/analytics/TimeRangeSelector.tsx @@ -19,7 +19,7 @@ export default function TimeRangeSelector({ value, onChange }: TimeRangeSelector return (
{OPTIONS.map((option) => {