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:
+92
-14
@@ -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 = () => {
|
||||||
|
|||||||
Reference in New Issue
Block a user