Skip to content
Open
Show file tree
Hide file tree
Changes from 19 commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
00bf30d
refine jobs and operations design
mira-2026 Aug 18, 2026
7b200d2
Refine Delivery layout and responsive actions
mira-2026 Aug 19, 2026
515fbdc
Refine release alert width
mira-2026 Aug 19, 2026
f08b41e
feat(greenfield): refine database UI and table sorting
mira-2026 Aug 19, 2026
2a62a7b
test(storybook): stabilize jobs run detail
mira-2026 Aug 19, 2026
883d7ee
fix: address dashboard review feedback
mira-2026 Aug 19, 2026
ea91c67
fix(greenfield): address dashboard review follow-ups
mira-2026 Aug 19, 2026
1e86aac
fix(greenfield): address follow-up review findings
mira-2026 Aug 19, 2026
692f9e6
fix(greenfield): address latest review findings
mira-2026 Aug 19, 2026
ae0916e
fix(greenfield): start table sorting ascending
mira-2026 Aug 19, 2026
e45bb23
fix(greenfield): preserve paginated operational history
mira-2026 Aug 19, 2026
e7e4f79
fix(greenfield): balance job history columns
mira-2026 Aug 19, 2026
76893dc
fix(greenfield): widen job history timestamps
mira-2026 Aug 19, 2026
eda4a0c
fix(greenfield): keep job timestamps on one line
mira-2026 Aug 19, 2026
b40d3f6
fix(greenfield): expose error-only retry controls
mira-2026 Aug 19, 2026
e20124d
fix(greenfield): address follow-up review findings
mira-2026 Aug 19, 2026
e027c77
fix(greenfield): refine history refresh and retained states
mira-2026 Aug 19, 2026
a3dd5cf
fix(greenfield): refresh mutable history archives
mira-2026 Aug 19, 2026
d6ac3ac
fix(greenfield): stabilize live history pagination
mira-2026 Aug 19, 2026
99a46f7
fix(greenfield): address retained history review
mira-2026 Aug 19, 2026
5a27c22
fix(greenfield): preserve history recovery and paging
mira-2026 Aug 20, 2026
5280856
fix(greenfield): refresh prefixed security audit
mira-2026 Aug 20, 2026
2ecd6f5
fix(greenfield): restore production blocker alerts
mira-2026 Aug 20, 2026
c09d24d
fix(greenfield): order production blockers responsively
mira-2026 Aug 20, 2026
c9da977
fix(greenfield): retain latest live history values
mira-2026 Aug 20, 2026
751bf3a
fix(greenfield): propagate retained heartbeat freshness
mira-2026 Aug 20, 2026
2e8c003
fix(greenfield): reset filtered history after rebase
mira-2026 Aug 20, 2026
9851011
fix(greenfield): reset task history after rebase
mira-2026 Aug 20, 2026
41c11c3
fix(greenfield): restore partial read recovery
mira-2026 Aug 20, 2026
7ce2b26
fix(greenfield): preserve partial dashboard history
mira-2026 Aug 20, 2026
5ff9f40
fix(greenfield): stabilize legacy run page identities
mira-2026 Aug 20, 2026
2d8bff2
fix(greenfield): close remaining history edge cases
mira-2026 Aug 20, 2026
5492d87
fix(greenfield): align retained story fixtures
mira-2026 Aug 20, 2026
c3c07b8
fix(greenfield): preserve incidents when paging archive
mira-2026 Aug 20, 2026
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
373 changes: 187 additions & 186 deletions greenfield/.bun-browser-test-timings.json

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -303,6 +303,11 @@
},
"minItems": 1,
"maxItems": 100
},
"stackNumber": {
"type": "integer",
"minimum": 1,
"maximum": 9007199254740991
}
},
"required": [
Expand Down Expand Up @@ -683,6 +688,11 @@
},
"minItems": 1,
"maxItems": 100
},
"stackNumber": {
"type": "integer",
"minimum": 1,
"maximum": 9007199254740991
}
},
"required": [
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
"limit": {
"type": "integer",
"minimum": 1,
"maximum": 500,
"maximum": 5000,
"default": 200
},
"query": {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@
],
"additionalProperties": false
},
"maxItems": 500,
"maxItems": 5000,
"$comment": "Live Valibot validation additionally requires every redacted log line ID to be unique."
},
"observedAtMs": {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
"limit": {
"type": "integer",
"minimum": 1,
"maximum": 500,
"maximum": 5000,
"default": 200
},
"sourceId": {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@
],
"additionalProperties": false
},
"maxItems": 500,
"maxItems": 5000,
"$comment": "Live Valibot validation additionally requires every redacted log line ID to be unique."
},
"observedAtMs": {
Expand Down
4 changes: 2 additions & 2 deletions greenfield/scripts/runCoverage.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ describe("coverage runner", () => {
test("discovers exact current inventories and creates nine complete batches", async () => {
const inventories = await loadCoverageTestInventories(projectRoot);
expect(inventories.bun).toHaveLength(510);
expect(inventories.browser).toHaveLength(190);
expect(inventories.browser).toHaveLength(191);
expect(inventories.storybook).toHaveLength(87);

const plans = createCoveragePartitionPlan("/tmp/coverage", inventories);
Expand Down Expand Up @@ -98,7 +98,7 @@ describe("coverage runner", () => {
.filter(({ partition }) => partition === "browser")
.flatMap(({ testFiles }) => testFiles)
).size
).toBe(190);
).toBe(191);
expect(
new Set(
plans
Expand Down
3 changes: 0 additions & 3 deletions greenfield/scripts/storybookPageCoverage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -151,9 +151,6 @@ export const dashboardPageStoryRequirements = Object.freeze([
"RunDetail",
"ScheduleDetail",
"Busy",
"Confirmation",
"Queued",
"UnknownOutcome",
],
file: "src/browser/jobs/stories/JobsRoute.stories.tsx",
path: "/jobs",
Expand Down
10 changes: 8 additions & 2 deletions greenfield/src/browser/agents/AgentHistoryTable.tsx
Original file line number Diff line number Diff line change
@@ -1,17 +1,18 @@
import { createColumnHelper, tableFeatures, useTable } from "@tanstack/react-table";
import { createColumnHelper, useTable } from "@tanstack/react-table";
import { History } from "lucide-react";

import type { AgentTaskRun } from "../../contracts/agentModel.ts";
import { formatDashboardDateTime } from "../lib/formatDateTime.ts";
import { Badge } from "../ui/Badge.tsx";
import { dashboardTableFeatures } from "../ui/dashboardTableFeatures.ts";
import { DataTable } from "../ui/DataTable.tsx";
import { EmptyState } from "../ui/EmptyState.tsx";
import { Heading } from "../ui/Heading.tsx";
import { Text } from "../ui/Text.tsx";
import { Virtualizer, type VirtualizerRenderState } from "../ui/Virtualizer.tsx";

const minimumVirtualizedRows = 50;
const historyTableFeatures = tableFeatures({});
const historyTableFeatures = dashboardTableFeatures;
Comment thread
rajohan marked this conversation as resolved.
const historyColumnHelper = createColumnHelper<
typeof historyTableFeatures,
AgentTaskRun
Expand All @@ -25,10 +26,12 @@ const historyColumns = historyColumnHelper.columns([
</Text>
),
header: "Agent",
enableSorting: false,
}),
historyColumnHelper.accessor("task", {
cell: ({ getValue }) => <Text className="wrap-break-word">{getValue()}</Text>,
header: "Task",
enableSorting: false,
}),
historyColumnHelper.accessor("status", {
cell: ({ getValue }) => (
Expand All @@ -37,6 +40,7 @@ const historyColumns = historyColumnHelper.columns([
</Badge>
),
header: "Status",
enableSorting: false,
}),
historyColumnHelper.accessor("startedAtMs", {
cell: ({ getValue }) => (
Expand All @@ -45,6 +49,7 @@ const historyColumns = historyColumnHelper.columns([
</time>
),
header: "Started",
enableSorting: false,
}),
historyColumnHelper.accessor(
(run) => (run.status === "completed" ? run.completedAtMs : undefined),
Expand All @@ -63,6 +68,7 @@ const historyColumns = historyColumnHelper.columns([
},
header: "Completed",
id: "completedAtMs",
enableSorting: false,
}
),
]);
Expand Down
27 changes: 26 additions & 1 deletion greenfield/src/browser/agents/AgentsRoute.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { act } from "react";

import type { AgentStatusProjection } from "../../contracts/agentModel.ts";
import type { AuthStatus } from "../../contracts/auth.ts";
import { liveHistoryArchiveQueryKey } from "../api/liveHistory.ts";
import { createDashboardQueryClient } from "../api/queryClient.ts";
import {
createDashboardTrpcClient,
Expand All @@ -20,7 +21,12 @@ import { createDashboardRouter } from "../router.tsx";
import type { DashboardWebAuthnClient } from "../security/webauthn/webauthnClient.ts";
import { captureExpectedConsoleErrors } from "../test/expectedConsoleError.ts";
import { noOpDashboardRealtimeClient } from "../test/realtime.ts";
import { agentConfigurationQueryKey, agentStatusesQueryKey } from "./agentQueries.ts";
import {
agentConfigurationQueryKey,
agentQueryKey,
agentStatusesQueryKey,
refreshAgentQueries,
} from "./agentQueries.ts";

const { render, screen, waitFor } = await import("@testing-library/react");
const userEventModule = await import("@testing-library/user-event");
Expand Down Expand Up @@ -66,6 +72,7 @@ class AgentTransport implements DashboardTrpcTransport {
startedAtMs: timestampMs,
state: "working",
};
historyQueryCount = 0;
statusQueryCount = 0;
statusQueryResponse: Promise<unknown> | undefined;

Expand Down Expand Up @@ -118,6 +125,7 @@ class AgentTransport implements DashboardTrpcTransport {
});
}
case "agents.listTaskHistory": {
this.historyQueryCount += 1;
return Promise.resolve({
runs: [
{
Expand Down Expand Up @@ -231,6 +239,16 @@ function renderAgentRoute(
}

describe("Dashboard agents route", () => {
test("invalidates mutable archived agent runs", async () => {
const queryClient = createDashboardQueryClient();
const archiveKey = liveHistoryArchiveQueryKey([...agentQueryKey, "history"]);
queryClient.setQueryData(archiveKey, { pages: [] });

await refreshAgentQueries(queryClient);

expect(queryClient.getQueryState(archiveKey)?.isInvalidated).toBeTrue();
});

test("recovers an initial configuration failure through explicit retry", async () => {
const transport = new AgentTransport();
const queryClient = createDashboardQueryClient();
Expand Down Expand Up @@ -381,6 +399,9 @@ describe("Dashboard agents route", () => {

await expectAgentShellReady();
expect(await screen.findByText("Newest agent task")).toBeTruthy();
expect(
screen.getByRole("columnheader", { name: "Started" }).querySelector("button")
).toBeNull();
await user.click(screen.getByRole("button", { name: "Load older tasks" }));
expect(await screen.findByText("Older agent task")).toBeTruthy();
expect(screen.getByText("Newest agent task")).toBeTruthy();
Expand Down Expand Up @@ -490,8 +511,12 @@ describe("Dashboard agents route", () => {
expect(screen.queryByText(/redacted status failure/u)).toBeNull();
expect(screen.queryByRole("button", { name: "Refresh" })).toBeNull();
transport.statusQueryResponse = undefined;
const historyQueriesBeforeRetry = transport.historyQueryCount;
await user.click(screen.getByRole("button", { name: "Try again" }));
await waitFor(() => expect(screen.queryByRole("alert")).toBeNull());
await waitFor(() =>
expect(transport.historyQueryCount).toBe(historyQueriesBeforeRetry + 2)
);
expect(screen.getAllByText("Implement agents route")).toHaveLength(2);
consoleErrors.expectObserved();
} finally {
Expand Down
38 changes: 28 additions & 10 deletions greenfield/src/browser/agents/AgentsRoute.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,15 @@
import { useLiveQuery } from "@tanstack/react-db";
import { useInfiniteQuery } from "@tanstack/react-query";
import { useInfiniteQuery, useQuery } from "@tanstack/react-query";

import type {
AgentDefinition,
AgentStatusProjection,
AgentTaskRun,
} from "../../contracts/agentModel.ts";
import {
liveHistoryRowIdentity,
useAccumulatedLiveHistoryRows,
} from "../api/liveHistory.ts";
import { useDashboardTrpcClient } from "../api/trpcContextValue.ts";
import { dashboardBrowserFailureMessage } from "../api/trpcError.ts";
import { useDashboardBrowserCollections } from "../data/dashboardCollectionsContextValue.ts";
Expand All @@ -14,7 +18,10 @@ import { Button } from "../ui/Button.tsx";
import { LoadingState } from "../ui/LoadingState.tsx";
import { PageHeader } from "../ui/PageHeader.tsx";
import { AgentHistoryTable } from "./AgentHistoryTable.tsx";
import { agentHistoryQueryOptions } from "./agentQueries.ts";
import {
agentHistoryLiveHeadQueryOptions,
agentHistoryQueryOptions,
} from "./agentQueries.ts";
import { AgentStatusGrid } from "./AgentStatusGrid.tsx";
import { useAgentCollectionQueryState } from "./useAgentCollectionQueryState.ts";
import { useAgentRealtimeInvalidation } from "./useAgentRealtimeInvalidation.ts";
Expand All @@ -32,12 +39,19 @@ export function AgentsRoute() {
const statuses = useLiveQuery(collections.statuses);
const collectionQueries = useAgentCollectionQueryState();
const history = useInfiniteQuery(agentHistoryQueryOptions(client));
const historyLiveHead = useQuery(agentHistoryLiveHeadQueryOptions(client));
Comment thread
rajohan marked this conversation as resolved.
const agents = configuration.data ?? emptyAgents;
const agentStatuses = statuses.data ?? emptyStatuses;
const runs = history.data?.pages.flatMap((page) => page.runs) ?? emptyRuns;
const runs = useAccumulatedLiveHistoryRows(
historyLiveHead.data?.runs ?? [],
history.data?.pages.flatMap((page) => page.runs) ?? emptyRuns,
liveHistoryRowIdentity,
"agents"
);
const error =
collectionQueries.configuration?.error ??
collectionQueries.statuses?.error ??
historyLiveHead.error ??
Comment thread
rajohan marked this conversation as resolved.
history.error;
const pending = configuration.isLoading || statuses.isLoading || history.isPending;
const hasCompleteData =
Expand All @@ -51,27 +65,31 @@ export function AgentsRoute() {
void Promise.allSettled([
collections.definitions.utils.refetch(),
collections.statuses.utils.refetch(),
historyLiveHead.refetch(),
history.refetch(),
]);
};

return (
<div>
<PageHeader
description="Reviewed roles, Dashboard-owned tasks, and separate Gateway session availability; availability is not online status or health. Updates automatically from agent and Gateway events, with 10-second status repair polling."
description="Reviewed roles, Dashboard-owned tasks, and separate Gateway session availability. Availability is not online status or health. Updates automatically from agent and Gateway events, with 10-second status repair polling."
eyebrow="Operations"
title="Agents"
/>
{pending && !hasCompleteData && (
<LoadingState className="mt-10" label="Loading agents…" />
)}
{error !== null && (
<div className="mt-6">
<Alert message={dashboardBrowserFailureMessage(error)} />
<Button className="mt-3" onClick={refresh} variant="secondary">
Try again
</Button>
</div>
<Alert
action={
<Button onClick={refresh} size="sm" variant="secondary">
Try again
</Button>
}
className="mt-6"
message={dashboardBrowserFailureMessage(error)}
/>
)}
{hasCompleteData && (
<div className="mt-8 space-y-10">
Expand Down
29 changes: 26 additions & 3 deletions greenfield/src/browser/agents/agentQueries.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,17 @@
import { infiniteQueryOptions, type QueryClient } from "@tanstack/react-query";
import {
infiniteQueryOptions,
queryOptions,
type QueryClient,
} from "@tanstack/react-query";

import type {
ListAgentTaskHistoryInput,
ListAgentTaskHistoryResult,
} from "../../contracts/agents.ts";
import {
liveHistoryArchiveQueryKey,
liveHistoryHeadQueryKey,
} from "../api/liveHistory.ts";
import type { DashboardTrpcClient } from "../api/trpcClient.ts";

type AgentHistoryCursor = NonNullable<ListAgentTaskHistoryInput["cursor"]>;
Expand All @@ -26,12 +34,27 @@ export function agentHistoryQueryOptions(client: DashboardTrpcClient) {
{ signal }
),
getNextPageParam: (lastPage) => lastPage.nextCursor,
queryKey: [...agentQueryKey, "history"],
queryKey: liveHistoryArchiveQueryKey([...agentQueryKey, "history"]),
staleTime: Number.POSITIVE_INFINITY,
Comment thread
rajohan marked this conversation as resolved.
});
}

/** @returns Live first-page projection for current agent task history. */
export function agentHistoryLiveHeadQueryOptions(client: DashboardTrpcClient) {
return queryOptions({
queryFn: ({ signal }): Promise<ListAgentTaskHistoryResult> =>
client.query("agents.listTaskHistory", { limit: 50 }, { signal }),
queryKey: liveHistoryHeadQueryKey([...agentQueryKey, "history"]),
staleTime: 10_000,
});
}

/** Invalidates every agent projection after one durable status transition. */
export async function refreshAgentQueries(queryClient: QueryClient): Promise<void> {
await queryClient.invalidateQueries({ queryKey: agentQueryKey });
await Promise.all([
queryClient.invalidateQueries({ queryKey: agentQueryKey }),
queryClient.invalidateQueries({
queryKey: liveHistoryArchiveQueryKey(agentQueryKey),
}),
]);
}
Loading