feat(settings): split system data and storage pages (#3348)

This commit is contained in:
Carlos Florêncio
2026-09-04 00:37:41 +00:00
committed by GitHub
parent 66d110b509
commit db3a11333e
46 changed files with 899 additions and 150 deletions
@@ -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",
@@ -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"],
@@ -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");
});
});
@@ -75,6 +75,7 @@ const SEGMENT_LABEL_KEYS: Record<string, string> = {
secrets: "settings:secrets",
security: "settings:security",
status: "common:status",
storage: "system:storageTitle",
system: "common:system",
"task-behavior": "settings:taskBehavior",
"terminal-editors": "settings:terminalAndEditors",
@@ -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() {
<BackupsTable />
</SettingsTarget>
<Separator />
<div className="space-y-4">
<SectionHeading
title={t("system:storageTitle")}
description={t("system:storageDescription")}
/>
<StorageMaintenanceSettings />
</div>
<Separator />
<SettingsTarget targetId={SYSTEM_SETTINGS_TARGETS.logs} className="space-y-4">
<SectionHeading
title={t("system:navLogs")}
@@ -8,6 +8,7 @@ import { SETTINGS_MENU_SECTIONS } from "@/components/app-sidebar/sections/settin
const SYSTEM_MENU_ITEMS =
SETTINGS_MENU_SECTIONS.find((section) => 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(<JobProgressIndicator kind="vacuum" />);
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);
}
});
});
@@ -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(" ");
@@ -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<void> {
@@ -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
@@ -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();
@@ -31,7 +31,7 @@ async function openStorageSettings(page: Page): Promise<void> {
// 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,
});
@@ -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", {
@@ -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",
]) {
@@ -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();
@@ -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();
@@ -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({
@@ -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<string, string> = {
"/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.",
@@ -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",
},
];
+1 -1
View File
@@ -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",
+1 -1
View File
@@ -58,7 +58,7 @@
"databaseOptimize": "Ōƥţĩḿĩźē",
"databaseOptimizeHelp": "Àśķś ţĥē ďàţàƀàśē ţō ŕēƒŕēśĥ ĩţś ĩńţēŕńàĺ ĩńďēxēś śō ćōḿḿōń qũēŕĩēś śţàŷ ƒàśţ. Ŕũń ĩţ àƒţēŕ ďēĺēţĩńĝ à ĺōţ ōƒ ďàţà ōŕ ĩƒ ţĥē àƥƥ ƒēēĺś śĺũĝĝĩśĥ. Qũĩćķ àńď śàƒē - ńō ďàţà ĩś ćĥàńĝēď.",
"databasePageDescription": "Ďàţàƀàśē ďŕĩvēŕ, śĩźē, àńď àvàĩĺàƀĺē ḿàĩńţēńàńćē ćōńţŕōĺś.",
"dataStoragePageDescription": "Ďàţàƀàśē śţàţĩśţĩćś, ƀàćķũƥś, ďĩśķ ćĺēàńũƥ, àńď śēŕvēŕ ĺōĝś.",
"dataStoragePageDescription": "Ďàţàƀàśē śţàţĩśţĩćś, ƀàćķũƥś, àńď śēŕvēŕ ĺōĝś.",
"databasePath": "Ƥàţĥ",
"databaseRunOptimize": "Ŕũń ōƥţĩḿĩźē",
"databaseRunVacuum": "Ŕũń vàćũũḿ",
+1 -1
View File
@@ -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",
+1 -1
View File
@@ -58,7 +58,7 @@
"databaseOptimize": "优化",
"databaseOptimizeHelp": "让数据库刷新内部索引,使常见查询保持快速。大量删除数据后或应用感觉变慢时运行。快速且安全;不更改任何数据。",
"databasePageDescription": "数据库驱动、大小与可用维护控件。",
"dataStoragePageDescription": "数据库统计、备份、磁盘清理和服务器日志。",
"dataStoragePageDescription": "数据库统计、备份和服务器日志。",
"databasePath": "路径",
"databaseRunOptimize": "运行优化",
"databaseRunVacuum": "运行 vacuum",
+1 -1
View File
@@ -58,7 +58,7 @@
"databaseOptimize": "優化",
"databaseOptimizeHelp": "讓數據庫刷新內部索引,使常見查詢保持快速。大量刪除數據後或應用感覺變慢時執行。快速且安全;不更改任何數據。",
"databasePageDescription": "數據庫驅動、大小與可用維護控制項。",
"dataStoragePageDescription": "數據庫統計、備份、磁碟清理和伺服器日誌。",
"dataStoragePageDescription": "數據庫統計、備份和伺服器日誌。",
"databasePath": "路徑",
"databaseRunOptimize": "執行優化",
"databaseRunVacuum": "執行 vacuum",
+1 -1
View File
@@ -58,7 +58,7 @@
"databaseOptimize": "最佳化",
"databaseOptimizeHelp": "讓資料庫重新整理內部索引,使常見查詢保持快速。大量刪除資料後或應用感覺變慢時執行。快速且安全;不更改任何資料。",
"databasePageDescription": "資料庫驅動、大小與可用維護控制項。",
"dataStoragePageDescription": "資料庫統計、備份、磁碟清理和伺服器日誌。",
"dataStoragePageDescription": "資料庫統計、備份和伺服器日誌。",
"databasePath": "路徑",
"databaseRunOptimize": "執行最佳化",
"databaseRunVacuum": "執行 vacuum",
+42
View File
@@ -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", () => {
+8 -3
View File
@@ -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<string, RouteRenderer> = {
titleKey="system:navDataStorage"
descriptionKey="system:dataStoragePageDescription"
>
<DataStorageSettings />
<DataLogsSettings />
</SystemRouteShell>
),
"/settings/system/backups": () => <SettingsRedirect to={SYSTEM_DATA_STORAGE_SETTINGS_HREF} />,
@@ -226,7 +227,11 @@ const SETTINGS_ROUTES: Record<string, RouteRenderer> = {
<UIStateCard />
</SystemRouteShell>
),
"/settings/system/storage": () => <SettingsRedirect to={SYSTEM_DATA_STORAGE_SETTINGS_HREF} />,
"/settings/system/storage": () => (
<SystemRouteShell titleKey="system:storageTitle" descriptionKey="system:storageDescription">
<StorageMaintenanceSettings />
</SystemRouteShell>
),
"/settings/system/updates": renderUpdatesRoute,
"/settings/changelog": () => <SettingsRedirect to="/settings/system/updates" />,
};
@@ -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.
@@ -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.
+1
View File
@@ -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 |
+1 -1
View File
@@ -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.
@@ -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`
@@ -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.
@@ -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.
@@ -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.
+1 -1
View File
@@ -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
+6 -6
View File
@@ -137,14 +137,14 @@ The System Database and Backups pages use the configured SQLite file path. They
<details>
<summary>Storage maintenance details</summary>
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
Binary file not shown.

Before

Width:  |  Height:  |  Size: 315 KiB

After

Width:  |  Height:  |  Size: 110 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 344 KiB

After

Width:  |  Height:  |  Size: 82 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 146 KiB

After

Width:  |  Height:  |  Size: 73 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 356 KiB

After

Width:  |  Height:  |  Size: 77 KiB

+2
View File
@@ -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)
@@ -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`.
@@ -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).
@@ -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.
@@ -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)
@@ -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