Always download SVG screenshots

The dashboard now always downloads SVG on right-click even when
PNG thumbnails are enabled, while keeping SVG as the default mode
in the docs and tests.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
GitHub Copilot
2026-02-18 15:56:41 +00:00
parent b5f3534995
commit cdcc9bfc23
4 changed files with 12 additions and 7 deletions
+1 -1
View File
@@ -13,7 +13,7 @@ https://github.com/user-attachments/assets/62c52183-83a3-4fb5-97b1-ed001de4f53a
- Typeahead find for quickly finding and launching sessions with minimal friction - Typeahead find for quickly finding and launching sessions with minimal friction
- Web terminal with reconnect support - Web terminal with reconnect support
- Ghostty WebAssembly terminal engine for fast rendering from [`ghostty-web`](https://github.com/rcarmo/ghostty-web) - Ghostty WebAssembly terminal engine for fast rendering from [`ghostty-web`](https://github.com/rcarmo/ghostty-web)
- Session dashboard with live SVG screenshots from [`go-te`](https://github.com/rcarmo/go-te) - Session dashboard with live SVG (or optional PNG) screenshots from [`go-te`](https://github.com/rcarmo/go-te)
- Docker watch mode (`webterm-command` / `webterm-theme` labels) - Docker watch mode (`webterm-command` / `webterm-theme` labels)
- Docker compose manifest ingestion - Docker compose manifest ingestion
- CPU sparkline tiles for compose services - CPU sparkline tiles for compose services
+4 -2
View File
@@ -16,7 +16,8 @@ webterm/server.go (LocalServer)
├── docker_exec_session.go (Docker exec-backed sessions) ├── docker_exec_session.go (Docker exec-backed sessions)
├── docker_watcher.go (container add/remove discovery) ├── docker_watcher.go (container add/remove discovery)
├── docker_stats.go (CPU sampling + sparkline data) ├── docker_stats.go (CPU sampling + sparkline data)
── svg_exporter.go (terminal snapshot -> SVG) ── svg_exporter.go (terminal snapshot -> SVG)
└── png_exporter.go (terminal snapshot -> PNG via coverage blending)
``` ```
## Packages ## Packages
@@ -24,6 +25,7 @@ webterm/server.go (LocalServer)
- `cmd/webterm`: CLI entrypoint - `cmd/webterm`: CLI entrypoint
- `webterm`: server/runtime/domain logic - `webterm`: server/runtime/domain logic
- `internal/terminalstate`: Go terminal emulator wrapper (`go-te`) used for screenshots - `internal/terminalstate`: Go terminal emulator wrapper (`go-te`) used for screenshots
- `webterm/coverage_table.go`: coverage map for approximate PNG rendering
## Runtime data flow ## Runtime data flow
@@ -33,7 +35,7 @@ webterm/server.go (LocalServer)
- live WS stream (`stdout`) - live WS stream (`stdout`)
- replay buffer (reconnect support) - replay buffer (reconnect support)
- terminal-state tracker (`go-te`) for screenshots - terminal-state tracker (`go-te`) for screenshots
4. Dashboard pulls `/screenshot.svg` and listens on `/events` for activity. 4. Dashboard pulls `/screenshot.svg` (default) or `/screenshot.png` when `WEBTERM_SCREENSHOT_MODE=png`, and listens on `/events` for activity.
## Static assets ## Static assets
+4 -4
View File
@@ -1087,12 +1087,11 @@ func (s *LocalServer) handleRoot(w http.ResponseWriter, r *http.Request) {
dockerWatchJS = "true" dockerWatchJS = "true"
} }
screenshotEndpoint := "/screenshot.svg" screenshotEndpoint := "/screenshot.svg"
screenshotDownloadEndpoint := "/screenshot.svg"
screenshotDownloadQuery := "sanitize_font_urls=1&download=1" screenshotDownloadQuery := "sanitize_font_urls=1&download=1"
screenshotDownloadExt := "svg" screenshotDownloadExt := "svg"
if s.screenshotMode == "png" { if s.screenshotMode == "png" {
screenshotEndpoint = "/screenshot.png" screenshotEndpoint = "/screenshot.png"
screenshotDownloadQuery = "download=1"
screenshotDownloadExt = "png"
} }
html := fmt.Sprintf(`<!DOCTYPE html> html := fmt.Sprintf(`<!DOCTYPE html>
<html> <html>
@@ -1151,6 +1150,7 @@ func (s *LocalServer) handleRoot(w http.ResponseWriter, r *http.Request) {
const composeMode = %s; const composeMode = %s;
const dockerWatchMode = %s; const dockerWatchMode = %s;
const screenshotEndpoint = %q; const screenshotEndpoint = %q;
const screenshotDownloadEndpoint = %q;
const screenshotDownloadQuery = %q; const screenshotDownloadQuery = %q;
const screenshotDownloadExt = %q; const screenshotDownloadExt = %q;
let cardsBySlug = {}; let cardsBySlug = {};
@@ -1172,7 +1172,7 @@ func (s *LocalServer) handleRoot(w http.ResponseWriter, r *http.Request) {
function downloadSanitizedScreenshot(slug) { function downloadSanitizedScreenshot(slug) {
if (!slug) return; if (!slug) return;
const link = document.createElement('a'); const link = document.createElement('a');
link.href = screenshotEndpoint + '?route_key=' + encodeURIComponent(slug) + '&' + screenshotDownloadQuery + '&_t=' + Date.now(); link.href = screenshotDownloadEndpoint + '?route_key=' + encodeURIComponent(slug) + '&' + screenshotDownloadQuery + '&_t=' + Date.now();
link.download = slug + '-screenshot.' + screenshotDownloadExt; link.download = slug + '-screenshot.' + screenshotDownloadExt;
document.body.appendChild(link); document.body.appendChild(link);
link.click(); link.click();
@@ -1605,7 +1605,7 @@ func (s *LocalServer) handleRoot(w http.ResponseWriter, r *http.Request) {
} }
</script> </script>
</body> </body>
</html>`, string(tilesJSON), composeModeJS, dockerWatchJS, screenshotEndpoint, screenshotDownloadQuery, screenshotDownloadExt) </html>`, string(tilesJSON), composeModeJS, dockerWatchJS, screenshotEndpoint, screenshotDownloadEndpoint, screenshotDownloadQuery, screenshotDownloadExt)
w.Header().Set("Content-Type", "text/html; charset=utf-8") w.Header().Set("Content-Type", "text/html; charset=utf-8")
_, _ = io.WriteString(w, html) _, _ = io.WriteString(w, html)
return return
+3
View File
@@ -552,6 +552,9 @@ func TestDashboardUsesPNGWhenEnabled(t *testing.T) {
if !strings.Contains(text, "screenshot.png") { if !strings.Contains(text, "screenshot.png") {
t.Fatalf("expected dashboard to request png screenshots when enabled") t.Fatalf("expected dashboard to request png screenshots when enabled")
} }
if !strings.Contains(text, "sanitize_font_urls=1&download=1") || !strings.Contains(text, "screenshot.svg") {
t.Fatalf("expected contextmenu downloads to use svg screenshots")
}
} }
func TestRootTerminalPageAndSparklineValidation(t *testing.T) { func TestRootTerminalPageAndSparklineValidation(t *testing.T) {