chore(release): v3.3.10 — merge Analytics and SQLite fixes (#849)

Co-authored-by: diegosouzapw <diegosouzapw@users.noreply.github.com>
This commit is contained in:
Diego Rodrigues de Sa e Souza
2026-03-31 00:15:20 -03:00
committed by GitHub
parent 9227964cb6
commit 6f9fec658f
4 changed files with 30 additions and 51 deletions

View File

@@ -295,51 +295,15 @@ export default function ComboHealthTab() {
const [error, setError] = useState<string | null>(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 (
<div className="flex flex-col gap-6">

View File

@@ -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.

View File

@@ -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,

View File

@@ -19,7 +19,7 @@ export default function TimeRangeSelector({ value, onChange }: TimeRangeSelector
return (
<div
role="tablist"
aria-label="시간 범위 선택"
aria-label="Select time range"
className="inline-flex items-center gap-1 rounded-lg bg-black/5 p-1 dark:bg-white/5"
>
{OPTIONS.map((option) => {