Skip to content

Commit add671a

Browse files
sonpiazclaude
andcommitted
feat: CEO Dashboard Loop UI — LoopSummaryCard, AccountabilityGrid, UnresolvedAlerts (AGT-336)
Co-Authored-By: Claude Opus 4.6 <[email protected]>
1 parent 059632d commit add671a

File tree

4 files changed

+352
-0
lines changed

4 files changed

+352
-0
lines changed

app/ceo/page.tsx

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,9 @@ import { cn } from "@/lib/utils";
2626
import { LiveFeed } from "@/components/ceo/LiveFeed";
2727
import { BlockersCard } from "@/components/ceo/BlockersCard";
2828
import { WinsCard } from "@/components/ceo/WinsCard";
29+
import { LoopSummaryCard } from "@/components/ceo/LoopSummaryCard";
30+
import { AccountabilityGrid } from "@/components/ceo/AccountabilityGrid";
31+
import { UnresolvedAlerts } from "@/components/ceo/UnresolvedAlerts";
2932
import { useState } from "react";
3033

3134
export default function CEODashboardPage() {
@@ -123,12 +126,21 @@ export default function CEODashboardPage() {
123126
</div>
124127
)}
125128

129+
{/* The Loop — Glanceable loop health */}
130+
<LoopSummaryCard />
131+
126132
{/* Blockers - Show first if any (RED) */}
127133
<BlockersCard />
128134

135+
{/* Unresolved Loop Alerts — Active SLA breaches */}
136+
<UnresolvedAlerts />
137+
129138
{/* Wins - Celebrate (GREEN) */}
130139
<WinsCard />
131140

141+
{/* Accountability Grid — Per-agent loop performance */}
142+
<AccountabilityGrid />
143+
132144
{/* Live Feed - What's happening */}
133145
<section>
134146
<h2 className="text-xs font-bold uppercase tracking-wider text-zinc-500 mb-2 flex items-center gap-2">
Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
"use client";
2+
3+
/**
4+
* AccountabilityGrid — AGT-336: Per-agent loop performance
5+
*
6+
* Shows each agent's loop metrics: completion rate, SLA breaches, avg times.
7+
* Uses loopMetrics.getAgentBreakdown query.
8+
*/
9+
10+
import { useQuery } from "convex/react";
11+
import { api } from "@/convex/_generated/api";
12+
import { cn } from "@/lib/utils";
13+
14+
interface AccountabilityGridProps {
15+
className?: string;
16+
}
17+
18+
const AGENT_COLORS: Record<string, string> = {
19+
max: "border-purple-500/30",
20+
sam: "border-blue-500/30",
21+
leo: "border-emerald-500/30",
22+
quinn: "border-amber-500/30",
23+
};
24+
25+
function formatMs(ms: number | null): string {
26+
if (ms === null) return "--";
27+
const minutes = Math.round(ms / 60_000);
28+
if (minutes < 60) return `${minutes}m`;
29+
return `${Math.round(minutes / 60)}h`;
30+
}
31+
32+
function rateColor(rate: number): string {
33+
if (rate >= 80) return "text-emerald-400";
34+
if (rate >= 50) return "text-amber-400";
35+
return "text-red-400";
36+
}
37+
38+
export function AccountabilityGrid({ className }: AccountabilityGridProps) {
39+
const agents = useQuery(api.loopMetrics.getAgentBreakdown, { sinceDays: 7 });
40+
41+
if (!agents) {
42+
return (
43+
<div className={className}>
44+
<div className="space-y-2">
45+
{[...Array(3)].map((_, i) => (
46+
<div key={i} className="h-20 bg-zinc-800 animate-pulse rounded-lg" />
47+
))}
48+
</div>
49+
</div>
50+
);
51+
}
52+
53+
if (agents.length === 0) {
54+
return (
55+
<div className={className}>
56+
<h2 className="text-xs font-bold uppercase tracking-wider text-zinc-500 mb-2">
57+
Accountability
58+
</h2>
59+
<div className="text-center py-4 text-zinc-600 text-sm">
60+
No loop data yet
61+
</div>
62+
</div>
63+
);
64+
}
65+
66+
return (
67+
<div className={className}>
68+
<h2 className="text-xs font-bold uppercase tracking-wider text-zinc-500 mb-2">
69+
Accountability
70+
</h2>
71+
<div className="space-y-2">
72+
{agents.map((agent) => (
73+
<div
74+
key={agent.agentName}
75+
className={cn(
76+
"bg-zinc-900 border rounded-lg p-3",
77+
AGENT_COLORS[agent.agentName] ?? "border-zinc-700/30"
78+
)}
79+
>
80+
{/* Row 1: Name + completion rate */}
81+
<div className="flex items-center justify-between mb-2">
82+
<span className="font-semibold text-white text-sm uppercase">
83+
{agent.agentName}
84+
</span>
85+
<span className={cn("font-bold text-lg tabular-nums", rateColor(agent.completionRate))}>
86+
{agent.completionRate}%
87+
</span>
88+
</div>
89+
90+
{/* Row 2: Stats */}
91+
<div className="flex items-center gap-3 text-xs text-zinc-400">
92+
<span>
93+
<span className="text-zinc-300 font-medium">{agent.closed}</span>/{agent.total} closed
94+
</span>
95+
{agent.broken > 0 && (
96+
<span className="text-red-400">
97+
{agent.broken} broken
98+
</span>
99+
)}
100+
{agent.slaBreaches > 0 && (
101+
<span className="text-amber-400">
102+
{agent.slaBreaches} SLA
103+
</span>
104+
)}
105+
<span className="ml-auto text-zinc-500">
106+
reply {formatMs(agent.avgReplyTimeMs)} / act {formatMs(agent.avgActionTimeMs)}
107+
</span>
108+
</div>
109+
110+
{/* Progress bar */}
111+
<div className="mt-2 h-1 bg-zinc-800 rounded-full overflow-hidden">
112+
<div
113+
className={cn(
114+
"h-full rounded-full transition-all duration-500",
115+
agent.completionRate >= 80 && "bg-emerald-500",
116+
agent.completionRate >= 50 && agent.completionRate < 80 && "bg-amber-500",
117+
agent.completionRate < 50 && "bg-red-500"
118+
)}
119+
style={{ width: `${agent.completionRate}%` }}
120+
/>
121+
</div>
122+
</div>
123+
))}
124+
</div>
125+
</div>
126+
);
127+
}

components/ceo/LoopSummaryCard.tsx

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
"use client";
2+
3+
/**
4+
* LoopSummaryCard — AGT-336: CEO Dashboard header
5+
*
6+
* Glanceable loop health: active loops, completed today, broken, avg time.
7+
* Uses loopMetrics.getDailySummary query.
8+
*/
9+
10+
import { useQuery } from "convex/react";
11+
import { api } from "@/convex/_generated/api";
12+
13+
interface LoopSummaryCardProps {
14+
className?: string;
15+
}
16+
17+
function formatDuration(ms: number | null): string {
18+
if (ms === null) return "--";
19+
const minutes = Math.round(ms / 60_000);
20+
if (minutes < 60) return `${minutes}m`;
21+
const hours = Math.floor(minutes / 60);
22+
const remaining = minutes % 60;
23+
return remaining > 0 ? `${hours}h ${remaining}m` : `${hours}h`;
24+
}
25+
26+
export function LoopSummaryCard({ className }: LoopSummaryCardProps) {
27+
const summary = useQuery(api.loopMetrics.getDailySummary);
28+
29+
if (!summary) {
30+
return (
31+
<div className={className}>
32+
<div className="grid grid-cols-4 gap-2">
33+
{[...Array(4)].map((_, i) => (
34+
<div key={i} className="h-16 bg-zinc-800 animate-pulse rounded-lg" />
35+
))}
36+
</div>
37+
</div>
38+
);
39+
}
40+
41+
const metrics = [
42+
{
43+
label: "Active",
44+
value: summary.totalActive,
45+
color: "text-blue-400",
46+
bg: "bg-blue-500/10 border-blue-500/20",
47+
},
48+
{
49+
label: "Done Today",
50+
value: summary.completedToday,
51+
color: "text-emerald-400",
52+
bg: "bg-emerald-500/10 border-emerald-500/20",
53+
},
54+
{
55+
label: "Broken",
56+
value: summary.brokenToday,
57+
color: summary.brokenToday > 0 ? "text-red-400" : "text-zinc-500",
58+
bg: summary.brokenToday > 0
59+
? "bg-red-500/10 border-red-500/20"
60+
: "bg-zinc-800/50 border-zinc-700/30",
61+
},
62+
{
63+
label: "Avg Time",
64+
value: formatDuration(summary.avgCompletionTimeMs),
65+
color: "text-amber-400",
66+
bg: "bg-amber-500/10 border-amber-500/20",
67+
},
68+
];
69+
70+
return (
71+
<div className={className}>
72+
<h2 className="text-xs font-bold uppercase tracking-wider text-zinc-500 mb-2">
73+
The Loop
74+
</h2>
75+
<div className="grid grid-cols-4 gap-2">
76+
{metrics.map((m) => (
77+
<div
78+
key={m.label}
79+
className={`${m.bg} border rounded-lg p-2 text-center`}
80+
>
81+
<div className={`text-lg font-bold tabular-nums ${m.color}`}>
82+
{m.value}
83+
</div>
84+
<div className="text-[10px] text-zinc-500 uppercase tracking-wide">
85+
{m.label}
86+
</div>
87+
</div>
88+
))}
89+
</div>
90+
</div>
91+
);
92+
}
Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
"use client";
2+
3+
/**
4+
* UnresolvedAlerts — AGT-336: Unresolved loop alerts
5+
*
6+
* Shows active + escalated loop alerts with joined message context.
7+
* Uses loopMetrics.getUnresolved query.
8+
*/
9+
10+
import { useQuery } from "convex/react";
11+
import { api } from "@/convex/_generated/api";
12+
import { formatDistanceToNow } from "date-fns";
13+
import { cn } from "@/lib/utils";
14+
15+
interface UnresolvedAlertsProps {
16+
className?: string;
17+
limit?: number;
18+
}
19+
20+
const ALERT_TYPE_LABELS: Record<string, string> = {
21+
reply_overdue: "Reply overdue",
22+
action_overdue: "Action overdue",
23+
report_overdue: "Report overdue",
24+
loop_broken: "Loop broken",
25+
};
26+
27+
const SEVERITY_STYLES: Record<string, { bg: string; border: string; badge: string }> = {
28+
critical: {
29+
bg: "bg-red-500/5",
30+
border: "border-red-500/30",
31+
badge: "bg-red-500/20 text-red-400",
32+
},
33+
warning: {
34+
bg: "bg-amber-500/5",
35+
border: "border-amber-500/30",
36+
badge: "bg-amber-500/20 text-amber-400",
37+
},
38+
};
39+
40+
export function UnresolvedAlerts({ className, limit = 10 }: UnresolvedAlertsProps) {
41+
const alerts = useQuery(api.loopMetrics.getUnresolved, { limit });
42+
43+
if (!alerts) {
44+
return (
45+
<div className={className}>
46+
<div className="animate-pulse space-y-2">
47+
{[...Array(3)].map((_, i) => (
48+
<div key={i} className="h-16 bg-zinc-800 rounded-lg" />
49+
))}
50+
</div>
51+
</div>
52+
);
53+
}
54+
55+
if (alerts.length === 0) {
56+
return null; // Clean state — no alerts, no noise
57+
}
58+
59+
return (
60+
<div className={className}>
61+
<div className="bg-amber-500/5 border border-amber-500/20 rounded-lg p-3">
62+
<div className="flex items-center gap-2 mb-2">
63+
<span className="h-2 w-2 rounded-full bg-amber-500 animate-pulse" />
64+
<span className="text-amber-400 font-semibold text-sm uppercase tracking-wide">
65+
Unresolved ({alerts.length})
66+
</span>
67+
</div>
68+
69+
<div className="space-y-2">
70+
{alerts.map((alert) => {
71+
const style = SEVERITY_STYLES[alert.severity] ?? SEVERITY_STYLES.warning;
72+
73+
return (
74+
<div
75+
key={alert.alertId}
76+
className={cn(
77+
"rounded-lg p-2 border",
78+
style.bg,
79+
style.border
80+
)}
81+
>
82+
{/* Row 1: Type badge + agents + time */}
83+
<div className="flex items-center gap-2 mb-1">
84+
<span className={cn("text-[10px] font-bold uppercase px-1.5 py-0.5 rounded", style.badge)}>
85+
{ALERT_TYPE_LABELS[alert.alertType] ?? alert.alertType}
86+
</span>
87+
<span className="text-xs text-zinc-400">
88+
{alert.fromAgent}<span className="text-white font-medium">{alert.toAgent}</span>
89+
</span>
90+
{alert.escalatedTo && (
91+
<span className="text-[10px] text-purple-400 ml-auto">
92+
esc → {alert.escalatedTo}
93+
</span>
94+
)}
95+
<span className="text-[10px] text-zinc-600 ml-auto">
96+
{formatDistanceToNow(alert.sentAt, { addSuffix: true })}
97+
</span>
98+
</div>
99+
100+
{/* Row 2: Message content */}
101+
<div className="text-xs text-zinc-300 truncate">
102+
{alert.content}
103+
</div>
104+
105+
{/* Row 3: Status + broken reason */}
106+
<div className="flex items-center gap-2 mt-1 text-[10px] text-zinc-500">
107+
<span>Status: {alert.messageStatusLabel}</span>
108+
{alert.loopBroken && alert.loopBrokenReason && (
109+
<span className="text-red-400 truncate">
110+
{alert.loopBrokenReason}
111+
</span>
112+
)}
113+
</div>
114+
</div>
115+
);
116+
})}
117+
</div>
118+
</div>
119+
</div>
120+
);
121+
}

0 commit comments

Comments
 (0)