Skip to content

✨ Add dynamic dirty-state tracking to preferences save bar (#796) - #799

Merged
ryan-winkler merged 2 commits into
latestfrom
fix/preferences-unsaved-state-796
Aug 16, 2026
Merged

✨ Add dynamic dirty-state tracking to preferences save bar (#796)#799
ryan-winkler merged 2 commits into
latestfrom
fix/preferences-unsaved-state-796

Conversation

@ryan-winkler

@ryan-winkler ryan-winkler commented Aug 15, 2026

Copy link
Copy Markdown

Summary

Fixes #796.

This pull request fixes the preferences save bar which previously displayed a false "Unsaved changes" warning when the form was unmodified. It also resolves a race condition in the season progress integration test.

Post-Mortem 1: Preferences Action Bar False Dirty State (#796)

  • Problem: When a user opens /settings/preferences, the page immediately shows "Unsaved changes. You have made changes to your preferences." before any user edits.
  • Root Cause: The action bar text was static HTML in src/templates/users/preferences.html. It had no client-side dirty-state logic.
  • Impact: Permanent warning banners cause alert fatigue, increase cognitive load (especially for neurodivergent ADHD/AuDHD users), and violate Nielsen Heuristic Add Rating sort for Custom Lists (descending, unrated last) #1 (Visibility of System Status).
  • Solution: Implemented client-side serialization and input state comparison using Alpine.js (formDirtyTracker) that dynamically updates the save bar between clean and dirty states.

Post-Mortem 2: Season Progress Integration Test Race Condition

  • Problem: In parallel test execution, test_season_progress_edit in src/app/tests/test_integration.py occasionally failed to find the updated episode tracking button locator after reloading.
  • Root Cause: The test clicked "Add" to submit an asynchronous HTMX episode save request and immediately asserted and reloaded the page before the HTTP request completed and the SQLite transaction committed.
  • Impact: Flaky integration test failure in parallel CI runners.
  • Solution: Wrapped the submission in Playwright's with self.page.expect_request(...) context manager to wait for the HTTP response before asserting and reloading.

Changes Made

  • Form Save Bar Component: Added src/templates/users/components/form_save_bar.html:
    • Serializes form inputs (ignoring CSRF token).
    • Maintains reactive isDirty state comparing live inputs with initial values.
    • Announces status changes via <div aria-live="polite" aria-atomic="true">.
    • Provides clear focus rings (focus-visible:ring-2 with --color-surface offset).
    • Retains button clickability so users are never locked out of saving.
  • Template Wiring: Replaced static markup in src/templates/users/preferences.html with the reusable save bar component and bound form listeners (@input, @change, @click).
  • Theme and Tokens:
    • Added semantic status tokens (--color-status-clean / --color-status-dirty) to input.css and compiled main.css.
    • Dark Theme: Emerald #10b981 (6.3:1 contrast) / Amber #f59e0b (5.8:1 contrast).
    • Light Theme: Emerald #047857 (5.2:1 contrast) / Amber #b45309 (4.2:1 contrast).
    • Added reduced-motion guard (@media (prefers-reduced-motion: reduce)).
  • Tests:
    • Added test_preferences_save_bar_renders_with_dirty_tracking_and_a11y in src/users/tests/views/test_preferences.py.
    • Stabilized test_season_progress_edit in src/app/tests/test_integration.py.

Validation

  • SECRET=test-only scripts/test.sh users.tests.views.test_preferences -> 10/10 passed
  • SECRET=test-only scripts/test.sh app.tests.test_integration -> 7/7 passed
  • SECRET=test-only scripts/test.sh -> 3876/3876 passed (fast suite, 0 failures)
  • uv run --no-sync ruff check src -> 0 errors
  • Interactive headless browser QA in both Dark and Light themes:
    • Clean initial load displays "All changes saved" / "Your preferences are up to date." with green status dot.
    • Modifying dropdowns/toggles switches dynamically to "Unsaved changes" with amber status dot.
    • Reverting modifications returns automatically to "All changes saved".
    • Form submit persists updates and redirects cleanly.

Contract Handoff

  • Domain guide regeneration/check outcome: Not applicable (no vocabulary or model schema changes)
  • Verified OpenAPI regeneration outcome: Not applicable (no API contract changes)
  • Contract-test outcome: Not applicable

Human Review

  • Pending human review.

Gstack QA

  • Completed — /design-review scored 98/100 (A+); /review passed with PR Quality Score 10/10.

Migration Sync Gate

  • Not applicable (no database models or migrations changed).

Notes

Ryan Winkler added 2 commits August 16, 2026 00:23
* Replace static unsaved changes copy with dynamic dirty tracking component
* Add accessible theme status tokens and live-region announcements
* Add regression test coverage for save bar rendering and accessibility

Fixes #796
* Wait for episode save HTTP response before page reload and assertions
* Prevent flaky failures under parallel test execution in CI
@ryan-winkler
ryan-winkler merged commit be48200 into latest Aug 16, 2026
9 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[BUG] Preferences page always shows "Unsaved changes" before any edit

1 participant