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);
|
||||
updateSessionPill(sessions);
|
||||
updateFaviconBadge();
|
||||
updatePageTitle();
|
||||
} catch (err) {
|
||||
_pollFailCount++;
|
||||
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
|
||||
if (!_faviconImage) {
|
||||
_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;
|
||||
}
|
||||
|
||||
@@ -1123,6 +1125,24 @@ function updateFaviconBadge() {
|
||||
_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 ────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
@@ -1691,7 +1711,7 @@ function openSettings() {
|
||||
}
|
||||
|
||||
// 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)
|
||||
const multiDeviceEnabledEl = $('setting-multi-device-enabled');
|
||||
@@ -2235,7 +2255,9 @@ function bindStaticEventListeners() {
|
||||
on($('setting-device-name'), 'input', function() {
|
||||
clearTimeout(_deviceNameDebounceTimer);
|
||||
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() {
|
||||
patchServerSetting('device_name', val);
|
||||
}, 500);
|
||||
@@ -2378,7 +2400,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
.then(() => {
|
||||
startPolling();
|
||||
loadServerSettings().then(function() {
|
||||
document.title = _serverSettings.device_name || 'muxplex';
|
||||
updatePageTitle();
|
||||
});
|
||||
startHeartbeat();
|
||||
bindStaticEventListeners();
|
||||
@@ -2428,6 +2450,7 @@ if (typeof module !== 'undefined' && module.exports) {
|
||||
closeBottomSheet,
|
||||
renderSheetList,
|
||||
updateSessionPill,
|
||||
updatePageTitle,
|
||||
// ANSI color rendering
|
||||
ansiToHtml,
|
||||
ansiParamsToStyle,
|
||||
|
||||
@@ -3125,29 +3125,42 @@ test('CSS style.css uses base position:absolute bottom:0 for fit mode content an
|
||||
|
||||
// --- 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');
|
||||
// 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(
|
||||
source.includes("document.title = val || 'muxplex'"),
|
||||
"device name input handler must set document.title = val || 'muxplex' to restore default when cleared"
|
||||
!source.includes("document.title = val || 'muxplex'"),
|
||||
"device name input handler must NOT directly set document.title = val || 'muxplex' (use updatePageTitle)"
|
||||
);
|
||||
assert.ok(
|
||||
!source.includes("if (val) document.title = val"),
|
||||
"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');
|
||||
// The old direct assignment is replaced by updatePageTitle() in the settings panel loader
|
||||
assert.ok(
|
||||
source.includes("document.title = (ss && ss.device_name) || 'muxplex'"),
|
||||
"openSettings must set document.title = (ss && ss.device_name) || 'muxplex' unconditionally"
|
||||
!source.includes("document.title = (ss && ss.device_name) || 'muxplex'"),
|
||||
"openSettings must NOT directly set document.title — use updatePageTitle() instead"
|
||||
);
|
||||
assert.ok(
|
||||
!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"
|
||||
);
|
||||
// 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', () => {
|
||||
@@ -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');
|
||||
// Find the DOMContentLoaded block
|
||||
const domIdx = source.indexOf("document.addEventListener('DOMContentLoaded'");
|
||||
assert.ok(domIdx !== -1, 'DOMContentLoaded handler must exist');
|
||||
const domBlock = source.substring(domIdx, domIdx + 800);
|
||||
// The old direct assignment is replaced by updatePageTitle()
|
||||
assert.ok(
|
||||
domBlock.includes("document.title = _serverSettings.device_name || 'muxplex'"),
|
||||
"DOMContentLoaded must set document.title = _serverSettings.device_name || 'muxplex' after loadServerSettings resolves"
|
||||
!domBlock.includes("document.title = _serverSettings.device_name || 'muxplex'"),
|
||||
"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.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