From ebf66fd92a2c1e93c36257b34f7524f78be41013 Mon Sep 17 00:00:00 2001 From: Anso Date: Fri, 12 Jun 2026 18:40:53 -0400 Subject: [PATCH] feat(settings): add Stacks section for stack workflow preferences (#1366) Move the browser-local Deploy progress, Progress style, and Diff preview before save controls out of Appearance into a new Stacks section under the Infrastructure group. These are stack lifecycle and editor workflow preferences, not visual style, so Settings now groups them where operators expect to find them. Add a browser-local masthead scope so these localStorage-backed sections read SCOPE browser instead of the misleading global, and apply it to both Appearance and Stacks. Control behavior, storage keys, and backing hooks are unchanged; this is an information-architecture move only. --- docs/features/app-store.mdx | 2 +- docs/features/appearance.mdx | 4 +- docs/features/deploy-progress.mdx | 4 +- docs/features/editor.mdx | 2 +- docs/reference/settings.mdx | 54 ++++++++----- .../components/settings/AppearanceSection.tsx | 64 --------------- .../src/components/settings/SettingsPage.tsx | 9 +-- .../settings/SettingsSectionContent.tsx | 2 + .../src/components/settings/StacksSection.tsx | 80 ++++++++++++++++++ .../settings/__tests__/StacksSection.test.tsx | 81 +++++++++++++++++++ .../settings/__tests__/registry.test.ts | 37 ++++++++- frontend/src/components/settings/index.ts | 2 + frontend/src/components/settings/registry.ts | 27 ++++++- frontend/src/components/settings/types.ts | 1 + 14 files changed, 266 insertions(+), 103 deletions(-) create mode 100644 frontend/src/components/settings/StacksSection.tsx create mode 100644 frontend/src/components/settings/__tests__/StacksSection.test.tsx diff --git a/docs/features/app-store.mdx b/docs/features/app-store.mdx index 72ea1e44..8034a6e0 100644 --- a/docs/features/app-store.mdx +++ b/docs/features/app-store.mdx @@ -117,7 +117,7 @@ Clicking **Deploy** runs the following sequence: ## Watching the deploy -Sencho can stream the live `docker compose` output for every template install. Open **Settings › Appearance › Display** and enable **Deploy progress**; from then on, every App Store deploy streams its output. In Modal style it opens a structured log modal you can minimise to a pill that follows you across navigation; in Inline style it shows as that pill, which opens the same log on click. See [Deploy Progress](/features/deploy-progress) for the full reference. +Sencho can stream the live `docker compose` output for every template install. Open **Settings › Infrastructure › Stacks** and enable **Deploy progress**; from then on, every App Store deploy streams its output. In Modal style it opens a structured log modal you can minimise to a pill that follows you across navigation; in Inline style it shows as that pill, which opens the same log on click. See [Deploy Progress](/features/deploy-progress) for the full reference. Deploying a template requires the `stack:create` permission on the target node, which **admin** and **node-admin** roles hold by default. The Deploy button stays disabled when your account does not have it. diff --git a/docs/features/appearance.mdx b/docs/features/appearance.mdx index c8a56fd0..fd3b7c52 100644 --- a/docs/features/appearance.mdx +++ b/docs/features/appearance.mdx @@ -50,5 +50,5 @@ The **Display** group holds the remaining per-browser preferences: - **Density** switches between **Comfortable** (roomy rows, the default) and **Compact** (tighter rows and tiles that fit more on screen for dense dashboards). - **Top navigation labels** shows text labels beside the top navigation icons. Turn it off for an icon-only bar; the destinations stay reachable by hover tooltip, accessible name, and the command palette. On the phone layout the navigation always keeps its labels. With labels off, **Top navigation alignment** chooses whether the icon-only bar sits to the left or centered. -- **Deploy progress** toggles live output streaming for deploy, restart, update, install, and Git operations, and **Progress style** chooses Modal or Inline presentation. -- **Diff preview before save** toggles a side-by-side diff of compose and env edits before they are written to disk. + +Deploy-progress behavior and the diff-preview-before-save step are stack workflow preferences, so they live in **Settings → Infrastructure → Stacks**, not here. diff --git a/docs/features/deploy-progress.mdx b/docs/features/deploy-progress.mdx index 255a6a9d..39fafbe7 100644 --- a/docs/features/deploy-progress.mdx +++ b/docs/features/deploy-progress.mdx @@ -7,7 +7,7 @@ When you trigger a stack action that runs through `docker compose` (Deploy, Upda ## Showing, hiding, and styling deploy progress -Deploy progress is **on by default**. To run operations without it, open **Settings > Appearance > Display** and turn off **Deploy progress**. While it is on, **Progress style** chooses how it appears: +Deploy progress is **on by default**. To run operations without it, open **Settings > Infrastructure > Stacks** and turn off **Deploy progress**. While it is on, **Progress style** chooses how it appears: - **Modal** (the default): a centered overlay that opens automatically and streams the full structured log. - **Inline**: a quiet status band on the stack detail itself, with the full log a click away under **View output**. @@ -15,7 +15,7 @@ Deploy progress is **on by default**. To run operations without it, open **Setti The choices are saved to the current browser only and synced across tabs in the same browser without a reload. - Settings Appearance Display panel with the deploy progress toggle. + Settings Stacks panel with the deploy progress toggle. ## Using the modal diff --git a/docs/features/editor.mdx b/docs/features/editor.mdx index 9901ea22..dc23c792 100644 --- a/docs/features/editor.mdx +++ b/docs/features/editor.mdx @@ -127,7 +127,7 @@ The same controls apply to the `compose.yaml` and `.env` editors. ## Diff preview before save -When **Diff preview before save** is enabled in **Settings → Appearance**, clicking **Save & Deploy** or **Save Only** opens a side-by-side diff modal before anything is written to disk. The left pane is the on-disk content; the right pane is your unsaved edits with additions highlighted in green. The footer reads `ON DISK → UNSAVED` so the panes are unambiguous. +When **Diff preview before save** is enabled in **Settings → Infrastructure → Stacks**, clicking **Save & Deploy** or **Save Only** opens a side-by-side diff modal before anything is written to disk. The left pane is the on-disk content; the right pane is your unsaved edits with additions highlighted in green. The footer reads `ON DISK → UNSAVED` so the panes are unambiguous. Diff preview modal showing side-by-side YAML diff with the unsaved version on the right and an ON DISK to UNSAVED legend in the footer diff --git a/docs/reference/settings.mdx b/docs/reference/settings.mdx index e51b9d52..69b8031a 100644 --- a/docs/reference/settings.mdx +++ b/docs/reference/settings.mdx @@ -15,7 +15,7 @@ Open the Settings Hub by clicking the **Profile** icon in the top bar and select |-------|----------------| | **Personal** | Account, Appearance | | **Access** | License, Users, SSO, API Tokens | -| **Infrastructure** | Nodes, Fleet Mesh, Registries, Cloud Backup, App Store | +| **Infrastructure** | Nodes, Fleet Mesh, Registries, Cloud Backup, App Store, Stacks | | **Monitoring** | Host Alerts, Docker & Storage | | **Notifications** | Channels, Notification Routing | | **Automation** | Webhooks | @@ -33,7 +33,7 @@ Every section renders inside the same masthead-and-sidebar layout. The masthead | Pill | Meaning | |------|---------| -| **SCOPE** `operator` / `global` | Setting applies to your account or this browser (Personal sections) or to the whole instance (every other non-node group) | +| **SCOPE** `operator` / `browser` / `global` | Setting applies to your account (`operator`), to this browser only (`browser`, for browser-local sections such as Appearance and Stacks), or to the whole instance (`global`, every other non-node group) | | **NODE** `` | Setting is per-node and is currently being edited against this node | | **EDITED** `` pending / `saved` | The current section has unsaved changes | | Section-specific stats | Each section can publish its own pills: `2FA on`/`off` and `BACKUP left` (Account); `PLAN`, `TRIAL d left`, `RENEWS`, `STATUS` (License); `OPERATORS` (Users); `CHANNELS` (Channels); `ROUTES` (Notification Routing); `WEBHOOKS` and `ENABLED` (Webhooks); `LABELS` (Labels); `PROVIDER`, `USED`, `SNAPSHOTS` (Cloud Backup); `DEV MODE` (Developer Diagnostics) | @@ -100,7 +100,7 @@ See [Two-Factor Authentication](/features/two-factor-authentication) for the enr Control how dense and how interactive the workspace feels. Each browser remembers its own choices so a compact laptop setup does not force the same rhythm on a larger desktop. - Appearance settings showing the Density selector, the Deploy progress toggle, and the Diff preview toggle + Appearance settings showing the Density selector ### Density @@ -112,25 +112,7 @@ Control how dense and how interactive the workspace feels. Each browser remember Density affects the dashboard stack table, the resource gauge strip, the Settings Hub sidebar, the Schedules and Audit Log tables, and every other data table in Sencho. Typography, color, and layout structure stay the same; only vertical padding compresses. -### Deploy progress - -Sencho streams live output whenever you deploy, restart, update, install, or run a Git operation. It is on by default; turn it off to run operations without it. When it is on, **Progress style** chooses how it appears: **Modal** (a centered overlay that closes automatically on success or stays open on failure) or **Inline** (a quiet status band on the stack detail). See [Deploy Progress](/features/deploy-progress) for the full reference. - -| Value | Behavior | -|-------|----------| -| **Enabled** (default) | A progress surface (the Modal overlay or the Inline band, per Progress style) shows for every long-running operation | -| **Disabled** | Operations run without it; results surface via toast notifications, and a failed operation still shows recovery actions on the stack page | - -### Diff preview before save - -When enabled, clicking **Save & Deploy** or **Save Only** in the compose or env editor opens a side-by-side diff modal before writing anything to disk. The left pane shows the current on-disk content; the right pane shows your unsaved edits, with additions highlighted green and removals highlighted red. - -| Value | Behavior | -|-------|----------| -| **Enabled** | Diff modal opens on every save that has unsaved changes | -| **Disabled** (default) | File is saved directly without a review step | - -If there are no unsaved changes the modal is skipped and the save proceeds immediately. See [Diff preview before save](/features/editor#diff-preview-before-save) in the Editor guide for the full workflow. +Deploy-progress behavior and the diff-preview-before-save step are stack workflow preferences and live in their own [Stacks](#stacks) section under Infrastructure. --- @@ -481,6 +463,34 @@ See [App Store](/features/app-store#custom-template-registry) for more on custom --- +## Stacks + +**Scope:** This browser (preferences are saved to local storage) + +Stack editor and lifecycle workflow preferences. Each browser remembers its own choices, so they follow you across tabs but never change what other operators see. The masthead reads `SCOPE browser`. + +### Deploy progress + +Sencho streams live output whenever you deploy, restart, update, install, or run a Git operation. It is on by default; turn it off to run operations without it. When it is on, **Progress style** chooses how it appears: **Modal** (a centered overlay that closes automatically on success or stays open on failure) or **Inline** (a quiet status band on the stack detail). See [Deploy Progress](/features/deploy-progress) for the full reference. + +| Value | Behavior | +|-------|----------| +| **Enabled** (default) | A progress surface (the Modal overlay or the Inline band, per Progress style) shows for every long-running operation | +| **Disabled** | Operations run without it; results surface via toast notifications, and a failed operation still shows recovery actions on the stack page | + +### Diff preview before save + +When enabled, clicking **Save & Deploy** or **Save Only** in the compose or env editor opens a side-by-side diff modal before writing anything to disk. The left pane shows the current on-disk content; the right pane shows your unsaved edits, with additions highlighted green and removals highlighted red. + +| Value | Behavior | +|-------|----------| +| **Enabled** | Diff modal opens on every save that has unsaved changes | +| **Disabled** (default) | File is saved directly without a review step | + +If there are no unsaved changes the modal is skipped and the save proceeds immediately. See [Diff preview before save](/features/editor#diff-preview-before-save) in the Editor guide for the full workflow. + +--- + ## Support **Scope:** Global diff --git a/frontend/src/components/settings/AppearanceSection.tsx b/frontend/src/components/settings/AppearanceSection.tsx index 6bf67c81..f8d7a835 100644 --- a/frontend/src/components/settings/AppearanceSection.tsx +++ b/frontend/src/components/settings/AppearanceSection.tsx @@ -1,13 +1,9 @@ import { Combobox } from '@/components/ui/combobox'; -import { Checkbox } from '@/components/ui/checkbox'; import { Slider } from '@/components/ui/slider'; import { SegmentedControl } from '@/components/ui/segmented-control'; import { TogglePill } from '@/components/ui/toggle-pill'; import { useDensity } from '@/hooks/use-density'; import type { Density } from '@/hooks/use-density'; -import { useDeployFeedbackEnabled } from '@/hooks/use-deploy-feedback-enabled'; -import { useDeployFeedbackStyle, type DeployFeedbackStyle } from '@/hooks/use-deploy-feedback-style'; -import { useComposeDiffPreviewEnabled } from '@/hooks/use-compose-diff-preview-enabled'; import { useTopNavLabels } from '@/hooks/use-top-nav-labels'; import { useTopNavAlign, type TopNavAlign } from '@/hooks/use-top-nav-align'; import { useTheme, THEME_MODE_OPTIONS, ACCENTS, CONTRAST, BORDER_BOOST, GLOW, TYPE_SCALE } from '@/hooks/use-theme'; @@ -29,11 +25,6 @@ const DENSITY_DESCRIPTIONS: Record = { compact: 'Tighter rows and tiles. Fits more on screen for dense dashboards.', }; -const DEPLOY_STYLE_OPTIONS: { value: DeployFeedbackStyle; label: string }[] = [ - { value: 'modal', label: 'Modal' }, - { value: 'inline', label: 'Inline' }, -]; - const TOP_NAV_ALIGN_OPTIONS: { value: TopNavAlign; label: string }[] = [ { value: 'left', label: 'Left' }, { value: 'center', label: 'Center' }, @@ -43,9 +34,6 @@ const fmtSigned = (v: number) => `${v > 0 ? '+' : ''}${v.toFixed(2)}`; export function AppearanceSection() { const [density, setDensity] = useDensity(); - const [isEnabled, setEnabled] = useDeployFeedbackEnabled(); - const [feedbackStyle, setFeedbackStyle] = useDeployFeedbackStyle(); - const [diffPreviewEnabled, setDiffPreviewEnabled] = useComposeDiffPreviewEnabled(); const [topNavLabels, setTopNavLabels] = useTopNavLabels(); const [topNavAlign, setTopNavAlign] = useTopNavAlign(); const { @@ -223,58 +211,6 @@ export function AppearanceSection() { /> )} - - -
- setEnabled(v === true)} - /> - -
-
- - {isEnabled && ( - - - - )} - - -
- setDiffPreviewEnabled(v === true)} - /> - -
-

diff --git a/frontend/src/components/settings/SettingsPage.tsx b/frontend/src/components/settings/SettingsPage.tsx index 50576ce7..cd792d31 100644 --- a/frontend/src/components/settings/SettingsPage.tsx +++ b/frontend/src/components/settings/SettingsPage.tsx @@ -21,6 +21,7 @@ import { getSettingsGroup, isItemVisible, isItemLocked, + scopeLabel, } from './index'; import type { SectionId, SettingsItemMeta, VisibilityContext } from './index'; import { SettingsSidebar } from './SettingsSidebar'; @@ -228,14 +229,6 @@ function SettingsPageInner({ currentSection, onSectionChange }: SettingsPageProp ); } -function scopeLabel(item: SettingsItemMeta): string { - // Personal sections (account, appearance) apply to the signed-in operator or - // this browser. Access sections (license, users, sso, api-tokens) are - // instance-global, so they read as global like every other non-node group. - if (item.group === 'personal') return 'operator'; - return 'global'; -} - function SettingsCommandItem({ item, glyph, diff --git a/frontend/src/components/settings/SettingsSectionContent.tsx b/frontend/src/components/settings/SettingsSectionContent.tsx index 5b587c81..f98deecd 100644 --- a/frontend/src/components/settings/SettingsSectionContent.tsx +++ b/frontend/src/components/settings/SettingsSectionContent.tsx @@ -13,6 +13,7 @@ import { DeveloperSection, DataRetentionSection, AppStoreSection, + StacksSection, SupportSection, AboutSection, RecoverySection, @@ -90,6 +91,7 @@ function renderSection( case 'data-retention': return onDirtyChange('data-retention', d)} />; case 'nodes': return ; case 'app-store': return ; + case 'stacks': return ; case 'recovery': return ; case 'support': return ; case 'about': return ; diff --git a/frontend/src/components/settings/StacksSection.tsx b/frontend/src/components/settings/StacksSection.tsx new file mode 100644 index 00000000..17888441 --- /dev/null +++ b/frontend/src/components/settings/StacksSection.tsx @@ -0,0 +1,80 @@ +import { Checkbox } from '@/components/ui/checkbox'; +import { SegmentedControl } from '@/components/ui/segmented-control'; +import { useDeployFeedbackEnabled } from '@/hooks/use-deploy-feedback-enabled'; +import { useDeployFeedbackStyle, type DeployFeedbackStyle } from '@/hooks/use-deploy-feedback-style'; +import { useComposeDiffPreviewEnabled } from '@/hooks/use-compose-diff-preview-enabled'; +import { SettingsSection } from './SettingsSection'; +import { SettingsField } from './SettingsField'; + +const DEPLOY_STYLE_OPTIONS: { value: DeployFeedbackStyle; label: string }[] = [ + { value: 'modal', label: 'Modal' }, + { value: 'inline', label: 'Inline' }, +]; + +export function StacksSection() { + const [isEnabled, setEnabled] = useDeployFeedbackEnabled(); + const [feedbackStyle, setFeedbackStyle] = useDeployFeedbackStyle(); + const [diffPreviewEnabled, setDiffPreviewEnabled] = useComposeDiffPreviewEnabled(); + + return ( +

+ + +
+ setEnabled(v === true)} + /> + +
+
+ + {isEnabled && ( + + + + )} + + +
+ setDiffPreviewEnabled(v === true)} + /> + +
+
+
+ +

+ ⓘ saved to this browser only · every device remembers its own choice +

+
+ ); +} diff --git a/frontend/src/components/settings/__tests__/StacksSection.test.tsx b/frontend/src/components/settings/__tests__/StacksSection.test.tsx new file mode 100644 index 00000000..af148c31 --- /dev/null +++ b/frontend/src/components/settings/__tests__/StacksSection.test.tsx @@ -0,0 +1,81 @@ +/** + * Guards the move of the stack-workflow controls out of Appearance into Stacks. + * + * The three controls (Deploy progress, Progress style, Diff preview before save) + * are browser-local localStorage preferences. Moving the JSX must not change the + * storage keys they write, so the deploy/editor consumers keep reading the same + * values. These tests assert the controls render in Stacks, still flip the same + * keys, and no longer render in Appearance. + */ +import { describe, it, expect, beforeEach, afterEach } from 'vitest'; +import { render, screen, fireEvent } from '@testing-library/react'; +import { StacksSection } from '../StacksSection'; +import { AppearanceSection } from '../AppearanceSection'; +import { DEPLOY_FEEDBACK_KEY } from '@/hooks/use-deploy-feedback-enabled'; +import { DEPLOY_FEEDBACK_STYLE_KEY } from '@/hooks/use-deploy-feedback-style'; +import { COMPOSE_DIFF_PREVIEW_KEY } from '@/hooks/use-compose-diff-preview-enabled'; + +beforeEach(() => { + window.localStorage.clear(); +}); + +afterEach(() => { + window.localStorage.clear(); +}); + +describe('StacksSection', () => { + it('renders the three workflow controls (Progress style while deploy progress is on)', () => { + render(); + expect(screen.getByText('Deploy progress')).toBeInTheDocument(); + // Deploy progress defaults on, so Progress style is visible. + expect(screen.getByText('Progress style')).toBeInTheDocument(); + expect(screen.getByText('Diff preview before save')).toBeInTheDocument(); + }); + + it('flips the deploy-progress key and hides Progress style when disabled', () => { + const { container } = render(); + const toggle = container.querySelector('#deploy-feedback') as HTMLElement; + // Default on => no stored value yet. + expect(window.localStorage.getItem(DEPLOY_FEEDBACK_KEY)).toBeNull(); + fireEvent.click(toggle); + expect(window.localStorage.getItem(DEPLOY_FEEDBACK_KEY)).toBe('false'); + // Progress style is gated on the enabled state and drops out. + expect(screen.queryByText('Progress style')).not.toBeInTheDocument(); + }); + + it('round-trips the progress-style key between Inline and Modal', () => { + render(); + fireEvent.click(screen.getByRole('radio', { name: 'Inline' })); + expect(window.localStorage.getItem(DEPLOY_FEEDBACK_STYLE_KEY)).toBe('inline'); + fireEvent.click(screen.getByRole('radio', { name: 'Modal' })); + expect(window.localStorage.getItem(DEPLOY_FEEDBACK_STYLE_KEY)).toBe('modal'); + }); + + it('hydrates each control from its stored value (read path survives the move)', () => { + window.localStorage.setItem(DEPLOY_FEEDBACK_KEY, 'false'); + window.localStorage.setItem(COMPOSE_DIFF_PREVIEW_KEY, 'true'); + const { container } = render(); + // Deploy progress reads its stored 'false': checkbox unchecked, Progress style hidden. + expect(container.querySelector('#deploy-feedback')?.getAttribute('aria-checked')).toBe('false'); + expect(screen.queryByText('Progress style')).not.toBeInTheDocument(); + // Diff preview reads its stored 'true': checkbox checked. + expect(container.querySelector('#compose-diff-preview')?.getAttribute('aria-checked')).toBe('true'); + }); + + it('flips the diff-preview key when enabled', () => { + const { container } = render(); + const toggle = container.querySelector('#compose-diff-preview') as HTMLElement; + expect(window.localStorage.getItem(COMPOSE_DIFF_PREVIEW_KEY)).toBeNull(); + fireEvent.click(toggle); + expect(window.localStorage.getItem(COMPOSE_DIFF_PREVIEW_KEY)).toBe('true'); + }); +}); + +describe('AppearanceSection no longer owns stack-workflow controls', () => { + it('does not render Deploy progress, Progress style, or Diff preview before save', () => { + render(); + expect(screen.queryByText('Deploy progress')).not.toBeInTheDocument(); + expect(screen.queryByText('Progress style')).not.toBeInTheDocument(); + expect(screen.queryByText('Diff preview before save')).not.toBeInTheDocument(); + }); +}); diff --git a/frontend/src/components/settings/__tests__/registry.test.ts b/frontend/src/components/settings/__tests__/registry.test.ts index bb6ea667..9a88a4d1 100644 --- a/frontend/src/components/settings/__tests__/registry.test.ts +++ b/frontend/src/components/settings/__tests__/registry.test.ts @@ -7,7 +7,8 @@ * Data Retention, and the renamed labels are applied. */ import { describe, it, expect } from 'vitest'; -import { SETTINGS_GROUPS, SETTINGS_ITEMS } from '../registry'; +import { SETTINGS_GROUPS, SETTINGS_ITEMS, scopeLabel } from '../registry'; +import type { SettingsItemMeta } from '../registry'; describe('settings registry', () => { it('points every item at a defined group', () => { @@ -74,4 +75,38 @@ describe('settings registry', () => { expect(registries?.tier).toBeNull(); expect(registries?.adminOnly).toBe(true); }); + + it('registers the Stacks section under Infrastructure with searchable workflow keywords', () => { + const stacks = SETTINGS_ITEMS.find(i => i.id === 'stacks'); + expect(stacks?.group).toBe('infrastructure'); + expect(stacks?.tier).toBeNull(); + for (const term of ['stack', 'deploy', 'progress', 'diff', 'save']) { + expect(stacks?.keywords, `keyword ${term}`).toContain(term); + } + }); + + it('scopes the browser-local sections (Appearance, Stacks) to the browser', () => { + expect(SETTINGS_ITEMS.find(i => i.id === 'appearance')?.scope).toBe('browser'); + expect(SETTINGS_ITEMS.find(i => i.id === 'stacks')?.scope).toBe('browser'); + }); +}); + +describe('scopeLabel', () => { + const item = (over: Partial): SettingsItemMeta => ({ + id: 'stacks', group: 'infrastructure', label: 'X', description: '', + keywords: [], tier: null, scope: 'global', ...over, + }); + + it('reads browser for browser-scoped sections regardless of their group', () => { + expect(scopeLabel(item({ scope: 'browser', group: 'personal' }))).toBe('browser'); + expect(scopeLabel(item({ scope: 'browser', group: 'infrastructure' }))).toBe('browser'); + }); + + it('reads operator for the signed-in Account (personal group)', () => { + expect(scopeLabel(item({ scope: 'global', group: 'personal' }))).toBe('operator'); + }); + + it('reads global for other non-node, non-browser groups', () => { + expect(scopeLabel(item({ scope: 'global', group: 'access' }))).toBe('global'); + }); }); diff --git a/frontend/src/components/settings/index.ts b/frontend/src/components/settings/index.ts index f2031b2c..9308e57b 100644 --- a/frontend/src/components/settings/index.ts +++ b/frontend/src/components/settings/index.ts @@ -10,6 +10,7 @@ export { NotificationsSection } from './NotificationsSection'; export { DeveloperSection } from './DeveloperSection'; export { DataRetentionSection } from './DataRetentionSection'; export { AppStoreSection } from './AppStoreSection'; +export { StacksSection } from './StacksSection'; export { SupportSection } from './SupportSection'; export { AboutSection } from './AboutSection'; export { RecoverySection } from './RecoverySection'; @@ -30,6 +31,7 @@ export { getSettingsGroup, isItemVisible, isItemLocked, + scopeLabel, } from './registry'; export type { SettingsGroupId, diff --git a/frontend/src/components/settings/registry.ts b/frontend/src/components/settings/registry.ts index bb9d13ff..7ad398d7 100644 --- a/frontend/src/components/settings/registry.ts +++ b/frontend/src/components/settings/registry.ts @@ -31,7 +31,7 @@ export const SETTINGS_GROUPS: readonly SettingsGroupMeta[] = [ ]; export type TierGate = 'paid' | null; -export type Scope = 'global' | 'node'; +export type Scope = 'global' | 'node' | 'browser'; export interface SettingsItemMeta { id: SectionId; @@ -64,7 +64,7 @@ export const SETTINGS_ITEMS: readonly SettingsItemMeta[] = [ description: 'Theme, accent, density, and display preferences saved to this browser.', keywords: ['theme', 'dim', 'oled', 'light', 'dark', 'accent', 'color', 'glow', 'border', 'contrast', 'density', 'comfortable', 'compact', 'spacing', 'display'], tier: null, - scope: 'global', + scope: 'browser', }, // Access { @@ -162,6 +162,15 @@ export const SETTINGS_ITEMS: readonly SettingsItemMeta[] = [ tier: null, scope: 'node', }, + { + id: 'stacks', + group: 'infrastructure', + label: 'Stacks', + description: 'Stack editor and lifecycle workflow preferences saved to this browser.', + keywords: ['stack', 'compose', 'deploy', 'progress', 'modal', 'inline', 'diff', 'preview', 'save', 'editor', 'workflow'], + tier: null, + scope: 'browser', + }, // Monitoring { id: 'host-alerts', @@ -297,3 +306,17 @@ export function isItemVisible(item: SettingsItemMeta, ctx: VisibilityContext): b export function isItemLocked(item: SettingsItemMeta, ctx: VisibilityContext): boolean { return item.tier === 'paid' ? !ctx.isPaid : false; } + +/** + * The masthead SCOPE value for a non-node section. Browser-local sections + * (Appearance, Stacks) persist to this browser's localStorage and read as + * browser regardless of their group; the signed-in Account is operator-scoped; + * Access sections (license, users, sso, api-tokens) are instance-global, so + * they read as global like every other non-node group. Node-scoped sections + * render a NODE pill instead and never reach here. + */ +export function scopeLabel(item: SettingsItemMeta): string { + if (item.scope === 'browser') return 'browser'; + if (item.group === 'personal') return 'operator'; + return 'global'; +} diff --git a/frontend/src/components/settings/types.ts b/frontend/src/components/settings/types.ts index e02e4d86..4c741ad2 100644 --- a/frontend/src/components/settings/types.ts +++ b/frontend/src/components/settings/types.ts @@ -59,6 +59,7 @@ export type SectionId = | 'data-retention' | 'nodes' | 'app-store' + | 'stacks' | 'notification-routing' | 'recovery' | 'support'