/** * Memory sync card list (#1136 — simplified rewrite). * * Renders one card per `source_kind` (data-source type) that has chunks * in the memory tree. Counts come straight from a SQL aggregate over * `mem_tree_chunks` so the snapshot is always exact at the moment of * the poll. No phases, no settings, no per-connection state — chunks * exist or they don't. */ import { useCallback, useEffect, useState } from 'react'; import { type FreshnessLabel, type MemorySyncStatus, memorySyncStatusList, } from '../../services/memorySyncService'; interface MemorySyncConnectionsProps { /** Optional pollIntervalMs — when set, the list refetches periodically. */ pollIntervalMs?: number; } const FRESHNESS_LABEL: Record = { active: 'Active', recent: 'Recent', idle: 'Idle', }; const PROVIDER_LABEL: Record = { slack: 'Slack', discord: 'Discord', telegram: 'Telegram', whatsapp: 'WhatsApp', gmail: 'Gmail', other_email: 'Email', notion: 'Notion', meeting_notes: 'Meeting notes', drive_docs: 'Drive docs', // category fallbacks (for chunks without a `:` prefix in source_id) chat: 'Chat', email: 'Email', document: 'Document', }; function freshnessBadgeClass(label: FreshnessLabel): string { switch (label) { case 'active': return 'bg-ocean-100 text-ocean-700'; case 'recent': return 'bg-sage-100 text-sage-700'; case 'idle': return 'bg-stone-100 text-stone-700'; } } function relativeTimestamp(epochMs: number | null): string | null { if (epochMs === null) return null; const delta = Date.now() - epochMs; if (delta < 1000) return 'just now'; const seconds = Math.floor(delta / 1000); if (seconds < 60) return `${seconds}s ago`; const minutes = Math.floor(seconds / 60); if (minutes < 60) return `${minutes}m ago`; const hours = Math.floor(minutes / 60); if (hours < 24) return `${hours}h ago`; const days = Math.floor(hours / 24); return `${days}d ago`; } interface SourceCardProps { status: MemorySyncStatus; } function SourceCard({ status }: SourceCardProps) { const label = PROVIDER_LABEL[status.provider] ?? status.provider; const lastSync = relativeTimestamp(status.last_chunk_at_ms); const lifetime = status.chunks_synced; const pending = status.chunks_pending; // Progress reflects the *active sync wave* (chunks within the most // recent ingest cluster), not lifetime, so the bar tracks "how much // of this sync's ingest has been processed". Hidden once the wave // is fully drained. const batchTotal = status.batch_total; const batchProcessed = status.batch_processed; const batchPending = batchTotal - batchProcessed; const pct = batchTotal > 0 ? Math.round((batchProcessed / batchTotal) * 100) : 0; const showProgress = batchTotal > 0 && batchPending > 0; return (
{label} {FRESHNESS_LABEL[status.freshness]}
{lifetime.toLocaleString()} chunks {lastSync && Last chunk {lastSync}}
{showProgress && (
{batchProcessed.toLocaleString()} of {batchTotal.toLocaleString()} processed {pending > 0 && ( · {pending.toLocaleString()} pending )}
)}
); } export function MemorySyncConnections({ pollIntervalMs }: MemorySyncConnectionsProps) { const [statuses, setStatuses] = useState([]); const [loadError, setLoadError] = useState(null); const [loading, setLoading] = useState(true); const loadStatuses = useCallback(async () => { try { console.debug('[ui-flow][memory-sync] fetching status list'); const list = await memorySyncStatusList(); setStatuses(list); setLoadError(null); } catch (err) { const message = err instanceof Error ? err.message : String(err); console.error('[ui-flow][memory-sync] status list failed', message); setLoadError(message); } finally { setLoading(false); } }, []); useEffect(() => { let cancelled = false; void (async () => { await loadStatuses(); if (cancelled) return; })(); return () => { cancelled = true; }; }, [loadStatuses]); useEffect(() => { if (!pollIntervalMs) return undefined; const id = setInterval(() => { void loadStatuses(); }, pollIntervalMs); return () => clearInterval(id); }, [pollIntervalMs, loadStatuses]); if (loading) { return (

Memory sources

Loading…

); } if (loadError) { return (

Memory sources

Failed to load sync status: {loadError}

); } if (statuses.length === 0) { return (

Memory sources

No content has been synced into memory yet. Connect an integration to start.

); } return (

Memory sources

{statuses.map(s => ( ))}
); }