From 2de22d37da7247af14b92f7df72cb1e74a260198 Mon Sep 17 00:00:00 2001 From: GitHub Copilot Date: Sat, 14 Feb 2026 19:42:09 +0000 Subject: [PATCH] Fix dashboard live refresh and UTF-8 hint rendering Restore reliable live thumbnail updates in the single-flight queue and set UTF-8 charset metadata/headers so typeahead hint symbols render correctly. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- go/webterm/server.go | 64 ++++++++++++++++++++++++++------------------ 1 file changed, 38 insertions(+), 26 deletions(-) diff --git a/go/webterm/server.go b/go/webterm/server.go index 2e5f9cb..381ab76 100644 --- a/go/webterm/server.go +++ b/go/webterm/server.go @@ -819,6 +819,7 @@ func (s *LocalServer) handleRoot(w http.ResponseWriter, r *http.Request) { html := fmt.Sprintf(` + Session Dashboard @@ -876,6 +877,7 @@ func (s *LocalServer) handleRoot(w http.ResponseWriter, r *http.Request) { const floatingResultsEl = document.getElementById('floating-results'); const keyIndicatorEl = document.getElementById('key-indicator'); const thumbnailCache = {}; + const activeObjectURLBySlug = {}; const refreshQueue = []; const queuedRefresh = {}; let screenshotRequestInFlight = false; @@ -1119,27 +1121,32 @@ func (s *LocalServer) handleRoot(w http.ResponseWriter, r *http.Request) { } 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(() => {}); - } + const url = '/screenshot.svg?route_key=' + encodeURIComponent(slug); + const controller = new AbortController(); + const timeout = setTimeout(() => controller.abort(), 5000); + fetch(url, { cache: 'no-cache', signal: controller.signal }) + .then((resp) => { + if (resp.status === 304) return null; + if (!resp.ok) throw new Error('screenshot fetch failed'); + return resp.blob(); + }) + .then((blob) => { + if (!blob) return; + const previous = activeObjectURLBySlug[slug]; + const objectURL = URL.createObjectURL(blob); + activeObjectURLBySlug[slug] = objectURL; + img.src = objectURL; + thumbnailCache[slug] = { src: objectURL, updatedAt: Date.now() }; + if (previous) { + URL.revokeObjectURL(previous); + } + }) + .catch(() => {}) + .finally(() => { + clearTimeout(timeout); + screenshotRequestInFlight = false; + setTimeout(processRefreshQueue, 0); + }); } function queueTileRefresh(slug) { @@ -1207,9 +1214,14 @@ func (s *LocalServer) handleRoot(w http.ResponseWriter, r *http.Request) { grid.innerHTML = ''; cardsBySlug = {}; refreshQueue.length = 0; + screenshotRequestInFlight = false; for (const key in queuedRefresh) { delete queuedRefresh[key]; } + for (const key in activeObjectURLBySlug) { + URL.revokeObjectURL(activeObjectURLBySlug[key]); + delete activeObjectURLBySlug[key]; + } if (tiles.length === 0) { grid.innerHTML = '
No containers found. Start containers with the webterm-command label.
'; subtitle.textContent = dockerWatchMode ? 'Watching for containers with webterm-command label...' : ''; @@ -1266,7 +1278,7 @@ func (s *LocalServer) handleRoot(w http.ResponseWriter, r *http.Request) { `, string(tilesJSON), composeModeJS, dockerWatchJS) - w.Header().Set("Content-Type", "text/html") + w.Header().Set("Content-Type", "text/html; charset=utf-8") _, _ = io.WriteString(w, html) return } @@ -1280,8 +1292,8 @@ func (s *LocalServer) handleRoot(w http.ResponseWriter, r *http.Request) { app, ok = s.sessionManager.GetDefaultApp() } if !ok { - w.Header().Set("Content-Type", "text/html") - _, _ = io.WriteString(w, "Webterm Server

No Apps Available

No terminal applications are configured.

") + w.Header().Set("Content-Type", "text/html; charset=utf-8") + _, _ = io.WriteString(w, "Webterm Server

No Apps Available

No terminal applications are configured.

") return } @@ -1308,8 +1320,8 @@ func (s *LocalServer) handleRoot(w http.ResponseWriter, r *http.Request) { } escapedFont := strings.ReplaceAll(fontFamily, `"`, """) dataAttrs := fmt.Sprintf(`data-session-websocket-url="%s" data-font-size="%d" data-scrollback="1000" data-theme="%s" data-font-family="%s"`, htmlAttrEscape(wsURL), s.fontSize, htmlAttrEscape(theme), escapedFont) - page := fmt.Sprintf(`%s
`, htmlEscape(app.Name), themeBG, dataAttrs) - w.Header().Set("Content-Type", "text/html") + page := fmt.Sprintf(`%s
`, htmlEscape(app.Name), themeBG, dataAttrs) + w.Header().Set("Content-Type", "text/html; charset=utf-8") _, _ = io.WriteString(w, page) }