fix: restore favicon activity badge + add activity count and hostname to page title
Favicon badge: removed crossOrigin='anonymous' from _drawFaviconBadge Image lazy-init. Same-origin favicons don't need CORS credentials; setting it caused silent cache-miss failures when the browser had the asset cached without CORS headers, preventing the canvas from drawing. Page title: new updatePageTitle() centralizes title logic. Format: '(N) hostname - muxplex' when N sessions have unseen bells, otherwise 'hostname - muxplex'. Hostname uses device_name setting with fallback to location.hostname. Called from pollSessions on every tick, from loadServerSettings callback, and immediately when device_name input changes (updating _serverSettings.device_name in place first so the helper sees the latest value without waiting for the debounced PATCH). Replaced 3 scattered document.title assignments with updatePageTitle(). Updated 3 existing source-pattern tests to reflect the new centralised approach; added 2 new tests for updatePageTitle existence and pollSessions call site.
This commit is contained in:
+27
-4
@@ -269,6 +269,7 @@ async function pollSessions() {
|
|||||||
handleBellTransitions(prev, sessions);
|
handleBellTransitions(prev, sessions);
|
||||||
updateSessionPill(sessions);
|
updateSessionPill(sessions);
|
||||||
updateFaviconBadge();
|
updateFaviconBadge();
|
||||||
|
updatePageTitle();
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
_pollFailCount++;
|
_pollFailCount++;
|
||||||
setConnectionStatus(_pollFailCount <= 2 ? 'warn' : 'err');
|
setConnectionStatus(_pollFailCount <= 2 ? 'warn' : 'err');
|
||||||
@@ -1062,7 +1063,8 @@ function _drawFaviconBadge() {
|
|||||||
// Lazy-init: create the Image object once and cache it — subsequent calls reuse it
|
// Lazy-init: create the Image object once and cache it — subsequent calls reuse it
|
||||||
if (!_faviconImage) {
|
if (!_faviconImage) {
|
||||||
_faviconImage = new Image();
|
_faviconImage = new Image();
|
||||||
_faviconImage.crossOrigin = 'anonymous';
|
// No crossOrigin: favicon is same-origin; crossOrigin on same-origin images can
|
||||||
|
// cause cache misses when the browser has the asset cached without CORS headers.
|
||||||
_faviconImage.src = _originalFavicon;
|
_faviconImage.src = _originalFavicon;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1123,6 +1125,24 @@ function updateFaviconBadge() {
|
|||||||
_drawFaviconBadge();
|
_drawFaviconBadge();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update the page title with an optional activity count prefix and the hostname.
|
||||||
|
* Format: "(N) hostname - muxplex" when N sessions have unseen bells, otherwise
|
||||||
|
* "hostname - muxplex". Hostname is device_name from server settings, falling back
|
||||||
|
* to location.hostname so even unconfigured installs show something useful.
|
||||||
|
* Call from pollSessions() on every tick, and whenever server settings change.
|
||||||
|
*/
|
||||||
|
function updatePageTitle() {
|
||||||
|
var hostname = (_serverSettings && _serverSettings.device_name) ||
|
||||||
|
(typeof location !== 'undefined' ? location.hostname : null) ||
|
||||||
|
'muxplex';
|
||||||
|
var count = (_currentSessions || []).filter(function(s) {
|
||||||
|
return s.bell && s.bell.unseen_count > 0;
|
||||||
|
}).length;
|
||||||
|
var prefix = count > 0 ? '(' + count + ') ' : '';
|
||||||
|
document.title = prefix + hostname + ' - muxplex';
|
||||||
|
}
|
||||||
|
|
||||||
// ─── Session open / close ────────────────────────────────────────────────────
|
// ─── Session open / close ────────────────────────────────────────────────────
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -1691,7 +1711,7 @@ function openSettings() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Update document.title from device_name setting
|
// Update document.title from device_name setting
|
||||||
document.title = (ss && ss.device_name) || 'muxplex';
|
updatePageTitle();
|
||||||
|
|
||||||
// Multi-device enabled checkbox (with smart default: checked if remote_instances non-empty)
|
// Multi-device enabled checkbox (with smart default: checked if remote_instances non-empty)
|
||||||
const multiDeviceEnabledEl = $('setting-multi-device-enabled');
|
const multiDeviceEnabledEl = $('setting-multi-device-enabled');
|
||||||
@@ -2235,7 +2255,9 @@ function bindStaticEventListeners() {
|
|||||||
on($('setting-device-name'), 'input', function() {
|
on($('setting-device-name'), 'input', function() {
|
||||||
clearTimeout(_deviceNameDebounceTimer);
|
clearTimeout(_deviceNameDebounceTimer);
|
||||||
var val = this.value;
|
var val = this.value;
|
||||||
document.title = val || 'muxplex';
|
// Update cached setting immediately so updatePageTitle() sees the new value
|
||||||
|
if (_serverSettings) _serverSettings.device_name = val;
|
||||||
|
updatePageTitle();
|
||||||
_deviceNameDebounceTimer = setTimeout(function() {
|
_deviceNameDebounceTimer = setTimeout(function() {
|
||||||
patchServerSetting('device_name', val);
|
patchServerSetting('device_name', val);
|
||||||
}, 500);
|
}, 500);
|
||||||
@@ -2378,7 +2400,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
.then(() => {
|
.then(() => {
|
||||||
startPolling();
|
startPolling();
|
||||||
loadServerSettings().then(function() {
|
loadServerSettings().then(function() {
|
||||||
document.title = _serverSettings.device_name || 'muxplex';
|
updatePageTitle();
|
||||||
});
|
});
|
||||||
startHeartbeat();
|
startHeartbeat();
|
||||||
bindStaticEventListeners();
|
bindStaticEventListeners();
|
||||||
@@ -2428,6 +2450,7 @@ if (typeof module !== 'undefined' && module.exports) {
|
|||||||
closeBottomSheet,
|
closeBottomSheet,
|
||||||
renderSheetList,
|
renderSheetList,
|
||||||
updateSessionPill,
|
updateSessionPill,
|
||||||
|
updatePageTitle,
|
||||||
// ANSI color rendering
|
// ANSI color rendering
|
||||||
ansiToHtml,
|
ansiToHtml,
|
||||||
ansiParamsToStyle,
|
ansiParamsToStyle,
|
||||||
|
|||||||
@@ -3125,29 +3125,42 @@ test('CSS style.css uses base position:absolute bottom:0 for fit mode content an
|
|||||||
|
|
||||||
// --- document.title quality fixes ---
|
// --- document.title quality fixes ---
|
||||||
|
|
||||||
test('device name input handler restores default title when value is cleared', () => {
|
test('device name input handler updates title via updatePageTitle when value is cleared', () => {
|
||||||
const source = fs.readFileSync(new URL('../app.js', import.meta.url), 'utf8');
|
const source = fs.readFileSync(new URL('../app.js', import.meta.url), 'utf8');
|
||||||
// Must use fallback form, not the conditional-only form
|
// Handler now calls updatePageTitle() (which uses location.hostname fallback) instead of
|
||||||
|
// directly setting document.title. Verify the old direct-assignment is gone and the
|
||||||
|
// centralised helper is used.
|
||||||
assert.ok(
|
assert.ok(
|
||||||
source.includes("document.title = val || 'muxplex'"),
|
!source.includes("document.title = val || 'muxplex'"),
|
||||||
"device name input handler must set document.title = val || 'muxplex' to restore default when cleared"
|
"device name input handler must NOT directly set document.title = val || 'muxplex' (use updatePageTitle)"
|
||||||
);
|
);
|
||||||
assert.ok(
|
assert.ok(
|
||||||
!source.includes("if (val) document.title = val"),
|
!source.includes("if (val) document.title = val"),
|
||||||
"device name input handler must NOT use conditional 'if (val) document.title = val' (skips restore)"
|
"device name input handler must NOT use conditional 'if (val) document.title = val' (skips restore)"
|
||||||
);
|
);
|
||||||
|
// The handler must update _serverSettings.device_name before calling updatePageTitle()
|
||||||
|
assert.ok(
|
||||||
|
source.includes('_serverSettings.device_name = val'),
|
||||||
|
"device name input handler must update _serverSettings.device_name before calling updatePageTitle()"
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('openSettings restores default title unconditionally when device_name is absent', () => {
|
test('openSettings updates title via updatePageTitle (not direct assignment)', () => {
|
||||||
const source = fs.readFileSync(new URL('../app.js', import.meta.url), 'utf8');
|
const source = fs.readFileSync(new URL('../app.js', import.meta.url), 'utf8');
|
||||||
|
// The old direct assignment is replaced by updatePageTitle() in the settings panel loader
|
||||||
assert.ok(
|
assert.ok(
|
||||||
source.includes("document.title = (ss && ss.device_name) || 'muxplex'"),
|
!source.includes("document.title = (ss && ss.device_name) || 'muxplex'"),
|
||||||
"openSettings must set document.title = (ss && ss.device_name) || 'muxplex' unconditionally"
|
"openSettings must NOT directly set document.title — use updatePageTitle() instead"
|
||||||
);
|
);
|
||||||
assert.ok(
|
assert.ok(
|
||||||
!source.includes("if (ss && ss.device_name) {\n document.title = ss.device_name;\n }"),
|
!source.includes("if (ss && ss.device_name) {\n document.title = ss.device_name;\n }"),
|
||||||
"openSettings must NOT use conditional block that skips restore when device_name is absent"
|
"openSettings must NOT use conditional block that skips restore when device_name is absent"
|
||||||
);
|
);
|
||||||
|
// The settings loader area (where it updates device_name field) must call updatePageTitle
|
||||||
|
assert.ok(
|
||||||
|
source.includes('updatePageTitle'),
|
||||||
|
"app.js must define and use updatePageTitle for title management"
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('remote instance event listener comments say Multi-Device tab not Sessions tab', () => {
|
test('remote instance event listener comments say Multi-Device tab not Sessions tab', () => {
|
||||||
@@ -3170,15 +3183,20 @@ test('remote instance event listener comments say Multi-Device tab not Sessions
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('DOMContentLoaded sets document.title from server settings at page load', () => {
|
test('DOMContentLoaded sets page title via updatePageTitle after loadServerSettings resolves', () => {
|
||||||
const source = fs.readFileSync(new URL('../app.js', import.meta.url), 'utf8');
|
const source = fs.readFileSync(new URL('../app.js', import.meta.url), 'utf8');
|
||||||
// Find the DOMContentLoaded block
|
// Find the DOMContentLoaded block
|
||||||
const domIdx = source.indexOf("document.addEventListener('DOMContentLoaded'");
|
const domIdx = source.indexOf("document.addEventListener('DOMContentLoaded'");
|
||||||
assert.ok(domIdx !== -1, 'DOMContentLoaded handler must exist');
|
assert.ok(domIdx !== -1, 'DOMContentLoaded handler must exist');
|
||||||
const domBlock = source.substring(domIdx, domIdx + 800);
|
const domBlock = source.substring(domIdx, domIdx + 800);
|
||||||
|
// The old direct assignment is replaced by updatePageTitle()
|
||||||
assert.ok(
|
assert.ok(
|
||||||
domBlock.includes("document.title = _serverSettings.device_name || 'muxplex'"),
|
!domBlock.includes("document.title = _serverSettings.device_name || 'muxplex'"),
|
||||||
"DOMContentLoaded must set document.title = _serverSettings.device_name || 'muxplex' after loadServerSettings resolves"
|
"DOMContentLoaded must NOT directly set document.title — delegate to updatePageTitle()"
|
||||||
|
);
|
||||||
|
assert.ok(
|
||||||
|
domBlock.includes("updatePageTitle"),
|
||||||
|
"DOMContentLoaded must call updatePageTitle() after loadServerSettings resolves"
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -4070,3 +4088,18 @@ test('closeSession after openSession with remoteId=0 does NOT fire DELETE /api/s
|
|||||||
globalThis.document.querySelector = origQS;
|
globalThis.document.querySelector = origQS;
|
||||||
globalThis.setTimeout = origSetTimeout;
|
globalThis.setTimeout = origSetTimeout;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('updatePageTitle function exists and uses activity count', () => {
|
||||||
|
const source = fs.readFileSync(new URL('../app.js', import.meta.url), 'utf8');
|
||||||
|
assert.ok(source.includes('function updatePageTitle'), 'must have updatePageTitle function');
|
||||||
|
assert.ok(source.includes('unseen_count'), 'must count unseen bells for title');
|
||||||
|
assert.ok(source.includes('document.title'), 'must set document.title');
|
||||||
|
assert.ok(source.includes('location.hostname'), 'must fall back to location.hostname');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('updatePageTitle is called from pollSessions', () => {
|
||||||
|
const source = fs.readFileSync(new URL('../app.js', import.meta.url), 'utf8');
|
||||||
|
// Use 700 chars — the call is ~562 chars into the function after comments/whitespace
|
||||||
|
const pollFn = source.substring(source.indexOf('async function pollSessions'), source.indexOf('async function pollSessions') + 700);
|
||||||
|
assert.ok(pollFn.includes('updatePageTitle'), 'pollSessions must call updatePageTitle');
|
||||||
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user