feat: dynamic settings UI with auto-discovery and basic/advanced toggle#600
feat: dynamic settings UI with auto-discovery and basic/advanced toggle#600
Conversation
…anced toggle Add a schema-driven settings UI that auto-discovers all 50 settings across 9 namespaces from the backend registry. No frontend changes needed when new settings are added to the backend. New files: - API endpoint module (settings.ts) wrapping GET/PUT/DELETE endpoints - Pinia store (useSettingsStore) with generation-counter stale-response protection and localStorage-persisted basic/advanced toggle - SettingField component with type-appropriate inputs (text, number, toggle, select, textarea, password), client-side validation from schema constraints, dirty tracking, and per-setting save/reset - SettingGroupRenderer component grouping settings by group field with basic/advanced level filtering - SettingSourceBadge and SettingRestartBadge display components - 8 test files (64 new tests) including property-based tests Refactored SettingsPage replaces static Company tab with dynamic namespace tabs, adds basic/advanced toggle in header, preserves custom Providers card UI with dynamic settings below, and keeps User tab unchanged. Closes #454 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Pre-reviewed by 5 agents, 10 findings addressed: - CLAUDE.md: add settings/ to components and stores package structure - resetSetting: add unwrapVoid to check error envelope on DELETE - validateSettingValue: use fullmatch semantics (^(?:pattern)$) for regex validation to match backend re.fullmatch() - validateSettingValue: accept "1", "0", and case variants for bool to match backend _check_default_bool() - SettingField: add aria-label on password visibility toggle button - Tests: fix silent-pass eye-toggle test (assert button exists) - Tests: assert disabled is absent in save-enabled test - Tests: add 19 unit tests for validateSettingValue (float, bool, enum, JSON, pattern branches) - Tests: add resetSetting error propagation test Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Dependency Review✅ No vulnerabilities or license issues or OpenSSF Scorecard issues found.Scanned FilesNone |
|
Note Reviews pausedIt looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
WalkthroughIntroduces a dynamic settings feature: new API endpoint wrappers and TypeScript types for settings; a Pinia settings store with validation, fetch/update/reset actions, optimistic updates, and persisted basic/advanced toggle; Vue components for editing and grouping settings (SettingField, SettingGroupRenderer, SettingSourceBadge, SettingRestartBadge); updates to SettingsPage to render namespace-driven tabs and integrate the store; new constants for namespace ordering and labels; and many Vitest tests and PrimeVue test mocks covering endpoints, store logic, components, and page behavior. 🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. 📝 Coding Plan
Comment |
Summary of ChangesHello, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed! This pull request significantly enhances the application's settings management by introducing a fully dynamic and extensible UI. The new system automatically adapts to backend setting definitions, providing tailored input fields and client-side validation without requiring manual frontend updates. This change streamlines the process of adding or modifying settings, improves user experience with intuitive controls, and ensures data integrity through robust validation. Highlights
Using Gemini Code AssistThe full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips. Invoking Gemini You can request assistance from Gemini at any point by creating a comment using either
Customization To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a Limitations & Feedback Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for GitHub and other Google products, sign up here. Footnotes
|
There was a problem hiding this comment.
Code Review
This pull request introduces a robust and dynamic settings UI with auto-discovery, type-appropriate inputs, and basic/advanced toggling. The implementation is well-structured, leveraging Pinia for state management and comprehensive client-side validation. The addition of property-based tests using fast-check significantly enhances the reliability of the settings store. Overall, the changes are well-engineered, thoroughly tested, and greatly improve the configurability of the application.
There was a problem hiding this comment.
Actionable comments posted: 6
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@web/src/__tests__/components/settings/SettingSourceBadge.test.ts`:
- Around line 13-53: Replace the repetitive individual it tests in the
SettingSourceBadge suite with a parameterized it.each table that iterates over
rows like [{ source: 'db', label: 'Database', severity: 'info' }, { source:
'env', label: 'Environment', severity: 'warn' }, { source: 'yaml', label:
'YAML', severity: 'secondary' }, { source: 'default', label: 'Default',
severity: 'contrast' }], and for each row mount SettingSourceBadge with
mount(SettingSourceBadge, { props: { source } }) then assert wrapper.text()
contains label and wrapper.find(`[data-severity="${severity}"]`).exists() is
true; keep the test suite name and component reference (SettingSourceBadge)
intact.
In `@web/src/stores/settings.ts`:
- Around line 143-146: The toggleAdvanced function currently writes to
localStorage without a guard; wrap the call to
localStorage.setItem(SETTINGS_ADVANCED_KEY, String(showAdvanced.value)) in a
try/catch so errors in restricted environments are swallowed or logged, keeping
the state flip (showAdvanced.value = !showAdvanced.value) intact; reference the
toggleAdvanced function, showAdvanced ref, and SETTINGS_ADVANCED_KEY when making
this change.
- Line 69: Guard the direct localStorage access used to initialize showAdvanced
(which currently calls localStorage.getItem(SETTINGS_ADVANCED_KEY) === 'true')
by wrapping the getItem call in a try/catch and defaulting to false on error;
update the initialization of showAdvanced to compute the boolean inside the
try/catch and use that value for ref(...). Also ensure any code that later
writes the same SETTINGS_ADVANCED_KEY (e.g., setters or watchers that update
localStorage when showAdvanced changes) uses try/catch to avoid throwing in
restricted contexts.
- Around line 50-58: The current client-side validation builds a RegExp from
validator_pattern and runs .test(value), which can be expensive for long inputs;
add an input-length guard before constructing/using the regex: define a sensible
MAX_VALIDATION_INPUT_LENGTH constant and, inside the block where
validator_pattern and value are validated (the code referencing
validator_pattern and value), if value.length exceeds the limit either skip the
regex test or return a concise validation error (e.g., "Input too long to
validate"), then only construct new RegExp(`^(?:${validator_pattern})$`) and
call .test(value) when value.length is within the limit; keep the existing
try/catch around RegExp construction to handle invalid patterns.
In `@web/src/views/SettingsPage.vue`:
- Line 207: The conditional update handler on TabView currently uses
"auth.mustChangePassword ? undefined : (activeTab = $event)" which returns
undefined but does not stop PrimeVue from visually switching tabs; remove this
ternary and restore a simple setter for the `@update`:value event (i.e., set
activeTab = $event) and rely on each TabPanel's :disabled binding
(auth.mustChangePassword) to prevent navigation; update the TabView's
`@update`:value handler to just assign activeTab and ensure TabPanel :disabled
still references auth.mustChangePassword.
- Around line 196-200: The ToggleSwitch handler currently calls
settingsStore.toggleAdvanced() and ignores the emitted boolean; update the event
binding so the emitted value from `@update`:model-value is passed into the store
(e.g., call settingsStore.toggleAdvanced(value) or create/use a setter like
settingsStore.setAdvanced(value)) so the store receives the explicit new boolean
from ToggleSwitch instead of always flipping state; update the usage of
ToggleSwitch, the settingsStore method signature (toggleAdvanced) or add
setAdvanced to accept the boolean, and ensure the template binding forwards the
emitted value.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Repository UI (base), Organization UI (inherited)
Review profile: ASSERTIVE
Plan: Pro
Run ID: c629eda1-cb46-45ec-a40f-941703b52f69
📒 Files selected for processing (19)
CLAUDE.mdweb/src/__tests__/api/settings.test.tsweb/src/__tests__/components/settings/SettingField.test.tsweb/src/__tests__/components/settings/SettingGroupRenderer.test.tsweb/src/__tests__/components/settings/SettingRestartBadge.test.tsweb/src/__tests__/components/settings/SettingSourceBadge.test.tsweb/src/__tests__/stores/settings.property.test.tsweb/src/__tests__/stores/settings.test.tsweb/src/__tests__/views/SettingsPage.dynamic.test.tsweb/src/__tests__/views/SettingsPage.test.tsweb/src/api/endpoints/settings.tsweb/src/api/types.tsweb/src/components/settings/SettingField.vueweb/src/components/settings/SettingGroupRenderer.vueweb/src/components/settings/SettingRestartBadge.vueweb/src/components/settings/SettingSourceBadge.vueweb/src/stores/settings.tsweb/src/utils/constants.tsweb/src/views/SettingsPage.vue
| <ToggleSwitch | ||
| :model-value="settingsStore.showAdvanced" | ||
| @update:model-value="settingsStore.toggleAdvanced()" | ||
| /> | ||
| <span>Advanced</span> |
There was a problem hiding this comment.
🧹 Nitpick | 🔵 Trivial
ToggleSwitch event handler ignores emitted value.
@update:model-value provides the new boolean value, but toggleAdvanced() ignores it and simply flips the current state. This works correctly since it's a toggle, but directly passing the value would be more explicit.
♻️ Optional: Use emitted value directly
You could update the store's toggleAdvanced to accept an optional value, or use a setter:
- `@update`:model-value="settingsStore.toggleAdvanced()"
+ `@update`:model-value="(val) => { settingsStore.showAdvanced = val; localStorage.setItem(SETTINGS_ADVANCED_KEY, String(val)) }"Or keep the toggle pattern and acknowledge this is intentional design.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@web/src/views/SettingsPage.vue` around lines 196 - 200, The ToggleSwitch
handler currently calls settingsStore.toggleAdvanced() and ignores the emitted
boolean; update the event binding so the emitted value from `@update`:model-value
is passed into the store (e.g., call settingsStore.toggleAdvanced(value) or
create/use a setter like settingsStore.setAdvanced(value)) so the store receives
the explicit new boolean from ToggleSwitch instead of always flipping state;
update the usage of ToggleSwitch, the settingsStore method signature
(toggleAdvanced) or add setAdvanced to accept the boolean, and ensure the
template binding forwards the emitted value.
web/src/views/SettingsPage.vue
Outdated
| <ErrorBoundary :error="companyStore.configError ?? providerStore.error" @retry="retryFetch"> | ||
| <ErrorBoundary :error="companyStore.configError ?? providerStore.error ?? settingsStore.error" @retry="retryFetch"> | ||
| <LoadingSkeleton v-if="loading" :lines="6" /> | ||
| <TabView v-else :value="activeTab" @update:value="auth.mustChangePassword ? undefined : (activeTab = $event)"> |
There was a problem hiding this comment.
🧹 Nitpick | 🔵 Trivial
Clarify tab change prevention logic.
The expression auth.mustChangePassword ? undefined : (activeTab = $event) returns undefined when password change is required, but this doesn't prevent PrimeVue's TabView from visually switching tabs. The :disabled prop on TabPanels (Line 214) is the actual mechanism preventing navigation.
Consider simplifying since the disabled state already handles restriction:
♻️ Simplified handler
- <TabView v-else :value="activeTab" `@update`:value="auth.mustChangePassword ? undefined : (activeTab = $event)">
+ <TabView v-else :value="activeTab" `@update`:value="activeTab = $event">The :disabled prop on each TabPanel already prevents interaction when auth.mustChangePassword is true.
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| <TabView v-else :value="activeTab" @update:value="auth.mustChangePassword ? undefined : (activeTab = $event)"> | |
| <TabView v-else :value="activeTab" `@update`:value="activeTab = $event"> |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@web/src/views/SettingsPage.vue` at line 207, The conditional update handler
on TabView currently uses "auth.mustChangePassword ? undefined : (activeTab =
$event)" which returns undefined but does not stop PrimeVue from visually
switching tabs; remove this ternary and restore a simple setter for the
`@update`:value event (i.e., set activeTab = $event) and rely on each TabPanel's
:disabled binding (auth.mustChangePassword) to prevent navigation; update the
TabView's `@update`:value handler to just assign activeTab and ensure TabPanel
:disabled still references auth.mustChangePassword.
Critical: - Migrate TabView (deprecated) to Tabs component (PrimeVue 4 API) -- fixes broken tab state sync, URL params, and tab lock logic - Convert shared mutable test fixtures to factory functions Major: - Narrow namespace params from string to SettingNamespace in store + endpoints - Log post-update/reset refresh failures instead of silent swallow - Surface retryFetch errors in ErrorBoundary instead of console-only - Add aria-labels to ToggleSwitch components (accessibility) - Make enum_values readonly string[] to match backend immutability - Document and validate backend max_length=8192 constraint Medium: - Add ReDoS input length guard for regex validation - Guard localStorage access with try/catch for restricted contexts - Log invalid validator_pattern regex with console.warn - Refactor SettingSourceBadge tests to use it.each - Extract shared PrimeVue test mocks to __mocks__/primevue.ts Tests: - Add partial-success tests (update/reset ok, refresh fails) - Add formatJson button behavior tests - Add Reset disabled state tests for non-db sources - Add watch re-sync test when entry prop changes - Add handleSave guard test when validation fails - Add max-length validation tests Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Actionable comments posted: 1
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
web/src/__tests__/views/SettingsPage.test.ts (1)
23-98: 🧹 Nitpick | 🔵 TrivialConsolidate duplicated PrimeVue mocks using the shared mock registrar.
This file redefines mocks already centralized in
web/src/__tests__/__mocks__/primevue.ts. The divergence (notably in interactive behavior) can make tests inconsistent across suites.♻️ Suggested refactor
+import { registerPrimeVueMocks } from '@/__tests__/__mocks__/primevue' + +registerPrimeVueMocks(vi) + vi.mock('primevue/tabs', () => ({ default: { template: '<div><slot /></div>' }, })) @@ -vi.mock('primevue/inputtext', () => ({ - default: { - props: ['modelValue', 'type', 'placeholder'], - template: '<input :type="type" :placeholder="placeholder" />', - }, -})) - -vi.mock('primevue/button', () => ({ - default: { - props: ['label', 'icon', 'type', 'size', 'loading', 'disabled', 'severity', 'text'], - template: '<button :disabled="disabled">{{ label }}</button>', - }, -})) - -vi.mock('primevue/toggleswitch', () => ({ - default: { - props: ['modelValue', 'disabled'], - emits: ['update:modelValue'], - template: '<button role="switch">{{ modelValue }}</button>', - }, -})) - -vi.mock('primevue/inputnumber', () => ({ - default: { - props: ['modelValue', 'min', 'max', 'minFractionDigits', 'maxFractionDigits', 'useGrouping', 'disabled'], - template: '<input type="number" />', - }, -})) - -vi.mock('primevue/select', () => ({ - default: { - props: ['modelValue', 'options', 'disabled'], - template: '<select></select>', - }, -})) - -vi.mock('primevue/textarea', () => ({ - default: { - props: ['modelValue', 'rows', 'disabled'], - template: '<textarea></textarea>', - }, -})) - -vi.mock('primevue/tag', () => ({ - default: { - props: ['value', 'severity'], - template: '<span>{{ value }}</span>', - }, -}))🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@web/src/__tests__/views/SettingsPage.test.ts` around lines 23 - 98, Tests redefine many PrimeVue component mocks (e.g., the vi.mock calls for 'primevue/tabs', 'primevue/inputtext', 'primevue/button', 'primevue/toggleswitch', etc.) that are already centralized in web/src/__tests__/__mocks__/primevue.ts; remove these duplicated vi.mock(...) blocks from SettingsPage.test.ts and instead load the shared registrar by importing or requiring the centralized mock module (e.g., add a top-level import of web/src/__tests__/__mocks__/primevue or replace the individual vi.mock calls with a single vi.mock('primevue', () => require('web/src/__tests__/__mocks__/primevue'))), so all tests use the same mocked behavior.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@web/src/stores/settings.ts`:
- Around line 52-58: The current JSON validation uses JSON.parse(value) which
accepts primitives (strings, numbers, booleans, null) as valid JSON; if you need
to restrict settings to only objects/arrays, parse the value then verify the
parsed result is a non-null object (Array.isArray(...) or typeof parsed ===
'object' && parsed !== null) and return the error 'Must be valid JSON' when the
parsed value is a primitive; update the validation branch that checks type ===
'json' (using the variables type and value in settings.ts) to perform this extra
check after parsing.
---
Outside diff comments:
In `@web/src/__tests__/views/SettingsPage.test.ts`:
- Around line 23-98: Tests redefine many PrimeVue component mocks (e.g., the
vi.mock calls for 'primevue/tabs', 'primevue/inputtext', 'primevue/button',
'primevue/toggleswitch', etc.) that are already centralized in
web/src/__tests__/__mocks__/primevue.ts; remove these duplicated vi.mock(...)
blocks from SettingsPage.test.ts and instead load the shared registrar by
importing or requiring the centralized mock module (e.g., add a top-level import
of web/src/__tests__/__mocks__/primevue or replace the individual vi.mock calls
with a single vi.mock('primevue', () =>
require('web/src/__tests__/__mocks__/primevue'))), so all tests use the same
mocked behavior.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Repository UI (base), Organization UI (inherited)
Review profile: ASSERTIVE
Plan: Pro
Run ID: 8b8dd9ba-d361-4c65-a0a4-6fa7f6e3c02c
📒 Files selected for processing (13)
web/src/__tests__/__mocks__/primevue.tsweb/src/__tests__/api/settings.test.tsweb/src/__tests__/components/settings/SettingField.test.tsweb/src/__tests__/components/settings/SettingGroupRenderer.test.tsweb/src/__tests__/components/settings/SettingSourceBadge.test.tsweb/src/__tests__/stores/settings.test.tsweb/src/__tests__/views/SettingsPage.dynamic.test.tsweb/src/__tests__/views/SettingsPage.test.tsweb/src/api/endpoints/settings.tsweb/src/api/types.tsweb/src/components/settings/SettingField.vueweb/src/stores/settings.tsweb/src/views/SettingsPage.vue
Primitives (strings, numbers, booleans, null) are valid JSON but have their own dedicated setting types (str, int, float, bool). JSON settings are for structural data only. Also removes non-functional registerPrimeVueMocks helper -- vi.mock() hoisting prevents using imported objects in factory scope. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Actionable comments posted: 4
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@web/src/__tests__/stores/settings.test.ts`:
- Around line 158-172: Add an advanced-only namespace fixture to the test by
creating another mock definition (e.g., mockAdvancedOnlyDefinition) whose
definitions all have level: 'advanced', include it in the mocked
settingsApi.getSchema response alongside mockSecurityDefinition, mockDefinition,
and mockAdvancedDefinition, then call useSettingsStore() and await
store.fetchAll(); assert that store.namespaces does not include the
advanced-only namespace while store.showAdvanced is false (optionally also
assert it appears when you set store.showAdvanced = true and refresh if needed).
Ensure you reference the existing test helpers and methods (mock...Definition
symbols, settingsApi.getSchema, useSettingsStore, store.fetchAll,
store.namespaces, store.showAdvanced) so the new fixture integrates with the
current test setup.
In `@web/src/stores/settings.ts`:
- Around line 147-160: After deleting the override in resetSetting (where
settingsApi.resetSetting is called) you currently swallow failures from the
subsequent settingsApi.getAllSettings call which leaves entries showing stale
resolved values; change the flow so that if the refresh fails you do not report
success—either rethrow/propagate the refresh error (so callers see failure) or
proactively update entries to remove/invalidate the specific namespace/key entry
(using entries and generation) before returning; ensure you still clear
savingKey and maintain generation consistency so the UI cannot display an
authoritative stale entry after reset.
- Around line 95-98: The computed namespaces currently ignores the showAdvanced
filter so namespaces with only advanced settings still appear; update the
computed named namespaces to build the present set from schema.value filtered by
the showAdvanced state (e.g., include a definition only if showAdvanced.value is
true or def.level !== 'advanced'), then map to namespaces and apply
NAMESPACE_ORDER.filter as before; reference the computed symbol namespaces, the
reactive schema, and the showAdvanced ref when making this change.
- Around line 128-141: In updateSetting, use the SettingEntry returned by
settingsApi.updateSetting(namespace, key, { value }) to immediately update
entries (and generation if needed) before performing the best-effort refresh;
specifically, after the await of settingsApi.updateSetting(...) apply the
returned entry to the entries reactive list (and clear savingKey) so the store
reflects the saved value even if the subsequent settingsApi.getAllSettings()
refresh fails, and keep the existing generation check/gen increment logic to
avoid races.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Repository UI (base), Organization UI (inherited)
Review profile: ASSERTIVE
Plan: Pro
Run ID: fb258bce-4ff5-4c43-895b-8b4cc8b9ffc3
📒 Files selected for processing (3)
web/src/__tests__/__mocks__/primevue.tsweb/src/__tests__/stores/settings.test.tsweb/src/stores/settings.ts
- Filter namespaces by showAdvanced -- hide tabs for namespaces with only advanced settings in basic mode - Use returned SettingEntry from updateSetting API to immediately update entries, avoiding stale UI when refresh fails - Optimistically revert entry to default on reset so the UI reflects the deleted DB override even when refresh fails - Add test for advanced-only namespace filtering - Update partial-success tests to verify optimistic entry updates Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Actionable comments posted: 2
♻️ Duplicate comments (2)
web/src/stores/settings.ts (2)
19-20: 🧹 Nitpick | 🔵 TrivialThe regex length guard is currently unreachable.
Line 19 rejects values above 8192 chars, so the Line 64 guard at 10,000 never executes. Either lower
MAX_VALIDATION_INPUT_LENGTHto<= MAX_SETTING_VALUE_LENGTHor remove the dead branch.Also applies to: 64-64
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@web/src/stores/settings.ts` around lines 19 - 20, The regex length guard is unreachable because MAX_VALIDATION_INPUT_LENGTH exceeds MAX_SETTING_VALUE_LENGTH; update the validation so the shorter limit is enforced first: either set MAX_VALIDATION_INPUT_LENGTH to a value <= MAX_SETTING_VALUE_LENGTH or remove the redundant branch that checks MAX_VALIDATION_INPUT_LENGTH in the same validation flow (look for the constants MAX_SETTING_VALUE_LENGTH and MAX_VALIDATION_INPUT_LENGTH and the validation logic that returns `Value must be at most ${MAX_SETTING_VALUE_LENGTH} characters` and the later regex-length guard) so only one consistent maximum is applied.
134-139:⚠️ Potential issue | 🟡 MinorOptimistic update should upsert when the entry is missing.
At Line 137, the
map-only replacement is a no-op if the target key is absent inentries. In that case, a successful PUT can still leave stale UI when the follow-up refresh fails.Proposed fix
try { const updatedEntry = await settingsApi.updateSetting(namespace, key, { value }) // Immediately apply the returned entry so the UI reflects the change const gen = ++generation - entries.value = entries.value.map((e) => - e.definition.namespace === namespace && e.definition.key === key ? updatedEntry : e, - ) + const idx = entries.value.findIndex( + (e) => e.definition.namespace === namespace && e.definition.key === key, + ) + if (idx >= 0) { + entries.value[idx] = updatedEntry + } else { + entries.value.push(updatedEntry) + }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@web/src/stores/settings.ts` around lines 134 - 139, The optimistic update currently replaces entries via entries.value.map(...) after calling settingsApi.updateSetting(namespace, key, { value }) but does nothing if the entry is missing; change the logic that applies updatedEntry (and uses the incremented generation) to perform an upsert: search entries.value for an item matching e.definition.namespace === namespace && e.definition.key === key and if found replace it with updatedEntry, otherwise append/insert updatedEntry into entries.value so the UI reflects the successful PUT even when the entry wasn't previously present; keep the existing generation increment and use the same updatedEntry result from settingsApi.updateSetting.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@web/src/__tests__/stores/settings.test.ts`:
- Around line 298-303: Replace hardcoded localStorage key strings with the
shared constant SETTINGS_ADVANCED_KEY in the tests: update the assertions that
call localStorage.getItem('settings_show_advanced') to use
localStorage.getItem(SETTINGS_ADVANCED_KEY) so they stay in sync with the
implementation; do this for the assertions surrounding store.toggleAdvanced()
and store.showAdvanced in the failing test blocks (also apply the same change to
the similar assertions at lines noted in the comment).
- Around line 243-247: The test currently only mocks settingsApi.getAllSettings
once, but store.updateSetting(...) triggers a post-update refresh so the second
getAllSettings call is undefined; update the test to mock getAllSettings for
both calls (e.g., use
vi.mocked(settingsApi.getAllSettings).mockResolvedValueOnce(initialData) and
.mockResolvedValueOnce(refreshedData) or mockResolvedValue sequence) and ensure
the mocked settingsApi.updateSetting promise (resolveUpdate) is used to simulate
completion before asserting; apply the same dual-getAllSettings mocking fix to
the other test block that covers lines 251-256.
---
Duplicate comments:
In `@web/src/stores/settings.ts`:
- Around line 19-20: The regex length guard is unreachable because
MAX_VALIDATION_INPUT_LENGTH exceeds MAX_SETTING_VALUE_LENGTH; update the
validation so the shorter limit is enforced first: either set
MAX_VALIDATION_INPUT_LENGTH to a value <= MAX_SETTING_VALUE_LENGTH or remove the
redundant branch that checks MAX_VALIDATION_INPUT_LENGTH in the same validation
flow (look for the constants MAX_SETTING_VALUE_LENGTH and
MAX_VALIDATION_INPUT_LENGTH and the validation logic that returns `Value must be
at most ${MAX_SETTING_VALUE_LENGTH} characters` and the later regex-length
guard) so only one consistent maximum is applied.
- Around line 134-139: The optimistic update currently replaces entries via
entries.value.map(...) after calling settingsApi.updateSetting(namespace, key, {
value }) but does nothing if the entry is missing; change the logic that applies
updatedEntry (and uses the incremented generation) to perform an upsert: search
entries.value for an item matching e.definition.namespace === namespace &&
e.definition.key === key and if found replace it with updatedEntry, otherwise
append/insert updatedEntry into entries.value so the UI reflects the successful
PUT even when the entry wasn't previously present; keep the existing generation
increment and use the same updatedEntry result from settingsApi.updateSetting.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Repository UI (base), Organization UI (inherited)
Review profile: ASSERTIVE
Plan: Pro
Run ID: 156719f6-e79b-434f-8382-d5a2ca50b6b6
📒 Files selected for processing (2)
web/src/__tests__/stores/settings.test.tsweb/src/stores/settings.ts
| vi.mocked(settingsApi.getSchema).mockResolvedValue([mockDefinition]) | ||
| vi.mocked(settingsApi.getAllSettings).mockResolvedValue([mockEntry]) | ||
| vi.mocked(settingsApi.updateSetting).mockReturnValue( | ||
| new Promise((r) => { resolveUpdate = r }), | ||
| ) |
There was a problem hiding this comment.
Mock getAllSettings for the post-update refresh in this test.
store.updateSetting(...) performs a refresh call after updateSetting. In this case, only the initial getAllSettings is mocked, so the second call is implicit/undefined and weakens test reliability.
Proposed fix
vi.mocked(settingsApi.getSchema).mockResolvedValue([mockDefinition])
- vi.mocked(settingsApi.getAllSettings).mockResolvedValue([mockEntry])
+ vi.mocked(settingsApi.getAllSettings)
+ .mockResolvedValueOnce([mockEntry]) // initial fetchAll
+ .mockResolvedValueOnce([mockEntry]) // refresh inside updateSettingAlso applies to: 251-256
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@web/src/__tests__/stores/settings.test.ts` around lines 243 - 247, The test
currently only mocks settingsApi.getAllSettings once, but
store.updateSetting(...) triggers a post-update refresh so the second
getAllSettings call is undefined; update the test to mock getAllSettings for
both calls (e.g., use
vi.mocked(settingsApi.getAllSettings).mockResolvedValueOnce(initialData) and
.mockResolvedValueOnce(refreshedData) or mockResolvedValue sequence) and ensure
the mocked settingsApi.updateSetting promise (resolveUpdate) is used to simulate
completion before asserting; apply the same dual-getAllSettings mocking fix to
the other test block that covers lines 251-256.
…n tests - Remove unreachable MAX_VALIDATION_INPUT_LENGTH (10k) -- the MAX_SETTING_VALUE_LENGTH (8192) check already rejects longer values before the regex branch is reached - Change updateSetting optimistic apply from map-only to upsert so newly created settings are appended if not already in the array - Replace hardcoded 'settings_show_advanced' strings in tests with SETTINGS_ADVANCED_KEY constant import Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@web/src/__tests__/stores/settings.test.ts`:
- Around line 380-506: The test suite is missing coverage for integer-specific
validation in validateSettingValue; add new tests (using the existing helper
makeDef and pattern in this file) for type: 'int' that assert acceptance of
valid integers (e.g., "42"), rejection of decimals like "3.5", rejection of
empty string, and enforcement of min_value and max_value boundaries (e.g.,
min_value: 0 rejects "-1", max_value: 100 rejects "200"); place these alongside
the other type-specific tests so integer behavior and Number.isInteger()
semantics are verified.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Repository UI (base), Organization UI (inherited)
Review profile: ASSERTIVE
Plan: Pro
Run ID: 2080f614-5909-4df0-bce8-60c5b8e1153d
📒 Files selected for processing (2)
web/src/__tests__/stores/settings.test.tsweb/src/stores/settings.ts
Covers valid integers, decimal rejection (Number.isInteger), empty string, non-numeric input, min/max boundary enforcement, and boundary value acceptance. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
♻️ Duplicate comments (1)
web/src/stores/settings.ts (1)
155-175:⚠️ Potential issue | 🟠 MajorDon't report a guessed
defaultentry as the post-reset truth.
settingsApi.resetSetting()returnsvoid, so this path does not know the resolved value/source untilgetAllSettings()succeeds. If that refresh fails, the store still resolves withdefinition.default,source: 'default', and the oldupdated_atpreserved from{ ...e }, even though the comment here already notes that YAML/env may override the default. That leaves the UI showing an authoritative-looking value that can be wrong.♻️ One safe option: require a successful refresh before reporting success
async function resetSetting(namespace: SettingNamespace, key: string): Promise<void> { savingKey.value = `${namespace}/${key}` try { await settingsApi.resetSetting(namespace, key) - // Optimistically revert entry to default since the DB override is deleted const gen = ++generation - entries.value = entries.value.map((e) => { - if (e.definition.namespace === namespace && e.definition.key === key) { - return { ...e, value: e.definition.default ?? '', source: 'default' as const } - } - return e - }) - // Best-effort full refresh to get actual resolved values (env/yaml may override default) - try { - const entriesData = await settingsApi.getAllSettings() - if (gen === generation) { - entries.value = entriesData - } - } catch (refreshErr) { - console.warn('Settings refresh failed after reset:', refreshErr) + const entriesData = await settingsApi.getAllSettings() + if (gen === generation) { + entries.value = entriesData } } finally { savingKey.value = null } }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@web/src/stores/settings.ts` around lines 155 - 175, The resetSetting function is prematurely mutating entries.value to a guessed default after calling settingsApi.resetSetting; instead, stop optimistically replacing the entry with { ...e, value: e.definition.default, source: 'default' } and only update entries.value from the authoritative settingsApi.getAllSettings() result; keep savingKey and generation handling, call settingsApi.getAllSettings() after reset, and if it succeeds (gen === generation) assign entries.value = entriesData, otherwise do not modify the existing entry (or restore the prior entry) and surface the refresh failure; reference resetSetting, generation, entries.value, settingsApi.resetSetting and settingsApi.getAllSettings when making the change.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Duplicate comments:
In `@web/src/stores/settings.ts`:
- Around line 155-175: The resetSetting function is prematurely mutating
entries.value to a guessed default after calling settingsApi.resetSetting;
instead, stop optimistically replacing the entry with { ...e, value:
e.definition.default, source: 'default' } and only update entries.value from the
authoritative settingsApi.getAllSettings() result; keep savingKey and generation
handling, call settingsApi.getAllSettings() after reset, and if it succeeds (gen
=== generation) assign entries.value = entriesData, otherwise do not modify the
existing entry (or restore the prior entry) and surface the refresh failure;
reference resetSetting, generation, entries.value, settingsApi.resetSetting and
settingsApi.getAllSettings when making the change.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Repository UI (base), Organization UI (inherited)
Review profile: ASSERTIVE
Plan: Pro
Run ID: defeaa4c-1379-413d-9f37-79f533e2b5f0
📒 Files selected for processing (2)
web/src/__tests__/stores/settings.test.tsweb/src/stores/settings.ts
🤖 I have created a release *beep* *boop* --- ## [0.3.8](v0.3.7...v0.3.8) (2026-03-19) ### Features * dynamic settings UI with auto-discovery and basic/advanced toggle ([#600](#600)) ([11b32b7](11b32b7)) ### Bug Fixes * **ci:** remove dst: . from GoReleaser archive config ([#598](#598)) ([c8bf862](c8bf862)) * **engine:** wire compaction_callback and plan_execute_config through auto-selection ([#601](#601)) ([795327f](795327f)) * harden setup wizard completion and status checks ([#616](#616)) ([d99d7b7](d99d7b7)) ### Maintenance * add /codebase-audit skill for deep parallel codebase auditing ([#613](#613)) ([db02320](db02320)) --- This PR was generated with [Release Please](https://github.com/googleapis/release-please). See [documentation](https://github.com/googleapis/release-please#release-please).
Summary
New files (14)
web/src/api/endpoints/settings.ts-- API endpoint module (getSchema, getAllSettings, updateSetting, resetSetting)web/src/stores/settings.ts-- Pinia store with generation-counter stale-response protection, exportedvalidateSettingValueweb/src/components/settings/SettingField.vue-- Single setting input with dirty tracking, save/reset, validationweb/src/components/settings/SettingGroupRenderer.vue-- Namespace renderer grouping bygroupfield with level filteringweb/src/components/settings/SettingSourceBadge.vue-- Value origin indicator (db/env/yaml/default)web/src/components/settings/SettingRestartBadge.vue-- Restart-required warning badgeModified files (4)
web/src/api/types.ts-- Added SettingDefinition, SettingEntry, UpdateSettingRequest typesweb/src/utils/constants.ts-- Added NAMESPACE_ORDER, NAMESPACE_DISPLAY_NAMES, SETTINGS_ADVANCED_KEYweb/src/views/SettingsPage.vue-- Refactored: dynamic namespace tabs, basic/advanced toggle, SettingGroupRenderer integrationCLAUDE.md-- Updated Package Structure (settings/ components + settings store)Test plan
Closes #454
🤖 Generated with Claude Code
Summary by CodeRabbit