Files
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

173 lines
9.7 KiB
Markdown

# 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 <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.