Files
harmony/fleet/harmony-fleet-operator/README.md

134 lines
5.3 KiB
Markdown

# 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](https://htmx.org/) + 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:
```rust
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`](src/service/mod.rs), 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)
```sh
# 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:
```sh
# 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`](src/service/mock.rs), 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
```sh
# 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.