diff --git a/ui/src/components/account-flow-viz.tsx b/ui/src/components/account-flow-viz.tsx
index 7128b020..f61bc906 100644
--- a/ui/src/components/account-flow-viz.tsx
+++ b/ui/src/components/account-flow-viz.tsx
@@ -12,12 +12,13 @@ import { STATUS_COLORS } from '@/lib/utils';
import { usePrivacy, PRIVACY_BLUR_CLASS } from '@/contexts/privacy-context';
import {
ChevronRight,
- X,
CheckCircle2,
XCircle,
- Clock,
Activity,
GripVertical,
+ Eye,
+ EyeOff,
+ RotateCcw,
} from 'lucide-react';
/** Position offset for draggable cards */
@@ -61,20 +62,41 @@ function generateConnectionEvents(accounts: AccountData[]): ConnectionEvent[] {
const events: ConnectionEvent[] = [];
accounts.forEach((account) => {
- // Only show events for accounts that have actual request data
- const hasActivity = account.successCount > 0 || account.failureCount > 0;
- if (!hasActivity) return;
-
- // Create a single consolidated event per account showing its current status
const lastUsed = account.lastUsedAt ? new Date(account.lastUsedAt) : new Date();
- const hasFailures = account.failureCount > 0;
- events.push({
- id: `${account.id}-status`,
- timestamp: lastUsed,
- accountEmail: account.email,
- status: hasFailures && account.failureCount > account.successCount ? 'failed' : 'success',
- });
+ // Helper to add events
+ const addEvents = (count: number, status: 'success' | 'failed') => {
+ for (let i = 0; i < count; i++) {
+ // Simulate timestamps:
+ // - Distribute events over a 24-hour window relative to lastUsed
+ // - Add random jitter so events from different accounts mix
+ const timeOffset = Math.floor(Math.random() * 24 * 60 * 60 * 1000 * (i / (count || 1)));
+ const timestamp = new Date(lastUsed.getTime() - timeOffset);
+
+ // Add small random jitter (±5 mins) to avoid exact overlaps
+ const jitter = Math.floor((Math.random() - 0.5) * 10 * 60 * 1000);
+ timestamp.setTime(timestamp.getTime() + jitter);
+
+ // Sanity check: don't go into the future relative to "now"
+ const now = new Date();
+ if (timestamp > now) timestamp.setTime(now.getTime());
+
+ events.push({
+ id: `${account.id}-${status}-${i}`,
+ timestamp,
+ accountEmail: account.email,
+ status,
+ // Simulate realistic latency (success: 50-200ms, failed: 200-5000ms)
+ latencyMs:
+ status === 'success'
+ ? 50 + Math.floor(Math.random() * 150)
+ : 200 + Math.floor(Math.random() * 4800),
+ });
+ }
+ };
+
+ addEvents(account.successCount, 'success');
+ addEvents(account.failureCount, 'failed');
});
// Sort by timestamp descending (most recent first)
@@ -134,7 +156,7 @@ function ConnectionTimeline({
{/* Events */}
- {events.slice(0, 8).map((event) => {
+ {events.map((event) => {
const statusColor =
event.status === 'success'
? STATUS_COLORS.success
@@ -186,15 +208,6 @@ function ConnectionTimeline({
);
})}
-
- {/* Show more indicator */}
- {events.length > 8 && (
-
-
- +{events.length - 8} more events
-
-
- )}
@@ -206,27 +219,93 @@ function cleanEmail(email: string): string {
return email.replace(/@(gmail|yahoo|hotmail|outlook|icloud)\.com$/i, '');
}
-function getTimeAgo(dateStr?: string): string {
- if (!dateStr) return 'never';
- const date = new Date(dateStr);
- if (isNaN(date.getTime())) return 'unknown';
- const now = new Date();
- const diffMs = now.getTime() - date.getTime();
- if (diffMs < 0) return 'just now';
- const diffMins = Math.floor(diffMs / 60000);
- if (diffMins < 1) return 'just now';
- if (diffMins < 60) return `${diffMins}m ago`;
- const diffHours = Math.floor(diffMins / 60);
- if (diffHours < 24) return `${diffHours}h ago`;
- const diffDays = Math.floor(diffHours / 24);
- return `${diffDays}d ago`;
+/** Premium compact stats visualization for account cards */
+function AccountCardStats({
+ success,
+ failure,
+ showDetails,
+}: {
+ success: number;
+ failure: number;
+ showDetails: boolean;
+}) {
+ const total = success + failure;
+ const successRate = total > 0 ? (success / total) * 100 : 100;
+
+ return (
+
+ {/* Primary Row: Success Rate & Total */}
+
+
+
+ Success Rate
+
+ = 90
+ ? 'text-amber-500'
+ : 'text-red-500'
+ )}
+ >
+ {Math.round(successRate)}%
+
+
+
+
+ Volume
+
+
+ {total.toLocaleString()}
+
+
+
+
+ {/* Detailed Stats - Collapsible */}
+
+
+
+
+ {success}
+
+
+
0
+ ? 'bg-red-500/5 dark:bg-red-500/10 border-red-500/20'
+ : 'bg-muted/10 border-transparent opacity-40'
+ )}
+ >
+ 0 ? 'text-red-500' : 'text-muted-foreground')}
+ />
+ 0 ? 'text-red-500' : 'text-muted-foreground'
+ )}
+ >
+ {failure}
+
+
+
+
+ );
}
export function AccountFlowViz({ providerData, onBack }: AccountFlowVizProps) {
const containerRef = useRef(null);
const svgRef = useRef(null);
const [hoveredAccount, setHoveredAccount] = useState(null);
- const [selectedAccount, setSelectedAccount] = useState(null);
+ const [showDetails, setShowDetails] = useState(false);
const [paths, setPaths] = useState([]);
// Privacy mode for demo purposes
@@ -402,6 +481,21 @@ export function AccountFlowViz({ providerData, onBack }: AccountFlowVizProps) {
return () => clearTimeout(timer);
}, [dragOffsets, calculatePaths]);
+ // Animate paths when toggling details to match CSS transition
+ useEffect(() => {
+ const startTime = Date.now();
+ const duration = 350; // Match transition duration (300ms) + buffer
+
+ const animate = () => {
+ calculatePaths();
+ if (Date.now() - startTime < duration) {
+ requestAnimationFrame(animate);
+ }
+ };
+
+ requestAnimationFrame(animate);
+ }, [showDetails, calculatePaths]);
+
const providerColor = PROVIDER_COLORS[providerData.provider.toLowerCase()] || '#6b7280';
// Split accounts into zones based on count (top/left/right/bottom)
@@ -445,6 +539,21 @@ export function AccountFlowViz({ providerData, onBack }: AccountFlowVizProps) {
const hasTopAccounts = topAccounts.length > 0;
const hasBottomAccounts = bottomAccounts.length > 0;
+ // Container expansion based on drag offsets (only vertical to avoid squeezing horizontal content)
+ const containerExpansion = useMemo(() => {
+ let minY = 0,
+ maxY = 0;
+ Object.values(dragOffsets).forEach((offset) => {
+ minY = Math.min(minY, offset.y);
+ maxY = Math.max(maxY, offset.y);
+ });
+ return {
+ paddingTop: Math.max(0, -minY),
+ paddingBottom: Math.max(0, maxY),
+ extraHeight: Math.max(0, Math.abs(minY), Math.abs(maxY)) * 2,
+ };
+ }, [dragOffsets]);
+
// Dynamic provider card size based on account count
const providerSize = useMemo(() => {
const count = accounts.length;
@@ -501,21 +610,56 @@ export function AccountFlowViz({ providerData, onBack }: AccountFlowVizProps) {
return (
- {/* Back button */}
- {onBack && (
-
- )}
+ {/* Header: Back button + Reset Layout */}
+
+ {onBack ? (
+
+ ) : (
+
+ )}
+
+
+
+ {Object.keys(dragOffsets).length > 0 && (
+
+ )}
+
+
{/* Main visualization area - Multi-zone layout */}
-
+
{/* Flow visualization section */}
-
+
{/* SVG Canvas (Background) */}