feat(web): Phase 3 — Dashboard Page (#450) (#453)
Some checks failed
ci/woodpecker/push/web Pipeline failed

Co-authored-by: Jason Woltje <jason@diversecanvas.com>
Co-committed-by: Jason Woltje <jason@diversecanvas.com>
This commit was merged in pull request #453.
This commit is contained in:
2026-02-22 21:18:50 +00:00
committed by jason.woltje
parent 716f230f72
commit b43e860c40
7 changed files with 677 additions and 74 deletions

View File

@@ -0,0 +1,169 @@
import type { ReactElement } from "react";
import { Card, SectionHeader, Badge } from "@mosaic/ui";
type BadgeVariantType =
| "badge-amber"
| "badge-red"
| "badge-teal"
| "badge-blue"
| "badge-muted"
| "badge-purple"
| "badge-pulse";
interface ActivityItem {
id: string;
icon: string;
iconBg: string;
title: string;
highlight: string;
rest: string;
timestamp: string;
badge?: {
text: string;
variant: BadgeVariantType;
};
}
const activityItems: ActivityItem[] = [
{
id: "act-1",
icon: "✓",
iconBg: "rgba(20,184,166,0.15)",
title: "",
highlight: "planner-agent",
rest: " completed task analysis for infra-refactor",
timestamp: "2m ago",
},
{
id: "act-2",
icon: "⚠",
iconBg: "rgba(245,158,11,0.15)",
title: "",
highlight: "executor-agent",
rest: " hit rate limit on Terraform API",
timestamp: "5m ago",
badge: { text: "warn", variant: "badge-amber" },
},
{
id: "act-3",
icon: "↑",
iconBg: "rgba(47,128,255,0.15)",
title: "",
highlight: "ORCH-002",
rest: " session started for api-v3-migration",
timestamp: "12m ago",
},
{
id: "act-4",
icon: "✗",
iconBg: "rgba(229,72,77,0.15)",
title: "",
highlight: "migrator-agent",
rest: " failed to connect to staging database",
timestamp: "18m ago",
badge: { text: "error", variant: "badge-red" },
},
{
id: "act-5",
icon: "✓",
iconBg: "rgba(20,184,166,0.15)",
title: "",
highlight: "reviewer-agent",
rest: " approved PR #214 in infra-refactor",
timestamp: "34m ago",
},
{
id: "act-6",
icon: "⟳",
iconBg: "rgba(139,92,246,0.15)",
title: "Token budget reset for ",
highlight: "gpt-4o",
rest: " model",
timestamp: "1h ago",
},
{
id: "act-7",
icon: "★",
iconBg: "rgba(20,184,166,0.15)",
title: "Project ",
highlight: "data-pipeline",
rest: " marked as completed",
timestamp: "2h ago",
},
];
interface ActivityItemRowProps {
item: ActivityItem;
}
function ActivityItemRow({ item }: ActivityItemRowProps): ReactElement {
return (
<div
style={{
display: "flex",
alignItems: "flex-start",
gap: 10,
padding: "8px 0",
borderBottom: "1px solid var(--border)",
}}
>
<div
style={{
width: 28,
height: 28,
borderRadius: 6,
flexShrink: 0,
background: item.iconBg,
display: "flex",
alignItems: "center",
justifyContent: "center",
fontSize: "0.8rem",
color: "var(--text)",
}}
>
{item.icon}
</div>
<div style={{ flex: 1, minWidth: 0 }}>
<div
style={{
fontSize: "0.8rem",
color: "var(--text-2)",
lineHeight: 1.4,
}}
>
{item.title}
<strong style={{ color: "var(--text)" }}>{item.highlight}</strong>
{item.rest}
{item.badge !== undefined && (
<Badge variant={item.badge.variant} style={{ marginLeft: 6 }}>
{item.badge.text}
</Badge>
)}
</div>
<div
style={{
fontSize: "0.7rem",
fontFamily: "var(--mono)",
color: "var(--muted)",
marginTop: 2,
}}
>
{item.timestamp}
</div>
</div>
</div>
);
}
export function ActivityFeed(): ReactElement {
return (
<Card>
<SectionHeader title="Activity Feed" subtitle="Recent agent events" />
<div>
{activityItems.map((item) => (
<ActivityItemRow key={item.id} item={item} />
))}
</div>
</Card>
);
}

View File

@@ -0,0 +1,45 @@
import type { ReactElement } from "react";
import { MetricsStrip, type MetricCell } from "@mosaic/ui";
const cells: MetricCell[] = [
{
label: "Active Agents",
value: "47",
color: "var(--ms-blue-400)",
trend: { direction: "up", text: "↑ +3 from yesterday" },
},
{
label: "Tasks Completed",
value: "1,284",
color: "var(--ms-teal-400)",
trend: { direction: "up", text: "↑ +128 today" },
},
{
label: "Avg Response Time",
value: "2.4s",
color: "var(--ms-purple-400)",
trend: { direction: "down", text: "↓ -0.3s improved" },
},
{
label: "Token Usage",
value: "3.2M",
color: "var(--ms-amber-400)",
trend: { direction: "neutral", text: "78% of budget" },
},
{
label: "Error Rate",
value: "0.4%",
color: "var(--ms-red-400)",
trend: { direction: "down", text: "↓ -0.1% improved" },
},
{
label: "Active Projects",
value: "8",
color: "var(--ms-cyan-500)",
trend: { direction: "neutral", text: "2 deploying" },
},
];
export function DashboardMetrics(): ReactElement {
return <MetricsStrip cells={cells} />;
}

View File

@@ -0,0 +1,241 @@
"use client";
import { useState } from "react";
import type { ReactElement } from "react";
import { Card, SectionHeader, Badge, Dot } from "@mosaic/ui";
interface AgentNode {
id: string;
initials: string;
avatarColor: string;
name: string;
task: string;
status: "teal" | "blue" | "amber" | "red" | "muted";
}
interface OrchestratorSession {
id: string;
orchId: string;
name: string;
badge: string;
badgeVariant:
| "badge-teal"
| "badge-amber"
| "badge-red"
| "badge-blue"
| "badge-muted"
| "badge-purple"
| "badge-pulse";
duration: string;
agents: AgentNode[];
}
const sessions: OrchestratorSession[] = [
{
id: "s1",
orchId: "ORCH-001",
name: "infra-refactor",
badge: "running",
badgeVariant: "badge-teal",
duration: "2h 14m",
agents: [
{
id: "a1",
initials: "PL",
avatarColor: "rgba(47,128,255,0.15)",
name: "planner-agent",
task: "Analyzing network topology",
status: "blue",
},
{
id: "a2",
initials: "EX",
avatarColor: "rgba(20,184,166,0.15)",
name: "executor-agent",
task: "Applying Terraform modules",
status: "teal",
},
{
id: "a3",
initials: "QA",
avatarColor: "rgba(245,158,11,0.15)",
name: "reviewer-agent",
task: "Waiting for executor output",
status: "amber",
},
],
},
{
id: "s2",
orchId: "ORCH-002",
name: "api-v3-migration",
badge: "running",
badgeVariant: "badge-teal",
duration: "45m",
agents: [
{
id: "a4",
initials: "MG",
avatarColor: "rgba(139,92,246,0.15)",
name: "migrator-agent",
task: "Rewriting endpoint handlers",
status: "blue",
},
],
},
];
interface AgentNodeItemProps {
agent: AgentNode;
}
function AgentNodeItem({ agent }: AgentNodeItemProps): ReactElement {
const [hovered, setHovered] = useState(false);
return (
<div
onMouseEnter={(): void => {
setHovered(true);
}}
onMouseLeave={(): void => {
setHovered(false);
}}
style={{
display: "flex",
alignItems: "center",
gap: 10,
padding: "7px 10px",
borderRadius: "var(--r-sm)",
border: `1px solid ${hovered ? "var(--ms-border-700)" : "var(--border)"}`,
background: hovered ? "var(--surface)" : "var(--bg-mid)",
transition: "border-color 0.15s, background 0.15s",
}}
>
<div
style={{
width: 30,
height: 30,
borderRadius: 6,
flexShrink: 0,
display: "flex",
alignItems: "center",
justifyContent: "center",
fontWeight: 700,
fontFamily: "var(--mono)",
fontSize: "0.7rem",
background: agent.avatarColor,
color: "var(--text)",
}}
>
{agent.initials}
</div>
<div style={{ flex: 1, minWidth: 0 }}>
<div
style={{
fontSize: "0.8rem",
fontWeight: 600,
color: "var(--text)",
whiteSpace: "nowrap",
overflow: "hidden",
textOverflow: "ellipsis",
}}
>
{agent.name}
</div>
<div
style={{
fontSize: "0.72rem",
color: "var(--muted)",
whiteSpace: "nowrap",
overflow: "hidden",
textOverflow: "ellipsis",
}}
>
{agent.task}
</div>
</div>
<Dot variant={agent.status} />
</div>
);
}
interface OrchCardProps {
session: OrchestratorSession;
}
function OrchCard({ session }: OrchCardProps): ReactElement {
return (
<div
style={{
background: "var(--bg-mid)",
border: "1px solid var(--border)",
borderRadius: "var(--r-md)",
padding: "12px 14px",
marginBottom: 10,
}}
>
<div
style={{
display: "flex",
alignItems: "center",
gap: 8,
marginBottom: 10,
}}
>
<Dot variant="teal" />
<span
style={{
fontFamily: "var(--mono)",
fontSize: "0.75rem",
color: "var(--ms-purple-400)",
fontWeight: 700,
}}
>
{session.orchId}
</span>
<span
style={{
fontSize: "0.8rem",
fontWeight: 600,
color: "var(--text)",
}}
>
{session.name}
</span>
<Badge variant={session.badgeVariant}>{session.badge}</Badge>
<span
style={{
marginLeft: "auto",
fontSize: "0.72rem",
fontFamily: "var(--mono)",
color: "var(--muted)",
}}
>
{session.duration}
</span>
</div>
<div style={{ display: "flex", flexDirection: "column", gap: 6 }}>
{session.agents.map((agent) => (
<AgentNodeItem key={agent.id} agent={agent} />
))}
</div>
</div>
);
}
export function OrchestratorSessions(): ReactElement {
return (
<Card>
<SectionHeader
title="Active Orchestrator Sessions"
subtitle="3 of 8 projects running"
actions={<Badge variant="badge-teal">3 active</Badge>}
/>
<div>
{sessions.map((session) => (
<OrchCard key={session.id} session={session} />
))}
</div>
</Card>
);
}

View File

@@ -0,0 +1,96 @@
"use client";
import { useState } from "react";
import type { ReactElement } from "react";
import { Card, SectionHeader } from "@mosaic/ui";
interface QuickAction {
id: string;
label: string;
icon: string;
iconBg: string;
}
const actions: QuickAction[] = [
{ id: "new-project", label: "New Project", icon: "🚀", iconBg: "rgba(47,128,255,0.15)" },
{ id: "spawn-agent", label: "Spawn Agent", icon: "🤖", iconBg: "rgba(139,92,246,0.15)" },
{ id: "view-telemetry", label: "View Telemetry", icon: "📊", iconBg: "rgba(20,184,166,0.15)" },
{ id: "review-tasks", label: "Review Tasks", icon: "📋", iconBg: "rgba(245,158,11,0.15)" },
];
interface ActionButtonProps {
action: QuickAction;
}
function ActionButton({ action }: ActionButtonProps): ReactElement {
const [hovered, setHovered] = useState(false);
return (
<button
type="button"
onMouseEnter={(): void => {
setHovered(true);
}}
onMouseLeave={(): void => {
setHovered(false);
}}
style={{
display: "flex",
flexDirection: "column",
alignItems: "center",
justifyContent: "center",
gap: 8,
padding: "16px 12px",
borderRadius: "var(--r-md)",
border: `1px solid ${hovered ? "var(--ms-border-700)" : "var(--border)"}`,
background: hovered ? "var(--surface)" : "var(--bg-mid)",
cursor: "pointer",
transition: "border-color 0.15s, background 0.15s",
width: "100%",
}}
>
<div
style={{
width: 24,
height: 24,
borderRadius: 6,
background: action.iconBg,
display: "flex",
alignItems: "center",
justifyContent: "center",
fontSize: "0.875rem",
}}
>
{action.icon}
</div>
<span
style={{
fontSize: "0.8rem",
fontWeight: 600,
color: "var(--text)",
}}
>
{action.label}
</span>
</button>
);
}
export function QuickActions(): ReactElement {
return (
<Card>
<SectionHeader title="Quick Actions" />
<div
style={{
display: "grid",
gridTemplateColumns: "1fr 1fr",
gap: 10,
}}
>
{actions.map((action) => (
<ActionButton key={action.id} action={action} />
))}
</div>
</Card>
);
}

View File

@@ -0,0 +1,98 @@
import type { ReactElement } from "react";
import { Card, SectionHeader, ProgressBar, type ProgressBarVariant } from "@mosaic/ui";
interface ModelBudget {
id: string;
label: string;
usage: string;
value: number;
variant: ProgressBarVariant;
}
const models: ModelBudget[] = [
{
id: "sonnet",
label: "claude-3-5-sonnet",
usage: "2.1M / 3M",
value: 70,
variant: "blue",
},
{
id: "haiku",
label: "claude-3-haiku",
usage: "890K / 5M",
value: 18,
variant: "teal",
},
{
id: "gpt4o",
label: "gpt-4o",
usage: "320K / 1M",
value: 32,
variant: "purple",
},
{
id: "llama",
label: "local/llama-3.3",
usage: "unlimited",
value: 55,
variant: "amber",
},
];
interface ModelRowProps {
model: ModelBudget;
}
function ModelRow({ model }: ModelRowProps): ReactElement {
return (
<div style={{ marginBottom: 14 }}>
<div
style={{
display: "flex",
alignItems: "center",
justifyContent: "space-between",
marginBottom: 5,
}}
>
<span
style={{
fontSize: "0.8rem",
fontWeight: 600,
color: "var(--text-2)",
fontFamily: "var(--mono)",
}}
>
{model.label}
</span>
<span
style={{
fontSize: "0.72rem",
color: "var(--muted)",
fontFamily: "var(--mono)",
}}
>
{model.usage}
</span>
</div>
<ProgressBar
value={model.value}
variant={model.variant}
label={`${model.label} token usage`}
/>
</div>
);
}
export function TokenBudget(): ReactElement {
return (
<Card>
<SectionHeader title="Token Budget" subtitle="Usage by model" />
<div>
{models.map((model) => (
<ModelRow key={model.id} model={model} />
))}
</div>
</Card>
);
}