Skip to content

Dashboard phase 2: read-only web UI, sign-in and API additions - #581

Merged
abahmed merged 5 commits into
mainfrom
feat/dashboard-ui
Oct 28, 2026
Merged

abahmed merged 5 commits into
mainfrom
feat/dashboard-ui

Conversation

@abahmed

@abahmed abahmed commented Oct 28, 2026 •

Copy link
Copy Markdown
Owner

Phase 2 of the dashboard (spec packages/brand/specs/dashboard-phase2.md, sections 5, 7, 7a, 7b; approved mockup).

API (internal/dashboard)

  • POST /login {"password"} → 204 + cookie kwatch_session (HttpOnly, SameSite=Strict, Path=/, 12h, no sliding; Secure only in shared mode). 401 wrong, 429 + Retry-After (shared failure limiter), 400 non-JSON, 403 cross-origin. POST /logout revokes. GET /api/session 204/401.
  • Cookie is HMAC-signed with a key derived from password+token: a per-start random part means a password reset or a kwatch restart signs everyone out. Failures are limited per connecting peer (100/min behind a trusted proxy) and per forwarded client (10/min). Password compared in constant time, dashes/case ignored.
  • Shared mode: /login and /api/session answer 403 https_required unless kwatch served TLS itself (tls.secretName) or, with trustProxy, X-Forwarded-Proto: https from a non-loopback peer. Failures are counted per last X-Forwarded-For hop behind the Ingress.
  • /api/* accepts the cookie or the bearer token. A request with no credential no longer counts as a failure.
  • List: q, severity (repeatable), state (repeatable; active|resolved picks the list, settling|open|recovering|flapping narrow it), since (duration or RFC 3339), counts by severity; Active = Urgent first, silent hidden (D4); Resolved by resolvedAt (D5). GET /api/namespaces?state=.
  • Detail: workloadKind, fixSteps/fixCommands (stored from the message steps, persisted additively), timeline kind + resolved reason.
  • Status: lastScan is now the engine's last loop (it was the request time), lastScanLate, scanEverySeconds, kwatchNamespace, kwatchDeployment (D6); channel type and name.
  • Skipped: storage flag (GAP 8), paging (GAP 9), ackedAt (GAP 12): the final screens don't use them (no ack in UI per 7a).

Bugs fixed from phase 1

  • suggestedFix returned incident.Fix (how it ended, e.g. "rollback"), not advice. Now the message steps.
  • Chart dashboard.enabled never turned the server on (only config.yaml did). The chart now passes KWATCH_DASHBOARD_ENABLED/PORT/MODE.

Web UI (internal/dashboard/web)

Plain HTML templates + vanilla JS (5 small files) + CSS, no build step: the repo has no web toolchain and the UI is small, so a framework would add a build and supply chain for no gain. go:embed, content-hashed names under /assets/ (1-year immutable), HTML/API no-store. CSP default-src 'none'; script-src 'self'; style-src 'self'; ... on every response; no inline script/style, API text via textContent only; brand woff2 fonts embedded (OFL texts shipped). Screens and states per mockup: sign in, active, detail, resolved, status; filters with More filters, hidden under 5 issues; 20 s live refresh paused when hidden; New highlight; aria-live for new Urgent; back link to the list you came from; phone order; one-line commands with Copy; tab title and favicon dot; light/dark from the system; [hidden] wins.

D8

  • digest = "Low": true (StatusLow/MarkerLow).
  • flapping "stays quiet unless it changes": mostly true: one update on entering, then only when it grows; weekly reminders still go out. Wording kept, flag for designer.
  • recovering "a little": true (resolve hold).
  • "Secrets are removed": overclaims; internal/redact replaces known patterns with [redacted]. UI now says "Known secret patterns are removed."
  • New resolve reason stopped_tracking shown as "kwatch stopped tracking it: …"; recovered shown as "Resolved: It stayed healthy." (needs designer review).

For kwatch-web (installer)

The chart never exposes the dashboard; the installer creates Ingress / HTTPRoute / VirtualService / Route / LoadBalancer or NodePort Service itself, pointing at Service <release>-dashboard (ClusterIP, port name dashboard, port 8061).

Values:

  • dashboard.enabled (false), dashboard.port (8061)
  • dashboard.mode: local (port-forward) | shared (https only, Secure cookie; render fails unless one of the next two is set)
  • dashboard.trustProxy: true when an Ingress/route/proxy terminates TLS and sets X-Forwarded-Proto
  • dashboard.tls.secretName: kubernetes.io/tls Secret kwatch serves https from itself (LB/NodePort with nothing in front); renewals reloaded
  • dashboard.existingSecret, dashboard.networkPolicyFrom (extra NetworkPolicy peers for the port; namespace + healthIngressFrom always allowed)

Secret <release>-dashboard (kwatch-dashboard): keys password (XXXX-XXXX-XXXX-XXXX, alphabet ABCDEFGHJKLMNPQRSTUVWXYZ23456789) and token; generated from crypto random bytes, kept across upgrades, annotated helm.sh/resource-policy: keep (survives uninstall/turn-off; GitOps should use existingSecret). Don't pre-create it unowned (Helm adoption fails) unless via existingSecret. Reset: patch password, kubectl rollout restart deploy/kwatch. Local: kubectl -n $NS port-forward svc/kwatch-dashboard <local>:8061.

Tests

Handler tests for sessions, expiry, reset, logout, shared https, origin, rate limit, filters, counts, namespaces, new fields, event kinds, scan health; static tests (no external URLs, no inline code, hashed assets, CSP). Chart template tests + lifecycle check that password/token survive helm upgrade. New scripts/test-kind-dashboard.sh in the operational e2e job: installs with the chart, signs in with the Secret's password, loads /api/incidents and /api/status with the cookie, signs out, then shared mode refuses plain http.

Local: dashboard/config/incident/app tests pass, test_helm.sh passes, shellcheck/actionlint clean. Local make ci got through verify (tests, coverage, negative regressions) but was stopped at the low-disk point before licensetest; CI is the gate. UI checked in a browser against fake data (sign in, list, detail, resolved, status).

Password sign-in with a signed 12h session cookie (Secure in shared
mode, sign-in refused over plain http), server-side list filters,
namespace list, severity counts, timeline kinds, fix steps, scan
health and channel names. Embedded UI with hashed assets and a strict
CSP. Chart: dashboard.mode, password in the dashboard Secret, Ingress
for shared mode. Kind dashboard check in the operational e2e job.
@abahmed abahmed added the e2e Run the real-cluster E2E suite on this pull request label Oct 28, 2026
dashboard.expose: local, service, ingress (nginx, ALB, GKE, AGIC,
Tailscale, ngrok helpers), gateway, istio, route, loadBalancer and
nodePort (kwatch serves TLS itself, renewals reloaded), external.
Shared ways are https only; X-Forwarded-Proto is trusted only behind a
proxy. allowedSourceRanges where the platform enforces it. The render
fails with a plain message when a required value is missing.
The chart keeps the ClusterIP Service, the password and token Secret,
dashboard.mode, trustProxy and tls.secretName. Ingress, Gateway,
Istio, Route, LoadBalancer and NodePort objects are created by the
kwatch.sh Dashboard menu or by the operator.
Password from crypto random bytes; the Secret is kept on uninstall and
GitOps users are pointed at existingSecret. Failures count against the
connecting peer as well as the forwarded client. The session key gains
a per-start random part, the revoked set evicts the oldest entry, and
the listener opens with the lifecycle context.
@abahmed
abahmed merged commit 620d39c into main Oct 28, 2026
24 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

e2e Run the real-cluster E2E suite on this pull request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant