Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 8 additions & 5 deletions apps/web/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -1048,8 +1048,6 @@ export function App() {
}, [refreshAgents, refreshEnvironmentTemplates, refreshSessions, refreshStartupConfiguration, refreshVaults]);

useEffect(() => {
if (view !== "dashboard" && view !== "sessions") return;
let timer: number | null = null;
void (async () => {
if (
sessionCollectionState === "ready" &&
Expand All @@ -1059,6 +1057,11 @@ export function App() {
await refreshRuntimeSnapshot();
}
})();
}, [refreshRuntimeSnapshot, sessionCollectionState]);

useEffect(() => {
if (view !== "dashboard" && view !== "sessions") return;
let timer: number | null = null;
const schedule = () => {
const jitter = Math.floor(Math.random() * 5_000);
timer = window.setTimeout(() => {
Expand Down Expand Up @@ -2289,7 +2292,7 @@ export function App() {
onStartSession={() => openSessionSetup()}
/>
</header>
<div className="page-transition" key={view}>
<div className="page-transition">
{view === "templates" ? (
<EnvironmentTemplatesView
key={`templates:${coreGeneration}`}
Expand All @@ -2299,7 +2302,7 @@ export function App() {
onConfigureConnection={() => setConnectionOpen(true)}
/>
) : null}
{view === "dashboard" ? (
<div className="cached-page-view" hidden={view !== "dashboard"}>
<DashboardView
agents={agents}
sessions={sessions}
Expand Down Expand Up @@ -2327,7 +2330,7 @@ export function App() {
setView("sessions");
}}
/>
) : null}
</div>
{view === "sessions" ? (
<SessionsView
key={`sessions:${coreGeneration}`}
Expand Down
19 changes: 9 additions & 10 deletions apps/web/src/features/dashboard/DashboardView.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -194,7 +194,7 @@ describe("Dashboard loaded-result presentation", () => {
expect(html).not.toContain("Execution ready");
});

it("renders current Docker resources without inventing a CPU percentage or history", () => {
it("renders current Docker resources immediately while durable history loads", () => {
const hosted = session("11111111-1111-4111-8111-111111111111", {
metadata: { title: "Managed research" },
environment: {
Expand Down Expand Up @@ -246,20 +246,19 @@ describe("Dashboard loaded-result presentation", () => {
expect(html).toContain("Cumulative CPU / capacity");
expect(html).toContain("1m 13s / 2 cores");
expect(html).toContain("512 MiB / 2.00 GiB");
expect(html).toContain('aria-label="Runtime durable-history charts"');
expect(html).toContain('aria-label="Runtime live-window charts"');
expect(html).toContain("Resource trends");
expect(html).toContain("Retained samples · durable history");
expect(html).toContain("Browser-local samples · reset on reload");
expect(html).not.toContain('aria-label="Runtime trend source"');
expect(html).toContain("History · loading");
expect(html).toContain("0 buckets");
expect(html).toContain('aria-label="Runtime durable range"');
expect(html).toContain("Live · loading history");
expect(html).toContain("1 sample");
expect(html).toContain('aria-label="Runtime live range"');
expect(html).toContain('aria-pressed="true">1h</button>');
expect(html).toContain("CPU usage");
expect(html).toContain("Memory usage");
expect(html).not.toContain("Compute uptime");
expect(html).toContain("Compute uptime");
expect(html).toContain("Token throughput");
expect(html).toContain("No retained CPU samples");
expect(html).toContain("0/2 valid points · 0 snapshots · no history is synthesized");
expect(html).not.toContain("No retained CPU samples");
expect(html).toContain("Latest value");
expect(html).toContain("Missing samples");
expect(html).toContain("Runtime targets");
Expand Down Expand Up @@ -360,7 +359,7 @@ describe("Dashboard loaded-result presentation", () => {
});

expect(html).toContain("Runtime: Runtime refresh failed");
expect(html).toContain("History · loading");
expect(html).toContain("Live · loading history");
expect(html).not.toContain("Live · 30s");
});

Expand Down
25 changes: 23 additions & 2 deletions apps/web/src/features/dashboard/RuntimeTrendCharts.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -42,15 +42,36 @@ describe("Runtime live-window chart accessibility", () => {
})).toBe("Memory usage all series hidden; use the legend to show a series");
});

it("reports a current gap as unavailable instead of announcing a stale value as latest", () => {
it("renders an unavailable current value as zero without retaining a stale value", () => {
const html = renderToStaticMarkup(
<RuntimeTrendCharts samples={[sample(60_000, .5), sample(120_000, null)]} />,
);

expect(html).toContain("Runtime worker</th><td>Unavailable</td><td>1</td>");
expect(html).toContain("Runtime worker</th><td>0%</td><td>0</td>");
expect(html).not.toContain("Runtime worker</th><td>50%</td><td>1</td>");
});

it("renders empty retained buckets as continuous zero-value chart series", () => {
const empty = (sampledAt: number): RuntimeTrendSample => ({
...sample(sampledAt, null),
targets: [],
memoryUsageBytes: null,
memoryLimitBytes: null,
inputTokensPerMinute: null,
outputTokensPerMinute: null,
});
const html = renderToStaticMarkup(
<RuntimeTrendCharts samples={[empty(60_000), empty(120_000)]} source="durable" />,
);

expect(html).toContain("usage</th><td>0%</td><td>0</td>");
expect(html).toContain("used</th><td>0 B</td><td>0</td>");
expect(html).toContain("input</th><td>0/min</td><td>0</td>");
expect(html).not.toContain("No retained CPU samples");
expect(html).not.toContain("No complete retained memory samples");
expect(html).not.toContain("No retained token samples");
});

it("renders uPlot chart mounts and reports trends only after two real samples", () => {
const html = renderToStaticMarkup(
<RuntimeTrendCharts samples={[sample(60_000, .25), sample(120_000, .5)]} />,
Expand Down
20 changes: 13 additions & 7 deletions apps/web/src/features/dashboard/RuntimeTrendCharts.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -486,7 +486,7 @@ function targetIds(samples: readonly RuntimeTrendSample[], field: "cpuRatio" | "
for (const sample of samples) {
for (const target of sample.targets) {
const value = target[field];
if (value !== null) latest.set(target.seriesId, value);
latest.set(target.seriesId, value ?? latest.get(target.seriesId) ?? 0);
}
}
return [...latest.entries()].sort((left, right) => right[1] - left[1]).slice(0, 3).map(([id]) => id);
Expand Down Expand Up @@ -522,16 +522,22 @@ export function RuntimeTrendCharts({
id,
label: targetLabel(samples, id),
tone: tones[index] ?? "blue",
points: samples.map((sample) => ({ sampledAt: sample.sampledAt, value: sample.targets.find((target) => target.seriesId === id)?.cpuRatio ?? null })).map((point) => ({ ...point, value: point.value === null ? null : point.value * 100 })),
points: samples.map((sample) => ({ sampledAt: sample.sampledAt, value: (sample.targets.find((target) => target.seriesId === id)?.cpuRatio ?? 0) * 100 })),
}));
const memoryUsed = samples.map((sample) => ({ sampledAt: sample.sampledAt, value: sample.memoryUsageBytes }));
const memoryLimit = samples.map((sample) => ({ sampledAt: sample.sampledAt, value: sample.memoryLimitBytes }));
if (cpu.length === 0 && samples.length > 0) {
cpu.push({ id: "cpu", label: "usage", tone: "orange", points: samples.map((sample) => ({ sampledAt: sample.sampledAt, value: 0 })) });
}
const memoryUsed = samples.map((sample) => ({ sampledAt: sample.sampledAt, value: sample.memoryUsageBytes ?? 0 }));
const memoryLimit = samples.map((sample) => ({ sampledAt: sample.sampledAt, value: sample.memoryLimitBytes ?? 0 }));
const uptime = uptimeIds.map((id, index): TrendSeries => ({
id,
label: targetLabel(samples, id),
tone: tones[(index + 2) % tones.length] ?? "blue",
points: samples.map((sample) => ({ sampledAt: sample.sampledAt, value: sample.targets.find((target) => target.seriesId === id)?.uptimeSeconds ?? null })),
points: samples.map((sample) => ({ sampledAt: sample.sampledAt, value: sample.targets.find((target) => target.seriesId === id)?.uptimeSeconds ?? 0 })),
}));
if (uptime.length === 0 && samples.length > 0) {
uptime.push({ id: "uptime", label: "Runtime", tone: "blue", points: samples.map((sample) => ({ sampledAt: sample.sampledAt, value: 0 })) });
}
const throughput = tokenThroughput(samples);
return {
cpu,
Expand All @@ -541,8 +547,8 @@ export function RuntimeTrendCharts({
] satisfies TrendSeries[],
uptime,
tokens: [
{ id: "input", label: "input", tone: "orange", points: throughput.map((sample) => ({ sampledAt: sample.sampledAt, value: sample.inputPerMinute })) },
{ id: "output", label: "output", tone: "green", points: throughput.map((sample) => ({ sampledAt: sample.sampledAt, value: sample.outputPerMinute })) },
{ id: "input", label: "input", tone: "orange", points: throughput.map((sample) => ({ sampledAt: sample.sampledAt, value: sample.inputPerMinute ?? 0 })) },
{ id: "output", label: "output", tone: "green", points: throughput.map((sample) => ({ sampledAt: sample.sampledAt, value: sample.outputPerMinute ?? 0 })) },
] satisfies TrendSeries[],
};
}, [samples]);
Expand Down
30 changes: 25 additions & 5 deletions apps/web/src/features/dashboard/RuntimeTrendPanel.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { useEffect, useMemo, useState } from "react";
import { useEffect, useMemo, useRef, useState } from "react";

import type { RuntimeDashboardSnapshot } from "./runtime-snapshot";
import { RUNTIME_SNAPSHOT_REFRESH_MS } from "./runtime-snapshot";
Expand Down Expand Up @@ -46,15 +46,23 @@ export function RuntimeTrendPanel({
const [durableSnapshot, setDurableSnapshot] = useState<RuntimeDurableSnapshot | null>(null);
const [durableState, setDurableState] = useState<"connecting" | "ready" | "unavailable" | "failed">("connecting");
const [durableError, setDurableError] = useState<string | null>(null);
const [durableRefresh, setDurableRefresh] = useState(0);
const [sourcePreference, setSourcePreference] = useState<RuntimeTrendSource>("durable");
const latestSnapshotRef = useRef(snapshot);
latestSnapshotRef.current = snapshot;
const durableTargetKey = useMemo(() => snapshot.observations
.filter((observation) => observation.mode === "openai_hosted" && observation.environment_id !== null)
.map((observation) => observation.session_id)
.sort()
.join("|"), [snapshot.observations]);
const visibleTrendSamples = useMemo(
() => runtimeTrendRange(trendSamples, selectedTrendRange),
[selectedTrendRange, trendSamples],
);
const durableAvailable = durableState !== "unavailable" || durableSnapshot !== null;
const source: RuntimeTrendSource = allowSourceSelection && sourcePreference === "live"
? "live"
: durableAvailable
: durableSnapshot !== null
? "durable"
: "live";
const selectedSamples = source === "durable"
Expand All @@ -73,11 +81,16 @@ export function RuntimeTrendPanel({
setTrendSamples((current) => appendRuntimeTrendSample(current, snapshot));
}, [snapshot]);

useEffect(() => {
const timer = window.setInterval(() => setDurableRefresh((current) => current + 1), 30_000);
return () => window.clearInterval(timer);
}, []);

useEffect(() => {
const controller = new AbortController();
setDurableState("connecting");
setDurableError(null);
void loadRuntimeHistory(snapshot, selectedDurableRange, controller.signal).then((result) => {
void loadRuntimeHistory(latestSnapshotRef.current, selectedDurableRange, controller.signal).then((result) => {
if (controller.signal.aborted) return;
if (result === null) {
setDurableSnapshot(null);
Expand All @@ -92,19 +105,26 @@ export function RuntimeTrendPanel({
setDurableError(error instanceof Error ? error.message : "Durable Runtime history request failed.");
});
return () => controller.abort();
}, [loadRuntimeHistory, selectedDurableRange, snapshot]);
}, [durableRefresh, durableTargetKey, loadRuntimeHistory, selectedDurableRange]);

const rangeOptions = source === "durable" ? RUNTIME_DURABLE_RANGES : RUNTIME_TREND_RANGES;
const waitingForHistory = source === "live" && (!allowSourceSelection || sourcePreference === "durable");
const sourceStatus = source === "durable"
? durableState === "failed"
? "History stale"
: durableState === "connecting"
? "History · loading"
: `Durable · ${durableSnapshot?.resolutionSeconds ?? 0}s`
: waitingForHistory
? durableState === "failed"
? "Live · history retrying"
: durableState === "unavailable"
? "Live · history unavailable"
: "Live · loading history"
: stale
? "Stale · retrying"
: `Live · ${RUNTIME_SNAPSHOT_REFRESH_MS / 1_000}s`;
const sourceStatusStale = source === "durable" ? durableState === "failed" : stale;
const sourceStatusStale = source === "durable" ? durableState === "failed" : stale || waitingForHistory && durableState === "failed";

return (
<section className="dashboard-runtime-live" aria-labelledby={headingId}>
Expand Down
Loading
Loading