From 3bb3944b52f6f751a44cd2f4817b0a9191eb820f Mon Sep 17 00:00:00 2001 From: Brian Krabach Date: Wed, 8 Apr 2026 09:24:12 -0700 Subject: [PATCH] docs: add design for consolidating settings server-side 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). --- .../2026-04-08-server-side-settings-design.md | 175 ++++++++++++++++++ 1 file changed, 175 insertions(+) create mode 100644 docs/plans/2026-04-08-server-side-settings-design.md diff --git a/docs/plans/2026-04-08-server-side-settings-design.md b/docs/plans/2026-04-08-server-side-settings-design.md new file mode 100644 index 0000000..e4e6ee9 --- /dev/null +++ b/docs/plans/2026-04-08-server-side-settings-design.md @@ -0,0 +1,175 @@ +# 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, notificationPermission + - `muxplex.sidebarOpen` -- sidebar toggle state + - `tmux-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 `display` sub-object**: Would require deep-merge logic in `patch_settings()` and frontend changes to `patchServerSetting()`. More code for the same result. Rejected. +- **Separate `display.json` file**: 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`: + +```python +# 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 iterate `DEFAULT_SETTINGS` keys automatically. +- `muxplex/main.py` -- `GET/PATCH /api/settings` endpoints already handle any keys in `DEFAULT_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_KEY` constant (`'muxplex.display'`) and `SIDEBAR_KEY` constant (`'muxplex.sidebarOpen'`). +- **Delete** `loadDisplaySettings()` function (reads from localStorage). +- **Delete** `saveDisplaySettings()` function (writes to localStorage). +- **Add** `getDisplaySettings()` function that reads from `_serverSettings` cache, falls back to `DISPLAY_DEFAULTS` for anything missing or before the initial fetch. +- **Remove** `notificationPermission` from `DISPLAY_DEFAULTS` (keep the runtime `_notificationPermission` variable for the browser API state). + +#### Update All Callsites + +- All ~16 `loadDisplaySettings()` calls become `getDisplaySettings()` (pure rename). +- All ~6 `saveDisplaySettings(ds)` calls become `patchServerSetting(key, value)` calls with an in-memory `_serverSettings` cache update so the UI stays responsive without waiting for the server round-trip. + +#### Sidebar Functions + +Rewrite `initSidebar()`, `toggleSidebar()`, and `bindSidebarClickAway()`: + +- Read `_serverSettings.sidebarOpen` instead 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 `await`ed 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_SETTINGS` with correct default values. +- One test verifying display keys round-trip through `patch_settings()` (write then read back). +- Existing test infrastructure for `load_settings`/`save_settings` already covers the mechanics. + +### Frontend Tests (`muxplex/frontend/tests/test_app.mjs`) + +- Update any existing tests that reference `loadDisplaySettings` or `saveDisplaySettings` to use `getDisplaySettings`. +- Mock `_serverSettings` instead of `localStorage` for display settings tests. + +### Verification Checklist (manual, post-implementation) + +1. `grep -r 'localStorage' app.js` returns only `tmux-web-device-id` references. +2. `grep -r 'DISPLAY_SETTINGS_KEY\|SIDEBAR_KEY\|muxplex\.display\|muxplex\.sidebarOpen' app.js` returns zero matches. +3. `grep -r 'loadDisplaySettings\|saveDisplaySettings' app.js` returns zero matches. +4. `pytest muxplex/tests/test_settings.py` passes. +5. Manual: change font size -> refresh -> persists. +6. Manual: toggle sidebar -> refresh -> persists. +7. `cat ~/.config/muxplex/settings.json` shows 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.