Files
harmony/fleet/harmony-fleet-operator

harmony-fleet-operator

IoT operator — reconciles Deployment CRDs into NATS KV desired-state and aggregates device/deployment state back into CR status.

Web frontend (optional)

A small server-side dashboard is built into the operator behind the web-frontend cargo feature. Stack: axum + maud (HTML-in-Rust) + vendored HTMX + Tailwind CSS. No WASM, no cargo-leptos, no JS build toolchain — cargo build --features web-frontend is the whole build.

Why this stack

Every interaction is an HTTP request that returns an HTML fragment, and HTMX swaps it into the DOM. There is no client-side state. The presentation layer is intentionally thin:

async fn devices_handler(State(s): State<AppState>) -> Result<Markup, AppError> {
    let devices = s.fleet.list_devices().await?;
    Ok(page("Devices", s.live_reload, devices_view::page(&devices)))
}

Each handler is extract state → call domain service → render Maud markup. All real work — listing devices, blacklisting, etc. — lives in service::FleetService, a trait the dashboard, tests, and a future CLI all share. Presentation never reaches past that trait.

Why Maud instead of Leptos? We don't use Leptos's reactivity (it's pure SSR + HTMX), so the runtime/macro footprint was dead weight. Maud is a compile-time HTML macro that produces a Markup value — smaller dep tree, faster compiles, same Rust-flavored ergonomics.

Why everything bundled? The operator already ships as a single container. Tailwind CSS, HTMX, and the small CSRF helper are embedded so air-gapped clusters need nothing extra to mount. The only build-time external is the standalone tailwindcss v4 CLI. A missing CLI produces a warning and empty embedded CSS; local development uses --css-from instead.

Running it locally (mock data, no NATS, no kube)

# One-time: install the standalone Tailwind v4 CLI (single static binary).
curl -L -o ~/.local/bin/tailwindcss \
  https://github.com/tailwindlabs/tailwindcss/releases/latest/download/tailwindcss-linux-x64
chmod +x ~/.local/bin/tailwindcss

Two terminals for the dev loop:

# Terminal 1 — Tailwind sidecar, regenerates CSS on every class change.
tailwindcss \
  -i fleet/harmony-fleet-operator/style/input.css \
  -o fleet/harmony-fleet-operator/style/dist/tailwind.css \
  --watch

# Terminal 2 — the operator, serving the dashboard against fake data and
# reading CSS from Tailwind's output. `--live-reload` reloads the browser
# tab whenever you restart the server.
cargo run -p harmony-fleet-operator --features web-frontend -- serve-web \
  --mock \
  --css-from fleet/harmony-fleet-operator/style/dist/tailwind.css \
  --live-reload

Open http://localhost:18080.

--mock uses MockFleetService, an in-memory seeded dataset (10 fake devices in mixed states, 4 deployments). You can blacklist a device and see its updated detail page. This exercises the same FleetService API as production without NATS or a Kubernetes cluster.

Iteration cost

Change Reload step
Tailwind class in a Maud template edit → save → refresh tab (Tailwind sidecar already rebuilt CSS; no Rust compile)
Maud template structure / handler logic edit → cargo run restarts → --live-reload auto-refreshes
FleetService types edit → cargo run restarts → tab auto-refreshes

The Rust recompile is the actual floor. Tailwind changes never trigger one.

Production builds

# Once, before cargo build: produce the embedded CSS.
tailwindcss \
  -i fleet/harmony-fleet-operator/style/input.css \
  -o fleet/harmony-fleet-operator/style/dist/tailwind.css \
  --minify

cargo build -p harmony-fleet-operator --features web-frontend --release

The release binary serves the embedded CSS unless you pass --css-from at runtime. (build.rs will also run tailwindcss if it's on PATH; the manual step above is just a guarantee that the embedded copy is correct.)

Layout

fleet/harmony-fleet-operator/
├── src/
│   ├── service/             ← domain abstraction (FleetService trait + Mock)
│   │   ├── mod.rs           ← trait + summary types
│   │   └── mock.rs          ← in-memory seeded data
│   └── frontend/            ← presentation layer (cfg web-frontend)
│       ├── server.rs        ← axum router + handlers
│       ├── layout.rs        ← page shell (Maud)
│       ├── assets.rs        ← embedded Tailwind/HTMX bytes
│       └── views/
│           ├── dashboard.rs
│           ├── devices.rs
│           └── deployments.rs
├── style/
│   └── input.css            ← Tailwind v4 entry point
└── vendor/
    ├── app.js               ← CSRF header helper
    └── htmx.min.js          ← HTMX v2.0.9

What's deferred

  • fleet-admin authorization. Zitadel login is implemented, but any authenticated tenant user can currently reach privileged dashboard routes.
  • Live log tail. Add a typed, authorized agent transport before exposing it.
  • Device commands. Define a bounded typed protocol and authorization before adding command controls.
  • Persistent alert state and receivers. Per-alert acknowledgement currently lives in operator memory; receiver configuration remains deployment-time.