-
- {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() {
+
+
+
+
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