fix(theme): apply per-user theme_pref on login, persist toggle to server

alpine-init.js resolved the theme purely from localStorage, so it never
reflected the logged-in user's theme_pref: switching user kept the previous
user's theme because localStorage persists per-browser.

- base.html injects window.__SERVER_THEME from the session theme (empty when
  anonymous, preserving OS-preference behaviour on the login page).
- alpine-init.js now treats the server value as authoritative for logged-in
  users (server > localStorage > OS > light) and syncs localStorage to it.
- The navbar toggle now persists the choice via POST /auth/set-theme, which
  updates the session and the user's theme_pref, so it survives navigation
  and matches on the next login.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Adriano Dal Pastro
2026-06-05 08:32:11 +00:00
parent 2eb5c51353
commit 5cc5123a0c
3 changed files with 57 additions and 2 deletions
@@ -16,9 +16,21 @@
/**
* Resolve the initial dark-mode state.
* Priority: localStorage > system preference > light (default).
* Priority: server (per-user theme_pref) > localStorage > system preference > light.
*
* The server value (window.__SERVER_THEME) is authoritative for logged-in
* users: it reflects the theme_pref of whoever is currently authenticated,
* so switching user actually switches theme instead of inheriting the
* previous user's localStorage value. It is empty for anonymous pages.
*/
function getInitialDark() {
var server = (typeof window !== 'undefined' && window.__SERVER_THEME) || '';
if (server === 'dark' || server === 'light') {
// Keep localStorage in sync so the rest of the app stays consistent.
try { localStorage.setItem(STORAGE_KEY, server); } catch (_) {}
return server === 'dark';
}
var stored = null;
try {
stored = localStorage.getItem(STORAGE_KEY);
@@ -70,6 +82,7 @@
toggle: function () {
this.dark = !this.dark;
this._apply();
this._persist();
},
set: function (dark) {
@@ -77,6 +90,24 @@
this._apply();
},
// Persist an explicit user choice to the server (session + theme_pref),
// so it survives navigation and matches on the next login. Best-effort.
_persist: function () {
try {
var csrf = document.querySelector('meta[name=csrf-token]');
fetch('/auth/set-theme', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRFToken': (csrf && csrf.content) || ''
},
body: JSON.stringify({ theme: this.dark ? 'dark' : 'light' })
}).catch(function () {});
} catch (_) {
// Non-fatal: localStorage already holds the choice for this browser.
}
},
_apply: function () {
if (this.dark) {
document.documentElement.classList.add('dark');