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";2122232425262728293031const FAILURES: ReadonlySet<SyncOutcome> = new Set<SyncOutcome>([32 "no_eligible_host",33 "runner_unavailable",34 "private_relay_unset",35 "vault_locked",36 "vault_id_mismatch",37 "run_failed",38]);394041424344const WARNINGS: ReadonlySet<SyncOutcome> = new Set<SyncOutcome>([45 "relay_unreachable",46 "handshake_failed",47 "sync_timeout",48]);4950export function isSyncFailure(outcome: SyncOutcome): boolean {51 return FAILURES.has(outcome);52}5354export function isSyncWarning(outcome: SyncOutcome): boolean {55 return WARNINGS.has(outcome);56}5758export interface SyncOutcomeSummary {59 status?: "idle" | "syncing" | "error";60 outcome?: SyncOutcome;61 entriesSent?: number;62 entriesReceived?: number;63 detail?: string;64 65 finishedAt?: number;66}67686970717273747576export type SyncIndicator = "busy" | "ok" | "warn" | "error" | "idle";7778export function syncIndicator(s: SyncOutcomeSummary, pairedCount: number): SyncIndicator {79 if (s.status === "syncing") return "busy";80 if (s.outcome && isSyncFailure(s.outcome)) return "error";81 if (s.outcome && isSyncWarning(s.outcome)) return "warn";82 if (s.outcome === "completed_changes" || s.outcome === "completed_no_changes") {83 return "ok";84 }85 86 87 if (pairedCount > 0 && s.outcome === undefined) return "ok";88 return "idle";89}9091const INDICATOR_LABEL: Record<SyncIndicator, string> = {92 busy: "Syncing",93 ok: "Sync healthy",94 warn: "Last sync attempt missed — will retry",95 error: "Sync problem",96 idle: "Not syncing",97};9899100export function syncIndicatorLabel(101 indicator: SyncIndicator,102 s: SyncOutcomeSummary,103): string {104 return `${INDICATOR_LABEL[indicator]} — ${describeSyncOutcome(s)}`;105}106107const COPY: Record<SyncOutcome, string> = {108 completed_changes: "Synced",109 completed_no_changes: "Up to date — nothing to send or receive",110 no_paired_devices: "No devices paired yet",111 no_eligible_host: "Paired, but no device here can accept a sync",112 runner_unavailable: "Sync isn't running in the background — reload Peach",113 sync_disabled: "Sync is switched off",114 private_relay_unset: "Private relay chosen but no address set",115 vault_locked: "Vault was locked — unlock and sync again",116 relay_unreachable: "Couldn't reach the relay",117 handshake_failed: "Reached the relay, but the device didn't answer",118 vault_id_mismatch: "That device holds a different vault",119 sync_timeout: "The device stopped responding mid-sync",120 run_failed: "Sync failed",121};122123124125126127128129export function describeSyncOutcome(s: SyncOutcomeSummary): string {130 if (!s.outcome) return "Not synced yet";131 const base = COPY[s.outcome] ?? "Sync failed";132 if (s.outcome === "completed_changes") {133 const moved = [134 (s.entriesSent ?? 0) > 0 ? `${s.entriesSent} sent` : null,135 (s.entriesReceived ?? 0) > 0 ? `${s.entriesReceived} received` : null,136 ].filter(Boolean);137 return moved.length > 0 ? `${base} — ${moved.join(", ")}` : base;138 }139 return s.detail ? `${base} (${s.detail})` : base;140}