From c0c6880b1a00ec367910267a3d2a0595839b3d5b Mon Sep 17 00:00:00 2001 From: Hare Date: Thu, 2 Jul 2026 23:34:30 +0900 Subject: [PATCH] feat: add settings admin shell --- web/workspace/deno.json | 2 +- web/workspace/src/app.css | 152 +++++++++++++++++ .../workspace-settings/SettingsPage.svelte | 153 ++++++++++++++++++ .../src/lib/workspace-settings/model.test.ts | 82 ++++++++++ .../src/lib/workspace-settings/model.ts | 83 ++++++++++ .../workspace-sidebar/WorkspaceSidebar.svelte | 28 +++- .../src/routes/settings/+page.svelte | 5 + 7 files changed, 498 insertions(+), 7 deletions(-) create mode 100644 web/workspace/src/lib/workspace-settings/SettingsPage.svelte create mode 100644 web/workspace/src/lib/workspace-settings/model.test.ts create mode 100644 web/workspace/src/lib/workspace-settings/model.ts create mode 100644 web/workspace/src/routes/settings/+page.svelte diff --git a/web/workspace/deno.json b/web/workspace/deno.json index b9bfb471..adda00c7 100644 --- a/web/workspace/deno.json +++ b/web/workspace/deno.json @@ -6,7 +6,7 @@ "dev": "deno run -A npm:vite@7.2.7 dev", "dev:backend": "cd ../.. && cargo run -p yoi-workspace-server -- serve --workspace . --db .yoi/workspace.db --listen 127.0.0.1:8787", "check": "deno run -A npm:@sveltejs/kit@2.49.4 sync && deno run -A npm:svelte-check@4.3.4 --tsconfig ./tsconfig.json", - "test": "deno test --allow-read=src src/lib/workspace-console/model.test.ts src/lib/workspace-console/worker-console.ui.test.ts", + "test": "deno test --allow-read=src src/lib/workspace-console/model.test.ts src/lib/workspace-console/worker-console.ui.test.ts src/lib/workspace-settings/model.test.ts", "build": "deno run -A npm:vite@7.2.7 build", "preview": "deno run -A npm:vite@7.2.7 preview" }, diff --git a/web/workspace/src/app.css b/web/workspace/src/app.css index e10c04ae..fda816ee 100644 --- a/web/workspace/src/app.css +++ b/web/workspace/src/app.css @@ -988,3 +988,155 @@ text-align: left; } } + +.settings-button { + text-decoration: none; + cursor: pointer; +} + +.settings-button:hover, +.settings-button:focus-visible, +.settings-button.active { + background: var(--interactive-selected); + color: var(--accent); +} + +.settings-shell { + gap: var(--space-5); +} + +.settings-hero, +.settings-notice, +.settings-section-header, +.settings-patterns { + display: flex; + align-items: flex-start; + justify-content: space-between; + gap: var(--space-4); +} + +.hero-copy { + max-width: 54rem; + margin: 0; + color: var(--text-muted); +} + +.badge { + flex: 0 0 auto; + border-radius: 999px; + padding: 0.35rem 0.65rem; + background: var(--bg-subtle); + color: var(--text-muted); + font-size: 0.72rem; + font-weight: 800; + letter-spacing: 0.06em; + text-transform: uppercase; +} + +.badge.warning { + color: var(--warning); +} + +.badge.success { + color: var(--success); +} + +.settings-notice { + border-left: 4px solid var(--warning); + padding-left: var(--space-4); +} + +.settings-diagnostic { + display: grid; + gap: var(--space-1); + max-width: 24rem; + padding: var(--space-3) var(--space-4); + border: 1px solid var(--line); + border-radius: var(--radius-panel); + background: var(--bg-raised); + color: var(--text-muted); +} + +.settings-diagnostic strong { + color: var(--text-strong); +} + +.settings-nav-card { + display: flex; + flex-wrap: wrap; + gap: var(--space-3); +} + +.settings-nav-link { + display: grid; + gap: var(--space-1); + min-width: min(16rem, 100%); + padding: var(--space-3) var(--space-4); + border: 1px solid var(--line); + border-radius: var(--radius-panel); + background: var(--bg-raised); + color: inherit; + text-decoration: none; +} + +.settings-nav-link:hover, +.settings-nav-link:focus-visible { + background: var(--interactive-hover); +} + +.settings-nav-link span { + color: var(--text-strong); + font-weight: 800; +} + +.settings-grid, +.settings-pattern-grid { + grid-template-columns: repeat(auto-fit, minmax(min(20rem, 100%), 1fr)); +} + +.settings-section, +.settings-patterns, +.settings-pattern { + display: grid; + gap: var(--space-4); +} + +.settings-section ul, +.settings-patterns ul { + margin: 0; + padding-left: 1.2rem; +} + +.settings-section li + li { + margin-top: var(--space-2); +} + +.settings-identity-list { + padding-top: var(--space-3); + border-top: 1px solid var(--line); +} + +.settings-pattern { + padding: var(--space-4); + border-radius: var(--radius-panel); + background: var(--bg-raised); +} + +.settings-pattern h3, +.settings-pattern p { + margin: 0; +} + +.status-message { + margin: 0; + color: var(--text-muted); +} + +@media (max-width: 760px) { + .settings-hero, + .settings-notice, + .settings-section-header, + .settings-patterns { + display: grid; + } +} diff --git a/web/workspace/src/lib/workspace-settings/SettingsPage.svelte b/web/workspace/src/lib/workspace-settings/SettingsPage.svelte new file mode 100644 index 00000000..fbe897b5 --- /dev/null +++ b/web/workspace/src/lib/workspace-settings/SettingsPage.svelte @@ -0,0 +1,153 @@ + + + + Settings · Yoi Workspace + + +
+ + +
+
+
+

Workspace Browser

+

Settings / Admin

+

+ Read-only shell for future local administration surfaces. This page creates + navigation and operator context without adding mutation authority. +

+
+ shell only +
+ +
+
+

Authority boundary

+

No browser admin permission model

+

{SETTINGS_PERMISSION_NOTICE}

+
+
+ Diagnostic pattern + Future controls must use typed Backend diagnostics and restart-required states. +
+
+ +
+ {#each SETTINGS_SECTIONS as section} + + {section.label} + {section.status === "read-only" ? "Read-only" : "Placeholder"} + + {/each} +
+ +
+ {#each SETTINGS_SECTIONS as section} +
+
+
+

{section.status}

+

{section.label}

+
+ {#if section.status === "placeholder"} + not implemented + {:else} + read-only + {/if} +
+

{section.summary}

+
    + {#each section.bullets as bullet} +
  • {bullet}
  • + {/each} +
+ + {#if section.id === "workspace-identity"} +
+
+
Workspace id
+
{workspace?.workspace_id ?? "loading"}
+
+
+
Display name
+
{workspace?.display_name ?? "loading"}
+
+
+
Record authority
+
.yoi tickets/objectives through the Backend projection
+
+
+ {/if} +
+ {/each} +
+ +
+
+

Implementation patterns

+

How future settings should appear

+
+
+ {#each SETTINGS_PATTERNS as pattern} +
+

{pattern.title}

+

{pattern.body}

+
+ {/each} +
+
+ + {#if loading} +

Loading workspace summary…

+ {:else if error} +

Workspace summary unavailable: {error}

+ {/if} +
+
diff --git a/web/workspace/src/lib/workspace-settings/model.test.ts b/web/workspace/src/lib/workspace-settings/model.test.ts new file mode 100644 index 00000000..6c876dc9 --- /dev/null +++ b/web/workspace/src/lib/workspace-settings/model.test.ts @@ -0,0 +1,82 @@ +import { + SETTINGS_PATTERNS, + SETTINGS_PERMISSION_NOTICE, + SETTINGS_ROUTE, + SETTINGS_SECTIONS, + settingsSectionHref, +} from "./model.ts"; + +declare const Deno: { + test(name: string, fn: () => void): void; +}; + +function assert(condition: unknown, message: string): asserts condition { + if (!condition) { + throw new Error(message); + } +} + +Deno.test("settings section navigation stays under the settings route", () => { + assert(SETTINGS_ROUTE === "/settings", "settings route should be stable"); + + for (const section of SETTINGS_SECTIONS) { + const href = settingsSectionHref(section.id); + assert( + href.startsWith("/settings#"), + `${section.id} should link under settings`, + ); + assert( + href.endsWith(section.id), + `${section.id} href should preserve section id`, + ); + } +}); + +Deno.test("settings shell advertises no fake browser admin model", () => { + assert( + SETTINGS_PERMISSION_NOTICE.includes("no browser user, role, permission"), + "notice should explicitly deny a browser permission model", + ); + assert( + SETTINGS_PERMISSION_NOTICE.includes("does not create an admin role"), + "notice should not imply an admin role exists", + ); +}); + +Deno.test("settings placeholders avoid mutation promises and raw authority leaks", () => { + const allText = [ + SETTINGS_PERMISSION_NOTICE, + ...SETTINGS_SECTIONS.flatMap((section) => [ + section.label, + section.summary, + ...section.bullets, + ]), + ...SETTINGS_PATTERNS.flatMap((pattern) => [pattern.title, pattern.body]), + ].join("\n"); + + assert( + allText.includes( + "does not add, remove, test, or persist Runtime endpoints", + ), + "Runtime Connections should remain a placeholder", + ); + assert( + allText.includes("Restart-required"), + "restart-required pattern should be visible", + ); + + for ( + const forbidden of [ + "/home/", + "socket path:", + "token:", + "secret:", + "store root:", + ] + ) { + assert( + !allText.includes(forbidden), + `settings copy should not expose ${forbidden}`, + ); + } +}); diff --git a/web/workspace/src/lib/workspace-settings/model.ts b/web/workspace/src/lib/workspace-settings/model.ts new file mode 100644 index 00000000..f823c162 --- /dev/null +++ b/web/workspace/src/lib/workspace-settings/model.ts @@ -0,0 +1,83 @@ +export type SettingsSectionId = + | "runtime-connections" + | "backend-config" + | "workspace-identity"; + +export type SettingsSection = { + readonly id: SettingsSectionId; + readonly label: string; + readonly status: "placeholder" | "read-only"; + readonly summary: string; + readonly bullets: readonly string[]; +}; + +export type SettingsPattern = { + readonly title: string; + readonly body: string; +}; + +export const SETTINGS_ROUTE = "/settings"; + +export const SETTINGS_PERMISSION_NOTICE = + "Yoi currently has no browser user, role, permission, or multi-user authorization model. This shell is intentionally local and descriptive; it does not create an admin role or grant mutation authority."; + +export const SETTINGS_SECTIONS: readonly SettingsSection[] = [ + { + id: "runtime-connections", + label: "Runtime Connections", + status: "placeholder", + summary: + "Future Runtime connection management will live here. The current view does not add, remove, test, or persist Runtime endpoints.", + bullets: [ + "Shows where connection diagnostics will surface without exposing tokens, sockets, store roots, or raw endpoint secrets.", + "Connection changes require a later typed Backend API and are not performed by this shell.", + "Restart-required states should be shown as bounded diagnostics rather than live mutation controls.", + ], + }, + { + id: "backend-config", + label: "Backend Config", + status: "placeholder", + summary: + "Configuration inspection is planned, but editing Backend config or secrets is out of scope for this shell.", + bullets: [ + "Only sanitized summaries belong in the browser; raw config paths, secret refs, tokens, and store roots stay backend-side.", + "Missing-provider or invalid-config states should be displayed as typed diagnostics.", + "No fake permission model is created to make config editing appear available.", + ], + }, + { + id: "workspace-identity", + label: "Workspace Identity", + status: "read-only", + summary: + "Workspace identity is presented as read-only context so operators can tell which workspace the browser is attached to.", + bullets: [ + "Use opaque workspace ids and display names rather than raw filesystem paths.", + "Repository/project-record authority remains backend-side and is not edited here.", + "Identity changes need a later explicit migration flow.", + ], + }, +]; + +export const SETTINGS_PATTERNS: readonly SettingsPattern[] = [ + { + title: "Sanitized diagnostics", + body: + "Settings cards should show bounded codes and operator-facing messages, not raw socket paths, credentials, secret refs, token values, or Runtime store paths.", + }, + { + title: "Restart-required changes", + body: + "When a future setting cannot apply live, the browser should say restart required and leave the mutation to a typed Backend workflow.", + }, + { + title: "Read-only until typed APIs exist", + body: + "Placeholder sections describe planned surfaces without pretending that user, role, permission, or Runtime mutation APIs already exist.", + }, +]; + +export function settingsSectionHref(id: SettingsSectionId): string { + return `${SETTINGS_ROUTE}#${id}`; +} diff --git a/web/workspace/src/lib/workspace-sidebar/WorkspaceSidebar.svelte b/web/workspace/src/lib/workspace-sidebar/WorkspaceSidebar.svelte index ca947577..7279cfaf 100644 --- a/web/workspace/src/lib/workspace-sidebar/WorkspaceSidebar.svelte +++ b/web/workspace/src/lib/workspace-sidebar/WorkspaceSidebar.svelte @@ -11,6 +11,7 @@ }; let { workspace, workspaceError = null, currentPath = '/' }: Props = $props(); + let settingsActive = $derived(currentPath.startsWith("/settings")); diff --git a/web/workspace/src/routes/settings/+page.svelte b/web/workspace/src/routes/settings/+page.svelte new file mode 100644 index 00000000..a7831cef --- /dev/null +++ b/web/workspace/src/routes/settings/+page.svelte @@ -0,0 +1,5 @@ + + +