fix(web): harden shell routes and add regression coverage

This commit is contained in:
saravanakumardb1 2026-03-10 10:34:31 -07:00
parent b1dee94173
commit 3ddfa25acb
8 changed files with 292 additions and 51 deletions

View File

@ -1,5 +1,5 @@
import { AppShell } from "@/components/AppShell";
import { mockNotes, mockWorkspaces } from "@/lib/mock-data";
import { mockNotes, mockOperatorWorkflows, mockSavedViews, mockWorkspaces } from "@/lib/mock-data";
export default function DashboardPage() {
const recentNotes = mockNotes.slice(0, 3);
@ -8,7 +8,7 @@ export default function DashboardPage() {
<AppShell
title="Dashboard"
description="Operational entry point for recent notes, active workspaces, and agent-relevant follow-ups."
actions={<div className="badge">Scaffold milestone</div>}
actions={<div className="badge">Operational shell</div>}
>
<section style={{ display: "grid", gridTemplateColumns: "repeat(auto-fit, minmax(240px, 1fr))", gap: "var(--ml-space-4)" }}>
<div className="surface-card" style={{ padding: "var(--ml-space-5)" }}>
@ -25,6 +25,46 @@ export default function DashboardPage() {
</div>
</section>
<section style={{ display: "grid", gridTemplateColumns: "minmax(0, 1.1fr) minmax(320px, 0.9fr)", gap: "var(--ml-space-4)" }}>
<section className="surface-card" style={{ padding: "var(--ml-space-6)", display: "grid", gap: "var(--ml-space-4)" }}>
<div style={{ fontSize: "var(--ml-fs-xl)", fontWeight: 700 }}>Saved views</div>
<div style={{ display: "grid", gap: "var(--ml-space-3)" }}>
{mockSavedViews.map((view) => (
<article key={view.id} className="surface-muted" style={{ padding: "var(--ml-space-4)", display: "grid", gap: "var(--ml-space-2)" }}>
<div style={{ display: "flex", justifyContent: "space-between", gap: "var(--ml-space-3)", flexWrap: "wrap" }}>
<strong>{view.name}</strong>
<span className="badge">{view.scope}</span>
</div>
<div style={{ color: "var(--ml-text-secondary)" }}>{view.description}</div>
<div style={{ display: "flex", justifyContent: "space-between", gap: "var(--ml-space-3)", flexWrap: "wrap" }}>
<span style={{ color: "var(--ml-text-secondary)" }}>{view.query}</span>
<span style={{ color: "var(--ml-text-secondary)" }}>{view.resultCount} results</span>
</div>
</article>
))}
</div>
</section>
<section className="surface-card" style={{ padding: "var(--ml-space-6)", display: "grid", gap: "var(--ml-space-4)" }}>
<div style={{ fontSize: "var(--ml-fs-xl)", fontWeight: 700 }}>Operator workflows</div>
<div style={{ display: "grid", gap: "var(--ml-space-3)" }}>
{mockOperatorWorkflows.map((workflow) => (
<article key={workflow.id} className="surface-muted" style={{ padding: "var(--ml-space-4)", display: "grid", gap: "var(--ml-space-2)" }}>
<div style={{ display: "flex", justifyContent: "space-between", gap: "var(--ml-space-3)", flexWrap: "wrap" }}>
<strong>{workflow.name}</strong>
<span className="badge">{workflow.status}</span>
</div>
<div style={{ color: "var(--ml-text-secondary)" }}>Owner: {workflow.owner}</div>
<div style={{ display: "flex", justifyContent: "space-between", gap: "var(--ml-space-3)", flexWrap: "wrap" }}>
<span style={{ color: "var(--ml-text-secondary)" }}>Queue: {workflow.queueCount}</span>
<span style={{ color: "var(--ml-text-secondary)" }}>SLA: {workflow.sla}</span>
</div>
</article>
))}
</div>
</section>
</section>
<section className="surface-card" style={{ padding: "var(--ml-space-6)", display: "grid", gap: "var(--ml-space-4)" }}>
<div style={{ fontSize: "var(--ml-fs-xl)", fontWeight: 700 }}>Recent note activity</div>
<div style={{ display: "grid", gap: "var(--ml-space-3)" }}>

View File

@ -1,6 +1,7 @@
import { AppShell } from "@/components/AppShell";
import { AgentTimeline } from "@/components/AgentTimeline";
import { ProposalReviewCard } from "@/components/ProposalReviewCard";
import { mockOperatorWorkflows } from "@/lib/mock-data";
import { mockAgentTimeline, mockApprovalQueue } from "@/lib/review-data";
export default function ReviewsPage() {
@ -8,24 +9,50 @@ export default function ReviewsPage() {
<AppShell
title="Agent review"
description="Approval queue, proposal comparison, and audit-oriented review surfaces for agent-mediated edits."
actions={<div className="badge">W3 in progress</div>}
actions={<div className="badge">Operator workflow shell</div>}
>
<section className="surface-card" style={{ padding: "var(--ml-space-5)", display: "grid", gap: "var(--ml-space-4)" }}>
<div style={{ fontWeight: 700 }}>Approval queue</div>
<div style={{ display: "grid", gap: "var(--ml-space-3)" }}>
{mockApprovalQueue.map((item) => (
<div key={item.id} className="surface-muted" style={{ padding: "var(--ml-space-4)", display: "flex", justifyContent: "space-between", gap: "var(--ml-space-3)", alignItems: "center", flexWrap: "wrap" }}>
<div style={{ display: "grid", gap: 4 }}>
<strong>{item.title}</strong>
<span style={{ color: "var(--ml-text-secondary)" }}>{item.owner}</span>
</div>
<div style={{ display: "flex", gap: "var(--ml-space-2)", flexWrap: "wrap" }}>
<span className="badge">{item.severity}</span>
<span className="badge">{item.status}</span>
<section style={{ display: "grid", gridTemplateColumns: "minmax(260px, 320px) minmax(0, 1fr)", gap: "var(--ml-space-4)" }}>
<aside className="surface-card" style={{ padding: "var(--ml-space-5)", display: "grid", gap: "var(--ml-space-4)" }}>
<div style={{ fontWeight: 700 }}>Operator workflows</div>
<div style={{ display: "grid", gap: "var(--ml-space-3)" }}>
{mockOperatorWorkflows.map((workflow) => (
<div key={workflow.id} className="surface-muted" style={{ padding: "var(--ml-space-4)", display: "grid", gap: "var(--ml-space-2)" }}>
<strong>{workflow.name}</strong>
<span style={{ color: "var(--ml-text-secondary)" }}>Owner: {workflow.owner}</span>
<div style={{ display: "flex", gap: "var(--ml-space-2)", flexWrap: "wrap" }}>
<span className="badge">{workflow.status}</span>
<span className="badge">Queue: {workflow.queueCount}</span>
<span className="badge">SLA {workflow.sla}</span>
</div>
</div>
))}
</div>
</aside>
<section className="surface-card" style={{ padding: "var(--ml-space-5)", display: "grid", gap: "var(--ml-space-4)" }}>
<div style={{ display: "flex", justifyContent: "space-between", gap: "var(--ml-space-3)", flexWrap: "wrap" }}>
<div style={{ fontWeight: 700 }}>Approval queue</div>
<div style={{ display: "flex", gap: "var(--ml-space-2)", flexWrap: "wrap" }}>
<span className="badge">severity:medium+</span>
<span className="badge">status:pending</span>
<span className="badge">owner:any</span>
</div>
))}
</div>
</div>
<div style={{ display: "grid", gap: "var(--ml-space-3)" }}>
{mockApprovalQueue.map((item) => (
<div key={item.id} className="surface-muted" style={{ padding: "var(--ml-space-4)", display: "flex", justifyContent: "space-between", gap: "var(--ml-space-3)", alignItems: "center", flexWrap: "wrap" }}>
<div style={{ display: "grid", gap: 4 }}>
<strong>{item.title}</strong>
<span style={{ color: "var(--ml-text-secondary)" }}>{item.owner}</span>
</div>
<div style={{ display: "flex", gap: "var(--ml-space-2)", flexWrap: "wrap" }}>
<span className="badge">{item.severity}</span>
<span className="badge">{item.status}</span>
</div>
</div>
))}
</div>
</section>
</section>
<ProposalReviewCard

View File

@ -0,0 +1,26 @@
import { render, screen } from "@testing-library/react";
import { describe, expect, it, vi } from "vitest";
import SearchPage from "./page";
vi.mock("next/link", () => ({
default: ({ href, children, ...props }: React.ComponentProps<"a"> & { href: string }) => (
<a href={href} {...props}>
{children}
</a>
),
}));
describe("SearchPage", () => {
it("renders an accessible search field, saved searches, and note links", () => {
render(<SearchPage />);
expect(screen.getByRole("heading", { level: 1, name: "Search" })).toBeInTheDocument();
expect(screen.getByRole("textbox", { name: "Search notes" })).toBeInTheDocument();
expect(screen.getByText("Saved searches")).toBeInTheDocument();
expect(screen.getByText("Launch readiness")).toBeInTheDocument();
expect(screen.getByRole("link", { name: /MVP cut line for agentic notes launch/i })).toHaveAttribute(
"href",
"/notes/note-prd-cutline"
);
});
});

View File

@ -1,33 +1,71 @@
import Link from "next/link";
import { AppShell } from "@/components/AppShell";
import { mockNotes } from "@/lib/mock-data";
import { mockNotes, mockSavedViews } from "@/lib/mock-data";
export default function SearchPage() {
return (
<AppShell
title="Search"
description="Lexical search, tag filtering, and retrieval entry points. Semantic ranking and explainability remain follow-up work."
actions={<div className="badge">Advanced retrieval next</div>}
actions={<div className="badge">Dense retrieval shell</div>}
>
<section className="surface-card" style={{ padding: "var(--ml-space-6)", display: "grid", gap: "var(--ml-space-4)" }}>
<input className="input-shell" placeholder="Search notes, tags, tasks, and linked context" />
<div style={{ display: "flex", gap: "var(--ml-space-2)", flexWrap: "wrap" }}>
<span className="badge">workspace:all</span>
<span className="badge">status:active</span>
<span className="badge">source:manual+agent</span>
</div>
<div style={{ display: "grid", gap: "var(--ml-space-3)" }}>
{mockNotes.map((note) => (
<article key={note.id} className="surface-muted" style={{ padding: "var(--ml-space-4)", display: "grid", gap: "var(--ml-space-2)" }}>
<strong>{note.title}</strong>
<span style={{ color: "var(--ml-text-secondary)" }}>{note.excerpt}</span>
<div style={{ display: "flex", gap: "var(--ml-space-2)", flexWrap: "wrap" }}>
{note.tags.map((tag) => (
<span key={tag} className="badge">#{tag}</span>
))}
</div>
</article>
))}
</div>
<section style={{ display: "grid", gridTemplateColumns: "minmax(260px, 320px) minmax(0, 1fr)", gap: "var(--ml-space-4)" }}>
<aside className="surface-card" style={{ padding: "var(--ml-space-5)", display: "grid", gap: "var(--ml-space-4)" }}>
<div style={{ fontWeight: 700 }}>Saved searches</div>
<div style={{ display: "grid", gap: "var(--ml-space-3)" }}>
{mockSavedViews
.filter((view) => view.scope === "search")
.map((view) => (
<div key={view.id} className="surface-muted" style={{ padding: "var(--ml-space-4)", display: "grid", gap: "var(--ml-space-2)" }}>
<strong>{view.name}</strong>
<span style={{ color: "var(--ml-text-secondary)" }}>{view.query}</span>
<span className="badge">{view.resultCount} results</span>
</div>
))}
</div>
<div style={{ display: "grid", gap: "var(--ml-space-2)" }}>
<strong>Retrieval filters</strong>
<span style={{ color: "var(--ml-text-secondary)" }}>workspace:any</span>
<span style={{ color: "var(--ml-text-secondary)" }}>status:active + draft</span>
<span style={{ color: "var(--ml-text-secondary)" }}>relationship scope: linked + cited</span>
<span style={{ color: "var(--ml-text-secondary)" }}>explainability: matched fields</span>
</div>
</aside>
<section className="surface-card" style={{ padding: "var(--ml-space-6)", display: "grid", gap: "var(--ml-space-4)" }}>
<input
aria-label="Search notes"
className="input-shell"
placeholder="Search notes, tags, tasks, and linked context"
/>
<div style={{ display: "flex", gap: "var(--ml-space-2)", flexWrap: "wrap" }}>
<span className="badge">workspace:all</span>
<span className="badge">status:active</span>
<span className="badge">source:manual+agent</span>
<span className="badge">matched:title+tags</span>
</div>
<div style={{ display: "grid", gap: "var(--ml-space-3)" }}>
{mockNotes.map((note) => (
<Link key={note.id} href={`/notes/${note.id}`} className="surface-muted" style={{ padding: "var(--ml-space-4)", display: "grid", gap: "var(--ml-space-2)" }}>
<div style={{ display: "grid", gridTemplateColumns: "minmax(0, 1.5fr) repeat(3, minmax(100px, auto))", gap: "var(--ml-space-3)", alignItems: "start" }}>
<div style={{ display: "grid", gap: "var(--ml-space-2)" }}>
<strong>{note.title}</strong>
<span style={{ color: "var(--ml-text-secondary)" }}>{note.excerpt}</span>
</div>
<span style={{ color: "var(--ml-text-secondary)" }}>{note.status}</span>
<span style={{ color: "var(--ml-text-secondary)" }}>{note.updatedBy}</span>
<span style={{ color: "var(--ml-text-secondary)" }}>{note.workspaceId.replace("workspace-", "")}</span>
</div>
<div style={{ display: "flex", gap: "var(--ml-space-2)", flexWrap: "wrap" }}>
{note.tags.map((tag) => (
<span key={tag} className="badge">#{tag}</span>
))}
</div>
</Link>
))}
</div>
</section>
</section>
</AppShell>
);

View File

@ -0,0 +1,26 @@
import { render, screen } from "@testing-library/react";
import { describe, expect, it, vi } from "vitest";
import WorkspacesPage from "./page";
vi.mock("next/link", () => ({
default: ({ href, children, ...props }: React.ComponentProps<"a"> & { href: string }) => (
<a href={href} {...props}>
{children}
</a>
),
}));
describe("WorkspacesPage", () => {
it("renders an accessible workspace filter and saved workspace views", () => {
render(<WorkspacesPage />);
expect(screen.getByRole("heading", { level: 1, name: "Workspaces" })).toBeInTheDocument();
expect(screen.getByRole("textbox", { name: "Filter workspaces" })).toBeInTheDocument();
expect(screen.getByText("Saved views")).toBeInTheDocument();
expect(screen.getByText("All workspaces")).toBeInTheDocument();
expect(screen.getByRole("link", { name: /MVP cut line for agentic notes launch/i })).toHaveAttribute(
"href",
"/notes/note-prd-cutline"
);
});
});

View File

@ -1,6 +1,6 @@
import Link from "next/link";
import { AppShell } from "@/components/AppShell";
import { getNotesForWorkspace, mockWorkspaces } from "@/lib/mock-data";
import { getNotesForWorkspace, mockSavedViews, mockWorkspaces } from "@/lib/mock-data";
export default function WorkspacesPage() {
return (
@ -9,15 +9,36 @@ export default function WorkspacesPage() {
description="Workspace-level organization, filters, and saved-view entry points for note collections."
actions={<div className="badge">Saved views scaffolded</div>}
>
<section className="surface-card" style={{ padding: "var(--ml-space-5)", display: "grid", gap: "var(--ml-space-4)" }}>
<div style={{ display: "grid", gap: "var(--ml-space-3)" }}>
<input className="input-shell" placeholder="Filter workspaces by owner, tag, or visibility" />
<div style={{ display: "flex", gap: "var(--ml-space-2)", flexWrap: "wrap" }}>
<span className="badge">Saved view: All workspaces</span>
<span className="badge">Saved view: Product strategy</span>
<span className="badge">Saved view: Agent review</span>
<section style={{ display: "grid", gridTemplateColumns: "minmax(260px, 320px) minmax(0, 1fr)", gap: "var(--ml-space-4)" }}>
<aside className="surface-card" style={{ padding: "var(--ml-space-5)", display: "grid", gap: "var(--ml-space-4)" }}>
<div style={{ fontWeight: 700 }}>Saved views</div>
<div style={{ display: "grid", gap: "var(--ml-space-3)" }}>
{mockSavedViews
.filter((view) => view.scope === "workspace")
.map((view) => (
<div key={view.id} className="surface-muted" style={{ padding: "var(--ml-space-4)", display: "grid", gap: "var(--ml-space-2)" }}>
<strong>{view.name}</strong>
<span style={{ color: "var(--ml-text-secondary)" }}>{view.description}</span>
<span className="badge">{view.resultCount} results</span>
</div>
))}
</div>
</div>
</aside>
<section className="surface-card" style={{ padding: "var(--ml-space-5)", display: "grid", gap: "var(--ml-space-4)" }}>
<div style={{ display: "grid", gap: "var(--ml-space-3)" }}>
<input
aria-label="Filter workspaces"
className="input-shell"
placeholder="Filter workspaces by owner, tag, or visibility"
/>
<div style={{ display: "flex", gap: "var(--ml-space-2)", flexWrap: "wrap" }}>
<span className="badge">owner:any</span>
<span className="badge">visibility:any</span>
<span className="badge">sort:updated</span>
</div>
</div>
</section>
</section>
<section style={{ display: "grid", gap: "var(--ml-space-4)" }}>
@ -30,7 +51,10 @@ export default function WorkspacesPage() {
<div style={{ fontSize: "var(--ml-fs-xl)", fontWeight: 700 }}>{workspace.name}</div>
<div style={{ color: "var(--ml-text-secondary)", marginTop: 6 }}>{workspace.description}</div>
</div>
<div className="badge">{workspace.visibility}</div>
<div style={{ display: "grid", gap: 8, justifyItems: "end" }}>
<div className="badge">{workspace.visibility}</div>
<span style={{ color: "var(--ml-text-secondary)" }}>Owner: {workspace.owner}</span>
</div>
</div>
<div style={{ display: "flex", gap: "var(--ml-space-2)", flexWrap: "wrap" }}>
{workspace.tags.map((tag) => (

View File

@ -15,7 +15,7 @@ const navItems = [
];
export function Sidebar() {
const pathname = usePathname();
const pathname = usePathname() ?? "";
return (
<aside className="sidebar" style={{ padding: "var(--ml-space-6)" }} aria-label="Primary">

View File

@ -1,4 +1,10 @@
import type { NoteDetail, NoteSummary, WorkspaceSummary } from "@/lib/types";
import type {
NoteDetail,
NoteSummary,
OperatorWorkflowSummary,
SavedViewSummary,
WorkspaceSummary,
} from "@/lib/types";
export const mockWorkspaces: WorkspaceSummary[] = [
{
@ -76,6 +82,60 @@ export const mockNotes: NoteSummary[] = [
},
];
export const mockSavedViews: SavedViewSummary[] = [
{
id: "view-all-workspaces",
name: "All workspaces",
scope: "workspace",
description: "Default operational workspace listing across all note domains.",
query: "visibility:any sort:updated",
resultCount: 3,
},
{
id: "view-launch-readiness",
name: "Launch readiness",
scope: "search",
description: "Notes tagged for launch, cut-line, approval, and release follow-up.",
query: "tag:launch tag:mvp status:active",
resultCount: 2,
},
{
id: "view-agent-review",
name: "Agent review queue",
scope: "review",
description: "Proposals and approval items requiring operator attention.",
query: "review:proposed severity:medium+",
resultCount: 2,
},
];
export const mockOperatorWorkflows: OperatorWorkflowSummary[] = [
{
id: "workflow-approvals",
name: "Approval triage",
queueCount: 2,
owner: "Operator",
sla: "< 4h",
status: "healthy",
},
{
id: "workflow-artifacts",
name: "Artifact follow-up",
queueCount: 3,
owner: "Knowledge Ops",
sla: "< 1d",
status: "at_risk",
},
{
id: "workflow-search-gaps",
name: "Search quality review",
queueCount: 1,
owner: "Web Agent",
sla: "< 2d",
status: "healthy",
},
];
export const mockNoteDetails: Record<string, NoteDetail> = {
"note-prd-cutline": {
...mockNotes[0],