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>
This commit is contained in:
+37
-25
@@ -819,6 +819,7 @@ func (s *LocalServer) handleRoot(w http.ResponseWriter, r *http.Request) {
|
|||||||
html := fmt.Sprintf(`<!DOCTYPE html>
|
html := fmt.Sprintf(`<!DOCTYPE html>
|
||||||
<html>
|
<html>
|
||||||
<head>
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
<title>Session Dashboard</title>
|
<title>Session Dashboard</title>
|
||||||
<link rel="manifest" href="/static/manifest.json">
|
<link rel="manifest" href="/static/manifest.json">
|
||||||
<meta name="theme-color" content="#0d1117">
|
<meta name="theme-color" content="#0d1117">
|
||||||
@@ -876,6 +877,7 @@ 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 activeObjectURLBySlug = {};
|
||||||
const refreshQueue = [];
|
const refreshQueue = [];
|
||||||
const queuedRefresh = {};
|
const queuedRefresh = {};
|
||||||
let screenshotRequestInFlight = false;
|
let screenshotRequestInFlight = false;
|
||||||
@@ -1119,27 +1121,32 @@ func (s *LocalServer) handleRoot(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
screenshotRequestInFlight = true;
|
screenshotRequestInFlight = true;
|
||||||
const img = card.img;
|
const img = card.img;
|
||||||
let released = false;
|
const url = '/screenshot.svg?route_key=' + encodeURIComponent(slug);
|
||||||
const release = () => {
|
const controller = new AbortController();
|
||||||
if (released) return;
|
const timeout = setTimeout(() => controller.abort(), 5000);
|
||||||
released = true;
|
fetch(url, { cache: 'no-cache', signal: controller.signal })
|
||||||
screenshotRequestInFlight = false;
|
.then((resp) => {
|
||||||
thumbnailCache[slug] = { src: img.currentSrc || img.src, updatedAt: Date.now() };
|
if (resp.status === 304) return null;
|
||||||
setTimeout(processRefreshQueue, 0);
|
if (!resp.ok) throw new Error('screenshot fetch failed');
|
||||||
};
|
return resp.blob();
|
||||||
const timeout = setTimeout(release, 5000);
|
})
|
||||||
const complete = () => {
|
.then((blob) => {
|
||||||
clearTimeout(timeout);
|
if (!blob) return;
|
||||||
img.removeEventListener('load', complete);
|
const previous = activeObjectURLBySlug[slug];
|
||||||
img.removeEventListener('error', complete);
|
const objectURL = URL.createObjectURL(blob);
|
||||||
release();
|
activeObjectURLBySlug[slug] = objectURL;
|
||||||
};
|
img.src = objectURL;
|
||||||
img.addEventListener('load', complete);
|
thumbnailCache[slug] = { src: objectURL, updatedAt: Date.now() };
|
||||||
img.addEventListener('error', complete);
|
if (previous) {
|
||||||
img.src = '/screenshot.svg?route_key=' + encodeURIComponent(slug);
|
URL.revokeObjectURL(previous);
|
||||||
if (typeof img.decode === 'function') {
|
|
||||||
img.decode().then(complete).catch(() => {});
|
|
||||||
}
|
}
|
||||||
|
})
|
||||||
|
.catch(() => {})
|
||||||
|
.finally(() => {
|
||||||
|
clearTimeout(timeout);
|
||||||
|
screenshotRequestInFlight = false;
|
||||||
|
setTimeout(processRefreshQueue, 0);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function queueTileRefresh(slug) {
|
function queueTileRefresh(slug) {
|
||||||
@@ -1207,9 +1214,14 @@ func (s *LocalServer) handleRoot(w http.ResponseWriter, r *http.Request) {
|
|||||||
grid.innerHTML = '';
|
grid.innerHTML = '';
|
||||||
cardsBySlug = {};
|
cardsBySlug = {};
|
||||||
refreshQueue.length = 0;
|
refreshQueue.length = 0;
|
||||||
|
screenshotRequestInFlight = false;
|
||||||
for (const key in queuedRefresh) {
|
for (const key in queuedRefresh) {
|
||||||
delete queuedRefresh[key];
|
delete queuedRefresh[key];
|
||||||
}
|
}
|
||||||
|
for (const key in activeObjectURLBySlug) {
|
||||||
|
URL.revokeObjectURL(activeObjectURLBySlug[key]);
|
||||||
|
delete activeObjectURLBySlug[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...' : '';
|
||||||
@@ -1266,7 +1278,7 @@ func (s *LocalServer) handleRoot(w http.ResponseWriter, r *http.Request) {
|
|||||||
</script>
|
</script>
|
||||||
</body>
|
</body>
|
||||||
</html>`, string(tilesJSON), composeModeJS, dockerWatchJS)
|
</html>`, 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)
|
_, _ = io.WriteString(w, html)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -1280,8 +1292,8 @@ func (s *LocalServer) handleRoot(w http.ResponseWriter, r *http.Request) {
|
|||||||
app, ok = s.sessionManager.GetDefaultApp()
|
app, ok = s.sessionManager.GetDefaultApp()
|
||||||
}
|
}
|
||||||
if !ok {
|
if !ok {
|
||||||
w.Header().Set("Content-Type", "text/html")
|
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
||||||
_, _ = io.WriteString(w, "<!DOCTYPE html><html><head><title>Webterm Server</title></head><body><h2>No Apps Available</h2><p>No terminal applications are configured.</p></body></html>")
|
_, _ = io.WriteString(w, "<!DOCTYPE html><html><head><meta charset=\"utf-8\"><title>Webterm Server</title></head><body><h2>No Apps Available</h2><p>No terminal applications are configured.</p></body></html>")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1308,8 +1320,8 @@ func (s *LocalServer) handleRoot(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
escapedFont := strings.ReplaceAll(fontFamily, `"`, """)
|
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)
|
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(`<!DOCTYPE html><html><head><title>%s</title><link rel="stylesheet" href="/static/monospace.css"><style>html,body{width:100%%;height:100%%}body{background:%s;margin:0;padding:0;overflow:hidden;font-family:var(--webterm-mono)}.webterm-terminal{width:100%%;height:100%%;display:block;overflow:hidden}</style></head><body><div id="terminal" class="webterm-terminal" %s></div><script type="module" src="/static/js/terminal.js"></script></body></html>`, htmlEscape(app.Name), themeBG, dataAttrs)
|
page := fmt.Sprintf(`<!DOCTYPE html><html><head><meta charset="utf-8"><title>%s</title><link rel="stylesheet" href="/static/monospace.css"><style>html,body{width:100%%;height:100%%}body{background:%s;margin:0;padding:0;overflow:hidden;font-family:var(--webterm-mono)}.webterm-terminal{width:100%%;height:100%%;display:block;overflow:hidden}</style></head><body><div id="terminal" class="webterm-terminal" %s></div><script type="module" src="/static/js/terminal.js"></script></body></html>`, htmlEscape(app.Name), themeBG, dataAttrs)
|
||||||
w.Header().Set("Content-Type", "text/html")
|
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
||||||
_, _ = io.WriteString(w, page)
|
_, _ = io.WriteString(w, page)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user