mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-08-08 08:12:20 +03:00
feat(i18n): migrate Settings batch 4 — final 4 large tabs (80+ strings)
- ComboDefaultsTab: strategy labels, toggles, provider overrides - PricingTab: model pricing, stats, save/reset, search - ResilienceTab: provider profiles, rate limiting, circuit breakers, policies - SystemStorageTab: export/import, backup/restore, database info - Expanded settings namespace to ~290 total keys - Completes Settings page i18n migration (17/17 files)
This commit is contained in:
@@ -3,6 +3,7 @@
|
||||
import { useState, useEffect } from "react";
|
||||
import { Card, Button, Input, Toggle } from "@/shared/components";
|
||||
import { cn } from "@/shared/utils/cn";
|
||||
import { useTranslations } from "next-intl";
|
||||
|
||||
export default function ComboDefaultsTab() {
|
||||
const [comboDefaults, setComboDefaults] = useState<any>({
|
||||
@@ -18,6 +19,8 @@ export default function ComboDefaultsTab() {
|
||||
const [providerOverrides, setProviderOverrides] = useState<any>({});
|
||||
const [newOverrideProvider, setNewOverrideProvider] = useState("");
|
||||
const [saving, setSaving] = useState(false);
|
||||
const t = useTranslations("settings");
|
||||
const tc = useTranslations("common");
|
||||
|
||||
useEffect(() => {
|
||||
fetch("/api/settings/combo-defaults")
|
||||
@@ -67,17 +70,15 @@ export default function ComboDefaultsTab() {
|
||||
tune
|
||||
</span>
|
||||
</div>
|
||||
<h3 className="text-lg font-semibold">Combo Defaults</h3>
|
||||
<span className="text-xs text-text-muted ml-auto">Global combo configuration</span>
|
||||
<h3 className="text-lg font-semibold">{t("comboDefaultsTitle")}</h3>
|
||||
<span className="text-xs text-text-muted ml-auto">{t("globalComboConfig")}</span>
|
||||
</div>
|
||||
<div className="flex flex-col gap-4">
|
||||
{/* Default Strategy */}
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className="font-medium text-sm">Default Strategy</p>
|
||||
<p className="text-xs text-text-muted">
|
||||
Applied to new combos without explicit strategy
|
||||
</p>
|
||||
<p className="font-medium text-sm">{t("defaultStrategy")}</p>
|
||||
<p className="text-xs text-text-muted">{t("defaultStrategyDesc")}</p>
|
||||
</div>
|
||||
<div
|
||||
role="tablist"
|
||||
@@ -176,8 +177,8 @@ export default function ComboDefaultsTab() {
|
||||
<div className="flex flex-col gap-3 pt-3 border-t border-border/50">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className="font-medium text-sm">Health Check</p>
|
||||
<p className="text-xs text-text-muted">Pre-check provider availability</p>
|
||||
<p className="font-medium text-sm">{t("healthCheck")}</p>
|
||||
<p className="text-xs text-text-muted">{t("healthCheckDesc")}</p>
|
||||
</div>
|
||||
<Toggle
|
||||
checked={comboDefaults.healthCheckEnabled !== false}
|
||||
@@ -191,8 +192,8 @@ export default function ComboDefaultsTab() {
|
||||
</div>
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className="font-medium text-sm">Track Metrics</p>
|
||||
<p className="text-xs text-text-muted">Record per-combo request metrics</p>
|
||||
<p className="font-medium text-sm">{t("trackMetrics")}</p>
|
||||
<p className="text-xs text-text-muted">{t("trackMetricsDesc")}</p>
|
||||
</div>
|
||||
<Toggle
|
||||
checked={comboDefaults.trackMetrics !== false}
|
||||
@@ -205,10 +206,8 @@ export default function ComboDefaultsTab() {
|
||||
|
||||
{/* Provider Overrides */}
|
||||
<div className="pt-3 border-t border-border/50">
|
||||
<p className="font-medium text-sm mb-2">Provider Overrides</p>
|
||||
<p className="text-xs text-text-muted mb-3">
|
||||
Override timeout and retries per provider. Provider settings override global defaults.
|
||||
</p>
|
||||
<p className="font-medium text-sm mb-2">{t("providerOverrides")}</p>
|
||||
<p className="text-xs text-text-muted mb-3">{t("providerOverridesDesc")}</p>
|
||||
|
||||
{Object.entries(providerOverrides).map(([provider, config]: [string, any]) => (
|
||||
<div
|
||||
@@ -230,7 +229,7 @@ export default function ComboDefaultsTab() {
|
||||
className="text-xs w-16"
|
||||
aria-label={`${provider} max retries`}
|
||||
/>
|
||||
<span className="text-[10px] text-text-muted">retries</span>
|
||||
<span className="text-[10px] text-text-muted">{t("retries")}</span>
|
||||
<Input
|
||||
type="number"
|
||||
min="5000"
|
||||
@@ -249,7 +248,7 @@ export default function ComboDefaultsTab() {
|
||||
className="text-xs w-24"
|
||||
aria-label={`${provider} timeout ms`}
|
||||
/>
|
||||
<span className="text-[10px] text-text-muted">ms</span>
|
||||
<span className="text-[10px] text-text-muted">{t("ms")}</span>
|
||||
<button
|
||||
onClick={() => removeProviderOverride(provider)}
|
||||
className="ml-auto text-red-400 hover:text-red-500 transition-colors"
|
||||
@@ -278,7 +277,7 @@ export default function ComboDefaultsTab() {
|
||||
onClick={addProviderOverride}
|
||||
disabled={!newOverrideProvider.trim()}
|
||||
>
|
||||
Add
|
||||
{tc("add")}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -286,7 +285,7 @@ export default function ComboDefaultsTab() {
|
||||
{/* Save */}
|
||||
<div className="pt-3 border-t border-border/50">
|
||||
<Button variant="primary" size="sm" onClick={saveComboDefaults} loading={saving}>
|
||||
Save Combo Defaults
|
||||
{t("saveComboDefaults")}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
import { useState, useEffect, useCallback, useMemo } from "react";
|
||||
import { Card } from "@/shared/components";
|
||||
import { useTranslations } from "next-intl";
|
||||
|
||||
const PRICING_FIELDS = ["input", "output", "cached", "reasoning", "cache_creation"];
|
||||
const FIELD_LABELS = {
|
||||
@@ -22,6 +23,7 @@ export default function PricingTab() {
|
||||
const [expandedProviders, setExpandedProviders] = useState(new Set());
|
||||
const [searchQuery, setSearchQuery] = useState("");
|
||||
const [editedProviders, setEditedProviders] = useState(new Set());
|
||||
const t = useTranslations("settings");
|
||||
|
||||
// Load catalog + pricing
|
||||
useEffect(() => {
|
||||
@@ -50,9 +52,7 @@ export default function PricingTab() {
|
||||
.map(([alias, info]: [string, any]) => ({
|
||||
alias,
|
||||
...info,
|
||||
pricedModels: pricingData[alias]
|
||||
? Object.keys(pricingData[alias]).length
|
||||
: 0,
|
||||
pricedModels: pricingData[alias] ? Object.keys(pricingData[alias]).length : 0,
|
||||
}))
|
||||
.sort((a, b) => b.modelCount - a.modelCount);
|
||||
return providers;
|
||||
@@ -66,11 +66,7 @@ export default function PricingTab() {
|
||||
(p) =>
|
||||
p.alias.toLowerCase().includes(q) ||
|
||||
p.id.toLowerCase().includes(q) ||
|
||||
p.models.some(
|
||||
(m) =>
|
||||
m.id.toLowerCase().includes(q) ||
|
||||
m.name.toLowerCase().includes(q)
|
||||
)
|
||||
p.models.some((m) => m.id.toLowerCase().includes(q) || m.name.toLowerCase().includes(q))
|
||||
);
|
||||
}, [allProviders, searchQuery]);
|
||||
|
||||
@@ -97,23 +93,20 @@ export default function PricingTab() {
|
||||
});
|
||||
}, []);
|
||||
|
||||
const handlePricingChange = useCallback(
|
||||
(provider, model, field, value) => {
|
||||
const numValue = parseFloat(value);
|
||||
if (isNaN(numValue) || numValue < 0) return;
|
||||
const handlePricingChange = useCallback((provider, model, field, value) => {
|
||||
const numValue = parseFloat(value);
|
||||
if (isNaN(numValue) || numValue < 0) return;
|
||||
|
||||
setPricingData((prev) => {
|
||||
const next = { ...prev };
|
||||
if (!next[provider]) next[provider] = {};
|
||||
if (!next[provider][model])
|
||||
next[provider][model] = { input: 0, output: 0, cached: 0, reasoning: 0, cache_creation: 0 };
|
||||
next[provider][model] = { ...next[provider][model], [field]: numValue };
|
||||
return next;
|
||||
});
|
||||
setEditedProviders((prev) => new Set(prev).add(provider));
|
||||
},
|
||||
[]
|
||||
);
|
||||
setPricingData((prev) => {
|
||||
const next = { ...prev };
|
||||
if (!next[provider]) next[provider] = {};
|
||||
if (!next[provider][model])
|
||||
next[provider][model] = { input: 0, output: 0, cached: 0, reasoning: 0, cache_creation: 0 };
|
||||
next[provider][model] = { ...next[provider][model], [field]: numValue };
|
||||
return next;
|
||||
});
|
||||
setEditedProviders((prev) => new Set(prev).add(provider));
|
||||
}, []);
|
||||
|
||||
const saveProvider = useCallback(
|
||||
async (providerAlias) => {
|
||||
@@ -148,36 +141,25 @@ export default function PricingTab() {
|
||||
[pricingData]
|
||||
);
|
||||
|
||||
const resetProvider = useCallback(
|
||||
async (providerAlias) => {
|
||||
if (
|
||||
!confirm(
|
||||
`Reset all pricing for ${providerAlias.toUpperCase()} to defaults?`
|
||||
)
|
||||
)
|
||||
return;
|
||||
try {
|
||||
const response = await fetch(
|
||||
`/api/pricing?provider=${providerAlias}`,
|
||||
{ method: "DELETE" }
|
||||
);
|
||||
if (response.ok) {
|
||||
const updated = await response.json();
|
||||
setPricingData(updated);
|
||||
setSaveStatus(`🔄 ${providerAlias.toUpperCase()} reset to defaults`);
|
||||
setEditedProviders((prev) => {
|
||||
const next = new Set(prev);
|
||||
next.delete(providerAlias);
|
||||
return next;
|
||||
});
|
||||
setTimeout(() => setSaveStatus(""), 3000);
|
||||
}
|
||||
} catch (error) {
|
||||
setSaveStatus(`❌ Reset failed: ${error.message}`);
|
||||
const resetProvider = useCallback(async (providerAlias) => {
|
||||
if (!confirm(`Reset all pricing for ${providerAlias.toUpperCase()} to defaults?`)) return;
|
||||
try {
|
||||
const response = await fetch(`/api/pricing?provider=${providerAlias}`, { method: "DELETE" });
|
||||
if (response.ok) {
|
||||
const updated = await response.json();
|
||||
setPricingData(updated);
|
||||
setSaveStatus(`🔄 ${providerAlias.toUpperCase()} reset to defaults`);
|
||||
setEditedProviders((prev) => {
|
||||
const next = new Set(prev);
|
||||
next.delete(providerAlias);
|
||||
return next;
|
||||
});
|
||||
setTimeout(() => setSaveStatus(""), 3000);
|
||||
}
|
||||
},
|
||||
[]
|
||||
);
|
||||
} catch (error) {
|
||||
setSaveStatus(`❌ Reset failed: ${error.message}`);
|
||||
}
|
||||
}, []);
|
||||
|
||||
const selectProviderFilter = useCallback((alias) => {
|
||||
setSelectedProvider((prev) => (prev === alias ? null : alias));
|
||||
@@ -194,9 +176,7 @@ export default function PricingTab() {
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="flex items-center justify-center py-16">
|
||||
<div className="text-text-muted animate-pulse">
|
||||
Loading pricing data...
|
||||
</div>
|
||||
<div className="text-text-muted animate-pulse">{t("loadingPricing")}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -206,30 +186,23 @@ export default function PricingTab() {
|
||||
{/* Header + Stats */}
|
||||
<div className="flex items-start justify-between flex-wrap gap-4">
|
||||
<div>
|
||||
<h2 className="text-xl font-bold">Model Pricing</h2>
|
||||
<h2 className="text-xl font-bold">{t("modelPricing")}</h2>
|
||||
<p className="text-text-muted text-sm mt-1">
|
||||
Configure cost rates per model • All rates in{" "}
|
||||
<strong>$/1M tokens</strong>
|
||||
Configure cost rates per model • All rates in <strong>$/1M tokens</strong>
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex gap-3 text-sm">
|
||||
<div className="bg-bg-subtle rounded-lg px-3 py-2 text-center">
|
||||
<div className="text-text-muted text-xs font-semibold">
|
||||
Providers
|
||||
</div>
|
||||
<div className="text-text-muted text-xs font-semibold">{t("providers")}</div>
|
||||
<div className="text-lg font-bold">{stats.providers}</div>
|
||||
</div>
|
||||
<div className="bg-bg-subtle rounded-lg px-3 py-2 text-center">
|
||||
<div className="text-text-muted text-xs font-semibold">
|
||||
Registry
|
||||
</div>
|
||||
<div className="text-text-muted text-xs font-semibold">{t("registry")}</div>
|
||||
<div className="text-lg font-bold">{stats.totalModels}</div>
|
||||
</div>
|
||||
<div className="bg-bg-subtle rounded-lg px-3 py-2 text-center">
|
||||
<div className="text-text-muted text-xs font-semibold">Priced</div>
|
||||
<div className="text-lg font-bold text-success">
|
||||
{stats.pricedCount as number}
|
||||
</div>
|
||||
<div className="text-text-muted text-xs font-semibold">{t("priced")}</div>
|
||||
<div className="text-lg font-bold text-success">{stats.pricedCount as number}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -249,7 +222,7 @@ export default function PricingTab() {
|
||||
</span>
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Search providers or models..."
|
||||
placeholder={t("searchProvidersModels")}
|
||||
value={searchQuery}
|
||||
onChange={(e) => setSearchQuery(e.target.value)}
|
||||
className="w-full pl-10 pr-3 py-2 bg-bg-base border border-border rounded-lg focus:outline-none focus:border-primary text-sm"
|
||||
@@ -261,7 +234,7 @@ export default function PricingTab() {
|
||||
className="px-3 py-2 text-xs bg-primary/10 text-primary border border-primary/20 rounded-lg hover:bg-primary/20 transition-colors flex items-center gap-1"
|
||||
>
|
||||
<span className="material-symbols-outlined text-sm">close</span>
|
||||
{selectedProvider.toUpperCase()} — Show All
|
||||
{selectedProvider.toUpperCase()} — {t("showAll")}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
@@ -276,12 +249,11 @@ export default function PricingTab() {
|
||||
selectedProvider === p.alias
|
||||
? "bg-primary text-white shadow-sm"
|
||||
: editedProviders.has(p.alias)
|
||||
? "bg-yellow-500/15 text-yellow-400 border border-yellow-500/30"
|
||||
: "bg-bg-subtle text-text-muted hover:bg-bg-hover border border-transparent"
|
||||
? "bg-yellow-500/15 text-yellow-400 border border-yellow-500/30"
|
||||
: "bg-bg-subtle text-text-muted hover:bg-bg-hover border border-transparent"
|
||||
}`}
|
||||
>
|
||||
{p.alias.toUpperCase()}{" "}
|
||||
<span className="opacity-60">({p.modelCount})</span>
|
||||
{p.alias.toUpperCase()} <span className="opacity-60">({p.modelCount})</span>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
@@ -306,32 +278,26 @@ export default function PricingTab() {
|
||||
))}
|
||||
|
||||
{displayProviders.length === 0 && (
|
||||
<div className="text-center py-12 text-text-muted">
|
||||
No providers match your search.
|
||||
</div>
|
||||
<div className="text-center py-12 text-text-muted">{t("noProvidersMatch")}</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Info Box */}
|
||||
<Card className="p-4 mt-2">
|
||||
<h3 className="text-sm font-semibold mb-2">
|
||||
<span className="material-symbols-outlined text-sm align-middle mr-1">
|
||||
info
|
||||
</span>
|
||||
How Pricing Works
|
||||
<span className="material-symbols-outlined text-sm align-middle mr-1">info</span>
|
||||
{t("howPricingWorks")}
|
||||
</h3>
|
||||
<div className="text-xs text-text-muted space-y-1">
|
||||
<p>
|
||||
<strong>Input</strong>: tokens sent to the model •{" "}
|
||||
<strong>Output</strong>: tokens generated •{" "}
|
||||
<strong>Cached</strong>: reused input (~50% of input rate) •{" "}
|
||||
<strong>Input</strong>: tokens sent to the model • <strong>Output</strong>: tokens
|
||||
generated • <strong>Cached</strong>: reused input (~50% of input rate) •{" "}
|
||||
<strong>Reasoning</strong>: thinking tokens (falls back to Output) •{" "}
|
||||
<strong>Cache Write</strong>: creating cache entries (falls back to
|
||||
Input)
|
||||
<strong>Cache Write</strong>: creating cache entries (falls back to Input)
|
||||
</p>
|
||||
<p>
|
||||
Cost = (input × input_rate) + (output × output_rate) + (cached ×
|
||||
cached_rate) per million tokens.
|
||||
Cost = (input × input_rate) + (output × output_rate) + (cached × cached_rate) per
|
||||
million tokens.
|
||||
</p>
|
||||
</div>
|
||||
</Card>
|
||||
@@ -352,20 +318,19 @@ function ProviderSection({
|
||||
onReset,
|
||||
saving,
|
||||
}) {
|
||||
const t = useTranslations("settings");
|
||||
const pricedCount = Object.keys(pricingData).length;
|
||||
const authBadge =
|
||||
provider.authType === "oauth"
|
||||
? "OAuth"
|
||||
: provider.authType === "apikey"
|
||||
? "API Key"
|
||||
: provider.authType;
|
||||
? "API Key"
|
||||
: provider.authType;
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`border rounded-lg overflow-hidden transition-colors ${
|
||||
isEdited
|
||||
? "border-yellow-500/40 bg-yellow-500/5"
|
||||
: "border-border"
|
||||
isEdited ? "border-yellow-500/40 bg-yellow-500/5" : "border-border"
|
||||
}`}
|
||||
>
|
||||
{/* Header (click to expand) */}
|
||||
@@ -385,9 +350,7 @@ function ProviderSection({
|
||||
<span className="font-semibold text-sm">
|
||||
{provider.id.charAt(0).toUpperCase() + provider.id.slice(1)}
|
||||
</span>
|
||||
<span className="text-text-muted text-xs ml-2">
|
||||
({provider.alias.toUpperCase()})
|
||||
</span>
|
||||
<span className="text-text-muted text-xs ml-2">({provider.alias.toUpperCase()})</span>
|
||||
</div>
|
||||
<span className="px-1.5 py-0.5 bg-bg-subtle text-text-muted text-[10px] rounded uppercase font-semibold">
|
||||
{authBadge}
|
||||
@@ -397,11 +360,7 @@ function ProviderSection({
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-3">
|
||||
{isEdited && (
|
||||
<span className="text-yellow-500 text-xs font-medium">
|
||||
unsaved
|
||||
</span>
|
||||
)}
|
||||
{isEdited && <span className="text-yellow-500 text-xs font-medium">{t("unsaved")}</span>}
|
||||
<span className="text-text-muted text-xs">
|
||||
{pricedCount}/{provider.modelCount} priced
|
||||
</span>
|
||||
@@ -426,8 +385,7 @@ function ProviderSection({
|
||||
{/* Actions bar */}
|
||||
<div className="flex items-center justify-between px-4 py-2 bg-bg-subtle/50">
|
||||
<span className="text-xs text-text-muted">
|
||||
{provider.modelCount} models •{" "}
|
||||
{pricedCount} with pricing configured
|
||||
{provider.modelCount} models • {pricedCount} with pricing configured
|
||||
</span>
|
||||
<div className="flex items-center gap-2">
|
||||
<button
|
||||
@@ -437,7 +395,7 @@ function ProviderSection({
|
||||
}}
|
||||
className="px-2.5 py-1 text-[11px] text-red-400 hover:bg-red-500/10 rounded border border-red-500/20 transition-colors"
|
||||
>
|
||||
Reset Defaults
|
||||
{t("resetDefaults")}
|
||||
</button>
|
||||
<button
|
||||
onClick={(e) => {
|
||||
@@ -447,7 +405,7 @@ function ProviderSection({
|
||||
disabled={saving || !isEdited}
|
||||
className="px-2.5 py-1 text-[11px] bg-primary text-white rounded hover:bg-primary/90 transition-colors disabled:opacity-40"
|
||||
>
|
||||
{saving ? "Saving..." : "Save Provider"}
|
||||
{saving ? t("saving") : t("saveProvider")}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -457,12 +415,9 @@ function ProviderSection({
|
||||
<table className="w-full text-sm">
|
||||
<thead className="text-[11px] text-text-muted uppercase bg-bg-subtle/30">
|
||||
<tr>
|
||||
<th className="px-4 py-2 text-left font-semibold">Model</th>
|
||||
<th className="px-4 py-2 text-left font-semibold">{t("model")}</th>
|
||||
{PRICING_FIELDS.map((field) => (
|
||||
<th
|
||||
key={field}
|
||||
className="px-2 py-2 text-right font-semibold w-24"
|
||||
>
|
||||
<th key={field} className="px-2 py-2 text-right font-semibold w-24">
|
||||
{FIELD_LABELS[field]}
|
||||
</th>
|
||||
))}
|
||||
@@ -474,9 +429,7 @@ function ProviderSection({
|
||||
key={model.id}
|
||||
model={model}
|
||||
pricing={pricingData[model.id]}
|
||||
onPricingChange={(field, value) =>
|
||||
onPricingChange(model.id, field, value)
|
||||
}
|
||||
onPricingChange={(field, value) => onPricingChange(model.id, field, value)}
|
||||
/>
|
||||
))}
|
||||
</tbody>
|
||||
@@ -498,9 +451,7 @@ function ModelRow({ model, pricing, onPricingChange }) {
|
||||
<td className="px-4 py-1.5">
|
||||
<div className="flex items-center gap-2">
|
||||
<span
|
||||
className={`w-1.5 h-1.5 rounded-full ${
|
||||
hasPricing ? "bg-success" : "bg-text-muted/30"
|
||||
}`}
|
||||
className={`w-1.5 h-1.5 rounded-full ${hasPricing ? "bg-success" : "bg-text-muted/30"}`}
|
||||
/>
|
||||
<span className="font-medium text-xs">{model.name}</span>
|
||||
{model.custom && (
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
import { useState, useEffect, useCallback } from "react";
|
||||
import { Card, Button } from "@/shared/components";
|
||||
import { useNotificationStore } from "@/store/notificationStore";
|
||||
import { useTranslations } from "next-intl";
|
||||
|
||||
// ─── State colors and labels ──────────────────────────────────────────────
|
||||
const STATE_STYLES = {
|
||||
@@ -46,6 +47,8 @@ function formatMs(ms) {
|
||||
function ProviderProfilesCard({ profiles, onSave, saving }) {
|
||||
const [editMode, setEditMode] = useState(false);
|
||||
const [draft, setDraft] = useState(profiles);
|
||||
const t = useTranslations("settings");
|
||||
const tc = useTranslations("common");
|
||||
|
||||
useEffect(() => {
|
||||
setDraft(profiles);
|
||||
@@ -72,12 +75,12 @@ function ProviderProfilesCard({ profiles, onSave, saving }) {
|
||||
<span className="material-symbols-outlined text-xl text-primary" aria-hidden="true">
|
||||
tune
|
||||
</span>
|
||||
<h2 className="text-lg font-bold">Provider Profiles</h2>
|
||||
<h2 className="text-lg font-bold">{t("providerProfiles")}</h2>
|
||||
</div>
|
||||
{editMode ? (
|
||||
<div className="flex gap-2">
|
||||
<Button size="sm" variant="secondary" onClick={() => setEditMode(false)}>
|
||||
Cancel
|
||||
{tc("cancel")}
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
@@ -86,20 +89,17 @@ function ProviderProfilesCard({ profiles, onSave, saving }) {
|
||||
onClick={handleSave}
|
||||
disabled={saving}
|
||||
>
|
||||
Save
|
||||
{tc("save")}
|
||||
</Button>
|
||||
</div>
|
||||
) : (
|
||||
<Button size="sm" variant="secondary" icon="edit" onClick={() => setEditMode(true)}>
|
||||
Edit
|
||||
{tc("edit")}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<p className="text-sm text-text-muted mb-4">
|
||||
Separate resilience settings for OAuth (session-based) and API Key (metered) providers.
|
||||
OAuth providers have stricter thresholds due to lower rate limits.
|
||||
</p>
|
||||
<p className="text-sm text-text-muted mb-4">{t("providerProfilesDesc")}</p>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
{["oauth", "apikey"].map((type) => (
|
||||
@@ -108,7 +108,7 @@ function ProviderProfilesCard({ profiles, onSave, saving }) {
|
||||
<span className="material-symbols-outlined text-base" aria-hidden="true">
|
||||
{type === "oauth" ? "lock" : "key"}
|
||||
</span>
|
||||
{type === "oauth" ? "OAuth Providers" : "API Key Providers"}
|
||||
{type === "oauth" ? t("oauthProviders") : t("apiKeyProviders")}
|
||||
</h3>
|
||||
<div className="space-y-2">
|
||||
{fields.map(({ key, label, suffix }) => (
|
||||
@@ -148,6 +148,8 @@ function ProviderProfilesCard({ profiles, onSave, saving }) {
|
||||
function RateLimitCard({ rateLimitStatus, defaults, onSaveDefaults, saving }) {
|
||||
const [editMode, setEditMode] = useState(false);
|
||||
const [draft, setDraft] = useState(defaults || {});
|
||||
const t = useTranslations("settings");
|
||||
const tc = useTranslations("common");
|
||||
|
||||
// Sync draft when defaults change from parent (standard prop-to-state sync)
|
||||
/* eslint-disable react-hooks/set-state-in-effect */
|
||||
@@ -169,12 +171,12 @@ function RateLimitCard({ rateLimitStatus, defaults, onSaveDefaults, saving }) {
|
||||
<span className="material-symbols-outlined text-xl text-primary" aria-hidden="true">
|
||||
speed
|
||||
</span>
|
||||
<h2 className="text-lg font-bold">Rate Limiting</h2>
|
||||
<h2 className="text-lg font-bold">{t("rateLimiting")}</h2>
|
||||
</div>
|
||||
{editMode ? (
|
||||
<div className="flex gap-2">
|
||||
<Button size="sm" variant="secondary" onClick={() => setEditMode(false)}>
|
||||
Cancel
|
||||
{tc("cancel")}
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
@@ -183,24 +185,21 @@ function RateLimitCard({ rateLimitStatus, defaults, onSaveDefaults, saving }) {
|
||||
onClick={handleSave}
|
||||
disabled={saving}
|
||||
>
|
||||
Save
|
||||
{tc("save")}
|
||||
</Button>
|
||||
</div>
|
||||
) : (
|
||||
<Button size="sm" variant="secondary" icon="edit" onClick={() => setEditMode(true)}>
|
||||
Edit
|
||||
{tc("edit")}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<p className="text-sm text-text-muted mb-4">
|
||||
API Key providers are automatically rate-limited with safe defaults. Limits are learned
|
||||
from response headers and adapt over time.
|
||||
</p>
|
||||
<p className="text-sm text-text-muted mb-4">{t("rateLimitingDesc")}</p>
|
||||
|
||||
<div className="rounded-lg bg-black/5 dark:bg-white/5 p-4 mb-4">
|
||||
<h3 className="text-xs font-bold uppercase tracking-wider mb-3 text-text-muted">
|
||||
Default Safety Net
|
||||
{t("defaultSafetyNet")}
|
||||
</h3>
|
||||
<div className="grid grid-cols-3 gap-4">
|
||||
{[
|
||||
@@ -233,7 +232,7 @@ function RateLimitCard({ rateLimitStatus, defaults, onSaveDefaults, saving }) {
|
||||
{rateLimitStatus && rateLimitStatus.length > 0 ? (
|
||||
<div className="space-y-2">
|
||||
<h3 className="text-xs font-bold uppercase tracking-wider text-text-muted">
|
||||
Active Limiters
|
||||
{t("activeLimiters")}
|
||||
</h3>
|
||||
{rateLimitStatus.map((rl, i) => (
|
||||
<div
|
||||
@@ -250,7 +249,7 @@ function RateLimitCard({ rateLimitStatus, defaults, onSaveDefaults, saving }) {
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<p className="text-xs text-text-muted">No active rate limiters yet.</p>
|
||||
<p className="text-xs text-text-muted">{t("noActiveLimiters")}</p>
|
||||
)}
|
||||
</div>
|
||||
</Card>
|
||||
@@ -261,6 +260,7 @@ function RateLimitCard({ rateLimitStatus, defaults, onSaveDefaults, saving }) {
|
||||
function CircuitBreakerCard({ breakers, onReset, loading }) {
|
||||
const activeBreakers = breakers.filter((b) => b.state !== "CLOSED");
|
||||
const totalBreakers = breakers.length;
|
||||
const t = useTranslations("settings");
|
||||
|
||||
return (
|
||||
<Card className="p-0 overflow-hidden">
|
||||
@@ -270,7 +270,7 @@ function CircuitBreakerCard({ breakers, onReset, loading }) {
|
||||
<span className="material-symbols-outlined text-xl text-primary" aria-hidden="true">
|
||||
electrical_services
|
||||
</span>
|
||||
<h2 className="text-lg font-bold">Circuit Breakers</h2>
|
||||
<h2 className="text-lg font-bold">{t("circuitBreakers")}</h2>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-xs text-text-muted">
|
||||
@@ -286,17 +286,14 @@ function CircuitBreakerCard({ breakers, onReset, loading }) {
|
||||
onClick={onReset}
|
||||
disabled={loading}
|
||||
>
|
||||
Reset All
|
||||
{t("resetAll")}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{breakers.length === 0 ? (
|
||||
<p className="text-sm text-text-muted">
|
||||
No circuit breakers active yet. They are created automatically when requests flow
|
||||
through the combo pipeline.
|
||||
</p>
|
||||
<p className="text-sm text-text-muted">{t("noCircuitBreakers")}</p>
|
||||
) : (
|
||||
<div className="space-y-2">
|
||||
{breakers.map((b) => {
|
||||
@@ -343,6 +340,7 @@ function PoliciesCard() {
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [unlocking, setUnlocking] = useState(null);
|
||||
const notify = useNotificationStore();
|
||||
const t = useTranslations("settings");
|
||||
|
||||
const fetchPolicies = useCallback(async () => {
|
||||
try {
|
||||
@@ -394,7 +392,7 @@ function PoliciesCard() {
|
||||
<Card className="p-6">
|
||||
<div className="flex items-center gap-2 text-text-muted animate-pulse">
|
||||
<span className="material-symbols-outlined text-[20px]">policy</span>
|
||||
Loading policies...
|
||||
{t("loadingPolicies")}
|
||||
</div>
|
||||
</Card>
|
||||
);
|
||||
@@ -408,7 +406,7 @@ function PoliciesCard() {
|
||||
<span className="material-symbols-outlined text-xl text-primary" aria-hidden="true">
|
||||
policy
|
||||
</span>
|
||||
<h2 className="text-lg font-bold">Policies & Locked Identifiers</h2>
|
||||
<h2 className="text-lg font-bold">{t("policiesLocked")}</h2>
|
||||
</div>
|
||||
{hasIssues && (
|
||||
<Button size="sm" variant="ghost" onClick={fetchPolicies}>
|
||||
@@ -423,9 +421,7 @@ function PoliciesCard() {
|
||||
<span className="material-symbols-outlined text-[20px]">verified_user</span>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-sm text-text-muted">
|
||||
All systems operational — no lockouts or tripped breakers
|
||||
</p>
|
||||
<p className="text-sm text-text-muted">{t("allOperational")}</p>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
@@ -433,7 +429,7 @@ function PoliciesCard() {
|
||||
{/* Circuit Breakers */}
|
||||
{circuitBreakers.filter((cb) => cb.state !== "closed").length > 0 && (
|
||||
<div className="mb-4">
|
||||
<p className="text-sm font-medium text-text-muted mb-2">Circuit Breakers</p>
|
||||
<p className="text-sm font-medium text-text-muted mb-2">{t("circuitBreakers")}</p>
|
||||
<div className="flex flex-col gap-1.5">
|
||||
{circuitBreakers
|
||||
.filter((cb) => cb.state !== "closed")
|
||||
@@ -479,7 +475,7 @@ function PoliciesCard() {
|
||||
{/* Locked Identifiers */}
|
||||
{lockedIds.length > 0 && (
|
||||
<div>
|
||||
<p className="text-sm font-medium text-text-muted mb-2">Locked Identifiers</p>
|
||||
<p className="text-sm font-medium text-text-muted mb-2">{t("lockedIdentifiers")}</p>
|
||||
<div className="flex flex-col gap-1.5">
|
||||
{lockedIds.map((id, i) => {
|
||||
const identifier = typeof id === "string" ? id : id.identifier || id.id;
|
||||
@@ -506,7 +502,7 @@ function PoliciesCard() {
|
||||
disabled={unlocking === identifier}
|
||||
className="text-xs"
|
||||
>
|
||||
{unlocking === identifier ? "Unlocking..." : "Force Unlock"}
|
||||
{unlocking === identifier ? t("unlocking") : t("forceUnlock")}
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
@@ -527,6 +523,7 @@ export default function ResilienceTab() {
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [saving, setSaving] = useState(false);
|
||||
const [error, setError] = useState(null);
|
||||
const t = useTranslations("settings");
|
||||
|
||||
const loadData = useCallback(async () => {
|
||||
try {
|
||||
@@ -601,7 +598,7 @@ export default function ResilienceTab() {
|
||||
return (
|
||||
<div className="flex items-center justify-center py-12 text-text-muted">
|
||||
<span className="material-symbols-outlined animate-spin mr-2">hourglass_empty</span>
|
||||
Loading resilience status...
|
||||
{t("loadingResilience")}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -614,7 +611,7 @@ export default function ResilienceTab() {
|
||||
<span className="text-sm">{error}</span>
|
||||
</div>
|
||||
<Button size="sm" variant="secondary" icon="refresh" onClick={loadData} className="mt-3">
|
||||
Retry
|
||||
{t("retry")}
|
||||
</Button>
|
||||
</Card>
|
||||
);
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
import { useState, useEffect, useRef } from "react";
|
||||
import { Card, Button, Badge } from "@/shared/components";
|
||||
import { useTranslations } from "next-intl";
|
||||
|
||||
export default function SystemStorageTab() {
|
||||
const [backups, setBackups] = useState([]);
|
||||
@@ -18,6 +19,8 @@ export default function SystemStorageTab() {
|
||||
const [confirmImport, setConfirmImport] = useState(false);
|
||||
const [pendingImportFile, setPendingImportFile] = useState<File | null>(null);
|
||||
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||
const t = useTranslations("settings");
|
||||
const tc = useTranslations("common");
|
||||
const [storageHealth, setStorageHealth] = useState({
|
||||
driver: "sqlite",
|
||||
dbPath: "~/.omniroute/storage.sqlite",
|
||||
@@ -149,7 +152,7 @@ export default function SystemStorageTab() {
|
||||
if (!file.name.endsWith(".sqlite")) {
|
||||
setImportStatus({
|
||||
type: "error",
|
||||
message: "Invalid file type. Only .sqlite files are accepted.",
|
||||
message: t("invalidFileType"),
|
||||
});
|
||||
return;
|
||||
}
|
||||
@@ -224,8 +227,8 @@ export default function SystemStorageTab() {
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<h3 className="text-lg font-semibold">System & Storage</h3>
|
||||
<p className="text-xs text-text-muted">All data stored locally on your machine</p>
|
||||
<h3 className="text-lg font-semibold">{t("systemStorage")}</h3>
|
||||
<p className="text-xs text-text-muted">{t("allDataLocal")}</p>
|
||||
</div>
|
||||
<Badge variant="success" size="sm">
|
||||
{storageHealth.driver || "json"}
|
||||
@@ -235,13 +238,17 @@ export default function SystemStorageTab() {
|
||||
{/* Storage info grid */}
|
||||
<div className="grid grid-cols-2 gap-3 mb-4">
|
||||
<div className="p-3 rounded-lg bg-bg border border-border">
|
||||
<p className="text-[11px] text-text-muted uppercase tracking-wide mb-1">Database Path</p>
|
||||
<p className="text-[11px] text-text-muted uppercase tracking-wide mb-1">
|
||||
{t("databasePath")}
|
||||
</p>
|
||||
<p className="text-sm font-mono text-text-main break-all">
|
||||
{storageHealth.dbPath || "~/.omniroute/storage.sqlite"}
|
||||
</p>
|
||||
</div>
|
||||
<div className="p-3 rounded-lg bg-bg border border-border">
|
||||
<p className="text-[11px] text-text-muted uppercase tracking-wide mb-1">Database Size</p>
|
||||
<p className="text-[11px] text-text-muted uppercase tracking-wide mb-1">
|
||||
{t("databaseSize")}
|
||||
</p>
|
||||
<p className="text-sm font-mono text-text-main">{formatBytes(storageHealth.sizeBytes)}</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -252,7 +259,7 @@ export default function SystemStorageTab() {
|
||||
<span className="material-symbols-outlined text-[14px] mr-1" aria-hidden="true">
|
||||
download
|
||||
</span>
|
||||
Export Database
|
||||
{t("exportDatabase")}
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
@@ -288,13 +295,13 @@ export default function SystemStorageTab() {
|
||||
<span className="material-symbols-outlined text-[14px] mr-1" aria-hidden="true">
|
||||
folder_zip
|
||||
</span>
|
||||
Export All (.tar.gz)
|
||||
{t("exportAll")}
|
||||
</Button>
|
||||
<Button variant="outline" size="sm" onClick={handleImportClick} loading={importLoading}>
|
||||
<span className="material-symbols-outlined text-[14px] mr-1" aria-hidden="true">
|
||||
upload
|
||||
</span>
|
||||
Import Database
|
||||
{t("importDatabase")}
|
||||
</Button>
|
||||
<input
|
||||
ref={fileInputRef}
|
||||
@@ -316,7 +323,7 @@ export default function SystemStorageTab() {
|
||||
warning
|
||||
</span>
|
||||
<div className="flex-1">
|
||||
<p className="text-sm font-medium text-amber-500 mb-1">Confirm Database Import</p>
|
||||
<p className="text-sm font-medium text-amber-500 mb-1">{t("confirmDbImport")}</p>
|
||||
<p className="text-xs text-text-muted mb-2">
|
||||
This will replace <strong>all current data</strong> with the content from{" "}
|
||||
<span className="font-mono">{pendingImportFile.name}</span>. A backup will be
|
||||
@@ -329,10 +336,10 @@ export default function SystemStorageTab() {
|
||||
onClick={handleImportConfirm}
|
||||
className="!bg-amber-500 hover:!bg-amber-600"
|
||||
>
|
||||
Yes, Import
|
||||
{t("yesImport")}
|
||||
</Button>
|
||||
<Button variant="outline" size="sm" onClick={handleImportCancel}>
|
||||
Cancel
|
||||
{tc("cancel")}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -364,11 +371,11 @@ export default function SystemStorageTab() {
|
||||
schedule
|
||||
</span>
|
||||
<div>
|
||||
<p className="text-sm font-medium">Last Backup</p>
|
||||
<p className="text-sm font-medium">{t("lastBackup")}</p>
|
||||
<p className="text-xs text-text-muted">
|
||||
{storageHealth.lastBackupAt
|
||||
? `${new Date(storageHealth.lastBackupAt).toLocaleString("pt-BR")} (${formatRelativeTime(storageHealth.lastBackupAt)})`
|
||||
: "No backup yet"}
|
||||
: t("noBackupYet")}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -381,7 +388,7 @@ export default function SystemStorageTab() {
|
||||
<span className="material-symbols-outlined text-[14px] mr-1" aria-hidden="true">
|
||||
backup
|
||||
</span>
|
||||
Backup Now
|
||||
{t("backupNow")}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
@@ -419,7 +426,7 @@ export default function SystemStorageTab() {
|
||||
>
|
||||
restore
|
||||
</span>
|
||||
<p className="font-medium">Backup & Restore</p>
|
||||
<p className="font-medium">{t("backupRestore")}</p>
|
||||
</div>
|
||||
<Button
|
||||
variant="outline"
|
||||
@@ -429,13 +436,10 @@ export default function SystemStorageTab() {
|
||||
if (!backupsExpanded && backups.length === 0) loadBackups();
|
||||
}}
|
||||
>
|
||||
{backupsExpanded ? "Hide" : "View Backups"}
|
||||
{backupsExpanded ? t("hide") : t("viewBackups")}
|
||||
</Button>
|
||||
</div>
|
||||
<p className="text-xs text-text-muted mb-3">
|
||||
Database snapshots are created automatically before restore and every 15 minutes when data
|
||||
changes. Retention: 24 hourly + 30 daily backups with smart rotation.
|
||||
</p>
|
||||
<p className="text-xs text-text-muted mb-3">{t("backupRetentionDesc")}</p>
|
||||
|
||||
{restoreStatus.message && (
|
||||
<div
|
||||
@@ -465,7 +469,7 @@ export default function SystemStorageTab() {
|
||||
>
|
||||
progress_activity
|
||||
</span>
|
||||
Loading backups...
|
||||
{t("loadingBackups")}
|
||||
</div>
|
||||
) : backups.length === 0 ? (
|
||||
<div className="text-center py-6 text-text-muted text-sm">
|
||||
@@ -475,7 +479,7 @@ export default function SystemStorageTab() {
|
||||
>
|
||||
folder_off
|
||||
</span>
|
||||
No backups available yet. Backups will be created automatically when data changes.
|
||||
{t("noBackupsYet")}
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
@@ -490,7 +494,7 @@ export default function SystemStorageTab() {
|
||||
<span className="material-symbols-outlined text-[14px]" aria-hidden="true">
|
||||
refresh
|
||||
</span>
|
||||
Refresh
|
||||
{t("refresh")}
|
||||
</button>
|
||||
</div>
|
||||
{backups.map((backup) => (
|
||||
@@ -531,7 +535,7 @@ export default function SystemStorageTab() {
|
||||
<div className="flex items-center gap-2 ml-3">
|
||||
{confirmRestoreId === backup.id ? (
|
||||
<>
|
||||
<span className="text-xs text-amber-500 font-medium">Confirm?</span>
|
||||
<span className="text-xs text-amber-500 font-medium">{t("confirm")}</span>
|
||||
<Button
|
||||
variant="primary"
|
||||
size="sm"
|
||||
@@ -539,14 +543,14 @@ export default function SystemStorageTab() {
|
||||
loading={restoringId === backup.id}
|
||||
className="!bg-amber-500 hover:!bg-amber-600"
|
||||
>
|
||||
Yes
|
||||
{t("yes")}
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => setConfirmRestoreId(null)}
|
||||
>
|
||||
No
|
||||
{t("no")}
|
||||
</Button>
|
||||
</>
|
||||
) : (
|
||||
@@ -561,7 +565,7 @@ export default function SystemStorageTab() {
|
||||
>
|
||||
restore
|
||||
</span>
|
||||
Restore
|
||||
{t("restore")}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -749,7 +749,110 @@
|
||||
"failedCreateChain": "Failed to create chain",
|
||||
"failedDeleteChain": "Failed to delete chain",
|
||||
"fillModelAndProviders": "Please fill model name and providers",
|
||||
"addAtLeastOneProvider": "Add at least one provider"
|
||||
"addAtLeastOneProvider": "Add at least one provider",
|
||||
"comboDefaultsTitle": "Combo Defaults",
|
||||
"globalComboConfig": "Global combo configuration",
|
||||
"defaultStrategy": "Default Strategy",
|
||||
"defaultStrategyDesc": "Applied to new combos without explicit strategy",
|
||||
"priority": "Priority",
|
||||
"weighted": "Weighted",
|
||||
"healthCheck": "Health Check",
|
||||
"healthCheckDesc": "Pre-check provider availability",
|
||||
"trackMetrics": "Track Metrics",
|
||||
"trackMetricsDesc": "Record per-combo request metrics",
|
||||
"providerOverrides": "Provider Overrides",
|
||||
"providerOverridesDesc": "Override timeout and retries per provider. Provider settings override global defaults.",
|
||||
"retries": "retries",
|
||||
"ms": "ms",
|
||||
"saveComboDefaults": "Save Combo Defaults",
|
||||
"maxNestingDepth": "Max Nesting Depth",
|
||||
"concurrencyPerModel": "Concurrency / Model",
|
||||
"queueTimeout": "Queue Timeout (ms)",
|
||||
"providerProfiles": "Provider Profiles",
|
||||
"providerProfilesDesc": "Separate resilience settings for OAuth (session-based) and API Key (metered) providers. OAuth providers have stricter thresholds due to lower rate limits.",
|
||||
"oauthProviders": "OAuth Providers",
|
||||
"apiKeyProviders": "API Key Providers",
|
||||
"transientCooldown": "Transient Cooldown",
|
||||
"rateLimitCooldown": "Rate Limit Cooldown",
|
||||
"maxBackoffLevel": "Max Backoff Level",
|
||||
"cbThreshold": "CB Threshold",
|
||||
"cbResetTime": "CB Reset Time",
|
||||
"rateLimiting": "Rate Limiting",
|
||||
"rateLimitingDesc": "API Key providers are automatically rate-limited with safe defaults. Limits are learned from response headers and adapt over time.",
|
||||
"defaultSafetyNet": "Default Safety Net",
|
||||
"rpm": "RPM",
|
||||
"minGap": "Min Gap",
|
||||
"maxConcurrent": "Max Concurrent",
|
||||
"activeLimiters": "Active Limiters",
|
||||
"noActiveLimiters": "No active rate limiters yet.",
|
||||
"reservoir": "Reservoir",
|
||||
"running": "Running",
|
||||
"queued": "Queued",
|
||||
"circuitBreakers": "Circuit Breakers",
|
||||
"tripped": "{count} tripped",
|
||||
"healthy": "{count} healthy",
|
||||
"resetAll": "Reset All",
|
||||
"noCircuitBreakers": "No circuit breakers active yet. They are created automatically when requests flow through the combo pipeline.",
|
||||
"failures": "{count} failure(s)",
|
||||
"policiesLocked": "Policies & Locked Identifiers",
|
||||
"allOperational": "All systems operational — no lockouts or tripped breakers",
|
||||
"loadingPolicies": "Loading policies...",
|
||||
"lockedIdentifiers": "Locked Identifiers",
|
||||
"forceUnlock": "Force Unlock",
|
||||
"unlocking": "Unlocking...",
|
||||
"failedUnlock": "Failed to unlock",
|
||||
"loadingResilience": "Loading resilience status...",
|
||||
"retry": "Retry",
|
||||
"systemStorage": "System & Storage",
|
||||
"allDataLocal": "All data stored locally on your machine",
|
||||
"databasePath": "Database Path",
|
||||
"exportDatabase": "Export Database",
|
||||
"exportAll": "Export All (.tar.gz)",
|
||||
"importDatabase": "Import Database",
|
||||
"confirmDbImport": "Confirm Database Import",
|
||||
"confirmDbImportDesc": "This will replace all current data with the content from {file}. A backup will be created automatically before the import.",
|
||||
"yesImport": "Yes, Import",
|
||||
"lastBackup": "Last Backup",
|
||||
"noBackupYet": "No backup yet",
|
||||
"backupNow": "Backup Now",
|
||||
"backupRestore": "Backup & Restore",
|
||||
"viewBackups": "View Backups",
|
||||
"hide": "Hide",
|
||||
"backupRetentionDesc": "Database snapshots are created automatically before restore and every 15 minutes when data changes. Retention: 24 hourly + 30 daily backups with smart rotation.",
|
||||
"loadingBackups": "Loading backups...",
|
||||
"noBackupsYet": "No backups available yet. Backups will be created automatically when data changes.",
|
||||
"backupsAvailable": "{count} backup(s) available",
|
||||
"refresh": "Refresh",
|
||||
"confirm": "Confirm?",
|
||||
"yes": "Yes",
|
||||
"no": "No",
|
||||
"restore": "Restore",
|
||||
"invalidFileType": "Invalid file type. Only .sqlite files are accepted.",
|
||||
"noChangesSinceBackup": "No changes since last backup",
|
||||
"backupFailed": "Backup failed",
|
||||
"restoreFailed": "Restore failed",
|
||||
"importFailed": "Import failed",
|
||||
"errorDuringRestore": "An error occurred during restore",
|
||||
"errorDuringImport": "An error occurred during import",
|
||||
"modelPricing": "Model Pricing",
|
||||
"modelPricingDesc": "Configure cost rates per model • All rates in $/1M tokens",
|
||||
"providers": "Providers",
|
||||
"registry": "Registry",
|
||||
"priced": "Priced",
|
||||
"searchProvidersModels": "Search providers or models...",
|
||||
"showAll": "Show All",
|
||||
"noProvidersMatch": "No providers match your search.",
|
||||
"howPricingWorks": "How Pricing Works",
|
||||
"cacheWrite": "Cache Write",
|
||||
"unsaved": "unsaved",
|
||||
"resetDefaults": "Reset Defaults",
|
||||
"saveProvider": "Save Provider",
|
||||
"saving": "Saving...",
|
||||
"model": "Model",
|
||||
"models": "models",
|
||||
"withPricing": "with pricing configured",
|
||||
"loadingPricing": "Loading pricing data...",
|
||||
"databaseSize": "Database Size"
|
||||
},
|
||||
"translator": {
|
||||
"title": "Translator",
|
||||
|
||||
@@ -749,7 +749,110 @@
|
||||
"failedCreateChain": "Falha ao criar cadeia",
|
||||
"failedDeleteChain": "Falha ao excluir cadeia",
|
||||
"fillModelAndProviders": "Preencha o nome do modelo e os provedores",
|
||||
"addAtLeastOneProvider": "Adicione pelo menos um provedor"
|
||||
"addAtLeastOneProvider": "Adicione pelo menos um provedor",
|
||||
"comboDefaultsTitle": "Padrões de Combo",
|
||||
"globalComboConfig": "Configuração global de combos",
|
||||
"defaultStrategy": "Estratégia Padrão",
|
||||
"defaultStrategyDesc": "Aplicada a novos combos sem estratégia explícita",
|
||||
"priority": "Prioridade",
|
||||
"weighted": "Ponderado",
|
||||
"healthCheck": "Verificação de Saúde",
|
||||
"healthCheckDesc": "Verificar disponibilidade do provedor antes",
|
||||
"trackMetrics": "Rastrear Métricas",
|
||||
"trackMetricsDesc": "Registrar métricas de requisição por combo",
|
||||
"providerOverrides": "Sobrescritas por Provedor",
|
||||
"providerOverridesDesc": "Substituir timeout e tentativas por provedor. Configurações do provedor substituem os padrões globais.",
|
||||
"retries": "tentativas",
|
||||
"ms": "ms",
|
||||
"saveComboDefaults": "Salvar Padrões de Combo",
|
||||
"maxNestingDepth": "Profundidade Máx. de Aninhamento",
|
||||
"concurrencyPerModel": "Concorrência / Modelo",
|
||||
"queueTimeout": "Timeout da Fila (ms)",
|
||||
"providerProfiles": "Perfis de Provedor",
|
||||
"providerProfilesDesc": "Configurações de resiliência separadas para provedores OAuth (baseados em sessão) e API Key (medidos). Provedores OAuth têm limites mais rigorosos devido a taxas mais baixas.",
|
||||
"oauthProviders": "Provedores OAuth",
|
||||
"apiKeyProviders": "Provedores API Key",
|
||||
"transientCooldown": "Cooldown Transitório",
|
||||
"rateLimitCooldown": "Cooldown de Rate Limit",
|
||||
"maxBackoffLevel": "Nível Máx. de Backoff",
|
||||
"cbThreshold": "Limiar do CB",
|
||||
"cbResetTime": "Tempo de Reset do CB",
|
||||
"rateLimiting": "Limitação de Taxa",
|
||||
"rateLimitingDesc": "Provedores API Key são automaticamente limitados com padrões seguros. Limites são aprendidos dos cabeçalhos de resposta e se adaptam ao longo do tempo.",
|
||||
"defaultSafetyNet": "Rede de Segurança Padrão",
|
||||
"rpm": "RPM",
|
||||
"minGap": "Intervalo Mín.",
|
||||
"maxConcurrent": "Máx. Concorrentes",
|
||||
"activeLimiters": "Limitadores Ativos",
|
||||
"noActiveLimiters": "Nenhum limitador de taxa ativo ainda.",
|
||||
"reservoir": "Reservatório",
|
||||
"running": "Em Execução",
|
||||
"queued": "Na Fila",
|
||||
"circuitBreakers": "Disjuntores",
|
||||
"tripped": "{count} aberto(s)",
|
||||
"healthy": "{count} saudável(is)",
|
||||
"resetAll": "Resetar Todos",
|
||||
"noCircuitBreakers": "Nenhum disjuntor ativo ainda. Eles são criados automaticamente quando requisições passam pelo pipeline de combos.",
|
||||
"failures": "{count} falha(s)",
|
||||
"policiesLocked": "Políticas e Identificadores Bloqueados",
|
||||
"allOperational": "Todos os sistemas operacionais — sem bloqueios ou disjuntores ativados",
|
||||
"loadingPolicies": "Carregando políticas...",
|
||||
"lockedIdentifiers": "Identificadores Bloqueados",
|
||||
"forceUnlock": "Forçar Desbloqueio",
|
||||
"unlocking": "Desbloqueando...",
|
||||
"failedUnlock": "Falha ao desbloquear",
|
||||
"loadingResilience": "Carregando status de resiliência...",
|
||||
"retry": "Tentar Novamente",
|
||||
"systemStorage": "Sistema e Armazenamento",
|
||||
"allDataLocal": "Todos os dados armazenados localmente na sua máquina",
|
||||
"databasePath": "Caminho do Banco de Dados",
|
||||
"exportDatabase": "Exportar Banco de Dados",
|
||||
"exportAll": "Exportar Tudo (.tar.gz)",
|
||||
"importDatabase": "Importar Banco de Dados",
|
||||
"confirmDbImport": "Confirmar Importação do Banco",
|
||||
"confirmDbImportDesc": "Isso substituirá todos os dados atuais pelo conteúdo de {file}. Um backup será criado automaticamente antes da importação.",
|
||||
"yesImport": "Sim, Importar",
|
||||
"lastBackup": "Último Backup",
|
||||
"noBackupYet": "Nenhum backup ainda",
|
||||
"backupNow": "Backup Agora",
|
||||
"backupRestore": "Backup e Restauração",
|
||||
"viewBackups": "Ver Backups",
|
||||
"hide": "Ocultar",
|
||||
"backupRetentionDesc": "Snapshots do banco são criados automaticamente antes da restauração e a cada 15 minutos quando há alterações. Retenção: 24 horários + 30 diários com rotação inteligente.",
|
||||
"loadingBackups": "Carregando backups...",
|
||||
"noBackupsYet": "Nenhum backup disponível ainda. Backups serão criados automaticamente quando houver alterações.",
|
||||
"backupsAvailable": "{count} backup(s) disponível(is)",
|
||||
"refresh": "Atualizar",
|
||||
"confirm": "Confirmar?",
|
||||
"yes": "Sim",
|
||||
"no": "Não",
|
||||
"restore": "Restaurar",
|
||||
"invalidFileType": "Tipo de arquivo inválido. Apenas arquivos .sqlite são aceitos.",
|
||||
"noChangesSinceBackup": "Sem alterações desde o último backup",
|
||||
"backupFailed": "Falha no backup",
|
||||
"restoreFailed": "Falha na restauração",
|
||||
"importFailed": "Falha na importação",
|
||||
"errorDuringRestore": "Ocorreu um erro durante a restauração",
|
||||
"errorDuringImport": "Ocorreu um erro durante a importação",
|
||||
"modelPricing": "Preços de Modelos",
|
||||
"modelPricingDesc": "Configure taxas de custo por modelo • Todas as taxas em $/1M tokens",
|
||||
"providers": "Provedores",
|
||||
"registry": "Registro",
|
||||
"priced": "Com Preço",
|
||||
"searchProvidersModels": "Buscar provedores ou modelos...",
|
||||
"showAll": "Mostrar Todos",
|
||||
"noProvidersMatch": "Nenhum provedor corresponde à sua busca.",
|
||||
"howPricingWorks": "Como os Preços Funcionam",
|
||||
"cacheWrite": "Escrita em Cache",
|
||||
"unsaved": "não salvo",
|
||||
"resetDefaults": "Restaurar Padrões",
|
||||
"saveProvider": "Salvar Provedor",
|
||||
"saving": "Salvando...",
|
||||
"model": "Modelo",
|
||||
"models": "modelos",
|
||||
"withPricing": "com preço configurado",
|
||||
"loadingPricing": "Carregando dados de preços...",
|
||||
"databaseSize": "Tamanho do Banco de Dados"
|
||||
},
|
||||
"translator": {
|
||||
"title": "Tradutor",
|
||||
|
||||
Reference in New Issue
Block a user