Help us shape the future of Go! Take the 2026 Go Developer Survey

dashboard

module
v0.6.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Oct 28, 2026 License: BSD-3-Clause

README

dashboard

Docs CI Go Reference status

A graphical dashboard for the go-wasmcontainers engine, in pure Go: containers, images, volumes, networks and a terminal running wctr, in one window, built with go-widgets and drawn on a browser canvas. It is meant to become the main view of the playground: a student does everything from the buttons or from wctr in the terminal, and both stay in sync, because both speak to the same engine through the same API (compatible with the Docker Engine API).

Every action names the command line it stands for — hover a button and the footer says “Stop = wctr stop web”; click it and the terminal shows # dashboard: wctr stop web above its prompt.

Try it: https://go-wasmcontainers.github.io/dashboard/ — a standalone demo with its own engine in a Web Worker and three demo images (hello, clock, fail). In the Terminal view: wctr run -d --name web clock, then watch it in Containers.

Status: M1, on engine v0.11.0. Containers (with stats and networks), images, volumes, networks (and joining a class), GPU surfaces, status bar and terminal, kept current by the engine's event stream. Tested with Playwright in Chromium (isolation headers and the service worker GitHub Pages needs); the Pages workflow also drives the live page in Chromium and Firefox.

What it shows

View
Containers name, image, state (coloured: running, exited 0, exited ≠ 0, created), status, created, ID; filter running/all, search, sort by any column; Start, Stop, Restart, Remove on the selected container (the engine's /start, /stop, /restart and DELETE: wctr start, wctr stop, wctr restart, wctr rm). Detail pane: Logs (follow on/off; ANSI colours rendered, stderr in red), Inspect (the inspect document as an indented tree, in its own key order), Environment, Mounts (volume data -> /data (rw)), Stats (wctr stats: a memory gauge against the container's limit, a sparkline of the last 60 readings, bytes written, layer and log size, pids; CPU shows -- with the engine's reason, since a wasm guest's CPU is not measured and 0% would read as idle).
Images repository:tag, image ID, digest, size, created; Pull… (progress lines), Run… (image, name, command, environment, entrypoint, working dir, memory, volumes -v NAME:/path[:ro], --rm, mirroring wctr run -d, with the command line shown live as you type), Remove (Force).
Volumes name, driver, mountpoint, created, size, how many containers use it; Create (wctr volume create NAME), the selected volume's inspect tree, Remove (Force: wctr volume rm -f NAME), Prune (All unused: wctr volume prune -a -f), with what it reclaimed. An engine older than v0.8.0 gets a message instead — never invented rows.
Networks name, ID, driver, subnet, and the containers attached with their addresses (web 10.42.0.2/24); Create (wctr network create NAME), the selected network's inspect tree (wctr network inspect NAME), Remove (wctr network rm NAME). A container's detail has a Networks tab: each network it is on, its address and DNS names. On a page that can join a class (the playground), the view also offers Join class…; see below. An engine older than v0.11.0 gets a message instead.
Terminal wctr run in-process against the same engine, with the engine's line editor (cli/lineedit: history, Ctrl-R, Tab completion of commands, flags and container names).
Surfaces one pane per GPU surface a container opens (wasi-gfx:surface, rendered by go-wasmcontainers/gpu in the engine's worker), titled <container> — surface N, with W×H, F fps, or waiting for first frame before the first. Frames are drawn scaled to fit with their aspect kept; a closed surface keeps its last frame, greyed, until Dismiss. See below.
status bar engine version and API, counts, storage from GET /system/df (images and volumes, and where the engine keeps its state and how full it is — IndexedDB 2MB of 1GB (persistent), or none for an in-memory engine), how it keeps up: live (GET /events); when the engine cuts the stream (a slow reader) or the connection breaks, the dashboard resynchronises and reconnects from the last event's time, backing off; an engine without the stream is polled every 2 s.

Wherever the wctr in use has no command for an action, the footer shows the API call instead (POST /containers/web/stop, with a note), and switches to the command line by itself once wctr has it.

Look

  • Crisp on any display. Text is the toolkit's anti-aliased OpenType face (Atkinson Hyperlegible; the terminals use JetBrains Mono). The canvas renders at device pixels: its backing store is the CSS size × devicePixelRatio, and it follows browser zoom and moves between displays (go-widgets/webcanvas PixelRatioAware, with every metric and face scaled through toolkit.SetMetricScale). Measured in the page with getImageData at ratio 2: a 2560-px backing store for 1280 CSS px, and title glyphs with 17 coverage levels (the 5×7 bitmap face had 2).
  • Icons on the navigation and in the search fields are Iconoir glyphs (MIT, go-icons/iconoir), drawn through the toolkit's icon seam. They are rasterised at the device size of their box, cached per icon, size and colour, and recoloured with the row's ink.
  • Delimitation. The sidebar has its own ground and a border line. The tables have a frame, a header separator, column separators and row lines. The border colour is at least 3:1 against every ground it touches, in both themes (WCAG 2.2 SC 1.4.11; asserted in ui/theme_test.go).

Keyboard

F6 moves between the navigation and the view's main widget; Tab / Shift-Tab between controls; arrows in lists and tables; Alt+1…6 switch views. While the terminal has focus it gets every key it uses (Ctrl-C/D/L/R/W/U, arrows, Tab, Alt+key, …) before any of these shortcuts; F6 leaves it. (Browsers reserve a few chords — Ctrl-W, Ctrl-T, Ctrl-N in Chrome — that no page can take.)

Architecture

Package What it is
vm the viewmodels: pure Go over an Engine interface, no widget; every action goes through the API
ui the go-widgets scene bound to the viewmodels; a webcanvas.App, tested natively against a pixel buffer
engine the Engine interface over the engine's api.Client; volumes, disk usage and events answer ErrUnsupported on the router's own 404, so the dashboard lights them up by itself when the engine serves them
bridge HTTP over postMessage between the page (dashboard, canvas) and the engine's Web Worker; responses stream (log follow, wait, pull)
cmd/dashboard, cmd/engine-worker the two js/wasm programs of the page
cmd/site builds the demo site (site build --out dist) and serves it (site serve --dir dist). site check [--revision SHA] FILE... reads the Go build info, in wasm too (go-fleettools/buildstamp), and fails unless the program was built from a clean commit; the Pages build runs it.
integration, e2e the viewmodels against a real engine (wazero) through the bridge; the page in real browsers (go test -tags e2e ./e2e/)

go-widgets gained a terminal emulator for this (toolkit.Terminal over the pure-Go vt.Screen, v0.342.0). The scene runs where go-widgets/webcanvas runs — a browser canvas; there is no native window build.

In the playground

The dashboard attaches to an engine that already runs in a worker. The worker serves the engine's API handler through the bridge (bridge.ServeJS(handler, self) and one line in its onmessage); the page sets window.__wctrEngine to that worker and runs dashboard.wasm on a <canvas id="screen">. cmd/engine-worker is the reference.

GPU surfaces

The engine's worker posts its GPU surfaces to the page (go-wasmcontainers/gpu, webgpu.Browser{OnSurface}), and the dashboard listens on window.__wctrEngine. No page code is needed. The messages:

Message Meaning
{t:"surface", op:"open", surface, container, pid, width, height} a pane opens; the size defaults to 640×480 and is clamped to 1920×1080
{t:"surface", op:"resize", surface, width, height} the surface changed size
{t:"surface", op:"close", surface} the last frame stays, greyed, until Dismiss
{t:"frame", surface, seq, width, height, bitmap} one present(); the ImageBitmap is transferred to the page

Each frame becomes the page's to handle:

  • Only the newest frame per surface is kept. An older one still waiting is closed and acknowledged as dropped.
  • At the next scene frame, the waiting bitmap is drawn into the surface's backing canvas (an OffscreenCanvas), closed at once, and acknowledged with {t:"frame-ack", surface, seq}. The engine paces present() on these acks.
  • The pane is redrawn from the backing canvas through go-widgets/webcanvas Layered (v0.6.0), scaled to fit with its aspect kept, at device pixels.

Robustness:

  • Frames are presented and acknowledged even while another view is shown, so the guest never waits on a hidden pane.
  • A frame for an unknown or closed surface, one out of order, or a message without a real ImageBitmap is dropped, closed and acknowledged.

Bounds: at most 16 panes, surface must be a u32, seq an integer from 1 to 2^53, and sizes are clamped.

The e2e checks this with 30 synthetic ImageBitmaps:

  • every frame is acknowledged exactly once, the malformed frame included;
  • the pane's centre pixel is the frame's colour, at a 4:3 aspect;
  • once the surface closes, the frame is shown greyed;
  • every bitmap ends closed: an ImageBitmap's width reads 0 after close().

Input back to the guest is planned: {t:"input", surface, kind, x, y, …}, in surface pixels.

Join class

A page whose engine can join a class says so with window.__wctrClass. Without that object the dashboard does not offer the dialog:

window.__wctrClass = {
  relay: "https://relay.example:8443", // the relay this engine was started with, "" for none
  useRelay(origin) { /* remember origin ("" forgets it), restart the engine with it (reload) */ },
  status() { return lastClassMessage; }, // the worker's last {t:"class", ...}, or null
};

The dialog runs in two steps:

  1. The relay. It accepts only https://host[:port]; wss://host names the same relay and is mapped to https://. Anything else is refused: credentials, a path, a query, a fragment, a malformed host or port. The page then restarts the engine with that origin, because the worker's CSP may reach exactly that origin and nothing else.

  2. Your name and the join code. The code is a secret:

    • its field is masked;
    • it is never in the accessibility tree or on the clipboard (toolkit v0.345.2);
    • it is posted once to the engine worker as {t:"join", code, name} and the field is cleared at once;
    • it is never in a command line, a message, the terminal, the URL, storage, the DOM or the test hook.

    The e2e checks every one of those places, with a planted positive.

The worker's {t:"class"} reports drive the status line, for example class: you are ada.class.lan on https://relay… · network class · addresses 10.42.2.0/24 (run --network class). Leave class posts {t:"leave"}. This is the protocol of go-wasmcontainers/playground's engine worker (playground#14, engine v0.11.0).

Planned, not built: the class's flow map. The relay has a GraphView of who talks to whom in a class. Showing it in the Networks view, for the class network, needs one thing the relay does not give a browser yet: a relay-issued, short-lived, read-only ticket, scoped to one class and returned to the engine worker with the session.

  • The page must never hold the join code or the session token, so the ticket is the only credential the view would use.
  • The view would fetch the graph through the worker, whose CSP already reaches the relay. It would draw the graph natively, cleaning every label, rather than embed the relay's page in an iframe. An iframe would need a frame-src hole and would put a third-party document inside the page.
  • Until the relay issues such a ticket, the view shows only what the engine knows: the class network's attached containers.

Security

  • Everything a container provides is text: names, labels, commands and inspect values go through vm.Clean (control and format characters out, bounded); logs and terminal output go to the go-widgets Terminal, whose screen has no reply channel and discards every OSC/DCS/APC string (no clipboard write, no links, no title, no reports), and whose paste strips control characters.
  • Bounded: lists, the pull log, the inspect tree (nodes, depth), pending log bytes, keys queued for the terminal, the bridge's buffers, bodies, headers and requests in flight.
  • The bridge carries a path only (never a scheme or host) and a fixed set of methods. The page talks to nothing but its own origin (CSP default-src 'self'), and to the engine only through its API.
  • Fuzzed: FuzzServer (bridge), FuzzInspectTree, FuzzClean; the terminal's parser is fuzzed in go-widgets.

Performance

A full frame (1280×800) with 200 containers and 100 images: 0.86 ms (containers), 1.07 ms (images), 0.71 ms (terminal) natively on one core (Apple M4 Max, BenchmarkFrame). The Networks view with 50 networks: 0.94 ms. That figure was taken under a 1-minute load of 12, so it is indicative, and the other views measured 0.94, 1.11 and 0.78 ms in the same run. The views only redraw when something changed: a refresh that finds the engine as it was emits no list event.

dashboard.wasm (-trimpath -ldflags="-s -w") is 19.41 MB raw and 4.93 MB gzipped (v0.4.2, go-widgets/toolkit v0.345.1). It was 22.12 MB and 5.53 MB with toolkit v0.344.0, so toolkit v0.345.1 cuts it by 12.3 % raw and 11.0 % gzipped.

Testing

go test -race ./...                 # viewmodels, bridge, client, scene, a real engine
go test -tags e2e ./e2e/            # the page in Chromium and Firefox (Playwright)
go run ./cmd/site build --out dist && go run ./cmd/site serve --dir dist --isolate

CI: gofmt, vet, build, -race, exact block coverage of vm, engine and bridge (covblocks), fuzz smoke, govulncheck judged by vulngate, js/wasm vet and build, a cross-build matrix; the Pages workflow publishes the demo and checks the CDN serves the build.

License

BSD-3-Clause — see LICENSE.

Directories

Path Synopsis
Package bridge carries HTTP requests between two JavaScript realms over postMessage: the dashboard runs on the page (it needs the canvas) and the engine in a Web Worker (it needs Atomics.wait), and the dashboard's API client — and wctr, run in the dashboard's terminal — reach the engine's API handler through this bridge.
Package bridge carries HTTP requests between two JavaScript realms over postMessage: the dashboard runs on the page (it needs the canvas) and the engine in a Web Worker (it needs Atomics.wait), and the dashboard's API client — and wctr, run in the dashboard's terminal — reach the engine's API handler through this bridge.
cmd
dashboard command
Command dashboard is a browser program (GOOS=js GOARCH=wasm); natively it only says so.
Command dashboard is a browser program (GOOS=js GOARCH=wasm); natively it only says so.
engine-worker command
Command engine-worker is the demo page's engine, a browser program (GOOS=js GOARCH=wasm, run in a Web Worker); natively it only says so.
Command engine-worker is the demo page's engine, a browser program (GOOS=js GOARCH=wasm, run in a Web Worker); natively it only says so.
site command
Command site builds and serves the dashboard's demo page.
Command site builds and serves the dashboard's demo page.
Package engine is what the dashboard needs from a container engine: the calls of an API compatible with the Docker Engine API, as the engine of go-wasmcontainers serves them.
Package engine is what the dashboard needs from a container engine: the calls of an API compatible with the Docker Engine API, as the engine of go-wasmcontainers serves them.
images
clock command
Command clock is a demo image: it prints a coloured, numbered line every second (or every -interval), forever or -n times, so there are logs to follow and a container to stop.
Command clock is a demo image: it prints a coloured, numbered line every second (or every -interval), forever or -n times, so there are logs to follow and a container to stop.
fail command
Command fail is a demo image: it reports an error on stderr and exits with status 3, to show an exit code in the dashboard.
Command fail is a demo image: it reports an error on stderr and exits with status 3, to show an exit code in the dashboard.
hello command
Command hello is a demo image: it greets and exits.
Command hello is a demo image: it greets and exits.
internal
images
Package images builds the demo images and serves them the way a registry does, as static files: the demo page's same-origin /v2 mirror and the tests' in-process registry are the same bytes.
Package images builds the demo images and serves them the way a registry does, as static files: the demo page's same-origin /v2 mirror and the tests' in-process registry are the same bytes.
site
Package site builds the dashboard's standalone demo page: the page and its scripts, the dashboard and the engine compiled for js/wasm, the container worker, and a same-origin static registry (/v2) holding the demo images.
Package site builds the dashboard's standalone demo page: the page and its scripts, the dashboard and the engine compiled for js/wasm, the container worker, and a same-origin static registry (/v2) holding the demo images.
testengine
Package testengine runs a real go-wasmcontainers engine in the test's process: the daemon with the wazero executor and an in-memory image store, its API handler, and a registry serving the demo images (built for wasip1 at test time).
Package testengine runs a real go-wasmcontainers engine in the test's process: the daemon with the wazero executor and an in-memory image store, its API handler, and a registry serving the demo images (built for wasip1 at test time).
Package ui is the dashboard's view: go-widgets bound to the viewmodels of package vm.
Package ui is the dashboard's view: go-widgets bound to the viewmodels of package vm.
Package vm holds the dashboard's viewmodels: pure Go state over the engine.Engine interface, natively testable, with no widget in sight.
Package vm holds the dashboard's viewmodels: pure Go state over the engine.Engine interface, natively testable, with no widget in sight.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL