feat(failover): add auto-failover master switch with proxy integration (#427)

* feat(failover): add auto-failover master switch with proxy integration

- Add persistent auto_failover_enabled setting in database
- Add get/set_auto_failover_enabled commands
- Provider router respects master switch state
- Proxy shutdown automatically disables failover
- Enabling failover auto-starts proxy server
- Optimistic updates for failover queue toggle

* feat(proxy): persist proxy takeover state across app restarts

- Add proxy_takeover_{app_type} settings for per-app state tracking
- Restore proxy takeover state automatically on app startup
- Preserve state on normal exit, clear on manual stop
- Add stop_with_restore_keep_state method for graceful shutdown

* fix(proxy): set takeover state for all apps in start_with_takeover
This commit is contained in:
YoVinchen
2025-12-21 22:39:50 +08:00
committed by GitHub
parent f047960a33
commit a1537807eb
25 changed files with 479 additions and 141 deletions
+6 -18
View File
@@ -51,9 +51,7 @@ export function ModelTestConfigPanel() {
closeButton: true,
});
} catch (e) {
toast.error(
t("streamCheck.configSaveFailed") + ": " + String(e),
);
toast.error(t("streamCheck.configSaveFailed") + ": " + String(e));
} finally {
setIsSaving(false);
}
@@ -82,9 +80,7 @@ export function ModelTestConfigPanel() {
</h4>
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
<div className="space-y-2">
<Label htmlFor="claudeModel">
{t("streamCheck.claudeModel")}
</Label>
<Label htmlFor="claudeModel">{t("streamCheck.claudeModel")}</Label>
<Input
id="claudeModel"
value={config.claudeModel}
@@ -96,9 +92,7 @@ export function ModelTestConfigPanel() {
</div>
<div className="space-y-2">
<Label htmlFor="codexModel">
{t("streamCheck.codexModel")}
</Label>
<Label htmlFor="codexModel">{t("streamCheck.codexModel")}</Label>
<Input
id="codexModel"
value={config.codexModel}
@@ -110,9 +104,7 @@ export function ModelTestConfigPanel() {
</div>
<div className="space-y-2">
<Label htmlFor="geminiModel">
{t("streamCheck.geminiModel")}
</Label>
<Label htmlFor="geminiModel">{t("streamCheck.geminiModel")}</Label>
<Input
id="geminiModel"
value={config.geminiModel}
@@ -132,9 +124,7 @@ export function ModelTestConfigPanel() {
</h4>
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
<div className="space-y-2">
<Label htmlFor="timeoutSecs">
{t("streamCheck.timeout")}
</Label>
<Label htmlFor="timeoutSecs">{t("streamCheck.timeout")}</Label>
<Input
id="timeoutSecs"
type="number"
@@ -151,9 +141,7 @@ export function ModelTestConfigPanel() {
</div>
<div className="space-y-2">
<Label htmlFor="maxRetries">
{t("streamCheck.maxRetries")}
</Label>
<Label htmlFor="maxRetries">{t("streamCheck.maxRetries")}</Label>
<Input
id="maxRetries"
type="number"
+3 -8
View File
@@ -93,8 +93,7 @@ export function PricingConfigPanel() {
<CardContent>
<Alert variant="destructive">
<AlertDescription>
{t("usage.loadPricingError")}:{" "}
{String(error)}
{t("usage.loadPricingError")}: {String(error)}
</AlertDescription>
</Alert>
</CardContent>
@@ -124,9 +123,7 @@ export function PricingConfigPanel() {
<div className="space-y-4">
{!pricing || pricing.length === 0 ? (
<Alert>
<AlertDescription>
{t("usage.noPricingData")}
</AlertDescription>
<AlertDescription>{t("usage.noPricingData")}</AlertDescription>
</Alert>
) : (
<div className="rounded-md bg-card/60 shadow-sm">
@@ -220,9 +217,7 @@ export function PricingConfigPanel() {
>
<DialogContent>
<DialogHeader>
<DialogTitle>
{t("usage.deleteConfirmTitle")}
</DialogTitle>
<DialogTitle>{t("usage.deleteConfirmTitle")}</DialogTitle>
<DialogDescription>
{t("usage.deleteConfirmDesc")}
</DialogDescription>
+3 -8
View File
@@ -87,9 +87,7 @@ export function RequestLogTable() {
<SelectValue placeholder={t("usage.appType")} />
</SelectTrigger>
<SelectContent>
<SelectItem value="all">
{t("usage.allApps")}
</SelectItem>
<SelectItem value="all">{t("usage.allApps")}</SelectItem>
<SelectItem value="claude">Claude</SelectItem>
<SelectItem value="codex">Codex</SelectItem>
<SelectItem value="gemini">Gemini</SelectItem>
@@ -149,9 +147,7 @@ export function RequestLogTable() {
<div className="flex flex-wrap items-center justify-between gap-3">
<div className="flex items-center gap-2 text-sm text-muted-foreground">
<span className="whitespace-nowrap">
{t("usage.timeRange")}:
</span>
<span className="whitespace-nowrap">{t("usage.timeRange")}:</span>
<Input
type="datetime-local"
className="h-8 w-[200px] bg-background"
@@ -283,8 +279,7 @@ export function RequestLogTable() {
)}
</TableCell>
<TableCell>
{log.providerName ||
t("usage.unknownProvider")}
{log.providerName || t("usage.unknownProvider")}
</TableCell>
<TableCell
className="font-mono text-sm max-w-[280px] truncate"
+1 -3
View File
@@ -26,9 +26,7 @@ export function UsageDashboard() {
<div className="flex flex-col sm:flex-row sm:items-center justify-between gap-4">
<div className="flex flex-col gap-1">
<h2 className="text-2xl font-bold">{t("usage.title")}</h2>
<p className="text-sm text-muted-foreground">
{t("usage.subtitle")}
</p>
<p className="text-sm text-muted-foreground">{t("usage.subtitle")}</p>
</div>
<Tabs
+15 -15
View File
@@ -56,7 +56,7 @@ export function UsageSummaryCards({ days }: UsageSummaryCardsProps) {
color: "text-purple-500",
bg: "bg-purple-500/10",
subValue: (
<div className="flex flex-col gap-1 text-xs text-muted-foreground mt-3 pt-3 border-t border-border/50">
<div className="flex flex-col gap-1 text-xs text-muted-foreground mt-3 pt-3 border-t border-border/50">
<div className="flex justify-between items-center">
<span>{t("usage.input")}</span>
<span className="text-foreground/80">
@@ -78,21 +78,21 @@ export function UsageSummaryCards({ days }: UsageSummaryCardsProps) {
icon: Database,
color: "text-orange-500",
bg: "bg-orange-500/10",
subValue: (
<div className="flex flex-col gap-1 text-xs text-muted-foreground mt-3 pt-3 border-t border-border/50">
<div className="flex justify-between items-center">
<span>{t("usage.cacheWrite")}</span>
<span className="text-foreground/80">
{(cacheWriteTokens / 1000).toFixed(1)}k
</span>
</div>
<div className="flex justify-between items-center">
<span>{t("usage.cacheRead")}</span>
<span className="text-foreground/80">
{(cacheReadTokens / 1000).toFixed(1)}k
</span>
</div>
subValue: (
<div className="flex flex-col gap-1 text-xs text-muted-foreground mt-3 pt-3 border-t border-border/50">
<div className="flex justify-between items-center">
<span>{t("usage.cacheWrite")}</span>
<span className="text-foreground/80">
{(cacheWriteTokens / 1000).toFixed(1)}k
</span>
</div>
<div className="flex justify-between items-center">
<span>{t("usage.cacheRead")}</span>
<span className="text-foreground/80">
{(cacheReadTokens / 1000).toFixed(1)}k
</span>
</div>
</div>
),
},
];