Oracle round-9 watch-out: same liveMeta.fetchedFrom leak class existed in two more user-visible surfaces: - src/components/Telemetry.tsx: 'source' block rendered the host string. Now hard-coded 'EA2'. - src/scenes/Landing.tsx: hero paragraph said 'runs end-to-end through EA2 on <host>'. Dropped the host suffix entirely. liveMeta import removed from Telemetry (no other refs). Landing still uses liveMeta for the workItems/distinctDefs stat counters.
135 lines
5.4 KiB
TypeScript
135 lines
5.4 KiB
TypeScript
// Bottom telemetry strip.
|
|
// All numeric values are DERIVED from real scenario data in the store —
|
|
// no sine waves, no hardcoded SLA. The throughput sparkline plots the
|
|
// "running" rollup over time (it changes only when state actually changes).
|
|
// The "ui tick" dot is a UI heartbeat labelled as such.
|
|
import { useEffect, useMemo, useState } from "react";
|
|
import { useApp, scenarioById } from "../state/store";
|
|
|
|
import { Pulse, Spark } from "./icons";
|
|
|
|
function Sparkline({ values, color, label }: { values: number[]; color: string; label: string }) {
|
|
const w = 120, h = 28, pad = 2;
|
|
if (!values.length) return <svg width={w} height={h} aria-label={label} />;
|
|
const max = Math.max(...values, 1);
|
|
const min = Math.min(...values, 0);
|
|
const span = Math.max(1, max - min);
|
|
const pts = values
|
|
.map((v, i) => {
|
|
const x = pad + ((w - pad * 2) * i) / Math.max(1, values.length - 1);
|
|
const y = h - pad - ((v - min) / span) * (h - pad * 2);
|
|
return `${x.toFixed(1)},${y.toFixed(1)}`;
|
|
})
|
|
.join(" ");
|
|
return (
|
|
<svg width={w} height={h} className="spark" aria-label={label}>
|
|
<polyline points={pts} fill="none" stroke={color} strokeWidth={1.6} />
|
|
</svg>
|
|
);
|
|
}
|
|
|
|
const HIST_LEN = 24;
|
|
|
|
export default function Telemetry() {
|
|
const scenarioId = useApp((s) => s.scenarioId);
|
|
const scenarios = useApp((s) => s.scenarios);
|
|
const mode = useApp((s) => s.mode);
|
|
const sc = scenarioById(scenarioId);
|
|
|
|
// Real rollup across every loaded scenario.
|
|
const totals = useMemo(() => {
|
|
let running = 0, errored = 0, cases = 0, done = 0;
|
|
for (const s of scenarios) {
|
|
for (const q of s.queue) {
|
|
cases += 1;
|
|
if (q.status === "running" || q.status === "waiting_for_user" || q.status === "waiting_for_agent") running += 1;
|
|
else if (q.status === "errored" || q.status === "failed") errored += 1;
|
|
else if (q.status === "completed" || q.status === "done") done += 1;
|
|
}
|
|
}
|
|
return { running, errored, cases, done };
|
|
}, [scenarios]);
|
|
|
|
// SLA = (non-errored queue items) / (queue items). Real, derived.
|
|
const sla = totals.cases === 0 ? 1 : 1 - totals.errored / totals.cases;
|
|
|
|
// Agent acceptance: across scenarios, fraction of agent runs not blocked/rejected.
|
|
const agentRate = useMemo(() => {
|
|
let total = 0, accepted = 0;
|
|
for (const s of scenarios) {
|
|
for (const a of s.agentRuns) {
|
|
total += 1;
|
|
if (a.status !== "proposed") accepted += 1;
|
|
}
|
|
}
|
|
return total === 0 ? 1 : accepted / total;
|
|
}, [scenarios]);
|
|
|
|
// Sparkline = history of `running` totals. Updates only when totals change.
|
|
const [history, setHistory] = useState<number[]>(() => Array(HIST_LEN).fill(totals.running));
|
|
useEffect(() => {
|
|
const t = setInterval(() => {
|
|
setHistory((h) => [...h.slice(1), totals.running]);
|
|
}, 1500);
|
|
return () => clearInterval(t);
|
|
}, [totals.running]);
|
|
|
|
return (
|
|
<div className="telemetry" data-anchor="telemetry">
|
|
<div className="t-block">
|
|
<span className="t-eyebrow"><Pulse size={11} /> running over time</span>
|
|
<Sparkline values={history} color="var(--run)" label="running cases over the last 36 seconds" />
|
|
<span className="t-v mono">{totals.running} now</span>
|
|
</div>
|
|
<div className="t-divider" />
|
|
<div className="t-block">
|
|
<span className="t-eyebrow">cases</span>
|
|
<span className="t-v mono">{totals.cases}</span>
|
|
</div>
|
|
<div className="t-block">
|
|
<span className="t-eyebrow">running</span>
|
|
<span className="t-v mono" style={{ color: "var(--run)" }}>{totals.running}</span>
|
|
</div>
|
|
<div className="t-block">
|
|
<span className="t-eyebrow">errored</span>
|
|
<span className="t-v mono" style={{ color: totals.errored > 0 ? "var(--block)" : "var(--text-2)" }}>{totals.errored}</span>
|
|
</div>
|
|
<div className="t-divider" />
|
|
<div className="t-block" title="derived: non-errored / total queue items">
|
|
<span className="t-eyebrow"><Spark size={11} /> sla (derived)</span>
|
|
<Gauge value={sla} />
|
|
<span className="t-v mono">{(sla * 100).toFixed(1)}%</span>
|
|
</div>
|
|
<div className="t-divider" />
|
|
<div className="t-block" title="derived: fraction of agent runs not in 'proposed' state">
|
|
<span className="t-eyebrow">agent acceptance (derived)</span>
|
|
<Gauge value={agentRate} accent="var(--ok)" />
|
|
<span className="t-v mono">{(agentRate * 100).toFixed(0)}%</span>
|
|
</div>
|
|
<div className="t-spacer" />
|
|
<div className="t-block">
|
|
<span className="t-eyebrow">scenario</span>
|
|
<span className="t-v">{sc?.family.label ?? "—"}</span>
|
|
</div>
|
|
<div className="t-block">
|
|
<span className="t-eyebrow">mode</span>
|
|
<span className={`mode-pill mode-${mode}`}>{mode === "live" ? "LIVE" : "SNAPSHOT"}</span>
|
|
</div>
|
|
<div className="t-block">
|
|
<span className="t-eyebrow">source</span>
|
|
<span className="t-v mono">EA2</span>
|
|
</div>
|
|
<div className="t-tick" aria-label="ui tick" title="UI heartbeat — pulses every 1.5s, does not represent backend activity" />
|
|
</div>
|
|
);
|
|
}
|
|
|
|
function Gauge({ value, accent = "var(--run)" }: { value: number; accent?: string }) {
|
|
const w = 60, h = 12;
|
|
return (
|
|
<div className="gauge" style={{ width: w, height: h }}>
|
|
<div className="gauge-fill" style={{ width: `${Math.min(100, Math.max(0, value * 100))}%`, background: accent }} />
|
|
</div>
|
|
);
|
|
}
|