Skip to content

Theme preference follows the browser, not the operator #198

Description

@eth-man

The light/dark choice is stored in localStorage under nexuspuppet.theme and nowhere else. It therefore belongs to a browser rather than to a person: an operator who sets dark on their laptop gets the default on the jump host, in a private window, or after clearing site data — and on a shared console, whoever changed it last has changed it for everyone using that machine.

ThemePreference is already 'light' | 'dark' | 'system' (apps/web/src/providers/theme-provider.tsx), so the vocabulary exists. What is missing is a home for it on the user.

The constraint that shapes this

localStorage cannot simply be replaced by the database.

THEME_BOOTSTRAP is an inline script that runs before paint and sets data-theme on <html>. It has to decide synchronously — there is no API round trip available at that point in the document. Move the preference to the server and read it after hydration, and every page load flashes the wrong theme first, which is precisely the bug that bootstrap exists to prevent (and its comment says so).

So the shape is not "move it", it is:

  • The user record becomes the source of truth — what an operator gets on a machine they have never used.
  • localStorage stays as a cache, primed from the user record on login and written through on change, so the pre-paint script keeps a synchronous answer.

Getting that backwards, or dropping the cache, trades a small annoyance for a flash on every page load.

What to build

  • theme on User, defaulting to system — a new account should follow the machine until somebody chooses
  • A field on the existing account contract, and a small self-service route: any authenticated user may set their own, so it must not require users:manage
  • The provider writes through to the API on change and updates localStorage in the same step
  • On login, the user's stored preference overwrites the cache — this is the whole point, and it is the one case where the server must win
  • THEME_BOOTSTRAP unchanged. If a change to it looks necessary, the design has gone wrong

Edge cases worth deciding

  • A browser with site data blocked. The bootstrap already catches and defaults to dark. With a server preference, the user still gets a flash on every load because nothing can be cached. Acceptable, but it should be a known consequence rather than a surprise.
  • Logging out. Clearing the cache on logout returns a shared machine to the default, which is arguably correct; leaving it means the next person inherits the last one's theme. Worth choosing deliberately.
  • A theme change made in another tab or on another machine. Not worth reconciling live. Last write wins, and the next login settles it.

Why this has sat unbuilt

It has been on the session task list since the console overhaul (#72) and is the only item there with no issue behind it, so nothing in the tracker kept surfacing it while replication, notifications, hybrid auth and compile receipts were being built.

Filed so it stops falling off. It is also a legitimate wontfix: a preference that survives in the browser is not obviously worth a schema change, and the honest counter-argument is that operators mostly use one machine.

Activity

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    needs-triageMaintainer needs to evaluate this issue

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions