feat(settings): add debug toggle and sidebar visibility toggle

feat(ui): replace hide-sidebar toggle with dynamic visibility toggle
This commit is contained in:
oyi77
2026-03-30 15:15:02 +07:00
parent 370070f489
commit 35f96d4a40
5 changed files with 112 additions and 5 deletions

View File

@@ -221,9 +221,7 @@ export default function AppearanceTab() {
<div className="pt-4 border-t border-border">
<div className="mb-3">
<p className="font-medium">
{getSettingsLabel("sidebarVisibility", "Hide sidebar items")}
</p>
<p className="font-medium">{t("sidebarVisibilityToggle")}</p>
<p className="text-sm text-text-muted">
{getSettingsLabel(
"sidebarVisibilityDesc",
@@ -249,7 +247,7 @@ export default function AppearanceTab() {
>
<p className="font-medium">{item.label}</p>
<Toggle
checked={hiddenSidebarSet.has(item.id)}
checked={!hiddenSidebarSet.has(item.id)}
onChange={() => toggleSidebarItem(item.id)}
disabled={loading}
/>

View File

@@ -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 (
<>
<div className="flex flex-col gap-6">
@@ -78,6 +108,18 @@ export default function ProxyTab() {
</Card>
<ProxyRegistryManager />
<Card className="p-6 mt-4">
<div className="flex items-center justify-between">
<div>
<p className="font-medium">{t("debugToggle")}</p>
</div>
<Toggle
checked={debugMode}
onChange={() => updateDebugMode(!debugMode)}
disabled={loading}
/>
</div>
</Card>
</div>
<ProxyConfigModal

View File

@@ -0,0 +1,64 @@
import { describe, it, expect, vi, beforeEach } from "vitest";
import { PATCH } from "../route";
// Mock the localDb functions used in the route
vi.mock("../../../../lib/localDb", () => {
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<string, unknown>) => {
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([]);
});
});

View File

@@ -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",

View File

@@ -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