From bba97cfc60fd1ce2e0f0be918db2db97fbf75654 Mon Sep 17 00:00:00 2001
From: Randi <55005611+rdself@users.noreply.github.com>
Date: Tue, 16 Jun 2026 08:57:21 -0400
Subject: [PATCH] =?UTF-8?q?=E5=A4=A7=E9=87=8FUI=E6=98=BE=E7=A4=BA=E5=92=8C?=
=?UTF-8?q?i18n=E4=BC=98=E5=8C=96=20(#3973)?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Integrated into release/v3.8.27
---
docs/guides/USER_GUIDE.md | 5 +
.../api-manager/ApiManagerPageClient.tsx | 90 ------
.../combos/CompressionCombosPageClient.tsx | 38 +--
.../context/combos/CompressionHub.tsx | 111 ++++---
.../components/ConnectionsHeaderToolbar.tsx | 28 +-
.../[id]/components/ExternalLinkModal.tsx | 32 +-
.../[id]/hooks/useExternalLinkFlow.ts | 30 +-
.../components/CodexCliGuideModal.tsx | 17 +-
.../dashboard/runtime/RuntimePageClient.tsx | 7 +-
.../components/ModelCooldownsCard.tsx | 0
.../components/ProviderCatalog.tsx | 16 +-
.../search-tools/components/ResultsPanel.tsx | 8 +-
.../components/SearchToolsConfigPane.tsx | 25 +-
.../components/tabs/CompareTab.tsx | 34 +--
.../components/tabs/ScrapeTab.tsx | 27 +-
.../dashboard/settings/SettingsPageClient.tsx | 47 ---
.../settings/components/AppearanceTab.tsx | 51 ++--
.../components/BackgroundDegradationTab.tsx | 19 +-
.../settings/components/ResilienceTab.tsx | 22 +-
.../settings/components/SystemStorageTab.tsx | 288 +++++++-----------
.../(dashboard)/dashboard/settings/page.tsx | 26 +-
.../dashboard/settings/resilience/page.tsx | 4 +-
.../dashboard/settings/routing/page.tsx | 2 +-
.../translator/components/MonitorTab.tsx | 24 +-
.../components/TranslateFlowDiagram.tsx | 27 +-
.../components/TranslatorConceptCard.tsx | 10 +-
.../advanced/TestBenchAccordion.tsx | 11 +-
src/i18n/messages/en.json | 14 +
src/shared/components/ProxyLogger.tsx | 60 ++--
src/shared/components/RequestLoggerV2.tsx | 72 ++---
.../compression/EngineConfigPage.tsx | 38 +--
src/shared/components/logTableStyles.ts | 7 +
src/shared/constants/sidebarVisibility.ts | 13 +-
tests/unit/dashboard-shell-tabs.test.ts | 16 +-
tests/unit/sidebar-visibility.test.ts | 11 +
.../ui/system-storage-manual-vacuum.test.tsx | 80 +++++
36 files changed, 588 insertions(+), 722 deletions(-)
rename src/app/(dashboard)/dashboard/{settings => runtime}/components/ModelCooldownsCard.tsx (100%)
delete mode 100644 src/app/(dashboard)/dashboard/settings/SettingsPageClient.tsx
create mode 100644 src/shared/components/logTableStyles.ts
create mode 100644 tests/unit/ui/system-storage-manual-vacuum.test.tsx
diff --git a/docs/guides/USER_GUIDE.md b/docs/guides/USER_GUIDE.md
index 3f2a7de96b..5dc9e741e7 100644
--- a/docs/guides/USER_GUIDE.md
+++ b/docs/guides/USER_GUIDE.md
@@ -897,6 +897,11 @@ The settings page is organized into **7 tabs** for easy navigation:
| **Resilience** | Request queue, connection cooldown, provider breaker config, and wait-for-cooldown behavior |
| **Advanced** | Global proxy configuration (HTTP/SOCKS5), per-provider proxy overrides |
+General no longer duplicates read-only logging and cache notes. Database retention and
+optimization settings are persisted through `/api/settings/database`; manual cache clearing uses
+`DELETE /api/cache`. Request and proxy log row caps are controlled by
+`CALL_LOGS_TABLE_MAX_ROWS` and `PROXY_LOGS_TABLE_MAX_ROWS`.
+
---
### Costs & Budget Management
diff --git a/src/app/(dashboard)/dashboard/api-manager/ApiManagerPageClient.tsx b/src/app/(dashboard)/dashboard/api-manager/ApiManagerPageClient.tsx
index 615c7d58d8..b75e7ede89 100644
--- a/src/app/(dashboard)/dashboard/api-manager/ApiManagerPageClient.tsx
+++ b/src/app/(dashboard)/dashboard/api-manager/ApiManagerPageClient.tsx
@@ -806,65 +806,6 @@ export default function ApiManagerPageClient() {
)}
- {/* Stats Summary Cards */}
- {keys.length > 0 && (
-
- {keys.length}{" "}
{keys.length === 1
? t("keyRegistered", { count: keys.length })
: t("keysRegistered", { count: keys.length })}
@@ -1257,36 +1197,6 @@ export default function ApiManagerPageClient() {
)}
- {/* Usage Tips Card */}
-
-
-
- lightbulb
-
-
-
{t("usageTips")}
-
-
- check
- {t("tipAuth")}
-
-
- check
- {t("tipSecure")}
-
-
- check
- {t("tipSeparate")}
-
-
- check
- {t("tipRestrict")}
-
-
-
-
-
-
{/* Add Key Modal */}
{
- if (!confirm(`Excluir o combo "${combo.name}"?`)) return;
+ if (!confirm(`Delete combo "${combo.name}"?`)) return;
const res = await fetch(`/api/context/combos/${combo.id}`, { method: "DELETE" });
if (res.ok) refresh();
};
@@ -186,9 +186,9 @@ function NamedCombosManager() {
return (
-
Combos nomeados
+
Named combos
- Salve pipelines diferentes e atribua a combos de roteamento específicos.
+ Save different pipelines and assign them to specific routing combos.
@@ -197,13 +197,13 @@ function NamedCombosManager() {
setName(event.target.value)}
- placeholder="Nome do combo"
+ placeholder="Combo name"
className="rounded-lg border border-border bg-bg px-3 py-2 text-sm text-text-main"
/>
setDescription(event.target.value)}
- placeholder="Descrição"
+ placeholder="Description"
className="rounded-lg border border-border bg-bg px-3 py-2 text-sm text-text-main"
/>
@@ -217,7 +217,7 @@ function NamedCombosManager() {
}
className="rounded-lg border border-border px-3 py-1.5 text-xs text-text-main"
>
- Adicionar etapa
+ Add step
{pipeline.map((step, index) => (
@@ -249,7 +249,7 @@ function NamedCombosManager() {
className="rounded-lg border border-border px-3 py-2 text-sm text-text-main"
disabled={pipeline.length <= 1}
>
- Remover
+ Remove
))}
@@ -257,7 +257,7 @@ function NamedCombosManager() {
-
Pacotes de idioma
+
Language packs
{languagePacks.map((pack) => (
@@ -282,7 +282,7 @@ function NamedCombosManager() {
checked={outputMode}
onChange={(event) => setOutputMode(event.target.checked)}
/>
- Ativado
+ Enabled
-
Atribuir ao roteamento
+
Assign to routing
{routingCombos.length === 0 ? (
-
Nenhum combo de roteamento disponível.
+
No routing combos available.
) : (
routingCombos.map((combo) => {
const id = combo.id ?? combo.name ?? "";
@@ -325,14 +325,14 @@ function NamedCombosManager() {
disabled={saving}
className="rounded-lg bg-primary px-4 py-2 text-sm font-medium text-white"
>
- {editingId ? "Salvar" : "Criar combo"}
+ {editingId ? "Save" : "Create combo"}
{editingId && (
- Cancelar
+ Cancel
)}
@@ -348,7 +348,7 @@ function NamedCombosManager() {
{combo.isDefault && (
- Padrão
+ Default
)}
@@ -364,21 +364,21 @@ function NamedCombosManager() {
))}
- Pacotes de idioma: {combo.languagePacks.join(", ")}
+ Language packs: {combo.languagePacks.join(", ")}
editCombo(combo)}
className="rounded-lg border border-border px-3 py-1.5 text-xs text-text-main"
>
- Editar
+ Edit
{!combo.isDefault && (
setDefault(combo.id)}
className="rounded-lg border border-border px-3 py-1.5 text-xs text-text-main"
>
- Definir como padrão
+ Set as default
)}
{!combo.isDefault && (
@@ -386,7 +386,7 @@ function NamedCombosManager() {
onClick={() => deleteCombo(combo)}
className="rounded-lg border border-danger/40 px-3 py-1.5 text-xs text-danger"
>
- Excluir
+ Delete
)}
diff --git a/src/app/(dashboard)/dashboard/context/combos/CompressionHub.tsx b/src/app/(dashboard)/dashboard/context/combos/CompressionHub.tsx
index 512cd4e390..6098af1f30 100644
--- a/src/app/(dashboard)/dashboard/context/combos/CompressionHub.tsx
+++ b/src/app/(dashboard)/dashboard/context/combos/CompressionHub.tsx
@@ -5,8 +5,8 @@
// IMPORTANT (hydration): this component deliberately does NOT use `useTranslations`.
// The previous combos redesign failed to hydrate on the production build; the only
// structural difference from the engine pages (which hydrate fine) was a page-level
-// `useTranslations("contextCombos")`. To stay on the proven-good path, all strings
-// here are hardcoded (pt-BR), exactly like `EngineConfigPage`.
+// `useTranslations("contextCombos")`. To stay on the proven-good path, strings here
+// remain literal English text, exactly like `EngineConfigPage`.
import { useCallback, useEffect, useState } from "react";
@@ -45,13 +45,13 @@ interface DefaultCombo {
// ── Constants ──────────────────────────────────────────────────────────────────
const MODES: { value: CompressionMode; label: string; hint: string }[] = [
- { value: "off", label: "Off", hint: "Sem compressão" },
- { value: "lite", label: "Lite", hint: "Limpeza rápida" },
- { value: "standard", label: "Standard", hint: "Caveman padrão" },
- { value: "aggressive", label: "Aggressive", hint: "Resumo + aging" },
- { value: "ultra", label: "Ultra", hint: "Poda heurística" },
- { value: "rtk", label: "RTK", hint: "Filtros de tool output" },
- { value: "stacked", label: "Stacked", hint: "Roda as camadas abaixo em sequência" },
+ { value: "off", label: "Off", hint: "No compression" },
+ { value: "lite", label: "Lite", hint: "Fast cleanup" },
+ { value: "standard", label: "Standard", hint: "Standard Caveman" },
+ { value: "aggressive", label: "Aggressive", hint: "Summary plus aging" },
+ { value: "ultra", label: "Ultra", hint: "Heuristic pruning" },
+ { value: "rtk", label: "RTK", hint: "Tool output filters" },
+ { value: "stacked", label: "Stacked", hint: "Run the layers below in sequence" },
];
// ── Helpers ────────────────────────────────────────────────────────────────────
@@ -161,11 +161,11 @@ export default function CompressionHub() {
});
if (!res.ok) {
setSettings(settings); // revert
- setError("Falha ao salvar as configurações.");
+ setError("Failed to save settings.");
}
} catch {
setSettings(settings);
- setError("Falha ao salvar as configurações.");
+ setError("Failed to save settings.");
}
},
[settings]
@@ -186,12 +186,9 @@ export default function CompressionHub() {
if (enabledNow) {
optimistic = combo.pipeline.filter((s) => s.engine !== engineId);
} else {
- const priorityOf = (eid: string) =>
- engines.find((e) => e.id === eid)?.stackPriority ?? 50;
+ const priorityOf = (eid: string) => engines.find((e) => e.id === eid)?.stackPriority ?? 50;
optimistic = [...combo.pipeline];
- let insertAt = optimistic.findIndex(
- (s) => priorityOf(s.engine) > priorityOf(engineId)
- );
+ let insertAt = optimistic.findIndex((s) => priorityOf(s.engine) > priorityOf(engineId));
if (insertAt < 0) insertAt = optimistic.length;
optimistic.splice(insertAt, 0, { engine: engineId });
}
@@ -206,7 +203,7 @@ export default function CompressionHub() {
});
if (!res.ok) {
setCombo(prev);
- setError("Falha ao atualizar a camada.");
+ setError("Failed to update layer.");
return;
}
const updated = await res.json();
@@ -215,7 +212,7 @@ export default function CompressionHub() {
}
} catch {
setCombo(prev);
- setError("Falha ao atualizar a camada.");
+ setError("Failed to update layer.");
}
},
[combo, engines]
@@ -243,11 +240,11 @@ export default function CompressionHub() {
});
if (!res.ok) {
setCombo(prev);
- setError("Falha ao reordenar o pipeline.");
+ setError("Failed to reorder pipeline.");
}
} catch {
setCombo(prev);
- setError("Falha ao reordenar o pipeline.");
+ setError("Failed to reorder pipeline.");
}
},
[combo]
@@ -257,7 +254,7 @@ export default function CompressionHub() {
if (loading) {
return (
- Carregando…
+ Loading...
);
}
@@ -281,7 +278,7 @@ export default function CompressionHub() {
Compression Hub
- Ligue, configure e ordene as camadas de compressão num só lugar.
+ Enable, configure, and order compression layers in one place.
@@ -290,7 +287,7 @@ export default function CompressionHub() {
onClick={() => setExplainerOpen((v) => !v)}
className="shrink-0 rounded-lg border border-border px-3 py-1.5 text-xs text-text-main hover:bg-bg"
>
- {explainerOpen ? "Ocultar explicação" : "Como funciona?"}
+ {explainerOpen ? "Hide explanation" : "How it works"}
@@ -302,32 +299,32 @@ export default function CompressionHub() {
{explainerOpen && (
- A compressão reduz tokens e custo reescrevendo
- o histórico antes de enviar ao provider, preservando o sentido.
+ Compression reduces tokens and cost by
+ rewriting history before it is sent to the provider while preserving meaning.
- Token Saver (master) : precisa estar ligado.
- Desligado, nada é comprimido.
+ Token Saver (master) : must be enabled.
+ When it is off, nothing is compressed.
- Modo : define a estratégia. Os modos simples
- (Lite/Standard/Aggressive/Ultra/RTK) rodam uma única técnica. O modo{" "}
- Stacked roda várias camadas em sequência — é
- o que usa a lista de camadas abaixo.
+ Mode : defines the strategy. Simple modes
+ (Lite/Standard/Aggressive/Ultra/RTK) run one technique.{" "}
+ Stacked runs multiple layers in sequence
+ and uses the layer list below.
- Camadas (pipeline) : no modo Stacked, cada
- camada ativa roda na ordem definida, passando o texto já comprimido para a próxima
- (ex.: Session Dedup → RTK → Caveman).
+ Layers (pipeline) : in Stacked mode, each
+ active layer runs in order and passes the compressed text to the next one (for
+ example: Session Dedup → RTK → Caveman).
- Configuração : cada camada tem liga/desliga e
- parâmetros próprios (botão ⚙).
+ Configuration : each layer has its own
+ enable switch and parameters (the settings button).
- Combos nomeados : salve diferentes pipelines
- e atribua a combos de roteamento específicos (seção abaixo).
+ Named combos : save different pipelines and
+ assign them to specific routing combos in the section below.
@@ -338,18 +335,18 @@ export default function CompressionHub() {
Token Saver
-
Chave geral da compressão.
+
Master switch for compression.
saveSettings({ enabled: !enabled })}
- ariaLabel="Ligar/desligar Token Saver"
+ ariaLabel="Toggle Token Saver"
/>
{/* Mode selector */}
-
Modo
+
Mode
{MODES.map((m) => (
check_circle
- Pipeline de camadas ativo — as camadas abaixo rodam em cada requisição.
+ Layer pipeline is active. The layers below run on each request.
) : (
info
- As camadas abaixo só rodam no modo Stacked com o Token Saver ligado.
+ The layers below only run in Stacked mode with Token Saver enabled.
{!enabled && (
saveSettings({ enabled: true })}
className="rounded border border-amber-500/50 px-2 py-0.5 font-medium hover:bg-amber-500/10"
>
- Ligar Token Saver
+ Enable Token Saver
)}
{enabled && mode !== "stacked" && (
@@ -398,7 +395,7 @@ export default function CompressionHub() {
onClick={() => saveSettings({ defaultMode: "stacked" })}
className="rounded border border-amber-500/50 px-2 py-0.5 font-medium hover:bg-amber-500/10"
>
- Usar modo Stacked
+ Use Stacked mode
)}
@@ -409,13 +406,13 @@ export default function CompressionHub() {
- Pipeline ativo (ordem de execução)
+ Active pipeline (execution order)
- {activeSteps.length} camada(s)
+ {activeSteps.length} layer(s)
{activeSteps.length === 0 ? (
- Nenhuma camada ativa. Ligue uma camada abaixo para montar o pipeline.
+ No active layers. Enable a layer below to build the pipeline.
) : (
@@ -431,7 +428,7 @@ export default function CompressionHub() {
type="button"
onClick={() => moveStep(index, -1)}
disabled={index === 0}
- aria-label="Mover para cima"
+ aria-label="Move up"
className="text-text-muted hover:text-text-main disabled:opacity-30"
>
arrow_upward
@@ -440,7 +437,7 @@ export default function CompressionHub() {
type="button"
onClick={() => moveStep(index, 1)}
disabled={index === activeSteps.length - 1}
- aria-label="Mover para baixo"
+ aria-label="Move down"
className="text-text-muted hover:text-text-main disabled:opacity-30"
>
arrow_downward
@@ -466,13 +463,11 @@ export default function CompressionHub() {
)}
-
- {engine?.description ?? ""}
-
+
{engine?.description ?? ""}
settings
@@ -480,7 +475,7 @@ export default function CompressionHub() {
toggleEngine(step.engine)}
- ariaLabel={`Desligar ${engine?.name ?? step.engine}`}
+ ariaLabel={`Disable ${engine?.name ?? step.engine}`}
/>
);
@@ -492,7 +487,7 @@ export default function CompressionHub() {
{/* ── Inactive layers ── */}
{inactiveEngines.length > 0 && (
-
Camadas disponíveis
+
Available layers
{inactiveEngines.map((engine) => (
settings
@@ -529,7 +524,7 @@ export default function CompressionHub() {
toggleEngine(engine.id)}
- ariaLabel={`Ligar ${engine.name}`}
+ ariaLabel={`Enable ${engine.name}`}
/>
))}
diff --git a/src/app/(dashboard)/dashboard/providers/[id]/components/ConnectionsHeaderToolbar.tsx b/src/app/(dashboard)/dashboard/providers/[id]/components/ConnectionsHeaderToolbar.tsx
index b73cce3ca7..eddd3a24fa 100644
--- a/src/app/(dashboard)/dashboard/providers/[id]/components/ConnectionsHeaderToolbar.tsx
+++ b/src/app/(dashboard)/dashboard/providers/[id]/components/ConnectionsHeaderToolbar.tsx
@@ -102,9 +102,7 @@ export default function ConnectionsHeaderToolbar({
"Route bare claude-* model IDs from Claude Code clients through the Claude Code account instead of asking for a provider prefix."
)}
>
-
- alt_route
-
+ alt_route
{providerText(
t,
@@ -162,9 +160,7 @@ export default function ConnectionsHeaderToolbar({
"Set a global Codex service mode, or leave accounts on their individual service-tier setting."
)}
>
-
- {providerText(t, "providerDetailServiceModeLabel", "Global service mode:")}
-
+ {providerText(t, "providerDetailServiceModeLabel", "Global service mode:")}
@@ -223,7 +219,9 @@ export default function ConnectionsHeaderToolbar({
{connections.length > 0 && (
{ await handleDistributeProxies(); }}
+ onDistribute={async () => {
+ await handleDistributeProxies();
+ }}
disabled={batchTesting || !!retestingId}
/>
)}
@@ -272,11 +270,7 @@ export default function ConnectionsHeaderToolbar({
>
) : (
<>
- gateConnectionFlow(openPrimaryAddFlow)}
- >
+ gateConnectionFlow(openPrimaryAddFlow)}>
{providerSupportsPat ? "Add PAT" : t("add")}
{providerId === "qoder" && (
@@ -295,7 +289,7 @@ export default function ConnectionsHeaderToolbar({
icon="menu_book"
onClick={() => onOpenCodexCliGuide()}
>
- Codex CLI Guide
+ {providerText(t, "codexCliGuideButton", "Codex CLI Guide")}
)}
{providerId === "codex" && (
@@ -305,7 +299,7 @@ export default function ConnectionsHeaderToolbar({
icon="share"
onClick={() => gateConnectionFlow(openExternalLinkFlow)}
>
- Adicionar Externo
+ {providerText(t, "codexExternalLinkButton", "External Codex link")}
)}
{providerId === "codex" && (
@@ -349,11 +343,7 @@ export default function ConnectionsHeaderToolbar({
>
) : (
connections.length === 0 && (
- gateConnectionFlow(openApiKeyAddFlow)}
- >
+ gateConnectionFlow(openApiKeyAddFlow)}>
{t("add")}
)
diff --git a/src/app/(dashboard)/dashboard/providers/[id]/components/ExternalLinkModal.tsx b/src/app/(dashboard)/dashboard/providers/[id]/components/ExternalLinkModal.tsx
index 02a4930465..09ec9ce6d3 100644
--- a/src/app/(dashboard)/dashboard/providers/[id]/components/ExternalLinkModal.tsx
+++ b/src/app/(dashboard)/dashboard/providers/[id]/components/ExternalLinkModal.tsx
@@ -1,6 +1,7 @@
"use client";
import { Modal, Button } from "@/shared/components";
+import { useTranslations } from "next-intl";
type ExternalLinkModalProps = {
isOpen: boolean;
@@ -21,16 +22,28 @@ export default function ExternalLinkModal({
copied,
onCopy,
}: ExternalLinkModalProps) {
+ const t = useTranslations("providers");
+ const tc = useTranslations("common");
+ const text = (key: string, fallback: string) =>
+ typeof t.has === "function" && t.has(key as never) ? t(key as never) : fallback;
+
return (
-
+
- Compartilhe este link com quem vai autenticar a conta do Codex. A pessoa abre a
- página, faz o login da OpenAI no próprio navegador e a conexão é cadastrada aqui. Uso
- único, expira em 15 minutos.
+ {text(
+ "codexExternalLinkModalDescription",
+ "Share this single-use link with the person who will authenticate the Codex account. They open it in their own browser, complete the OpenAI login, and the connection is registered here. The link expires in 15 minutes."
+ )}
{loading ? (
-
Gerando link…
+
+ {text("codexExternalLinkGenerating", "Generating link...")}
+
) : error ? (
{error}
) : url ? (
@@ -44,19 +57,22 @@ export default function ExternalLinkModal({
icon="open_in_new"
onClick={() => window.open(url, "_blank", "noopener")}
>
- Abrir
+ {tc("open")}
onCopy(url, "extlink")}
>
- {copied === "extlink" ? "Copiado" : "Copiar"}
+ {copied === "extlink" ? tc("copied") : tc("copy")}
sync
- Aguardando a autenticação no navegador da pessoa… esta janela atualiza sozinha.
+ {text(
+ "codexExternalLinkWaiting",
+ "Waiting for browser authentication. This window refreshes automatically."
+ )}
>
) : null}
diff --git a/src/app/(dashboard)/dashboard/providers/[id]/hooks/useExternalLinkFlow.ts b/src/app/(dashboard)/dashboard/providers/[id]/hooks/useExternalLinkFlow.ts
index 125e663c03..209ea814e9 100644
--- a/src/app/(dashboard)/dashboard/providers/[id]/hooks/useExternalLinkFlow.ts
+++ b/src/app/(dashboard)/dashboard/providers/[id]/hooks/useExternalLinkFlow.ts
@@ -1,5 +1,7 @@
import { useState, useEffect, useCallback } from "react";
+import { useTranslations } from "next-intl";
import { useCopyToClipboard } from "@/shared/hooks/useCopyToClipboard";
+import { providerText } from "../providerPageHelpers";
type UseExternalLinkFlowParams = {
providerId: string;
@@ -12,6 +14,7 @@ export function useExternalLinkFlow({
notify,
fetchConnections,
}: UseExternalLinkFlowParams) {
+ const t = useTranslations("providers") as any;
const [externalLinkModalOpen, setExternalLinkModalOpen] = useState(false);
const [externalLinkUrl, setExternalLinkUrl] = useState("");
const [externalLinkToken, setExternalLinkToken] = useState(null);
@@ -19,7 +22,7 @@ export function useExternalLinkFlow({
const [externalLinkError, setExternalLinkError] = useState(null);
const { copied: externalLinkCopied, copy: externalLinkCopy } = useCopyToClipboard();
- // "Adicionar Externo": generate a single-use public link so a third party can
+ // External Codex link: generate a single-use public link so a third party can
// complete the Codex device flow in their own browser.
const openExternalLinkFlow = useCallback(async () => {
setExternalLinkModalOpen(true);
@@ -38,14 +41,19 @@ export function useExternalLinkFlow({
setExternalLinkUrl(data.url);
setExternalLinkToken(data.token || null);
} else {
- setExternalLinkError(data?.error || "Falha ao gerar o link.");
+ setExternalLinkError(
+ data?.error ||
+ providerText(t, "codexExternalLinkCreateFailed", "Failed to generate the link.")
+ );
}
} catch {
- setExternalLinkError("Não foi possível contatar o servidor.");
+ setExternalLinkError(
+ providerText(t, "codexExternalLinkNetworkError", "Could not contact the server.")
+ );
} finally {
setExternalLinkLoading(false);
}
- }, [providerId]);
+ }, [providerId, t]);
// While the share popup is open, poll the ticket status so the dashboard can
// notify + refresh the connections the moment the external visitor finishes.
@@ -63,14 +71,22 @@ export function useExternalLinkFlow({
if (data?.status === "completed") {
active = false;
clearInterval(interval);
- notify.success("Conta Codex conectada pelo link externo.");
+ notify.success(
+ providerText(
+ t,
+ "codexExternalLinkConnected",
+ "Codex account connected through the external link."
+ )
+ );
fetchConnections();
setExternalLinkModalOpen(false);
setExternalLinkToken(null);
} else if (data?.status === "expired") {
active = false;
clearInterval(interval);
- setExternalLinkError("O link expirou sem ser concluído.");
+ setExternalLinkError(
+ providerText(t, "codexExternalLinkExpired", "The link expired before completion.")
+ );
}
} catch {
/* transient network error — keep polling */
@@ -80,7 +96,7 @@ export function useExternalLinkFlow({
active = false;
clearInterval(interval);
};
- }, [externalLinkModalOpen, externalLinkToken, providerId, notify, fetchConnections]);
+ }, [externalLinkModalOpen, externalLinkToken, providerId, notify, fetchConnections, t]);
return {
externalLinkModalOpen,
diff --git a/src/app/(dashboard)/dashboard/providers/components/CodexCliGuideModal.tsx b/src/app/(dashboard)/dashboard/providers/components/CodexCliGuideModal.tsx
index 76d13870b4..1d1effdb84 100644
--- a/src/app/(dashboard)/dashboard/providers/components/CodexCliGuideModal.tsx
+++ b/src/app/(dashboard)/dashboard/providers/components/CodexCliGuideModal.tsx
@@ -1,6 +1,7 @@
"use client";
import { useEffect, useState } from "react";
+import { useTranslations } from "next-intl";
import ReactMarkdown, { type Components } from "react-markdown";
import { Modal, Button } from "@/shared/components";
@@ -118,9 +119,13 @@ interface CodexCliGuideModalProps {
}
export default function CodexCliGuideModal({ isOpen, onClose }: CodexCliGuideModalProps) {
+ const t = useTranslations("providers");
+ const tc = useTranslations("common");
const [content, setContent] = useState("");
const [loading, setLoading] = useState(false);
const [error, setError] = useState(false);
+ const text = (key: string, fallback: string) =>
+ typeof t.has === "function" && t.has(key as never) ? t(key as never) : fallback;
useEffect(() => {
if (!isOpen || content) return;
@@ -148,14 +153,16 @@ export default function CodexCliGuideModal({ isOpen, onClose }: CodexCliGuideMod
}, [isOpen, content]);
return (
-
+
{loading && (
sync
-
Carregando guia…
+
+ {text("codexCliGuideLoading", "Loading guide...")}
+
)}
{error && (
@@ -163,7 +170,9 @@ export default function CodexCliGuideModal({ isOpen, onClose }: CodexCliGuideMod
error_outline
-
Não foi possível carregar o guia.
+
+ {text("codexCliGuideLoadFailed", "Could not load the guide.")}
+
- Tentar novamente
+ {tc("retry")}
)}
diff --git a/src/app/(dashboard)/dashboard/runtime/RuntimePageClient.tsx b/src/app/(dashboard)/dashboard/runtime/RuntimePageClient.tsx
index 7a7176f6aa..beed568a11 100644
--- a/src/app/(dashboard)/dashboard/runtime/RuntimePageClient.tsx
+++ b/src/app/(dashboard)/dashboard/runtime/RuntimePageClient.tsx
@@ -5,10 +5,7 @@ import { useTranslations } from "next-intl";
import Link from "next/link";
import Card from "@/shared/components/Card";
import ProviderIcon from "@/shared/components/ProviderIcon";
-
-// ─────────────────────────────────────────────────────────────────────────────
-// Types
-// ─────────────────────────────────────────────────────────────────────────────
+import ModelCooldownsCard from "./components/ModelCooldownsCard";
type KnownBreakerState = "CLOSED" | "OPEN" | "HALF_OPEN" | "DEGRADED";
type BreakerState = KnownBreakerState | (string & {});
@@ -615,6 +612,8 @@ export default function RuntimePageClient() {
/>
+
+
{/* Row 2 — Resilience layers (left, 2/3) + Live Feed (right, 1/3) */}
diff --git a/src/app/(dashboard)/dashboard/settings/components/ModelCooldownsCard.tsx b/src/app/(dashboard)/dashboard/runtime/components/ModelCooldownsCard.tsx
similarity index 100%
rename from src/app/(dashboard)/dashboard/settings/components/ModelCooldownsCard.tsx
rename to src/app/(dashboard)/dashboard/runtime/components/ModelCooldownsCard.tsx
diff --git a/src/app/(dashboard)/dashboard/search-tools/components/ProviderCatalog.tsx b/src/app/(dashboard)/dashboard/search-tools/components/ProviderCatalog.tsx
index bbc0df5f3f..177a9aac43 100644
--- a/src/app/(dashboard)/dashboard/search-tools/components/ProviderCatalog.tsx
+++ b/src/app/(dashboard)/dashboard/search-tools/components/ProviderCatalog.tsx
@@ -145,7 +145,10 @@ function ProviderCard({
);
}
-export default function ProviderCatalog({ selectedProvider, onSelectProvider }: ProviderCatalogProps) {
+export default function ProviderCatalog({
+ selectedProvider,
+ onSelectProvider,
+}: ProviderCatalogProps) {
const [providers, setProviders] = useState([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
@@ -211,7 +214,7 @@ export default function ProviderCatalog({ selectedProvider, onSelectProvider }:
data-testid={`filter-${kind}`}
>
{kind === "all"
- ? `Todos (${providers.length})`
+ ? `All (${providers.length})`
: kind === "search"
? `Search (${searchCount})`
: `Fetch (${fetchCount})`}
@@ -221,17 +224,14 @@ export default function ProviderCatalog({ selectedProvider, onSelectProvider }:
{filtered.length === 0 && (
- Nenhum provider encontrado.{" "}
+ No provider found.{" "}
- Configurar providers →
+ Configure providers →
)}
-
+
{filtered.map((item) => (
- 🔌
- Nenhum provider de search ativo
+
+ 🔌
+
+ No active search provider
- Configurar mais providers →
+ Configure more providers →
)}
diff --git a/src/app/(dashboard)/dashboard/search-tools/components/SearchToolsConfigPane.tsx b/src/app/(dashboard)/dashboard/search-tools/components/SearchToolsConfigPane.tsx
index 2b0aeb7330..f804f2940e 100644
--- a/src/app/(dashboard)/dashboard/search-tools/components/SearchToolsConfigPane.tsx
+++ b/src/app/(dashboard)/dashboard/search-tools/components/SearchToolsConfigPane.tsx
@@ -44,7 +44,7 @@ export default function SearchToolsConfigPane({
>
- Configuração
+ Configuration
@@ -69,7 +69,7 @@ export default function SearchToolsConfigPane({
{selectedProvider && (
- Custo:{" "}
+ Cost:{" "}
${selectedProvider.costPerQuery.toFixed(4)}/query
@@ -97,10 +97,10 @@ export default function SearchToolsConfigPane({
}
>
{selectedProvider.status === "configured"
- ? "Configurado"
+ ? "Configured"
: selectedProvider.status === "rate_limited"
? "Rate limited"
- : "Sem credencial"}
+ : "Missing credential"}
@@ -111,7 +111,7 @@ export default function SearchToolsConfigPane({
{activeTab === "search" && (
- Tipo de busca
+ Search type
- Formato
+ Format
@@ -161,7 +161,7 @@ export default function SearchToolsConfigPane({
{activeTab === "compare" && (
- Selecione até 4 providers na aba Compare para comparar em paralelo.
+ Select up to 4 providers on the Compare tab to compare them side by side.
)}
@@ -177,10 +177,7 @@ export default function SearchToolsConfigPane({
onChange={(e: React.ChangeEvent) =>
onConfigChange({ rerankModel: e.target.value })
}
- options={[
- { value: "", label: "Nenhum" },
- ...rerankModels,
- ]}
+ options={[{ value: "", label: "None" }, ...rerankModels]}
className="w-full"
/>
@@ -194,7 +191,7 @@ export default function SearchToolsConfigPane({
aria-expanded={historyExpanded}
>
- Histórico
+ History
{historyExpanded ? "▼" : "▶"}
@@ -202,7 +199,7 @@ export default function SearchToolsConfigPane({
{historyExpanded && (
- Histórico disponível na aba Search.
+ History is available on the Search tab.
)}
diff --git a/src/app/(dashboard)/dashboard/search-tools/components/tabs/CompareTab.tsx b/src/app/(dashboard)/dashboard/search-tools/components/tabs/CompareTab.tsx
index 828f41adeb..a9165a7684 100644
--- a/src/app/(dashboard)/dashboard/search-tools/components/tabs/CompareTab.tsx
+++ b/src/app/(dashboard)/dashboard/search-tools/components/tabs/CompareTab.tsx
@@ -35,16 +35,12 @@ function computeOverlap(urlsA: string[], urlsB: string[]): string {
function getBestIndex(values: number[], higherIsBetter = false): number {
if (values.length === 0) return -1;
- return higherIsBetter
- ? values.indexOf(Math.max(...values))
- : values.indexOf(Math.min(...values));
+ return higherIsBetter ? values.indexOf(Math.max(...values)) : values.indexOf(Math.min(...values));
}
function getWorstIndex(values: number[], higherIsBetter = false): number {
if (values.length === 0) return -1;
- return higherIsBetter
- ? values.indexOf(Math.min(...values))
- : values.indexOf(Math.max(...values));
+ return higherIsBetter ? values.indexOf(Math.min(...values)) : values.indexOf(Math.max(...values));
}
/** Build a map of url → count across all compare results to find overlaps */
@@ -61,7 +57,7 @@ function buildUrlCountMap(allResults: CompareResult[]): Map {
export default function CompareTab({ providers, onMetrics }: CompareTabProps) {
const t = useTranslations("search");
const activeSearchProviders = providers.filter(
- (p) => p.kind === "search" && p.status === "configured",
+ (p) => p.kind === "search" && p.status === "configured"
);
const [query, setQuery] = useState("");
@@ -143,7 +139,7 @@ export default function CompareTab({ providers, onMetrics }: CompareTabProps) {
error: err instanceof Error ? err.message : "Failed",
} as CompareResult;
}
- }),
+ })
);
const resolved = settled.map((s) =>
@@ -158,7 +154,7 @@ export default function CompareTab({ providers, onMetrics }: CompareTabProps) {
urls: [],
results: [],
error: "Request failed",
- } as CompareResult),
+ } as CompareResult)
);
setResults(resolved);
setLoading(false);
@@ -188,15 +184,14 @@ export default function CompareTab({ providers, onMetrics }: CompareTabProps) {
className="flex flex-col items-center justify-center flex-1 py-16 text-center"
data-testid="compare-no-providers"
>
- ⚖
+
+ ⚖
+
- Nenhum provider de search ativo — configure em Providers →
+ No active search provider. Configure one in Providers.
-
- Configurar providers
+
+ Configure providers
);
@@ -405,8 +400,7 @@ export default function CompareTab({ providers, onMetrics }: CompareTabProps) {
{cr.provider.replace("-search", "")}
{": "}
- {cr.error ? "—" : computeOverlap(baseUrls, cr.urls)}{" "}
- in common
+ {cr.error ? "—" : computeOverlap(baseUrls, cr.urls)} in common
);
})}
@@ -422,7 +416,9 @@ export default function CompareTab({ providers, onMetrics }: CompareTabProps) {
className="flex flex-col items-center justify-center flex-1 py-12 text-center"
data-testid="compare-empty-state"
>
- ⚖
+
+ ⚖
+
Select providers and enter a query to compare
diff --git a/src/app/(dashboard)/dashboard/search-tools/components/tabs/ScrapeTab.tsx b/src/app/(dashboard)/dashboard/search-tools/components/tabs/ScrapeTab.tsx
index 5625663d70..c94ec90ee4 100644
--- a/src/app/(dashboard)/dashboard/search-tools/components/tabs/ScrapeTab.tsx
+++ b/src/app/(dashboard)/dashboard/search-tools/components/tabs/ScrapeTab.tsx
@@ -41,7 +41,8 @@ export default function ScrapeTab({ configState, onMetrics }: ScrapeTabProps) {
url: url.trim(),
format: configState.fetchFormat,
full_page: configState.fullPage,
- provider: configState.provider && configState.provider !== "auto" ? configState.provider : undefined,
+ provider:
+ configState.provider && configState.provider !== "auto" ? configState.provider : undefined,
});
};
@@ -97,17 +98,20 @@ export default function ScrapeTab({ configState, onMetrics }: ScrapeTabProps) {
{/* Options summary */}
- Formato:{" "}
- {configState.fetchFormat}
+ Format: {configState.fetchFormat}
Full page:{" "}
- {configState.fullPage ? "Sim" : "Não"}
+
+ {configState.fullPage ? "Yes" : "No"}
+
Provider:{" "}
- {configState.provider === "auto" || !configState.provider ? "auto" : configState.provider}
+ {configState.provider === "auto" || !configState.provider
+ ? "auto"
+ : configState.provider}
@@ -125,10 +129,7 @@ export default function ScrapeTab({ configState, onMetrics }: ScrapeTabProps) {
{/* Loading spinner */}
{loading && (
-
+
- )}
+ {result && !loading &&
}
{/* Empty state — no result yet */}
{!result && !loading && !error && (
@@ -152,9 +151,7 @@ export default function ScrapeTab({ configState, onMetrics }: ScrapeTabProps) {
📄
-
- Digite uma URL para extrair o conteúdo
-
+
Digite uma URL para extrair o conteúdo
Providers disponíveis: Firecrawl, Jina Reader, Tavily.{" "}
diff --git a/src/app/(dashboard)/dashboard/settings/SettingsPageClient.tsx b/src/app/(dashboard)/dashboard/settings/SettingsPageClient.tsx
deleted file mode 100644
index 8691b383da..0000000000
--- a/src/app/(dashboard)/dashboard/settings/SettingsPageClient.tsx
+++ /dev/null
@@ -1,47 +0,0 @@
-"use client";
-
-import { useEffect, useState } from "react";
-import { SegmentedControl } from "@/shared/components";
-import AppearanceTab from "./components/AppearanceTab";
-import ResilienceTab from "./components/ResilienceTab";
-import SystemStorageTab from "./components/SystemStorageTab";
-
-export type SettingsTab = "general" | "appearance" | "resilience";
-
-const SETTINGS_TABS: Array<{ value: SettingsTab; label: string; icon: string }> = [
- { value: "general", label: "General", icon: "settings" },
- { value: "appearance", label: "Appearance", icon: "palette" },
- { value: "resilience", label: "Resilience", icon: "health_and_safety" },
-];
-
-type SettingsPageClientProps = {
- initialTab: SettingsTab;
-};
-
-export default function SettingsPageClient({ initialTab }: SettingsPageClientProps) {
- const [activeTab, setActiveTab] = useState(initialTab);
-
- useEffect(() => {
- setActiveTab(initialTab);
- }, [initialTab]);
-
- const activeLabel = SETTINGS_TABS.find((tab) => tab.value === activeTab)?.label || "General";
-
- return (
-
-
setActiveTab(value as SettingsTab)}
- aria-label="Settings sections"
- className="w-fit"
- />
-
-
- {activeTab === "general" ?
: null}
- {activeTab === "appearance" ?
: null}
- {activeTab === "resilience" ?
: null}
-
-
- );
-}
diff --git a/src/app/(dashboard)/dashboard/settings/components/AppearanceTab.tsx b/src/app/(dashboard)/dashboard/settings/components/AppearanceTab.tsx
index 32e030363d..8ff4799d0d 100644
--- a/src/app/(dashboard)/dashboard/settings/components/AppearanceTab.tsx
+++ b/src/app/(dashboard)/dashboard/settings/components/AppearanceTab.tsx
@@ -261,27 +261,40 @@ export default function AppearanceTab() {
disabled={loading}
/>
+
+
+
-
-
-
- {getSettingsLabel("endpointTokenSaver", "Token Saver")}
-
-
- {getSettingsLabel(
- "endpointTokenSaverDesc",
- "Show the Token Saver panel on the Endpoint page."
- )}
-
-
-
{
- await updateSetting(SHOW_TOKEN_SAVER_ON_ENDPOINT_KEY, checked);
- }}
- disabled={loading}
- />
+
+
+
+ {getSettingsLabel("endpointPinInformationTitle", "Pin Information to Endpoint Page")}
+
+
+ Choose which sections to pin to the top of the Endpoint page.
+
+
+
+
+
+
+
+ {getSettingsLabel("endpointTokenSaver", "Token Saver")}
+
+
+ {getSettingsLabel(
+ "endpointTokenSaverDesc",
+ "Show the Token Saver panel on the Endpoint page."
+ )}
+
+
{
+ await updateSetting(SHOW_TOKEN_SAVER_ON_ENDPOINT_KEY, checked);
+ }}
+ disabled={loading}
+ />
diff --git a/src/app/(dashboard)/dashboard/settings/components/BackgroundDegradationTab.tsx b/src/app/(dashboard)/dashboard/settings/components/BackgroundDegradationTab.tsx
index d42926584f..ef35d57e96 100644
--- a/src/app/(dashboard)/dashboard/settings/components/BackgroundDegradationTab.tsx
+++ b/src/app/(dashboard)/dashboard/settings/components/BackgroundDegradationTab.tsx
@@ -1,7 +1,7 @@
"use client";
import { useState, useEffect } from "react";
-import { Card } from "@/shared/components";
+import { Card, Toggle } from "@/shared/components";
import { useTranslations } from "next-intl";
export default function BackgroundDegradationTab() {
@@ -120,19 +120,12 @@ export default function BackgroundDegradationTab() {
"Automatically use cheaper models for background utility tasks"}
- save({ enabled: !config.enabled })}
+ save({ enabled })}
disabled={loading || saving}
- className={`relative inline-flex h-6 w-11 items-center rounded-full transition-colors ${
- config.enabled ? "bg-sky-500" : "bg-white/10"
- }`}
- >
-
-
+ ariaLabel={t("enableDegradation") || "Enable Background Degradation"}
+ />
{/* Stats */}
diff --git a/src/app/(dashboard)/dashboard/settings/components/ResilienceTab.tsx b/src/app/(dashboard)/dashboard/settings/components/ResilienceTab.tsx
index 3e9d28edac..fda5ce43bf 100644
--- a/src/app/(dashboard)/dashboard/settings/components/ResilienceTab.tsx
+++ b/src/app/(dashboard)/dashboard/settings/components/ResilienceTab.tsx
@@ -5,7 +5,6 @@ import { Button, Card } from "@/shared/components";
import { useNotificationStore } from "@/store/notificationStore";
import { useTranslations } from "next-intl";
import AutoDisableCard from "./AutoDisableCard";
-import ModelCooldownsCard from "./ModelCooldownsCard";
import ModelLockoutCard from "./ModelLockoutCard";
type RequestQueueSettings = {
@@ -802,7 +801,9 @@ function ProviderCooldownCard({
) : (
<>
-
{t("resilienceProviderCooldownEnabled")}
+
+ {t("resilienceProviderCooldownEnabled")}
+
{value.enabled ? t("statusEnabled") : t("statusDisabled")}
@@ -932,24 +933,7 @@ export default function ResilienceTab() {
return (
-
-
-
-
info
-
-
- {tx("resilienceStructureTitle", "Resilience Structure")}
-
-
- {tx(
- "resilienceStructureDesc",
- "This page only configures behavior. Live breaker state is shown on the Health page. Combo-specific retry and round-robin slot control remain on combo settings."
- )}
-
-
-
-
{
+ setClearCacheLoading(true);
+ setClearCacheStatus({ type: "", message: "" });
+ try {
+ const res = await fetch("/api/cache", { method: "DELETE" });
+ const data = await res.json().catch(() => null);
+ if (res.ok) {
+ setClearCacheStatus({
+ type: "success",
+ message: t("cacheCleared") || "Cache cleared successfully",
+ });
+ } else {
+ setClearCacheStatus({
+ type: "error",
+ message: data?.error || t("clearCacheFailed") || "Failed to clear cache",
+ });
+ }
+ } catch {
+ setClearCacheStatus({ type: "error", message: t("errorOccurred") });
+ } finally {
+ setClearCacheLoading(false);
+ }
+ };
+
+ const handlePurgeExpiredLogs = async () => {
+ setPurgeLogsLoading(true);
+ setPurgeLogsStatus({ type: "", message: "" });
+ try {
+ const res = await fetch("/api/settings/purge-logs", { method: "POST" });
+ const data = await res.json().catch(() => null);
+ if (res.ok) {
+ const deleted = data?.deleted ?? 0;
+ setPurgeLogsStatus({
+ type: "success",
+ message: t("logsDeleted", { count: deleted }) || `Purged ${deleted} expired log(s)`,
+ });
+ } else {
+ setPurgeLogsStatus({
+ type: "error",
+ message: data?.error || t("purgeLogsFailed") || "Failed to purge logs",
+ });
+ }
+ } catch {
+ setPurgeLogsStatus({ type: "error", message: t("errorOccurred") });
+ } finally {
+ setPurgeLogsLoading(false);
+ }
+ };
+
+ const handleManualVacuum = async () => {
+ setManualVacuumLoading(true);
+ setManualVacuumStatus({ type: "", message: "" });
+ try {
+ const res = await fetch("/api/settings/database/vacuum", { method: "POST" });
+ const data = await res.json().catch(() => null);
+ if (res.ok && data?.success !== false) {
+ setManualVacuumStatus({
+ type: "success",
+ message: data?.message || "VACUUM completed",
+ });
+ await loadDatabaseSettings();
+ await loadStorageHealth();
+ } else {
+ setManualVacuumStatus({
+ type: "error",
+ message: data?.error || "VACUUM failed",
+ });
+ }
+ } catch {
+ setManualVacuumStatus({ type: "error", message: t("errorOccurred") });
+ } finally {
+ setManualVacuumLoading(false);
+ }
+ };
+
const handleManualBackup = async () => {
setManualBackupLoading(true);
setManualBackupStatus({ type: "", message: "" });
@@ -579,98 +656,6 @@ export default function SystemStorageTab() {
- {/* Logs Settings Section */}
-
-
-
-
{t("logsSettingsTitle")}
-
- Configure detailed logging and call log pipeline settings
-
-
-
-
-
-
- {t("detailedLogsLabel")}
- {t("detailedLogsDesc")}
-
-
-
-
- {t("callLogPipelineLabel")}
- {t("callLogPipelineDesc")}
-
-
-
-
- {t("maxDetailSizeLabel")}
- {t("maxDetailSizeDesc")}
-
-
-
-
- {t("ringBufferSizeLabel")}
- {t("ringBufferSizeDesc")}
-
-
-
-
-
- {/* Cache Settings Section */}
-
-
-
-
{t("cacheSettings")}
-
- Configure semantic and prompt caching behavior
-
-
-
-
-
-
- {t("semanticCacheEnabledLabel")}
-
- Enable semantic caching for similar requests
-
-
-
-
-
- {t("semanticCacheMaxSizeLabel")}
- {t("semanticCacheMaxSizeDesc")}
-
-
-
-
- {t("semanticCacheTTLLabel")}
-
- Time-to-live for semantic cache entries (ms)
-
-
-
-
-
- {t("promptCacheEnabledLabel")}
- {t("promptCacheEnabledDesc")}
-
-
-
-
- {t("promptCacheStrategyLabel")}
- {t("promptCacheStrategyDesc")}
-
-
-
-
- {t("alwaysPreserveClientCacheLabel")}
- {t("alwaysPreserveClientCacheDesc")}
-
-
-
-
-
@@ -888,111 +873,62 @@ export default function SystemStorageTab() {
{t("maintenance") || "Maintenance"}
-
+
{
- setClearCacheLoading(true);
- setClearCacheStatus({ type: "", message: "" });
- try {
- const res = await fetch("/api/cache", { method: "DELETE" });
- const data = await res.json();
- if (res.ok) {
- setClearCacheStatus({
- type: "success",
- message: t("cacheCleared") || "Cache cleared successfully",
- });
- } else {
- setClearCacheStatus({
- type: "error",
- message: data.error || t("clearCacheFailed") || "Failed to clear cache",
- });
- }
- } catch {
- setClearCacheStatus({ type: "error", message: t("errorOccurred") });
- } finally {
- setClearCacheLoading(false);
- }
- }}
+ onClick={handleClearCache}
>
delete_sweep
{t("clearCache") || "Clear Cache"}
- {clearCacheStatus.message && (
-
-
-
- {clearCacheStatus.type === "success" ? "check_circle" : "error"}
-
- {clearCacheStatus.message}
-
-
- )}
-
-
{
- setPurgeLogsLoading(true);
- setPurgeLogsStatus({ type: "", message: "" });
- try {
- const res = await fetch("/api/settings/purge-logs", { method: "POST" });
- const data = await res.json();
- if (res.ok) {
- setPurgeLogsStatus({
- type: "success",
- message:
- t("logsDeleted", { count: data.deleted }) ||
- `Purged ${data.deleted} expired log(s)`,
- });
- } else {
- setPurgeLogsStatus({
- type: "error",
- message: data.error || t("purgeLogsFailed") || "Failed to purge logs",
- });
- }
- } catch {
- setPurgeLogsStatus({ type: "error", message: t("errorOccurred") });
- } finally {
- setPurgeLogsLoading(false);
- }
- }}
+ onClick={handlePurgeExpiredLogs}
>
auto_delete
{t("purgeExpiredLogs") || "Purge Expired Logs"}
- {purgeLogsStatus.message && (
-
-
-
- {purgeLogsStatus.type === "success" ? "check_circle" : "error"}
-
- {purgeLogsStatus.message}
+
+
+ cleaning_services
+
+ Manual VACUUM
+
+
+
+ {[clearCacheStatus, purgeLogsStatus, manualVacuumStatus]
+ .filter((status) => status.message)
+ .map((status, index) => (
+
+
+
+ {status.type === "success" ? "check_circle" : "error"}
+
+ {status.message}
+
-
- )}
+ ))}
diff --git a/src/app/(dashboard)/dashboard/settings/page.tsx b/src/app/(dashboard)/dashboard/settings/page.tsx
index e828e5e6d3..963f097a3c 100644
--- a/src/app/(dashboard)/dashboard/settings/page.tsx
+++ b/src/app/(dashboard)/dashboard/settings/page.tsx
@@ -1,22 +1,30 @@
-import SettingsPageClient, { type SettingsTab } from "./SettingsPageClient";
+import { redirect } from "next/navigation";
-const LEGACY_TAB_ROUTES: Record
= {
- appearance: "appearance",
- general: "general",
- resilience: "resilience",
+const LEGACY_TAB_ROUTES: Record = {
+ advanced: "/dashboard/settings/advanced",
+ ai: "/dashboard/settings/ai",
+ appearance: "/dashboard/settings/appearance",
+ featureFlags: "/dashboard/settings/feature-flags",
+ "feature-flags": "/dashboard/settings/feature-flags",
+ general: "/dashboard/settings/general",
+ resilience: "/dashboard/settings/resilience",
+ routing: "/dashboard/settings/routing",
+ security: "/dashboard/settings/security",
+ sidebar: "/dashboard/settings/sidebar",
};
type SettingsPageProps = {
searchParams?: Promise>;
};
-function normalizeTab(value: string | undefined): SettingsTab {
- return value && value in LEGACY_TAB_ROUTES ? LEGACY_TAB_ROUTES[value] : "general";
+function resolveSettingsRoute(value: string | undefined): string {
+ return value
+ ? LEGACY_TAB_ROUTES[value] || "/dashboard/settings/general"
+ : "/dashboard/settings/general";
}
export default async function SettingsPage({ searchParams }: SettingsPageProps) {
const params = searchParams ? await searchParams : {};
const tab = Array.isArray(params.tab) ? params.tab[0] : params.tab;
-
- return ;
+ redirect(resolveSettingsRoute(tab));
}
diff --git a/src/app/(dashboard)/dashboard/settings/resilience/page.tsx b/src/app/(dashboard)/dashboard/settings/resilience/page.tsx
index e3e9c32290..64eae85a0f 100644
--- a/src/app/(dashboard)/dashboard/settings/resilience/page.tsx
+++ b/src/app/(dashboard)/dashboard/settings/resilience/page.tsx
@@ -7,7 +7,9 @@ export default function SettingsResiliencePage() {
const t = useTranslations("settings");
return (
-
{t("resilienceSettingsIntro")}
+
+ {t("resilienceSettingsIntro")} {t("resilienceStructureDesc")}
+
);
diff --git a/src/app/(dashboard)/dashboard/settings/routing/page.tsx b/src/app/(dashboard)/dashboard/settings/routing/page.tsx
index 31dc24a2d3..49ebebd7a0 100644
--- a/src/app/(dashboard)/dashboard/settings/routing/page.tsx
+++ b/src/app/(dashboard)/dashboard/settings/routing/page.tsx
@@ -12,9 +12,9 @@ export default function SettingsRoutingPage() {
return (
{t("routingSettingsIntro")}
+
-
diff --git a/src/app/(dashboard)/dashboard/translator/components/MonitorTab.tsx b/src/app/(dashboard)/dashboard/translator/components/MonitorTab.tsx
index a03474d2c2..d5c6eacc26 100644
--- a/src/app/(dashboard)/dashboard/translator/components/MonitorTab.tsx
+++ b/src/app/(dashboard)/dashboard/translator/components/MonitorTab.tsx
@@ -35,10 +35,7 @@ interface StatCardProps {
color: "blue" | "green" | "red" | "purple" | "amber" | "cyan";
}
-const COLOR_MAP: Record<
- StatCardProps["color"],
- { shell: string; icon: string }
-> = {
+const COLOR_MAP: Record = {
blue: { shell: "bg-blue-500/10", icon: "text-blue-500" },
green: { shell: "bg-green-500/10", icon: "text-green-500" },
red: { shell: "bg-red-500/10", icon: "text-red-500" },
@@ -92,7 +89,7 @@ export default function MonitorTab({ onGoToTranslate }: MonitorTabProps) {
return fallback;
}
},
- [t],
+ [t]
);
const [events, setEvents] = useState([]);
@@ -136,9 +133,8 @@ export default function MonitorTab({ onGoToTranslate }: MonitorTabProps) {
const successCount = events.filter((e) => e.status === "success").length;
const errorCount = events.filter((e) => e.status === "error").length;
const comboCount = events.filter((e) => e.isComboRouted).length;
- const uniqueEndpoints = new Set(
- events.map((e) => e.routeEndpoint ?? e.endpoint).filter(Boolean),
- ).size;
+ const uniqueEndpoints = new Set(events.map((e) => e.routeEndpoint ?? e.endpoint).filter(Boolean))
+ .size;
const avgLatency =
events.length > 0
? Math.round(events.reduce((sum, e) => sum + (e.latency ?? 0), 0) / events.length)
@@ -160,7 +156,7 @@ export default function MonitorTab({ onGoToTranslate }: MonitorTabProps) {
{translateOrFallback(
"monitorOriginHint",
- "Eventos gerados pelo Translate ou pelo pipeline principal aparecem aqui em tempo real.",
+ "Eventos gerados pelo Translate ou pelo pipeline principal aparecem aqui em tempo real."
)}
@@ -269,12 +265,12 @@ export default function MonitorTab({ onGoToTranslate }: MonitorTabProps) {
@@ -284,9 +280,7 @@ export default function MonitorTab({ onGoToTranslate }: MonitorTabProps) {
{t("time")}
-
- {translateOrFallback("routeDetails", "Route")}
-
+ {translateOrFallback("routeDetails", "Route")}
{t("source")}
{t("target")}
{t("model")}
diff --git a/src/app/(dashboard)/dashboard/translator/components/TranslateFlowDiagram.tsx b/src/app/(dashboard)/dashboard/translator/components/TranslateFlowDiagram.tsx
index 11c4ec6264..320b8db951 100644
--- a/src/app/(dashboard)/dashboard/translator/components/TranslateFlowDiagram.tsx
+++ b/src/app/(dashboard)/dashboard/translator/components/TranslateFlowDiagram.tsx
@@ -12,10 +12,7 @@ interface FlowNodeProps {
tooltipContent?: string;
}
-const COLOR_MAP: Record<
- FlowNodeProps["color"],
- { border: string; bg: string; text: string }
-> = {
+const COLOR_MAP: Record = {
primary: { border: "border-primary/30", bg: "bg-primary/5", text: "text-primary" },
orange: { border: "border-orange-500/30", bg: "bg-orange-500/5", text: "text-orange-500" },
blue: { border: "border-blue-500/30", bg: "bg-blue-500/5", text: "text-blue-500" },
@@ -66,9 +63,7 @@ function FlowArrow({ label }: { label?: string }) {
>
arrow_forward
- {label && (
- {label}
- )}
+ {label && {label} }
);
}
@@ -84,7 +79,7 @@ export default function TranslateFlowDiagram() {
return fallback;
}
},
- [t],
+ [t]
);
return (
@@ -92,18 +87,18 @@ export default function TranslateFlowDiagram() {
-
+
@@ -111,21 +106,21 @@ export default function TranslateFlowDiagram() {
icon="hub"
color="emerald"
title={tr("conceptDiagramHubLabel", "OpenAI (hub)")}
- example={tr("conceptDiagramExampleHub", "formato pivô")}
+ example={tr("conceptDiagramExampleHub", "pivot format")}
tooltipContent={tr(
"conceptDiagramHubTooltip",
- "Hub intermediário usado pelo translator para converter entre formatos não-compatíveis diretamente. Todos os formatos passam por OpenAI como pivô.",
+ "Intermediate hub used by the translator to convert between formats that are not directly compatible. All formats pass through OpenAI as the pivot."
)}
/>
diff --git a/src/app/(dashboard)/dashboard/translator/components/TranslatorConceptCard.tsx b/src/app/(dashboard)/dashboard/translator/components/TranslatorConceptCard.tsx
index c612788e2e..151e2ffb77 100644
--- a/src/app/(dashboard)/dashboard/translator/components/TranslatorConceptCard.tsx
+++ b/src/app/(dashboard)/dashboard/translator/components/TranslatorConceptCard.tsx
@@ -18,7 +18,7 @@ export default function TranslatorConceptCard() {
return fallback;
}
},
- [t],
+ [t]
);
return (
@@ -35,13 +35,13 @@ export default function TranslatorConceptCard() {
{tr(
"conceptHeadline",
- 'Sua app fala o "idioma" de uma API. O Translator converte para usar outro provider.',
+ 'Your app speaks one API "language". Translator converts it to use another provider.'
)}
{tr(
"friendlySubtitle",
- "Use sua app existente com qualquer provider — sem reescrever código.",
+ "Use your existing app with any provider without rewriting code."
)}
@@ -56,7 +56,7 @@ export default function TranslatorConceptCard() {
aria-controls="translator-concept-how-it-works"
className="flex items-center gap-2 text-xs font-medium text-primary hover:text-primary/80 transition-colors w-full justify-start py-1 rounded"
>
- {tr("conceptHowItWorksToggle", "Como funciona")}
+ {tr("conceptHowItWorksToggle", "How it works")}
{open ? "expand_less" : "expand_more"}
@@ -69,7 +69,7 @@ export default function TranslatorConceptCard() {
>
{tr(
"conceptHowItWorksBody",
- "Sua app envia um pedido no formato dela. O Translator detecta o formato, converte via OpenAI como hub intermediário (ou direto, quando há tradutor direto disponível), envia ao provider escolhido e devolve a resposta convertida de volta no formato da sua app.",
+ "Your app sends a request in its own format. Translator detects that format, converts through OpenAI as an intermediate hub (or directly when a direct translator is available), sends it to the selected provider, and converts the response back to your app's format."
)}
)}
diff --git a/src/app/(dashboard)/dashboard/translator/components/advanced/TestBenchAccordion.tsx b/src/app/(dashboard)/dashboard/translator/components/advanced/TestBenchAccordion.tsx
index 3bc14deaea..309a841de4 100644
--- a/src/app/(dashboard)/dashboard/translator/components/advanced/TestBenchAccordion.tsx
+++ b/src/app/(dashboard)/dashboard/translator/components/advanced/TestBenchAccordion.tsx
@@ -160,7 +160,7 @@ function TestBenchContent() {
const latency = Date.now() - start;
if (!sendRes.ok) {
- const errData = await sendRes.json().catch(() => ({})) as { error?: string };
+ const errData = (await sendRes.json().catch(() => ({}))) as { error?: string };
setResults((prev) => ({
...prev,
[scenario.id]: {
@@ -425,10 +425,7 @@ function TestBenchContent() {
);
}
-export default function TestBenchAccordion({
- forceOpen,
- onOpenChange,
-}: TestBenchAccordionProps) {
+export default function TestBenchAccordion({ forceOpen, onOpenChange }: TestBenchAccordionProps) {
const t = useTranslations("translator");
const translateOrFallback = (key: string, fallback: string): string => {
@@ -462,10 +459,10 @@ export default function TestBenchAccordion({
return (
{t("noMatchingLogs")}
) : (
-
-
-
+
+
+
{visibleColumns.status && (
-
- {t("colStatus")}
-
+ {t("colStatus")}
)}
{visibleColumns.proxy && (
-
- {t("colProxy")}
-
+ {t("colProxy")}
)}
{visibleColumns.tls && (
-
- {t("colTls")}
-
+ {t("colTls")}
)}
{visibleColumns.type && (
-
- {t("colType")}
-
+ {t("colType")}
)}
{visibleColumns.level && (
-
- {t("colLevel")}
-
+ {t("colLevel")}
)}
{visibleColumns.provider && (
-
- {t("colProvider")}
-
+ {t("colProvider")}
)}
{visibleColumns.target && (
-
- {t("colTarget")}
-
+ {t("colTarget")}
)}
{visibleColumns.latency && (
-
- {t("colLatency")}
-
+ {t("colLatency")}
)}
{visibleColumns.ip && (
-
- {t("colClientIp")}
-
+ {t("colClientIp")}
)}
{visibleColumns.time && (
-
- {t("colTime")}
-
+ {t("colTime")}
)}
diff --git a/src/shared/components/RequestLoggerV2.tsx b/src/shared/components/RequestLoggerV2.tsx
index 039c2f5615..952eaaf29d 100644
--- a/src/shared/components/RequestLoggerV2.tsx
+++ b/src/shared/components/RequestLoggerV2.tsx
@@ -39,6 +39,14 @@ import {
readSavedRefreshIntervalSec,
writeSavedRefreshIntervalSec,
} from "./requestLoggerPreferences";
+import {
+ LOG_TABLE_CLASS,
+ LOG_TABLE_HEAD_CLASS,
+ LOG_TABLE_HEADER_BG_STYLE,
+ LOG_TABLE_HEADER_CELL_CLASS,
+ LOG_TABLE_HEADER_CELL_RIGHT_CLASS,
+ LOG_TABLE_ROW_CLASS,
+} from "./logTableStyles";
// Number of call-log rows fetched per page. The viewer grows its window by this
// amount on "Load more" / infinite scroll so users can browse past the first
@@ -935,79 +943,47 @@ const RequestLoggerV2 = forwardRef{t("noMatchingLogs")}
) : (
-
-
-
+
+
+
{visibleColumns.status && (
-
- {t("columns.status")}
-
+ {t("columns.status")}
)}
{visibleColumns.cacheSource && (
-
- {t("columns.cacheSource")}
-
+ {t("columns.cacheSource")}
)}
{visibleColumns.model && (
-
- {t("columns.model")}
-
+ {t("columns.model")}
)}
{visibleColumns.requestedModel && (
-
- {t("columns.requested")}
-
+ {t("columns.requested")}
)}
{visibleColumns.provider && (
-
- {t("columns.provider")}
-
+ {t("columns.provider")}
)}
{visibleColumns.protocol && (
-
- {t("columns.protocol")}
-
+ {t("columns.protocol")}
)}
{visibleColumns.account && (
-
- {t("columns.account")}
-
+ {t("columns.account")}
)}
{visibleColumns.apiKey && (
-
- {t("columns.apiKey")}
-
+ {t("columns.apiKey")}
)}
{visibleColumns.combo && (
-
- {t("columns.combo")}
-
+ {t("columns.combo")}
)}
{visibleColumns.tokens && (
-
- {t("columns.tokens")}
-
+ {t("columns.tokens")}
)}
{visibleColumns.tps && (
-
- {t("columns.tps")}
-
+ {t("columns.tps")}
)}
{visibleColumns.duration && (
-
- {t("columns.duration")}
-
+ {t("columns.duration")}
)}
{visibleColumns.time && (
-
- {t("columns.time")}
-
+ {t("columns.time")}
)}
diff --git a/src/shared/components/compression/EngineConfigPage.tsx b/src/shared/components/compression/EngineConfigPage.tsx
index 663b0ec913..4d78744c33 100644
--- a/src/shared/components/compression/EngineConfigPage.tsx
+++ b/src/shared/components/compression/EngineConfigPage.tsx
@@ -242,9 +242,11 @@ export function EngineConfigPage({ engineId }: { engineId: string }) {
{toggleError &&
{toggleError}
}
- As camadas ativadas rodam quando a compressão está no modo "stacked". Configure
- em{" "}
+ Enabled layers run when compression is in "stacked" mode. Configure it in{" "}
Compression Settings
@@ -269,7 +270,7 @@ export function EngineConfigPage({ engineId }: { engineId: string }) {
{/* ── Config form ── */}
-
Configuração
+
Configuration
- {saving ? "Salvando…" : "Salvar"}
+ {saving ? "Saving..." : "Save"}
{saveError && {saveError}
}
@@ -302,20 +303,20 @@ export function EngineConfigPage({ engineId }: { engineId: string }) {
disabled={previewLoading}
className="px-4 py-1.5 rounded bg-primary text-primary-foreground text-sm font-medium disabled:opacity-50"
>
- {previewLoading ? "Processando…" : "Preview"}
+ {previewLoading ? "Processing..." : "Preview"}
{previewError && {previewError}
}
{preview && (
- Tokens originais: {preview.originalTokens}
+ Original tokens: {preview.originalTokens}
- Tokens comprimidos: {preview.compressedTokens}
+ Compressed tokens: {preview.compressedTokens}
- Economia: {preview.savingsPct.toFixed(1)}%
+ Savings: {preview.savingsPct.toFixed(1)}%
)}
@@ -323,17 +324,20 @@ export function EngineConfigPage({ engineId }: { engineId: string }) {
{/* ── Analytics strip ── */}
-
Últimos 7 dias
+
Last 7 days
{analytics && analytics.runs === 0 ? (
-
Sem dados ainda / No data yet
+
No data yet
) : analytics ? (
-
-
-
+
+
+
) : (
-
Sem dados ainda / No data yet
+
No data yet
)}
diff --git a/src/shared/components/logTableStyles.ts b/src/shared/components/logTableStyles.ts
new file mode 100644
index 0000000000..99ba7851bc
--- /dev/null
+++ b/src/shared/components/logTableStyles.ts
@@ -0,0 +1,7 @@
+export const LOG_TABLE_CLASS = "w-full text-left border-collapse text-xs";
+export const LOG_TABLE_HEAD_CLASS = "sticky top-0 z-10";
+export const LOG_TABLE_ROW_CLASS = "border-b border-border";
+export const LOG_TABLE_HEADER_CELL_CLASS =
+ "px-3 py-2.5 font-semibold text-text-muted uppercase tracking-wider text-[10px]";
+export const LOG_TABLE_HEADER_CELL_RIGHT_CLASS = `${LOG_TABLE_HEADER_CELL_CLASS} text-right`;
+export const LOG_TABLE_HEADER_BG_STYLE = { backgroundColor: "var(--color-bg, #fff)" } as const;
diff --git a/src/shared/constants/sidebarVisibility.ts b/src/shared/constants/sidebarVisibility.ts
index d41e4ad701..cc194d69ee 100644
--- a/src/shared/constants/sidebarVisibility.ts
+++ b/src/shared/constants/sidebarVisibility.ts
@@ -85,7 +85,6 @@ export const HIDEABLE_SIDEBAR_ITEM_IDS = [
"batch",
"batch-files",
// Configuration
- "settings",
"settings-general",
"settings-appearance",
"settings-ai",
@@ -755,13 +754,6 @@ const BATCH_GROUP: SidebarItemGroup = {
};
const CONFIGURATION_ITEMS: readonly SidebarItemDefinition[] = [
- {
- id: "settings",
- href: "/dashboard/settings",
- i18nKey: "settings",
- subtitleKey: "settingsSubtitle",
- icon: "settings",
- },
{
id: "settings-general",
href: "/dashboard/settings/general",
@@ -955,7 +947,7 @@ const MINIMAL_SHOWN: ReadonlySet = new Set([
"costs",
"logs",
"health",
- "settings",
+ "settings-general",
"settings-sidebar",
"docs",
"changelog",
@@ -988,7 +980,7 @@ const DEVELOPER_SHOWN: ReadonlySet = new Set([
"skills",
"mcp",
"a2a",
- "settings",
+ "settings-general",
"settings-routing",
"settings-resilience",
"settings-sidebar",
@@ -1019,7 +1011,6 @@ const ADMIN_SHOWN: ReadonlySet = new Set([
"audit",
"audit-mcp",
"audit-a2a",
- "settings",
"settings-general",
"settings-routing",
"settings-resilience",
diff --git a/tests/unit/dashboard-shell-tabs.test.ts b/tests/unit/dashboard-shell-tabs.test.ts
index bcda5fb7fd..fb09fab916 100644
--- a/tests/unit/dashboard-shell-tabs.test.ts
+++ b/tests/unit/dashboard-shell-tabs.test.ts
@@ -45,19 +45,13 @@ test("endpoint page keeps APIs, MCP, and A2A as in-page tabs", () => {
assert.ok(source.includes('activeEndpointTab === "a2a" ? : null'));
});
-test("settings root renders the General, Appearance, and Resilience tab panel", () => {
+test("settings root redirects to section pages instead of rendering a tab shell", () => {
const pageSource = readSource("src/app/(dashboard)/dashboard/settings/page.tsx");
- const clientSource = readSource("src/app/(dashboard)/dashboard/settings/SettingsPageClient.tsx");
- assert.ok(pageSource.includes("return "));
- assert.ok(
- clientSource.includes('export type SettingsTab = "general" | "appearance" | "resilience"')
- );
- for (const label of ["General", "Appearance", "Resilience"]) {
- assert.ok(clientSource.includes('label: "' + label + '"'));
- }
- assert.ok(clientSource.includes('role="tabpanel"'));
- assert.ok(clientSource.includes("aria-label={activeLabel}"));
+ assert.ok(pageSource.includes('import { redirect } from "next/navigation"'));
+ assert.ok(pageSource.includes('general: "/dashboard/settings/general"'));
+ assert.ok(pageSource.includes('resilience: "/dashboard/settings/resilience"'));
+ assert.ok(pageSource.includes("redirect(resolveSettingsRoute(tab))"));
});
test("provider limit status chips use English fallback labels", () => {
diff --git a/tests/unit/sidebar-visibility.test.ts b/tests/unit/sidebar-visibility.test.ts
index 2b5b57566e..3051364416 100644
--- a/tests/unit/sidebar-visibility.test.ts
+++ b/tests/unit/sidebar-visibility.test.ts
@@ -105,6 +105,11 @@ test("sidebar visibility drops stale entries from saved settings", () => {
false
);
assert.equal((allSidebarItemIds as string[]).includes("auto-combo"), false);
+ assert.equal(
+ (sidebarVisibility.HIDEABLE_SIDEBAR_ITEM_IDS as readonly string[]).includes("settings"),
+ false
+ );
+ assert.equal((allSidebarItemIds as string[]).includes("settings"), false);
assert.deepEqual(sidebarVisibility.normalizeHiddenSidebarItems(["auto-combo" as any, "logs"]), [
"logs",
]);
@@ -156,9 +161,15 @@ test("legacy dashboard routes redirect to their consolidated surfaces", async ()
join(repoRoot, "src/app/(dashboard)/dashboard/usage/page.tsx"),
"utf8"
);
+ const settingsPage = await readFile(
+ join(repoRoot, "src/app/(dashboard)/dashboard/settings/page.tsx"),
+ "utf8"
+ );
assert.match(autoComboPage, /redirect\("\/dashboard\/combos\?filter=intelligent"\)/);
assert.match(usagePage, /redirect\("\/dashboard\/logs"\)/);
+ assert.match(settingsPage, /redirect\(resolveSettingsRoute\(tab\)\)/);
+ assert.match(settingsPage, /\/dashboard\/settings\/general/);
const compressionPage = await readFile(
join(repoRoot, "src/app/(dashboard)/dashboard/compression/page.tsx"),
diff --git a/tests/unit/ui/system-storage-manual-vacuum.test.tsx b/tests/unit/ui/system-storage-manual-vacuum.test.tsx
new file mode 100644
index 0000000000..54509e8fd2
--- /dev/null
+++ b/tests/unit/ui/system-storage-manual-vacuum.test.tsx
@@ -0,0 +1,80 @@
+// @vitest-environment jsdom
+// Regression for #3973: the System Storage tab gained a "Manual VACUUM" button that
+// POSTs to /api/settings/database/vacuum and surfaces the result. This guards that the
+// button is rendered and wired to the vacuum endpoint (the only net-new runtime behavior
+// in the PR beyond the structurally-tested settings-shell redirect).
+import React from "react";
+import { act } from "react";
+import { createRoot, type Root } from "react-dom/client";
+import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
+
+vi.mock("next-intl", () => ({
+ useTranslations: () => (key: string) => key,
+ useLocale: () => "en",
+}));
+
+import SystemStorageTab from "@/app/(dashboard)/dashboard/settings/components/SystemStorageTab";
+
+let container: HTMLDivElement;
+let root: Root;
+const fetchCalls: Array<{ url: string; method: string }> = [];
+
+beforeEach(() => {
+ fetchCalls.length = 0;
+ vi.stubGlobal(
+ "fetch",
+ vi.fn((input: RequestInfo | URL, init?: RequestInit) => {
+ const url = typeof input === "string" ? input : input.toString();
+ const method = (init?.method || "GET").toUpperCase();
+ fetchCalls.push({ url, method });
+ // Keep the heavy DB-settings form unrendered (its shape is irrelevant to this
+ // test): a non-ok GET leaves dbSettings null, so the `!dbSettingsLoading &&
+ // dbSettings` form block is skipped while the Maintenance card stays rendered.
+ if (method === "GET" && /\/api\/settings\/database$/.test(url)) {
+ return Promise.resolve({ ok: false, status: 404, json: async () => ({}) } as unknown as Response);
+ }
+ return Promise.resolve({
+ ok: true,
+ status: 200,
+ // Permissive body covering every loader; vacuum returns a success payload.
+ json: async () => ({ success: true, message: "VACUUM completed", backups: [] }),
+ } as unknown as Response);
+ })
+ );
+ container = document.createElement("div");
+ document.body.appendChild(container);
+ root = createRoot(container);
+});
+
+afterEach(() => {
+ act(() => root.unmount());
+ container.remove();
+ vi.unstubAllGlobals();
+ vi.restoreAllMocks();
+});
+
+describe("SystemStorageTab — Manual VACUUM button (#3973)", () => {
+ it("POSTs to /api/settings/database/vacuum when the Manual VACUUM button is clicked", async () => {
+ await act(async () => {
+ root.render( );
+ });
+ // Let mount-time loaders settle.
+ await act(async () => {
+ await Promise.resolve();
+ });
+
+ const vacuumButton = Array.from(container.querySelectorAll("button")).find((b) =>
+ (b.textContent || "").includes("Manual VACUUM")
+ );
+ expect(vacuumButton, "Manual VACUUM button should be rendered").toBeTruthy();
+
+ await act(async () => {
+ vacuumButton!.dispatchEvent(new MouseEvent("click", { bubbles: true }));
+ await Promise.resolve();
+ });
+
+ const vacuumCall = fetchCalls.find((c) => c.url.includes("/api/settings/database/vacuum"));
+ expect(vacuumCall, "a POST to the vacuum endpoint should be issued").toBeTruthy();
+ expect(vacuumCall!.method).toBe("POST");
+ });
+});