import type { SyncState } from './shared'; import { useEffect, useState } from 'react'; import { AlertTriangle, Check, RefreshCw } from 'lucide-react'; import { toneText } from '@/components/Data/tone'; import { CHAIN_SYNC_TONE } from './shared'; // How old the numbers on screen are. // // This exists because "empty wallet" and "we could not reach the chain" used to render identically — as a // zero balance — and the second one is alarming while the first is not. The sidecar now serves the last // snapshot it managed to read and refreshes behind the response, so the number is almost always real; what // the owner needs is its age, and a plain statement when the upstream is failing. // // Nothing here is a control. Refreshing is the data layer's job and it is already happening. /** Re-render on a timer so "2 minutes ago" does not sit frozen on an idle screen. */ const TICK_MS = 15_000; function relativeAge(syncedAt: number, now: number): string { const seconds = Math.max(0, Math.round((now - syncedAt) / 1000)); if (seconds < 45) return 'just now'; const minutes = Math.round(seconds / 60); if (minutes < 60) return `${minutes}m ago`; const hours = Math.round(minutes / 60); if (hours < 24) return `${hours}h ago`; return `${Math.round(hours / 24)}d ago`; } type SyncBadgeProps = { sync: SyncState | null; className?: string }; export const SyncBadge = ({ sync, className }: SyncBadgeProps) => { const [now, setNow] = useState(() => Date.now()); useEffect(() => { const timer = setInterval(() => setNow(Date.now()), TICK_MS); return () => clearInterval(timer); }, []); // Null from a node backend, which reads live and has no age to report. Rendering "just now" for it // would be inventing a fact. if (!sync) return null; // A rescan outranks everything else here, including a stale age and an upstream error. While the node // is searching, the number beside this badge is not "your balance" — it is "your balance so far", and // that difference is the entire reason an imported wallet showing zero is alarming rather than boring. const { rescan } = sync; if (rescan?.running) { return ( Scanning the chain · {Math.min(rescan.done + 1, rescan.total)} of {rescan.total} ); } const failing = sync.lastError !== null; // Never read successfully AND failing: the only case where the numbers beside this are not real. Say so // loudly, because a zero here means "unknown", not "empty". if (sync.syncedAt === null) { if (!failing) return null; return ( Could not reach the chain ); } const age = relativeAge(sync.syncedAt, now); if (failing) { return ( {age} · chain unreachable ); } return ( {sync.stale ? : } {sync.stale ? `${age} · refreshing` : age} ); };