1414 * 6. Agent Comms — last 3-5 messages
1515 */
1616
17- import { useMemo } from "react" ;
17+ import { useState , useMemo } from "react" ;
1818import { useQuery } from "convex/react" ;
1919import { api } from "@/convex/_generated/api" ;
2020import { 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 */
2524const AGENT_COLORS : Record < string , string > = {
@@ -34,21 +33,40 @@ function agentColor(name: string) {
3433}
3534
3635function 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+
4048interface 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