-
- {BENCH_RANGES.map((r) => (
- setRange(r)}
- >
- {r}d
-
- ))}
-
- {runs.data.nextCursor !== null ? (
+ {runs.nextCursor !== null ? (
Figures reflect the 100 most recent runs — more exist.
) : null}
diff --git a/apps/web/src/pages/insights-page.tsx b/apps/web/src/pages/insights-page.tsx
index bb837b5ed..979aea514 100644
--- a/apps/web/src/pages/insights-page.tsx
+++ b/apps/web/src/pages/insights-page.tsx
@@ -5,12 +5,10 @@ import {
Badge,
PageShell,
RichEmptyState,
- RUN_STATUS_DOT_TONE,
RUN_STATUS_TONE,
Skeleton,
StatGrid,
StatGridItem,
- StatusDot,
Table,
TableBody,
TableCell,
@@ -20,16 +18,20 @@ import {
type BadgeTone,
type RunStatus,
} from "@corbits/react-ui";
-import { ArrowRight, ChartBar } from "@/lib/icons";
+import { ChartBar } from "@/lib/icons";
import { runOutcomeStatus, runStatusLabel, withListingAbandoned } from "@corbits/workflows/client";
import type * as React from "react";
-import { useEffect, useState } from "react";
+import { useState } from "react";
import { workflowRunStatuses, type WorkflowRunStatus } from "@intx/types";
import { SignedOutNotice, type APIQuery } from "@/lib/api-query";
import { workbenchesQueryKey, listWorkbenches } from "@/chat/workbench-tenants";
import { useBench } from "../bench-context";
+import { readLastWorkbenchId } from "../last-workbench";
+import { Redirect } from "../redirect";
+import { NEW_WORKBENCH_PATH } from "../routes";
+import { workbenchInsightsPath } from "../insights-deeplinks";
import { BenchInsights } from "./bench-insights";
import { resolveWorkbenchInsightsScope } from "../insights-workbench-scope";
import { parseInsightsPath } from "../insights-path";
@@ -42,24 +44,17 @@ import {
type InsightsRun,
} from "../insights-api";
import {
- computeInsightsStats,
durationLabel,
- formatCount,
groupRunsByDefinition,
purposeRunsForInsights,
runDisplayName,
} from "../insights-stats";
import { useNavigate } from "../navigation";
-import { tenantKeys } from "../query-client";
import { useAPIQuery } from "../api";
import { INSIGHTS_PATH_PREFIX, INSIGHTS_RUNS_PATH } from "../path-ids";
import { benchLink, useFromBench } from "../shell/page-crumbs";
import { StageTopBar } from "../shell/stage-top-bar";
-import {
- listScheduledWorkflows,
- useTenantQuery,
- type ScheduledWorkflowDefinition,
-} from "../routines-api";
+import { useTenantQuery } from "../routines-api";
export function formatWhen(iso: string): string {
const date = new Date(iso);
@@ -98,12 +93,6 @@ function insightsStatusTone(status: string): BadgeTone {
return "neutral";
}
-function tileValue(value: string | number | null, loading: boolean): string {
- if (loading) return "";
- if (value === null) return "—";
- return String(value);
-}
-
function InsightsStat({
label,
value,
@@ -155,226 +144,10 @@ function onRowActivate(onActivate: () => void) {
};
}
-function runsDetailLabel(stats: { readonly running: number; readonly errored: number }): string {
- if (stats.running > 0) {
- return `${formatCount(stats.running)} running`;
- }
- if (stats.errored > 0) {
- return `${formatCount(stats.errored)} errored`;
- }
- return "runs";
-}
-
-const ELAPSED_TICK_MS = 1_000;
-
-// So the elapsed label counts up like the live indicator, instead of
-// freezing at whatever instant this component last rendered.
-function useTickingNow(enabled: boolean): number {
- const [now, setNow] = useState(() => Date.now());
- useEffect(() => {
- if (!enabled) return undefined;
- const timer = setInterval(() => setNow(Date.now()), ELAPSED_TICK_MS);
- return () => clearInterval(timer);
- }, [enabled]);
- return now;
-}
-
-// Liveness is not a windowed property, so this filters the full run set,
-// never the range-filtered one. A persisted `endedAt` means the fire
-// already finished, even if `status` still reads `running`.
-export function isRunningNow(run: InsightsRun, now: number = Date.now()): boolean {
- const outcome = runOutcomeStatus(withListingAbandoned(run, now), now);
- return outcome === "running" || outcome === "updating";
-}
-
function insightsRunStatus(run: InsightsRun, now: number = Date.now()): string {
return runOutcomeStatus(withListingAbandoned(run, now), now) ?? run.status;
}
-// Wall-clock time since start, never a fabricated live counter.
-export function elapsedLabel(createdAt: string, now: number): string {
- const startMs = Date.parse(createdAt);
- if (Number.isNaN(startMs)) return "—";
- return durationLabel(Math.max(0, now - startMs));
-}
-
-// Renders nothing when nothing is running — same convention as react-ui's
-// `WorkflowDock`: an empty strip reports the normal case, not an empty
-// state worth showing.
-function RunningNowStrip({
- runs,
- onOpenRun,
-}: {
- readonly runs: readonly InsightsRun[];
- readonly onOpenRun: (id: string) => void;
-}) {
- const maybeLive = runs.some((run) => run.status === "running" || run.status === "updating");
- const now = useTickingNow(maybeLive);
- const running = runs.filter((run) => isRunningNow(run, now));
- if (running.length === 0) return null;
-
- return (
-
-
-
Running now
-
- {formatCount(running.length)} in progress
-
-
-
- {running.map((run) => (
-
- onOpenRun(run.id)}>
-
- {runDisplayName(run)}
- {elapsedLabel(run.createdAt, now)}
- {runStatusLabel("running")}
-
-
- ))}
-
-
- );
-}
-
-function RecentRunRows({
- runs,
- onOpenRun,
- onOpenRuns,
-}: {
- readonly runs: readonly InsightsRun[];
- readonly onOpenRun: (id: string) => void;
- readonly onOpenRuns: () => void;
-}) {
- return (
-
-
- {runs.map((row) => (
- onOpenRun(row.id))}
- >
-
-
- {runDisplayName(row)}
-
- {formatWhen(row.createdAt)}
-
-
-
-
-
- {runStatusLabel(insightsRunStatus(row))}
-
-
-
- ))}
-
-
-
- All runs
-
-
-
-
-
-
- );
-}
-
-function InsightsLanding({
- runs,
- runsNextCursor,
- routines,
- loading,
- onOpenRun,
- onOpenRuns,
-}: {
- readonly runs: readonly InsightsRun[];
- // Non-null means more runs exist than fetched, so KPIs disclose the cap
- // instead of presenting a truncated series as complete.
- readonly runsNextCursor: string | null;
- readonly routines: readonly ScheduledWorkflowDefinition[];
- readonly loading: boolean;
- readonly onOpenRun: (id: string) => void;
- readonly onOpenRuns: () => void;
-}) {
- const stats = computeInsightsStats(runs, routines);
- const purposeRuns = purposeRunsForInsights(runs);
- const runningNow = purposeRuns.filter((run) => isRunningNow(run));
- const recent = purposeRuns.slice(0, 12);
-
- return (
-
-
-
-
-
- {runningNow.length > 0 || loading ? (
-
- ) : null}
-
-
-
-
- {runsNextCursor !== null ? (
-
- Runs and outcomes below reflect the 100 most recent runs — more exist.{" "}
-
- See all runs
-
- .
-
- ) : null}
-
-
-
-
Recent runs
-
- {recent.length > 0 ? (
-
- ) : (
- }
- title="No runs yet"
- description="When a routine or automation fires, it shows up here."
- />
- )}
-
-
- );
-}
-
export function runDurationLabel(run: InsightsRun): string {
if (run.endedAt === undefined || run.endedAt === null) return "—";
const startMs = Date.parse(run.createdAt);
@@ -598,7 +371,6 @@ export function InsightsRunDetail({
export function InsightsPage({
path,
runs,
- routines,
tenantId = null,
}: {
readonly path: string;
@@ -606,18 +378,15 @@ export function InsightsPage({
data: readonly InsightsRun[];
nextCursor: string | null;
}>;
- readonly routines: APIQuery
;
readonly tenantId?: string | null;
}) {
const navigate = useNavigate();
const { mode, runId } = parseInsightsPath(path);
- const unauth = runs.kind === "unauthenticated" || routines.kind === "unauthenticated";
-
- if (unauth) {
+ if (runs.kind === "unauthenticated") {
return (
-
+
@@ -625,124 +394,91 @@ export function InsightsPage({
);
}
- const loading = runs.kind === "loading" || routines.kind === "loading";
-
const runsData = runs.kind === "ready" ? runs.data.data : [];
const runsNextCursor = runs.kind === "ready" ? runs.data.nextCursor : null;
- const routinesData = routines.kind === "ready" ? routines.data : [];
if (mode === "run" && runId !== null) {
const run = runsData.find((r) => r.id === runId) ?? null;
return
;
}
- if (mode === "runs") {
- return (
-
navigate(`${INSIGHTS_RUNS_PATH}/${encodeURIComponent(id)}`)}
- />
- );
- }
-
- if (runs.kind === "error") {
- return (
-
-
-
- }
- title="Couldn't load insights"
- description="Something went wrong on our side. Try again in a moment."
- actions={[{ label: "Retry", onClick: runs.retry }]}
- />
-
-
- );
- }
-
return (
-
-
-
-
- navigate(`${INSIGHTS_RUNS_PATH}/${encodeURIComponent(id)}`)}
- onOpenRuns={() => navigate(INSIGHTS_RUNS_PATH)}
- />
-
-
-
+ navigate(`${INSIGHTS_RUNS_PATH}/${encodeURIComponent(id)}`)}
+ />
);
}
// Titles the page by the workbench name, never the tenant's. A legacy or
// mis-wired id gets an honest empty state instead of a doomed fetch.
function InsightsWorkbenchPage({
+ workbenchId,
workbenchesLoading,
resolution,
onOpenRun,
}: {
+ readonly workbenchId: string;
readonly onOpenRun: (id: string) => void;
readonly workbenchesLoading: boolean;
readonly resolution: ReturnType;
}) {
- if (workbenchesLoading) {
+ if (resolution.kind === "ready") {
return (
-
+
);
}
- if (resolution.kind === "not-found") {
- return (
-
-
-
+ return (
+
+
+
+ {workbenchesLoading ? (
+
+ ) : (
}
- title="Workbench not found"
- description="This conversation may have been deleted, or you may not have access to it."
+ title={
+ resolution.kind === "not-found"
+ ? "Workbench not found"
+ : "No insights for this workbench yet"
+ }
+ description={
+ resolution.kind === "not-found"
+ ? "This workbench may have been deleted, or you may not have access to it."
+ : "This workbench predates per-workbench insights."
+ }
/>
-
-
- );
- }
- if (resolution.kind === "legacy") {
+ )}
+
+
+ );
+}
+
+// `/insights` with no workbench hops to the last-visited workbench's
+// Insights, or the new-workbench picker when none is on record.
+function InsightsLandingRedirect({ benchTenantId }: { readonly benchTenantId: string | null }) {
+ const navigate = useNavigate();
+ const { workbenches, isLoading } = useWorkbenchList(benchTenantId);
+ if (isLoading) {
return (
-
-
-
- }
- title="No insights for this conversation yet"
- description="This conversation predates per-workbench insights."
- />
-
-
+
+
+
);
}
- return (
-
- );
+ const lastId = benchTenantId === null ? null : readLastWorkbenchId(benchTenantId);
+ const to =
+ lastId !== null && workbenches.some((workbench) => workbench.id === lastId)
+ ? workbenchInsightsPath(lastId)
+ : NEW_WORKBENCH_PATH;
+ return ;
}
export function InsightsRoute({ path }: { readonly path?: string }) {
@@ -757,24 +493,13 @@ export function InsightsRoute({ path }: { readonly path?: string }) {
const { mode, workbenchId } = parseInsightsPath(currentPath);
const runs = useAPIQuery(
- selectedTenantId === null ? "" : insightsTopLevelRunsPath(selectedTenantId),
+ selectedTenantId === null || mode === "landing" || mode === "workbench"
+ ? ""
+ : insightsTopLevelRunsPath(selectedTenantId),
TopLevelRunsSchema,
);
- const routines = useTenantQuery(
- selectedTenantId === null
- ? ["tenant", "none", "routines"]
- : tenantKeys.routines(selectedTenantId),
- selectedTenantId !== null,
- () => listScheduledWorkflows(selectedTenantId as string),
- );
- const routinesForPage: APIQuery =
- selectedTenantId === null ? { kind: "ready", data: [] } : routines;
-
- const runsForPage: APIQuery<{
- data: readonly InsightsRun[];
- nextCursor: string | null;
- }> = selectedTenantId === null ? { kind: "ready", data: { data: [], nextCursor: null } } : runs;
+ if (mode === "landing") return ;
if (mode === "workbench" && workbenchId !== null) {
return (
@@ -788,19 +513,15 @@ export function InsightsRoute({ path }: { readonly path?: string }) {
);
}
- return (
-
- );
+ const runsForPage: APIQuery<{
+ data: readonly InsightsRun[];
+ nextCursor: string | null;
+ }> = selectedTenantId === null ? { kind: "ready", data: { data: [], nextCursor: null } } : runs;
+
+ return ;
}
-/** Resolves the workbench-scoped route's own workbench list — split out of
- * `InsightsRoute` so the landing/runs/run-detail modes above never pay for
- * a workbench-list fetch they don't need. */
+/** Resolves the workbench-scoped route's own workbench list. */
function InsightsWorkbenchPageRoute({
workbenchId,
benchTenantId,
@@ -814,6 +535,7 @@ function InsightsWorkbenchPageRoute({
const resolution = resolveWorkbenchInsightsScope(workbenches, workbenchId);
return (