diff --git a/public/plugins/oumta-dashboard/oumta-dashboard.css b/public/plugins/oumta-dashboard/oumta-dashboard.css index c474296..5811006 100644 --- a/public/plugins/oumta-dashboard/oumta-dashboard.css +++ b/public/plugins/oumta-dashboard/oumta-dashboard.css @@ -216,9 +216,11 @@ font-size: 9px; text-transform: uppercase; } -.oumta-timeline-severity[data-sev="info"] { color: var(--t-accent); } -.oumta-timeline-severity[data-sev="warning"] { color: #fb923c; } -.oumta-timeline-severity[data-sev="error"] { color: var(--t-danger); } +.oumta-timeline-severity[data-sev="info"] { color: var(--t-accent); } +.oumta-timeline-severity[data-sev="warning"] { color: #fb923c; } +.oumta-timeline-severity[data-sev="error"] { color: var(--t-danger); } +.oumta-timeline-severity[data-sev="recovered"] { color: #4ade80; } +.oumta-timeline-severity[data-sev="image"] { color: #60a5fa; } .oumta-timeline-message { font-size: 12px; color: var(--t-bright); } .oumta-timeline-meta { font-size: 10px; color: var(--t-dim); margin-top: 2px; } diff --git a/public/plugins/oumta-dashboard/oumta-dashboard.js b/public/plugins/oumta-dashboard/oumta-dashboard.js index 478ff69..221dc3f 100644 --- a/public/plugins/oumta-dashboard/oumta-dashboard.js +++ b/public/plugins/oumta-dashboard/oumta-dashboard.js @@ -1,9 +1,8 @@ // ── Oumta Dashboard plugin ── // // Fullscreen view that consumes the Oumta Go backend (`oumta-dashboard`). -// Currently ALL data is mocked so we can iterate on layout without waiting -// for the backend. See TODO.md for every call that must later become a real -// fetch against the backend. +// All reads go through `api()` below; configure the backend URL in the +// hyprcrd settings overlay (oumta tab) or accept the localhost default. const h = window.hyprcrd; const $ = id => document.getElementById(id); @@ -16,7 +15,7 @@ const $ = id => document.getElementById(id); const LS_KEY = 'oumta-dashboard-settings'; const state = { settings: { - baseUrl: '', // e.g. https://dashboard.oumta.internal + baseUrl: 'http://localhost:8080', // e.g. https://dashboard.oumta.internal; default fits local dev authToken: '', // Keycloak bearer; empty = no auth yet pollIntervalSec: 15, }, @@ -41,116 +40,23 @@ function saveSettings() { // ── Central API surface ───────────────────────────────────────── // -// Every backend call routes through `api()`. Right now each case returns -// mock data. When the backend endpoints are ready, swap the `return MOCK…` -// lines for real `fetch(…)` calls. The UI code above this layer should -// not need to change. -// -// Field naming legend for the mocks below: -// [REAL] = already defined in Go code (internal/httpapi/router.go). -// Must match byte-for-byte when flipping to real fetch. -// [PROPOSED] = our guess based on docs/README.md prose. Backend author -// is free to rename — when they do, adjust the renderer -// (there's only one place per field) or add a small adapter -// inside api() that normalises the response. - -const MOCK_NAMESPACES = [ - { id: 'oumta-dev', label: 'oumta-dev', namespace: 'oumta-dev' }, - { id: 'oumta-beta', label: 'oumta-beta', namespace: 'oumta-beta' }, -]; - -// Overview mock — shape matches the existing Go struct `clusterOverviewResponse` -// byte-for-byte (see cmd/api/main.go → internal/httpapi/router.go). -// [REAL] clusterState, workloadsUp, workloadsDown, updatedAtUtc -// Anything else the overview page shows (incidents count, attention list) is -// DERIVED client-side from the workloads response — see renderOverview(). -function mockOverview(ns) { - const wl = mockWorkloads(ns); - const up = wl.filter(w => w.state === 'healthy').length; - const down = wl.filter(w => w.state === 'unavailable').length; - const degraded = wl.filter(w => w.state === 'degraded').length; - const clusterState = down > 0 ? 'down' : (degraded > 0 ? 'degraded' : 'healthy'); - return { - clusterState, - workloadsUp: up, - workloadsDown: down, - updatedAtUtc: new Date().toISOString(), - }; -} - -// Workloads mock — [PROPOSED] shape. Fields chosen from docs/README.md §2: -// Name, Namespace, Zustand, Replikas (ready/desired), CPU, Speicher, -// Image, Letzter Grund für Zustandswechsel. -// `workloadId` is our synthetic stable handle (kind/namespace/name) so the -// timeline can be linked back. If the backend emits a different handle -// format we'll adjust the link-builders. -function mockWorkloads(ns) { - const id = name => `deployment/${ns}/${name}`; - return [ - { workloadId: id('api'), name: 'api', namespace: ns, state: 'degraded', replicasReady: 2, replicasDesired: 3, cpu: '420m', memory: '512Mi', image: 'oumta/api:1.4.2', lastReason: 'CrashLoopBackOff', lastTransitionAt: minutesAgo(12) }, - { workloadId: id('api-canary'), name: 'api-canary', namespace: ns, state: 'healthy', replicasReady: 1, replicasDesired: 1, cpu: '140m', memory: '310Mi', image: 'oumta/api:1.4.3-rc2', lastReason: '—', lastTransitionAt: minutesAgo(180) }, - { workloadId: id('web'), name: 'web', namespace: ns, state: 'healthy', replicasReady: 2, replicasDesired: 2, cpu: '95m', memory: '256Mi', image: 'oumta/web:2.1.0', lastReason: '—', lastTransitionAt: minutesAgo(720) }, - { workloadId: id('worker'), name: 'worker', namespace: ns, state: 'healthy', replicasReady: 4, replicasDesired: 4, cpu: '820m', memory: '1Gi', image: 'oumta/worker:1.4.2', lastReason: '—', lastTransitionAt: minutesAgo(121) }, - { workloadId: id('ingest'), name: 'ingest', namespace: ns, state: 'unavailable', replicasReady: 0, replicasDesired: 1, cpu: '—', memory: '—', image: 'oumta/ingest:3.0.0', lastReason: 'ImagePullBackOff', lastTransitionAt: minutesAgo(3) }, - { workloadId: id('scheduler'), name: 'scheduler', namespace: ns, state: 'healthy', replicasReady: 1, replicasDesired: 1, cpu: '55m', memory: '120Mi', image: 'oumta/scheduler:0.9.1', lastReason: '—', lastTransitionAt: minutesAgo(2880)}, - { workloadId: id('postgres'), name: 'postgres', namespace: ns, state: 'healthy', replicasReady: 1, replicasDesired: 1, cpu: '180m', memory: '768Mi', image: 'postgres:16.2', lastReason: '—', lastTransitionAt: minutesAgo(4320)}, - { workloadId: id('redis'), name: 'redis', namespace: ns, state: 'healthy', replicasReady: 1, replicasDesired: 1, cpu: '25m', memory: '80Mi', image: 'redis:7.2-alpine', lastReason: '—', lastTransitionAt: minutesAgo(4320)}, - { workloadId: id('nats'), name: 'nats', namespace: ns, state: 'degraded', replicasReady: 2, replicasDesired: 3, cpu: '60m', memory: '200Mi', image: 'nats:2.10.14', lastReason: 'Readiness probe failed', lastTransitionAt: minutesAgo(45) }, - { workloadId: id('reports'), name: 'reports', namespace: ns, state: 'healthy', replicasReady: 1, replicasDesired: 1, cpu: '75m', memory: '180Mi', image: 'oumta/reports:1.2.0', lastReason: '—', lastTransitionAt: minutesAgo(600) }, - { workloadId: id('notifier'), name: 'notifier', namespace: ns, state: 'healthy', replicasReady: 1, replicasDesired: 1, cpu: '30m', memory: '96Mi', image: 'oumta/notifier:0.5.3', lastReason: '—', lastTransitionAt: minutesAgo(1500)}, - { workloadId: id('migrations'), name: 'migrations', namespace: ns, state: 'healthy', replicasReady: 0, replicasDesired: 0, cpu: '—', memory: '—', image: 'oumta/migrations:1.4.2', lastReason: 'Completed', lastTransitionAt: minutesAgo(300) }, - ]; -} - -// Timeline mock — [PROPOSED] shape. -// Fields inferred from docs §3 (Detail page) & §4 (filters): timestamp, -// severity, workloadId, message, reason. `kind` is a derived hint for -// rendering (deployment vs. incident vs. scale etc.) and can be dropped -// if the backend doesn't provide it. -function mockTimeline(ns, workloadId = null) { - const id = name => `deployment/${ns}/${name}`; - const all = [ - { timestamp: minutesAgo(3), severity: 'error', workloadId: id('ingest'), message: 'Pod ingest-6d8bf4 entered ImagePullBackOff', reason: 'ImagePullBackOff', kind: 'incident' }, - { timestamp: minutesAgo(8), severity: 'warning', workloadId: id('nats'), message: 'Replica 3/3 readiness probe failed', reason: 'ProbeFailed', kind: 'incident' }, - { timestamp: minutesAgo(12), severity: 'warning', workloadId: id('api'), message: 'Pod api-7c9ff6 restarted (3rd time in 10m)', reason: 'CrashLoopBackOff', kind: 'incident' }, - { timestamp: minutesAgo(26), severity: 'info', workloadId: id('api'), message: 'HorizontalPodAutoscaler targeted 3 replicas', reason: 'HPA', kind: 'scale' }, - { timestamp: minutesAgo(48), severity: 'info', workloadId: id('web'), message: 'Rollout completed to revision 17', reason: 'DeploymentRolled', kind: 'deployment' }, - { timestamp: minutesAgo(58), severity: 'info', workloadId: id('web'), message: 'Rollout started: oumta/web:2.1.0', reason: 'DeploymentStarted', kind: 'deployment' }, - { timestamp: minutesAgo(121), severity: 'info', workloadId: id('worker'), message: 'Scaled up from 2 to 4 replicas', reason: 'HPA', kind: 'scale' }, - { timestamp: minutesAgo(180), severity: 'info', workloadId: id('api-canary'), message: 'Canary became ready', reason: 'CanaryReady', kind: 'deployment' }, - { timestamp: minutesAgo(240), severity: 'info', workloadId: id('api'), message: 'Rollout started: oumta/api:1.4.2', reason: 'DeploymentStarted', kind: 'deployment' }, - { timestamp: minutesAgo(300), severity: 'info', workloadId: id('migrations'), message: 'Job completed successfully in 1m42s', reason: 'JobSucceeded', kind: 'deployment' }, - { timestamp: minutesAgo(720), severity: 'info', workloadId: id('reports'), message: 'Rolled out revision 9', reason: 'DeploymentRolled', kind: 'deployment' }, - { timestamp: minutesAgo(1500), severity: 'warning', workloadId: id('ingest'), message: 'OOMKilled: container exceeded 256Mi limit', reason: 'OOMKilled', kind: 'incident' }, - { timestamp: minutesAgo(2880), severity: 'info', workloadId: id('scheduler'), message: 'Initial rollout: oumta/scheduler:0.9.1', reason: 'DeploymentStarted', kind: 'deployment' }, - ]; - return workloadId ? all.filter(e => e.workloadId === workloadId) : all; -} - -function minutesAgo(m) { return new Date(Date.now() - m * 60_000).toISOString(); } +// Every backend call routes through `api()`. Requests go against +// `state.settings.baseUrl`; the active namespace (if any) is attached +// as a `namespace` query parameter, and the configured auth token is +// sent as a bearer header. Response shapes are defined by the Go +// backend in `internal/httpapi/contracts.go`. async function api(path, opts = {}) { - // TODO(api): when backend is ready, build a real fetch here. - // const url = new URL(path, state.settings.baseUrl); - // 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(); - - // Mock dispatch — keep in sync with backend `docs/README.md`. - const ns = state.activeNamespace?.namespace || ''; - if (path === '/api/namespaces') return { items: MOCK_NAMESPACES }; // TODO(api) - if (path === '/api/cluster/overview') return mockOverview(ns); // TODO(api): GET /api/cluster/overview?namespace= - if (path === '/api/workloads') return { items: mockWorkloads(ns) }; // TODO(api): GET /api/workloads?namespace=&state=&since= - if (path.startsWith('/api/workloads/') && path.endsWith('/timeline')) { - // /api/workloads/{namespace}/{name}/timeline - const parts = path.replace('/api/workloads/', '').replace('/timeline', '').split('/'); - const workloadId = `deployment/${parts[0]}/${parts[1]}`; - return { items: mockTimeline(ns, workloadId) }; // TODO(api) + const url = new URL(path, state.settings.baseUrl); + if (!url.searchParams.has('namespace') && state.activeNamespace) { + url.searchParams.set('namespace', state.activeNamespace.namespace); } - if (path === '/api/timeline') return { items: mockTimeline(ns) }; // TODO(api): GET /api/timeline?namespace= - throw new Error('unknown api path: ' + path); + 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(); } // ── Fullscreen overlay show/hide ───────────────────────────────── @@ -364,16 +270,28 @@ function renderTimeline() { body.innerHTML = '