tweak status change representation
This commit is contained in:
1 parent
29e818ac64
commit
89d84d3538
2 files changed
+43
-123
No files matched your search
@@ -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; }
|
||||
|
||||
|
||||
@@ -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 = '<div class="oumta-empty">No events.</div>';
|
||||
return;
|
||||
}
|
||||
body.innerHTML = events.map(e => `
|
||||
<div class="oumta-timeline-entry">
|
||||
<div class="oumta-timeline-time">${formatTime(e.timestamp)}</div>
|
||||
<div class="oumta-timeline-severity" data-sev="${e.severity}">${e.severity}</div>
|
||||
<div>
|
||||
<div class="oumta-timeline-message">${escapeHtml(e.message)}</div>
|
||||
<div class="oumta-timeline-meta">${escapeHtml(e.workloadId)} · ${escapeHtml(e.reason || '')}</div>
|
||||
body.innerHTML = events.map(e => {
|
||||
// Recovery = letzter State-Uebergang endet in "-> healthy".
|
||||
// Image-Rollout = vom Backend markiert via reason "ImageRolled".
|
||||
// Beide kriegen eigene Badges, damit man Erholungen und neue
|
||||
// Images von generischen Info-Events (nur-Revision-Rollouts)
|
||||
// trennen kann.
|
||||
const isRecovery = typeof e.message === 'string' && e.message.endsWith('-> healthy');
|
||||
const isImageRoll = e.reason === 'ImageRolled';
|
||||
let sev = e.severity;
|
||||
if (isRecovery) sev = 'recovered';
|
||||
else if (isImageRoll) sev = 'image';
|
||||
return `
|
||||
<div class="oumta-timeline-entry">
|
||||
<div class="oumta-timeline-time">${formatTime(e.timestamp)}</div>
|
||||
<div class="oumta-timeline-severity" data-sev="${sev}">${sev}</div>
|
||||
<div>
|
||||
<div class="oumta-timeline-message">${escapeHtml(e.message)}</div>
|
||||
<div class="oumta-timeline-meta">${escapeHtml(e.workloadId)} · ${escapeHtml(e.reason || '')}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`).join('');
|
||||
`;
|
||||
}).join('');
|
||||
}
|
||||
|
||||
// ── Workload → detail-as-timeline navigation ──────────────────
|
||||
|
||||
Reference in new issue
Block a user