Fullscreen cluster dashboard view consuming the Oumta Go backend. All data currently mocked — see plugin TODO.md for integration plan. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
9.7 KiB
oumta-dashboard — TODO
Everything in this file is work the plugin still needs to do before it's more than a mockup. Organised by phase.
Status
- Phase 0 (current): UI shell + mock data. Everything renders but no backend call is real.
- Phase 1: connect to real Go backend (
oumta-dashboardrepo) as endpoints become available. - Phase 2: Keycloak bearer auth, PHP proxy decision, persisted settings.
- Phase 3: nice-to-haves (card auto-creation on incident, etc.).
Phase 1 — wire up the real API
All API calls live in oumta-dashboard.js → api(). Every case currently returns a mock… helper; swap them for fetch against the backend. The URL is constructed from state.settings.baseUrl.
Endpoints to wire (listed in the order the UI consumes them)
| UI call | Backend endpoint (planned) | Status in backend | Notes |
|---|---|---|---|
api('/api/namespaces') |
GET /api/namespaces |
not yet implemented | returns the namespace tabs shown at the top. See "Open backend questions" below. |
api('/api/cluster/overview') |
GET /api/cluster/overview?namespace=… |
exists (stub) | current stub returns zeros. Also need namespace query param. |
api('/api/workloads') |
GET /api/workloads?namespace=…&state=…&since=… |
not yet | filters: state, since (ISO), also maybe search. |
api('/api/workloads/{ns}/{name}/timeline') |
GET /api/workloads/{ns}/{name}/timeline |
not yet | workload-detail timeline |
api('/api/timeline') |
GET /api/timeline?namespace=… |
not yet | global timeline |
Real fetch implementation sketch
In api() replace the mock dispatch with:
const url = new URL(path, state.settings.baseUrl);
if (!url.searchParams.has('namespace') && state.activeNamespace)
url.searchParams.set('namespace', state.activeNamespace.namespace);
const headers = state.settings.authToken
? { Authorization: 'Bearer ' + state.settings.authToken }
: {};
const res = await fetch(url, { ...opts, headers });
if (!res.ok) throw new Error(`${res.status}: ${await res.text()}`);
return res.json();
There's also a commented-out sketch inside api() itself — delete the mock branches once real endpoints work.
Contract alignment — [REAL] vs [PROPOSED] fields
Mocks in oumta-dashboard.js are labelled inline:
- [REAL] — field matches the Go struct exactly (from
internal/httpapi/router.go). Must stay byte-for-byte aligned when flipping to real. - [PROPOSED] — our guess based on prose in backend
docs/README.md. Backend author may pick different names; UI has one place per field to update, and there's a commented-out adapter slot inapi()if several diverge.
Overview (GET /api/cluster/overview) — entirely [REAL]:
clusterState (string: healthy | degraded | down | unknown)
workloadsUp (int)
workloadsDown (int)
updatedAtUtc (string, RFC3339)
Extras shown on the Overview page (incidents count, attention list) are derived client-side from the workloads response — no contract addition needed from backend.
Workloads (GET /api/workloads) — [PROPOSED], derived from docs/README.md §2:
items: [{
workloadId // synthetic handle, format "deployment/{ns}/{name}"
name // ← from doc "Name"
namespace // ← "Namespace"
state // ← "Zustand" — vocab: healthy | degraded | unavailable
replicasReady // ← "Replikas (ready/…)"
replicasDesired // ← "Replikas (…/desired)"
cpu // ← "CPU-Auslastung"
memory // ← "Speicherverbrauch"
image // ← "Aktives Docker-Image"
lastReason // ← "Letzter bekannter Grund fuer Zustandswechsel"
lastTransitionAt // ← "inkl. letzter Statuswechsel-Zeit"
}]
Timeline (GET /api/timeline, GET /api/workloads/{ns}/{name}/timeline) — [PROPOSED], derived from docs/README.md §3:
items: [{
timestamp // RFC3339
severity // info | warning | error
workloadId // same handle as above
message // short human description
reason // cause code, e.g. CrashLoopBackOff, ImagePullBackOff
kind // deployment | incident | scale — optional UI hint, drop if absent
}]
Namespaces (GET /api/namespaces) — [PROPOSED], new endpoint:
items: [{ id, label, namespace }]
id could equal namespace if backend treats them as the same. label is optional display text.
If/when real shapes differ, the fix path in priority order:
- Rename in
mockXxx()+ renderer (one place each). - If multiple fields diverge on one endpoint, add a mapping step inside
api()beforereturn. - If the shape differs fundamentally, split the renderer.
Phase 2 — auth, persistence, proxy decision
Keycloak bearer flow
- Decision pending (see user note): probably SSO-wrapped. The plugin already sends
Authorization: Bearer <state.settings.authToken>if a token is configured; the open question is how the token arrives. - Options:
- A. User pastes a token into the settings tab (good for POC, not for prod).
- B. hyprcrd itself becomes a Keycloak client, acquires a token, exposes it via
window.hyprcrd.authToken— plugin reads that. Requires a hyprcrd-core change. - C. PHP proxy (see below) attaches the token server-side; the browser never sees it.
- Recommendation: go with C once we decide on the proxy. Until then A is fine for dev.
PHP proxy in hyprcrd
- Today the plugin calls the Go backend directly from the browser. That means:
- The Go backend must return permissive CORS (it does:
Access-Control-Allow-Originis wide-open in dev). - The user's browser must reach the backend (issue if the backend lives inside a private network).
- The Go backend must return permissive CORS (it does:
- If we add a PHP proxy (file:
public/plugins/oumta-dashboard/backend.php), we'd:- Route
/api/oumta/*→ forward tobaseUrl + *. - Store
baseUrl+authTokenin hyprcrd'ssettingstable instead oflocalStorage. - Get CORS for free because the request originates from hyprcrd's own origin.
- Route
- Trigger for adding the proxy: when we get the Keycloak spec, or when users report "dashboard only works when I'm VPN'd".
Settings persistence
- Currently
localStorage['oumta-dashboard-settings']— fine for a single user in one browser. - If we want per-tenant server-side settings, add a hyprcrd
settingskey likeoumta.base_urland expose via a small PHP endpoint.
Phase 3 — realtime
startLive()currently just starts polling (everysettings.pollIntervalSec).- SSE plan (see commented-out block in
startLive()):- Backend exposes
GET /api/events?namespace=…as anEventSource-compatible stream. - On
onopen→setLive('live'), apply incoming events live (invalidatestate.data.overviewetc. or surgically patch). - On
onerror→ fall back to polling,setLive('polling')with a hint.
- Backend exposes
- SSE event shape proposal (to negotiate with backend):
{ type: 'workload.state', workloadId, state, reason, timestamp }, etc. Parking this until the backend is ready to talk.
Phase 3+ — nice to haves (not in scope yet)
From the original questions catalog we parked:
- Timezone handling: UTC vs. local browser time (probably just use local + tooltip with UTC).
- Plugin settings in hyprcrd's settings-tab vs. per-user vs. shared across tenants.
- Auto-create a hyprcrd card when an incident is opened (tie the two systems together).
- Filter persistence in URL hash so links are shareable.
- Light/dark accent variants — currently inherits theme variables, should just work.
Open backend questions (to sync with the Go team)
- Namespace list endpoint — UI needs an endpoint to populate the namespace tabs. The doc says "Namespace-Auswahl per Menü" and mentions env-var config on the backend. What's the endpoint?
GET /api/namespaces? If not, how does the UI discover them? - Response envelope — the existing
overviewendpoint returns a flat object. Lists should probably be{ items: [...] }(consistent with our renderers) or[](then we'll adapt). Pick one and stick with it across all list endpoints. - Cluster state values — our Overview card colour-codes on
healthy | degraded | down | unknown. Backend currently returns"unknown"as a literal. Confirm the vocabulary. - Workload state values — we colour-code on
healthy | degraded | unavailable. The doc uses these but the old .NET API also hadwithRestarts/withWarningsas summary counters. Only the three we use? Or more? - Workload ID shape — we use
deployment/{namespace}/{name}as a synthetic id (so workloads can be linked from the timeline). Does the backend emit that exact form, or just{namespace}/{name}? We can adapt either way. - Timeline pagination — limit? cursor? since? The .NET version had all three; the Go doc mentions only filters. Enough for MVP?
- Realtime channel — path + event format. We've got a plan above; needs the backend team's blessing.
Internal follow-ups
renderWorkloadsis called on every keystroke in the filter — fine up to ~1k rows, might need debouncing above that.refresh()tears down the scoped-workload timeline whenever it fires. For Phase 1 that's acceptable; once real data lands, consider NOT overwritingstate.data.timelinewhenstate.data.scopeWorkloadis active.bootstrap()only runs on first show. If the user changes the API base URL in settings while the overlay is open, we re-start live mode but don't re-fetch namespaces. Add a "reset & reload" action if that matters.- Namespace tab bar shows
namespace.label; we currently fake it. Once real, consider showing the full k8s namespace iflabelandnamespacediffer.