feat(dashboard): lazy routes, Needs-you strip, URL state, exact numbers

- Workspace, SettingsHub, SessionDetail and the Workflow sub-tab are lazy
  (main bundle 2,166 kB -> ~920 kB), wrapped in RouteErrorBoundary: a stale
  chunk after a self-update reloads once (vite:preloadError too), any other
  error shows a Reload panel instead of a blank page.
- Live refresh: lib/throttle.ts + hooks/useLiveRefresh.ts replace trailing
  debounces that froze under a steady event stream; refresh skips hidden
  tabs and refetches after a WebSocket reconnect. Monitor loads only while
  shown, with one batched GET /api/agents?session_ids= instead of N+1; a
  failing endpoint is an inline error, not a blank Dashboard.
- NeedsYouStrip on Monitor and above Analytics. Tabs, filters, page, sort
  and search live in the URL (Dashboard, Sessions, Activity, SessionDetail).
- Sessions: tool / workspace / date-range filters, CSV/JSON export
  (formula-neutralised), focusable row links, Open in workspace (the dead
  /run links are gone), stale responses dropped, error + Retry.
- Fixes: "NaNs" durations, 30/7-day charts skipping idle days, weekday
  labels west of UTC (now averages), cache hit rate denominator, billable
  tokens vs cache reads, exact tool-call counts, "To" date = end of day,
  invalid ?from= no longer throws, recent list no longer shrinks to 15,
  stable message keys, transcript switch races.
- Light-theme-unreadable bg-black code panels and dark-only DAG fills use
  tokens; status configs and chart colours use theme tokens; Health tab
  and other literals translated.
- Server (additive): GET /api/agents?session_ids=, GET /api/sessions
  awaiting=1&from=&to=, stats tool_call_count/distinct_tool_count, event
  rows carry session/agent labels.
This commit is contained in:
2026-10-05 13:22:04 +07:00
parent 8fb703c6db
commit 03f1673fce
63 changed files with 3879 additions and 1662 deletions
@@ -14,7 +14,7 @@
import { useCallback, useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
import { api } from "../../lib/api";
import { eventBus } from "../../lib/eventBus";
import { useLiveRefresh } from "../../hooks/useLiveRefresh";
import type { PipelineStats } from "../../lib/types";
/** Percentage of `n` out of `total`, 0 when nothing was recorded. */
@@ -26,7 +26,12 @@ function pct(n: number, total: number): number {
* Per-node pass statistics for the selected pipeline template.
* @returns The panel element, or an empty-state hint when nothing was recorded.
*/
export function LanePipelinePanel() {
export function LanePipelinePanel({
onAvailability,
}: {
/** Told whether any pipeline template is loaded (pipelines are dormant by default). */
onAvailability?: (hasPipelines: boolean) => void;
} = {}) {
const { t } = useTranslation("workflows");
const [data, setData] = useState<PipelineStats | null>(null);
const [selectedId, setSelectedId] = useState<string | null>(null);
@@ -46,18 +51,14 @@ export function LanePipelinePanel() {
void refresh();
}, [refresh]);
// Stages move as agents declare them; debounce so a burst costs one fetch.
// Stages move as agents declare them; throttled (not debounced) so a burst
// costs one fetch and a steady stream still refreshes every 3 s.
useLiveRefresh(() => void refresh(), { match: (msg) => msg.type === "lane_update" });
const hasPipelines = !!data && data.pipelines.length > 0;
useEffect(() => {
let timer: ReturnType<typeof setTimeout>;
const unsub = eventBus.subscribe(() => {
clearTimeout(timer);
timer = setTimeout(() => void refresh(), 3000);
});
return () => {
unsub();
clearTimeout(timer);
};
}, [refresh]);
if (data) onAvailability?.(hasPipelines);
}, [data, hasPipelines, onAvailability]);
if (error) return <p className="text-xs text-status-danger">{error}</p>;
if (!data) return null;
@@ -71,7 +72,7 @@ export function LanePipelinePanel() {
<select
data-testid="pipeline-stats-lane-picker"
aria-label={t("lanes.laneLabel")}
className="max-w-[14rem] truncate rounded border border-border bg-surface-1 px-2 py-0.5 text-xs text-fg-secondary"
className="select max-w-[14rem] truncate py-0.5 text-xs"
value={laneId ?? ""}
onChange={(e) => setLaneId(e.target.value ? Number(e.target.value) : null)}
>
@@ -113,7 +114,7 @@ export function LanePipelinePanel() {
<select
data-testid="pipeline-stats-picker"
aria-label={t("lanes.pipelineLabel")}
className="rounded border border-border bg-surface-1 px-2 py-0.5 text-xs text-fg-secondary"
className="select w-auto py-0.5 text-xs"
value={selected.id}
onChange={(e) => setSelectedId(e.target.value)}
>