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}
);
};