1234567export type SyncOutcome =8 | "completed_changes"9 | "completed_no_changes"10 | "no_paired_devices"11 | "no_eligible_host"12 | "runner_unavailable"13 | "sync_disabled"14 | "private_relay_unset"15 | "vault_locked"16 | "relay_unreachable"17 | "handshake_failed"18 | "vault_id_mismatch"19 | "sync_timeout"20 | "run_failed";212223const FAILURES: ReadonlySet<SyncOutcome> = new Set<SyncOutcome>([24 "no_eligible_host",25 "runner_unavailable",26 "private_relay_unset",27 "vault_locked",28 "relay_unreachable",29 "handshake_failed",30 "vault_id_mismatch",31 "sync_timeout",32 "run_failed",33]);3435export function isSyncFailure(outcome: SyncOutcome): boolean {36 return FAILURES.has(outcome);37}3839export interface SyncOutcomeSummary {40 status?: "idle" | "syncing" | "error";41 outcome?: SyncOutcome;42 entriesSent?: number;43 entriesReceived?: number;44 detail?: string;45 46 finishedAt?: number;47}48495051525354export type SyncIndicator = "busy" | "ok" | "error" | "idle";5556export function syncIndicator(s: SyncOutcomeSummary, pairedCount: number): SyncIndicator {57 if (s.status === "syncing") return "busy";58 if (s.outcome && isSyncFailure(s.outcome)) return "error";59 if (s.outcome === "completed_changes" || s.outcome === "completed_no_changes") {60 return "ok";61 }62 63 64 if (pairedCount > 0 && s.outcome === undefined) return "ok";65 return "idle";66}6768const INDICATOR_LABEL: Record<SyncIndicator, string> = {69 busy: "Syncing",70 ok: "Sync healthy",71 error: "Sync problem",72 idle: "Not syncing",73};747576export function syncIndicatorLabel(77 indicator: SyncIndicator,78 s: SyncOutcomeSummary,79): string {80 return `${INDICATOR_LABEL[indicator]} — ${describeSyncOutcome(s)}`;81}8283const COPY: Record<SyncOutcome, string> = {84 completed_changes: "Synced",85 completed_no_changes: "Up to date — nothing to send or receive",86 no_paired_devices: "No devices paired yet",87 no_eligible_host: "Paired, but no device here can accept a sync",88 runner_unavailable: "Sync isn't running in the background — reload Peach",89 sync_disabled: "Sync is switched off",90 private_relay_unset: "Private relay chosen but no address set",91 vault_locked: "Vault was locked — unlock and sync again",92 relay_unreachable: "Couldn't reach the relay",93 handshake_failed: "Reached the relay, but the device didn't answer",94 vault_id_mismatch: "That device holds a different vault",95 sync_timeout: "The device stopped responding mid-sync",96 run_failed: "Sync failed",97};9899100101102103104105export function describeSyncOutcome(s: SyncOutcomeSummary): string {106 if (!s.outcome) return "Not synced yet";107 const base = COPY[s.outcome] ?? "Sync failed";108 if (s.outcome === "completed_changes") {109 const moved = [110 (s.entriesSent ?? 0) > 0 ? `${s.entriesSent} sent` : null,111 (s.entriesReceived ?? 0) > 0 ? `${s.entriesReceived} received` : null,112 ].filter(Boolean);113 return moved.length > 0 ? `${base} — ${moved.join(", ")}` : base;114 }115 return s.detail ? `${base} (${s.detail})` : base;116}