mirror of
https://github.com/farion1231/cc-switch.git
synced 2026-08-04 11:43:57 +08:00
feat(db): rebuild Codex usage on upgrade and via maintenance action
Schema v16 wipes codex_session detail rows, _codex_session rollups and Codex rollout cursors inside the migration savepoint (cursor deletion uses pure shape matching so CODEX_HOME drift cannot orphan cursors); the next session sync re-imports history from source JSONL under the corrected importer. Fresh installs traverse the same branch as a no-op. Add a manual "Rebuild Codex usage" maintenance action (single-flight, hard-fail backup before reset, unconditional refresh notification even when reimport is empty or fails after reset) with a destructive confirm dialog, result toast and four-locale strings. Historical proxy-side duplicate rows are intentionally left untouched; history whose source JSONL was already deleted cannot be reconstructed.
This commit is contained in:
@@ -19,6 +19,8 @@ import {
|
||||
RefreshCw,
|
||||
Coins,
|
||||
LayoutGrid,
|
||||
DatabaseBackup,
|
||||
Loader2,
|
||||
} from "lucide-react";
|
||||
import { ProviderIcon } from "@/components/ProviderIcon";
|
||||
import {
|
||||
@@ -43,6 +45,10 @@ import { getLocaleFromLanguage } from "./format";
|
||||
import { getUsageRangePresetLabel, resolveUsageRange } from "@/lib/usageRange";
|
||||
import { UsageDateRangePicker } from "./UsageDateRangePicker";
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { ConfirmDialog } from "@/components/ConfirmDialog";
|
||||
import { usageApi } from "@/lib/api/usage";
|
||||
import { toast } from "sonner";
|
||||
|
||||
const APP_FILTER_OPTIONS: AppTypeFilter[] = ["all", ...KNOWN_APP_TYPES];
|
||||
|
||||
@@ -94,6 +100,8 @@ export function UsageDashboard({
|
||||
const [refreshIntervalMs, setRefreshIntervalMs] = useState(() =>
|
||||
normalizeRefreshInterval(savedRefreshIntervalMs),
|
||||
);
|
||||
const [showRebuildConfirm, setShowRebuildConfirm] = useState(false);
|
||||
const [rebuildingCodex, setRebuildingCodex] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
setRefreshIntervalMs(normalizeRefreshInterval(savedRefreshIntervalMs));
|
||||
@@ -138,6 +146,34 @@ export function UsageDashboard({
|
||||
}
|
||||
};
|
||||
|
||||
const rebuildCodexUsage = async () => {
|
||||
setShowRebuildConfirm(false);
|
||||
setRebuildingCodex(true);
|
||||
try {
|
||||
const result = await usageApi.rebuildCodexUsage();
|
||||
await queryClient.invalidateQueries({ queryKey: usageKeys.all });
|
||||
const message = t("usage.rebuildCodex.completed", {
|
||||
imported: result.imported,
|
||||
errors: result.errors.length,
|
||||
suspected: result.suspectedDuplicates,
|
||||
deferred: result.deferredFiles,
|
||||
});
|
||||
if (result.errors.length > 0 || result.deferredFiles > 0) {
|
||||
toast.warning(message);
|
||||
} else {
|
||||
toast.success(message);
|
||||
}
|
||||
} catch (error) {
|
||||
toast.error(
|
||||
t("usage.rebuildCodex.failed", {
|
||||
error: String(error),
|
||||
}),
|
||||
);
|
||||
} finally {
|
||||
setRebuildingCodex(false);
|
||||
}
|
||||
};
|
||||
|
||||
const language = i18n.resolvedLanguage || i18n.language || "en";
|
||||
const locale = getLocaleFromLanguage(language);
|
||||
const resolvedRange = useMemo(() => resolveUsageRange(range), [range]);
|
||||
@@ -429,7 +465,55 @@ export function UsageDashboard({
|
||||
<PricingConfigPanel />
|
||||
</AccordionContent>
|
||||
</AccordionItem>
|
||||
<AccordionItem
|
||||
value="maintenance"
|
||||
className="rounded-xl glass-card overflow-hidden"
|
||||
>
|
||||
<AccordionTrigger className="px-6 py-4 hover:no-underline hover:bg-muted/50 data-[state=open]:bg-muted/50">
|
||||
<div className="flex items-center gap-3">
|
||||
<DatabaseBackup className="h-5 w-5 text-orange-500" />
|
||||
<div className="text-left">
|
||||
<h3 className="text-base font-semibold">
|
||||
{t("usage.rebuildCodex.title")}
|
||||
</h3>
|
||||
<p className="text-sm text-muted-foreground font-normal">
|
||||
{t("usage.rebuildCodex.description")}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</AccordionTrigger>
|
||||
<AccordionContent className="px-6 pb-6 pt-4 border-t border-border/50">
|
||||
<div className="flex items-center justify-between gap-4 rounded-lg border border-destructive/20 bg-destructive/5 p-4">
|
||||
<p className="text-sm text-muted-foreground">
|
||||
{t("usage.rebuildCodex.warning")}
|
||||
</p>
|
||||
<Button
|
||||
variant="destructive"
|
||||
disabled={rebuildingCodex}
|
||||
onClick={() => setShowRebuildConfirm(true)}
|
||||
className="shrink-0"
|
||||
>
|
||||
{rebuildingCodex ? (
|
||||
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
|
||||
) : (
|
||||
<DatabaseBackup className="mr-2 h-4 w-4" />
|
||||
)}
|
||||
{t("usage.rebuildCodex.action")}
|
||||
</Button>
|
||||
</div>
|
||||
</AccordionContent>
|
||||
</AccordionItem>
|
||||
</Accordion>
|
||||
|
||||
<ConfirmDialog
|
||||
isOpen={showRebuildConfirm}
|
||||
title={t("usage.rebuildCodex.confirmTitle")}
|
||||
message={t("usage.rebuildCodex.confirmMessage")}
|
||||
confirmText={t("usage.rebuildCodex.confirmAction")}
|
||||
variant="destructive"
|
||||
onConfirm={() => void rebuildCodexUsage()}
|
||||
onCancel={() => setShowRebuildConfirm(false)}
|
||||
/>
|
||||
</motion.div>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user