Files
hyprcrd/public/plugins/oumta-dashboard/TODO.md
T
thandClaude Opus 4.6 39e94b8015 oumta dashboard plugin (mockup)
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>
2026-04-14 21:24:07 +02:00

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-dashboard repo) 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 in api() 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:

  1. Rename in mockXxx() + renderer (one place each).
  2. If multiple fields diverge on one endpoint, add a mapping step inside api() before return.
  3. 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-Origin is wide-open in dev).
    • The user's browser must reach the backend (issue if the backend lives inside a private network).
  • If we add a PHP proxy (file: public/plugins/oumta-dashboard/backend.php), we'd:
    • Route /api/oumta/* → forward to baseUrl + *.
    • Store baseUrl + authToken in hyprcrd's settings table instead of localStorage.
    • Get CORS for free because the request originates from hyprcrd's own origin.
  • 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 settings key like oumta.base_url and expose via a small PHP endpoint.

Phase 3 — realtime

  • startLive() currently just starts polling (every settings.pollIntervalSec).
  • SSE plan (see commented-out block in startLive()):
    • Backend exposes GET /api/events?namespace=… as an EventSource-compatible stream.
    • On onopen → setLive('live'), apply incoming events live (invalidate state.data.overview etc. or surgically patch).
    • On onerror → fall back to polling, setLive('polling') with a hint.
  • 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)

  1. 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?
  2. Response envelope — the existing overview endpoint 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.
  3. Cluster state values — our Overview card colour-codes on healthy | degraded | down | unknown. Backend currently returns "unknown" as a literal. Confirm the vocabulary.
  4. Workload state values — we colour-code on healthy | degraded | unavailable. The doc uses these but the old .NET API also had withRestarts / withWarnings as summary counters. Only the three we use? Or more?
  5. 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.
  6. Timeline pagination — limit? cursor? since? The .NET version had all three; the Go doc mentions only filters. Enough for MVP?
  7. Realtime channel — path + event format. We've got a plan above; needs the backend team's blessing.

Internal follow-ups

  • renderWorkloads is 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 overwriting state.data.timeline when state.data.scopeWorkload is 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 if label and namespace differ.