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>
This commit is contained in:
1 parent
69ad2c7e71
commit
39e94b8015
9 files changed
+1107
-2
No files matched your search
@@ -0,0 +1,172 @@
|
||||
# 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.
|
||||
Reference in new issue
Block a user