Move all display/UX settings from browser localStorage to server-side
settings.json. Flat keys approach (Approach A) - adds 10 new keys to
DEFAULT_SETTINGS, requiring zero changes to existing load/save/patch
functions or API endpoints.
Key decisions:
- No federation settings sync (each server owns its own settings)
- Drop notificationPermission (browser API is source of truth)
- sidebarOpen defaults to None for auto-detect on first load
- No migration needed - users reset preferences
- Fix auto_open vs auto_open_created naming alignment
Verified compatible with muxplex v0.2.0 (1b5207b).
9.8 KiB
Consolidate Settings Server-Side Design
Goal
Move all user-configurable settings from browser localStorage to server-side ~/.config/muxplex/settings.json, eliminating the split storage model. After this change, the only remaining localStorage usage is tmux-web-device-id (per-browser device identity for the heartbeat system, not a setting).
Background
Muxplex currently splits settings across two storage layers:
- Server-side (
~/.config/muxplex/settings.json): Infrastructure and session behavior settings (host, port, auth, sort_order, federation config, etc.) -- 17 keys total. - Client-side (browser localStorage): Display/UX preferences stored across 3 keys:
muxplex.display-- fontSize, gridColumns, bellSound, hoverPreviewDelay, viewMode, showDeviceBadges, showHoverPreview, activityIndicator, gridViewMode, notificationPermissionmuxplex.sidebarOpen-- sidebar toggle statetmux-web-device-id-- per-browser device identity (not a setting)
This split complicates the mental model and means display preferences don't roam with the server -- if you reinstall your browser or clear localStorage, your preferences are lost even though the server is untouched.
Approach
Flat keys (Approach A) -- add 10 new keys directly as flat top-level entries in DEFAULT_SETTINGS in settings.py.
This requires zero changes to load_settings(), save_settings(), or patch_settings() -- they all iterate DEFAULT_SETTINGS keys, so new keys work automatically through the existing GET/PATCH /api/settings API.
Alternatives Considered
- Nested
displaysub-object: Would require deep-merge logic inpatch_settings()and frontend changes topatchServerSetting(). More code for the same result. Rejected. - Separate
display.jsonfile: New file, new API endpoints, new load/save functions. Over-engineered for a single-user tmux UI. Rejected.
Architecture
Multi-Device / Federation Settings Model
Each muxplex server owns its own settings.json. Federation is for session aggregation, not config replication. No settings sync across servers.
When browsing through Server A (the aggregator), all sessions -- local and federated -- render using Server A's display settings. Server B's settings are irrelevant; it only provides raw session data and WebSocket proxying. Server B's settings only matter when someone browses directly to Server B, which is a separate context where different settings may be desirable (e.g., different font size for different monitors).
Syncing would introduce disproportionate complexity (conflict resolution, partial connectivity, auth scope expansion, bootstrap problem) for minimal UX benefit in a single-user tool.
Data Flow
Browser ──GET /api/settings──> Server A (settings.json)
│ │
│ _serverSettings cache │ fontSize: 16, gridColumns: 3, ...
│ (in-memory, immediate) │ + all infra/session settings
│ │
│──PATCH /api/settings──> │ patch_settings() merges into file
│ (fire-and-forget) │
│ │
│ ├── Local tmux sessions
│ ├── Server B sessions (federation proxy)
│ └── Server C sessions (federation proxy)
│
│ All sessions render using Server A's display settings.
│ Server B/C settings are irrelevant from this browser.
Components
Server-Side Changes (muxplex/settings.py)
Add 10 new keys to DEFAULT_SETTINGS:
# Display preferences (consumed by frontend)
"fontSize": 14,
"hoverPreviewDelay": 1500,
"gridColumns": "auto",
"bellSound": False,
"viewMode": "auto",
"showDeviceBadges": True,
"showHoverPreview": True,
"activityIndicator": "both",
"gridViewMode": "flat",
# UI state
"sidebarOpen": None, # None = auto-detect from screen width
sidebarOpen defaults to None (not True/False) so the frontend can distinguish "never set by user" from "explicitly set" and fall back to screen-width detection (open at >= 960px, closed below) on first load.
No changes needed to:
load_settings(),save_settings(),patch_settings()-- they iterateDEFAULT_SETTINGSkeys automatically.muxplex/main.py--GET/PATCH /api/settingsendpoints already handle any keys inDEFAULT_SETTINGS. None of the new keys need redaction (no sensitive data).
Intentionally excluded: notificationPermission is NOT added. The browser Notification.permission API is the source of truth; the cached localStorage value was never meaningfully consumed.
Frontend Changes (muxplex/frontend/app.js)
Startup Sequence
Move await loadServerSettings() to the top of DOMContentLoaded, before first render. Currently it's called lazily when opening the settings panel. Display settings like fontSize and gridColumns affect rendering, so they must be available before anything renders (~50ms on localhost; page is blank until restoreState anyway).
Replace the Storage Layer
- Delete
DISPLAY_SETTINGS_KEYconstant ('muxplex.display') andSIDEBAR_KEYconstant ('muxplex.sidebarOpen'). - Delete
loadDisplaySettings()function (reads from localStorage). - Delete
saveDisplaySettings()function (writes to localStorage). - Add
getDisplaySettings()function that reads from_serverSettingscache, falls back toDISPLAY_DEFAULTSfor anything missing or before the initial fetch. - Remove
notificationPermissionfromDISPLAY_DEFAULTS(keep the runtime_notificationPermissionvariable for the browser API state).
Update All Callsites
- All ~16
loadDisplaySettings()calls becomegetDisplaySettings()(pure rename). - All ~6
saveDisplaySettings(ds)calls becomepatchServerSetting(key, value)calls with an in-memory_serverSettingscache update so the UI stays responsive without waiting for the server round-trip.
Sidebar Functions
Rewrite initSidebar(), toggleSidebar(), and bindSidebarClickAway():
- Read
_serverSettings.sidebarOpeninstead of localStorage. - Write via
patchServerSetting('sidebarOpen', isOpen)(fire-and-forget, non-blocking). - Keep the
None-> screen-width auto-detect logic for first-time users.
Bonus Fix: auto_open -> auto_open_created
The openSettings() function reads the server setting as ss.auto_open but the canonical server-side key is auto_open_created. Fix this alignment as part of this work since we're already touching that code path.
Only Remaining localStorage Usage
tmux-web-device-id in initDeviceId() (3 occurrences) -- untouched. This is per-browser device identity for the heartbeat/bell system, not a user setting.
Error Handling
Race on Startup
loadServerSettings() is awaited before first render, so display settings are always available. If the fetch fails (server down, network issue), getDisplaySettings() falls back to DISPLAY_DEFAULTS -- the app still loads with sensible defaults, just not personalized.
Fire-and-Forget Writes
Setting changes call patchServerSetting() which updates the in-memory _serverSettings cache immediately (so the UI is responsive), then fires the PATCH /api/settings request. If the PATCH fails, a "Failed to save setting" toast appears. The in-memory cache stays updated for the current session, but the change won't persist across page refreshes. This matches the current behavior for existing server settings.
sidebarOpen: None Auto-Detect
On first load (or after a settings reset), sidebarOpen is None. The frontend detects this and uses screen-width heuristic (open at >= 960px, closed below). Once the user explicitly toggles the sidebar, it writes true/false and the auto-detect is bypassed from then on.
No Migration
No migration of existing localStorage values. Users get defaults and set preferences again. Old localStorage keys (muxplex.display, muxplex.sidebarOpen) become orphans -- harmless, ignored by the new code.
Testing Strategy
Server-Side Tests (muxplex/tests/test_settings.py)
- One test verifying the 10 new keys exist in
DEFAULT_SETTINGSwith correct default values. - One test verifying display keys round-trip through
patch_settings()(write then read back). - Existing test infrastructure for
load_settings/save_settingsalready covers the mechanics.
Frontend Tests (muxplex/frontend/tests/test_app.mjs)
- Update any existing tests that reference
loadDisplaySettingsorsaveDisplaySettingsto usegetDisplaySettings. - Mock
_serverSettingsinstead oflocalStoragefor display settings tests.
Verification Checklist (manual, post-implementation)
grep -r 'localStorage' app.jsreturns onlytmux-web-device-idreferences.grep -r 'DISPLAY_SETTINGS_KEY\|SIDEBAR_KEY\|muxplex\.display\|muxplex\.sidebarOpen' app.jsreturns zero matches.grep -r 'loadDisplaySettings\|saveDisplaySettings' app.jsreturns zero matches.pytest muxplex/tests/test_settings.pypasses.- Manual: change font size -> refresh -> persists.
- Manual: toggle sidebar -> refresh -> persists.
cat ~/.config/muxplex/settings.jsonshows the new display keys.
Open Questions
None -- all design decisions have been validated.
Future Work
- Federation settings sync: Investigate syncing user preferences (fontSize, sort_order, etc.) across federated servers. Deferred -- each server owns its own settings for now. Standard sysadmin solutions (copy the file, ansible, dotfiles repo) work fine in the meantime.
Compatibility
Verified against muxplex v0.2.0 (commit 1b5207b). The v0.2.0 release added a DELETE /api/federation/{remote_id}/sessions/{session_name} endpoint and updated killSession() -- neither affects settings storage. DEFAULT_SETTINGS in settings.py is stable at 17 keys; none of the new display keys conflict with existing keys.