Reduce dashboard screenshot churn with single-flight focus gating

Ensure only one screenshot request is in flight at a time, queue per-tile refreshes, and pause screenshot fetches while the dashboard is hidden or unfocused.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
GitHub Copilot
2026-02-14 19:33:22 +00:00
parent 5da9c28528
commit eb0465403e
+92 -14
View File
@@ -876,7 +876,9 @@ func (s *LocalServer) handleRoot(w http.ResponseWriter, r *http.Request) {
const floatingResultsEl = document.getElementById('floating-results'); const floatingResultsEl = document.getElementById('floating-results');
const keyIndicatorEl = document.getElementById('key-indicator'); const keyIndicatorEl = document.getElementById('key-indicator');
const thumbnailCache = {}; const thumbnailCache = {};
const THUMBNAIL_TTL_MS = 5000; const refreshQueue = [];
const queuedRefresh = {};
let screenshotRequestInFlight = false;
const grid = document.getElementById('grid'); const grid = document.getElementById('grid');
const subtitle = document.getElementById('subtitle'); const subtitle = document.getElementById('subtitle');
@@ -931,14 +933,13 @@ func (s *LocalServer) handleRoot(w http.ResponseWriter, r *http.Request) {
function getThumbnailSrc(tile) { function getThumbnailSrc(tile) {
const slug = tile.slug || ''; const slug = tile.slug || '';
if (!slug) return ''; if (!slug) return '';
const now = Date.now(); const card = cardsBySlug[slug];
const existing = thumbnailCache[slug]; if (card && card.img && card.img.src) {
if (!existing || (now - existing.updatedAt) > THUMBNAIL_TTL_MS) { thumbnailCache[slug] = { src: card.img.src, updatedAt: Date.now() };
const src = '/screenshot.svg?route_key=' + encodeURIComponent(slug) + '&_t=' + now; return card.img.src;
thumbnailCache[slug] = { src, updatedAt: now };
return src;
} }
return existing.src; const existing = thumbnailCache[slug];
return existing ? existing.src : '';
} }
function updateTileSelection() { function updateTileSelection() {
@@ -1093,16 +1094,68 @@ func (s *LocalServer) handleRoot(w http.ResponseWriter, r *http.Request) {
document.addEventListener('keydown', handleKeydown); document.addEventListener('keydown', handleKeydown);
function refreshTile(slug) { function dashboardCanRequestScreenshots() {
return document.visibilityState === 'visible' && document.hasFocus();
}
function onDashboardFocusChanged() {
if (dashboardCanRequestScreenshots()) {
processRefreshQueue();
}
}
document.addEventListener('visibilitychange', onDashboardFocusChanged);
window.addEventListener('focus', onDashboardFocusChanged);
window.addEventListener('blur', onDashboardFocusChanged);
function processRefreshQueue() {
if (screenshotRequestInFlight || refreshQueue.length === 0 || !dashboardCanRequestScreenshots()) return;
const slug = refreshQueue.shift();
delete queuedRefresh[slug];
const card = cardsBySlug[slug]; const card = cardsBySlug[slug];
if (!card) return; if (!card || !card.img) {
card.img.src = '/screenshot.svg?route_key=' + encodeURIComponent(slug) + '&_t=' + Date.now(); setTimeout(processRefreshQueue, 0);
return;
}
screenshotRequestInFlight = true;
const img = card.img;
let released = false;
const release = () => {
if (released) return;
released = true;
screenshotRequestInFlight = false;
thumbnailCache[slug] = { src: img.currentSrc || img.src, updatedAt: Date.now() };
setTimeout(processRefreshQueue, 0);
};
const timeout = setTimeout(release, 5000);
const complete = () => {
clearTimeout(timeout);
img.removeEventListener('load', complete);
img.removeEventListener('error', complete);
release();
};
img.addEventListener('load', complete);
img.addEventListener('error', complete);
img.src = '/screenshot.svg?route_key=' + encodeURIComponent(slug);
if (typeof img.decode === 'function') {
img.decode().then(complete).catch(() => {});
}
}
function queueTileRefresh(slug) {
if (!slug || queuedRefresh[slug]) return;
queuedRefresh[slug] = true;
refreshQueue.push(slug);
processRefreshQueue();
}
function refreshTile(slug) {
queueTileRefresh(slug);
} }
function refreshAll() { function refreshAll() {
for (const tile of tiles) { for (const tile of tiles) {
const card = cardsBySlug[tile.slug]; queueTileRefresh(tile.slug);
if (card) card.img.src = '/screenshot.svg?route_key=' + encodeURIComponent(tile.slug);
} }
} }
@@ -1129,9 +1182,34 @@ func (s *LocalServer) handleRoot(w http.ResponseWriter, r *http.Request) {
} }
} }
const pendingRefresh = {};
const lastRefresh = {};
const REFRESH_DEBOUNCE_MS = 500;
function scheduleRefreshTile(slug) {
const now = Date.now();
const last = lastRefresh[slug] || 0;
if (now - last < REFRESH_DEBOUNCE_MS) {
if (!pendingRefresh[slug]) {
pendingRefresh[slug] = setTimeout(() => {
pendingRefresh[slug] = null;
refreshTile(slug);
lastRefresh[slug] = Date.now();
}, REFRESH_DEBOUNCE_MS - (now - last));
}
return;
}
refreshTile(slug);
lastRefresh[slug] = now;
}
function renderTiles() { function renderTiles() {
grid.innerHTML = ''; grid.innerHTML = '';
cardsBySlug = {}; cardsBySlug = {};
refreshQueue.length = 0;
for (const key in queuedRefresh) {
delete queuedRefresh[key];
}
if (tiles.length === 0) { if (tiles.length === 0) {
grid.innerHTML = '<div class="empty">No containers found. Start containers with the webterm-command label.</div>'; grid.innerHTML = '<div class="empty">No containers found. Start containers with the webterm-command label.</div>';
subtitle.textContent = dockerWatchMode ? 'Watching for containers with webterm-command label...' : ''; subtitle.textContent = dockerWatchMode ? 'Watching for containers with webterm-command label...' : '';
@@ -1159,7 +1237,7 @@ func (s *LocalServer) handleRoot(w http.ResponseWriter, r *http.Request) {
if (e.data === '__dashboard__') { if (e.data === '__dashboard__') {
refreshTilesList(); refreshTilesList();
} else { } else {
refreshTile(e.data); scheduleRefreshTile(e.data);
} }
}); });
source.onerror = () => { source.onerror = () => {