feat: dynamic favicon badge — amber dot overlay when sessions have unseen bells
This commit is contained in:
@@ -248,6 +248,7 @@ async function pollSessions() {
|
|||||||
renderSidebar(sessions, _viewingSession);
|
renderSidebar(sessions, _viewingSession);
|
||||||
handleBellTransitions(prev, sessions);
|
handleBellTransitions(prev, sessions);
|
||||||
updateSessionPill(sessions);
|
updateSessionPill(sessions);
|
||||||
|
updateFaviconBadge();
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
_pollFailCount++;
|
_pollFailCount++;
|
||||||
setConnectionStatus(_pollFailCount <= 2 ? 'warn' : 'err');
|
setConnectionStatus(_pollFailCount <= 2 ? 'warn' : 'err');
|
||||||
@@ -815,6 +816,61 @@ function updatePillBell() {
|
|||||||
if (hasBell) el.classList.remove('hidden'); else el.classList.add('hidden');
|
if (hasBell) el.classList.remove('hidden'); else el.classList.add('hidden');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Dynamic favicon — activity dot overlay
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
var _originalFavicon = null; // cached original favicon href
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update the favicon with an activity dot if any session has unseen bells.
|
||||||
|
* Uses a 32x32 canvas to draw the original favicon + a colored circle overlay.
|
||||||
|
* Restores the original favicon when there are no unseen bells.
|
||||||
|
*/
|
||||||
|
function updateFaviconBadge() {
|
||||||
|
var hasActivity = _currentSessions && _currentSessions.some(function (s) {
|
||||||
|
return s.bell && s.bell.unseen_count > 0;
|
||||||
|
});
|
||||||
|
|
||||||
|
var link = document.querySelector('link[rel="icon"][sizes="32x32"]') ||
|
||||||
|
document.querySelector('link[rel="icon"]');
|
||||||
|
if (!link) return;
|
||||||
|
|
||||||
|
// Cache the original favicon on first call
|
||||||
|
if (!_originalFavicon) _originalFavicon = link.href;
|
||||||
|
|
||||||
|
if (!hasActivity) {
|
||||||
|
// Restore original favicon when no activity
|
||||||
|
if (link.href !== _originalFavicon) link.href = _originalFavicon;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Draw favicon + activity dot on canvas
|
||||||
|
var canvas = document.createElement('canvas');
|
||||||
|
canvas.width = 32;
|
||||||
|
canvas.height = 32;
|
||||||
|
var ctx = canvas.getContext('2d');
|
||||||
|
if (!ctx) return;
|
||||||
|
|
||||||
|
var img = new Image();
|
||||||
|
img.crossOrigin = 'anonymous';
|
||||||
|
img.onload = function () {
|
||||||
|
ctx.drawImage(img, 0, 0, 32, 32);
|
||||||
|
|
||||||
|
// Activity dot — brand amber (same as bell indicator)
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.arc(24, 8, 7, 0, 2 * Math.PI); // top-right area
|
||||||
|
ctx.fillStyle = '#F1A640'; // var(--bell-color)
|
||||||
|
ctx.fill();
|
||||||
|
ctx.strokeStyle = '#0D1117'; // var(--bg) — border for contrast
|
||||||
|
ctx.lineWidth = 2;
|
||||||
|
ctx.stroke();
|
||||||
|
|
||||||
|
link.href = canvas.toDataURL('image/png');
|
||||||
|
};
|
||||||
|
img.src = _originalFavicon;
|
||||||
|
}
|
||||||
|
|
||||||
// ─── Session open / close ────────────────────────────────────────────────────
|
// ─── Session open / close ────────────────────────────────────────────────────
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -2040,3 +2040,26 @@ test('applyDisplaySettings calls window._setTerminalFontSize when available', ()
|
|||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
// --- Issue: Dynamic favicon badge with activity dot ---
|
||||||
|
|
||||||
|
test('updateFaviconBadge function exists in app.js', () => {
|
||||||
|
const source = fs.readFileSync(new URL('../app.js', import.meta.url), 'utf8');
|
||||||
|
assert.ok(
|
||||||
|
source.includes('function updateFaviconBadge'),
|
||||||
|
'app.js must define updateFaviconBadge function'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('pollSessions calls updateFaviconBadge', () => {
|
||||||
|
const source = fs.readFileSync(new URL('../app.js', import.meta.url), 'utf8');
|
||||||
|
const pollStart = source.indexOf('async function pollSessions()');
|
||||||
|
assert.ok(pollStart !== -1, 'pollSessions must exist');
|
||||||
|
// Find the closing brace of pollSessions (next line starting with "}")
|
||||||
|
const pollEnd = source.indexOf('\n}', pollStart);
|
||||||
|
const pollBody = source.substring(pollStart, pollEnd + 2);
|
||||||
|
assert.ok(
|
||||||
|
pollBody.includes('updateFaviconBadge'),
|
||||||
|
'pollSessions must call updateFaviconBadge — update favicon on every poll cycle'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user