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:
+79
-11
@@ -40,9 +40,9 @@
|
||||
* - `./components/SplashScreen`
|
||||
* - `./pages/Dashboard`
|
||||
* - `./pages/History`
|
||||
* - `./pages/SessionDetail`
|
||||
* - `./pages/SettingsHub`
|
||||
* - `./pages/Workspace`
|
||||
* - `./pages/SessionDetail` (lazy)
|
||||
* - `./pages/SettingsHub` (lazy)
|
||||
* - `./pages/Workspace` (lazy)
|
||||
*
|
||||
* ## Testing pointers
|
||||
* - Prefer colocated `__tests__` with Vitest + Testing Library for UI.
|
||||
@@ -54,23 +54,57 @@
|
||||
* - `skills/file-headers/` — mandatory `@author` header policy.
|
||||
* ============================================================================= */
|
||||
|
||||
import { BrowserRouter, Routes, Route, Navigate } from "react-router-dom";
|
||||
import { useCallback } from "react";
|
||||
import { BrowserRouter, Routes, Route, Navigate, useParams } from "react-router-dom";
|
||||
import { lazy, Suspense, useCallback, useEffect, type ReactNode } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Layout } from "./components/Layout";
|
||||
import { DocumentTitle } from "./components/DocumentTitle";
|
||||
import { SplashScreen } from "./components/SplashScreen";
|
||||
import { GlobalTooltip } from "./components/Tooltip";
|
||||
import { Toaster } from "./components/Toaster";
|
||||
import { ConfirmHost } from "./components/ConfirmHost";
|
||||
import { RouteErrorBoundary, reloadOnce } from "./components/RouteErrorBoundary";
|
||||
import { Dashboard } from "./pages/Dashboard";
|
||||
import { History } from "./pages/History";
|
||||
import { SessionDetail } from "./pages/SessionDetail";
|
||||
import { SettingsHub } from "./pages/SettingsHub";
|
||||
import { Workspace } from "./pages/Workspace";
|
||||
import { NotFound } from "./pages/NotFound";
|
||||
import { useWebSocket } from "./hooks/useWebSocket";
|
||||
import { useNotifications } from "./hooks/useNotifications";
|
||||
import { eventBus } from "./lib/eventBus";
|
||||
import type { WSMessage } from "./lib/types";
|
||||
|
||||
// The heavy pages (xterm, the diff viewer, the settings editors) load on first
|
||||
// visit instead of in the initial bundle.
|
||||
const Workspace = lazy(() => import("./pages/Workspace").then((m) => ({ default: m.Workspace })));
|
||||
const SettingsHub = lazy(() =>
|
||||
import("./pages/SettingsHub").then((m) => ({ default: m.SettingsHub }))
|
||||
);
|
||||
const SessionDetail = lazy(() =>
|
||||
import("./pages/SessionDetail").then((m) => ({ default: m.SessionDetail }))
|
||||
);
|
||||
|
||||
function PageFallback() {
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<div role="status" className="flex items-center justify-center py-20 text-sm text-fg-muted">
|
||||
{t("common:loading")}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function Page({ children }: { children: ReactNode }) {
|
||||
return (
|
||||
<RouteErrorBoundary>
|
||||
<Suspense fallback={<PageFallback />}>{children}</Suspense>
|
||||
</RouteErrorBoundary>
|
||||
);
|
||||
}
|
||||
|
||||
/** Keyed by the id so moving to another session starts from fresh state. */
|
||||
function SessionDetailRoute() {
|
||||
const { id } = useParams();
|
||||
return <SessionDetail key={id} />;
|
||||
}
|
||||
|
||||
/**
|
||||
* Application root component mounted by {@link main.tsx}.
|
||||
* @returns Routed dashboard UI inside `BrowserRouter`.
|
||||
@@ -83,19 +117,39 @@ export default function App() {
|
||||
const { connected } = useWebSocket(onMessage);
|
||||
useNotifications();
|
||||
|
||||
// A self-update replaced the hashed assets this tab still references: Vite
|
||||
// reports the failed preload here; reload once (guarded against loops).
|
||||
useEffect(() => {
|
||||
const onPreloadError = (e: Event) => {
|
||||
if (reloadOnce()) e.preventDefault();
|
||||
};
|
||||
window.addEventListener("vite:preloadError", onPreloadError);
|
||||
return () => window.removeEventListener("vite:preloadError", onPreloadError);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<>
|
||||
<SplashScreen />
|
||||
{/* One renderer gives every `title` in the app the designed bubble —
|
||||
see components/Tooltip.tsx for why it is not a wrapper component. */}
|
||||
<GlobalTooltip />
|
||||
{/* App-wide toasts and promise-based confirms: lib/toast.ts, lib/confirm.ts. */}
|
||||
<Toaster />
|
||||
<ConfirmHost />
|
||||
<BrowserRouter>
|
||||
<DocumentTitle />
|
||||
<Routes>
|
||||
<Route element={<Layout wsConnected={connected} />}>
|
||||
<Route index element={<Dashboard />} />
|
||||
<Route path="sessions" element={<History />} />
|
||||
<Route path="sessions/:id" element={<SessionDetail />} />
|
||||
<Route
|
||||
path="sessions/:id"
|
||||
element={
|
||||
<Page>
|
||||
<SessionDetailRoute />
|
||||
</Page>
|
||||
}
|
||||
/>
|
||||
<Route path="activity" element={<Navigate to="/sessions?view=events" replace />} />
|
||||
<Route
|
||||
path="workflows"
|
||||
@@ -107,8 +161,22 @@ export default function App() {
|
||||
element={<Navigate to="/settings/agents/claude-code" replace />}
|
||||
/>
|
||||
<Route path="run" element={<Navigate to="/workspace" replace />} />
|
||||
<Route path="workspace" element={<Workspace />} />
|
||||
<Route path="settings/*" element={<SettingsHub />} />
|
||||
<Route
|
||||
path="workspace"
|
||||
element={
|
||||
<Page>
|
||||
<Workspace />
|
||||
</Page>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path="settings/*"
|
||||
element={
|
||||
<Page>
|
||||
<SettingsHub />
|
||||
</Page>
|
||||
}
|
||||
/>
|
||||
<Route path="*" element={<NotFound />} />
|
||||
</Route>
|
||||
</Routes>
|
||||
|
||||
Reference in New Issue
Block a user