10 — Water Tank Maintenance

Extends 09 with long-running background maintenance goroutines on the pump and valve. Each maintenance op runs N steps × 1 s, updating progress under a mutex; HTMX picks up the progress on its existing 1 s poll. Cancellation is via context.Context. While maintenance is running, the affected equipment is locked out — buttons disabled, float-switch suppressed, an orange dashed ring drawn on the schematic.

Interactivity level: 5 — HTMX partial updates (with background ops) State scope: Global (server build) / Individual (WASM build — each browser runs its own maintenance ops)

Idle — simulation running, no maintenance
Idle — sim running, no maintenance
Pump maintenance running — orange ring + progress bar
Pump maintenance in progress
What changed: the user clicked Pump Maintenance. A goroutine kicks off a multi-step procedure ("Inspecting impeller", …) that emits progress every second; the orange dashed ring on the pump and the "MAINT 38%" caption come from the shared watertank widget reading State.MaintType / State.MaintProgress. The Pump On button is disabled (lockout); the float-switch stops auto-toggling the pump too.

The maintenance goroutine

runMaintenance walks a list of steps, sleeping 1 s between each. At every step it records progress and a log line under the simulation mutex, then selects on cancellation:

func (s *Simulation) runMaintenance(ctx context.Context, kind string) {
    steps := stepsFor(kind)
    for i, step := range steps {
        select {
        case <-ctx.Done():
            s.mu.Lock()
            s.maintStatus = "cancelled"
            s.mu.Unlock()
            return
        case <-time.After(1 * time.Second):
            s.mu.Lock()
            s.maintProgress = float64(i+1) / float64(len(steps)) * 100
            s.appendLog(step)
            s.mu.Unlock()
        }
    }
    // … set status = "completed"
}

StartMaintenance rejects a second concurrent op with an error; CancelMaintenance calls the context's cancel(), which the select above picks up.


Equipment lockout

TogglePump/ToggleValve early-return when their equipment is under maintenance. The float-switch logic is suppressed for the same equipment — water can rise past 95% during a pump maintenance window without the pump auto-shutting (because it's already off and locked out).

The schematic reflects all this without adding any rendering code in the example: Snapshot() just sets MaintType/MaintProgress on the watertank.State, and the widget draws the orange ring and MAINT n% label.


Cancellation, end to end

Three independent context.Contexts are at play:

  • The HTTP request context (r.Context()) — auto-cancelled when the client disconnects.
  • The simulation tick goroutine — cancelled by Stop().
  • The maintenance goroutine — cancelled by CancelMaintenance() or by stopping the simulation.

All three terminate cleanly via select { case <-ctx.Done(): ... }. There's no shared "I'm done" flag; the context tree is the single source of truth.

Source on Codeberg


Running

task go-example:10         # server on :1349
task docs:capture:10       # capture the two screenshots above