Skip to content

feat: add optional web UI dashboard#18

Open
RaghavSood wants to merge 2 commits into
mainfrom
feat/web-ui-dashboard
Open

feat: add optional web UI dashboard#18
RaghavSood wants to merge 2 commits into
mainfrom
feat/web-ui-dashboard

Conversation

@RaghavSood

@RaghavSood RaghavSood commented Mar 10, 2026

Copy link
Copy Markdown
Collaborator

Summary

  • Adds an embedded web dashboard at /ui for real-time queue monitoring, vault/device browsing, and sending test notifications
  • Gated behind ui: true config flag (default false) - all /ui routes return 404 when disabled
  • Deploy yaml dropped (deployment scripts were removed from the repo in a subsequent PR)
  • Static assets embedded via //go:embed - no runtime file path dependency, works in Docker

Changes

  • config/config.go - UI bool field with env/config support
  • storage/database.go - ListAllVaults() and ListDevicesByVault() query methods
  • api/server.go - inspector + uiEnabled fields, conditional route registration
  • api/ui_handler.go - UI routes: index page, queue JSON, vault/device APIs, SSE stream
  • cmd/server/main.go - Creates asynq.Inspector when UI enabled, passes to server
  • ui/ - Embedded assets: HTML template (dark Tailwind theme), vanilla JS, pre-built CSS

Key design decisions

  • SSE (EventSource) for queue updates - simpler than WebSocket for read-only streams
  • //go:embed for all assets - no runtime file path dependency, works in Docker
  • Token field omitted from device API responses for security
  • Reuses existing /notify endpoint for sending notifications

Test plan

  • go build ./... passes
  • Run with UI=true, open http://localhost:<port>/ui
  • Queue cards display (show - until worker starts - expected)
  • Vault table loads with registered vaults and device counts
  • Static assets (CSS, JS) served correctly from embedded FS
  • Run with UI=false (default), confirm /ui returns 404

receipts

dashboard landing page - queue status cards + vault registry (2 vaults registered, 1 and 2 devices each) + send notification form:
https://files.catbox.moe/wmj588.png

full page view - all three sections rendered after vault data loads:
https://files.catbox.moe/va89rp.png

/ui/api/vaults JSON (token omitted from device API - security design correct):

[{"vault_id":"prod-vault-xyz789","device_count":1},{"vault_id":"test-vault-abc123","device_count":2}]

/ui/api/vaults/test-vault-abc123/devices JSON (no token field in response):

[{"party_name":"iPhone-14","device_type":"iOS","created_at":"2026-05-31T12:34:03.546+02:00"},{"party_name":"MacBook-Pro","device_type":"iOS","created_at":"2026-05-31T12:34:03.508+02:00"}]

UI=false gating - GET /ui returns HTTP 404 (curl confirmed)

static assets - /ui/static/dist/output.css HTTP 200, /ui/static/src/app.js HTTP 200

Also rebased onto current main (branch was cut before MySQL migration + websocket fixes + vault_id widening), and fixed the ignored fs.Sub error flagged by CodeRabbit (cb74af5).

🤖 Generated with Claude Code

@coderabbitai

coderabbitai Bot commented Mar 10, 2026

Copy link
Copy Markdown

Warning

Review limit reached

@gomesalexandre, we couldn't start this review because you've reached your PR review rate limit.

More reviews will be available in 49 minutes and 50 seconds. Learn how PR review limits work.

Your organization has run out of usage credits. Purchase more in the billing tab.

⌛ How to resolve this issue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans include higher PR review limits than trial, open-source, and free plans. In all cases, reviews become available again over time. During sustained high-volume PR review activity, CodeRabbit may temporarily slow when the next review becomes available.

Please see our Fair Usage Limits Policy for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 409b108d-8209-470e-a37c-ad78ac32f56c

📥 Commits

Reviewing files that changed from the base of the PR and between ea7ad4d and cb74af5.

⛔ Files ignored due to path filters (1)
  • ui/dist/output.css is excluded by !**/dist/**
📒 Files selected for processing (10)
  • api/server.go
  • api/ui_handler.go
  • cmd/server/main.go
  • config/config.go
  • storage/database.go
  • ui/embed.go
  • ui/src/app.js
  • ui/src/input.css
  • ui/tailwind.config.js
  • ui/templates/index.html

Walkthrough

The changes introduce a UI feature for the notification and queue management system, including backend API handlers for queue info and vault data retrieval, database methods for vault queries, frontend assets with real-time SSE-based queue updates, and configuration support for enabling the UI.

Changes

Cohort / File(s) Summary
Backend API Server
api/server.go, api/ui_handler.go, cmd/server/main.go
Server struct extended with inspector and uiEnabled fields. NewServer constructor updated to accept these parameters. New ui_handler module introduces HTTP route registration for UI pages, queue info, vaults, vault devices, and SSE queue stream with periodic updates. Server initialization in main conditionally creates asynq.Inspector when UI is enabled and passes it to NewServer.
Configuration
config/config.go
Added public UI boolean field to Config struct with mapstructure and JSON tags; default value set to false via viper.
Database
storage/database.go
Added VaultSummary struct with VaultID and DeviceCount fields. Introduced ListAllVaults method to query vault summaries grouped by vault_id with device counts. Introduced ListDevicesByVault method to retrieve devices for a specific vault, both with 5-second context timeouts.
UI/Frontend
ui/embed.go, ui/src/app.js, ui/src/input.css, ui/tailwind.config.js, ui/templates/index.html
Created embedded resources module (Templates and Static variables). Added app.js with SSE queue stream listener, vault/device loading and rendering, notification form submission, and HTML escaping utilities. Introduced Tailwind CSS input file with base directives and configuration. Created dashboard template with queue status, vaults table, and notification form sections.
Deployment Configuration
deploy/dev/01_notification.yaml
Added ui data key set to "true" in Kubernetes notification ConfigMap.
🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 20.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title 'feat: add optional web UI dashboard' directly and concisely describes the main change: adding a new optional web dashboard UI feature, which is the core objective of this PR.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/web-ui-dashboard

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (5)
api/server.go (1)

42-44: Consider adding validation for inspector when UI is enabled.

When uiEnabled is true, the inspector parameter should be non-nil since UI routes depend on it for queue inspection. Adding a validation check would prevent potential nil pointer dereferences in UI handlers.

🛡️ Proposed validation
 	if wsHandler == nil {
 		return nil, fmt.Errorf("ws handler is nil")
 	}
+	if uiEnabled && inspector == nil {
+		return nil, fmt.Errorf("inspector is required when UI is enabled")
+	}
 	return &Server{

Also applies to: 76-77

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@api/server.go` around lines 42 - 44, The Server constructor that accepts the
parameters (vapidPublicKey string, inspector *asynq.Inspector, uiEnabled bool)
must validate that inspector is non-nil when uiEnabled is true to avoid nil
pointer derefs in UI routes; update the constructor (the function that returns
(*Server, error) and references inspector/uiEnabled) to check if uiEnabled &&
inspector == nil and return a clear error (e.g., fmt.Errorf("inspector required
when uiEnabled is true")) instead of proceeding, and add the same validation
wherever the same parameter combination is used (the other constructor/call site
noted around the same code) to ensure consistent behavior.
ui/src/app.js (3)

5-19: SSE connection lacks reconnect logic.

If the SSE connection drops (network issues, server restart), the queue status will stop updating with no visual indication to the user. EventSource automatically reconnects on some errors, but not all.

Consider adding onerror and onclose handlers with exponential backoff reconnection:

♻️ Suggested reconnect logic
  function connectQueueStream() {
    var es = new EventSource("/ui/api/queue/stream");
    es.onmessage = function (e) {
      try {
        var d = JSON.parse(e.data);
        document.getElementById("q-pending").textContent = d.pending;
        document.getElementById("q-active").textContent = d.active;
        document.getElementById("q-completed").textContent = d.completed;
        document.getElementById("q-retry").textContent = d.retry;
        document.getElementById("q-archived").textContent = d.archived;
        document.getElementById("q-failed").textContent = d.failed;
      } catch (_) {}
    };
+   es.onerror = function () {
+     es.close();
+     setTimeout(connectQueueStream, 3000);
+   };
  }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@ui/src/app.js` around lines 5 - 19, connectQueueStream currently opens an
EventSource but lacks robust reconnect handling; implement onerror (and onclose
if needed) handlers inside connectQueueStream to detect drops, close the current
EventSource, and retry connection with exponential backoff (e.g., start 1s,
double up to a max) while resetting the backoff when onmessage succeeds; ensure
you store the EventSource instance in a variable scoped to connectQueueStream so
you can call close() before reconnecting and avoid creating multiple concurrent
connections, and also provide a max retry or a visible state update if
reconnection repeatedly fails.

24-33: Silent error handling on vault fetch may confuse users.

When loadVaults() fails, the catch block is empty and the UI remains in "Loading..." state indefinitely. Consider showing an error state:

♻️ Show error state on vault load failure
      .catch(function () {
+       var tbody = document.getElementById("vault-table-body");
+       tbody.innerHTML = '<tr><td colspan="3" class="px-4 py-3 text-red-400">Failed to load vaults</td></tr>';
      });
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@ui/src/app.js` around lines 24 - 33, The fetch in loadVaults currently
swallows errors; update the catch block in loadVaults to handle failures by
logging the error (console.error) and updating UI state so the "Loading..."
doesn't persist — for example set vaults = [], call renderVaultTable() (and/or
populateVaultSelect()) to re-render an empty state, and surface a user-visible
error message (e.g., show or set text on an existing error element or add a
simple vault load error string) so users see the failure instead of a stuck
loader.

187-189: escapeAttr is incomplete but safe in current usage.

The function escapes & and " but not ', <, or >. Since all data attributes in the template use double quotes, this is currently safe. For defensive completeness, consider escaping additional characters:

♻️ More complete attribute escaping
  function escapeAttr(s) {
-   return (s || "").replace(/&/g, "&amp;").replace(/"/g, "&quot;");
+   return (s || "").replace(/&/g, "&amp;").replace(/"/g, "&quot;").replace(/'/g, "&#39;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
  }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@ui/src/app.js` around lines 187 - 189, escapeAttr currently only replaces &
and " which is fragile; update the escapeAttr function to defensively escape the
other common HTML-meaningful characters (at minimum <, > and ') in addition to &
and " so attribute values cannot break out or inject HTML. Locate the escapeAttr
function and change its replacement logic to map &[amp], " to &quot;, ' to &#39;
(or &apos;), < to &lt;, and > to &gt; (e.g. a single regex replace using a
lookup map) while still handling null/undefined by returning an empty string.
api/ui_handler.go (1)

27-36: Template is parsed on every request.

template.ParseFS is called on each request to /ui. While acceptable for a low-traffic dev dashboard, consider parsing once at startup or using sync.Once for better performance if traffic increases.

♻️ Cache template at server level

Add a field to the Server struct and parse during initialization:

// In server.go or during NewServer
uiTemplate *template.Template
 func (s *Server) uiIndex(c echo.Context) error {
-	tmpl, err := template.ParseFS(ui.Templates, "templates/index.html")
-	if err != nil {
-		s.logger.Errorf("Failed to parse UI template: %v", err)
-		return c.String(http.StatusInternalServerError, "template error")
-	}
 	c.Response().Header().Set("Content-Type", "text/html; charset=utf-8")
 	c.Response().WriteHeader(http.StatusOK)
-	return tmpl.Execute(c.Response().Writer, nil)
+	return s.uiTemplate.Execute(c.Response().Writer, nil)
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@api/ui_handler.go` around lines 27 - 36, The ui template is being parsed on
every request in uiIndex via template.ParseFS(ui.Templates,...); add a cached
template field on Server (e.g., uiTemplate *template.Template) and initialize it
once during server construction (NewServer or server initialization) by parsing
ui.Templates there (or protect lazy init with sync.Once), then change uiIndex to
use s.uiTemplate.Execute(...) and return an error if the cached template is nil
or execution fails.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@api/ui_handler.go`:
- Around line 15-25: In registerUIRoutes, stop ignoring the error returned by
fs.Sub when creating staticFS: change the call to capture (staticFS, err :=
fs.Sub(ui.Static, ".")). If err != nil, log the error via the server logger
(e.g., s.logger.Errorf or use echo's logger) and return (or fail fast) so you
don't register the static file handler with an invalid staticFS; only call
http.FileServer(http.FS(staticFS)) and register the "/ui/static/*" route when
err == nil.

---

Nitpick comments:
In `@api/server.go`:
- Around line 42-44: The Server constructor that accepts the parameters
(vapidPublicKey string, inspector *asynq.Inspector, uiEnabled bool) must
validate that inspector is non-nil when uiEnabled is true to avoid nil pointer
derefs in UI routes; update the constructor (the function that returns (*Server,
error) and references inspector/uiEnabled) to check if uiEnabled && inspector ==
nil and return a clear error (e.g., fmt.Errorf("inspector required when
uiEnabled is true")) instead of proceeding, and add the same validation wherever
the same parameter combination is used (the other constructor/call site noted
around the same code) to ensure consistent behavior.

In `@api/ui_handler.go`:
- Around line 27-36: The ui template is being parsed on every request in uiIndex
via template.ParseFS(ui.Templates,...); add a cached template field on Server
(e.g., uiTemplate *template.Template) and initialize it once during server
construction (NewServer or server initialization) by parsing ui.Templates there
(or protect lazy init with sync.Once), then change uiIndex to use
s.uiTemplate.Execute(...) and return an error if the cached template is nil or
execution fails.

In `@ui/src/app.js`:
- Around line 5-19: connectQueueStream currently opens an EventSource but lacks
robust reconnect handling; implement onerror (and onclose if needed) handlers
inside connectQueueStream to detect drops, close the current EventSource, and
retry connection with exponential backoff (e.g., start 1s, double up to a max)
while resetting the backoff when onmessage succeeds; ensure you store the
EventSource instance in a variable scoped to connectQueueStream so you can call
close() before reconnecting and avoid creating multiple concurrent connections,
and also provide a max retry or a visible state update if reconnection
repeatedly fails.
- Around line 24-33: The fetch in loadVaults currently swallows errors; update
the catch block in loadVaults to handle failures by logging the error
(console.error) and updating UI state so the "Loading..." doesn't persist — for
example set vaults = [], call renderVaultTable() (and/or populateVaultSelect())
to re-render an empty state, and surface a user-visible error message (e.g.,
show or set text on an existing error element or add a simple vault load error
string) so users see the failure instead of a stuck loader.
- Around line 187-189: escapeAttr currently only replaces & and " which is
fragile; update the escapeAttr function to defensively escape the other common
HTML-meaningful characters (at minimum <, > and ') in addition to & and " so
attribute values cannot break out or inject HTML. Locate the escapeAttr function
and change its replacement logic to map &[amp], " to &quot;, ' to &#39; (or
&apos;), < to &lt;, and > to &gt; (e.g. a single regex replace using a lookup
map) while still handling null/undefined by returning an empty string.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: f3aba03d-fd3a-4f0d-93f1-51c82f321182

📥 Commits

Reviewing files that changed from the base of the PR and between 60769d1 and ea7ad4d.

⛔ Files ignored due to path filters (1)
  • ui/dist/output.css is excluded by !**/dist/**
📒 Files selected for processing (11)
  • api/server.go
  • api/ui_handler.go
  • cmd/server/main.go
  • config/config.go
  • deploy/dev/01_notification.yaml
  • storage/database.go
  • ui/embed.go
  • ui/src/app.js
  • ui/src/input.css
  • ui/tailwind.config.js
  • ui/templates/index.html

Comment thread api/ui_handler.go

@johnnyluo johnnyluo left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

@realpaaao

Copy link
Copy Markdown

@RaghavSood Still intended to merge this one?

@RaghavSood

Copy link
Copy Markdown
Collaborator Author

Hmmm let me clean it up and deploy, need to add better auth handling if we intend to use it outside of dev

RaghavSood and others added 2 commits May 31, 2026 12:32
…tions

Add an embedded web dashboard (HTML + Tailwind + vanilla JS) gated behind
a `ui: true` config flag. Provides real-time queue status via SSE, vault/device
browsing, and a notification sending form. Enabled in dev deployment only.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Ignored error from fs.Sub would silently serve an invalid filesystem.
Now logs + returns early, so the static route is only registered when
the embedded FS is valid.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@gomesalexandre gomesalexandre force-pushed the feat/web-ui-dashboard branch from ea7ad4d to cb74af5 Compare May 31, 2026 10:39
@gomesalexandre

Copy link
Copy Markdown
Contributor

@RaghavSood took this off ur plate - rebased onto current main (branch was stale, cut before MySQL migration + websocket + vault_id widening PRs landed), fixed the ignored fs.Sub error flagged by CodeRabbit (cb74af5), dropped the removed deploy yaml, and added dashboard screenshots + API receipt.

ci green, still approved, ready to merge.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants