Compare commits

..

1 Commits

Author SHA1 Message Date
diegosouzapw
896503a0de fix(dashboard): un-gate Dev Tools sidebar section from Debug Mode (#14021)
Removed the visibility: "debug" field from the 'devtools' section in
sidebarVisibility/sections.ts. It was the only section using this gate,
so with debugMode defaulting to false, Playground/Translator/Search
Tools were completely absent from both the live Sidebar and Settings ->
Sidebar, with no toggle and no explanation.
2026-09-21 21:35:35 -03:00
7 changed files with 63 additions and 84 deletions

View File

@@ -0,0 +1 @@
- fix(dashboard): un-gate the Dev Tools sidebar section (Playground, Translator, Search Tools) from Debug Mode so it is discoverable by default (#14021)

View File

@@ -1 +0,0 @@
- fix(db): degrade getSettings() to defaults instead of crashing the Home dashboard when the key_value table is corrupted (#14060)

View File

@@ -13,17 +13,7 @@ export const dynamic = "force-dynamic";
export default async function HomePage() {
// Even if getSettings() rejects, getMachineId() runs concurrently, which is acceptable
// as both paths fail-fast on error and avoids the waterfall penalty.
// Defense-in-depth (#14060): getSettings() already degrades to defaults on a corrupted
// key_value table, but a future unguarded read anywhere in its dependency chain should
// not be able to crash this Server Component render again.
const [settings, machineId] = await Promise.all([
getSettings().catch((error: unknown) => {
const message = error instanceof Error ? error.message : String(error);
console.warn(`[Home] Failed to load settings; using defaults: ${message}`);
return { setupComplete: false };
}),
getMachineId(),
]);
const [settings, machineId] = await Promise.all([getSettings(), getMachineId()]);
const isBootstrapped = process.env.OMNIROUTE_BOOTSTRAPPED === "true";
return (
<>

View File

@@ -151,13 +151,7 @@ function applySessionAffinityLegacyFallback(settings: Record<string, unknown>):
export async function getSettings() {
const db = getDbInstance();
let rows: unknown[] = [];
try {
rows = db.prepare("SELECT key, value FROM key_value WHERE namespace = 'settings'").all();
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
console.warn(`[DB] Failed to read settings; using defaults: ${message}`);
}
const rows = db.prepare("SELECT key, value FROM key_value WHERE namespace = 'settings'").all();
const settings: Record<string, unknown> = {
cloudEnabled: true,
tailscaleEnabled: false,

View File

@@ -851,7 +851,6 @@ export const SIDEBAR_SECTIONS: readonly SidebarSectionDefinition[] = [
titleKey: "devtoolsSection",
titleFallback: "Dev Tools",
children: DEVTOOLS_ITEMS,
visibility: "debug",
},
{
id: "agentic-features",

View File

@@ -0,0 +1,60 @@
// Repro for issue #14021: Playground/Translator/Search Tools (the "Dev Tools" sidebar
// group) are gated behind Debug Mode, but nothing in the UI says so, and Settings →
// Sidebar applies the exact same debug filter — so with debug off there is no toggle for
// Playground at all and no explanation. This test exercises the SAME filter predicate
// both Sidebar.tsx (:277) and SidebarTab.tsx (:470) apply to `SIDEBAR_SECTIONS`, using the
// real section/item config, and proves that with debugMode=false the "playground" item is
// completely absent from what either surface would render — matching the issue's
// acceptance criterion ("with debug off, Settings -> Sidebar either lists Playground or
// explains why it cannot be toggled").
import test from "node:test";
import assert from "node:assert/strict";
const sidebarVisibility = await import("../../src/shared/constants/sidebarVisibility.ts");
function visibleItemIdsWithDebug(showDebug: boolean): string[] {
// This is exactly the predicate used in:
// src/shared/components/Sidebar.tsx:277
// src/app/(dashboard)/dashboard/settings/components/SidebarTab.tsx:470
const visibleSections = sidebarVisibility.SIDEBAR_SECTIONS.filter(
(section) => section.visibility !== "debug" || showDebug
);
const ids: string[] = [];
for (const section of visibleSections) {
for (const item of sidebarVisibility.getSectionItems(section)) {
ids.push(item.id);
}
}
return ids;
}
test("issue #14021: devtools section is not debug-gated in config", () => {
const devtools = sidebarVisibility.SIDEBAR_SECTIONS.find((s) => s.id === "devtools");
assert.ok(devtools, "expected a 'devtools' sidebar section to exist");
assert.notEqual(
devtools!.visibility,
"debug",
"the devtools section must not be gated behind debugMode, per fix for #14021"
);
});
test("issue #14021: with debugMode=false, Playground is discoverable in both the Sidebar and Settings->Sidebar", () => {
const idsDebugOff = visibleItemIdsWithDebug(false);
const idsDebugOn = visibleItemIdsWithDebug(true);
// Sanity: Playground DOES exist and IS reachable once debug is on (proves it's not a
// typo/missing-id issue).
assert.ok(
idsDebugOn.includes("playground"),
"expected 'playground' to be a real, resolvable sidebar item when debugMode=true"
);
// The fix: playground is a normal hideable item
// (HIDEABLE_SIDEBAR_ITEM_IDS includes "playground" — sidebarVisibility/types.ts:79) and
// now appears with debug off too, satisfying the issue's acceptance criterion.
assert.ok(
idsDebugOff.includes("playground"),
"FIX #14021: with debugMode=false, 'playground' item should be discoverable from " +
"every sidebar-derived surface (main Sidebar AND Settings->Sidebar)."
);
});

View File

@@ -1,64 +0,0 @@
import test from "node:test";
import assert from "node:assert/strict";
import fs from "node:fs";
import os from "node:os";
import path from "node:path";
const TEST_DATA_DIR = fs.mkdtempSync(path.join(os.tmpdir(), "omniroute-issue-14060-"));
process.env.DATA_DIR = TEST_DATA_DIR;
const core = await import("../../src/lib/db/core.ts");
const settingsDb = await import("../../src/lib/db/settings.ts");
test.after(() => {
core.resetDbInstance();
fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true, maxRetries: 5, retryDelay: 100 });
});
test("getSettings() degrades to defaults (does not throw) when only the key_value table's page is corrupted", async () => {
const db = core.getDbInstance();
const dbPath = (db as { name?: string }).name;
assert.ok(typeof dbPath === "string" && dbPath.length > 0, "expected a file-backed db path");
db.prepare(
"INSERT INTO key_value (namespace, key, value) VALUES ('settings', 'requireLogin', 'true')"
).run();
const pageSize = (db.pragma("page_size") as Array<{ page_size: number }>)[0].page_size;
const rootPageRow = db
.prepare("SELECT rootpage FROM sqlite_master WHERE type = 'table' AND name = 'key_value'")
.get() as { rootpage: number } | undefined;
assert.ok(rootPageRow?.rootpage, "expected to find key_value's rootpage in sqlite_master");
const rootPage = rootPageRow!.rootpage;
core.resetDbInstance();
const buf = fs.readFileSync(dbPath as string);
const pageStart = (rootPage - 1) * pageSize;
for (let i = pageStart; i < Math.min(buf.length, pageStart + pageSize); i++) {
buf[i] = 0xff;
}
fs.writeFileSync(dbPath as string, buf);
const reopened = core.getDbInstance();
const bootHealthy = !!reopened
.prepare(
"SELECT name FROM sqlite_master WHERE type = 'table' AND name = 'provider_connections'"
)
.get();
assert.equal(bootHealthy, true, "expected the boot probe's own tables to remain readable");
core.resetDbInstance();
const settings = await settingsDb.getSettings();
assert.equal(
typeof settings,
"object",
"expected getSettings() to degrade to a defaults object instead of throwing"
);
assert.equal(
settings.requireLogin,
true,
"expected the built-in default for requireLogin (row could not be read from the corrupted table)"
);
});