feat(settings): split system data and storage pages (#3348)
@@ -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",
|
||||
},
|
||||
];
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -58,7 +58,7 @@
|
||||
"databaseOptimize": "Ōƥţĩḿĩźē",
|
||||
"databaseOptimizeHelp": "Àśķś ţĥē ďàţàƀàśē ţō ŕēƒŕēśĥ ĩţś ĩńţēŕńàĺ ĩńďēxēś śō ćōḿḿōń qũēŕĩēś śţàŷ ƒàśţ. Ŕũń ĩţ àƒţēŕ ďēĺēţĩńĝ à ĺōţ ōƒ ďàţà ōŕ ĩƒ ţĥē àƥƥ ƒēēĺś śĺũĝĝĩśĥ. Qũĩćķ àńď śàƒē - ńō ďàţà ĩś ćĥàńĝēď.",
|
||||
"databasePageDescription": "Ďàţàƀàśē ďŕĩvēŕ, śĩźē, àńď àvàĩĺàƀĺē ḿàĩńţēńàńćē ćōńţŕōĺś.",
|
||||
"dataStoragePageDescription": "Ďàţàƀàśē śţàţĩśţĩćś, ƀàćķũƥś, ďĩśķ ćĺēàńũƥ, àńď śēŕvēŕ ĺōĝś.",
|
||||
"dataStoragePageDescription": "Ďàţàƀàśē śţàţĩśţĩćś, ƀàćķũƥś, àńď śēŕvēŕ ĺōĝś.",
|
||||
"databasePath": "Ƥàţĥ",
|
||||
"databaseRunOptimize": "Ŕũń ōƥţĩḿĩźē",
|
||||
"databaseRunVacuum": "Ŕũń vàćũũḿ",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -58,7 +58,7 @@
|
||||
"databaseOptimize": "优化",
|
||||
"databaseOptimizeHelp": "让数据库刷新内部索引,使常见查询保持快速。大量删除数据后或应用感觉变慢时运行。快速且安全;不更改任何数据。",
|
||||
"databasePageDescription": "数据库驱动、大小与可用维护控件。",
|
||||
"dataStoragePageDescription": "数据库统计、备份、磁盘清理和服务器日志。",
|
||||
"dataStoragePageDescription": "数据库统计、备份和服务器日志。",
|
||||
"databasePath": "路径",
|
||||
"databaseRunOptimize": "运行优化",
|
||||
"databaseRunVacuum": "运行 vacuum",
|
||||
|
||||
@@ -58,7 +58,7 @@
|
||||
"databaseOptimize": "優化",
|
||||
"databaseOptimizeHelp": "讓數據庫刷新內部索引,使常見查詢保持快速。大量刪除數據後或應用感覺變慢時執行。快速且安全;不更改任何數據。",
|
||||
"databasePageDescription": "數據庫驅動、大小與可用維護控制項。",
|
||||
"dataStoragePageDescription": "數據庫統計、備份、磁碟清理和伺服器日誌。",
|
||||
"dataStoragePageDescription": "數據庫統計、備份和伺服器日誌。",
|
||||
"databasePath": "路徑",
|
||||
"databaseRunOptimize": "執行優化",
|
||||
"databaseRunVacuum": "執行 vacuum",
|
||||
|
||||
@@ -58,7 +58,7 @@
|
||||
"databaseOptimize": "最佳化",
|
||||
"databaseOptimizeHelp": "讓資料庫重新整理內部索引,使常見查詢保持快速。大量刪除資料後或應用感覺變慢時執行。快速且安全;不更改任何資料。",
|
||||
"databasePageDescription": "資料庫驅動、大小與可用維護控制項。",
|
||||
"dataStoragePageDescription": "資料庫統計、備份、磁碟清理和伺服器日誌。",
|
||||
"dataStoragePageDescription": "資料庫統計、備份和伺服器日誌。",
|
||||
"databasePath": "路徑",
|
||||
"databaseRunOptimize": "執行最佳化",
|
||||
"databaseRunVacuum": "執行 vacuum",
|
||||
|
||||
@@ -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", () => {
|
||||
|
||||
@@ -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.
|
||||
@@ -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 |
|
||||
|
||||
@@ -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.
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||

|
||||

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

|
||||

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

|
||||

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

|
||||

|
||||
|
||||
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
|
||||
|
||||
|
Before Width: | Height: | Size: 315 KiB After Width: | Height: | Size: 110 KiB |
|
Before Width: | Height: | Size: 344 KiB After Width: | Height: | Size: 82 KiB |
|
Before Width: | Height: | Size: 146 KiB After Width: | Height: | Size: 73 KiB |
|
Before Width: | Height: | Size: 356 KiB After Width: | Height: | Size: 77 KiB |
@@ -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
|
||||
|
||||