Skip to content

feat(settings): keep settings with the account so they follow it across devices - #436

Merged
joszz merged 1 commit into
mainfrom
feat/synced-settings
Oct 5, 2026
Merged

joszz merged 1 commit into
mainfrom
feat/synced-settings

Conversation

@joszz

@joszz joszz commented Oct 5, 2026 •

Copy link
Copy Markdown
Owner

Pull Request

Summary

Theme, language, units, the dashboard layout and the map's layers and filters were stored per browser, so every phone and computer had to be set up separately. They now belong to the signed-in account. Built on 2026-09-27 and recovered from a stash: it never reached main.

  • Server. New UserSettings table (migration AddUserSettings), one JSON object per account per section (ui, dashboard, map). GET /api/settings, PATCH /api/settings/{section}. Saves merge key by key with a version column and a randomised retry, so devices saving at once keep each other's keys; 409 settings.conflict if one keeps losing. Accounts are keyed by a SHA-256 of sign-in method + subject: no name or email stored, and password and provider accounts of the same name stay apart. Server accepts only the known sections, plain keys and 16 KB per section.
  • Browser. persistSettings hands each store to the new settingsSync store. The router guard reads the account's copy alongside the session check, and no protected view mounts before it is taken in. Each local change sends only the changed keys; other devices pick changes up when they come back into view (at most every 30 s) or back online. An account with no copy is filled from the first browser that signs in, so upgrading loses nothing.
  • Per device on purpose: the notification checklist, because it decides whether that browser is subscribed to push.
  • Vehicle type defaults are applied in VehicleTypeSetting instead of a dashboard watcher, so an override synced from another device cannot reapply defaults over its cards.

Changes since the stash: ported onto main (one conflict in settingsDashboard.ts, now using main's storedCards()), migration regenerated so it sorts after DropRawTopicIndex, the per-device notification checklist (localKeys) with a test, and three .env.example lines that still said "per browser".

Type of Change

  • Bug fix
  • New feature
  • Refactor / cleanup
  • Documentation
  • Dependency update
  • Other

Related Issues

None.

Checklist

  • Tests added or updated (27 backend incl. end-to-end in AuthenticationFlowTests, 12 frontend sync tests plus router and vehicle-type setting tests)
  • Linting passes (pnpm lint, pnpm type-check, dotnet build, dotnet format --verify-no-changes, no pending model changes)
  • No secrets or personal data committed
  • Responsive on mobile (no layout change; checked 390 and 1440 px, no horizontal overflow)
  • i18n keys added for any new UI strings (none needed)
  • If unraid/garagestack.xml changed (it did not)

Verification

  • 793 backend tests (on main with feat(telemetry): fold telemetry older than a year into quarter-hour rows #435) and 763 frontend tests pass.
  • PostgreSQL 18: eight devices saving one section at the same moment all succeed (8/8), both before the account has a row and after; all 16 keys kept.
  • Two browsers in demo mode: theme and distance unit changed on the desktop arrive on the phone after it comes back online and survive a reload; the phone's notification checklist stays its own, and the desktop's save never carries it.
  • Entry JS grows from 80.27 to 81.25 KB gzip; CSS unchanged.

Merge note

Rebased onto main after #435. The only conflict was the two repository registrations in ServiceCollectionExtensions.cs. AddUserSettings was regenerated (20261005120400) so it follows AddTelemetryCompactedUntil; its body is unchanged.

🤖 Generated with Claude Code

…ss devices

Theme, language, units, the dashboard layout and the map's layers and filters were stored per
browser, so every phone and computer had to be set up on its own. They now belong to the
signed-in account.

- Server: a UserSettings table (migration AddUserSettings) holds one JSON object per account per
  section (ui, dashboard, map), which the server does not interpret beyond the section list, key
  names and a 16 KB cap. GET /api/settings and PATCH /api/settings/{section}; a save merges key
  by key with a version column and a randomised retry, so devices saving at the same moment keep
  each other's keys, and answers 409 settings.conflict when it keeps losing. Accounts are keyed by
  a SHA-256 of the sign-in method and subject, so no name or email is stored and a password
  account never shares settings with a provider account of the same name.
- Browser: persistSettings hands each store to the new settingsSync store. The router guard reads
  the account's copy alongside the session check and mounts no protected view before it is taken
  in. Each local write sends only the keys that changed; the page reads again when it comes back
  into view (at most every 30 s) or back online. An account with no copy yet is filled from the
  first browser that signs in. Each browser keeps its own copy, so pages open instantly and the
  login page keeps the theme and language.
- The notification checklist stays per device: it decides whether that browser is subscribed to
  push, so unticking everything on one computer must not unsubscribe a phone.
- Applying the vehicle type's card defaults moved from a dashboard watcher into
  VehicleTypeSetting, so an override arriving from another device cannot reapply defaults over
  its cards.

Checked on PostgreSQL 18: eight devices saving one section at the same moment all succeed, both
before the account has a row and after. Two-browser check in demo mode: theme and units follow,
the notification checklist does not. The entry chunk grows by 1 KB gzip.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@joszz
joszz force-pushed the feat/synced-settings branch from 1d92cee to ace370b Compare October 5, 2026 12:05
@joszz
joszz merged commit f3a87e3 into main Oct 5, 2026
22 checks passed
@joszz
joszz deleted the feat/synced-settings branch October 5, 2026 12:07
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.

1 participant