Conversation
| # Return preferences for any user — the signed-in user comes from DisableAuthentication | ||
| Avo.configuration.user_preferences = { | ||
| load: ->(user:, request:) { {"color_scheme" => "dark", "theme" => "slate", "accent_color" => "blue"} }, | ||
| save: ->(user:, request:, key:, value:, preferences:) { } |
There was a problem hiding this comment.
[rubocop] reported by reviewdog 🐶
[Corrected] Layout/SpaceInsideBlockBraces: Space inside empty braces detected.
| # "auto" is the default for color_scheme | ||
| Avo.configuration.user_preferences = { | ||
| load: ->(user:, request:) { {"color_scheme" => "auto"} }, | ||
| save: ->(user:, request:, key:, value:, preferences:) { } |
There was a problem hiding this comment.
[rubocop] reported by reviewdog 🐶
[Corrected] Layout/SpaceInsideBlockBraces: Space inside empty braces detected.
| it "logs the error and continues normally" do | ||
| Avo.configuration.user_preferences = { | ||
| load: ->(user:, request:) { raise "Database connection lost" }, | ||
| save: ->(user:, request:, key:, value:, preferences:) { } |
There was a problem hiding this comment.
[rubocop] reported by reviewdog 🐶
[Corrected] Layout/SpaceInsideBlockBraces: Space inside empty braces detected.
| it "accepts a hash with load and save lambdas" do | ||
| prefs = { | ||
| load: ->(user:, request:) { {} }, | ||
| save: ->(user:, request:, key:, value:, preferences:) { } |
There was a problem hiding this comment.
[rubocop] reported by reviewdog 🐶
[Corrected] Layout/SpaceInsideBlockBraces: Space inside empty braces detected.
| it "returns true when user_preferences is a hash" do | ||
| configuration.user_preferences = { | ||
| load: ->(user:, request:) { {} }, | ||
| save: ->(user:, request:, key:, value:, preferences:) { } |
There was a problem hiding this comment.
[rubocop] reported by reviewdog 🐶
[Corrected] Layout/SpaceInsideBlockBraces: Space inside empty braces detected.
| loaded_prefs = {color_scheme: "dark", theme: "slate"} | ||
| configuration.user_preferences = { | ||
| load: ->(user:, request:) { loaded_prefs }, | ||
| save: ->(user:, request:, key:, value:, preferences:) { } |
There was a problem hiding this comment.
[rubocop] reported by reviewdog 🐶
[Corrected] Layout/SpaceInsideBlockBraces: Space inside empty braces detected.
| before do | ||
| Avo.configuration.user_preferences = { | ||
| load: ->(user:, request:) { {} }, | ||
| save: ->(user:, request:, key:, value:, preferences:) { } |
There was a problem hiding this comment.
[rubocop] reported by reviewdog 🐶
[Corrected] Layout/SpaceInsideBlockBraces: Space inside empty braces detected.
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 2 potential issues.
Bugbot Autofix is OFF. To automatically fix reported issues with Cloud Agents, enable Autofix in the Cursor dashboard.
| const button = this.hasSaveButtonTarget ? this.saveButtonTarget : event.currentTarget | ||
| const preferences = this.collectPreferences() | ||
|
|
||
| button.disabled = true |
There was a problem hiding this comment.
Save button permanently disabled after first save attempt
High Severity
button.disabled = true is set at the start of the save() method but is never reset to false in any code path — not in the success handler, the error handler, showSaveSuccess, showSaveError, or a finally block. After the first save attempt (whether it succeeds or fails), the button remains permanently disabled. On error, the user can see the button but cannot retry. On success, if the user makes another change, the save wrapper slides back in but the button inside is still disabled and unclickable.
Additional Locations (1)
| // Collapse the button after showing success feedback | ||
| setTimeout(() => { | ||
| this.snapshotSavedState() | ||
| }, 1500) |
There was a problem hiding this comment.
Delayed snapshot captures unsaved changes as saved state
Medium Severity
After a successful save, snapshotSavedState() is called inside a 1500ms setTimeout. This method reads the current values of currentSchemeValue, currentThemeValue, and currentAccentValue at the time the timeout fires — not the values that were actually persisted. If the user changes a preference during that 1500ms window, the new unsaved value gets incorrectly recorded as the "saved" state, hiding the save button and making the user believe those changes were persisted.


Description
Makes the color settings persistent.
Checklist:
Screenshots & recording
Note
Medium Risk
Adds a new server-backed user-preferences API and request-time cookie syncing, which affects per-request behavior and introduces new persistence hooks. Risk is mitigated by opt-in configuration and key allowlisting, but bugs could impact UI theming and preference storage.
Overview
Enables optional persistence of UI color settings by introducing a configurable
user_preferencesinterface (hash callbacks or adapter object) plus support for custom preference keys.On each request,
BaseApplicationController#load_user_preferencesloads server preferences (when configured) and syncs them into cookies (removing defaults) to keep the existing cookie-driven theming while preventing FOUC.Adds
GET/PATCH /user_preferenceJSON endpoints to read/update allowed preference keys, and updates the color-scheme switcher to show a dirty-state “Save” button that PATCHes current cookie values and provides success/error feedback. Includes aDBConfigAdapter, dummy-app storage viausers.avo_preferences, and request/system specs covering load/sync/save flows.Written by Cursor Bugbot for commit 2d625e4. This will update automatically on new commits. Configure here.