refactor(settings)!: reorganise settings into fourteen routed categories - #154
Merged
Conversation
Settings was six categories holding around thirty sections, two of which carried most of the weight: AI held eleven sections and Administration held seven heavyweight tools. The AI category also rendered a different page depending on who opened it, since five of its sections were admin only, so the same label meant different things to different users. Replace the single tab-switching page with fourteen categories grouped under General, Meetings, Data and About, each on its own route. Organise by domain rather than by role: an admin-only category lives in the group its subject belongs to and is simply not shown to users who cannot see any of its content. Introduce a settings registry as the manifest for every user-facing setting, carrying its category, access, keywords, Advanced criterion and shipped default. It backs cross-category search, the Advanced gate and the changed-count badge, replacing the per-tab keyword blobs. Hoist the settings object, the debounced autosave and the save status into the route layout. This is required rather than tidy: useDebouncedAutosave cancels its pending timer on unmount without flushing, so a hook owned by a category page would silently discard an edit made within a second of navigating elsewhere. It also keeps the session to one GET /settings rather than one per category visited. Add the surface primitives the categories will be converted onto: a card, a divider-separated row, a full-width block slot for composite UI, and the Advanced gate. Declare the settings surface tokens in globals.css alongside the existing radius and padding tokens, so Settings finally honours compact density instead of ignoring it. Resolve two naming collisions. Voice activity detection and speaker diarization move from Personal to Recording, beside the gain controls that explain the same symptoms, so "capture" no longer names two different things. The category for user accounts is Users and access, never People, which already means the contacts library. Preserve every pre-redesign entry point: legacy ?tab= values redirect to their new routes, /settings/capture redirects to /settings/recording, and a forced password change locks the whole area to Profile from the layout so no route or stale bookmark can slip past it. Content still renders through the previous section components; the category pages mount them unchanged. Converting them onto the new primitives follows in the next commits.
Rebuild Profile, Appearance, Integrations and Users on the card-and-row model, replacing the three-level surface stack with two levels. The Password card is the clearest case: it was one card holding three field cards, each with its own border, background and padding ring, carrying no information the adjacency and shared heading did not already give. It is now one card with three divider-separated rows, and the form still wraps all three so password managers keep treating them as one credential change. Split CaptureSettings in two. Device selection and levels stay visible; the browser processing toggles and the quiet-audio reminders move to a new CaptureProcessingSettings behind the Advanced gate, since all three ship enabled, suit almost everyone and are touched only when something sounds wrong. The two identical gain sliders become one component, and the live input meter uses the block slot rather than a nested panel. Add settingsControls.ts as the shared control vocabulary. Inputs, selects, textareas and buttons were styled inline in every component, which is how seven different border radii accumulated; they now come from one definition at the design system's 8px control radius. Give GeneralSettings a sections prop. Its four sections no longer belong on one page: appearance, date and time and spellcheck sit under Appearance, while the processing defaults sit under Recording with the capture controls they explain. Correct the Advanced primitive to group sibling cards rather than wrap them. Wrapping produced exactly the card-inside-card nesting this work removes, because a category's advanced content is itself made of cards. Calendar connections, connected apps and calendar provider credentials adopt the card and the block slot. Users and invitations are re-homed and given the outer card only; their tables and forms keep their current interaction design. Retire keywords.ts, whose per-tab keyword blobs are superseded by the registry's per-setting keywords.
Move every remaining section onto the card-and-row model and delete SettingsSection, SettingsField and SettingsPanel. SettingsSection carried a width prop resolving to one of four maximum widths, applied inside a column that already constrained the content. Across the tabs that was 8 compact, 7 regular, 8 wide and 6 full, all left-aligned in the same column, which is why card edges never lined up. Width is now decided once per page by the category layout, so cards cannot disagree, and data-heavy pages declare full-bleed at page level. SettingsField and SettingsPanel each drew a border and a background, so every control sat in a card inside a card. Fields become rows separated by hairlines; panels become blocks or, where they group repeated items in the admin tables, a recessed inset that steps the same direction the card steps from the page. Give the admin tools their own card. Users, invitations, CLI usage, system logs and backup previously relied on chrome supplied by the Administration page that wrapped them, and would otherwise have rendered with no surface at all now that each has its own route. Their tables, consoles and wizards are otherwise untouched. Fix the last two surfaces stepping the wrong way in dark mode: the AI routing selection tiles and the CLI provider panel were darker than the card containing them. Border radii in the settings tree drop from seven values to four, all from the shared control and surface definitions.
The redesign renames the user-visible settings paths, so every reference in the documentation pointed somewhere that no longer exists. Rewrite them across ADMIN, USAGE, CALENDAR, TELEMETRY, DEPLOYMENT, MCP, CAPTURE, BACKUP_RESTORE, GETTING_STARTED, SECURITY, DEVELOPMENT, the CLI OAuth ADR, and the README. Several needed more than a rename. Language and Glossary moved to Transcription rather than following the rest of the old AI tab, notes structure moved to Notes and live assistance, and the calendar provider credentials joined the personal calendar connections under Integrations, which removes the hand-off the old copy had to describe. Document the structure itself in ADMIN.md: the fourteen categories, who sees each one, that categories are real routes with the old query links redirecting, and how the Advanced gate behaves. Repoint the two CLI OAuth plan documents at AiRoutingSection, since the AISettings orchestrator they linked to was split across the new AI routes. Delete the settings redesign plan now the work has landed.
Two problems reported from the redesigned pages. Padding. SettingsCard draws no padding of its own because rows and blocks own theirs, so any other content placed directly in a card body sat flush against the card edge. Add a fallback gutter for direct children that are not a row or a block, marked with a settings-cell class, so a caller cannot produce edge-to-edge content by forgetting to wrap it. Restructure the telemetry toggle, which was a bordered label spanning the full card width, into a proper row with a switch. Strip the leftover mx-auto max-w-* wrappers inside cards. These centred content inside an already-constrained card, which is the same width inside width that made the old sections ragged, one level down, and it left uneven gutters either side. System and logs froze while scrolling. Four compounding causes, all in the log viewer: LogLine was declared inside SystemTab, so it was a new component type on every render and React discarded and rebuilt every visible line each time a log arrived. Hoist it to module scope and memoise it. Each websocket message called setLogs with a fresh copy of the whole array, so a busy container cost one render and one full copy per line. Buffer arrivals in a ref and flush every 200ms, and cap the buffer at 2000 lines so neither memory nor the DOM grows without bound. filteredLogs was state written from an effect, which re-rendered the whole tab a second time for every change. It is derived with useMemo now. Auto-scroll wrote scrollTop on every batch regardless of where the user was, so scrolling back through history was repeatedly yanked to the bottom. It now follows new output only while the viewer is already at the bottom. The flush timer is also cleared with the socket, so it no longer survives navigating away.
Valtora
marked this pull request as ready for review
July 28, 2026 11:22
15 tasks
Valtora
added a commit
that referenced
this pull request
Jul 28, 2026
Bump docs/VERSION to 2.0.0 ahead of tagging. The release workflow fails fast if the pushed tag does not match this value exactly, so the bump has to land on main before v2.0.0 is created. A major bump is correct for this range. The voiceprint fix removes GET /speakers/voiceprints/method-status and POST /speakers/voiceprints/rebuild outright under a BREAKING CHANGE footer, replacing the operator trigger with scheduled maintenance, and the settings work moves every category onto its own route. The upgrade is also not pull and recreate alone: operators must remove the backup_temp mounts and the second recordings bind from their Compose file, because a stale mount reinstates the shared, permanent /tmp that this range fixes. There are no new Alembic revisions in v1.7.0..HEAD, so the upgrade carries no schema migration. The full release validation set passes locally at this commit: 1142 backend tests, frontend lint, 320 frontend unit tests, the production build, and the docs, Alembic and held-pin validators. All four release images (api, worker, worker-io, frontend) were built from 22219f2 and scanned with the release gate flags, meaning CRITICAL and HIGH, fixed findings only, against .trivyignore. All four pass, so no new .trivyignore entries are required. Refs: #151, #154, docs/DEPLOYMENT.md
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Pull Request
Description
Settings was six categories holding around thirty sections, unevenly distributed: AI held eleven sections and Administration held seven heavyweight tools. The AI category also rendered a different page depending on who opened it, because five of its sections were admin-only, so the same label meant different things to different users.
Alongside that, the visual inconsistency had three separate causes rather than one:
SettingsPagewrapped content inmx-auto max-w-4xl, then everySettingsSectionre-constrained itself with awidthprop resolving tomax-w-2xl,max-w-3xl,max-w-4xlormax-w-none. Across the tabs that was 8 compact, 7 regular, 8 wide and 6 full, all left-aligned in the same column, so card edges never lined up.dark:bg-gray-900, a sectiondark:bg-gray-950/90(darker than the page), a nested field panelgray-950/80(the same as its parent), and a nested subtle panelgray-900/70— lighter than the card containing it. Depth carried no consistent meaning.globals.cssdefines--surface-radius,--surface-paddingand--workspace-max-*, plus a compact-density variant. No settings component referenced any of them, so seven distinct border radii were in use and Settings was the only area that did not respond to compact density.What changed
Information architecture. Fourteen categories in four groups (General, Meetings, Data, About), organised by domain rather than by role. Admin-only categories live in the group their subject belongs to and are simply not shown to users who cannot see any of their content: admins see fourteen, everyone else nine.
Two naming collisions are resolved. Voice activity detection and speaker diarization move from Personal to Recording, beside the gain controls that explain the same symptoms, so "capture" no longer names two different things. The category for user accounts is Users and access, never People, which already means the contacts library.
AI splits into Your AI (routing and subscription connect, everyone) and AI providers (provider, models, credentials, admins).
AiRoutingSectionrenders for every user and contains the Claude/ChatGPT connect flow, so a single admin-only AI category would have removed a feature non-admins need — and would have produced an eight-section page for admins, reproducing the original problem elsewhere.Real routes per category, replacing
?tab=. Legacy values redirect:audio/companion/captureto/settings/recording,general/account/personalto/settings/profile,admin/administrationto/settings/users,aito/settings/your-ai. Six in-app links that used the old query form now point at the new routes directly.State hoisted to
settings/layout.tsx. This is required rather than tidy:useDebouncedAutosavecancels its pending timer on unmount without flushing, so a hook owned by a category page would silently discard an edit made within a second of navigating to another category. The layout survives navigation, so the debounce does too, and the session issues oneGET /settingsrather than one per category visited.Settings registry (
settingsRegistry.ts) as the manifest for every user-facing setting: category, access, keywords, Advanced criterion and shipped default. It backs cross-category search, the Advanced gate and the changed-count badge, replacing the per-tab keyword blobs.Advanced gate. One collapsible block per category, orthogonal to admin — admin answers who may change a setting, advanced answers how likely anyone is to need it. Conflating them would make the gate useless on a single-user install where the owner is the admin. A setting is gated if it has a safe default and is rarely changed, can silently degrade output if set wrong, needs external credentials, or only matters on a multi-user install — with one override: never gate a page's primary purpose. A floor rule prevents a page hiding all of its content. It auto-expands when a search result lives inside it, and shows a count when anything inside differs from its default. Seven of fourteen categories carry one.
Search resolves to individual settings rather than categories, and routes straight to them. Keystroke-driven tab switching is dropped; under real routes it would push a history entry per character.
Surface model. Two container levels, never three: one card per section, divider-separated rows inside, and a full-width block slot for composite UI (live meter, users table, log console). Surface tokens are declared in
globals.cssbeside the existing radius and padding tokens, so Settings now honours compact density.SettingsSection,SettingsFieldandSettingsPanelare deleted;settingsControls.tsis the shared control vocabulary. Radii in the settings tree drop from seven values to four.Mobile navigates by drill-in below
lg:/settingsrenders the grouped category list and each category opens as its own page. The previous six-chip horizontal strip does not scale to fourteen entries across four groups.Two fixes from review of the rendered pages
Card padding.
SettingsCarddraws no padding of its own because rows and blocks own theirs, so any other content placed directly in a card body sat flush against the card edge. Direct children that are not a row or a block now inherit a fallback gutter, so a caller cannot produce edge-to-edge content by forgetting to wrap it. Leftovermx-auto max-w-*wrappers inside cards are removed — the same width-inside-width that made the old sections ragged, one level down.System and logs froze while scrolling. Four compounding causes in the log viewer:
LogLinewas declared insideSystemTab, making it a new component type on every render, so React discarded and rebuilt every visible line each time a log arrived. Hoisted to module scope and memoised.setLogswith a fresh copy of the whole array. Arrivals are now buffered in a ref and flushed every 200ms, with the buffer capped at 2000 lines.filteredLogswas state written from an effect, re-rendering the whole tab a second time per change. It is derived withuseMemonow.scrollTopon every batch regardless of scroll position, so reading back through history was repeatedly yanked to the bottom. It now follows output only while the viewer is already at the bottom. The flush timer is cleared with the socket, so it no longer survives navigating away.Notes
No new dependencies. No backend changes. Forced password change is preserved and now enforced from the layout, so no route or stale bookmark can slip past it.
Accepted risk, stated plainly: the registry declares defaults in the frontend while the backend keeps its own inline (
ctx.merged_config.get("enable_vad", True)). A backend default changing without a matching registry update makes the changed-count badge wrong. Cosmetic, but real. Serving defaults from the API is the follow-up if it becomes a problem.Type of change
The breaking-change box is ticked for honesty about the user-visible surface: every settings path is renamed and the URL scheme changed. No API, database or configuration contract changed, and every previous entry point redirects.
Checks run
source .venv/bin/activate && pytest— 1122 passedscripts/check.py, which re-runs the backend suite already run above)cd frontend && npm run lint— cleancd frontend && npm run test— 52 files, 320 tests passedcd frontend && npm run build— compiled successfullypython3 scripts/validate_docs.py— 30 files validatedpython3 scripts/validate_alembic.py— heade1a7c93b8d24Also
git diff --check— no whitespace errors.Migration impact
Documentation impact
Every user-visible settings path in the documentation pointed somewhere that no longer exists. Rewritten across
ADMIN.md,USAGE.md,CALENDAR.md,TELEMETRY.md,DEPLOYMENT.md,MCP.md,CAPTURE.md,BACKUP_RESTORE.md,GETTING_STARTED.md,SECURITY.md,DEVELOPMENT.md, the CLI OAuth ADR and the README.Several needed more than a rename: Language and Glossary moved to Transcription rather than following the rest of the old AI tab, notes structure moved to Notes and live assistance, and the calendar provider credentials joined the personal calendar connections under Integrations, which removes the hand-off the old copy had to describe.
ADMIN.mdgains a Settings Structure section documenting the fourteen categories, who sees each, the routing and redirect behaviour, and how the Advanced gate works.Security impact
The forced-password-change lockout is preserved with its behaviour unchanged; only the redirect target moved from
/settings?tab=account&forcePasswordChange=1to/settings/profile, inAuthGuard.tsx,lib/api/client.ts, the login page, the OAuth authorize page and the setup wizard. Enforcement moved from the settings page component into the settings layout, so it now covers every category route rather than a single tab, and cannot be bypassed by a direct URL or a stale bookmark.docs/SECURITY.mdneeded a path reference update only; no boundary changed.Manual verification
Performed:
/settings/capturereturn 200 against a dev server.?tab=values render and resolve.Still pending, and the reason this is a draft: the capture settings UI changed shape.
frontend/src/lib/capture/is untouched and the write path throughuseCapture().updateSettingsis unchanged, but microphone selection, the gain sliders and the echo cancellation / noise suppression / browser auto gain toggles all moved — the last three from raw checkboxes toSwitchcomponents behind the Advanced gate. A wiring mistake there would degrade capture silently rather than visibly, so this needs a browser smoke pass before merge:Also worth eyeballing before merge: the new gutters on Your AI and Privacy, and scrolling
/settings/systemagainst a busy container to confirm the log viewer now stays responsive.New tests
settingsRegistry.test.ts(29 cases) guards the one failure mode with no visible symptom: a section imported by no page still builds, still lints and still passes every other test while rendering nowhere. It asserts that every field declared on theSettingsinterface is registered exactly once or explicitly listed as unsurfaced, that no category is left with nothing visible after gating, that every gated entry cites a criterion, that every category has a route, and that every legacy?tab=value resolves.Settingscarries an index signature, sokeyof Settingswidens tostringand no type-level assertion can enforce coverage — the test parses the interface out oftypes/index.tsinstead, with a sanity assertion on the parsed count so a regex that matched nothing could not make the coverage checks pass vacuously.settingsMetadata.test.tsis removed with the module it covered.settingsState.test.tsdrops the three search-scoring cases, whose behaviour no longer exists; that intent is covered by the registry search tests.