From 35f96d4a40ff561f5886da128db219efbab6860b Mon Sep 17 00:00:00 2001 From: oyi77 Date: Mon, 30 Mar 2026 15:15:02 +0700 Subject: [PATCH] feat(settings): add debug toggle and sidebar visibility toggle feat(ui): replace hide-sidebar toggle with dynamic visibility toggle --- .../settings/components/AppearanceTab.tsx | 6 +- .../settings/components/ProxyTab.tsx | 44 ++++++++++++- .../api/settings/__tests__/settings.test.ts | 64 +++++++++++++++++++ src/i18n/messages/en.json | 2 + src/shared/validation/settingsSchemas.ts | 1 + 5 files changed, 112 insertions(+), 5 deletions(-) create mode 100644 src/app/api/settings/__tests__/settings.test.ts diff --git a/src/app/(dashboard)/dashboard/settings/components/AppearanceTab.tsx b/src/app/(dashboard)/dashboard/settings/components/AppearanceTab.tsx index 5afb230437..3c5d685530 100644 --- a/src/app/(dashboard)/dashboard/settings/components/AppearanceTab.tsx +++ b/src/app/(dashboard)/dashboard/settings/components/AppearanceTab.tsx @@ -221,9 +221,7 @@ export default function AppearanceTab() {
-

- {getSettingsLabel("sidebarVisibility", "Hide sidebar items")} -

+

{t("sidebarVisibilityToggle")}

{getSettingsLabel( "sidebarVisibilityDesc", @@ -249,7 +247,7 @@ export default function AppearanceTab() { >

{item.label}

toggleSidebarItem(item.id)} disabled={loading} /> diff --git a/src/app/(dashboard)/dashboard/settings/components/ProxyTab.tsx b/src/app/(dashboard)/dashboard/settings/components/ProxyTab.tsx index bc5ae5c47e..6d3ffafee1 100644 --- a/src/app/(dashboard)/dashboard/settings/components/ProxyTab.tsx +++ b/src/app/(dashboard)/dashboard/settings/components/ProxyTab.tsx @@ -1,7 +1,7 @@ "use client"; import { useState, useEffect, useRef } from "react"; -import { Card, Button, ProxyConfigModal } from "@/shared/components"; +import { Card, Button, ProxyConfigModal, Toggle } from "@/shared/components"; import { useTranslations } from "next-intl"; import ProxyRegistryManager from "./ProxyRegistryManager"; @@ -11,6 +11,8 @@ export default function ProxyTab() { const mountedRef = useRef(true); const t = useTranslations("settings"); const tc = useTranslations("common"); + const [debugMode, setDebugMode] = useState(false); + const [loading, setLoading] = useState(true); const loadGlobalProxy = async () => { try { @@ -22,6 +24,21 @@ export default function ProxyTab() { } catch {} }; + const updateDebugMode = async (value: boolean) => { + try { + const res = await fetch("/api/settings", { + method: "PATCH", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ debugMode: value }), + }); + if (res.ok) { + setDebugMode(value); + } + } catch (err) { + console.error("Failed to update debugMode:", err); + } + }; + useEffect(() => { mountedRef.current = true; async function init() { @@ -40,6 +57,19 @@ export default function ProxyTab() { }; }, []); + useEffect(() => { + fetch("/api/settings") + .then((res) => { + if (!res.ok) throw new Error(`HTTP error ${res.status}`); + return res.json(); + }) + .then((data) => { + setDebugMode(data.debugMode === true); + setLoading(false); + }) + .catch(() => setLoading(false)); + }, []); + return ( <>
@@ -78,6 +108,18 @@ export default function ProxyTab() { + +
+
+

{t("debugToggle")}

+
+ updateDebugMode(!debugMode)} + disabled={loading} + /> +
+
{ + const original = vi.importActual("../../../../lib/localDb"); + return { + ...original, + getSettings: vi.fn(), + updateSettings: vi.fn(), + }; +}); + +import { getSettings, updateSettings } from "../../../../lib/localDb"; + +// Helper to create a Request with JSON body +function createPatchRequest(body: unknown) { + return new Request("http://localhost/api/settings", { + method: "PATCH", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(body), + }); +} + +describe("PATCH /api/settings", () => { + beforeEach(() => { + vi.resetAllMocks(); + // Default settings before each test + (getSettings as any).mockResolvedValue({ + debugMode: false, + hiddenSidebarItems: [], + }); + // Mock updateSettings to merge updates into the original + (updateSettings as any).mockImplementation(async (updates: Record) => { + const current = await (getSettings as any)(); + return { ...current, ...updates }; + }); + }); + + it("toggles debugMode via PATCH", async () => { + const req = createPatchRequest({ debugMode: true }); + const res = await PATCH(req as any); + expect(res.status).toBe(200); + const json = await res.json(); + expect(json.debugMode).toBe(true); + // Ensure password is not leaked + expect(json).not.toHaveProperty("password"); + // Verify DB update called with correct payload + expect(updateSettings).toHaveBeenCalledOnce(); + const calledWith = (updateSettings as any).mock.calls[0][0]; + expect(calledWith.debugMode).toBe(true); + }); + + it("updates hiddenSidebarItems via PATCH", async () => { + const req = createPatchRequest({ hiddenSidebarItems: [] }); + const res = await PATCH(req as any); + expect(res.status).toBe(200); + const json = await res.json(); + expect(json.hiddenSidebarItems).toEqual([]); + expect(updateSettings).toHaveBeenCalledOnce(); + const calledWith = (updateSettings as any).mock.calls[0][0]; + expect(calledWith.hiddenSidebarItems).toEqual([]); + }); +}); diff --git a/src/i18n/messages/en.json b/src/i18n/messages/en.json index dc9105da85..7aa80d86ec 100644 --- a/src/i18n/messages/en.json +++ b/src/i18n/messages/en.json @@ -1678,6 +1678,8 @@ "darkMode": "Dark Mode", "lightMode": "Light Mode", "systemTheme": "System Theme", + "debugToggle": "Enable Debug Mode", + "sidebarVisibilityToggle": "Show Sidebar Items", "enableCache": "Enable Cache", "cacheTTL": "Cache TTL", "maxCacheSize": "Max Cache Size", diff --git a/src/shared/validation/settingsSchemas.ts b/src/shared/validation/settingsSchemas.ts index 9fea61ae3d..4517b33d5a 100644 --- a/src/shared/validation/settingsSchemas.ts +++ b/src/shared/validation/settingsSchemas.ts @@ -26,6 +26,7 @@ export const updateSettingsSchema = z.object({ requireAuthForModels: z.boolean().optional(), blockedProviders: z.array(z.string().max(100)).optional(), hideHealthCheckLogs: z.boolean().optional(), + debugMode: z.boolean().optional(), hiddenSidebarItems: z.array(z.enum(HIDEABLE_SIDEBAR_ITEM_IDS)).optional(), // Routing settings (#134) fallbackStrategy: z