diff --git a/apps/web/components/app-sidebar/sections/settings/settings-menu-sections.ts b/apps/web/components/app-sidebar/sections/settings/settings-menu-sections.ts index f3e9aee50c..daf850ae69 100644 --- a/apps/web/components/app-sidebar/sections/settings/settings-menu-sections.ts +++ b/apps/web/components/app-sidebar/sections/settings/settings-menu-sections.ts @@ -37,6 +37,7 @@ import { SECRETS_SETTINGS_HREF, SYSTEM_ABOUT_SETTINGS_HREF, SYSTEM_DATA_STORAGE_SETTINGS_HREF, + SYSTEM_STORAGE_SETTINGS_HREF, SYSTEM_STATUS_SETTINGS_HREF, TASK_BEHAVIOR_SETTINGS_HREF, TERMINAL_EDITORS_SETTINGS_HREF, @@ -197,6 +198,11 @@ export const SETTINGS_MENU_SECTIONS: SettingsMenuSection[] = [ labelKey: "system:navDataStorage", icon: IconDatabase, }, + { + href: SYSTEM_STORAGE_SETTINGS_HREF, + labelKey: "system:storageTitle", + icon: IconDatabase, + }, { href: "/settings/system/feature-toggles", labelKey: "system:navFeatureToggles", diff --git a/apps/web/components/app-sidebar/sections/settings/settings-nav-copy.test.ts b/apps/web/components/app-sidebar/sections/settings/settings-nav-copy.test.ts index eacf1392b2..5954ca7920 100644 --- a/apps/web/components/app-sidebar/sections/settings/settings-nav-copy.test.ts +++ b/apps/web/components/app-sidebar/sections/settings/settings-nav-copy.test.ts @@ -23,6 +23,7 @@ const NAV_LABELS: Array<[key: string, english: string]> = [ ["settings:terminalAndEditors", "Terminal & Editors"], ["settings:taskBehavior", "Task Behavior"], ["system:navDataStorage", "Data & Logs"], + ["system:storageTitle", "Storage"], ["common:prompts", "Prompts"], ["settings:utilityAgents", "Utility Agents"], ["settings:globalSecrets", "Global Secrets"], diff --git a/apps/web/components/app-sidebar/sections/settings/settings-tree.test.ts b/apps/web/components/app-sidebar/sections/settings/settings-tree.test.ts index 7b631d2e08..91487a0111 100644 --- a/apps/web/components/app-sidebar/sections/settings/settings-tree.test.ts +++ b/apps/web/components/app-sidebar/sections/settings/settings-tree.test.ts @@ -102,4 +102,18 @@ describe("settings menu shape", () => { // And the row set is unique: one page, one row. expect(new Set(hrefs).size).toBe(hrefs.length); }); + + it("exposes Data & Logs and Storage as direct System destinations", () => { + const system = SETTINGS_MENU_SECTIONS.find((section) => section.id === "system"); + + expect(system?.items.map((item) => item.href)).toEqual([ + "/settings/system/status", + "/settings/system/data-storage", + "/settings/system/storage", + "/settings/system/feature-toggles", + "/settings/system/updates", + "/settings/system/about", + ]); + expect(system?.items[2]?.labelKey).toBe("system:storageTitle"); + }); }); diff --git a/apps/web/components/settings/settings-breadcrumb-labels.ts b/apps/web/components/settings/settings-breadcrumb-labels.ts index 6354bd96fc..54138b98ad 100644 --- a/apps/web/components/settings/settings-breadcrumb-labels.ts +++ b/apps/web/components/settings/settings-breadcrumb-labels.ts @@ -75,6 +75,7 @@ const SEGMENT_LABEL_KEYS: Record = { secrets: "settings:secrets", security: "settings:security", status: "common:status", + storage: "system:storageTitle", system: "common:system", "task-behavior": "settings:taskBehavior", "terminal-editors": "settings:terminalAndEditors", diff --git a/apps/web/components/settings/system/data-storage-settings.tsx b/apps/web/components/settings/system/data-logs-settings.tsx similarity index 81% rename from apps/web/components/settings/system/data-storage-settings.tsx rename to apps/web/components/settings/system/data-logs-settings.tsx index df2bd16056..93ff76a4c4 100644 --- a/apps/web/components/settings/system/data-storage-settings.tsx +++ b/apps/web/components/settings/system/data-logs-settings.tsx @@ -7,7 +7,6 @@ import { SettingsTarget } from "@/components/settings/settings-target"; import { BackupsTable } from "@/components/settings/system/backups-table"; import { DatabaseStatsCard } from "@/components/settings/system/database-stats-card"; import { LogViewer } from "@/components/settings/system/log-viewer"; -import { StorageMaintenanceSettings } from "@/components/settings/system/storage/storage-maintenance-settings"; import { BACKUP_SQL_COMMAND } from "@/components/settings/system/system-route-shell"; import { SYSTEM_SETTINGS_TARGETS } from "@/lib/settings-discovery/catalog/system"; @@ -22,8 +21,8 @@ function SectionHeading({ title, description }: { title: string; description?: s ); } -/** Data & storage: the former Database, Backups, Storage and Logs pages as one page. */ -export function DataStorageSettings() { +/** Data & Logs contains database status, backups, and diagnostic logs. */ +export function DataLogsSettings() { const { t } = useTranslation(); const backupDirectory = useAppStore((s) => s.system.database?.backup_directory); return ( @@ -51,14 +50,6 @@ export function DataStorageSettings() { -
- - -
- section.id === "system")?.items ?? []; +const PSEUDO_ACCENTED = /[À-ɏ]/; afterEach(cleanup); @@ -118,9 +119,25 @@ describe("System menu labels", () => { it("renders every System row label through the catalog", () => { const labels = SYSTEM_MENU_ITEMS.map((item) => t(item.labelKey)); expect(labels).toEqual( - expect.arrayContaining(["Status", "Data & Logs", "Feature Toggles", "Updates", "About"]), + expect.arrayContaining([ + "Status", + "Data & Logs", + "Storage", + "Feature Toggles", + "Updates", + "About", + ]), ); }); + + it("looks up the Storage title through the pseudo-locale", async () => { + await activateLocale("pseudo"); + try { + expect(t("system:storageTitle")).toMatch(PSEUDO_ACCENTED); + } finally { + await activateLocale("en"); + } + }); }); /** @@ -131,8 +148,6 @@ describe("System menu labels", () => { * clean by lint, so this is the check that keeps them migrated. */ describe("copy the guard cannot see, under the pseudo-locale", () => { - const ACCENTED = /[À-ɏ]/; - beforeAll(async () => { await activateLocale("pseudo"); }); @@ -146,7 +161,7 @@ describe("copy the guard cannot see, under the pseudo-locale", () => { { id: "j1", kind: "vacuum", state, started_at: "2026-08-03T10:00:00Z", message: "" }, ]; render(); - expect(screen.getByTestId("system-job-vacuum").textContent).toMatch(ACCENTED); + expect(screen.getByTestId("system-job-vacuum").textContent).toMatch(PSEUDO_ACCENTED); cleanup(); } }); @@ -167,13 +182,13 @@ describe("copy the guard cannot see, under the pseudo-locale", () => { expect(description).toContain("VACUUM INTO"); expect(description).toContain(path); // The surrounding sentence is still translated. - expect(description).toMatch(ACCENTED); + expect(description).toMatch(PSEUDO_ACCENTED); }); it("accents every System menu label", () => { expect(SYSTEM_MENU_ITEMS.length).toBeGreaterThan(0); for (const item of SYSTEM_MENU_ITEMS) { - expect(t(item.labelKey)).toMatch(ACCENTED); + expect(t(item.labelKey)).toMatch(PSEUDO_ACCENTED); } }); }); diff --git a/apps/web/components/settings/system/system-route-copy.test.ts b/apps/web/components/settings/system/system-route-copy.test.ts index 32b97a740a..669405ac22 100644 --- a/apps/web/components/settings/system/system-route-copy.test.ts +++ b/apps/web/components/settings/system/system-route-copy.test.ts @@ -2,7 +2,7 @@ import { cleanup, render, screen } from "@testing-library/react"; import { createElement, type ReactNode } from "react"; import { afterEach, describe, expect, it, vi } from "vitest"; import { t } from "@/lib/i18n"; -import { DataStorageSettings } from "./data-storage-settings"; +import { DataLogsSettings } from "./data-logs-settings"; import { BACKUP_SQL_COMMAND } from "./system-route-shell"; const databaseState = vi.hoisted(() => ({ value: null as unknown })); @@ -17,10 +17,6 @@ vi.mock("@/components/settings/settings-target", () => ({ vi.mock("./backups-table", () => ({ BackupsTable: () => null })); vi.mock("./database-stats-card", () => ({ DatabaseStatsCard: () => null })); vi.mock("./log-viewer", () => ({ LogViewer: () => null })); -vi.mock("./storage/storage-maintenance-settings", () => ({ - StorageMaintenanceSettings: () => null, -})); - afterEach(() => { cleanup(); databaseState.value = null; @@ -63,7 +59,7 @@ const ROUTE_COPY: Array<{ route: string; titleKey: string; title: string; descri description: "Database driver, size, and available maintenance controls.", }, { - // /settings/system/logs now redirects into Data & Storage, whose Logs + // /settings/system/logs now redirects into Data & Logs, whose Logs // section titles itself with `system:navLogs`. The description key is // unchanged, so the sentence users read is still pinned below. route: "logs", @@ -121,18 +117,18 @@ describe("System route headers keep their pre-migration English", () => { }); }); -describe("Data & Storage backup location copy", () => { +describe("Data & Logs backup location copy", () => { it("renders the resolved SQLite backup directory", () => { const path = "/var/lib/kandev/backups"; databaseState.value = { backup_directory: path }; - render(createElement(DataStorageSettings)); + render(createElement(DataLogsSettings)); expect(screen.getByText(`VACUUM INTO snapshots stored under ${path}.`)).toBeTruthy(); }); it("omits the location when database information is unavailable", () => { - render(createElement(DataStorageSettings)); + render(createElement(DataLogsSettings)); expect(screen.queryByText(/VACUUM INTO snapshots stored under/)).toBeNull(); }); @@ -140,12 +136,29 @@ describe("Data & Storage backup location copy", () => { it("omits the location when the backend has no backup directory", () => { databaseState.value = { backup_directory: "" }; - render(createElement(DataStorageSettings)); + render(createElement(DataLogsSettings)); expect(screen.queryByText(/VACUUM INTO snapshots stored under/)).toBeNull(); }); }); +describe("Data & Logs composition", () => { + it("keeps database, backups, and logs without the Storage section", () => { + render(createElement(DataLogsSettings)); + + expect(screen.getByText(t("system:navDatabase"))).toBeTruthy(); + expect(screen.getByText(t("system:navBackups"))).toBeTruthy(); + expect(screen.getByText(t("system:navLogs"))).toBeTruthy(); + expect(screen.queryByText(t("system:storageTitle"))).toBeNull(); + }); + + it("describes only database statistics, backups, and server logs", () => { + expect(t("system:dataStoragePageDescription")).toBe( + "Database statistics, backups, and server logs.", + ); + }); +}); + /** "Feature Toggles" -> "featureToggles", matching the catalog key suffix. */ function camelRoute(title: string): string { const [first, ...rest] = title.split(" "); diff --git a/apps/web/e2e/tests/auth/data-storage-gating-helpers.ts b/apps/web/e2e/tests/auth/data-storage-gating-helpers.ts index 1c972eca1d..29fd016248 100644 --- a/apps/web/e2e/tests/auth/data-storage-gating-helpers.ts +++ b/apps/web/e2e/tests/auth/data-storage-gating-helpers.ts @@ -1,7 +1,7 @@ import { expect, type APIRequestContext, type BrowserContext } from "@playwright/test"; /** - * Shared setup and API probes for the Data & storage member-gating specs + * Shared setup and API probes for the Data & Logs and Storage member-gating specs * (system-data-storage-member-gating.spec.ts desktop and its mobile-* twin). * * The backend mounts system backups and storage maintenance on two route @@ -24,6 +24,7 @@ export const GATING_MEMBER = { }; export const DATA_STORAGE_ROUTE = "/settings/system/data-storage"; +export const STORAGE_ROUTE = "/settings/system/storage"; /** Creates the member account from an authenticated admin context. */ export async function createMember(adminContext: BrowserContext, baseUrl: string): Promise { diff --git a/apps/web/e2e/tests/auth/mobile-system-data-storage-member-gating.spec.ts b/apps/web/e2e/tests/auth/mobile-system-data-storage-member-gating.spec.ts index 69ddf975f3..221c363a6c 100644 --- a/apps/web/e2e/tests/auth/mobile-system-data-storage-member-gating.spec.ts +++ b/apps/web/e2e/tests/auth/mobile-system-data-storage-member-gating.spec.ts @@ -9,24 +9,20 @@ import { expectMemberApiGating, GATING_ADMIN, GATING_MEMBER, + STORAGE_ROUTE, } from "./data-storage-gating-helpers"; /** - * Mobile parity for the Data & storage member gate. The gating adds no new - * composition: it removes controls from the backups table and disables the - * storage actions inside surfaces that already have shipped phone layouts - * (see tests/system/mobile-storage-maintenance.spec.ts). What is - * viewport-specific is the consequence: dropping the backups Actions column - * changes the table's column count, and a table is the surface most likely to - * push a phone into horizontal scroll. This spec proves the member view is - * complete, inert, and contained at Pixel 5 width. + * Mobile parity for the Data & Logs and Storage member gates. The split keeps + * backups on Data & Logs and disables storage actions on Storage. This spec + * proves both owning pages are complete, inert, and contained at Pixel 5 width. * * Runs in the `mobile-chrome` project (routed away from the desktop `auth` * project via its testIgnore) and restarts the worker backend with auth on * and its own database, mirroring system-data-storage-member-gating.spec.ts. * afterAll restarts to the fixture baseline. */ -test.describe.serial("Data & storage member gating (mobile)", () => { +test.describe.serial("Data & Logs and Storage member gating (mobile)", () => { let snapshotName = ""; test.beforeAll(async ({ backend, browser }) => { @@ -75,6 +71,7 @@ test.describe.serial("Data & storage member gating (mobile)", () => { await expect(page.getByTestId("system-backups-restore")).toHaveCount(0); await expect(page.getByTestId("system-backups-delete")).toHaveCount(0); + await page.goto(STORAGE_ROUTE); const analyze = page.getByTestId("storage-analyze"); await expect(analyze).toBeDisabled(); await expect(page.getByTestId("storage-run-now")).toBeDisabled(); @@ -98,7 +95,10 @@ test.describe.serial("Data & storage member gating (mobile)", () => { await ctx.close(); }); - test("an admin still gets every control on a phone", async ({ browser, backend }) => { + test("an admin still gets every control across both pages on a phone", async ({ + browser, + backend, + }) => { const ctx = await browser.newContext({ ...devices["Pixel 5"], baseURL: backend.frontendUrl, @@ -125,6 +125,7 @@ test.describe.serial("Data & storage member gating (mobile)", () => { expect(box!.height).toBeGreaterThanOrEqual(44); } await expect(page.getByTestId("system-backups-admin-only")).toHaveCount(0); + await page.goto(STORAGE_ROUTE); await expect(page.getByTestId("storage-analyze")).toBeEnabled(); await expect diff --git a/apps/web/e2e/tests/auth/system-data-storage-member-gating.spec.ts b/apps/web/e2e/tests/auth/system-data-storage-member-gating.spec.ts index 79d525f618..ee0da1c366 100644 --- a/apps/web/e2e/tests/auth/system-data-storage-member-gating.spec.ts +++ b/apps/web/e2e/tests/auth/system-data-storage-member-gating.spec.ts @@ -9,20 +9,21 @@ import { expectMemberApiGating, GATING_ADMIN, GATING_MEMBER, + STORAGE_ROUTE, } from "./data-storage-gating-helpers"; /** * System backups and storage maintenance act on the whole install: a snapshot * is a byte copy of the multi-user database, and a cleanup pass rewrites - * shared state. Both are admin only on the backend, so Settings > System > - * Data & storage must not offer a member controls that can only answer 403. + * shared state. Both are admin only on the backend, so their owning pages must + * not offer a member controls that can only answer 403. * * Runs in the `auth` project (backend restarted with auth required). Serial: * the two tests share the admin, member, and snapshot created in beforeAll. * A per-file database keeps the single-shot auth setup from colliding with * the other auth specs sharing the worker; afterAll restarts to baseline. */ -test.describe.serial("Data & storage member gating", () => { +test.describe.serial("Data & Logs and Storage member gating", () => { let snapshotName = ""; test.beforeAll(async ({ backend, browser }) => { @@ -68,6 +69,10 @@ test.describe.serial("Data & storage member gating", () => { await expect(page.getByTestId("system-backups-restore")).toHaveCount(0); await expect(page.getByTestId("system-backups-delete")).toHaveCount(0); + await expect(page.getByTestId("storage-analyze")).toHaveCount(0); + await expect(page.getByTestId("storage-run-now")).toHaveCount(0); + + await page.goto(STORAGE_ROUTE); // Storage keeps its controls visible but inert, with the reason attached. await expect(page.getByTestId("storage-analyze")).toBeDisabled(); await expect(page.getByTestId("storage-run-now")).toBeDisabled(); @@ -75,7 +80,10 @@ test.describe.serial("Data & storage member gating", () => { await ctx.close(); }); - test("an admin still gets every control on the same page", async ({ browser, backend }) => { + test("an admin still gets every control across both owning pages", async ({ + browser, + backend, + }) => { const ctx = await browser.newContext({ baseURL: backend.frontendUrl }); await login(ctx, backend.baseUrl, GATING_ADMIN); @@ -87,6 +95,10 @@ test.describe.serial("Data & storage member gating", () => { await expect(page.getByTestId("system-backups-restore").first()).toBeVisible(); await expect(page.getByTestId("system-backups-delete").first()).toBeVisible(); await expect(page.getByTestId("system-backups-admin-only")).toHaveCount(0); + await expect(page.getByTestId("storage-analyze")).toHaveCount(0); + await expect(page.getByTestId("storage-run-now")).toHaveCount(0); + + await page.goto(STORAGE_ROUTE); await expect(page.getByTestId("storage-analyze")).toBeEnabled(); await expect(page.getByTestId("storage-run-now")).toBeEnabled(); diff --git a/apps/web/e2e/tests/docker/storage-maintenance.spec.ts b/apps/web/e2e/tests/docker/storage-maintenance.spec.ts index 4bb0b48847..e2b54cd69e 100644 --- a/apps/web/e2e/tests/docker/storage-maintenance.spec.ts +++ b/apps/web/e2e/tests/docker/storage-maintenance.spec.ts @@ -31,7 +31,7 @@ async function openStorageSettings(page: Page): Promise { // document request once if the first load is interrupted under CI load. for (let attempt = 0; attempt < 2; attempt += 1) { try { - await page.goto("/settings/system/data-storage", { + await page.goto("/settings/system/storage", { waitUntil: "commit", timeout: 20_000, }); diff --git a/apps/web/e2e/tests/system/mobile-storage-maintenance.spec.ts b/apps/web/e2e/tests/system/mobile-storage-maintenance.spec.ts index 4f74218b85..805c74eb4d 100644 --- a/apps/web/e2e/tests/system/mobile-storage-maintenance.spec.ts +++ b/apps/web/e2e/tests/system/mobile-storage-maintenance.spec.ts @@ -35,7 +35,7 @@ test.describe("Mobile storage maintenance", () => { }); }); - await testPage.goto("/settings/system/data-storage"); + await testPage.goto("/settings/system/storage"); const trigger = testPage.getByTestId("storage-resource-temporary-artifacts-trigger"); await trigger.tap(); const cleanButton = testPage.getByTestId("storage-temporary-artifacts-clean"); @@ -86,7 +86,7 @@ test.describe("Mobile storage maintenance", () => { }); }); - await testPage.goto("/settings/system/data-storage"); + await testPage.goto("/settings/system/storage"); await testPage.getByTestId("storage-run-now").tap(); await expect(testPage.getByTestId("storage-busy")).toContainText("A test command is running"); await expect(testPage.getByTestId("storage-run-anyway")).toBeVisible(); @@ -119,7 +119,7 @@ test.describe("Mobile storage maintenance", () => { await mobile.mobileMenuButton.click(); await testPage.getByRole("link", { name: "Settings" }).click(); const index = testPage.getByTestId("settings-index"); - await index.locator('a[href="/settings/system/data-storage"]').click(); + await index.locator('a[href="/settings/system/storage"]').click(); await expect(testPage.getByTestId("storage-settings-page")).toBeVisible(); await expect(testPage.getByTestId("storage-disk-capacity-card")).toBeVisible(); @@ -215,7 +215,7 @@ test.describe("Mobile storage maintenance", () => { await testPage.route(overviewPattern, holdOverview); try { - await testPage.goto("/settings/system/data-storage"); + await testPage.goto("/settings/system/storage"); await overviewObserved; const spinner = testPage.getByTestId("storage-overview-spinner"); @@ -280,7 +280,7 @@ test.describe("Mobile storage maintenance", () => { body: JSON.stringify({ job_id: "mobile-force-purge" }), }); }); - await testPage.goto("/settings/system/data-storage"); + await testPage.goto("/settings/system/storage"); await expect(testPage.getByTestId("storage-quarantine-force-clear")).toBeVisible(); await testPage.getByTestId("storage-quarantine-card").scrollIntoViewIfNeeded(); await prCapture.screenshot("quarantine-actions", { diff --git a/apps/web/e2e/tests/system/sidebar-navigation.spec.ts b/apps/web/e2e/tests/system/sidebar-navigation.spec.ts index d2d255a097..f69654bdd9 100644 --- a/apps/web/e2e/tests/system/sidebar-navigation.spec.ts +++ b/apps/web/e2e/tests/system/sidebar-navigation.spec.ts @@ -3,6 +3,7 @@ import { test, expect } from "../../fixtures/test-base"; const SYSTEM_ENTRIES: Array<{ href: string; label: string; title: string }> = [ { href: "/settings/system/status", label: "Status", title: "Status" }, { href: "/settings/system/data-storage", label: "Data & Logs", title: "Data & Logs" }, + { href: "/settings/system/storage", label: "Storage", title: "Storage" }, { href: "/settings/system/feature-toggles", label: "Feature Toggles", title: "Feature Toggles" }, { href: "/settings/system/updates", label: "Updates", title: "Updates" }, { href: "/settings/system/about", label: "About", title: "About" }, @@ -26,12 +27,11 @@ test.describe("System sidebar navigation", () => { } // Standalone Changelog entry is NOT present, and neither are the merged - // pages' old rows (Database, Backups, Storage, Logs, Licenses). + // pages' old rows (Database, Backups, Logs, Licenses). await expect(testPage.locator('a[href="/settings/changelog"]')).toHaveCount(0); for (const gone of [ "/settings/system/database", "/settings/system/backups", - "/settings/system/storage", "/settings/system/logs", "/settings/system/licenses", ]) { diff --git a/apps/web/e2e/tests/system/storage-maintenance.spec.ts b/apps/web/e2e/tests/system/storage-maintenance.spec.ts index a89c12f534..6f122126eb 100644 --- a/apps/web/e2e/tests/system/storage-maintenance.spec.ts +++ b/apps/web/e2e/tests/system/storage-maintenance.spec.ts @@ -55,7 +55,7 @@ test.describe("System storage maintenance", () => { }); }); - await testPage.goto("/settings/system/data-storage"); + await testPage.goto("/settings/system/storage"); const trigger = testPage.getByTestId("storage-resource-temporary-artifacts-trigger"); await expect(trigger).toContainText("0.05 GB"); await trigger.click(); @@ -86,7 +86,7 @@ test.describe("System storage maintenance", () => { const overviewResponse = testPage.waitForResponse( (response) => new URL(response.url()).pathname === "/api/v1/system/storage", ); - await testPage.goto("/settings/system/data-storage"); + await testPage.goto("/settings/system/storage"); await overviewResponse; // The preceding temporary-artifact scenario loads and caches a Storage // snapshot. Force a fresh read after seeding this test's cache so this @@ -130,7 +130,7 @@ test.describe("System storage maintenance", () => { }); test("reuses the cached snapshot until Analyze refreshes it", async ({ testPage }) => { - await testPage.goto("/settings/system/data-storage"); + await testPage.goto("/settings/system/storage"); const analyzedTime = testPage.locator("time[datetime]").filter({ hasText: "Last analyzed" }); await expect(analyzedTime).toHaveText(/^Last analyzed .+/); const initialAnalyzedAt = await analyzedTime.getAttribute("datetime"); @@ -199,7 +199,7 @@ test.describe("System storage maintenance", () => { await testPage.route(overviewPattern, holdOverview); try { - await testPage.goto("/settings/system/data-storage"); + await testPage.goto("/settings/system/storage"); await overviewObserved; await expect(testPage.getByTestId("storage-policy-card")).toBeVisible(); @@ -234,7 +234,7 @@ test.describe("System storage maintenance", () => { backend, }) => { const orphan = seedOrphanWorkspace(backend.tmpDir); - await testPage.goto("/settings/system/data-storage"); + await testPage.goto("/settings/system/storage"); const overviewBox = await testPage.getByTestId("storage-overview-card").boundingBox(); const policyBox = await testPage.getByTestId("storage-policy-card").boundingBox(); expect(overviewBox).not.toBeNull(); @@ -344,7 +344,7 @@ test.describe("System storage maintenance", () => { ) .toBe("RUNNING"); - await testPage.goto("/settings/system/data-storage"); + await testPage.goto("/settings/system/storage"); const responsePromise = testPage.waitForResponse( (response) => response.request().method() === "POST" && @@ -435,7 +435,7 @@ test.describe("System storage maintenance", () => { body: JSON.stringify({ job_id: "eligible-purge" }), }); }); - await testPage.goto("/settings/system/data-storage"); + await testPage.goto("/settings/system/storage"); await expect( testPage.getByTestId("storage-quarantine-eligible-entry").getByText("Eligible now"), ).toBeVisible(); diff --git a/apps/web/e2e/tests/task/unarchive-storage-recovery.spec.ts b/apps/web/e2e/tests/task/unarchive-storage-recovery.spec.ts index 466bc6d892..ab29114ffa 100644 --- a/apps/web/e2e/tests/task/unarchive-storage-recovery.spec.ts +++ b/apps/web/e2e/tests/task/unarchive-storage-recovery.spec.ts @@ -30,7 +30,7 @@ test("unarchive restores a quarantined workspace before branch recovery", async const old = new Date(Date.now() - 8 * 24 * 60 * 60 * 1000); fs.utimesSync(root, old, old); - await testPage.goto("/settings/system/data-storage"); + await testPage.goto("/settings/system/storage"); const runNow = testPage.getByTestId("storage-run-now"); await expect(runNow).toBeEnabled({ timeout: 30_000 }); await runNow.click(); diff --git a/apps/web/lib/settings-discovery/catalog.test.ts b/apps/web/lib/settings-discovery/catalog.test.ts index 76415009c3..78e625d281 100644 --- a/apps/web/lib/settings-discovery/catalog.test.ts +++ b/apps/web/lib/settings-discovery/catalog.test.ts @@ -7,6 +7,14 @@ import { SETTINGS_DISCOVERY_DEFINITIONS, SETTINGS_DISCOVERY_ROUTE_EXCLUSIONS } f import { resolveSettingsDiscovery } from "./resolve"; const GITHUB_CONNECTION_ID = "integration-github-connection"; +const STORAGE_DISCOVERY_IDS = [ + "system-storage-actions", + "system-storage-schedule", + "system-storage-workspaces", + "system-storage-go-cache", + "system-storage-docker", + "system-storage-quarantine", +] as const; const STABLE_CONTROL_IDS = [ "appearance-color-theme", "appearance-rich-output-motion", @@ -133,6 +141,28 @@ describe("settings discovery catalog invariants", () => { }); }); +describe("system discovery page ownership", () => { + it("assigns storage sections to the standalone Storage page", () => { + const byId = new Map(SETTINGS_DISCOVERY_DEFINITIONS.map((entry) => [entry.id, entry])); + + expect(byId.get("system-storage")).toMatchObject({ + kind: "page", + labelKey: "system:storageTitle", + href: "/settings/system/storage", + order: 625, + }); + for (const id of STORAGE_DISCOVERY_IDS) { + expect(byId.get(id)).toMatchObject({ + parentId: "system-storage", + href: "/settings/system/storage", + }); + } + expect(byId.get("system-database")?.parentId).toBe("system-data-storage"); + expect(byId.get("system-backups")?.parentId).toBe("system-data-storage"); + expect(byId.get("system-logs")?.parentId).toBe("system-data-storage"); + }); +}); + describe("resolveSettingsDiscovery dynamic entries", () => { it("uses the capitalized Settings navigation labels for workspace pages", () => { const resolved = resolveSettingsDiscovery({ diff --git a/apps/web/lib/settings-discovery/catalog/index.ts b/apps/web/lib/settings-discovery/catalog/index.ts index 05f35b1613..3076e4640a 100644 --- a/apps/web/lib/settings-discovery/catalog/index.ts +++ b/apps/web/lib/settings-discovery/catalog/index.ts @@ -23,7 +23,7 @@ export const SETTINGS_DISCOVERY_DEFINITIONS = [ // never rendered. const TO_APPEARANCE = "Redirects to the canonical Appearance page."; // i18n-exempt: maintainer-facing exclusion note; read by tests only. -const TO_DATA_STORAGE = "Redirects to the canonical Data & storage page."; +const TO_DATA_LOGS = "Redirects to the canonical Data & Logs page."; // i18n-exempt: maintainer-facing exclusion note; read by tests only. const TO_KEYBOARD_SHORTCUTS = "Redirects to the canonical Keyboard Shortcuts page."; // i18n-exempt: maintainer-facing exclusion note; read by tests only. @@ -64,10 +64,9 @@ export const SETTINGS_DISCOVERY_ROUTE_EXCLUSIONS: Record = { "/settings/integrations/slack": TO_WORKSPACE_INTEGRATIONS, "/settings/executor/new": "Transient executor creation flow, not a stable setting.", "/settings/system": "Redirects to the canonical System Status page.", - "/settings/system/database": TO_DATA_STORAGE, - "/settings/system/backups": TO_DATA_STORAGE, - "/settings/system/storage": TO_DATA_STORAGE, - "/settings/system/logs": TO_DATA_STORAGE, + "/settings/system/database": TO_DATA_LOGS, + "/settings/system/backups": TO_DATA_LOGS, + "/settings/system/logs": TO_DATA_LOGS, "/settings/system/licenses": "Redirects to the canonical About page.", "/settings/system/message-queue": TO_TASK_BEHAVIOR, "/settings/changelog": "Redirects to the canonical System Updates page.", diff --git a/apps/web/lib/settings-discovery/catalog/system.ts b/apps/web/lib/settings-discovery/catalog/system.ts index 86db4016a0..0f800277fa 100644 --- a/apps/web/lib/settings-discovery/catalog/system.ts +++ b/apps/web/lib/settings-discovery/catalog/system.ts @@ -3,7 +3,9 @@ import type { SettingsDiscoveryDefinition } from "../types"; export const SYSTEM_SETTINGS_HREF = "/settings/system"; export const SYSTEM_STATUS_SETTINGS_HREF = `${SYSTEM_SETTINGS_HREF}/status`; const SYSTEM_DATA_STORAGE_DISCOVERY_ID = "system-data-storage"; +const SYSTEM_STORAGE_DISCOVERY_ID = "system-storage"; export const SYSTEM_DATA_STORAGE_SETTINGS_HREF = `${SYSTEM_SETTINGS_HREF}/data-storage`; +export const SYSTEM_STORAGE_SETTINGS_HREF = `${SYSTEM_SETTINGS_HREF}/storage`; export const SYSTEM_ABOUT_SETTINGS_HREF = `${SYSTEM_SETTINGS_HREF}/about`; export const SYSTEM_SETTINGS_TARGETS = { database: "setting-system-database", @@ -67,66 +69,6 @@ export const SYSTEM_DISCOVERY_DEFINITIONS: SettingsDiscoveryDefinition[] = [ targetId: SYSTEM_SETTINGS_TARGETS.backups, order: 622, }, - { - id: "system-storage-actions", - kind: "section", - labelKey: "system:storageActionsTitle", - parentId: SYSTEM_DATA_STORAGE_DISCOVERY_ID, - groupId: "system", - href: SYSTEM_DATA_STORAGE_SETTINGS_HREF, - targetId: SYSTEM_SETTINGS_TARGETS.storageActions, - order: 623, - }, - { - id: "system-storage-schedule", - kind: "section", - labelKey: "system:storageScheduleTitle", - parentId: SYSTEM_DATA_STORAGE_DISCOVERY_ID, - groupId: "system", - href: SYSTEM_DATA_STORAGE_SETTINGS_HREF, - targetId: SYSTEM_SETTINGS_TARGETS.storageSchedule, - order: 624, - }, - { - id: "system-storage-workspaces", - kind: "section", - labelKey: "system:storageWorkspacesTitle", - parentId: SYSTEM_DATA_STORAGE_DISCOVERY_ID, - groupId: "system", - href: SYSTEM_DATA_STORAGE_SETTINGS_HREF, - targetId: SYSTEM_SETTINGS_TARGETS.storageWorkspaces, - order: 625, - }, - { - id: "system-storage-go-cache", - kind: "section", - labelKey: "system:storageGoBuildCache", - parentId: SYSTEM_DATA_STORAGE_DISCOVERY_ID, - groupId: "system", - href: SYSTEM_DATA_STORAGE_SETTINGS_HREF, - targetId: SYSTEM_SETTINGS_TARGETS.storageGoCache, - order: 626, - }, - { - id: "system-storage-docker", - kind: "section", - labelKey: "system:storageDockerTitle", - parentId: SYSTEM_DATA_STORAGE_DISCOVERY_ID, - groupId: "system", - href: SYSTEM_DATA_STORAGE_SETTINGS_HREF, - targetId: SYSTEM_SETTINGS_TARGETS.storageDocker, - order: 627, - }, - { - id: "system-storage-quarantine", - kind: "section", - labelKey: "system:storageQuarantineSafetyTitle", - parentId: SYSTEM_DATA_STORAGE_DISCOVERY_ID, - groupId: "system", - href: SYSTEM_DATA_STORAGE_SETTINGS_HREF, - targetId: SYSTEM_SETTINGS_TARGETS.storageQuarantine, - order: 628, - }, { id: "system-logs", kind: "section", @@ -135,15 +77,83 @@ export const SYSTEM_DISCOVERY_DEFINITIONS: SettingsDiscoveryDefinition[] = [ groupId: "system", href: SYSTEM_DATA_STORAGE_SETTINGS_HREF, targetId: SYSTEM_SETTINGS_TARGETS.logs, + order: 623, + }, + { + id: "system-storage", + kind: "page", + labelKey: "system:storageTitle", + groupId: "system", + href: SYSTEM_STORAGE_SETTINGS_HREF, + order: 625, + }, + { + id: "system-storage-actions", + kind: "section", + labelKey: "system:storageActionsTitle", + parentId: SYSTEM_STORAGE_DISCOVERY_ID, + groupId: "system", + href: SYSTEM_STORAGE_SETTINGS_HREF, + targetId: SYSTEM_SETTINGS_TARGETS.storageActions, + order: 626, + }, + { + id: "system-storage-schedule", + kind: "section", + labelKey: "system:storageScheduleTitle", + parentId: SYSTEM_STORAGE_DISCOVERY_ID, + groupId: "system", + href: SYSTEM_STORAGE_SETTINGS_HREF, + targetId: SYSTEM_SETTINGS_TARGETS.storageSchedule, + order: 627, + }, + { + id: "system-storage-workspaces", + kind: "section", + labelKey: "system:storageWorkspacesTitle", + parentId: SYSTEM_STORAGE_DISCOVERY_ID, + groupId: "system", + href: SYSTEM_STORAGE_SETTINGS_HREF, + targetId: SYSTEM_SETTINGS_TARGETS.storageWorkspaces, + order: 628, + }, + { + id: "system-storage-go-cache", + kind: "section", + labelKey: "system:storageGoBuildCache", + parentId: SYSTEM_STORAGE_DISCOVERY_ID, + groupId: "system", + href: SYSTEM_STORAGE_SETTINGS_HREF, + targetId: SYSTEM_SETTINGS_TARGETS.storageGoCache, order: 629, }, + { + id: "system-storage-docker", + kind: "section", + labelKey: "system:storageDockerTitle", + parentId: SYSTEM_STORAGE_DISCOVERY_ID, + groupId: "system", + href: SYSTEM_STORAGE_SETTINGS_HREF, + targetId: SYSTEM_SETTINGS_TARGETS.storageDocker, + order: 630, + }, + { + id: "system-storage-quarantine", + kind: "section", + labelKey: "system:storageQuarantineSafetyTitle", + parentId: SYSTEM_STORAGE_DISCOVERY_ID, + groupId: "system", + href: SYSTEM_STORAGE_SETTINGS_HREF, + targetId: SYSTEM_SETTINGS_TARGETS.storageQuarantine, + order: 631, + }, { id: "system-feature-toggles", kind: "page", labelKey: "system:navFeatureToggles", groupId: "system", href: "/settings/system/feature-toggles", - order: 630, + order: 640, }, { id: "system-updates", @@ -151,7 +161,7 @@ export const SYSTEM_DISCOVERY_DEFINITIONS: SettingsDiscoveryDefinition[] = [ labelKey: "system:navUpdates", groupId: "system", href: "/settings/system/updates", - order: 640, + order: 650, }, { id: "system-about", @@ -159,7 +169,7 @@ export const SYSTEM_DISCOVERY_DEFINITIONS: SettingsDiscoveryDefinition[] = [ labelKey: "system:navAbout", groupId: "system", href: SYSTEM_ABOUT_SETTINGS_HREF, - order: 650, + order: 660, }, { id: "system-licenses", @@ -169,7 +179,7 @@ export const SYSTEM_DISCOVERY_DEFINITIONS: SettingsDiscoveryDefinition[] = [ groupId: "system", href: SYSTEM_ABOUT_SETTINGS_HREF, targetId: SYSTEM_SETTINGS_TARGETS.licenses, - order: 651, + order: 661, }, { id: "system-users", @@ -177,7 +187,7 @@ export const SYSTEM_DISCOVERY_DEFINITIONS: SettingsDiscoveryDefinition[] = [ labelKey: "system:navUsers", groupId: "access", href: "/settings/system/users", - order: 660, + order: 670, requires: "users", }, ]; diff --git a/apps/web/src/locales/en/system.json b/apps/web/src/locales/en/system.json index 26e85c3b7d..aafb0bc014 100644 --- a/apps/web/src/locales/en/system.json +++ b/apps/web/src/locales/en/system.json @@ -58,7 +58,7 @@ "databaseOptimize": "Optimize", "databaseOptimizeHelp": "Asks the database to refresh its internal indexes so common queries stay fast. Run it after deleting a lot of data or if the app feels sluggish. Quick and safe - no data is changed.", "databasePageDescription": "Database driver, size, and available maintenance controls.", - "dataStoragePageDescription": "Database statistics, backups, disk cleanup, and server logs.", + "dataStoragePageDescription": "Database statistics, backups, and server logs.", "databasePath": "Path", "databaseRunOptimize": "Run optimize", "databaseRunVacuum": "Run vacuum", diff --git a/apps/web/src/locales/pseudo/system.json b/apps/web/src/locales/pseudo/system.json index 604f7a6698..becfc35c23 100644 --- a/apps/web/src/locales/pseudo/system.json +++ b/apps/web/src/locales/pseudo/system.json @@ -58,7 +58,7 @@ "databaseOptimize": "Ōƥţĩḿĩźē", "databaseOptimizeHelp": "Àśķś ţĥē ďàţàƀàśē ţō ŕēƒŕēśĥ ĩţś ĩńţēŕńàĺ ĩńďēxēś śō ćōḿḿōń qũēŕĩēś śţàŷ ƒàśţ. Ŕũń ĩţ àƒţēŕ ďēĺēţĩńĝ à ĺōţ ōƒ ďàţà ōŕ ĩƒ ţĥē àƥƥ ƒēēĺś śĺũĝĝĩśĥ. Qũĩćķ àńď śàƒē - ńō ďàţà ĩś ćĥàńĝēď.", "databasePageDescription": "Ďàţàƀàśē ďŕĩvēŕ, śĩźē, àńď àvàĩĺàƀĺē ḿàĩńţēńàńćē ćōńţŕōĺś.", - "dataStoragePageDescription": "Ďàţàƀàśē śţàţĩśţĩćś, ƀàćķũƥś, ďĩśķ ćĺēàńũƥ, àńď śēŕvēŕ ĺōĝś.", + "dataStoragePageDescription": "Ďàţàƀàśē śţàţĩśţĩćś, ƀàćķũƥś, àńď śēŕvēŕ ĺōĝś.", "databasePath": "Ƥàţĥ", "databaseRunOptimize": "Ŕũń ōƥţĩḿĩźē", "databaseRunVacuum": "Ŕũń vàćũũḿ", diff --git a/apps/web/src/locales/pt-pt/system.json b/apps/web/src/locales/pt-pt/system.json index 34e018b251..b7ff315629 100644 --- a/apps/web/src/locales/pt-pt/system.json +++ b/apps/web/src/locales/pt-pt/system.json @@ -513,7 +513,7 @@ "versionLatest": "Mais recente", "versionTitle": "Versão", "versionViewUpdates": "Ver as atualizações", - "dataStoragePageDescription": "Estatísticas da base de dados, cópias de segurança, limpeza de disco e registos do servidor.", + "dataStoragePageDescription": "Estatísticas da base de dados, cópias de segurança e registos do servidor.", "messageQueueAutoMergeDescription": "Junta mensagens consecutivas compatíveis em fila da mesma origem num único turno antes da entrega.", "messageQueueAutoMergeNotice": "Se uma mensagem tiver outra origem ou tarefa, modelo, modo, metadados ou limites partilhados incompatíveis, permanece como mensagem em fila separada. Isto aplica-se apenas a novas admissões e não altera o controlo de junção manual.", "messageQueueAutoMergeToggleLabel": "Juntar automaticamente mensagens consecutivas", diff --git a/apps/web/src/locales/zh-cn/system.json b/apps/web/src/locales/zh-cn/system.json index 36618c5d53..bc1de2c606 100644 --- a/apps/web/src/locales/zh-cn/system.json +++ b/apps/web/src/locales/zh-cn/system.json @@ -58,7 +58,7 @@ "databaseOptimize": "优化", "databaseOptimizeHelp": "让数据库刷新内部索引,使常见查询保持快速。大量删除数据后或应用感觉变慢时运行。快速且安全;不更改任何数据。", "databasePageDescription": "数据库驱动、大小与可用维护控件。", - "dataStoragePageDescription": "数据库统计、备份、磁盘清理和服务器日志。", + "dataStoragePageDescription": "数据库统计、备份和服务器日志。", "databasePath": "路径", "databaseRunOptimize": "运行优化", "databaseRunVacuum": "运行 vacuum", diff --git a/apps/web/src/locales/zh-hk/system.json b/apps/web/src/locales/zh-hk/system.json index e0aab1fbdd..e23fead512 100644 --- a/apps/web/src/locales/zh-hk/system.json +++ b/apps/web/src/locales/zh-hk/system.json @@ -58,7 +58,7 @@ "databaseOptimize": "優化", "databaseOptimizeHelp": "讓數據庫刷新內部索引,使常見查詢保持快速。大量刪除數據後或應用感覺變慢時執行。快速且安全;不更改任何數據。", "databasePageDescription": "數據庫驅動、大小與可用維護控制項。", - "dataStoragePageDescription": "數據庫統計、備份、磁碟清理和伺服器日誌。", + "dataStoragePageDescription": "數據庫統計、備份和伺服器日誌。", "databasePath": "路徑", "databaseRunOptimize": "執行優化", "databaseRunVacuum": "執行 vacuum", diff --git a/apps/web/src/locales/zh-tw/system.json b/apps/web/src/locales/zh-tw/system.json index 9019e354b8..cc76f398dc 100644 --- a/apps/web/src/locales/zh-tw/system.json +++ b/apps/web/src/locales/zh-tw/system.json @@ -58,7 +58,7 @@ "databaseOptimize": "最佳化", "databaseOptimizeHelp": "讓資料庫重新整理內部索引,使常見查詢保持快速。大量刪除資料後或應用感覺變慢時執行。快速且安全;不更改任何資料。", "databasePageDescription": "資料庫驅動、大小與可用維護控制項。", - "dataStoragePageDescription": "資料庫統計、備份、磁碟清理和伺服器日誌。", + "dataStoragePageDescription": "資料庫統計、備份和伺服器日誌。", "databasePath": "路徑", "databaseRunOptimize": "執行最佳化", "databaseRunVacuum": "執行 vacuum", diff --git a/apps/web/src/settings-routes.test.ts b/apps/web/src/settings-routes.test.ts index af4aae0cb4..0394f7f86e 100644 --- a/apps/web/src/settings-routes.test.ts +++ b/apps/web/src/settings-routes.test.ts @@ -18,6 +18,8 @@ import { } from "@/components/settings/settings-breadcrumbs"; import { TaskBehaviorSettings } from "@/components/settings/task-behavior-settings"; import { LegacyExecutorSettingsRoute } from "@/components/settings/legacy-executor-settings-route"; +import { StorageMaintenanceSettings } from "@/components/settings/system/storage/storage-maintenance-settings"; +import { SystemRouteShell } from "@/components/settings/system/system-route-shell"; import { WorkspaceSettingsShell } from "@/components/settings/workspaces/workspace-settings-shell"; import { SETTINGS_DISCOVERY_ROUTE_EXCLUSIONS } from "@/lib/settings-discovery/catalog"; import { workspaceId, workflowId } from "@/lib/types/ids"; @@ -267,6 +269,46 @@ describe("renderSettingsRoute", () => { }); }); +describe("system data and storage routes", () => { + it("renders Storage maintenance on its direct route", () => { + const route = renderSettingsRoute("/settings/system/storage") as ReactElement<{ + titleKey: string; + descriptionKey: string; + children: ReactElement; + }>; + + expect(isValidElement(route)).toBe(true); + expect(route.type).toBe(SystemRouteShell); + expect(route.props.titleKey).toBe("system:storageTitle"); + expect(route.props.descriptionKey).toBe("system:storageDescription"); + expect(route.props.children.type).toBe(StorageMaintenanceSettings); + }); +}); + +describe("system page breadcrumbs", () => { + const noRecords: CrumbValues = { + workspaceName: () => null, + agentDisplayName: () => null, + agentProfileName: () => null, + automationName: () => null, + executorName: () => null, + executorProfileName: () => null, + executorTypeTitle: () => null, + integrationTitle: () => null, + pluginName: () => null, + }; + + it("resolves Storage from its translated catalog label", () => { + expect(resolveSettingsBreadcrumbs("/settings/system/storage", (key) => key, noRecords)).toEqual( + { + parents: [{ label: "common:settings", href: "/settings", phoneOnlyLink: true }], + title: "system:storageTitle", + titleFromUrlSegment: false, + }, + ); + }); +}); + // Split from the block above to stay inside the per-function line limit: this // half is only about decoding `%20`-style segments into component props. describe("renderSettingsRoute identifier decoding", () => { diff --git a/apps/web/src/settings-routes.tsx b/apps/web/src/settings-routes.tsx index a30e7daae6..6f0705ca86 100644 --- a/apps/web/src/settings-routes.tsx +++ b/apps/web/src/settings-routes.tsx @@ -41,12 +41,13 @@ import { AboutSettings } from "@/components/settings/system/about-settings"; import { ApiTokens } from "@/components/settings/account/api-tokens"; import { SecuritySettings } from "@/components/settings/account/security-settings"; import { UsersTable } from "@/components/settings/system/users-table"; -import { DataStorageSettings } from "@/components/settings/system/data-storage-settings"; +import { DataLogsSettings } from "@/components/settings/system/data-logs-settings"; import { DiskUsageCard } from "@/components/settings/system/disk-usage-card"; import { FeatureTogglesRoute } from "@/components/settings/system/feature-toggles-route"; import { HealthIssuesCard } from "@/components/settings/system/health-issues-card"; import { SystemPageShell } from "@/components/settings/system/system-page-shell"; import { SystemRouteShell } from "@/components/settings/system/system-route-shell"; +import { StorageMaintenanceSettings } from "@/components/settings/system/storage/storage-maintenance-settings"; import { UIStateCard } from "@/components/settings/system/ui-state-card"; import { UpdatesCard } from "@/components/settings/system/updates-card"; import { VersionSummaryCard } from "@/components/settings/system/version-summary-card"; @@ -200,7 +201,7 @@ const SETTINGS_ROUTES: Record = { titleKey="system:navDataStorage" descriptionKey="system:dataStoragePageDescription" > - + ), "/settings/system/backups": () => , @@ -226,7 +227,11 @@ const SETTINGS_ROUTES: Record = { ), - "/settings/system/storage": () => , + "/settings/system/storage": () => ( + + + + ), "/settings/system/updates": renderUpdatesRoute, "/settings/changelog": () => , }; diff --git a/docs/audits/settings-typography/003-normalize-card-and-section-headings.md b/docs/audits/settings-typography/003-normalize-card-and-section-headings.md index 1b8af6d8f9..12eeb16c1a 100644 --- a/docs/audits/settings-typography/003-normalize-card-and-section-headings.md +++ b/docs/audits/settings-typography/003-normalize-card-and-section-headings.md @@ -17,7 +17,7 @@ Equivalent settings cards do not share a predictable title role. Some use the ba - A source inventory found 52 explicit `text-base` settings `CardTitle` usages mixed with plain default usages, including `apps/web/components/settings/profile-edit/profile-details-card.tsx:24-29` and `apps/web/components/settings/ssh-sessions-card.tsx:63-65`. - `apps/web/components/settings/profile-edit/profile-details-card.tsx:27` uses the default `CardTitle` size for the same kind of settings card. - `apps/web/app/settings/agents/[agentId]/profile-mcp-config-card.tsx:429` also uses the default title size, while `apps/web/components/settings/system/about-card.tsx:39` uses `text-base`. -- `apps/web/components/settings/settings-section.tsx:45` and `apps/web/components/settings/system/data-storage-settings.tsx:13` use `text-lg font-semibold` for section headings. +- `apps/web/components/settings/settings-section.tsx:45` and `apps/web/components/settings/system/data-logs-settings.tsx:13` use `text-lg font-semibold` for section headings. - `apps/web/app/settings/executors/page.tsx:106-115` and `:143-152` render executor card titles as un-sized paragraphs, so they inherit the card body's `text-xs/relaxed`; their descriptions are also `text-xs`. - `apps/web/components/settings/plugins/marketplace-entry-row.tsx:27-43` leaves the marketplace entry name at the surrounding Plugins Browse `TabsContent` inherited `text-xs/relaxed` (`apps/packages/ui/src/tabs.tsx:70-75`) while explicitly rendering its description at `text-sm`, inverting the expected title-to-description hierarchy. - `apps/web/components/settings/executor-profiles-card.tsx:65-79`, `apps/web/app/settings/agents/[agentId]/agent-setup-parts.tsx:225-237`, and profile-edit cards such as `env-vars-card.tsx:376-389` place title/description groups beside small action buttons without a shared `min-w-0` responsive header. diff --git a/docs/decisions/2026-09-03-separate-system-data-storage-pages.md b/docs/decisions/2026-09-03-separate-system-data-storage-pages.md new file mode 100644 index 0000000000..ed1e2ada3d --- /dev/null +++ b/docs/decisions/2026-09-03-separate-system-data-storage-pages.md @@ -0,0 +1,52 @@ +# ADR-2026-09-03-separate-system-data-storage-pages: Separate System Data and Storage Pages + +**Status:** accepted +**Date:** 2026-09-03 +**Area:** frontend + +## Context + +The System `Data & Logs` route combines database status, backups, storage +maintenance, and diagnostic logs. These sections support different operator +tasks. + +Storage maintenance is a large workflow. It includes disk capacity, cleanup +actions, policy settings, run history, and quarantine management. On a phone, +the combined route places diagnostic logs many screens below the page entry. + +The settings restructure reduced four System rows to one row. Restoring every +old row would make the System navigation dense again. + +## Decision + +System settings will expose two direct page destinations: + +- `Data & Logs` at `/settings/system/data-storage` will contain database + status, backups, and diagnostic logs. +- `Storage` at `/settings/system/storage` will contain the complete storage + maintenance workflow. + +The existing `Data & Logs` path remains stable. The legacy Database, Backups, +and Logs paths will continue to redirect to that page. + +Desktop and phone navigation will show both destinations directly. The split +will not add tabs, drawers, or another navigation level. + +## Consequences + +- Operators can open storage maintenance without moving through unrelated + database and backup content. +- Diagnostic logs are no longer placed after the large storage workflow. +- The System navigation gains one row. +- Settings discovery must assign each storage target to the Storage page. +- The Storage route owns its save contributor and dirty-route protection. +- Public operations screenshots must show the new Storage destination. + +## Alternatives Considered + +- **Keep one combined page.** Rejected because the page combines separate + tasks and produces excessive phone scrolling. +- **Restore four standalone pages.** Rejected because Database, Backups, and + Logs remain small enough to share one operational data page. +- **Put Logs with Storage.** Rejected because the storage workflow would still + place diagnostics after its longest content. diff --git a/docs/decisions/INDEX.md b/docs/decisions/INDEX.md index 51a9e4daaf..153c75a821 100644 --- a/docs/decisions/INDEX.md +++ b/docs/decisions/INDEX.md @@ -236,3 +236,4 @@ Read individual ADRs for full context. Create new ones via `/record decision` or | 2026-09-01-server-owned-saved-prompt-expansion | [Keep Saved-Prompt Expansion Server-Owned](2026-09-01-server-owned-saved-prompt-expansion.md) | accepted | backend, frontend, protocol, security | 2026-09-01 | | 2026-09-01-passthrough-initial-prompt-turn-boundary | [Keep Passthrough Initial Prompt State in Lifecycle](2026-09-01-passthrough-initial-prompt-turn-boundary.md) | accepted | backend, workflow | 2026-09-01 | | 2026-09-02-automation-self-archive | [Permit Automation Self-Archive as Terminal Completion](2026-09-02-automation-self-archive.md) | accepted | backend, agentctl, protocol, security, workflow | 2026-09-02 | +| 2026-09-03-separate-system-data-storage-pages | [Separate System Data and Storage Pages](2026-09-03-separate-system-data-storage-pages.md) | accepted | frontend | 2026-09-03 | diff --git a/docs/plans/backup-location-actions/plan.md b/docs/plans/backup-location-actions/plan.md index fd30433933..58ffbcd357 100644 --- a/docs/plans/backup-location-actions/plan.md +++ b/docs/plans/backup-location-actions/plan.md @@ -45,7 +45,7 @@ Return the absolute `backups` sibling path for SQLite, resolving the derived path with `filepath.Abs`, and an empty value for PostgreSQL. Add `backup_directory` to `DatabaseStats` in `apps/web/lib/types/system.ts`. -Use the shared database state in `data-storage-settings.tsx` for the Backups description. +Use the shared database state in `data-logs-settings.tsx` for the Backups description. Remove the static `BACKUP_DIR` value from `system-route-shell.tsx`. Keep `BACKUP_SQL_COMMAND` as a stable interpolated command value. diff --git a/docs/plans/backup-location-actions/task-01-resolved-backup-directory.md b/docs/plans/backup-location-actions/task-01-resolved-backup-directory.md index bc6ff7be0d..53e7b3573a 100644 --- a/docs/plans/backup-location-actions/task-01-resolved-backup-directory.md +++ b/docs/plans/backup-location-actions/task-01-resolved-backup-directory.md @@ -63,7 +63,7 @@ Run the final two commands from the repository root. - `apps/backend/internal/system/database/stats.go` - `apps/backend/internal/system/database/stats_test.go` - `apps/web/lib/types/system.ts` -- `apps/web/components/settings/system/data-storage-settings.tsx` +- `apps/web/components/settings/system/data-logs-settings.tsx` - `apps/web/components/settings/system/system-route-shell.tsx` - `apps/web/components/settings/system/system-route-copy.test.ts` - `apps/web/components/settings/system/system-invisible-copy.test.tsx` diff --git a/docs/plans/system-data-storage-page-split/plan.md b/docs/plans/system-data-storage-page-split/plan.md new file mode 100644 index 0000000000..6c98d32b1d --- /dev/null +++ b/docs/plans/system-data-storage-page-split/plan.md @@ -0,0 +1,125 @@ +--- +created: 2026-09-03 +status: implemented +requirements: + - REQ-SYSTEM-PAGE-DATA-STORAGE-PAGES-001 +system_design: + - ../../specs/system-page/system-design/system-data-storage-pages.md +legacy_specs: + - ../../specs/system-page/requirements/system-page.md + - ../../specs/system-page/system-design/system-page-01.md +--- + +# Implementation Plan: System Data and Storage Page Split + +## Overview + +This change creates separate `Data & Logs` and `Storage` pages. The first work +order changes routes, navigation, discovery, copy, and browser coverage. The +second work order updates public operations documentation and screenshots. + +## Scope + +### In scope + +- Keep Database, Backups, and Logs on `/settings/system/data-storage`. +- Render storage maintenance on `/settings/system/storage`. +- Add a direct Storage entry to desktop and phone settings navigation. +- Reassign storage discovery targets to the Storage page. +- Preserve legacy redirects and storage save protection. +- Update all affected locales, tests, operations text, and screenshots. + +### Out of scope + +- Backend, API, database, or storage policy changes. +- New settings navigation levels or responsive primitives. +- Separate Database, Backups, or Logs pages. +- A rename of the existing `Data & Logs` path. + +## Technical approach + +### Route and component composition + +Rename the combined content component to match its remaining Data and Logs +scope. Remove `StorageMaintenanceSettings` and its separator from that +component. + +Replace the Storage redirect in `apps/web/src/settings-routes.tsx` with a +`SystemRouteShell` that renders `StorageMaintenanceSettings`. Reuse the existing +Storage title and description keys. + +### Navigation and discovery + +Add `SYSTEM_STORAGE_SETTINGS_HREF` to the System discovery catalog. Add a +Storage page definition and reparent every storage section to it. Keep existing +storage target IDs stable. + +Add the Storage row to `SETTINGS_MENU_SECTIONS`. Add the translated breadcrumb +mapping for the `storage` path segment. Update route and catalog unit tests. + +### Localization + +Change `dataStoragePageDescription` so that it names database statistics, +backups, and server logs only. Update every locale. Use `pnpm run i18n:zh-hant` +for the Traditional Chinese catalogs. + +### Compatibility + +Keep the Database, Backups, and Logs redirects unchanged. Keep the current +`Data & Logs` URL and discovery page identity unchanged. + +### Public documentation + +Update `docs/public/operations.md` to point storage tasks to the Storage page. +Recapture the four affected screenshots with isolated disposable data. The +screenshots must show the Storage route and must not expose developer data. + +## Tests + +- `AC-SYSTEM-PAGE-DATA-STORAGE-PAGES-001.1` through `.7`: route, menu, + breadcrumb, discovery, and copy unit tests. +- `AC-SYSTEM-PAGE-DATA-STORAGE-PAGES-001.9`: existing storage policy and role + gating tests after route migration. + +## E2E tests + +- `AC-SYSTEM-PAGE-DATA-STORAGE-PAGES-001.1` through `.5`: desktop System + navigation and the Database, Backups, Logs, and Storage page specifications. +- `AC-SYSTEM-PAGE-DATA-STORAGE-PAGES-001.7` through `.9`: mobile System + navigation, storage maintenance, and member gating specifications. +- `AC-SYSTEM-PAGE-DATA-STORAGE-PAGES-001.9`: authenticated and container-backed + storage maintenance specifications. + +## Work orders + +- [x] [Task 01: Split the System pages](task-01-split-system-pages.md) +- [x] [Task 02: Update storage operations documentation](task-02-update-storage-operations-docs.md) + +## Verification results + +Task 01 is done. Focused unit tests passed with 107 tests, typecheck passed, +full web lint passed, the i18n check passed, and the desktop, authenticated, and +phone Playwright suites passed with 18, 2, and 10 tests respectively. The +container-backed storage suite could not start because Docker is unavailable in +the environment. + +Task 02 is done. Public documentation tests and validation passed, and the four +Storage screenshots were recaptured from an isolated Playwright fixture with +fictional paths and no developer data. A landing-repository capture target was +not configured, so the documented screenshot fallback used the isolated native +Playwright capture. + +The PR review follow-up added an explicit pseudo-locale assertion for the +Storage title and clarified the immediate command wording in the system design. +The focused remediation test passed. + +## Risks + +- Tests that use `/settings/system/data-storage` for storage can pass against + the wrong route unless every reference is classified by feature. +- Auth tests currently assert backup and storage controls on one page. They + must exercise both routes in the same role context. +- Existing operations screenshots show the `Data & Logs` breadcrumb. They must + be recaptured after the route split. +- Discovery parent changes can break exact-target navigation if hrefs and + parent identities do not change together. diff --git a/docs/plans/system-data-storage-page-split/task-01-split-system-pages.md b/docs/plans/system-data-storage-page-split/task-01-split-system-pages.md new file mode 100644 index 0000000000..76df995391 --- /dev/null +++ b/docs/plans/system-data-storage-page-split/task-01-split-system-pages.md @@ -0,0 +1,132 @@ +--- +id: "01-split-system-pages" +title: "Split the System pages" +status: done +wave: 1 +depends_on: [] +plan: "plan.md" +requirements: + - REQ-SYSTEM-PAGE-DATA-STORAGE-PAGES-001 +acceptance_criteria: + - AC-SYSTEM-PAGE-DATA-STORAGE-PAGES-001.1 + - AC-SYSTEM-PAGE-DATA-STORAGE-PAGES-001.2 + - AC-SYSTEM-PAGE-DATA-STORAGE-PAGES-001.3 + - AC-SYSTEM-PAGE-DATA-STORAGE-PAGES-001.4 + - AC-SYSTEM-PAGE-DATA-STORAGE-PAGES-001.5 + - AC-SYSTEM-PAGE-DATA-STORAGE-PAGES-001.6 + - AC-SYSTEM-PAGE-DATA-STORAGE-PAGES-001.7 + - AC-SYSTEM-PAGE-DATA-STORAGE-PAGES-001.8 + - AC-SYSTEM-PAGE-DATA-STORAGE-PAGES-001.9 +system_design: + - ../../specs/system-page/system-design/system-data-storage-pages.md +--- + +# Task 01: Split the System Pages + +## Summary + +Create direct `Data & Logs` and `Storage` routes. Update navigation, discovery, +localization, unit tests, and Playwright coverage as one vertical change. + +## In scope + +- Remove storage maintenance from the Data and Logs content component. +- Render storage maintenance directly on `/settings/system/storage`. +- Add the Storage route to the System menu, breadcrumbs, and discovery catalog. +- Update the Data and Logs description in every locale. +- Move every storage-focused Playwright flow to the Storage route. +- Make authenticated role tests exercise both owning pages. +- Preserve legacy redirects and exact discovery targets. + +## Out of scope + +- Backend or API changes. +- Storage policy or cleanup behavior changes. +- Public operations text and screenshots. + +## Acceptance + +- Each route renders only its specified operational sections. +- Desktop and phone navigation open both routes directly. +- Storage save, permission, action, and containment tests pass on the Storage + route. + +## Verification + +```bash +pnpm exec vitest run components/settings/system/system-route-copy.test.ts components/settings/system/system-invisible-copy.test.tsx components/app-sidebar/sections/settings/settings-nav-copy.test.ts components/app-sidebar/sections/settings/settings-tree.test.ts lib/settings-discovery/catalog.test.ts src/settings-routes.test.ts +pnpm run i18n:check +pnpm e2e:run --project chromium tests/system/sidebar-navigation.spec.ts tests/system/database-page.spec.ts tests/system/backups-page.spec.ts tests/system/logs-page.spec.ts tests/system/storage-maintenance.spec.ts +pnpm e2e:run --project auth tests/auth/system-data-storage-member-gating.spec.ts +pnpm e2e:run --project mobile-chrome tests/system/mobile-database-page.spec.ts tests/system/mobile-logs-bundle.spec.ts tests/system/mobile-storage-maintenance.spec.ts tests/auth/mobile-system-data-storage-member-gating.spec.ts +pnpm e2e:run --project containers tests/docker/storage-maintenance.spec.ts +``` + +Run these commands from `apps/web`. + +## Files likely touched + +- `apps/web/components/settings/system/data-logs-settings.tsx` +- `apps/web/components/settings/system/system-route-copy.test.ts` +- `apps/web/components/settings/system/system-invisible-copy.test.tsx` +- `apps/web/components/settings/settings-breadcrumb-labels.ts` +- `apps/web/components/app-sidebar/sections/settings/settings-menu-sections.ts` +- `apps/web/components/app-sidebar/sections/settings/settings-nav-copy.test.ts` +- `apps/web/components/app-sidebar/sections/settings/settings-tree.test.ts` +- `apps/web/lib/settings-discovery/catalog/system.ts` +- `apps/web/lib/settings-discovery/catalog.test.ts` +- `apps/web/src/settings-routes.tsx` +- `apps/web/src/settings-routes.test.ts` +- `apps/web/src/locales/*/system.json` +- `apps/web/e2e/tests/system/*.spec.ts` +- `apps/web/e2e/tests/auth/*data-storage-member-gating*.ts` +- `apps/web/e2e/tests/docker/storage-maintenance.spec.ts` +- `apps/web/e2e/tests/task/unarchive-storage-recovery.spec.ts` + +## Dependencies + +None. + +## Risks + +- A broad route replacement can move database or log tests to Storage by + mistake. Classify each reference before editing it. +- The auth tests use manual browser contexts. Both route visits must retain the + same authenticated context. +- The Storage route must keep the route-scoped save contributor mounted until + the navigation guard resolves. + +## Parallelism + +`sequential` + +## Inputs + +- `REQ-SYSTEM-PAGE-DATA-STORAGE-PAGES-001` and all acceptance criteria. +- `docs/specs/system-page/system-design/system-data-storage-pages.md`. +- ADRs 0045, 0046, and 2026-08-04-navigation-manifest-boundaries. +- Existing System route, discovery, settings menu, and Playwright patterns. + +## Results + +The route composition, System navigation, +breadcrumbs, discovery ownership, localized copy, unit tests, and desktop, +authenticated, phone, and container-backed Playwright references were updated. + +Verification passed: + +- Focused Vitest command: 107 tests passed. +- `pnpm run typecheck` passed. +- Full `pnpm run lint` passed. +- `pnpm run i18n:check` passed. +- Desktop Playwright command: 18 tests passed. +- Authenticated role command: 2 tests passed. +- Phone Playwright command: 10 tests passed. +- `python3 scripts/lint-spec-files.py --all` and `git diff --check` passed. + +The exact container Playwright command was attempted, but Docker is unavailable +in this environment, so that suite could not run. + +PR review follow-up added an explicit pseudo-locale assertion for +`system:storageTitle` and clarified the system-design wording for immediate +commands. The focused remediation test passed with 12 tests. diff --git a/docs/plans/system-data-storage-page-split/task-02-update-storage-operations-docs.md b/docs/plans/system-data-storage-page-split/task-02-update-storage-operations-docs.md new file mode 100644 index 0000000000..58c11bb081 --- /dev/null +++ b/docs/plans/system-data-storage-page-split/task-02-update-storage-operations-docs.md @@ -0,0 +1,100 @@ +--- +id: "02-update-storage-operations-docs" +title: "Update storage operations documentation" +status: done +wave: 2 +depends_on: + - "01-split-system-pages" +plan: "plan.md" +requirements: + - REQ-SYSTEM-PAGE-DATA-STORAGE-PAGES-001 +acceptance_criteria: + - AC-SYSTEM-PAGE-DATA-STORAGE-PAGES-001.1 + - AC-SYSTEM-PAGE-DATA-STORAGE-PAGES-001.4 + - AC-SYSTEM-PAGE-DATA-STORAGE-PAGES-001.7 +system_design: + - ../../specs/system-page/system-design/system-data-storage-pages.md +--- + +# Task 02: Update Storage Operations Documentation + +## Summary + +Update the public storage instructions for the new Storage destination. +Recapture screenshots that show the old `Data & Logs` breadcrumb. + +## In scope + +- Update storage paths and screenshot captions in `docs/public/operations.md`. +- Seed isolated disposable data for the storage examples. +- Recapture the four affected Storage screenshots. +- Check that the images show the Storage route and contain no private data. + +## Out of scope + +- Changes to storage behavior or API documentation. +- New public documentation pages. +- Changes to unrelated screenshots. + +## Acceptance + +- Public instructions direct operators to `Settings > System > Storage`. +- Every affected screenshot shows the Storage destination and safe demo data. +- Public documentation validation passes with no broken links or metadata. + +## Verification + +```bash +node --test scripts/validate-public-docs.test.mjs +node scripts/validate-public-docs.mjs +git diff --check -- docs/public docs/screenshots +``` + +Run these commands from the repository root. + +## Files likely touched + +- `docs/public/operations.md` +- `docs/screenshots/system-storage.png` +- `docs/screenshots/system-maintenance-policy.png` +- `docs/screenshots/system-quarantine.png` +- `docs/screenshots/system-docker-cleanup.png` + +## Dependencies + +- Task 01 must complete before screenshot capture. + +## Risks + +- A screenshot from a developer instance can expose local paths or task names. +- A crop can hide the new route title and fail to show the navigation change. + +## Parallelism + +`sequential` + +## Inputs + +- The implemented Storage route from Task 01. +- The public operations how-to guide. +- The product demo seeding and capture procedures. + +## Results + +Implemented across commits `407e3df65` (the route split) and `97de44c48` +(the public documentation and screenshot refresh). Storage instructions and +captions now point to `Settings > System > Storage`. The four affected +screenshots were recaptured with an isolated Playwright fixture using fictional +`/var/lib/kandev/...` paths, then visually inspected and copied into +`docs/screenshots/`. The maintenance-policy and Docker-cleanup captures were +subsequently reframed so each visibly retains the Storage breadcrumb or page +heading alongside the relevant controls. + +Verification passed: + +- `node --test scripts/validate-public-docs.test.mjs` passed all 61 tests. +- `node scripts/validate-public-docs.mjs` validated 42 published pages. +- `git diff --check -- docs/public docs/screenshots` passed. + +No landing-repository capture target was configured. The capture used the +isolated native Playwright fallback and did not include developer data. diff --git a/docs/public/authentication.md b/docs/public/authentication.md index 1e6d8c9a81..c5bf0c549f 100644 --- a/docs/public/authentication.md +++ b/docs/public/authentication.md @@ -51,7 +51,7 @@ A server that listens on non-loopback interfaces with authentication disabled lo Roles: `admin` (user management, authentication settings, destructive system operations, feature toggles) and `member` (everything else, scoped to their own workspaces). -Install-wide data in `Settings > System > Data & storage` follows the same split. Members can read the database stats, the backup listing, and the storage usage, policy, run history, and quarantine contents. Creating, downloading, restoring, and deleting a backup is admin only, because a backup is a copy of the whole database and downloading one would export every user's workspaces. Changing storage settings, adopting a Go cache, and running an analysis, cleanup, or quarantine restore/purge are admin only for the same reason database vacuum, optimize, and reset are: they act on the whole install. +Install-wide data in `Settings > System > Data & Logs` and `Settings > System > Storage` follows the same split. Members can read the database stats, the backup listing, and the storage usage, policy, run history, and quarantine contents. Creating, downloading, restoring, and deleting a backup is admin only, because a backup is a copy of the whole database and downloading one would export every user's workspaces. Changing storage settings, adopting a Go cache, and running an analysis, cleanup, or quarantine restore/purge are admin only for the same reason database vacuum, optimize, and reset are: they act on the whole install. ## Personal access tokens diff --git a/docs/public/operations.md b/docs/public/operations.md index e4cb7ab6c7..e024c74d17 100644 --- a/docs/public/operations.md +++ b/docs/public/operations.md @@ -137,14 +137,14 @@ The System Database and Backups pages use the configured SQLite file path. They
Storage maintenance details -Open **Settings > System > Data & Logs**, then use the **Storage** section to -inspect Kandev-managed disk usage and configure cleanup. +Open **Settings > System > Storage** to inspect Kandev-managed disk usage and +configure cleanup. **Analyze** is read-only. **Run now** applies only the enabled cleanup rules and refuses to start while another maintenance run owns the cleanup gate. If task resources are active, the page names the active work and offers **Run anyway** after an explicit disruption warning. Use that override only when the active task work can tolerate cleanup running alongside it. -![Settings > System > Data & Logs, Storage section showing disk capacity, storage analysis, and cleanup controls.](../screenshots/system-storage.png) +![Settings > System > Storage showing disk capacity, storage analysis, and cleanup controls.](../screenshots/system-storage.png) Storage analysis results are cached in the running backend for 15 minutes, so page reloads and policy saves reuse the displayed snapshot instead of scanning disk again. The page shows when that @@ -163,7 +163,7 @@ permanent deletion. Each entry shows its `delete_after` retention deadline: **De time, not an exact promise, the first successful scheduled or full manual maintenance run after the deadline performs the purge, subject to the idle gate and any preemption. -![Settings > System > Data & Logs showing the maintenance policy, schedule, workspace cleanup, and folder allowlist.](../screenshots/system-maintenance-policy.png) +![Settings > System > Storage showing the maintenance policy, schedule, workspace cleanup, and folder allowlist.](../screenshots/system-maintenance-policy.png) Use **Clear eligible** to remove only entries whose deadlines have passed. It reports protected entries that remain. **Force clear all** requires typing `DELETE ALL NOW` and attempts to permanently @@ -172,7 +172,7 @@ deleted. Safety-validation or deletion failures may leave entries visible and re override bypasses only the retention timestamp; path, ownership, state, and filesystem safety checks still apply. -![Settings > System > Data & Logs showing quarantined resources with restore, delete, and force-clear controls.](../screenshots/system-quarantine.png) +![Settings > System > Storage showing quarantined resources with restore, delete, and force-clear controls.](../screenshots/system-quarantine.png) Kandev keeps at most one restorable Go-cache generation for each original cache path. If that generation is still active when the replacement cache exceeds its limit, the next rotation is @@ -205,7 +205,7 @@ Host-wide Docker build-cache and unused-image cleanup remain disabled until you owns a dedicated Docker daemon. Do not enable those rules on a daemon shared with unrelated workloads. -![Settings > System > Data & Logs showing Docker cleanup controls, cache retention, unused image cleanup, and quarantine safety.](../screenshots/system-docker-cleanup.png) +![Settings > System > Storage showing Docker cleanup controls, cache retention, unused image cleanup, and quarantine safety.](../screenshots/system-docker-cleanup.png) The Storage page also reports **Kandev temporary artifacts** created by services that need a short-lived directory under the host temporary root. Each current artifact is registered in the diff --git a/docs/screenshots/system-docker-cleanup.png b/docs/screenshots/system-docker-cleanup.png index f188d58a1f..ab6e897560 100644 Binary files a/docs/screenshots/system-docker-cleanup.png and b/docs/screenshots/system-docker-cleanup.png differ diff --git a/docs/screenshots/system-maintenance-policy.png b/docs/screenshots/system-maintenance-policy.png index c44f32df70..f048d7b50e 100644 Binary files a/docs/screenshots/system-maintenance-policy.png and b/docs/screenshots/system-maintenance-policy.png differ diff --git a/docs/screenshots/system-quarantine.png b/docs/screenshots/system-quarantine.png index 50bd4ba8b8..f67894fe50 100644 Binary files a/docs/screenshots/system-quarantine.png and b/docs/screenshots/system-quarantine.png differ diff --git a/docs/screenshots/system-storage.png b/docs/screenshots/system-storage.png index 93d10dee6f..69d43e3be1 100644 Binary files a/docs/screenshots/system-storage.png and b/docs/screenshots/system-storage.png differ diff --git a/docs/specs/system-page/README.md b/docs/specs/system-page/README.md index 1666205048..779e26e61e 100644 --- a/docs/specs/system-page/README.md +++ b/docs/specs/system-page/README.md @@ -32,6 +32,7 @@ feedback. - [Backup location and action guidance](requirements/backup-location-actions.md) +- [System Data and Storage Pages](requirements/system-data-storage-pages.md) - [Storage Maintenance](requirements/storage-maintenance.md) - [Storage maintenance scan, capacity, and dependency cleanup](requirements/storage-overview-parallel-scan.md) - [System pages](requirements/system-page.md) @@ -41,6 +42,7 @@ feedback. - [Backup location and action guidance](system-design/backup-location-actions.md) +- [System Data and Storage Pages](system-design/system-data-storage-pages.md) - [Storage Maintenance System Design Part 1](system-design/storage-maintenance-01.md) - [Storage Maintenance System Design Part 2](system-design/storage-maintenance-02.md) - [Storage Maintenance System Design Part 3](system-design/storage-maintenance-03.md) diff --git a/docs/specs/system-page/requirements/system-data-storage-pages.md b/docs/specs/system-page/requirements/system-data-storage-pages.md new file mode 100644 index 0000000000..07412e7994 --- /dev/null +++ b/docs/specs/system-page/requirements/system-data-storage-pages.md @@ -0,0 +1,65 @@ +--- +status: active +system: system-page +created: 2026-09-03 +owners: + - kandev +--- + +# System Data and Storage Pages Requirements + +## Overview + +The system-page system owns the information architecture for operational data, +storage maintenance, and diagnostics. Operators need focused destinations for +storage work and system data work. + +This requirement supersedes the route allocation for Database, Backups, Logs, +and Storage in `REQ-SYSTEM-PAGE-SYSTEM-PAGE-001`. The operational behavior of +those features remains unchanged. + +## Terminology + +- **Data & Logs:** The page for database status, backups, and diagnostic logs. +- **Storage:** The page for disk analysis, cleanup policy, run history, and + quarantine management. + +## Requirements + +### REQ-SYSTEM-PAGE-DATA-STORAGE-PAGES-001: Focused system data and storage destinations + +**Intent:** Operators can open storage maintenance without moving through +unrelated database, backup, and log content. + +**User story:** As an operator, I want separate data and storage destinations, +so that I can reach the required maintenance task quickly. + +#### Acceptance criteria + +- **AC-SYSTEM-PAGE-DATA-STORAGE-PAGES-001.1:** System settings shall show + `Data & Logs` and `Storage` as separate page destinations. +- **AC-SYSTEM-PAGE-DATA-STORAGE-PAGES-001.2:** `Data & Logs` shall use + `/settings/system/data-storage` and show Database, Backups, and Logs. +- **AC-SYSTEM-PAGE-DATA-STORAGE-PAGES-001.3:** `Data & Logs` shall not show + storage analysis, cleanup, policy, history, or quarantine controls. +- **AC-SYSTEM-PAGE-DATA-STORAGE-PAGES-001.4:** `Storage` shall use + `/settings/system/storage` and show the complete storage maintenance flow. +- **AC-SYSTEM-PAGE-DATA-STORAGE-PAGES-001.5:** Database, Backups, and Logs + legacy routes shall continue to redirect to `Data & Logs`. +- **AC-SYSTEM-PAGE-DATA-STORAGE-PAGES-001.6:** Settings discovery shall open + each database, backup, log, or storage result on its owning page and target. +- **AC-SYSTEM-PAGE-DATA-STORAGE-PAGES-001.7:** Desktop and phone settings + navigation shall provide a direct entry to both pages with no new nested + navigation. +- **AC-SYSTEM-PAGE-DATA-STORAGE-PAGES-001.8:** The phone pages shall retain one + content scroll owner and shall not create document horizontal overflow. +- **AC-SYSTEM-PAGE-DATA-STORAGE-PAGES-001.9:** The route split shall preserve + storage policy save protection, role gates, loading states, error states, + and maintenance actions. + +## Out of scope + +- New backend APIs or storage-maintenance behavior. +- Separate pages for Database, Backups, and Logs. +- A new tab, drawer, or accordion navigation model. +- A new canonical path for `Data & Logs`. diff --git a/docs/specs/system-page/requirements/system-page.md b/docs/specs/system-page/requirements/system-page.md index 79edc58e63..04c336f324 100644 --- a/docs/specs/system-page/requirements/system-page.md +++ b/docs/specs/system-page/requirements/system-page.md @@ -9,6 +9,11 @@ owners: ## Overview +> **Route allocation update (2026-09-03):** +> [System Data and Storage Pages](system-data-storage-pages.md) supersedes the +> Database, Backups, Logs, and Storage route allocation in this migrated source. +> The operational behavior in this document remains authoritative. + Today, settings-style operational concerns (health, disk usage, current version, OSS attribution, log access, database maintenance) have no home in kandev's UI. The existing settings sidebar is product-configuration only (executors, agents, integrations); the only system-shaped entry — Changelog — sits awkwardly inside it. When something goes wrong, users have no path inside the app to see "where is my database, how big is it, what version am I on, is there an update, where are the logs." This forces them into the filesystem and into GitHub, and it makes recoverable problems (bloated SQLite, corrupt state) look unrecoverable. Radarr and Sonarr solve this with a dedicated **System** area: a group of read-only diagnostic pages plus a small number of gated maintenance actions. This spec brings the same shape to kandev, scoped to the kandev-specific surface (database diagnostics, SQLite backups/maintenance, worktrees, GitHub releases, and daily backend log files). diff --git a/docs/specs/system-page/system-design/backup-location-actions.md b/docs/specs/system-page/system-design/backup-location-actions.md index cd2483fdde..851d6acb58 100644 --- a/docs/specs/system-page/system-design/backup-location-actions.md +++ b/docs/specs/system-page/system-design/backup-location-actions.md @@ -33,7 +33,7 @@ the database stats request returns the error. PostgreSQL and other drivers return an empty value because they have no local SQLite snapshot directory. The frontend adds `backup_directory` to `DatabaseStats`. -`DataStorageSettings` reads the shared database state after `DatabaseStatsCard` loads it. +`DataLogsSettings` reads the shared database state after `DatabaseStatsCard` loads it. The Backups description uses the resolved value and keeps `VACUUM INTO` as an interpolated command value. The description stays absent until the database response supplies a directory. @@ -53,8 +53,9 @@ The icon button and its accessible name remain the primary interaction contract. ## Responsive behavior -The existing Data and Storage route remains the mobile entry point. -The Backups table remains the visible action surface on mobile. +The Data & Logs route remains the mobile entry point for the Backups table. +Storage maintenance has its own mobile route and remains the visible action +surface for storage controls. `queued-ghost-row-actions.tsx` is the nearest shipped coarse-pointer action pattern. The row actions use pointer-specific minimum dimensions of 44 pixels without changing desktop density. diff --git a/docs/specs/system-page/system-design/system-data-storage-pages.md b/docs/specs/system-page/system-design/system-data-storage-pages.md new file mode 100644 index 0000000000..6508b6b8c6 --- /dev/null +++ b/docs/specs/system-page/system-design/system-data-storage-pages.md @@ -0,0 +1,120 @@ +--- +status: current +system: system-page +requirements: + - REQ-SYSTEM-PAGE-DATA-STORAGE-PAGES-001 +--- + +# System Data and Storage Pages System Design + +## Purpose and boundaries + +The system-page system owns these routes and their operational content. This +design changes frontend composition and navigation only. Existing backend APIs, +permissions, jobs, and persistence remain unchanged. + +This design supersedes the route allocation for Database, Backups, Logs, and +Storage in `system-page-01.md`. + +## Requirement mapping + +| Requirement | Design section | +| --- | --- | +| `REQ-SYSTEM-PAGE-DATA-STORAGE-PAGES-001` | [Route composition](#route-composition), [Navigation and discovery](#navigation-and-discovery), [Mobile design contract](#mobile-design-contract), [Compatibility](#compatibility) | + +## Route composition + +`apps/web/src/settings-routes.tsx` will own two static route renderers. + +The `/settings/system/data-storage` renderer will use `SystemRouteShell` with +the existing `Data & Logs` title. Its content component will render these +sections in order: + +1. Database. +2. Backups. +3. Logs. + +The `/settings/system/storage` renderer will use `SystemRouteShell` with the +existing Storage title and description. It will render +`StorageMaintenanceSettings` directly. This removes the duplicate Storage +section heading from the new page. + +The route split will not duplicate domain hooks or action handlers. +`StorageMaintenanceSettings` will remain the single owner of storage data, +draft state, and action state. + +## Navigation and discovery + +`apps/web/lib/settings-discovery/catalog/system.ts` will export a stable href +for the Storage page. The System menu will add a direct Storage row after +`Data & Logs`. + +The existing `system-data-storage` discovery page identity and href will remain +stable. Database, Backups, and Logs will remain its children. A new +`system-storage` page identity will own all existing storage targets. + +The breadcrumb label map will resolve the `storage` segment through the same +translation key as the menu and page title. Catalog order will place Storage +after `Data & Logs` and before Feature Toggles. + +## Copy and localization + +The `Data & Logs` page description will no longer mention disk cleanup. The +Storage route will reuse the existing localized Storage title and description. + +All changed copy will remain complete in English, pseudo, Portuguese, Simplified +Chinese, and both Traditional Chinese catalogs. Traditional Chinese updates +will use the repository conversion command. + +## Save lifecycle + +The Storage page will mount the existing `system:storage-policy` save +contributor. The route-scoped settings coordinator will continue to show the +shared save action and navigation guard for dirty storage policy settings. + +The `Data & Logs` page will not mount the storage contributor. Database, +backup, and log commands will continue to execute immediately. + +## Mobile design contract + +The desktop outcome and the phone outcome are the same. Both surfaces provide +direct destinations for system data and storage maintenance. + +The existing Settings index and full-height Settings sheet are the nearest +mobile examples. They provide the route list, touch targets, and direct +navigation behavior. The change does not add another overlay. + +Each destination remains an inline settings page with the existing page scroll +owner. The split reduces content depth and preserves safe-area behavior. It +does not change touch controls inside either page. + +Desktop and phone share route definitions, discovery data, permissions, state, +and action handlers. Playwright coverage will open both destinations from the +correct settings navigation surface. Phone coverage will also check horizontal +containment and storage save behavior. + +## Compatibility + +The canonical `Data & Logs` path remains `/settings/system/data-storage`. +Existing `/settings/system/database`, `/settings/system/backups`, and +`/settings/system/logs` paths will continue to redirect there. + +`/settings/system/storage` will stop redirecting and will become a rendered +page. Saved last-page behavior will accept both canonical routes because both +remain in the static route table. + +Loading and error states remain local to the component that owns each API. +Changing one route does not change API availability or admin checks. + +## Verification + +Unit tests will cover route rendering, navigation labels, breadcrumb labels, +discovery ownership, and localized copy. Playwright tests will cover desktop, +phone, authenticated member, and container-backed storage paths. + +## Related decisions + +- [Separate System Data and Storage Pages](../../../decisions/2026-09-03-separate-system-data-storage-pages.md) +- [Centralize Navigation and Namespace Plugin Destinations](../../../decisions/2026-08-04-navigation-manifest-boundaries.md) +- [Install-wide storage maintenance](../../../decisions/0045-install-wide-storage-maintenance.md) +- [Settings route save coordinator](../../../decisions/0046-settings-route-save-coordinator.md) diff --git a/docs/specs/system-page/system-design/system-page-01.md b/docs/specs/system-page/system-design/system-page-01.md index ad78b6c2f0..de75518051 100644 --- a/docs/specs/system-page/system-design/system-page-01.md +++ b/docs/specs/system-page/system-design/system-page-01.md @@ -11,6 +11,11 @@ owners: ## Purpose and boundaries +> **Route allocation update (2026-09-03):** +> [System Data and Storage Pages](system-data-storage-pages.md) supersedes the +> Database, Backups, Logs, and Storage route allocation in this migrated source. +> The backend and operational design in this document remains authoritative. + This design preserves the technical source detail for `REQ-SYSTEM-PAGE-SYSTEM-PAGE-001` during migration. ## Requirement mapping