diff --git a/ui/src/components/account-flow-viz.tsx b/ui/src/components/account-flow-viz.tsx
index 8ed2a6b0..3951dd40 100644
--- a/ui/src/components/account-flow-viz.tsx
+++ b/ui/src/components/account-flow-viz.tsx
@@ -4,11 +4,12 @@
* Inspired by modern dark theme design with glass panels and glow effects
*/
-import { useRef, useEffect, useState, useCallback } from 'react';
+import { useRef, useEffect, useState, useCallback, useMemo } from 'react';
import { cn } from '@/lib/utils';
import { ProviderIcon } from '@/components/provider-icon';
import { PROVIDER_COLORS } from '@/lib/provider-config';
-import { ChevronRight, X, CheckCircle2, XCircle, Clock } from 'lucide-react';
+import { STATUS_COLORS } from '@/lib/utils';
+import { ChevronRight, X, CheckCircle2, XCircle, Clock, Activity } from 'lucide-react';
interface AccountData {
id: string;
@@ -32,6 +33,157 @@ interface AccountFlowVizProps {
onBack?: () => void;
}
+interface ConnectionEvent {
+ id: string;
+ timestamp: Date;
+ accountEmail: string;
+ status: 'success' | 'failed' | 'pending';
+ latencyMs?: number;
+}
+
+/** Generate mock connection events based on account data */
+function generateConnectionEvents(accounts: AccountData[]): ConnectionEvent[] {
+ const events: ConnectionEvent[] = [];
+ const now = new Date();
+
+ accounts.forEach((account) => {
+ // Generate events based on success/failure counts
+ const successEvents = Math.min(account.successCount, 3);
+ const failEvents = Math.min(account.failureCount, 2);
+
+ for (let i = 0; i < successEvents; i++) {
+ events.push({
+ id: `${account.id}-s-${i}`,
+ timestamp: new Date(now.getTime() - Math.random() * 3600000), // Last hour
+ accountEmail: account.email,
+ status: 'success',
+ latencyMs: Math.floor(Math.random() * 500) + 50,
+ });
+ }
+
+ for (let i = 0; i < failEvents; i++) {
+ events.push({
+ id: `${account.id}-f-${i}`,
+ timestamp: new Date(now.getTime() - Math.random() * 7200000), // Last 2 hours
+ accountEmail: account.email,
+ status: 'failed',
+ });
+ }
+ });
+
+ // Sort by timestamp descending (most recent first)
+ return events.sort((a, b) => b.timestamp.getTime() - a.timestamp.getTime());
+}
+
+/** Format timestamp for timeline display */
+function formatTimelineTime(date: Date): string {
+ const now = new Date();
+ const diffMs = now.getTime() - date.getTime();
+ const diffMins = Math.floor(diffMs / 60000);
+
+ if (diffMins < 1) return 'now';
+ if (diffMins < 60) return `${diffMins}m`;
+ const diffHours = Math.floor(diffMins / 60);
+ if (diffHours < 24) return `${diffHours}h`;
+ return `${Math.floor(diffHours / 24)}d`;
+}
+
+/** Connection Timeline Component - right sidebar panel */
+function ConnectionTimeline({ events }: { events: ConnectionEvent[] }) {
+ if (events.length === 0) {
+ return (
+
+
No recent connections
+
+ );
+ }
+
+ return (
+
+ {/* Header */}
+
+
+
+ Connection Timeline
+
+
+
+ {/* Timeline container */}
+
+
+ {/* Vertical line */}
+
+
+ {/* Events */}
+
+ {events.slice(0, 8).map((event) => {
+ const statusColor =
+ event.status === 'success'
+ ? STATUS_COLORS.success
+ : event.status === 'failed'
+ ? STATUS_COLORS.failed
+ : STATUS_COLORS.degraded;
+
+ return (
+
+ {/* Timeline dot */}
+
+
+ {/* Event content */}
+
+
+
+ {cleanEmail(event.accountEmail)}
+
+
+ {formatTimelineTime(event.timestamp)}
+
+
+
+
+ {event.status}
+
+ {event.latencyMs && (
+
+ {event.latencyMs}ms
+
+ )}
+
+
+
+ );
+ })}
+
+
+ {/* Show more indicator */}
+ {events.length > 8 && (
+
+
+ +{events.length - 8} more events
+
+
+ )}
+
+
+
+ );
+}
+
/** Strip common email domains for cleaner display */
function cleanEmail(email: string): string {
return email.replace(/@(gmail|yahoo|hotmail|outlook|icloud)\.com$/i, '');
@@ -64,6 +216,9 @@ export function AccountFlowViz({ providerData, onBack }: AccountFlowVizProps) {
const maxRequests = Math.max(...accounts.map((a) => a.successCount + a.failureCount), 1);
const totalRequests = accounts.reduce((acc, a) => acc + a.successCount + a.failureCount, 0);
+ // Generate connection events for timeline
+ const connectionEvents = useMemo(() => generateConnectionEvents(accounts), [accounts]);
+
// Calculate SVG paths for bezier curves
const calculatePaths = useCallback(() => {
if (!containerRef.current || !svgRef.current) return;
@@ -116,233 +271,246 @@ export function AccountFlowViz({ providerData, onBack }: AccountFlowVizProps) {
const providerColor = PROVIDER_COLORS[providerData.provider.toLowerCase()] || '#6b7280';
return (
-
+
{/* Back button */}
{onBack && (
)}
- {/* Main visualization area */}
-
- {/* SVG Canvas (Background) */}
-