refactor: reorganize settings — merge Notifications into Sessions, move device name to Display, remove view scope

This commit is contained in:
Brian Krabach
2026-03-31 11:53:25 -07:00
parent 3707283baf
commit 3de9e98de8
3 changed files with 120 additions and 49 deletions
+3 -28
View File
@@ -1593,36 +1593,23 @@ function applyDisplaySettings(ds) {
} }
/** /**
* Load grid view mode preference based on viewPreferenceScope in display settings. * Load grid view mode preference from display settings (localStorage).
* When scope is 'local' (default), reads gridViewMode from display settings (localStorage).
* When scope is 'server', reads grid_view_mode from _serverSettings.
* Returns 'flat' as default. * Returns 'flat' as default.
* @returns {string} * @returns {string}
*/ */
function loadGridViewMode() { function loadGridViewMode() {
var ds = loadDisplaySettings(); var ds = loadDisplaySettings();
var scope = ds.viewPreferenceScope || 'local';
if (scope === 'server') {
return (_serverSettings && _serverSettings.grid_view_mode) || 'flat';
}
return ds.gridViewMode || 'flat'; return ds.gridViewMode || 'flat';
} }
/** /**
* Save grid view mode preference to the appropriate scope and update _gridViewMode. * Save grid view mode preference to display settings (localStorage) and update _gridViewMode.
* When scope is 'local', saves gridViewMode to display settings (localStorage).
* When scope is 'server', patches the server setting via patchServerSetting.
* @param {string} mode - The grid view mode to save. * @param {string} mode - The grid view mode to save.
*/ */
function saveGridViewMode(mode) { function saveGridViewMode(mode) {
var ds = loadDisplaySettings(); var ds = loadDisplaySettings();
var scope = ds.viewPreferenceScope || 'local';
if (scope === 'server') {
patchServerSetting('grid_view_mode', mode);
} else {
ds.gridViewMode = mode; ds.gridViewMode = mode;
saveDisplaySettings(ds); saveDisplaySettings(ds);
}
_gridViewMode = mode; _gridViewMode = mode;
} }
@@ -1693,8 +1680,6 @@ function openSettings() {
if (gridColumnsEl) gridColumnsEl.value = String(settings.gridColumns); if (gridColumnsEl) gridColumnsEl.value = String(settings.gridColumns);
const viewModeEl = $('setting-view-mode'); const viewModeEl = $('setting-view-mode');
if (viewModeEl) viewModeEl.value = loadGridViewMode(); if (viewModeEl) viewModeEl.value = loadGridViewMode();
const viewScopeEl = $('setting-view-scope');
if (viewScopeEl) viewScopeEl.value = settings.viewPreferenceScope || 'local';
// Populate Notifications tab from display settings // Populate Notifications tab from display settings
const bellSoundEl = $('setting-bell-sound'); const bellSoundEl = $('setting-bell-sound');
@@ -2225,17 +2210,7 @@ function bindStaticEventListeners() {
renderGrid(_currentSessions || []); renderGrid(_currentSessions || []);
} }
}); });
on($('setting-view-scope'), 'change', function() {
var el = $('setting-view-scope');
if (!el) return;
var newScope = el.value;
var currentMode = _gridViewMode;
var ds = loadDisplaySettings();
ds.viewPreferenceScope = newScope;
saveDisplaySettings(ds);
// Migrate current mode to new scope
saveGridViewMode(currentMode);
});
// Sessions settings — bind change events for server-side persistence // Sessions settings — bind change events for server-side persistence
on($('setting-default-session'), 'change', function() { on($('setting-default-session'), 'change', function() {
+4 -14
View File
@@ -88,7 +88,6 @@
<nav class="settings-tabs"> <nav class="settings-tabs">
<button class="settings-tab settings-tab--active" data-tab="display">Display</button> <button class="settings-tab settings-tab--active" data-tab="display">Display</button>
<button class="settings-tab" data-tab="sessions">Sessions</button> <button class="settings-tab" data-tab="sessions">Sessions</button>
<button class="settings-tab" data-tab="notifications">Notifications</button>
<button class="settings-tab" data-tab="new-session">Commands</button> <button class="settings-tab" data-tab="new-session">Commands</button>
<button class="settings-tab" data-tab="devices">Multi-Device</button> <button class="settings-tab" data-tab="devices">Multi-Device</button>
</nav> </nav>
@@ -124,6 +123,10 @@
<option value="4">4</option> <option value="4">4</option>
</select> </select>
</div> </div>
<div class="settings-field">
<label class="settings-label" for="setting-device-name">Device Name</label>
<input type="text" id="setting-device-name" class="settings-input" placeholder="This device" />
</div>
</div> </div>
<div class="settings-panel hidden" data-tab="sessions"> <div class="settings-panel hidden" data-tab="sessions">
<div class="settings-field"> <div class="settings-field">
@@ -152,8 +155,6 @@
<label class="settings-label" for="setting-auto-open">Auto-open created sessions</label> <label class="settings-label" for="setting-auto-open">Auto-open created sessions</label>
<input type="checkbox" id="setting-auto-open" class="settings-checkbox" checked /> <input type="checkbox" id="setting-auto-open" class="settings-checkbox" checked />
</div> </div>
</div>
<div class="settings-panel hidden" data-tab="notifications">
<div class="settings-field"> <div class="settings-field">
<label class="settings-label" for="setting-bell-sound">Bell Sound</label> <label class="settings-label" for="setting-bell-sound">Bell Sound</label>
<input type="checkbox" id="setting-bell-sound" class="settings-checkbox" /> <input type="checkbox" id="setting-bell-sound" class="settings-checkbox" />
@@ -186,10 +187,6 @@
<input type="checkbox" id="setting-multi-device-enabled" class="settings-checkbox" /> <input type="checkbox" id="setting-multi-device-enabled" class="settings-checkbox" />
</div> </div>
<div id="multi-device-fields"> <div id="multi-device-fields">
<div class="settings-field">
<label class="settings-label" for="setting-device-name">Device Name</label>
<input type="text" id="setting-device-name" class="settings-input" placeholder="This device" />
</div>
<div class="settings-field"> <div class="settings-field">
<label class="settings-label" for="setting-view-mode">View Mode</label> <label class="settings-label" for="setting-view-mode">View Mode</label>
<select id="setting-view-mode" class="settings-select"> <select id="setting-view-mode" class="settings-select">
@@ -198,13 +195,6 @@
<option value="filtered">Filtered</option> <option value="filtered">Filtered</option>
</select> </select>
</div> </div>
<div class="settings-field">
<label class="settings-label" for="setting-view-scope">View Scope</label>
<select id="setting-view-scope" class="settings-select">
<option value="local">Local</option>
<option value="server">Server</option>
</select>
</div>
<div class="settings-field settings-field--column"> <div class="settings-field settings-field--column">
<label class="settings-label">Remote Instances</label> <label class="settings-label">Remote Instances</label>
<div id="setting-remote-instances" class="settings-remote-list"></div> <div id="setting-remote-instances" class="settings-remote-list"></div>
+111 -5
View File
@@ -2936,14 +2936,15 @@ test('loadGridViewMode reads from localStorage when scope is local', () => {
_localStorageStore = {}; _localStorageStore = {};
}); });
test('loadGridViewMode reads from serverSettings when scope is server', () => { test('loadGridViewMode reads from localStorage regardless of viewPreferenceScope', () => {
// Set display settings with viewPreferenceScope 'server', server has grid_view_mode 'filtered' // viewPreferenceScope is no longer used — loadGridViewMode always reads from localStorage
_localStorageStore = {}; _localStorageStore = {};
_localStorageStore['muxplex.display'] = JSON.stringify({ viewPreferenceScope: 'server' }); _localStorageStore['muxplex.display'] = JSON.stringify({ viewPreferenceScope: 'server', gridViewMode: 'filtered' });
app._setServerSettings({ grid_view_mode: 'filtered' }); app._setServerSettings({ grid_view_mode: 'grouped' });
const mode = app.loadGridViewMode(); const mode = app.loadGridViewMode();
assert.strictEqual(mode, 'filtered', 'loadGridViewMode should return grid_view_mode from serverSettings when scope is server'); // Must return localStorage value ('filtered'), NOT server value ('grouped')
assert.strictEqual(mode, 'filtered', 'loadGridViewMode should always return gridViewMode from localStorage');
// Cleanup // Cleanup
_localStorageStore = {}; _localStorageStore = {};
@@ -3711,3 +3712,108 @@ test('applyFitLayout does NOT measure DOM dimensions (pure arithmetic)', () => {
'applyFitLayout must NOT set inline tile heights — CSS grid 1fr rows handle sizing'); 'applyFitLayout must NOT set inline tile heights — CSS grid 1fr rows handle sizing');
}); });
// ─── Settings tab reorganization (4 tabs) ────────────────────────────────────
test('HTML settings dialog has exactly 4 tab buttons', () => {
const source = fs.readFileSync(new URL('../index.html', import.meta.url), 'utf8');
const tabMatches = source.match(/class="settings-tab[^"]*"\s+data-tab=/g) || [];
assert.strictEqual(tabMatches.length, 4, 'settings dialog must have exactly 4 tab buttons (not 5)');
});
test('HTML index.html has no Notifications tab button', () => {
const source = fs.readFileSync(new URL('../index.html', import.meta.url), 'utf8');
assert.ok(!source.includes('data-tab="notifications"'), 'Notifications tab button must be removed');
});
test('HTML Sessions panel contains bell-sound checkbox', () => {
const source = fs.readFileSync(new URL('../index.html', import.meta.url), 'utf8');
// Find the sessions PANEL div specifically (not the tab button)
const sessionsPanelStart = source.indexOf('<div class="settings-panel hidden" data-tab="sessions"');
assert.ok(sessionsPanelStart !== -1, 'sessions panel div must exist');
// Find the end: next settings-panel div
const nextPanel = source.indexOf('<div class="settings-panel', sessionsPanelStart + 1);
const sessionsPanelContent = source.substring(sessionsPanelStart, nextPanel !== -1 ? nextPanel : sessionsPanelStart + 4000);
assert.ok(sessionsPanelContent.includes('setting-bell-sound'), 'bell-sound checkbox must be in sessions panel');
});
test('HTML Sessions panel contains desktop notifications request button', () => {
const source = fs.readFileSync(new URL('../index.html', import.meta.url), 'utf8');
// Find the sessions PANEL div specifically (not the tab button)
const sessionsPanelStart = source.indexOf('<div class="settings-panel hidden" data-tab="sessions"');
assert.ok(sessionsPanelStart !== -1, 'sessions panel div must exist');
const nextPanel = source.indexOf('<div class="settings-panel', sessionsPanelStart + 1);
const sessionsPanelContent = source.substring(sessionsPanelStart, nextPanel !== -1 ? nextPanel : sessionsPanelStart + 4000);
assert.ok(sessionsPanelContent.includes('notification-request-btn'), 'notification-request-btn must be in sessions panel');
});
test('HTML Display panel contains device name input outside #multi-device-fields', () => {
const source = fs.readFileSync(new URL('../index.html', import.meta.url), 'utf8');
// Find the display PANEL div specifically (not the tab button)
const displayPanelStart = source.indexOf('<div class="settings-panel" data-tab="display"');
assert.ok(displayPanelStart !== -1, 'display panel div must exist');
const nextPanel = source.indexOf('<div class="settings-panel', displayPanelStart + 1);
const displayPanelContent = source.substring(displayPanelStart, nextPanel !== -1 ? nextPanel : displayPanelStart + 3000);
assert.ok(displayPanelContent.includes('setting-device-name'), 'device name input must be in display panel');
// Must NOT be inside #multi-device-fields
const multiDeviceIdx = displayPanelContent.indexOf('multi-device-fields');
const deviceNameIdx = displayPanelContent.indexOf('setting-device-name');
if (multiDeviceIdx !== -1) {
assert.ok(deviceNameIdx < multiDeviceIdx, 'device name input must NOT be inside #multi-device-fields');
}
});
test('HTML index.html has no setting-view-scope select element', () => {
const source = fs.readFileSync(new URL('../index.html', import.meta.url), 'utf8');
assert.ok(!source.includes('setting-view-scope'), '#setting-view-scope must be removed from HTML');
});
test('loadGridViewMode always reads from localStorage (ignores viewPreferenceScope=server)', () => {
// After removing view scope, loadGridViewMode should ALWAYS use localStorage regardless
_localStorageStore = {};
_localStorageStore['muxplex.display'] = JSON.stringify({ viewPreferenceScope: 'server', gridViewMode: 'grouped' });
app._setServerSettings({ grid_view_mode: 'filtered' });
const mode = app.loadGridViewMode();
// Must return the localStorage value ('grouped'), NOT the server value ('filtered')
assert.strictEqual(mode, 'grouped', 'loadGridViewMode must always read from localStorage — server scope removed');
// Cleanup
_localStorageStore = {};
app._setServerSettings(null);
});
test('saveGridViewMode always writes to localStorage (ignores viewPreferenceScope=server)', () => {
_localStorageStore = {};
_localStorageStore['muxplex.display'] = JSON.stringify({ viewPreferenceScope: 'server' });
app._setServerSettings({ grid_view_mode: 'flat' });
const fetchCalls = [];
const origFetch = globalThis.fetch;
globalThis.fetch = async (url, opts) => { fetchCalls.push({ url, opts }); return { ok: true, json: async () => ({}) }; };
app.saveGridViewMode('filtered');
// Must NOT call fetch (no server PATCH), must write to localStorage
assert.strictEqual(fetchCalls.length, 0, 'saveGridViewMode must NOT call fetch — server scope removed');
const saved = JSON.parse(_localStorageStore['muxplex.display'] || '{}');
assert.strictEqual(saved.gridViewMode, 'filtered', 'gridViewMode must be saved to localStorage');
// Cleanup
globalThis.fetch = origFetch;
_localStorageStore = {};
app._setGridViewMode('flat');
app._setServerSettings(null);
});
test('bindStaticEventListeners does NOT bind change event to setting-view-scope', () => {
const source = fs.readFileSync(new URL('../app.js', import.meta.url), 'utf8');
const fnStart = source.indexOf('function bindStaticEventListeners(');
assert.ok(fnStart !== -1, 'bindStaticEventListeners must exist');
const fnEnd = source.indexOf('\nfunction ', fnStart + 1);
const fnBody = source.substring(fnStart, fnEnd > fnStart ? fnEnd : fnStart + 8000);
assert.ok(
!fnBody.includes('setting-view-scope'),
'bindStaticEventListeners must NOT reference setting-view-scope — view scope removed',
);
});