Skip to content

feature: persistent color settings#4268

Open
adrianthedev wants to merge 1 commit into4-devfrom
feature/persistent-color-settings
Open

feature: persistent color settings#4268
adrianthedev wants to merge 1 commit into4-devfrom
feature/persistent-color-settings

Conversation

@adrianthedev
Copy link
Collaborator

@adrianthedev adrianthedev commented Feb 16, 2026

Description

Makes the color settings persistent.

Checklist:

  • I have performed a self-review of my own code
  • I have commented my code, particularly in hard-to-understand areas
  • I have made corresponding changes to the documentation
  • I have added tests that prove my fix is effective or that my feature works

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_preferences interface (hash callbacks or adapter object) plus support for custom preference keys.

On each request, BaseApplicationController#load_user_preferences loads 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_preference JSON 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 a DBConfigAdapter, dummy-app storage via users.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.

# 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:) { }
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[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:) { }
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[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:) { }
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[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:) { }
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[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:) { }
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[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:) { }
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[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:) { }
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[rubocop] reported by reviewdog 🐶
[Corrected] Layout/SpaceInsideBlockBraces: Space inside empty braces detected.

Copy link

@cursor cursor bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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)

Fix in Cursor Fix in Web

// Collapse the button after showing success feedback
setTimeout(() => {
this.snapshotSavedState()
}, 1500)
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Fix in Cursor Fix in Web

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

Comments