07 — Water Tank

A real-time SCADA-style dashboard with a simulated water tank. The model is a background goroutine that ticks the level every 500 ms; the page polls every second to redraw the schematic. Pump and valve are clickable on the SVG and via buttons.

Interactivity level: 3 — Polling (whole page refresh) State scope: Global (server build — one shared simulation) / Individual (WASM build — each browser runs its own tank)

Stopped — tank at 0%, simulation idle
Stopped (level = 0%)
Running — pump filling, level 33%
Running (pump filling)
What changed between the two shots: the user pressed Start Simulation. The pump turns the inlet pipe blue (flow), the level climbs ~6%/s, and the navbar badge flips from Stopped to Running. The 95% / 5% dashed marks are the float-switch trip points; the pump auto-cuts at 95% and auto-restarts at 5%.

The simulation

Simulation is a tiny state machine: tank level, pump on/off, valve open/closed, plus a tick goroutine started from Start():

go func() {
    ticker := time.NewTicker(500 * time.Millisecond)
    for {
        select {
        case <-ctx.Done(): return
        case <-ticker.C:    s.tick()
        }
    }
}()

Each tick adds 3 if the pump is on, subtracts 1 if the valve is open, and trips the float switches at 5% and 95%. Stop() cancels the context; the goroutine exits cleanly.


Schematic — shared widget

The SVG is rendered by watertank.Render(state), shared across examples 07–11. The example's only schematic-related code is Snapshot(), which copies the simulation state into a watertank.State:

func (s *Simulation) Snapshot() watertank.State {
    s.mu.Lock(); defer s.mu.Unlock()
    return watertank.State{
        Level:     s.tankLevel,
        PumpOn:    s.pumpOn,
        ValveOpen: s.valveOpen,
        Running:   s.running,
        PumpHref:  "/pump",
        ValveHref: "/valve",
    }
}

The <a href> wrapping pump/valve in the SVG makes the schematic itself clickable — clicking the pump POSTs to /pump and toggles it.


Dual build: server and WASM

main.go and main_wasm.go share simulation.go. The server build uses app.HandleRoot/HandleDisplay for polling; the WASM build calls into the same Simulation via a thin JS bridge.

//go:build !(js && wasm)
func main() {
    sim := &Simulation{pumpOn: true}
    app := lofigui.NewApp()
    // ... register /, /start, /stop, /pump, /valve ...
    log.Fatal(http.ListenAndServe(":1347", nil))
}

Source on Codeberg


Running

task go-example:07         # server on :1347
task docs:capture:07       # capture the two screenshots above