feat(US-1305): Add status history to activity sidebar

- Added daily summary card showing active/idle time and utilization %
- Added Status History tab with today's status transitions
- Display previous/new status with duration of each state
- Mini progress bar for visual time breakdown
- Backend was already complete (status-history-service logs all status changes)
This commit is contained in:
Brad Groux 2026-01-28 08:14:55 -06:00
parent b53ef09dcd
commit 8a9416b7cc
3 changed files with 224 additions and 34 deletions

View file

@ -1107,3 +1107,4 @@
{"type":"task.created","taskId":"task_20260128_b2CW4g","project":"project-a","status":"todo","id":"evt_-g7kMhjBnCYG","timestamp":"2026-01-28T14:13:49.936Z"}
{"type":"task.created","taskId":"task_20260128_NEm1K_","project":"project-b","status":"todo","id":"evt_aCXwbrsfHVkW","timestamp":"2026-01-28T14:13:49.937Z"}
{"type":"task.created","taskId":"task_20260128_eCyXeF","status":"todo","id":"evt_qx8JflCKJ_1V","timestamp":"2026-01-28T14:13:49.944Z"}
{"type":"task.status_changed","taskId":"task_20260128_d47DtQ","project":"platform","status":"blocked","previousStatus":"todo","id":"evt_dcyHNA6aIlp5","timestamp":"2026-01-28T14:13:59.205Z"}

View file

@ -1,4 +1,33 @@
[
{
"id": "activity_1769609642656_9lea2p5ac",
"type": "comment_added",
"taskId": "task_20260128_V0MVwS",
"taskTitle": "US-1405: Dashboard metrics cards refresh",
"details": {
"author": "Veritas",
"preview": "Verified dashboard metrics cards are fully functio..."
},
"timestamp": "2026-01-28T14:14:02.656Z"
},
{
"id": "activity_1769609642614_cy9x653r2",
"type": "task_updated",
"taskId": "task_20260128_V0MVwS",
"taskTitle": "US-1405: Dashboard metrics cards refresh",
"timestamp": "2026-01-28T14:14:02.614Z"
},
{
"id": "activity_1769609639205_sn6jggief",
"type": "status_changed",
"taskId": "task_20260128_d47DtQ",
"taskTitle": "FEATURE REQUEST: Host Kimi K2.5 Model In Azure",
"details": {
"from": "todo",
"status": "blocked"
},
"timestamp": "2026-01-28T14:13:59.205Z"
},
{
"id": "activity_1769609585007_vp6ouz8mc",
"type": "comment_added",

View file

@ -1,5 +1,5 @@
import { useState } from 'react';
import { Activity, Trash2, RefreshCw, Clock, Coffee, ArrowRight, Zap } from 'lucide-react';
import { Activity, Trash2, RefreshCw, Coffee, ArrowRight, Zap } from 'lucide-react';
import { Button } from '@/components/ui/button';
import { ScrollArea } from '@/components/ui/scroll-area';
import {
@ -102,8 +102,147 @@ function ActivityRow({ activity }: { activity: ActivityItem }) {
);
}
// Status Badge component
function StatusBadge({ status }: { status: string }) {
const colorClass = getStatusColor(status);
return (
<span className={cn(
'inline-flex items-center px-2 py-0.5 rounded text-xs font-medium text-white',
colorClass
)}>
{status}
</span>
);
}
// Daily Summary Card
function DailySummaryCard() {
const { data: summary, isLoading } = useDailySummary();
if (isLoading || !summary) {
return (
<div className="px-4 py-3 border-b bg-muted/30">
<div className="animate-pulse flex items-center gap-4">
<div className="h-10 w-20 bg-muted rounded"></div>
<div className="h-10 w-20 bg-muted rounded"></div>
<div className="h-10 w-20 bg-muted rounded"></div>
</div>
</div>
);
}
const total = summary.activeMs + summary.idleMs + summary.errorMs;
const activePercent = total > 0 ? Math.round((summary.activeMs / total) * 100) : 0;
return (
<div className="px-4 py-3 border-b bg-muted/30">
<div className="text-xs font-medium text-muted-foreground mb-2">Today's Summary</div>
<div className="flex items-center gap-3">
<div className="flex items-center gap-1.5">
<Zap className="h-4 w-4 text-green-500" />
<div>
<div className="text-sm font-bold text-green-500">{formatDurationMs(summary.activeMs)}</div>
<div className="text-xs text-muted-foreground">Active</div>
</div>
</div>
<div className="flex items-center gap-1.5">
<Coffee className="h-4 w-4 text-muted-foreground" />
<div>
<div className="text-sm font-bold text-muted-foreground">{formatDurationMs(summary.idleMs)}</div>
<div className="text-xs text-muted-foreground">Idle</div>
</div>
</div>
<div className="flex items-center gap-1.5 ml-auto">
<div className="text-right">
<div className="text-sm font-bold">{activePercent}%</div>
<div className="text-xs text-muted-foreground">Utilization</div>
</div>
</div>
</div>
{/* Mini progress bar */}
{total > 0 && (
<div className="mt-2 h-1.5 rounded-full overflow-hidden flex bg-muted">
<div
className="bg-green-500 transition-all"
style={{ width: `${(summary.activeMs / total) * 100}%` }}
/>
<div
className="bg-gray-400 transition-all"
style={{ width: `${(summary.idleMs / total) * 100}%` }}
/>
{summary.errorMs > 0 && (
<div
className="bg-red-500 transition-all"
style={{ width: `${(summary.errorMs / total) * 100}%` }}
/>
)}
</div>
)}
</div>
);
}
// Status Transition Row
function StatusTransitionRow({ entry }: { entry: StatusHistoryEntry }) {
return (
<div className="flex items-center gap-2 py-3 px-2 hover:bg-muted/50 rounded-md transition-colors">
<span className="text-xs text-muted-foreground w-14 shrink-0">
{new Date(entry.timestamp).toLocaleTimeString([], {
hour: '2-digit',
minute: '2-digit'
})}
</span>
<StatusBadge status={entry.previousStatus} />
<ArrowRight className="h-3 w-3 text-muted-foreground shrink-0" />
<StatusBadge status={entry.newStatus} />
{entry.durationMs && (
<span className="text-xs text-muted-foreground ml-auto">
{formatDurationMs(entry.durationMs)}
</span>
)}
</div>
);
}
// Status History List
function StatusHistoryList() {
const { data: history, isLoading } = useStatusHistory(50);
// Filter to today's entries
const today = new Date().toISOString().split('T')[0];
const todayEntries = history?.filter(entry =>
entry.timestamp.startsWith(today)
) || [];
if (isLoading) {
return (
<div className="text-center text-muted-foreground py-8">
Loading status history...
</div>
);
}
if (todayEntries.length === 0) {
return (
<div className="text-center text-muted-foreground py-8">
No status changes today
</div>
);
}
return (
<div className="divide-y divide-border">
{todayEntries.map(entry => (
<StatusTransitionRow key={entry.id} entry={entry} />
))}
</div>
);
}
export function ActivitySidebar({ open, onOpenChange }: ActivitySidebarProps) {
const [filter, setFilter] = useState<string>('all');
const [tab, setTab] = useState<string>('tasks');
const { data: activities, isLoading, refetch, isRefetching } = useActivities(100);
const clearActivities = useClearActivities();
@ -146,43 +285,64 @@ export function ActivitySidebar({ open, onOpenChange }: ActivitySidebarProps) {
</Button>
</div>
</div>
{activityTypes.length > 1 && (
<Select value={filter} onValueChange={setFilter}>
<SelectTrigger className="mt-2">
<SelectValue placeholder="Filter activities" />
</SelectTrigger>
<SelectContent>
<SelectItem value="all">All Activities</SelectItem>
{activityTypes.map(type => (
<SelectItem key={type} value={type}>
{activityIcons[type]} {activityLabels[type]}
</SelectItem>
))}
</SelectContent>
</Select>
)}
</SheetHeader>
<ScrollArea className="h-[calc(100vh-120px)]">
<div className="px-2 py-2">
{isLoading ? (
<div className="text-center text-muted-foreground py-8">
Loading activities...
</div>
) : filteredActivities.length === 0 ? (
<div className="text-center text-muted-foreground py-8">
No activities yet
</div>
) : (
<div className="divide-y divide-border">
{filteredActivities.map(activity => (
<ActivityRow key={activity.id} activity={activity} />
))}
{/* Daily Summary always visible at top */}
<DailySummaryCard />
<Tabs value={tab} onValueChange={setTab} className="flex flex-col h-[calc(100vh-200px)]">
<TabsList className="mx-4 mt-2 grid w-[calc(100%-32px)] grid-cols-2">
<TabsTrigger value="tasks">Task Activity</TabsTrigger>
<TabsTrigger value="status">Status History</TabsTrigger>
</TabsList>
<TabsContent value="tasks" className="flex-1 mt-0">
{activityTypes.length > 1 && (
<div className="px-4 py-2">
<Select value={filter} onValueChange={setFilter}>
<SelectTrigger>
<SelectValue placeholder="Filter activities" />
</SelectTrigger>
<SelectContent>
<SelectItem value="all">All Activities</SelectItem>
{activityTypes.map(type => (
<SelectItem key={type} value={type}>
{activityIcons[type]} {activityLabels[type]}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
)}
</div>
</ScrollArea>
<ScrollArea className="h-full">
<div className="px-2 py-2">
{isLoading ? (
<div className="text-center text-muted-foreground py-8">
Loading activities...
</div>
) : filteredActivities.length === 0 ? (
<div className="text-center text-muted-foreground py-8">
No activities yet
</div>
) : (
<div className="divide-y divide-border">
{filteredActivities.map(activity => (
<ActivityRow key={activity.id} activity={activity} />
))}
</div>
)}
</div>
</ScrollArea>
</TabsContent>
<TabsContent value="status" className="flex-1 mt-0">
<ScrollArea className="h-full">
<div className="px-2 py-2">
<StatusHistoryList />
</div>
</ScrollArea>
</TabsContent>
</Tabs>
</SheetContent>
</Sheet>
);