# 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: ```js 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 ` 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.