mirror of
https://github.com/vee1e/krply.git
synced 2026-09-01 09:49:51 +00:00
Audit: correlation event_ids include stage and response code so the multi-stage lines of one request no longer collapse under dedup; match scans are bounded to a time window and a page instead of the object's full history. Web: dry-run results are read from dry_run_result (nested) so the verdict is rendered correctly and conflicts/errors/skipped are shown; the plans view no longer POSTs an unsolicited plan on page load; coverage and streams surface API errors instead of showing a misleading empty state and follow cursor pagination so they are not stuck on the oldest page; the diff path tokenizer handles backslash-escaped dotted keys. Deploy/CI: the chart no longer grants the query server a cluster-wide read ClusterRole, runs as non-root with a read-only root filesystem, adds liveness/readiness probes, wires the ConfigMap as env (STORE_PATH, LISTEN_ADDR), defaults the journal to a PVC instead of an ephemeral emptyDir, and adds imagePullSecrets; the replay ClusterRole drops the unused update verb; a Dockerfile builds a static distroless image; GitHub actions are pinned by commit SHA, jobs set least-privilege permissions, the Vercel deploy skips fork PRs, CI passes the Makefile test timeouts, and make lint runs a real web syntax check. Docs: event-schema/consistency no longer describe an ingest_sequence field, an observed-time-based event_id, or restart-from-checkpoint; the threat model documents the unauthenticated HTTP API surface and the chart's RBAC change; replay-safety matches the enforced dry-run gate.
122 lines
4.3 KiB
JavaScript
122 lines
4.3 KiB
JavaScript
// Coverage: the coverage surface. For each cluster, one block. For each
|
|
// stream, a strip chart of event cadence over time. Solid = activity,
|
|
// hatched = gap, faint = idle, void = outside the observed window.
|
|
import { api } from '../api.js';
|
|
import { el, asList, stripRow, fmtTime, emptyState } from '../util.js';
|
|
|
|
export async function viewCoverage(mount) {
|
|
mount.appendChild(el('h1', {}, [
|
|
el('span', { className: 'eyebrow', textContent: 'coverage' }),
|
|
el('span', { textContent: 'Stream coverage over time' }),
|
|
]));
|
|
mount.appendChild(el('hr', { className: 'rule' }));
|
|
|
|
let clusters, streams, events;
|
|
try {
|
|
[clusters, streams, events] = await Promise.all([
|
|
api.clusters(),
|
|
api.streams(),
|
|
api.eventsAll({}),
|
|
]);
|
|
} catch (err) {
|
|
mount.appendChild(el('div', { className: 'notice error', textContent: `failed to load coverage: ${err.message || String(err)}` }));
|
|
return;
|
|
}
|
|
clusters = asList(clusters);
|
|
streams = asList(streams);
|
|
events = asList(events);
|
|
|
|
if (!clusters.length && !streams.length) {
|
|
mount.appendChild(emptyState(
|
|
'No clusters or streams recorded yet.',
|
|
'Start a recording with: krply record --namespace shop --resource deployments --resource configmaps --store krply.db',
|
|
));
|
|
return;
|
|
}
|
|
|
|
const byCluster = new Map();
|
|
for (const st of streams) {
|
|
const list = byCluster.get(st.cluster_id) || [];
|
|
list.push(st);
|
|
byCluster.set(st.cluster_id, list);
|
|
}
|
|
for (const cl of clusters) if (!byCluster.has(cl.id)) byCluster.set(cl.id, []);
|
|
const clusterIds = clusters.length ? clusters.map((cl) => cl.id) : [...byCluster.keys()];
|
|
|
|
for (const id of clusterIds) {
|
|
const sts = byCluster.get(id) || [];
|
|
const cl = clusters.find((x) => x.id === id) || { id };
|
|
const block = el('section', { className: 'cluster-block' });
|
|
|
|
const span = observedSpan(events, id);
|
|
const head = el('header', { className: 'cluster-head' }, [
|
|
el('span', { className: 'cname', textContent: cl.context || cl.id }),
|
|
el('span', { className: 'cmeta' }, [
|
|
`${sts.length} stream${sts.length === 1 ? '' : 's'}`,
|
|
sts.length ? ` · first ${fmtTime(earliestFirst(sts))}` : '',
|
|
sts.length ? ` · last ${fmtTime(latestLast(sts))}` : '',
|
|
]),
|
|
]);
|
|
block.appendChild(head);
|
|
|
|
if (!sts.length) {
|
|
block.appendChild(el('div', { className: 'strip-rows' }, [
|
|
el('div', { className: 'strip-row', style: 'padding:14px 16px' }, [
|
|
el('span', { className: 'muted', textContent: 'no streams known for this cluster yet' }),
|
|
]),
|
|
]));
|
|
mount.appendChild(block);
|
|
continue;
|
|
}
|
|
|
|
const rows = el('div', { className: 'strip-rows' });
|
|
for (const st of sts) rows.appendChild(stripRow(st, span, events.filter((e) => e.stream_id === st.id)));
|
|
block.appendChild(rows);
|
|
block.appendChild(stripLegend());
|
|
mount.appendChild(block);
|
|
}
|
|
|
|
}
|
|
|
|
function stripLegend() {
|
|
const legend = el('div', { className: 'strip-legend' });
|
|
const item = (cls, label) =>
|
|
el('span', {}, [el('i', { className: `sq ${cls}` }), el('span', { textContent: label })]);
|
|
legend.appendChild(item('ok', 'events'));
|
|
legend.appendChild(item('degraded', 'gap'));
|
|
legend.appendChild(item('idle', 'idle'));
|
|
legend.appendChild(item('void', 'outside window'));
|
|
return legend;
|
|
}
|
|
|
|
function observedSpan(events, clusterId) {
|
|
let min = null;
|
|
let max = null;
|
|
for (const ev of events) {
|
|
if (ev.cluster_id && ev.cluster_id !== clusterId) continue;
|
|
const t = ev.observed_at ? new Date(ev.observed_at).getTime() : NaN;
|
|
if (Number.isNaN(t)) continue;
|
|
if (min == null || t < min) min = t;
|
|
if (max == null || t > max) max = t;
|
|
}
|
|
const now = Date.now();
|
|
return { start: new Date(min != null ? min : now), end: new Date(max != null ? max : now) };
|
|
}
|
|
|
|
function earliestFirst(sts) {
|
|
let min = null;
|
|
for (const st of sts) {
|
|
const t = st.first_observed_at ? new Date(st.first_observed_at).getTime() : NaN;
|
|
if (!Number.isNaN(t) && (min == null || t < min)) min = t;
|
|
}
|
|
return min != null ? new Date(min).toISOString() : '';
|
|
}
|
|
|
|
function latestLast(sts) {
|
|
let max = null;
|
|
for (const st of sts) {
|
|
const t = st.last_observed_at ? new Date(st.last_observed_at).getTime() : NaN;
|
|
if (!Number.isNaN(t) && (max == null || t > max)) max = t;
|
|
}
|
|
return max != null ? new Date(max).toISOString() : '';
|
|
}
|