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
24 changes: 23 additions & 1 deletion greenfield/docs/architecture/greenfield-rewrite/progress.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ closes a phase; dated entries below provide the evidence, not a second status so
| 0 — Evidence and qualification | Complete | All eight mandatory spikes pass on exact Bun revision `17d6843606d76620cb55d31424d7fb0aed51c367`: build, transport, cross-process SQLite/outbox, Drizzle/Bun SQLite, browser data, chat batching, shutdown, and capped resources. Source-derived parity and the OpenClaw source audit pass as additional evidence. |
| 1 — Foundation | Complete | The self-contained future root builds immutable browser/web/worker artifacts, protects project-local production state, installs exact Bun and systemd artifacts, migrates a database copy, atomically promotes the release/database pair, serves readiness/browser assets, writes project-local logs, and proves crash-safe rollback and shutdown in a disposable lifecycle. |
| 2 — Trust and transport | Complete for the stated server scope | Authentication, MFA, WebAuthn, automation credentials, audit, authenticated renewable SSE, one-shot native Gateway bootstrap verification, and the consolidated [threat model](../../security/greenfield-phase-two-threat-model.md) have executable evidence. Browser UI and production cutover remain later gates. |
| 3 — Core operator domains | Started | Task and agent-directory parity are implemented with durable history, realtime invalidation, and browser workflows. Monitoring ingestion plus report, incident, and notification server parity are implemented. Their browser workflows, schedules/jobs, overview, cache/metrics, and the real worker remain open. |
| 3 — Core operator domains | Started | Task and agent-directory parity are implemented with durable history, realtime invalidation, and browser workflows. Monitoring ingestion plus report, incident, and notification server parity are implemented; report and incident browser readers are also complete. Notification browser state, schedules/jobs, overview, cache/metrics, and the real worker remain open. |
| 4 — Gateway and chat | Not started | The Phase 2 verifier is one-shot only. Persistent native Gateway lifecycle, current-protocol re-audit, sessions, chat journal/recovery, attachments, and frontend remain open. |
| 5 — Privileged and external domains | Not started | Worker-owned file/media, Docker, database, OpenClaw, GitHub, deployment, backup, and other privileged adapters remain open. |
| 6 — Parity, hardening, and cutover | Not started | Full UI parity, generated `/docs`, load/resource/restore evidence, cutover rehearsal, fresh production database, and legacy removal remain open. |
Expand Down Expand Up @@ -868,3 +868,25 @@ full-browser parity, production rehearsal, cutover, and legacy deletion remain o
reader rather than choosing one arbitrary observation report; greenfield stays inactive until
the complete cutover stack lands. Browser workflows, schedules/jobs, overview, cache/metrics,
and real worker execution remain open Phase 3 gates.

### 2026-08-07 — Phase 3 report and incident browser readers

- `/reports` now lists only bounded report summaries and loads the potentially large Markdown body
through an exact query after explicit selection or a validated UUIDv7 deep link. Status, free-form
kind, and source filters apply atomically; overlapping keyset pages are identity-deduplicated;
transient refresh failures preserve usable cached rows; and raw HTML remains disabled in the
existing shared Markdown renderer.
- Report deletion has an explicit confirmation boundary, removes the durable success from every
cached filtered page before refetch, and presents fixed `NOT_FOUND` and bounded
`PRECONDITION_FAILED` outcomes without exposing server text. Exact detail loading remains
independent of list availability, so a valid deep link can still render during a catalog-list
failure.
- Net-new `/incidents` is intentionally absent from the main navigation but linked from Reports.
It provides kind, monitor, lifecycle-state, and severity filters; a selectable TanStack Table
with the shared virtualizer; and exact detail deep links outside the currently loaded page.
- `monitoring.reports` and `monitoring.incidents` use the shared coalescing invalidation hook,
terminal-resync handling, and 30-second fallback refresh. Both routes retain the authenticated
boundary, validated lazy tRPC contract loading, cancellation signals, and separate query roots.
- Frontend parity now marks legacy `/reports` implemented. `/incidents` has no legacy route and is
tracked as a net-new reader. The notification center, schedules/jobs, overview, cache/metrics,
and the real worker remain open Phase 3 gates.
24 changes: 24 additions & 0 deletions greenfield/src/browser/api/trpcClient.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,30 @@ describe("Dashboard browser tRPC client", () => {
expect(calls).toEqual([{ input: {}, kind: "mutation", path: "auth.logout" }]);
});

test("loads report and incident reader contracts on demand", async () => {
const reportCalls: TransportCall[] = [];
const incidentCalls: TransportCall[] = [];
const reportClient = createDashboardTrpcClient(
createRecordingTransport({ reports: [] }, reportCalls)
);
const incidentClient = createDashboardTrpcClient(
createRecordingTransport({ incidents: [] }, incidentCalls)
);

expect(await reportClient.query("reports.list", { limit: 50 })).toEqual({
reports: [],
});
expect(await incidentClient.query("incidents.list", { limit: 50 })).toEqual({
incidents: [],
});
expect(reportCalls).toEqual([
{ input: { limit: 50 }, kind: "query", path: "reports.list" },
]);
expect(incidentCalls).toEqual([
{ input: { limit: 50 }, kind: "query", path: "incidents.list" },
]);
});

test("rejects invalid input before transport access", async () => {
const calls: TransportCall[] = [];
const client = createDashboardTrpcClient(
Expand Down
8 changes: 8 additions & 0 deletions greenfield/src/browser/api/trpcClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,10 @@ async function procedureContractsFor(
const module = await import("../../contracts/agents.ts");
return module.agentProcedureContracts;
}
case "incidents": {
const module = await import("../../contracts/incidents.ts");
return module.incidentProcedureContracts;
}
case "accountSecurity": {
const module = await import("../../contracts/accountSecurity.ts");
return module.accountSecurityProcedureContracts;
Expand All @@ -80,6 +84,10 @@ async function procedureContractsFor(
const module = await import("../../contracts/auth.ts");
return module.authProcedureContracts;
}
case "reports": {
const module = await import("../../contracts/reports.ts");
return module.reportProcedureContracts;
}
case "automationSecurity": {
const module = await import("../../contracts/automationSecurity.ts");
return module.automationSecurityProcedureContracts;
Expand Down
26 changes: 23 additions & 3 deletions greenfield/src/browser/layout/DashboardShell.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,21 @@
import { Dialog, DialogBackdrop, DialogPanel, DialogTitle } from "@headlessui/react";
import { Outlet, useLocation } from "@tanstack/react-router";
import { Bot, Home, ListTodo, Menu, ShieldCheck, X, type LucideIcon } from "lucide-react";
import {
Bot,
Home,
ListTodo,
Menu,
Newspaper,
ShieldCheck,
X,
type LucideIcon,
} from "lucide-react";
import { useState } from "react";

import type { DashboardNavigationPath } from "../lib/dashboardRoutes.ts";
import type {
DashboardAuthenticatedPath,
DashboardNavigationPath,
} from "../lib/dashboardRoutes.ts";
import { Icon } from "../ui/Icon.tsx";
import { IconOnlyButton } from "../ui/IconOnlyButton.tsx";
import { NavigationLink } from "../ui/NavigationLink.tsx";
Expand All @@ -18,8 +30,16 @@ const navigationItems: readonly NavigationItem[] = Object.freeze([
{ icon: Home, label: "Dashboard", to: "/" },
{ icon: Bot, label: "Agents", to: "/agents" },
{ icon: ListTodo, label: "Tasks", to: "/tasks" },
{ icon: Newspaper, label: "Reports", to: "/reports" },
{ icon: ShieldCheck, label: "Account security", to: "/account-security" },
]);
const routeTitles: readonly Pick<NavigationItem, "label" | "to">[] = Object.freeze([
...navigationItems,
]);
const authenticatedRouteTitles: readonly {
readonly label: string;
readonly to: DashboardAuthenticatedPath;
}[] = Object.freeze([...routeTitles, { label: "Incidents", to: "/incidents" }]);

interface NavigationProps {
readonly currentPath: string;
Expand Down Expand Up @@ -99,7 +119,7 @@ export function DashboardShell() {
}

const currentTitle =
navigationItems.find((item) => item.to === location.pathname)?.label ??
authenticatedRouteTitles.find((item) => item.to === location.pathname)?.label ??
"Mira Dashboard";
return (
<div className="bg-primary-900 text-primary-50 flex h-full overflow-hidden">
Expand Down
9 changes: 7 additions & 2 deletions greenfield/src/browser/lib/dashboardRoutes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,16 @@ export const dashboardRoutePaths = Object.freeze([
"/",
"/account-security",
"/agents",
"/incidents",
"/login",
"/reports",
"/tasks",
] as const);

export type DashboardRoutePath = (typeof dashboardRoutePaths)[number];

/** Routes shown inside the authenticated application navigation. */
export type DashboardNavigationPath = Exclude<DashboardRoutePath, "/login">;
/** Routes rendered only inside an authenticated application shell. */
export type DashboardAuthenticatedPath = Exclude<DashboardRoutePath, "/login">;

/** Authenticated routes shown in the main application navigation. */
export type DashboardNavigationPath = Exclude<DashboardAuthenticatedPath, "/incidents">;
Loading