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
9 changes: 7 additions & 2 deletions apps/web/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -949,6 +949,7 @@ export function App() {
void (async () => {
const generation = coreGeneration;
void refreshAgents();
void refreshRuntimeSnapshot();
const refreshed = await refreshSessions();
if (!refreshed || generation !== connectionGenerationRef.current) return;
const filter = sessionAgentFilterRef.current;
Expand All @@ -957,7 +958,7 @@ export function App() {
const sessionId = selectedIdRef.current;
if (sessionId) await refreshSelectedSession(sessionId);
})();
}, [coreGeneration, refreshAgents, refreshFilteredSessions, refreshSelectedSession, refreshSessions]);
}, [coreGeneration, refreshAgents, refreshFilteredSessions, refreshRuntimeSnapshot, refreshSelectedSession, refreshSessions]);

const refreshDashboard = useCallback(() => {
if (dashboardRefreshInFlightRef.current) return;
Expand Down Expand Up @@ -1047,7 +1048,7 @@ export function App() {
}, [refreshAgents, refreshEnvironmentTemplates, refreshSessions, refreshStartupConfiguration, refreshVaults]);

useEffect(() => {
if (view !== "dashboard") return;
if (view !== "dashboard" && view !== "sessions") return;
let timer: number | null = null;
void (async () => {
if (
Expand Down Expand Up @@ -2346,6 +2347,10 @@ export function App() {
turnError={turnError}
turnState={turnState}
environmentObservation={environmentObservation}
runtimeSnapshot={runtimeSnapshot}
runtimeError={runtimeCollectionError}
runtimeStale={runtimeCollectionState === "failed"}
loadRuntimeHistory={loadDashboardRuntimeHistory}
sendError={sendError}
streamError={streamError}
streamState={streamState}
Expand Down
131 changes: 4 additions & 127 deletions apps/web/src/features/dashboard/RuntimeObservabilityContent.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import {
Search,
Server,
} from "lucide-react";
import { useEffect, useMemo, useState, type ReactNode } from "react";
import { useMemo, useState, type ReactNode } from "react";
import {
flexRender,
getCoreRowModel,
Expand All @@ -35,30 +35,11 @@ import {
type RuntimeDashboardRow,
} from "./dashboard-model";
import type { RuntimeDashboardSnapshot } from "./runtime-snapshot";
import { RUNTIME_SNAPSHOT_REFRESH_MS } from "./runtime-snapshot";
import { RuntimeTrendCharts } from "./RuntimeTrendCharts";
import type { RuntimeTrendSource } from "./RuntimeTrendCharts";
import {
RUNTIME_DURABLE_RANGES,
type RuntimeDurableRange,
type RuntimeDurableSnapshot,
} from "./runtime-history";
import {
appendRuntimeTrendSample,
RUNTIME_TREND_RANGES,
RUNTIME_TREND_WINDOW_MS,
runtimeTrendRange,
type RuntimeTrendRange,
type RuntimeTrendSample,
} from "./runtime-trends";
import { RuntimeTrendPanel, type RuntimeHistoryLoader } from "./RuntimeTrendPanel";

const PAGE_SIZE = 10;

export type RuntimeHistoryLoader = (
snapshot: RuntimeDashboardSnapshot,
range: RuntimeDurableRange,
signal: AbortSignal,
) => Promise<RuntimeDurableSnapshot | null>;
export type { RuntimeHistoryLoader } from "./RuntimeTrendPanel";

function RuntimeMetric({
icon,
Expand Down Expand Up @@ -308,68 +289,6 @@ export function RuntimeObservabilityContent({
}) {
const model = useMemo(() => buildRuntimeDashboardModel(snapshot.sessions, snapshot.observations), [snapshot]);
const summary = model.summary;
const [trendSamples, setTrendSamples] = useState<RuntimeTrendSample[]>(() => appendRuntimeTrendSample([], snapshot));
const [selectedTrendRange, setSelectedTrendRange] = useState<RuntimeTrendRange>(RUNTIME_TREND_WINDOW_MS);
const [selectedDurableRange, setSelectedDurableRange] = useState<RuntimeDurableRange>(RUNTIME_DURABLE_RANGES[0].milliseconds);
const [durableSnapshot, setDurableSnapshot] = useState<RuntimeDurableSnapshot | null>(null);
const [durableState, setDurableState] = useState<"connecting" | "ready" | "unavailable" | "failed">("connecting");
const [durableError, setDurableError] = useState<string | null>(null);
const visibleTrendSamples = useMemo(
() => runtimeTrendRange(trendSamples, selectedTrendRange),
[selectedTrendRange, trendSamples],
);
const source: RuntimeTrendSource = durableState === "unavailable" && durableSnapshot === null
? "live"
: "durable";
const selectedSamples = source === "durable"
? durableSnapshot?.samples ?? []
: visibleTrendSamples;
const latestTrendSample = selectedSamples.at(-1);
const selectedRange = source === "durable" ? selectedDurableRange : selectedTrendRange;
const rangeEnd = source === "durable" && durableSnapshot !== null
? durableSnapshot.rangeEnd
: latestTrendSample?.sampledAt ?? snapshot.loadedAt;
const rangeStart = source === "durable" && durableSnapshot !== null
? durableSnapshot.rangeStart
: rangeEnd - selectedTrendRange;

useEffect(() => {
setTrendSamples((current) => appendRuntimeTrendSample(current, snapshot));
}, [snapshot]);

useEffect(() => {
const controller = new AbortController();
setDurableState("connecting");
setDurableError(null);
void loadRuntimeHistory(snapshot, selectedDurableRange, controller.signal).then((result) => {
if (controller.signal.aborted) return;
if (result === null) {
setDurableSnapshot(null);
setDurableState("unavailable");
return;
}
setDurableSnapshot(result);
setDurableState("ready");
}).catch((error: unknown) => {
if (controller.signal.aborted) return;
setDurableState("failed");
setDurableError(error instanceof Error ? error.message : "Durable Runtime history request failed.");
});
return () => controller.abort();
}, [loadRuntimeHistory, selectedDurableRange, snapshot]);

const rangeOptions = source === "durable" ? RUNTIME_DURABLE_RANGES : RUNTIME_TREND_RANGES;
const sourceStatus = source === "durable"
? durableState === "failed"
? "History stale"
: durableState === "connecting"
? "History · loading"
: `Durable · ${durableSnapshot?.resolutionSeconds ?? 0}s`
: stale
? "Stale · retrying"
: `Live · ${RUNTIME_SNAPSHOT_REFRESH_MS / 1_000}s`;
const sourceStatusStale = source === "durable" ? durableState === "failed" : stale;

return (
<>
<div className="dashboard-runtime-summary" aria-label="Runtime resource snapshot">
Expand All @@ -379,49 +298,7 @@ export function RuntimeObservabilityContent({
<RuntimeMetric icon={<Gauge size={17} />} label="Reported tokens" value={formatDashboardTokens(summary.totalTokens)} detail={`${summary.tokenCoverageCount}/${summary.sessionCount} Sessions report usage`} />
</div>

<section className="dashboard-runtime-live" aria-labelledby="dashboard-runtime-live-heading">
<header className="dashboard-runtime-live-toolbar">
<div>
<h3 id="dashboard-runtime-live-heading">Resource trends</h3>
<p>{source === "durable" ? "Retained samples · durable history" : "Browser-local samples · reset on reload"}</p>
</div>
<div className="dashboard-runtime-live-controls">
<span
className={`${sourceStatusStale ? "dashboard-runtime-live-status dashboard-runtime-live-status-stale" : "dashboard-runtime-live-status"}${source === "durable" ? " dashboard-runtime-live-status-durable" : ""}`}
aria-label={source === "durable"
? `${sourceStatus}; ${durableSnapshot?.targetCount ?? 0} Runtime targets`
: stale
? "Runtime sampling refresh failed; showing retained samples"
: `Live Runtime sampling every ${RUNTIME_SNAPSHOT_REFRESH_MS / 1_000} seconds`}
>
<i aria-hidden="true" />{sourceStatus}
</span>
<span className="dashboard-runtime-sample-count">
{selectedSamples.length} {source === "durable" ? selectedSamples.length === 1 ? "bucket" : "buckets" : selectedSamples.length === 1 ? "sample" : "samples"}
{source === "durable" && durableSnapshot ? <> · {durableSnapshot.sampleCount}/{durableSnapshot.expectedSampleCount} observations</> : null}
{latestTrendSample ? <> · <time dateTime={new Date(latestTrendSample.sampledAt).toISOString()}>{new Date(latestTrendSample.sampledAt).toLocaleTimeString([], { hour: "2-digit", minute: "2-digit", second: "2-digit" })}</time></> : null}
</span>
<div className="dashboard-runtime-range" role="group" aria-label={source === "durable" ? "Runtime durable range" : "Runtime live range"}>
{rangeOptions.map((range) => (
<button
key={range.label}
type="button"
aria-pressed={selectedRange === range.milliseconds}
onClick={() => {
if (source === "durable") setSelectedDurableRange(range.milliseconds as RuntimeDurableRange);
else setSelectedTrendRange(range.milliseconds as RuntimeTrendRange);
}}
>
{range.label}
</button>
))}
</div>
</div>
</header>
{durableState === "failed" && durableError ? <p className="dashboard-runtime-history-error" role="status">Durable history refresh failed: {durableError}</p> : null}
{durableState === "unavailable" ? <p className="dashboard-runtime-history-note">Durable history is not configured; Live samples remain available.</p> : null}
<RuntimeTrendCharts samples={selectedSamples} source={source} rangeStart={rangeStart} rangeEnd={rangeEnd} />
</section>
<RuntimeTrendPanel snapshot={snapshot} stale={stale} loadRuntimeHistory={loadRuntimeHistory} />

<details className="dashboard-runtime-explorer">
<summary>
Expand Down
11 changes: 11 additions & 0 deletions apps/web/src/features/dashboard/RuntimeTrendCharts.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,17 @@ describe("Runtime live-window chart accessibility", () => {
expect(html).not.toContain('aria-label="CPU usage: 2 live samples"');
});

it("can retain an honest uptime card when a consumer requires four metric panels", () => {
const html = renderToStaticMarkup(
<RuntimeTrendCharts samples={[sample(60_000, .25)]} source="durable" showDurableUptimePlaceholder />,
);

expect(html.match(/data-chart-engine="uplot"/g)).toHaveLength(4);
expect(html).toContain("Compute uptime");
expect(html).toContain("Live-only metric");
expect(html).toContain("Select Live to inspect current Runtime uptime");
});

it("announces an isolated durable value as sparse rather than empty", () => {
const html = renderToStaticMarkup(
<RuntimeTrendCharts samples={[sample(60_000, .25)]} source="durable" />,
Expand Down
4 changes: 3 additions & 1 deletion apps/web/src/features/dashboard/RuntimeTrendCharts.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -507,11 +507,13 @@ export function RuntimeTrendCharts({
source = "live",
rangeStart,
rangeEnd,
showDurableUptimePlaceholder = false,
}: {
samples: readonly RuntimeTrendSample[];
source?: RuntimeTrendSource;
rangeStart?: number;
rangeEnd?: number;
showDurableUptimePlaceholder?: boolean;
}) {
const charts = useMemo(() => {
const cpuIds = targetIds(samples, "cpuRatio");
Expand Down Expand Up @@ -556,7 +558,7 @@ export function RuntimeTrendCharts({
<div className="dashboard-runtime-trend-grid" aria-label={durable ? "Runtime durable-history charts" : "Runtime live-window charts"}>
<TrendChart title="CPU usage" subtitle={durable ? "bucketed cumulative-delta utilization · durable history" : "reported or cumulative-delta utilization · live window"} samples={samples} series={charts.cpu} maximum={cpuMaximum} formatValue={(value) => `${Math.round(value)}%`} rangeStart={oldest} rangeEnd={newest} source={source} bands={[{ from: 0, to: 30, tone: "safe" }, { from: 30, to: 70, tone: "warning" }, { from: 70, to: 100, tone: "danger" }]} ticks={[1, .7, .3, 0]} emptyMessage={durable ? "No retained CPU samples" : undefined} />
<TrendChart title="Memory usage" subtitle={durable ? "complete target aggregate / configured limit · durable history" : "working set / configured limit · live window"} samples={samples} series={charts.memory} maximum={memoryMaximum} formatValue={(value) => formatDashboardBytes(Math.round(value))} rangeStart={oldest} rangeEnd={newest} source={source} emptyMessage={durable ? "No complete retained memory samples" : undefined} />
{!durable ? <TrendChart title="Compute uptime" subtitle="provider started_at → observed_at · allocation series" samples={samples} series={charts.uptime} maximum={uptimeMaximum} formatValue={(value) => formatDashboardDuration(value)} rangeStart={oldest} rangeEnd={newest} source={source} /> : null}
{!durable || showDurableUptimePlaceholder ? <TrendChart title="Compute uptime" subtitle={durable ? "current Runtime measurement · not retained in durable history" : "provider started_at → observed_at · allocation series"} samples={samples} series={durable ? [] : charts.uptime} maximum={uptimeMaximum} formatValue={(value) => formatDashboardDuration(value)} rangeStart={oldest} rangeEnd={newest} source={source} emptyMessage={durable ? "Live-only metric" : undefined} emptyDetail={durable ? "Select Live to inspect current Runtime uptime" : undefined} /> : null}
<TrendChart title="Token throughput" subtitle={durable ? "canonical Session Usage deltas · durable history" : "Session Usage deltas · missing usage excluded"} samples={samples} series={charts.tokens} maximum={tokenMaximum} formatValue={(value) => `${formatDashboardTokens(Math.round(value))}/min`} rangeStart={oldest} rangeEnd={newest} source={source} emptyMessage={durable ? "No retained token samples" : undefined} />
</div>
);
Expand Down
Loading
Loading