mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-08-04 22:32:12 +03:00
chore(i18n): replace hardcoded UI text with t() calls across dashboard (round 1)
Subagents refactored 8 high-impact dashboard pages, replacing 81 of the 407 hardcoded English/PT strings flagged by the audit with proper useTranslations() lookups. Added 73 corresponding keys to en.json across the home, apiManager, providers, settings, and usage namespaces. Pages affected: - BudgetTab (27 → 0) - HomePageClient (2 → 0) - RoutingTab (25 → 7) - ResilienceTab (38 → 18) - SystemStorageTab (42 → 21) - providers/[id] (17 → 15) - ApiManagerPageClient (14 → 13) - OneproxyTab (13 → 10) Also adds two helper scripts: - scripts/i18n/extract-keys-from-diff.mjs — extracts new keys from git diff - scripts/i18n/merge-keys.mjs — merges a pending-keys JSON into en.json Remaining hardcoded strings will be addressed in follow-up rounds.
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -156,3 +156,4 @@ _ideia/_triage.json
|
|||||||
|
|
||||||
# i18n audit artifact (generated by scripts/i18n/audit-dashboard-pages.mjs)
|
# i18n audit artifact (generated by scripts/i18n/audit-dashboard-pages.mjs)
|
||||||
scripts/i18n/_audit.json
|
scripts/i18n/_audit.json
|
||||||
|
scripts/i18n/_pending-keys.json
|
||||||
|
|||||||
130
scripts/i18n/extract-keys-from-diff.mjs
Normal file
130
scripts/i18n/extract-keys-from-diff.mjs
Normal file
@@ -0,0 +1,130 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
/**
|
||||||
|
* Extract NEW i18n keys created by subagents from git diff.
|
||||||
|
*
|
||||||
|
* For each modified .tsx file, walk the unified diff and pair "-" lines
|
||||||
|
* (containing literal English text) with their following "+" lines
|
||||||
|
* (now using `t("key")`). When we find a stable pairing, record the key
|
||||||
|
* and its original English value.
|
||||||
|
*
|
||||||
|
* The output is a per-namespace map ready to merge into en.json.
|
||||||
|
*/
|
||||||
|
import { execSync } from "node:child_process";
|
||||||
|
|
||||||
|
const diff = execSync('git diff --unified=0 "src/app/(dashboard)"', {
|
||||||
|
encoding: "utf8",
|
||||||
|
maxBuffer: 32 * 1024 * 1024,
|
||||||
|
});
|
||||||
|
|
||||||
|
const blocks = diff.split(/^diff --git /m).slice(1);
|
||||||
|
|
||||||
|
const allPairs = []; // { file, removed, added }
|
||||||
|
|
||||||
|
for (const block of blocks) {
|
||||||
|
const headLine = block.split("\n")[0];
|
||||||
|
const fileMatch = headLine.match(/b\/(.+?)$/);
|
||||||
|
const file = fileMatch ? fileMatch[1] : "?";
|
||||||
|
const lines = block.split("\n");
|
||||||
|
|
||||||
|
// Walk in groups: consecutive "-" lines, then consecutive "+" lines.
|
||||||
|
// Pair them positionally (removed[i] ↔ added[i]).
|
||||||
|
let removed = [];
|
||||||
|
let added = [];
|
||||||
|
function flush() {
|
||||||
|
const n = Math.min(removed.length, added.length);
|
||||||
|
for (let i = 0; i < n; i++) allPairs.push({ file, removed: removed[i], added: added[i] });
|
||||||
|
// If removed.length > added.length, pair remaining removed with last added (multi-string in one new line)
|
||||||
|
if (removed.length > added.length && added.length > 0) {
|
||||||
|
for (let i = added.length; i < removed.length; i++) {
|
||||||
|
allPairs.push({ file, removed: removed[i], added: added[added.length - 1] });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
removed = [];
|
||||||
|
added = [];
|
||||||
|
}
|
||||||
|
for (const line of lines) {
|
||||||
|
if (line.startsWith("---") || line.startsWith("+++")) continue;
|
||||||
|
if (line.startsWith("-")) {
|
||||||
|
if (added.length) flush();
|
||||||
|
removed.push(line.slice(1));
|
||||||
|
} else if (line.startsWith("+")) {
|
||||||
|
added.push(line.slice(1));
|
||||||
|
} else {
|
||||||
|
flush();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
flush();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Patterns inside JSX or attribute strings */
|
||||||
|
const T_CALL_RE = /\bt\(\s*["']([^"']+)["']\s*\)/g;
|
||||||
|
const JSX_TEXT_RE = />([^<>{}\n]+)</g;
|
||||||
|
const ATTR_RE = /\b(?:title|placeholder|aria-label|alt|label)\s*=\s*["']([^"']+)["']/g;
|
||||||
|
|
||||||
|
const newKeys = new Map(); // key -> { value, file }
|
||||||
|
|
||||||
|
function recordKey(key, value, file) {
|
||||||
|
const trimmed = value.trim();
|
||||||
|
if (!trimmed) return;
|
||||||
|
// Ignore if value contains JSX expression syntax — those were dynamic
|
||||||
|
if (/^\{|\}$/.test(trimmed)) return;
|
||||||
|
if (!newKeys.has(key)) {
|
||||||
|
newKeys.set(key, { value: trimmed, file });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const { file, removed, added } of allPairs) {
|
||||||
|
// Extract every t("xxx") key from the "added" line
|
||||||
|
const tKeys = [...added.matchAll(T_CALL_RE)].map((m) => m[1]);
|
||||||
|
if (!tKeys.length) continue;
|
||||||
|
|
||||||
|
// Extract candidate strings from the "removed" line: JSX text + attr values
|
||||||
|
const candidates = [];
|
||||||
|
for (const m of removed.matchAll(JSX_TEXT_RE)) candidates.push(m[1]);
|
||||||
|
for (const m of removed.matchAll(ATTR_RE)) candidates.push(m[1]);
|
||||||
|
|
||||||
|
// Direct strings without surrounding markup (rare)
|
||||||
|
const stripped = removed
|
||||||
|
.replace(/<[^<>]+>/g, "")
|
||||||
|
.replace(/\bt\([^)]*\)/g, "")
|
||||||
|
.trim();
|
||||||
|
if (stripped && /[A-Za-z]/.test(stripped)) candidates.push(stripped);
|
||||||
|
|
||||||
|
// For each tKey in added, try to align with the candidate at the same index.
|
||||||
|
// The agents typically replaced strings in the same left-to-right order.
|
||||||
|
for (let i = 0; i < tKeys.length; i++) {
|
||||||
|
const candidate = candidates[i] ?? candidates[candidates.length - 1];
|
||||||
|
if (!candidate) continue;
|
||||||
|
recordKey(tKeys[i], candidate, file);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Group by file's primary namespace via useTranslations() call in file
|
||||||
|
import { readFileSync } from "node:fs";
|
||||||
|
function inferNamespaceForFile(file) {
|
||||||
|
try {
|
||||||
|
const src = readFileSync(file, "utf8");
|
||||||
|
const m = src.match(/useTranslations\s*\(\s*["']([^"']+)["']\s*\)/);
|
||||||
|
return m?.[1] ?? "common";
|
||||||
|
} catch {
|
||||||
|
return "common";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const byNamespace = new Map();
|
||||||
|
for (const [key, { value, file }] of newKeys) {
|
||||||
|
const ns = inferNamespaceForFile(file);
|
||||||
|
if (!byNamespace.has(ns)) byNamespace.set(ns, []);
|
||||||
|
byNamespace.get(ns).push({ key, value });
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const [ns, items] of byNamespace) {
|
||||||
|
console.log(`\nNEW_KEYS_FOR_NAMESPACE: ${ns}`);
|
||||||
|
console.log("{");
|
||||||
|
for (const { key, value } of items) {
|
||||||
|
// Escape value for JSON
|
||||||
|
console.log(` ${JSON.stringify(key)}: ${JSON.stringify(value)},`);
|
||||||
|
}
|
||||||
|
console.log("}");
|
||||||
|
}
|
||||||
|
console.log(`\nTotal extracted: ${newKeys.size} keys across ${byNamespace.size} namespaces`);
|
||||||
38
scripts/i18n/merge-keys.mjs
Normal file
38
scripts/i18n/merge-keys.mjs
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
/**
|
||||||
|
* Merge keys from scripts/i18n/_pending-keys.json into src/i18n/messages/en.json
|
||||||
|
*
|
||||||
|
* Format of _pending-keys.json:
|
||||||
|
* { "namespace": { "key": "value", ... }, ... }
|
||||||
|
*
|
||||||
|
* Keys are appended to the END of each namespace block. Existing keys with
|
||||||
|
* the same name are preserved (we never overwrite).
|
||||||
|
*/
|
||||||
|
import { readFileSync, writeFileSync } from "node:fs";
|
||||||
|
|
||||||
|
const SRC = "src/i18n/messages/en.json";
|
||||||
|
const PENDING = "scripts/i18n/_pending-keys.json";
|
||||||
|
|
||||||
|
const enJson = JSON.parse(readFileSync(SRC, "utf8"));
|
||||||
|
const pending = JSON.parse(readFileSync(PENDING, "utf8"));
|
||||||
|
|
||||||
|
let added = 0;
|
||||||
|
let skipped = 0;
|
||||||
|
for (const [ns, keys] of Object.entries(pending)) {
|
||||||
|
if (!Object.prototype.hasOwnProperty.call(enJson, ns)) {
|
||||||
|
console.warn(`! namespace missing in en.json: ${ns} — skipping`);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const target = enJson[ns];
|
||||||
|
for (const [k, v] of Object.entries(keys)) {
|
||||||
|
if (Object.prototype.hasOwnProperty.call(target, k)) {
|
||||||
|
skipped++;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
target[k] = v;
|
||||||
|
added++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
writeFileSync(SRC, JSON.stringify(enJson, null, 2) + "\n");
|
||||||
|
console.log(`✓ merged ${added} new keys (${skipped} skipped — already present)`);
|
||||||
@@ -603,7 +603,7 @@ export default function HomePageClient({ machineId }: HomePageClientProps) {
|
|||||||
<span className="material-symbols-outlined text-[18px]">check_circle</span>
|
<span className="material-symbols-outlined text-[18px]">check_circle</span>
|
||||||
{updateSteps.find((s) => s.step === "complete")?.message || "Update complete!"}
|
{updateSteps.find((s) => s.step === "complete")?.message || "Update complete!"}
|
||||||
</p>
|
</p>
|
||||||
<p className="text-xs text-text-muted mt-1">Reloading page automatically...</p>
|
<p className="text-xs text-text-muted mt-1">{t("reloadingPageAutomatically")}</p>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@@ -788,7 +788,7 @@ export default function HomePageClient({ machineId }: HomePageClientProps) {
|
|||||||
<Card>
|
<Card>
|
||||||
<div className="flex items-center justify-between mb-3">
|
<div className="flex items-center justify-between mb-3">
|
||||||
<div>
|
<div>
|
||||||
<h2 className="text-base font-semibold">Provider Topology</h2>
|
<h2 className="text-base font-semibold">{t("providerTopology")}</h2>
|
||||||
<p className="text-xs text-text-muted">
|
<p className="text-xs text-text-muted">
|
||||||
Connected providers routing through OmniRoute in real time
|
Connected providers routing through OmniRoute in real time
|
||||||
</p>
|
</p>
|
||||||
|
|||||||
@@ -1279,7 +1279,7 @@ const PermissionsModal = memo(function PermissionsModal({
|
|||||||
{/* Max Sessions Limit (T08) */}
|
{/* Max Sessions Limit (T08) */}
|
||||||
<div className="flex items-start justify-between gap-3 p-3 rounded-lg border border-border bg-surface/40">
|
<div className="flex items-start justify-between gap-3 p-3 rounded-lg border border-border bg-surface/40">
|
||||||
<div className="flex flex-col gap-1">
|
<div className="flex flex-col gap-1">
|
||||||
<p className="text-sm font-medium text-text-main">Max Active Sessions</p>
|
<p className="text-sm font-medium text-text-main">{t("maxActiveSessions")}</p>
|
||||||
<p className="text-xs text-text-muted">
|
<p className="text-xs text-text-muted">
|
||||||
0 = unlimited. Return 429 when this key exceeds concurrent sticky sessions.
|
0 = unlimited. Return 429 when this key exceeds concurrent sticky sessions.
|
||||||
</p>
|
</p>
|
||||||
|
|||||||
@@ -3440,13 +3440,13 @@ export default function ProviderDetailPage() {
|
|||||||
<div className="flex min-w-0 flex-wrap items-center gap-2">
|
<div className="flex min-w-0 flex-wrap items-center gap-2">
|
||||||
<h2 className="text-lg font-semibold">{t("connections")}</h2>
|
<h2 className="text-lg font-semibold">{t("connections")}</h2>
|
||||||
{providerId === "codex" && (
|
{providerId === "codex" && (
|
||||||
<div title="Apply Codex Fast tier to all Codex connections by default">
|
<div title={t("providerDetailFastTierTooltip")}>
|
||||||
<Toggle
|
<Toggle
|
||||||
size="sm"
|
size="sm"
|
||||||
checked={codexGlobalFastServiceTier}
|
checked={codexGlobalFastServiceTier}
|
||||||
onChange={handleToggleCodexGlobalFastServiceTier}
|
onChange={handleToggleCodexGlobalFastServiceTier}
|
||||||
disabled={savingCodexGlobalFastServiceTier}
|
disabled={savingCodexGlobalFastServiceTier}
|
||||||
label="Fast default"
|
label={t("providerDetailFastDefaultLabel")}
|
||||||
ariaLabel="Toggle Codex Fast default"
|
ariaLabel="Toggle Codex Fast default"
|
||||||
className="rounded-lg border border-sky-500/20 bg-sky-500/5 px-2 py-1"
|
className="rounded-lg border border-sky-500/20 bg-sky-500/5 px-2 py-1"
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -140,7 +140,7 @@ export default function OneproxyTab() {
|
|||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<div>
|
<div>
|
||||||
<h2 className="text-lg font-semibold text-text-main">1proxy Free Proxy Marketplace</h2>
|
<h2 className="text-lg font-semibold text-text-main">{t("oneproxyTitle")}</h2>
|
||||||
<p className="text-sm text-text-muted mt-1">
|
<p className="text-sm text-text-muted mt-1">
|
||||||
Fetch and rotate free validated proxies from the 1proxy community platform
|
Fetch and rotate free validated proxies from the 1proxy community platform
|
||||||
</p>
|
</p>
|
||||||
@@ -173,7 +173,7 @@ export default function OneproxyTab() {
|
|||||||
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
|
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
|
||||||
<Card className="p-4">
|
<Card className="p-4">
|
||||||
<div className="text-2xl font-bold text-text-main">{stats.total}</div>
|
<div className="text-2xl font-bold text-text-main">{stats.total}</div>
|
||||||
<div className="text-sm text-text-muted">Total Proxies</div>
|
<div className="text-sm text-text-muted">{t("oneproxyTotalProxies")}</div>
|
||||||
</Card>
|
</Card>
|
||||||
<Card className="p-4">
|
<Card className="p-4">
|
||||||
<div className="text-2xl font-bold text-green-600">{stats.active}</div>
|
<div className="text-2xl font-bold text-green-600">{stats.active}</div>
|
||||||
@@ -183,7 +183,7 @@ export default function OneproxyTab() {
|
|||||||
<div className="text-2xl font-bold text-text-main">
|
<div className="text-2xl font-bold text-text-main">
|
||||||
{stats.avgQuality != null ? `${stats.avgQuality}` : "—"}
|
{stats.avgQuality != null ? `${stats.avgQuality}` : "—"}
|
||||||
</div>
|
</div>
|
||||||
<div className="text-sm text-text-muted">Avg Quality</div>
|
<div className="text-sm text-text-muted">{t("oneproxyAvgQuality")}</div>
|
||||||
</Card>
|
</Card>
|
||||||
<Card className="p-4">
|
<Card className="p-4">
|
||||||
<div className="text-2xl font-bold text-text-main">
|
<div className="text-2xl font-bold text-text-main">
|
||||||
|
|||||||
@@ -62,16 +62,17 @@ function SectionDescription({
|
|||||||
trigger: string;
|
trigger: string;
|
||||||
effect: string;
|
effect: string;
|
||||||
}) {
|
}) {
|
||||||
|
const t = useTranslations("settings");
|
||||||
return (
|
return (
|
||||||
<div className="grid grid-cols-1 gap-2 text-xs text-text-muted sm:grid-cols-3">
|
<div className="grid grid-cols-1 gap-2 text-xs text-text-muted sm:grid-cols-3">
|
||||||
<div>
|
<div>
|
||||||
<span className="font-semibold text-text-main">Scope:</span> {scope}
|
<span className="font-semibold text-text-main">{t("resilienceScope")}</span> {scope}
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<span className="font-semibold text-text-main">Trigger:</span> {trigger}
|
<span className="font-semibold text-text-main">{t("resilienceTrigger")}</span> {trigger}
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<span className="font-semibold text-text-main">Effect:</span> {effect}
|
<span className="font-semibold text-text-main">{t("resilienceEffect")}</span> {effect}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@@ -203,6 +204,7 @@ function RequestQueueCard({
|
|||||||
onSave: (next: RequestQueueSettings) => Promise<void>;
|
onSave: (next: RequestQueueSettings) => Promise<void>;
|
||||||
saving: boolean;
|
saving: boolean;
|
||||||
}) {
|
}) {
|
||||||
|
const t = useTranslations("settings");
|
||||||
const [editing, setEditing] = useState(false);
|
const [editing, setEditing] = useState(false);
|
||||||
const [draft, setDraft] = useState(value);
|
const [draft, setDraft] = useState(value);
|
||||||
|
|
||||||
@@ -216,7 +218,7 @@ function RequestQueueCard({
|
|||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<span className="material-symbols-outlined text-xl text-primary">speed</span>
|
<span className="material-symbols-outlined text-xl text-primary">speed</span>
|
||||||
<h2 className="text-lg font-bold">Request Queue & Rate</h2>
|
<h2 className="text-lg font-bold">{t("resilienceRequestQueueTitle")}</h2>
|
||||||
</div>
|
</div>
|
||||||
<SectionDescription
|
<SectionDescription
|
||||||
scope="Per request queue"
|
scope="Per request queue"
|
||||||
@@ -248,7 +250,7 @@ function RequestQueueCard({
|
|||||||
{editing ? (
|
{editing ? (
|
||||||
<>
|
<>
|
||||||
<BooleanField
|
<BooleanField
|
||||||
label="Auto-enable for API-key providers"
|
label={t("resilienceAutoEnableApiKeyProviders")}
|
||||||
description="Enable queue protection by default for active API-key connections."
|
description="Enable queue protection by default for active API-key connections."
|
||||||
checked={draft.autoEnableApiKeyProviders}
|
checked={draft.autoEnableApiKeyProviders}
|
||||||
onChange={(autoEnableApiKeyProviders) =>
|
onChange={(autoEnableApiKeyProviders) =>
|
||||||
@@ -256,13 +258,13 @@ function RequestQueueCard({
|
|||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
<NumberField
|
<NumberField
|
||||||
label="Requests per minute"
|
label={t("resilienceRequestsPerMinute")}
|
||||||
value={draft.requestsPerMinute}
|
value={draft.requestsPerMinute}
|
||||||
min={1}
|
min={1}
|
||||||
onChange={(requestsPerMinute) => setDraft((prev) => ({ ...prev, requestsPerMinute }))}
|
onChange={(requestsPerMinute) => setDraft((prev) => ({ ...prev, requestsPerMinute }))}
|
||||||
/>
|
/>
|
||||||
<NumberField
|
<NumberField
|
||||||
label="Minimum time between requests"
|
label={t("resilienceMinTimeBetweenRequests")}
|
||||||
value={draft.minTimeBetweenRequestsMs}
|
value={draft.minTimeBetweenRequestsMs}
|
||||||
suffix="ms"
|
suffix="ms"
|
||||||
onChange={(minTimeBetweenRequestsMs) =>
|
onChange={(minTimeBetweenRequestsMs) =>
|
||||||
@@ -270,7 +272,7 @@ function RequestQueueCard({
|
|||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
<NumberField
|
<NumberField
|
||||||
label="Concurrent requests"
|
label={t("resilienceConcurrentRequests")}
|
||||||
value={draft.concurrentRequests}
|
value={draft.concurrentRequests}
|
||||||
min={1}
|
min={1}
|
||||||
onChange={(concurrentRequests) =>
|
onChange={(concurrentRequests) =>
|
||||||
@@ -278,7 +280,7 @@ function RequestQueueCard({
|
|||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
<NumberField
|
<NumberField
|
||||||
label="Maximum queue wait time"
|
label={t("resilienceMaxQueueWaitTime")}
|
||||||
value={draft.maxWaitMs}
|
value={draft.maxWaitMs}
|
||||||
min={1}
|
min={1}
|
||||||
suffix="ms"
|
suffix="ms"
|
||||||
@@ -288,31 +290,33 @@ function RequestQueueCard({
|
|||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
<div className="rounded-xl border border-border bg-bg-subtle p-4">
|
<div className="rounded-xl border border-border bg-bg-subtle p-4">
|
||||||
<div className="text-xs text-text-muted">Auto-enable for API-key providers</div>
|
<div className="text-xs text-text-muted">
|
||||||
|
{t("resilienceAutoEnableApiKeyProviders")}
|
||||||
|
</div>
|
||||||
<div className="mt-1 text-sm font-semibold text-text-main">
|
<div className="mt-1 text-sm font-semibold text-text-main">
|
||||||
{value.autoEnableApiKeyProviders ? "Enabled" : "Disabled"}
|
{value.autoEnableApiKeyProviders ? "Enabled" : "Disabled"}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="rounded-xl border border-border bg-bg-subtle p-4">
|
<div className="rounded-xl border border-border bg-bg-subtle p-4">
|
||||||
<div className="text-xs text-text-muted">Requests per minute</div>
|
<div className="text-xs text-text-muted">{t("resilienceRequestsPerMinute")}</div>
|
||||||
<div className="mt-1 text-sm font-semibold text-text-main">
|
<div className="mt-1 text-sm font-semibold text-text-main">
|
||||||
{value.requestsPerMinute}
|
{value.requestsPerMinute}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="rounded-xl border border-border bg-bg-subtle p-4">
|
<div className="rounded-xl border border-border bg-bg-subtle p-4">
|
||||||
<div className="text-xs text-text-muted">Minimum time between requests</div>
|
<div className="text-xs text-text-muted">{t("resilienceMinTimeBetweenRequests")}</div>
|
||||||
<div className="mt-1 text-sm font-semibold text-text-main">
|
<div className="mt-1 text-sm font-semibold text-text-main">
|
||||||
{formatMs(value.minTimeBetweenRequestsMs)}
|
{formatMs(value.minTimeBetweenRequestsMs)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="rounded-xl border border-border bg-bg-subtle p-4">
|
<div className="rounded-xl border border-border bg-bg-subtle p-4">
|
||||||
<div className="text-xs text-text-muted">Concurrent requests</div>
|
<div className="text-xs text-text-muted">{t("resilienceConcurrentRequests")}</div>
|
||||||
<div className="mt-1 text-sm font-semibold text-text-main">
|
<div className="mt-1 text-sm font-semibold text-text-main">
|
||||||
{value.concurrentRequests}
|
{value.concurrentRequests}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="rounded-xl border border-border bg-bg-subtle p-4">
|
<div className="rounded-xl border border-border bg-bg-subtle p-4">
|
||||||
<div className="text-xs text-text-muted">Maximum queue wait time</div>
|
<div className="text-xs text-text-muted">{t("resilienceMaxQueueWaitTime")}</div>
|
||||||
<div className="mt-1 text-sm font-semibold text-text-main">
|
<div className="mt-1 text-sm font-semibold text-text-main">
|
||||||
{formatMs(value.maxWaitMs)}
|
{formatMs(value.maxWaitMs)}
|
||||||
</div>
|
</div>
|
||||||
@@ -333,6 +337,7 @@ function ConnectionCooldownCard({
|
|||||||
onSave: (next: ResilienceResponse["connectionCooldown"]) => Promise<void>;
|
onSave: (next: ResilienceResponse["connectionCooldown"]) => Promise<void>;
|
||||||
saving: boolean;
|
saving: boolean;
|
||||||
}) {
|
}) {
|
||||||
|
const t = useTranslations("settings");
|
||||||
const [editing, setEditing] = useState(false);
|
const [editing, setEditing] = useState(false);
|
||||||
const [draft, setDraft] = useState(value);
|
const [draft, setDraft] = useState(value);
|
||||||
|
|
||||||
@@ -347,7 +352,7 @@ function ConnectionCooldownCard({
|
|||||||
{editing ? (
|
{editing ? (
|
||||||
<>
|
<>
|
||||||
<NumberField
|
<NumberField
|
||||||
label="Base cooldown"
|
label={t("resilienceBaseCooldown")}
|
||||||
value={current.baseCooldownMs}
|
value={current.baseCooldownMs}
|
||||||
min={0}
|
min={0}
|
||||||
suffix="ms"
|
suffix="ms"
|
||||||
@@ -356,7 +361,7 @@ function ConnectionCooldownCard({
|
|||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
<BooleanField
|
<BooleanField
|
||||||
label="Use upstream retry hints"
|
label={t("resilienceUseUpstreamRetryHints")}
|
||||||
description="Use upstream retry-after/reset values when available."
|
description="Use upstream retry-after/reset values when available."
|
||||||
checked={current.useUpstreamRetryHints}
|
checked={current.useUpstreamRetryHints}
|
||||||
onChange={(useUpstreamRetryHints) =>
|
onChange={(useUpstreamRetryHints) =>
|
||||||
@@ -368,7 +373,9 @@ function ConnectionCooldownCard({
|
|||||||
/>
|
/>
|
||||||
<div className="flex flex-col gap-1">
|
<div className="flex flex-col gap-1">
|
||||||
<label className="flex items-center justify-between gap-2 text-sm">
|
<label className="flex items-center justify-between gap-2 text-sm">
|
||||||
<span className="text-text-muted">Use upstream 429 hints for breaker cooldown</span>
|
<span className="text-text-muted">
|
||||||
|
{t("resilienceUseUpstream429HintsForBreaker")}
|
||||||
|
</span>
|
||||||
<select
|
<select
|
||||||
className="rounded border border-border-default bg-surface-1 px-2 py-1 text-sm font-mono"
|
className="rounded border border-border-default bg-surface-1 px-2 py-1 text-sm font-mono"
|
||||||
value={
|
value={
|
||||||
@@ -396,9 +403,9 @@ function ConnectionCooldownCard({
|
|||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<option value="default">Default (per provider)</option>
|
<option value="default">{t("resilienceDefaultPerProvider")}</option>
|
||||||
<option value="on">Always on</option>
|
<option value="on">{t("resilienceAlwaysOn")}</option>
|
||||||
<option value="off">Always off</option>
|
<option value="off">{t("resilienceAlwaysOff")}</option>
|
||||||
</select>
|
</select>
|
||||||
</label>
|
</label>
|
||||||
<p className="text-xs text-text-muted">
|
<p className="text-xs text-text-muted">
|
||||||
|
|||||||
@@ -303,6 +303,7 @@ function StringListEditor({
|
|||||||
onChange: (next: string[]) => void;
|
onChange: (next: string[]) => void;
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
}) {
|
}) {
|
||||||
|
const t = useTranslations("settings");
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col gap-1.5">
|
<div className="flex flex-col gap-1.5">
|
||||||
<span className="text-xs font-medium text-text-main">{label}</span>
|
<span className="text-xs font-medium text-text-main">{label}</span>
|
||||||
@@ -324,7 +325,7 @@ function StringListEditor({
|
|||||||
size="sm"
|
size="sm"
|
||||||
icon="close"
|
icon="close"
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
aria-label="Remove entry"
|
aria-label={t("routingRemoveEntry")}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
const next = [...items];
|
const next = [...items];
|
||||||
next.splice(idx, 1);
|
next.splice(idx, 1);
|
||||||
@@ -356,6 +357,7 @@ function OpEditor({
|
|||||||
onChange: (next: any) => void;
|
onChange: (next: any) => void;
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
}) {
|
}) {
|
||||||
|
const t = useTranslations("settings");
|
||||||
const updateField = (field: string, value: any) => onChange({ ...op, [field]: value });
|
const updateField = (field: string, value: any) => onChange({ ...op, [field]: value });
|
||||||
const kind = op?.kind as TransformOpKind | undefined;
|
const kind = op?.kind as TransformOpKind | undefined;
|
||||||
const opDescription = kind ? OP_KIND_DESCRIPTIONS[kind] : null;
|
const opDescription = kind ? OP_KIND_DESCRIPTIONS[kind] : null;
|
||||||
@@ -376,14 +378,14 @@ function OpEditor({
|
|||||||
return wrap(
|
return wrap(
|
||||||
<div className="flex flex-col gap-2">
|
<div className="flex flex-col gap-2">
|
||||||
<StringListEditor
|
<StringListEditor
|
||||||
label="Needles (substrings to match)"
|
label={t("routingNeedlesSubstrings")}
|
||||||
hint={FIELD_HINTS.needles}
|
hint={FIELD_HINTS.needles}
|
||||||
items={op.needles || []}
|
items={op.needles || []}
|
||||||
onChange={(next) => updateField("needles", next)}
|
onChange={(next) => updateField("needles", next)}
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
/>
|
/>
|
||||||
<Toggle
|
<Toggle
|
||||||
label="Case sensitive"
|
label={t("routingCaseSensitive")}
|
||||||
description={FIELD_HINTS.caseSensitive}
|
description={FIELD_HINTS.caseSensitive}
|
||||||
checked={op.caseSensitive !== false}
|
checked={op.caseSensitive !== false}
|
||||||
onChange={(c) => updateField("caseSensitive", c)}
|
onChange={(c) => updateField("caseSensitive", c)}
|
||||||
@@ -396,14 +398,14 @@ function OpEditor({
|
|||||||
return wrap(
|
return wrap(
|
||||||
<div className="flex flex-col gap-2">
|
<div className="flex flex-col gap-2">
|
||||||
<StringListEditor
|
<StringListEditor
|
||||||
label="Prefixes"
|
label={t("routingPrefixes")}
|
||||||
hint={FIELD_HINTS.prefixes}
|
hint={FIELD_HINTS.prefixes}
|
||||||
items={op.prefixes || []}
|
items={op.prefixes || []}
|
||||||
onChange={(next) => updateField("prefixes", next)}
|
onChange={(next) => updateField("prefixes", next)}
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
/>
|
/>
|
||||||
<Toggle
|
<Toggle
|
||||||
label="Case sensitive"
|
label={t("routingCaseSensitive")}
|
||||||
description={FIELD_HINTS.caseSensitive}
|
description={FIELD_HINTS.caseSensitive}
|
||||||
checked={op.caseSensitive !== false}
|
checked={op.caseSensitive !== false}
|
||||||
onChange={(c) => updateField("caseSensitive", c)}
|
onChange={(c) => updateField("caseSensitive", c)}
|
||||||
@@ -416,21 +418,21 @@ function OpEditor({
|
|||||||
return wrap(
|
return wrap(
|
||||||
<div className="flex flex-col gap-2">
|
<div className="flex flex-col gap-2">
|
||||||
<Input
|
<Input
|
||||||
label="Match"
|
label={t("routingMatch")}
|
||||||
hint={FIELD_HINTS.matchLiteral}
|
hint={FIELD_HINTS.matchLiteral}
|
||||||
value={op.match || ""}
|
value={op.match || ""}
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
onChange={(e) => updateField("match", e.target.value)}
|
onChange={(e) => updateField("match", e.target.value)}
|
||||||
/>
|
/>
|
||||||
<Input
|
<Input
|
||||||
label="Replacement"
|
label={t("routingReplacement")}
|
||||||
hint={FIELD_HINTS.replacementText}
|
hint={FIELD_HINTS.replacementText}
|
||||||
value={op.replacement || ""}
|
value={op.replacement || ""}
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
onChange={(e) => updateField("replacement", e.target.value)}
|
onChange={(e) => updateField("replacement", e.target.value)}
|
||||||
/>
|
/>
|
||||||
<Toggle
|
<Toggle
|
||||||
label="Replace all occurrences"
|
label={t("routingReplaceAllOccurrences")}
|
||||||
description={FIELD_HINTS.allOccurrences}
|
description={FIELD_HINTS.allOccurrences}
|
||||||
checked={op.allOccurrences !== false}
|
checked={op.allOccurrences !== false}
|
||||||
onChange={(c) => updateField("allOccurrences", c)}
|
onChange={(c) => updateField("allOccurrences", c)}
|
||||||
@@ -443,21 +445,21 @@ function OpEditor({
|
|||||||
return wrap(
|
return wrap(
|
||||||
<div className="flex flex-col gap-2">
|
<div className="flex flex-col gap-2">
|
||||||
<Input
|
<Input
|
||||||
label="Pattern (regex)"
|
label={t("routingPatternRegex")}
|
||||||
hint={FIELD_HINTS.pattern}
|
hint={FIELD_HINTS.pattern}
|
||||||
value={op.pattern || ""}
|
value={op.pattern || ""}
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
onChange={(e) => updateField("pattern", e.target.value)}
|
onChange={(e) => updateField("pattern", e.target.value)}
|
||||||
/>
|
/>
|
||||||
<Input
|
<Input
|
||||||
label="Flags"
|
label={t("routingFlags")}
|
||||||
hint={FIELD_HINTS.regexFlags}
|
hint={FIELD_HINTS.regexFlags}
|
||||||
value={op.flags || "g"}
|
value={op.flags || "g"}
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
onChange={(e) => updateField("flags", e.target.value)}
|
onChange={(e) => updateField("flags", e.target.value)}
|
||||||
/>
|
/>
|
||||||
<Input
|
<Input
|
||||||
label="Replacement"
|
label={t("routingReplacement")}
|
||||||
hint={FIELD_HINTS.replacementText}
|
hint={FIELD_HINTS.replacementText}
|
||||||
value={op.replacement || ""}
|
value={op.replacement || ""}
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
@@ -468,7 +470,7 @@ function OpEditor({
|
|||||||
case "drop_block_if_contains":
|
case "drop_block_if_contains":
|
||||||
return wrap(
|
return wrap(
|
||||||
<StringListEditor
|
<StringListEditor
|
||||||
label="Needles"
|
label={t("routingNeedles")}
|
||||||
hint={FIELD_HINTS.needles}
|
hint={FIELD_HINTS.needles}
|
||||||
items={op.needles || []}
|
items={op.needles || []}
|
||||||
onChange={(next) => updateField("needles", next)}
|
onChange={(next) => updateField("needles", next)}
|
||||||
@@ -480,7 +482,7 @@ function OpEditor({
|
|||||||
return wrap(
|
return wrap(
|
||||||
<div className="flex flex-col gap-2">
|
<div className="flex flex-col gap-2">
|
||||||
<div className="flex flex-col gap-1.5">
|
<div className="flex flex-col gap-1.5">
|
||||||
<label className="text-sm font-medium text-text-main">Block text</label>
|
<label className="text-sm font-medium text-text-main">{t("routingBlockText")}</label>
|
||||||
<textarea
|
<textarea
|
||||||
rows={3}
|
rows={3}
|
||||||
value={op.text || ""}
|
value={op.text || ""}
|
||||||
@@ -491,7 +493,7 @@ function OpEditor({
|
|||||||
<p className="text-xs text-text-muted">{FIELD_HINTS.blockText}</p>
|
<p className="text-xs text-text-muted">{FIELD_HINTS.blockText}</p>
|
||||||
</div>
|
</div>
|
||||||
<Input
|
<Input
|
||||||
label="Idempotency key"
|
label={t("routingIdempotencyKey")}
|
||||||
hint={FIELD_HINTS.idempotencyKey}
|
hint={FIELD_HINTS.idempotencyKey}
|
||||||
value={op.idempotencyKey || ""}
|
value={op.idempotencyKey || ""}
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
@@ -503,14 +505,14 @@ function OpEditor({
|
|||||||
return wrap(
|
return wrap(
|
||||||
<div className="flex flex-col gap-2">
|
<div className="flex flex-col gap-2">
|
||||||
<Input
|
<Input
|
||||||
label="Entrypoint"
|
label={t("routingEntrypoint")}
|
||||||
hint={FIELD_HINTS.billingEntrypoint}
|
hint={FIELD_HINTS.billingEntrypoint}
|
||||||
value={op.entrypoint || "sdk-cli"}
|
value={op.entrypoint || "sdk-cli"}
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
onChange={(e) => updateField("entrypoint", e.target.value)}
|
onChange={(e) => updateField("entrypoint", e.target.value)}
|
||||||
/>
|
/>
|
||||||
<Select
|
<Select
|
||||||
label="Version format"
|
label={t("routingVersionFormat")}
|
||||||
hint={FIELD_HINTS.billingVersionFormat}
|
hint={FIELD_HINTS.billingVersionFormat}
|
||||||
value={op.versionFormat || "ex-machina"}
|
value={op.versionFormat || "ex-machina"}
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
@@ -521,7 +523,7 @@ function OpEditor({
|
|||||||
]}
|
]}
|
||||||
/>
|
/>
|
||||||
<Select
|
<Select
|
||||||
label="CCH algorithm"
|
label={t("routingCchAlgorithm")}
|
||||||
hint={FIELD_HINTS.billingCchAlgo}
|
hint={FIELD_HINTS.billingCchAlgo}
|
||||||
value={op.cchAlgo || "sha256-first-user"}
|
value={op.cchAlgo || "sha256-first-user"}
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
@@ -538,7 +540,7 @@ function OpEditor({
|
|||||||
return wrap(
|
return wrap(
|
||||||
<div className="flex flex-col gap-2">
|
<div className="flex flex-col gap-2">
|
||||||
<StringListEditor
|
<StringListEditor
|
||||||
label="Words to obfuscate (ZWJ inserted after first char)"
|
label={t("routingWordsToObfuscate")}
|
||||||
hint={FIELD_HINTS.obfuscateWords}
|
hint={FIELD_HINTS.obfuscateWords}
|
||||||
items={op.words || []}
|
items={op.words || []}
|
||||||
onChange={(next) => updateField("words", next)}
|
onChange={(next) => updateField("words", next)}
|
||||||
|
|||||||
@@ -551,7 +551,7 @@ export default function SystemStorageTab() {
|
|||||||
<div className="p-3 rounded-lg bg-bg border border-border mb-4">
|
<div className="p-3 rounded-lg bg-bg border border-border mb-4">
|
||||||
<div className="flex items-start justify-between gap-3 flex-wrap">
|
<div className="flex items-start justify-between gap-3 flex-wrap">
|
||||||
<div>
|
<div>
|
||||||
<p className="text-sm font-medium text-text-main">Logs Settings</p>
|
<p className="text-sm font-medium text-text-main">{t("logsSettingsTitle")}</p>
|
||||||
<p className="text-xs text-text-muted">
|
<p className="text-xs text-text-muted">
|
||||||
Configure detailed logging and call log pipeline settings
|
Configure detailed logging and call log pipeline settings
|
||||||
</p>
|
</p>
|
||||||
@@ -560,26 +560,26 @@ export default function SystemStorageTab() {
|
|||||||
<div className="mt-3 grid grid-cols-1 md:grid-cols-2 gap-3">
|
<div className="mt-3 grid grid-cols-1 md:grid-cols-2 gap-3">
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<label className="text-sm">
|
<label className="text-sm">
|
||||||
<span className="font-medium">Detailed Logs Enabled</span>
|
<span className="font-medium">{t("detailedLogsLabel")}</span>
|
||||||
<p className="text-xs text-text-muted">Enable detailed request/response logging</p>
|
<p className="text-xs text-text-muted">{t("detailedLogsDesc")}</p>
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<label className="text-sm">
|
<label className="text-sm">
|
||||||
<span className="font-medium">Call Log Pipeline</span>
|
<span className="font-medium">{t("callLogPipelineLabel")}</span>
|
||||||
<p className="text-xs text-text-muted">Enable call log processing pipeline</p>
|
<p className="text-xs text-text-muted">{t("callLogPipelineDesc")}</p>
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<label className="text-sm">
|
<label className="text-sm">
|
||||||
<span className="font-medium">Max Detail Size (KB)</span>
|
<span className="font-medium">{t("maxDetailSizeLabel")}</span>
|
||||||
<p className="text-xs text-text-muted">Maximum size for detailed log entries</p>
|
<p className="text-xs text-text-muted">{t("maxDetailSizeDesc")}</p>
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<label className="text-sm">
|
<label className="text-sm">
|
||||||
<span className="font-medium">Ring Buffer Size</span>
|
<span className="font-medium">{t("ringBufferSizeLabel")}</span>
|
||||||
<p className="text-xs text-text-muted">Size of the ring buffer for logs</p>
|
<p className="text-xs text-text-muted">{t("ringBufferSizeDesc")}</p>
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -589,7 +589,7 @@ export default function SystemStorageTab() {
|
|||||||
<div className="p-3 rounded-lg bg-bg border border-border mb-4">
|
<div className="p-3 rounded-lg bg-bg border border-border mb-4">
|
||||||
<div className="flex items-start justify-between gap-3 flex-wrap">
|
<div className="flex items-start justify-between gap-3 flex-wrap">
|
||||||
<div>
|
<div>
|
||||||
<p className="text-sm font-medium text-text-main">Cache Settings</p>
|
<p className="text-sm font-medium text-text-main">{t("cacheSettings")}</p>
|
||||||
<p className="text-xs text-text-muted">
|
<p className="text-xs text-text-muted">
|
||||||
Configure semantic and prompt caching behavior
|
Configure semantic and prompt caching behavior
|
||||||
</p>
|
</p>
|
||||||
@@ -598,7 +598,7 @@ export default function SystemStorageTab() {
|
|||||||
<div className="mt-3 grid grid-cols-1 md:grid-cols-2 gap-3">
|
<div className="mt-3 grid grid-cols-1 md:grid-cols-2 gap-3">
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<label className="text-sm">
|
<label className="text-sm">
|
||||||
<span className="font-medium">Semantic Cache Enabled</span>
|
<span className="font-medium">{t("semanticCacheEnabledLabel")}</span>
|
||||||
<p className="text-xs text-text-muted">
|
<p className="text-xs text-text-muted">
|
||||||
Enable semantic caching for similar requests
|
Enable semantic caching for similar requests
|
||||||
</p>
|
</p>
|
||||||
@@ -606,13 +606,13 @@ export default function SystemStorageTab() {
|
|||||||
</div>
|
</div>
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<label className="text-sm">
|
<label className="text-sm">
|
||||||
<span className="font-medium">Semantic Cache Max Size</span>
|
<span className="font-medium">{t("semanticCacheMaxSizeLabel")}</span>
|
||||||
<p className="text-xs text-text-muted">Maximum number of semantic cache entries</p>
|
<p className="text-xs text-text-muted">{t("semanticCacheMaxSizeDesc")}</p>
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<label className="text-sm">
|
<label className="text-sm">
|
||||||
<span className="font-medium">Semantic Cache TTL</span>
|
<span className="font-medium">{t("semanticCacheTTLLabel")}</span>
|
||||||
<p className="text-xs text-text-muted">
|
<p className="text-xs text-text-muted">
|
||||||
Time-to-live for semantic cache entries (ms)
|
Time-to-live for semantic cache entries (ms)
|
||||||
</p>
|
</p>
|
||||||
@@ -620,20 +620,20 @@ export default function SystemStorageTab() {
|
|||||||
</div>
|
</div>
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<label className="text-sm">
|
<label className="text-sm">
|
||||||
<span className="font-medium">Prompt Cache Enabled</span>
|
<span className="font-medium">{t("promptCacheEnabledLabel")}</span>
|
||||||
<p className="text-xs text-text-muted">Enable prompt caching</p>
|
<p className="text-xs text-text-muted">{t("promptCacheEnabledDesc")}</p>
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<label className="text-sm">
|
<label className="text-sm">
|
||||||
<span className="font-medium">Prompt Cache Strategy</span>
|
<span className="font-medium">{t("promptCacheStrategyLabel")}</span>
|
||||||
<p className="text-xs text-text-muted">Strategy for prompt caching</p>
|
<p className="text-xs text-text-muted">{t("promptCacheStrategyDesc")}</p>
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<label className="text-sm">
|
<label className="text-sm">
|
||||||
<span className="font-medium">Always Preserve Client Cache</span>
|
<span className="font-medium">{t("alwaysPreserveClientCacheLabel")}</span>
|
||||||
<p className="text-xs text-text-muted">Client cache preservation policy</p>
|
<p className="text-xs text-text-muted">{t("alwaysPreserveClientCacheDesc")}</p>
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -642,7 +642,7 @@ export default function SystemStorageTab() {
|
|||||||
<div className="p-3 rounded-lg bg-bg border border-border mb-4">
|
<div className="p-3 rounded-lg bg-bg border border-border mb-4">
|
||||||
<div className="flex items-start justify-between gap-3 flex-wrap">
|
<div className="flex items-start justify-between gap-3 flex-wrap">
|
||||||
<div>
|
<div>
|
||||||
<p className="text-sm font-medium text-text-main">Log retention policy</p>
|
<p className="text-sm font-medium text-text-main">{t("logRetentionPolicyTitle")}</p>
|
||||||
<p className="text-xs text-text-muted">
|
<p className="text-xs text-text-muted">
|
||||||
Request logs retain up to <code>CALL_LOGS_TABLE_MAX_ROWS</code> rows (default:
|
Request logs retain up to <code>CALL_LOGS_TABLE_MAX_ROWS</code> rows (default:
|
||||||
100,000). Proxy logs retain up to <code>PROXY_LOGS_TABLE_MAX_ROWS</code> rows. Older
|
100,000). Proxy logs retain up to <code>PROXY_LOGS_TABLE_MAX_ROWS</code> rows. Older
|
||||||
|
|||||||
@@ -461,26 +461,29 @@ export default function BudgetTab() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="grid grid-cols-2 lg:grid-cols-6 gap-2">
|
<div className="grid grid-cols-2 lg:grid-cols-6 gap-2">
|
||||||
<KpiBlock label="Today" value={formatCurrency(stats.today)} />
|
<KpiBlock label={t("budgetKpiToday")} value={formatCurrency(stats.today)} />
|
||||||
<KpiBlock label="This month" value={formatCurrency(stats.month)} />
|
<KpiBlock label={t("budgetKpiThisMonth")} value={formatCurrency(stats.month)} />
|
||||||
<KpiBlock
|
<KpiBlock
|
||||||
label="Proj EOM"
|
label={t("budgetKpiProjEom")}
|
||||||
value={formatCurrency(stats.projectionEom)}
|
value={formatCurrency(stats.projectionEom)}
|
||||||
tone={projectionOverBudget ? "amber" : undefined}
|
tone={projectionOverBudget ? "amber" : undefined}
|
||||||
hint={projectionOverBudget ? "above limit ⚠" : "on track"}
|
hint={projectionOverBudget ? "above limit ⚠" : "on track"}
|
||||||
/>
|
/>
|
||||||
<KpiBlock
|
<KpiBlock
|
||||||
label="Blocked"
|
label={t("budgetKpiBlocked")}
|
||||||
value={String(stats.counts.blocked)}
|
value={String(stats.counts.blocked)}
|
||||||
tone={stats.counts.blocked > 0 ? "red" : undefined}
|
tone={stats.counts.blocked > 0 ? "red" : undefined}
|
||||||
/>
|
/>
|
||||||
<KpiBlock
|
<KpiBlock
|
||||||
label="At risk"
|
label={t("budgetKpiAtRisk")}
|
||||||
value={String(stats.counts.alerting)}
|
value={String(stats.counts.alerting)}
|
||||||
tone={stats.counts.alerting > 0 ? "amber" : undefined}
|
tone={stats.counts.alerting > 0 ? "amber" : undefined}
|
||||||
hint="≥ warning"
|
hint="≥ warning"
|
||||||
/>
|
/>
|
||||||
<KpiBlock label="Active keys" value={`${stats.counts.active} / ${rows.length}`} />
|
<KpiBlock
|
||||||
|
label={t("budgetKpiActiveKeys")}
|
||||||
|
value={`${stats.counts.active} / ${rows.length}`}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
@@ -493,7 +496,7 @@ export default function BudgetTab() {
|
|||||||
</span>
|
</span>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
placeholder="Search keys..."
|
placeholder={t("budgetSearchKeysPlaceholder")}
|
||||||
value={searchQuery}
|
value={searchQuery}
|
||||||
onChange={(e) => setSearchQuery(e.target.value)}
|
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"
|
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"
|
||||||
@@ -504,10 +507,10 @@ export default function BudgetTab() {
|
|||||||
onChange={(e) => setSortKey(e.target.value as typeof sortKey)}
|
onChange={(e) => setSortKey(e.target.value as typeof sortKey)}
|
||||||
className="bg-bg-base border border-border rounded-md px-2 py-1.5 text-xs text-text-main cursor-pointer"
|
className="bg-bg-base border border-border rounded-md px-2 py-1.5 text-xs text-text-main cursor-pointer"
|
||||||
>
|
>
|
||||||
<option value="usedDesc">Sort: % Used ↓</option>
|
<option value="usedDesc">{t("budgetSortPctUsed")}</option>
|
||||||
<option value="todayDesc">Sort: Today $ ↓</option>
|
<option value="todayDesc">{t("budgetSortTodayDollar")}</option>
|
||||||
<option value="monthDesc">Sort: Month $ ↓</option>
|
<option value="monthDesc">{t("budgetSortMonthDollar")}</option>
|
||||||
<option value="name">Sort: Name (A–Z)</option>
|
<option value="name">{t("budgetSortNameAZ")}</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -601,9 +604,9 @@ export default function BudgetTab() {
|
|||||||
<div>Key</div>
|
<div>Key</div>
|
||||||
<div className="text-right">Today</div>
|
<div className="text-right">Today</div>
|
||||||
<div className="text-right">Month</div>
|
<div className="text-right">Month</div>
|
||||||
<div className="text-right">Daily lim</div>
|
<div className="text-right">{t("budgetColDailyLim")}</div>
|
||||||
<div className="text-right">Monthly lim</div>
|
<div className="text-right">{t("budgetColMonthlyLim")}</div>
|
||||||
<div className="text-right">Used %</div>
|
<div className="text-right">{t("budgetColUsedPct")}</div>
|
||||||
<div className="text-center">Status</div>
|
<div className="text-center">Status</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -1103,7 +1103,9 @@
|
|||||||
"updateNow": "Update Now",
|
"updateNow": "Update Now",
|
||||||
"updating": "Updating...",
|
"updating": "Updating...",
|
||||||
"updateAvailableDesc": "A new version is available. Click to update.",
|
"updateAvailableDesc": "A new version is available. Click to update.",
|
||||||
"updateStarted": "Update started..."
|
"updateStarted": "Update started...",
|
||||||
|
"reloadingPageAutomatically": "Reloading page automatically...",
|
||||||
|
"providerTopology": "Provider Topology"
|
||||||
},
|
},
|
||||||
"analytics": {
|
"analytics": {
|
||||||
"title": "Analytics",
|
"title": "Analytics",
|
||||||
@@ -1231,7 +1233,8 @@
|
|||||||
"permissionsTitle": "Permissions: {name}",
|
"permissionsTitle": "Permissions: {name}",
|
||||||
"allowAllDesc": "This key can access all available models.",
|
"allowAllDesc": "This key can access all available models.",
|
||||||
"restrictDesc": "This key can access {selectedCount} of {totalModels} models.",
|
"restrictDesc": "This key can access {selectedCount} of {totalModels} models.",
|
||||||
"selectedCount": "{count} selected"
|
"selectedCount": "{count} selected",
|
||||||
|
"maxActiveSessions": "Max Active Sessions"
|
||||||
},
|
},
|
||||||
"auditLog": {
|
"auditLog": {
|
||||||
"title": "Audit Log",
|
"title": "Audit Log",
|
||||||
@@ -3522,7 +3525,9 @@
|
|||||||
"providerSummaryAll": "Total",
|
"providerSummaryAll": "Total",
|
||||||
"ideProviders": "IDE Providers",
|
"ideProviders": "IDE Providers",
|
||||||
"ideProvidersDesc": "Editors with built-in AI subscription. Use the provider page to import credentials directly from the IDE keychain.",
|
"ideProvidersDesc": "Editors with built-in AI subscription. Use the provider page to import credentials directly from the IDE keychain.",
|
||||||
"noIdeProviders": "No IDE providers match the current filters."
|
"noIdeProviders": "No IDE providers match the current filters.",
|
||||||
|
"providerDetailFastTierTooltip": "Apply Codex Fast tier to all Codex connections by default",
|
||||||
|
"providerDetailFastDefaultLabel": "Fast default"
|
||||||
},
|
},
|
||||||
"settings": {
|
"settings": {
|
||||||
"title": "Settings",
|
"title": "Settings",
|
||||||
@@ -4216,7 +4221,61 @@
|
|||||||
"optional": "Optional",
|
"optional": "Optional",
|
||||||
"current": "Current",
|
"current": "Current",
|
||||||
"remove": "Remove",
|
"remove": "Remove",
|
||||||
"search": "Search"
|
"search": "Search",
|
||||||
|
"oneproxyTitle": "1proxy Free Proxy Marketplace",
|
||||||
|
"oneproxyTotalProxies": "Total Proxies",
|
||||||
|
"oneproxyAvgQuality": "Avg Quality",
|
||||||
|
"resilienceScope": "Scope:",
|
||||||
|
"resilienceTrigger": "Trigger:",
|
||||||
|
"resilienceEffect": "Effect:",
|
||||||
|
"resilienceRequestQueueTitle": "Request Queue & Rate",
|
||||||
|
"resilienceAutoEnableApiKeyProviders": "Auto-enable for API-key providers",
|
||||||
|
"resilienceRequestsPerMinute": "Requests per minute",
|
||||||
|
"resilienceMinTimeBetweenRequests": "Minimum time between requests",
|
||||||
|
"resilienceConcurrentRequests": "Concurrent requests",
|
||||||
|
"resilienceMaxQueueWaitTime": "Maximum queue wait time",
|
||||||
|
"resilienceBaseCooldown": "Base cooldown",
|
||||||
|
"resilienceUseUpstreamRetryHints": "Use upstream retry hints",
|
||||||
|
"resilienceDefaultPerProvider": "Default (per provider)",
|
||||||
|
"resilienceAlwaysOn": "Always on",
|
||||||
|
"resilienceAlwaysOff": "Always off",
|
||||||
|
"routingRemoveEntry": "Remove entry",
|
||||||
|
"routingNeedlesSubstrings": "Needles (substrings to match)",
|
||||||
|
"routingCaseSensitive": "Case sensitive",
|
||||||
|
"routingPrefixes": "Prefixes",
|
||||||
|
"routingMatch": "Match",
|
||||||
|
"routingReplacement": "Replacement",
|
||||||
|
"routingReplaceAllOccurrences": "Replace all occurrences",
|
||||||
|
"routingPatternRegex": "Pattern (regex)",
|
||||||
|
"routingFlags": "Flags",
|
||||||
|
"routingNeedles": "Needles",
|
||||||
|
"routingBlockText": "Block text",
|
||||||
|
"routingIdempotencyKey": "Idempotency key",
|
||||||
|
"routingEntrypoint": "Entrypoint",
|
||||||
|
"routingVersionFormat": "Version format",
|
||||||
|
"routingCchAlgorithm": "CCH algorithm",
|
||||||
|
"routingWordsToObfuscate": "Words to obfuscate (ZWJ inserted after first char)",
|
||||||
|
"logsSettingsTitle": "Logs Settings",
|
||||||
|
"detailedLogsLabel": "Detailed Logs Enabled",
|
||||||
|
"detailedLogsDesc": "Enable detailed request/response logging",
|
||||||
|
"callLogPipelineLabel": "Call Log Pipeline",
|
||||||
|
"callLogPipelineDesc": "Enable call log processing pipeline",
|
||||||
|
"maxDetailSizeLabel": "Max Detail Size (KB)",
|
||||||
|
"maxDetailSizeDesc": "Maximum size for detailed log entries",
|
||||||
|
"ringBufferSizeLabel": "Ring Buffer Size",
|
||||||
|
"ringBufferSizeDesc": "Size of the ring buffer for logs",
|
||||||
|
"semanticCacheEnabledLabel": "Semantic Cache Enabled",
|
||||||
|
"semanticCacheMaxSizeLabel": "Semantic Cache Max Size",
|
||||||
|
"semanticCacheMaxSizeDesc": "Maximum number of semantic cache entries",
|
||||||
|
"semanticCacheTTLLabel": "Semantic Cache TTL",
|
||||||
|
"promptCacheEnabledLabel": "Prompt Cache Enabled",
|
||||||
|
"promptCacheEnabledDesc": "Enable prompt caching",
|
||||||
|
"promptCacheStrategyLabel": "Prompt Cache Strategy",
|
||||||
|
"promptCacheStrategyDesc": "Strategy for prompt caching",
|
||||||
|
"alwaysPreserveClientCacheLabel": "Always Preserve Client Cache",
|
||||||
|
"alwaysPreserveClientCacheDesc": "Client cache preservation policy",
|
||||||
|
"logRetentionPolicyTitle": "Log retention policy",
|
||||||
|
"resilienceUseUpstream429HintsForBreaker": "Use upstream 429 hints (breaker)"
|
||||||
},
|
},
|
||||||
"contextRtk": {
|
"contextRtk": {
|
||||||
"title": "RTK Engine",
|
"title": "RTK Engine",
|
||||||
@@ -4830,7 +4889,21 @@
|
|||||||
"quotaCutoffsDefaultHint": "Default min remaining: {default}%",
|
"quotaCutoffsDefaultHint": "Default min remaining: {default}%",
|
||||||
"quotaCutoffsResetAll": "Reset all",
|
"quotaCutoffsResetAll": "Reset all",
|
||||||
"quotaCutoffsNoWindows": "No quota windows are available for this account yet.",
|
"quotaCutoffsNoWindows": "No quota windows are available for this account yet.",
|
||||||
"quotaThresholdInvalid": "Enter a whole number from 0 to 100."
|
"quotaThresholdInvalid": "Enter a whole number from 0 to 100.",
|
||||||
|
"budgetKpiToday": "Today",
|
||||||
|
"budgetKpiThisMonth": "This month",
|
||||||
|
"budgetKpiProjEom": "Proj EOM",
|
||||||
|
"budgetKpiBlocked": "Blocked",
|
||||||
|
"budgetKpiAtRisk": "At risk",
|
||||||
|
"budgetKpiActiveKeys": "Active keys",
|
||||||
|
"budgetSearchKeysPlaceholder": "Search keys...",
|
||||||
|
"budgetSortPctUsed": "Sort: % Used ↓",
|
||||||
|
"budgetSortTodayDollar": "Sort: Today $ ↓",
|
||||||
|
"budgetSortMonthDollar": "Sort: Month $ ↓",
|
||||||
|
"budgetSortNameAZ": "Sort: Name (A–Z)",
|
||||||
|
"budgetColDailyLim": "Daily lim",
|
||||||
|
"budgetColMonthlyLim": "Monthly lim",
|
||||||
|
"budgetColUsedPct": "Used %"
|
||||||
},
|
},
|
||||||
"modals": {
|
"modals": {
|
||||||
"waitingAuth": "Waiting for Authorization",
|
"waitingAuth": "Waiting for Authorization",
|
||||||
|
|||||||
Reference in New Issue
Block a user