feat: EMQX-flavor web dashboard with multi-user RBAC, SSE, and cluster-wide event aggregation#151
feat: EMQX-flavor web dashboard with multi-user RBAC, SSE, and cluster-wide event aggregation#151debsahu wants to merge 48 commits intowind-c:mainfrom
Conversation
|
Thanks for this impressive work — the dashboard is a major feature addition. I have one architectural suggestion before merge.
|
|
Yes will send update soon. |
|
Restructure pushed — Two quick questions while you're reviewing:
|
Add GET /api/v1/mqtt/sessions for paginated online + offline session
listing (with ?online=true|false filter) and DELETE
/api/v1/mqtt/sessions/{id} to disconnect a connected client. Offline
sessions are pulled from the storage backend through the new
Server.Hooks() accessor, which exposes the previously-unexported hook
bus so REST handlers can inspect stored state without the broker
reaching back into them.
DELETE is best-effort for v1: if the client is online it is
disconnected, otherwise the stored entry is acknowledged but
expiration is left to the existing hook eviction path.
Add the dashboard landing page rendering hero cards (Connections, Subscriptions, Retained, Inflight, Msg In/sec, Msg Out/sec) that refresh every 2s via htmx polling against a fragment endpoint. Per-second rates are derived in-process by RateSampler, which snapshots the cumulative MessagesReceived / MessagesSent counters once per second and reports the delta. SSE wiring lands in Phase 3.
Adds four detail pages to the web dashboard backed by the existing REST data-walking patterns: subscription list with topic/clientid filters, topic trie tree, retained-message list with admin-only clear, and online sessions list with admin-only disconnect.
- Vercel theme via CSS variables (light + dark): replaces brand/slate/rose/
emerald palette with a monochrome shadcn-style token system. .dark is
safelisted so the rule survives Tailwind's purge pass.
- Cluster topology SVG on Overview: monochrome ring layout with leader
crown, follower hollow rings, "this node" halo, full-mesh edges.
Sized 240x160 to fit alongside the cards.
- (*cluster.Agent).Leader() now falls back to matching the leader's
address against the membership list when raft.LocalID does not equal
the discovery node name. Fixes the "all nodes show as follower" bug.
- Per-page template tree isolation: each page parses with shared
partials + its own file so the {{define "content"}} block stops
colliding across pages (was rendering the wrong page after the last
alphabetical template won the global dispatch).
- Flash field added on subscriptions/topics/retained/sessions page data
structs so _flash.html no longer trips on missing fields.
- Retained page: hide $SYS topics by default, with a checkbox to bring
them back via ?include_sys=1.
- Logout cookie attributes now match login (HttpOnly, SameSite=Lax,
Secure when TLS) so strict browsers actually expire the session.
- Mobile-friendly nav drawer: hamburger top-bar on screens narrower
than lg, slide-in nav with backdrop, auto-collapse on link click,
Esc-to-close.
- Account link in the sidebar so viewer-role users can find their
profile.
Routes() spawned a RateSampler ticker goroutine (and a redis pub/sub Bridge in cluster mode) but never exposed a way to stop them. CI's TestLeaks in cmd/single caught the leaked goroutine. Return a cleanup func from Routes() and invoke it from cmd/single and cmd/cluster before server.Close(). Update dashboard tests to defer cleanup so they don't leak either.
Move mqtt/dashboard/ to dashboard/ at the module root, sibling to cluster/. Per-review-feedback structural change: the dashboard is a self-contained subsystem (auth, routing, templates, SSE) and not a broker hook, so it does not belong nested under mqtt/. Pure rename — no logic changes. Imports, Makefile asset paths, tailwind content glob, theme.js header, and one config.go doc-comment reference updated accordingly.
4a897ea to
22ee912
Compare
|
Thanks for the quick restructure — top-level 1. Dashboard colors / layoutI'd recommend not following the EMQX palette directly — comqtt should have its own visual identity rather than mirroring a competitor's style. Suggested palette (IoT-Blue):
Why Sky Blue over Indigo:
Layout density: Prefer compact/tight spacing. The dashboard is an ops tool — information density over comfort. Data-table-heavy view is fine. 2. Chroma dependencyKeep it. 1 MB on a 40 MB binary is ~2.5% — negligible. YAML syntax highlighting on the Settings page is a high-frequency ops workflow where readability matters. Chroma is pure Go, no CGO, well-maintained — no concerns. |
Replaces the monochrome Vercel/tweakcn theme with an IoT-Blue palette per upstream review feedback on wind-c#151: - Brand accent (--primary, --ring): Sky 500 (#0EA5E9 light / Sky 400 #38BDF8 dark) — distinct from EMQX's Indigo, evokes IoT/connectivity. - Foreground: Slate 900 (#0F172A) — high-contrast on Slate 50 in light mode; inverts in dark mode. - Background: Slate 50 (#F8FAFC) light / Slate 900 dark. - Cards: white light / Slate 800 dark for elevation. - Status colors: Emerald 500 success, Amber 500 warning (newly added --warning + --warning-foreground tokens), Red 500 destructive. - Borders + inputs use Slate 200 / Slate 700. Native <select> dropdown chrome doesn't follow the theme — the OS draws its own arrow and option bg. Adds a base-layer rule that strips OS appearance and paints a Slate-tinted SVG chevron, with a brighter chevron variant under .dark. Background defaults to --card so the control reads as a surface element, with a Sky focus ring matching links + buttons. Templates that hard-coded `bg-input` on selects (users, tools, sessions) are updated to drop that class so the new base-layer fill takes effect; the border-input class stays for the 1px outline. dashboard/static/tailwind.css regenerated from input.css + config via `make dashboard`.
|
For local testing of the latest push (IoT-Blue palette + themed select chrome): # 1. Fetch the PR branch
gh pr checkout 151
# or, without gh:
# git fetch origin pull/151/head:pr-151 && git checkout pr-151
# 2. Build (static dashboard assets are committed, so plain `go build` works)
go build -o ./comqtt ./cmd/single
# 3. Run with a known admin password
DASHBOARD_INITIAL_PASSWORD=preview ./comqtt --storage-way=0
# 4. Open in a browser
open http://localhost:8080/dashboard/
# username: admin password: previewTo populate the views (Topics, Subscriptions, Retained, and the Overview SSE feed), in a second terminal (needs # Persistent subscribers
mosquitto_sub -h localhost -i sub-sensors -t 'sensors/#' -q 1 &
mosquitto_sub -h localhost -i sub-status -t 'devices/+/status' -q 0 &
mosquitto_sub -h localhost -i sub-events -t 'events/#' &
# Retained seeds
for t in sensors/temp/room1 sensors/temp/room2 sensors/humidity/room1 \
devices/d1/status devices/d2/status events/login; do
mosquitto_pub -h localhost -t "$t" -m "{\"v\":$RANDOM}" -r -q 1
done
# Continuous traffic for the Recent Events feed and sparklines
while true; do
mosquitto_pub -h localhost -t "events/burst/$((RANDOM % 10))" -m "tick-$RANDOM"
sleep 2
doneThings to look at:
Layout density (your "compact, data-table-heavy" preference) is the next pass. Happy to send that as a separate commit if the palette looks right first. |
|
The dashboard you've implemented feels overly heavyweight. |
|
Thanks for taking the time to look at this carefully and for the honest feedback. The scope concern is fair, and I'd rather get the direction right than push something into the codebase that you'll regret carrying. Two paths I can offer, whichever fits comqtt's direction better. Option 1: Slim down the PR significantly. Trim aggressively to a minimal core: file-based single-user auth, six pages (Login, Overview, Clients, Subscriptions, Topics, Retained), polling instead of SSE, no cluster event aggregation, no Settings page (drops the chroma dep), no Tools page, no Users page, no sparklines. That brings the diff from roughly 8.9k LOC to around 3k LOC and removes most of what felt heavyweight. The cost is that Phase 3 and Phase 4 features (realtime, cluster polish, multi-user RBAC) do not ship; users who want them later need a follow-up PR. Option 2: Keep comqtt lightweight; ship the dashboard as a separate add-on. The dashboard touches comqtt at exactly one place: a 5-line
Option 2 is what I would recommend if your read is that comqtt should stay focused on the broker itself. It directly addresses the "code volume surpasses comqtt" point: your repo gains five lines, not 8.9k. It also lets the dashboard iterate on its own cadence without burdening your review bandwidth. Your call either way. If you want to explore Option 1 first, I can prepare a slimmed branch so you can compare; if Option 2 is the right framing, I'll close this PR and open the small |
|
I support Option 2! |
|
Thanks. Proceeding with Option 2. Status:
Next: I will scaffold |
Web dashboard add-on for comqtt MQTT broker, shipped as a separate Go module so stock comqtt stays focused on the broker. Originally proposed in wind-c/comqtt#151. Per maintainer preference, keeping comqtt lightweight, the dashboard moved here. This v0.1.0 covers single-mode broker only: - dashboard/ package: auth (HMAC-signed cookie sessions, file + Redis cred stores, lockout, CSRF, RBAC middleware), handlers, SSE pipeline (ring-buffered hub, mqtt-hook bridge, /dashboard/events handler), HTML templates, embedded static assets (htmx, htmx-sse, tailwind.css, theme.js). - rest/ package: dashboard-specific REST endpoints layered on top of upstream comqtt /api/v1/mqtt/* (paginated client list with prefix search, subscriptions, topics tree, retained list with payload preview, sessions; plus DELETE for unsubscribe / clear-retained / disconnect-session). - cmd/comqtt-dashboard: single-mode broker driver wiring upstream comqtt (storage, auth, bridge, listeners) + the dashboard. Drop-in replacement for upstream comqtt-single on the same flags. Build is fully self-contained: go build produces a 40 MB single binary including all dashboard assets. Tests: 151 pass under -race across dashboard and rest packages. Soft-fork note: go.mod uses a replace directive to pin comqtt at the same commit pushed in wind-c/comqtt#158 (Server.Hooks() accessor). The replace will be dropped once an upstream release includes the accessor. Deferred to v0.2.0: - Cluster-mode binary with cluster/rest mirrors for cross-node aggregation. - Helm chart (deploy/helm/comqtt-dashboard/). - CI workflows + Docker image publishing.
|
Closing per the plan we agreed on: the dashboard moved to a separate add-on module so stock comqtt stays focused on the broker.
The only upstream change the add-on needs is the small Thanks again for the careful review and for nudging this in a direction that keeps comqtt lightweight. |
|
Perfect, looking forward to it! |
The HTTPRoute now targets the broker's HTTP listener (REST API + /metrics on port 8080) rather than a dashboard, since wind-c#151 closed and the dashboard moved to a separate add-on module. Renames: - values.yaml: gateway.dashboard -> gateway.api - HTTPRoute resource name: <release>-dashboard -> <release>-api - README, NOTES.txt, schema, sub-path limitation updated to match - Chart.yaml changelog reframed (REST API + metrics, not dashboard) The HTTPRoute itself is still useful for users who want to expose the REST API or /metrics through Gateway API, just no longer dashboard-specific.
EMQX-flavor web dashboard embedded in the comqtt broker. Closes the Roadmap "Dashboard" entry. 41 commits, ~8.9k LOC, all tests pass under
-race.What's in
Backend (Phase 1, 17 commits)
mqtt/dashboard/auth/): HMAC-signed cookie sessions (~30 LOC, no JWT), file + Redis multi-user credential stores, in-memory lockout tracker, CSRF tokens, RBAC middleware (admin/viewer roles).GET /clients(search by ClientID prefix),GET /subscriptions(filter by topic + clientid),GET /topics(tree),GET /retained(with optional payload preview, 4 KiB cap),GET /sessions. PlusDELETE /clients/{id}/subscriptions/{topic},DELETE /retained/{topic},DELETE /sessions/{id}.mqtt/dashboard/sse/): ring-buffered hub + mqtt-hook bridge + handler at/dashboard/events. The hub fan-outs are non-blocking with drop-on-full so the broker hot path can never stall.cluster/rest/for every new endpoint, fan-out via the existingfetchMhelper.(*mqtt.Server).Hooks()accessor (5 lines) so REST handlers can inspect storage-backed sessions without monkey-patching the broker package.Static UI (Phase 2, 15 commits)
html/template+ htmx (vendored, ~50 KB) + Tailwind v3 (built by standalone CLI binary, no Node toolchain). All assets bundled viago:embed.go buildproduces a single binary.localStorage.:8080listener alongside/api/v1/*./redirects to/dashboard/.Realtime + detail (Phase 3, 4 commits)
htmx-ssedirect DOM swap;?as=jsonreturns raw events for programmatic consumers.Cluster polish (Phase 4, 6 commits)
WATCH/MULTI/EXECfor atomic mutations.EnsureSecretSETNX-with-race-loss-reread.comqtt:dashboard:eventsand subscribes to receive peer events. Self-echo filter + 4096-entry FIFO LRU dedup. The full pipeline is exercised by an integration test that spins two in-process brokers + miniredis and observes a connect on node-A from node-B's SSE stream.cmd/cluster/main.goplumbs the redis client through to the dashboard so cluster mode actually uses these components when redis is configured (storage-way=3 or auth-ds=1).Hardening (Phase 6)
go test -race ./mqtt/dashboard/... ./mqtt/rest/...is clean across all dashboard packages. 127 tests under race detector.RequireRole(RoleAdmin)middleware (defense in depth at the server) AND every state-changing button renders withdisabledattribute whenUser.Role != \"admin\"(UX layer). The viewer can browse every page and observe state, but can't mutate.Tested
go test -race ./mqtt/dashboard/... ./mqtt/rest/...go test -tags=integration ./mqtt/dashboard/... -run TestClustergo build ./cmd/single ./cmd/clusterConfiguration
New keys under
dashboard:in the broker config:Plus
DASHBOARD_INITIAL_PASSWORDenv var if you want to skip the random password printed on first boot.In cluster mode: the dashboard reads the same
cfg.Redissettings the broker already uses forstorage-way=3. No extra redis config needed.Deliberately deferred
These were all in scope per the design doc but pushed out for review tractability:
POST /api/v1/cluster/peersendpoint.dashboard.*values + Secret-mounted session secret to the chart.File breakdown
Approximately:
mqtt/dashboard/(auth, sse, handlers, dashboard.go, embed.go)mqtt/rest/cluster/rest/rest.gocmd/single/main.go,cmd/cluster/main.go,mqtt/server.go,cluster/agent.go,config/config.gogithub.com/alecthomas/chroma/v2(pure Go, ~1 MB binary bloat) for Settings page YAML highlighting.Out of scope for this PR (already filed or to be filed)
dashboard.*values block (follow-up PR after feat(deploy): add Helm chart, GoReleaser, and GHCR release workflow (closes #137) #150 merges)ConnectedAttimestamp on the Client detail page (currently 0 placeholder)How to try locally