Skip to content

Commit 0d048bf

Browse files
sonpiazclaude
andcommitted
feat: Command Center UI overhaul — remove sidebar, add time filter, fix agent clicks
- Remove 220px AgentSidebar panel, full-width content layout - Default view changed from Kanban to Overview - Tab order: Overview → Kanban → Team → Comms - Agent name clicks in Team/Overview open slide-over panel (fix redirect loop) - Add 1d/7d/30d time range toggle for Overview metrics - Top bar stats sync with active view's time range - Remove yellow "tasks blocked" banner - Precise timestamps ("42m", "1h 3m") instead of "about 1 hour" - Increase dashboard.getStats take() limits for count accuracy Co-Authored-By: Claude Opus 4.6 <[email protected]>
1 parent 0ede7af commit 0d048bf

File tree

6 files changed

+113
-87
lines changed

6 files changed

+113
-87
lines changed

app/page.tsx

Lines changed: 31 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@ import { startOfDay, endOfDay, startOfWeek, endOfWeek, subDays } from "date-fns"
99
import { NotificationTopBarWrapper } from "@/components/notification-topbar-wrapper";
1010
import { MissionQueue } from "@/components/dashboard-v2/mission-queue";
1111
import { SettingsModal } from "@/components/dashboard-v2/settings-modal";
12-
import { AgentSidebar } from "@/components/evox/AgentSidebar";
1312
import { AgentSettingsModal } from "@/components/evox/AgentSettingsModal";
1413
import { ShortcutsHelpModal } from "@/components/evox/ShortcutsHelpModal";
1514
import { useKeyboardShortcuts } from "@/hooks/useKeyboardShortcuts";
@@ -18,7 +17,7 @@ import { ActivityDrawer } from "@/components/dashboard-v2/activity-drawer";
1817
import { TaskDetailModal } from "@/components/dashboard-v2/task-detail-modal";
1918
import { ViewTabs, type MainViewTab } from "@/components/evox/ViewTabs";
2019
import { CommunicationLog } from "@/components/evox/CommunicationLog";
21-
import { CEODashboard } from "@/components/evox/CEODashboard";
20+
import { CEODashboard, type TimeRange } from "@/components/evox/CEODashboard";
2221
import { HallOfFame } from "@/components/evox/HallOfFame";
2322
import type { KanbanTask } from "@/components/dashboard-v2/task-card";
2423
import type { DateFilterMode } from "@/components/dashboard-v2/date-filter";
@@ -42,18 +41,29 @@ function HomeContent() {
4241
const [selectedTask, setSelectedTask] = useState<KanbanTask | null>(null);
4342
const [agentSettingsId, setAgentSettingsId] = useState<Id<"agents"> | null>(null);
4443
const [shortcutsHelpOpen, setShortcutsHelpOpen] = useState(false);
44+
const [overviewTimeRange, setOverviewTimeRange] = useState<TimeRange>("1d");
4545
const searchParams = useSearchParams();
4646
const router = useRouter();
4747
const viewParam = searchParams.get("view") as MainViewTab | null;
48-
const activeViewTab: MainViewTab = viewParam && ["ceo", "kanban", "comms", "team"].includes(viewParam) ? viewParam : "kanban";
48+
const activeViewTab: MainViewTab = viewParam && ["ceo", "kanban", "comms", "team"].includes(viewParam) ? viewParam : "ceo";
4949
const setActiveViewTab = useCallback((tab: MainViewTab) => {
5050
router.replace(`/?view=${tab}`, { scroll: false });
5151
}, [router]);
5252

5353
const agents = useQuery(api.agents.list);
5454

55-
// AGT-189: Calculate date range for done tasks filtering (same as MissionQueue)
55+
// Calculate date range for top bar stats — adapts to active view
5656
const { startTs, endTs } = useMemo(() => {
57+
if (activeViewTab === "ceo") {
58+
// Overview: use overview time range
59+
const daysBack = overviewTimeRange === "1d" ? 1 : overviewTimeRange === "7d" ? 7 : 30;
60+
const now = new Date();
61+
return {
62+
startTs: daysBack === 1 ? startOfDay(now).getTime() : subDays(now, daysBack).getTime(),
63+
endTs: now.getTime(),
64+
};
65+
}
66+
// Kanban and other views: use kanban dateMode
5767
if (dateMode === "day") {
5868
return {
5969
startTs: startOfDay(date).getTime(),
@@ -71,7 +81,7 @@ function HomeContent() {
7181
endTs: now.getTime(),
7282
};
7383
}
74-
}, [date, dateMode]);
84+
}, [date, dateMode, activeViewTab, overviewTimeRange]);
7585

7686
const dashboardStats = useQuery(api.dashboard.getStats, { startTs, endTs });
7787

@@ -117,10 +127,6 @@ function HomeContent() {
117127
}
118128
};
119129

120-
const handleAgentDoubleClick = (agentId: Id<"agents">) => {
121-
setAgentSettingsId(agentId);
122-
};
123-
124130
const handleTaskClick = (task: KanbanTask) => {
125131
setSelectedTask(task);
126132
};
@@ -147,20 +153,19 @@ function HomeContent() {
147153
<TaskDetailModal open={selectedTask !== null} task={selectedTask} onClose={() => setSelectedTask(null)} />
148154

149155
<div className="flex flex-1 min-h-0 overflow-hidden">
150-
{/* Sidebar - hidden on mobile, visible on md+ */}
151-
<div className="hidden md:flex flex-col w-[220px] shrink-0">
152-
<AgentSidebar
153-
selectedAgentId={selectedAgentId}
154-
onAgentClick={handleAgentClick}
155-
onAgentDoubleClick={handleAgentDoubleClick}
156-
className="flex-1"
157-
/>
158-
</div>
159156
<main className="flex-1 min-w-0 flex flex-col overflow-hidden">
160157
<ViewTabs activeTab={activeViewTab} onTabChange={setActiveViewTab} />
161158
<div className="flex-1 min-h-0 overflow-hidden">
162159
{activeViewTab === "ceo" && (
163-
<CEODashboard className="h-full" />
160+
<CEODashboard
161+
className="h-full"
162+
timeRange={overviewTimeRange}
163+
onTimeRangeChange={setOverviewTimeRange}
164+
onAgentClick={(name) => {
165+
const agent = agentsList.find(a => a.name.toLowerCase() === name.toLowerCase());
166+
if (agent) handleAgentClick(agent._id);
167+
}}
168+
/>
164169
)}
165170
{activeViewTab === "kanban" && (
166171
<MissionQueue
@@ -176,7 +181,13 @@ function HomeContent() {
176181
<CommunicationLog className="h-full" />
177182
)}
178183
{activeViewTab === "team" && (
179-
<HallOfFame className="h-full" />
184+
<HallOfFame
185+
className="h-full"
186+
onAgentClick={(name) => {
187+
const agent = agentsList.find(a => a.name.toLowerCase() === name.toLowerCase());
188+
if (agent) handleAgentClick(agent._id);
189+
}}
190+
/>
180191
)}
181192
</div>
182193
</main>

components/evox/CEODashboard.tsx

Lines changed: 56 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -14,12 +14,11 @@
1414
* 6. Agent Comms — last 3-5 messages
1515
*/
1616

17-
import { useMemo } from "react";
17+
import { useState, useMemo } from "react";
1818
import { useQuery } from "convex/react";
1919
import { api } from "@/convex/_generated/api";
2020
import { cn } from "@/lib/utils";
21-
import { formatDistanceToNow, subDays, startOfDay, format } from "date-fns";
22-
import Link from "next/link";
21+
import { subDays, startOfDay, format } from "date-fns";
2322

2423
/** Agent colors */
2524
const AGENT_COLORS: Record<string, string> = {
@@ -34,21 +33,40 @@ function agentColor(name: string) {
3433
}
3534

3635
function timeAgo(ts: number) {
37-
return formatDistanceToNow(ts, { addSuffix: false });
36+
const diff = Date.now() - ts;
37+
const minutes = Math.floor(diff / 60000);
38+
if (minutes < 1) return "now";
39+
if (minutes < 60) return `${minutes}m`;
40+
const hours = Math.floor(minutes / 60);
41+
if (hours < 24) return `${hours}h ${minutes % 60}m`;
42+
const days = Math.floor(hours / 24);
43+
return `${days}d`;
3844
}
3945

46+
export type TimeRange = "1d" | "7d" | "30d";
47+
4048
interface CEODashboardProps {
4149
className?: string;
50+
onAgentClick?: (name: string) => void;
51+
timeRange?: TimeRange;
52+
onTimeRangeChange?: (range: TimeRange) => void;
4253
}
4354

44-
export function CEODashboard({ className }: CEODashboardProps = {}) {
55+
const TIME_RANGE_DAYS: Record<TimeRange, number> = { "1d": 1, "7d": 7, "30d": 30 };
56+
const TIME_RANGE_LABEL: Record<TimeRange, string> = { "1d": "Today", "7d": "7d", "30d": "30d" };
57+
58+
export function CEODashboard({ className, onAgentClick, timeRange: externalTimeRange, onTimeRangeChange }: CEODashboardProps = {}) {
59+
const [internalTimeRange, setInternalTimeRange] = useState<TimeRange>("1d");
60+
const timeRange = externalTimeRange ?? internalTimeRange;
61+
const setTimeRange = onTimeRangeChange ?? setInternalTimeRange;
62+
const days = TIME_RANGE_DAYS[timeRange];
63+
4564
// Data queries — only real data
4665
const agentStatus = useQuery(api.ceoMetrics.getAgentStatus);
47-
const todayMetrics = useQuery(api.ceoMetrics.getTodayMetrics);
48-
const blockers = useQuery(api.ceoMetrics.getBlockers);
66+
const todayMetrics = useQuery(api.ceoMetrics.getTodayMetrics, { days });
4967
const liveFeed = useQuery(api.ceoMetrics.getLiveFeed, { limit: 5 });
5068
const commits = useQuery(api.gitActivity.getRecent, { limit: 5 });
51-
const velocityTrend = useQuery(api.dashboard.getVelocityTrend, { days: 7 });
69+
const velocityTrend = useQuery(api.dashboard.getVelocityTrend, { days });
5270
const comms = useQuery(api.ceoMetrics.getRecentComms, { limit: 5 });
5371

5472
// Computed values
@@ -70,23 +88,22 @@ export function CEODashboard({ className }: CEODashboardProps = {}) {
7088
if (!tasks) return null;
7189
const now = Date.now();
7290
const day24h = 24 * 60 * 60 * 1000;
73-
const day7 = 7 * day24h;
91+
const periodMs = days * day24h;
7492

75-
const tasks24h = tasks.filter(t => t.updatedAt > now - day24h);
76-
const tasks7d = tasks.filter(t => t.updatedAt > now - day7);
93+
const tasksInPeriod = tasks.filter(t => t.updatedAt > now - periodMs);
7794
const withErrors = tasks.filter(t => (t.retryCount && t.retryCount > 0) || t.lastError);
7895

79-
const completed24h = tasks24h.filter(t => t.status?.toLowerCase() === "done");
80-
const attempted24h = tasks24h.filter(t => t.status?.toLowerCase() === "done" || t.status?.toLowerCase() === "in_progress");
81-
const successRate = attempted24h.length > 0
82-
? Math.round((completed24h.filter(t => !t.lastError && (!t.retryCount || t.retryCount === 0)).length / attempted24h.length) * 100)
96+
const completedInPeriod = tasksInPeriod.filter(t => t.status?.toLowerCase() === "done");
97+
const attemptedInPeriod = tasksInPeriod.filter(t => t.status?.toLowerCase() === "done" || t.status?.toLowerCase() === "in_progress");
98+
const successRate = attemptedInPeriod.length > 0
99+
? Math.round((completedInPeriod.filter(t => !t.lastError && (!t.retryCount || t.retryCount === 0)).length / attemptedInPeriod.length) * 100)
83100
: 100;
84101

85-
const errors7d = withErrors.filter(t => t.updatedAt > now - day7).length;
102+
const errorsInPeriod = withErrors.filter(t => t.updatedAt > now - periodMs).length;
86103

87-
// 7-day success trend for sparkline
104+
// Success trend sparkline (per-day for the period)
88105
const successByDay: number[] = [];
89-
for (let i = 6; i >= 0; i--) {
106+
for (let i = days - 1; i >= 0; i--) {
90107
const dayStart = startOfDay(subDays(now, i)).getTime();
91108
const dayEnd = dayStart + day24h;
92109
const dayTasks = tasks.filter(t => t.updatedAt >= dayStart && t.updatedAt < dayEnd);
@@ -95,25 +112,24 @@ export function CEODashboard({ className }: CEODashboardProps = {}) {
95112
successByDay.push(dayAttempted.length > 0 ? (dayCompleted.length / dayAttempted.length) * 100 : 100);
96113
}
97114

98-
// 7-day error bars
115+
// Error bars (per-day for the period)
99116
const errorsByDay: { label: string; value: number }[] = [];
100-
for (let i = 6; i >= 0; i--) {
117+
for (let i = days - 1; i >= 0; i--) {
101118
const dayStart = startOfDay(subDays(now, i)).getTime();
102119
const dayEnd = dayStart + day24h;
103120
const dayErrors = withErrors.filter(t => t.updatedAt >= dayStart && t.updatedAt < dayEnd);
104121
errorsByDay.push({ label: format(dayStart, "EEE"), value: dayErrors.length });
105122
}
106123

107-
return { successRate, errors7d, successByDay, errorsByDay };
108-
}, [tasks]);
124+
return { successRate, errorsInPeriod, successByDay, errorsByDay };
125+
}, [tasks, days]);
109126

110127
const commitCount = commits?.length ?? 0;
111128
const totalAgents = agentStatus?.total ?? 0;
112129
const completed = todayMetrics?.completed ?? 0;
113130
const inProgress = todayMetrics?.inProgress ?? 0;
114131
const blocked = todayMetrics?.blocked ?? 0;
115132

116-
const hasAlerts = blockers && blockers.length > 0;
117133
const dataLoaded = !!agentStatus && !!todayMetrics;
118134
const isLoading = !dataLoaded;
119135

@@ -125,9 +141,14 @@ export function CEODashboard({ className }: CEODashboardProps = {}) {
125141
EVOX
126142
</h1>
127143
<div className="flex items-center gap-3">
128-
<Link href="/?view=team" className="text-xs text-primary0 hover:text-primary transition-colors">
129-
Team
130-
</Link>
144+
<div className="flex items-center gap-1 bg-surface-1 border border-border-default rounded-lg p-0.5">
145+
{(["1d", "7d", "30d"] as const).map(r => (
146+
<button key={r} onClick={() => setTimeRange(r)}
147+
className={cn("px-2 py-1 text-[10px] rounded transition-colors", timeRange === r ? "bg-surface-4 text-primary" : "text-tertiary hover:text-secondary")}>
148+
{r}
149+
</button>
150+
))}
151+
</div>
131152
<div className="flex items-center gap-2">
132153
<div className={cn(
133154
"h-2 w-2 rounded-full",
@@ -190,10 +211,10 @@ export function CEODashboard({ className }: CEODashboardProps = {}) {
190211
<div className="text-[10px] text-tertiary">agents</div>
191212
</div>
192213

193-
{/* Today */}
214+
{/* Period Summary */}
194215
<div className="bg-surface-1 border border-border-default rounded-xl p-4">
195216
<div className="text-[10px] font-bold uppercase tracking-wider text-primary0 mb-1">
196-
Today
217+
{TIME_RANGE_LABEL[timeRange]}
197218
</div>
198219
{isLoading ? (
199220
<div className="text-2xl font-bold text-tertiary"></div>
@@ -219,7 +240,7 @@ export function CEODashboard({ className }: CEODashboardProps = {}) {
219240
)}>
220241
{healthMetrics ? `${healthMetrics.successRate}%` : "—"}
221242
</div>
222-
<div className="text-[10px] text-tertiary">24h</div>
243+
<div className="text-[10px] text-tertiary">{timeRange}</div>
223244
{healthMetrics && healthMetrics.successByDay.length > 0 && (
224245
<svg width={80} height={24} className="mt-2">
225246
<polyline
@@ -252,11 +273,11 @@ export function CEODashboard({ className }: CEODashboardProps = {}) {
252273
<div className={cn(
253274
"text-2xl font-bold tabular-nums",
254275
!healthMetrics ? "text-tertiary" :
255-
healthMetrics.errors7d === 0 ? "text-emerald-400" : "text-red-400"
276+
healthMetrics.errorsInPeriod === 0 ? "text-emerald-400" : "text-red-400"
256277
)}>
257-
{healthMetrics ? healthMetrics.errors7d : "—"}
278+
{healthMetrics ? healthMetrics.errorsInPeriod : "—"}
258279
</div>
259-
<div className="text-[10px] text-tertiary">7d</div>
280+
<div className="text-[10px] text-tertiary">{timeRange}</div>
260281
{healthMetrics && healthMetrics.errorsByDay.length > 0 && (
261282
<div className="flex items-end gap-[2px] h-6 mt-2">
262283
{healthMetrics.errorsByDay.map((d, i) => {
@@ -278,31 +299,21 @@ export function CEODashboard({ className }: CEODashboardProps = {}) {
278299
</div>
279300
</div>
280301

281-
{/* ─── Alerts Banner ─── */}
282-
{hasAlerts && (
283-
<div className="flex items-center gap-2 bg-yellow-500/10 border border-yellow-500/20 rounded-lg px-4 py-2.5 text-sm">
284-
<span className="text-yellow-400 shrink-0">!</span>
285-
<span className="text-yellow-300">
286-
{blockers.length} task{blockers.length > 1 ? "s" : ""} blocked
287-
</span>
288-
</div>
289-
)}
290-
291302
{/* ─── Team Strip ─── */}
292303
<div className="flex items-center gap-1 overflow-x-auto py-1">
293304
<span className="text-[10px] font-bold uppercase tracking-wider text-tertiary mr-2 shrink-0">
294305
Team
295306
</span>
296307
{agentStatus?.agents.map((agent) => (
297-
<Link
308+
<button
298309
key={agent.name}
299-
href={`/agents/${agent.name.toLowerCase()}`}
310+
onClick={() => onAgentClick?.(agent.name)}
300311
className="flex items-center gap-1.5 px-2.5 py-1.5 shrink-0 hover:bg-surface-2 rounded-lg transition-colors"
301312
>
302313
<span className="text-xs font-medium text-primary">
303314
{agent.name}
304315
</span>
305-
</Link>
316+
</button>
306317
))}
307318
</div>
308319

0 commit comments

Comments
 (0)