240a2e43d433

wire lastSyncAt into GET_SYNC_STATE; rewrite renderSync paired devices + trusted networks; vault advanced dropdown; rename formatHistoryTime to formatSnapshotTime

time
2026-04-30 21:47 UTC · 6 months ago
machine
koh2
parent
aa5fb5dedb2b
2 files src/background/messages.ts+8 −3src/popup/views/Settings.tsx+119 −65
src/background/messages.ts+8 −3
@@ -637,11 +637,12 @@637637         const { getPairedDevices, getTrustedNetworks } = await import('./sync-storage');638638         const { isOnTailscale } = await import('./sync-network');639639         const { isProUser } = await import('./license');640+        const { storageGet } = await import('../shared/storage');640641         const pairedDevices = await getPairedDevices();641642         const trustedNetworks = await getTrustedNetworks();642643         const tailscale = await isOnTailscale();643644         const pro = await isProUser();644-        645+645646         let status: SyncStatus = "offline";646647         if (tailscale || trustedNetworks.some((n: { isTailscale?: boolean }) => !n.isTailscale)) {647648           if (pairedDevices.length === 0) {@@ -652,8 +653,12 @@652653         } else {653654           status = "not_trusted";654655         }655-        656-        return { ok: true, data: { pairedDevices, trustedNetworks, status, isPro: pro } };656+657+        const lastSyncRaw = await storageGet("peach_last_sync_at");658+        const lastSyncParsed = lastSyncRaw ? Number.parseInt(lastSyncRaw, 10) : NaN;659+        const lastSyncAt = Number.isFinite(lastSyncParsed) ? lastSyncParsed : undefined;660+661+        return { ok: true, data: { pairedDevices, trustedNetworks, status, lastSyncAt, isPro: pro } };657662       }658663       case "TRIGGER_SYNC": {659664         const gate = await requirePro("sync");

view file

src/popup/views/Settings.tsx+119 −65
@@ -60,7 +60,7 @@6060   onExport: () => void;6161   onBreachCheckDone: () => void;6262   onOpenHealth?: () => void;63-  onOpenSyncPairing?: (mode: "initiate" | "accept") => void;63+  onOpenSyncPairing?: () => void;6464   onOpenFileImport?: () => void;6565   onOpenPanic?: () => void;6666 }@@ -171,7 +171,7 @@171171     }172172   }173173 174-  function formatHistoryTime(savedAt: number): string {174+  function formatSnapshotTime(savedAt: number): string {175175     const diff = Date.now() - savedAt;176176     if (diff < 60_000) return "Just now";177177     if (diff < 3_600_000) return `${Math.floor(diff / 60_000)}m ago`;@@ -196,6 +196,8 @@196196     )[reason] ?? reason;197197   }198198 199+  const [showVaultAdvanced, setShowVaultAdvanced] = createSignal(false);200+199201   const [showChangePw, setShowChangePw] = createSignal(false);200202   const [changePwCurrent, setChangePwCurrent] = createSignal("");201203   const [changePwNew, setChangePwNew] = createSignal("");@@ -417,6 +419,7 @@417419       const count = data?.syncedCount ?? 0;418420       setSyncFeedback({ kind: "ok", text: count > 0 ? `Synced with ${count} ${count === 1 ? "device" : "devices"}` : "Up to date" });419421       setLastSyncAt(Date.now());422+      void refreshSyncState();420423     } else {421424       const error = (res as { error?: string }).error;422425       const map: Record<string, string> = {@@ -433,16 +436,40 @@433436     }, 3500);434437   }435438 439+  async function refreshSyncState() {440+    const res = await sendMessage({ type: "GET_SYNC_STATE" });441+    const data = (res as { ok: boolean; data?: {442+      pairedDevices?: PairedDevice[];443+      trustedNetworks?: TrustedNetwork[];444+      status?: SyncStatus;445+      lastSyncAt?: number;446+    }}).data;447+    if (!data) return;448+    setPairedDevices(data.pairedDevices ?? []);449+    setTrustedNetworks(data.trustedNetworks ?? []);450+    setSyncStatus(data.status ?? "offline");451+    setLastSyncAt(data.lastSyncAt);452+  }453+436454   async function handleRemoveDevice(deviceId: string) {455+    setPairedDevices((prev) => prev.filter((d) => d.deviceId !== deviceId));437456     await sendMessage({ type: "REMOVE_PAIRED_DEVICE", payload: { deviceId } });457+    void refreshSyncState();438458   }439459 440460   async function handleRemoveNetwork(ssid?: string, bssid?: string, isTailscale?: boolean) {441461     await sendMessage({ type: "REMOVE_TRUSTED_NETWORK", payload: { ssid, bssid, isTailscale } });462+    void refreshSyncState();442463   }443464 465+  const [markingNetwork, setMarkingNetwork] = createSignal(false);466+444467   async function handleMarkNetworkTrusted() {468+    if (markingNetwork()) return;469+    setMarkingNetwork(true);445470     await sendMessage({ type: "MARK_NETWORK_TRUSTED" });471+    await refreshSyncState();472+    setMarkingNetwork(false);446473   }447474 448475   const [resetIdentityConfirm, setResetIdentityConfirm] = createSignal(false);@@ -1129,7 +1156,7 @@11291156                   <div class="peach-history-row">11301157                     <div class="peach-history-meta">11311158                       <span class="peach-history-time">1132-                        {formatHistoryTime(snap.savedAt)}1159+                        {formatSnapshotTime(snap.savedAt)}11331160                       </span>11341161                       <span class="peach-history-sub">11351162                         {[@@ -1162,40 +1189,53 @@11621189             </p>11631190           </Show>11641191 1165-          <div class="peach-danger-card">1166-            <div class="peach-danger-card-head">1167-              <span class="peach-danger-card-glyph" aria-hidden="true">⟳</span>1168-              <h3 class="peach-danger-card-title">Replace vault from backup</h3>1169-            </div>1170-            <p class="peach-danger-card-copy">1171-              Wipes the current vault and restores from a backup file.1172-              Recoverable only if your backup is good.1173-            </p>1174-            <button1175-              class="peach-danger-card-button"1176-              onClick={() => props.onOpenFileImport?.()}1177-            >1178-              Choose backup file1179-            </button>1180-          </div>1192+          <button1193+            class="peach-settings-advanced-toggle"1194+            onClick={() => setShowVaultAdvanced((v) => !v)}1195+            aria-expanded={showVaultAdvanced()}1196+          >1197+            <span class="peach-settings-sub-label" style="margin:0">ADVANCED</span>1198+            <span class="peach-settings-advanced-chevron">1199+              {showVaultAdvanced() ? "▲" : "▼"}1200+            </span>1201+          </button>11811202 1182-          <Show when={props.onOpenPanic}>1183-            <div class="peach-danger-card peach-danger-card--hazard">1203+          <Show when={showVaultAdvanced()}>1204+            <div class="peach-danger-card">11841205               <div class="peach-danger-card-head">1185-                <span class="peach-danger-card-glyph" aria-hidden="true">⚠</span>1186-                <h3 class="peach-danger-card-title">Destroy vault</h3>1206+                <span class="peach-danger-card-glyph" aria-hidden="true">⟳</span>1207+                <h3 class="peach-danger-card-title">Replace vault from backup</h3>11871208               </div>11881209               <p class="peach-danger-card-copy">1189-                Permanently wipes every entry, setting, and license on this1190-                device. There is no recovery.1210+                Wipes the current vault and restores from a backup file.1211+                Recoverable only if your backup is good.11911212               </p>11921213               <button11931214                 class="peach-danger-card-button"1194-                onClick={() => props.onOpenPanic?.()}1215+                onClick={() => props.onOpenFileImport?.()}11951216               >1196-                Begin destruction1217+                Choose backup file11971218               </button>11981219             </div>1220+1221+            <Show when={props.onOpenPanic}>1222+              <div class="peach-danger-card peach-danger-card--hazard">1223+                <div class="peach-danger-card-head">1224+                  <span class="peach-danger-card-glyph" aria-hidden="true">⚠</span>1225+                  <h3 class="peach-danger-card-title">Destroy vault</h3>1226+                </div>1227+                <p class="peach-danger-card-copy">1228+                  Permanently wipes every entry, setting, and license on this1229+                  device. There is no recovery.1230+                </p>1231+                <button1232+                  class="peach-danger-card-button"1233+                  onClick={() => props.onOpenPanic?.()}1234+                >1235+                  Begin destruction1236+                </button>1237+              </div>1238+            </Show>11991239           </Show>12001240         </div>12011241       </div>@@ -1302,64 +1342,78 @@13021342 13031343           <div class="peach-settings-row peach-settings-row--meta peach-settings-row--stack">13041344             <span class="peach-sync-section-label">Paired devices ({pairedDevices().length})</span>1305-            {pairedDevices().map((device) => (1306-              <div class="peach-sync-device-row">1307-                <div class="peach-sync-device-info">1308-                  <span class="peach-sync-device-name">{device.displayName}</span>1309-                  <span class="peach-sync-device-meta">1310-                    Last seen: {formatRelativeTime(device.lastSeenAt)} · Last synced: {formatRelativeTime(device.lastSyncAt)}1311-                  </span>1312-                </div>1313-                <button1314-                  class="peach-btn peach-btn--ghost peach-btn--sm peach-sync-remove-btn"1315-                  onClick={() => void handleRemoveDevice(device.deviceId)}1316-                >1317-                  Remove1318-                </button>1345+            <Show when={pairedDevices().length === 0}>1346+              <p class="peach-settings-hint">1347+                No paired devices yet. Add a device to start syncing.1348+              </p>1349+            </Show>1350+            <Show when={pairedDevices().length > 0}>1351+              <div class="peach-history-list">1352+                <For each={pairedDevices()}>1353+                  {(device) => (1354+                    <div class="peach-history-row">1355+                      <div class="peach-history-meta">1356+                        <span class="peach-history-time">{device.displayName}</span>1357+                        <span class="peach-history-sub">1358+                          {device.lastSyncAt1359+                            ? `Last synced: ${formatSnapshotTime(device.lastSyncAt)}`1360+                            : "Never synced"}1361+                        </span>1362+                      </div>1363+                      <button1364+                        class="peach-btn peach-btn--subtle peach-btn--sm peach-history-btn"1365+                        onClick={() => void handleRemoveDevice(device.deviceId)}1366+                      >1367+                        Remove1368+                      </button>1369+                    </div>1370+                  )}1371+                </For>13191372               </div>1320-            ))}1373+            </Show>13211374             <div class="peach-settings-actions--row">13221375               <button13231376                 class="peach-btn peach-btn--subtle peach-btn--sm"1324-                onClick={() => props.onOpenSyncPairing?.("initiate")}1377+                onClick={() => props.onOpenSyncPairing?.()}13251378               >1326-                Pair a new device1379+                Add a device13271380               </button>1328-              <button1329-                class="peach-btn peach-btn--subtle peach-btn--sm"1330-                onClick={() => props.onOpenSyncPairing?.("accept")}1331-              >1332-                Pair with another device1333-              </button>13341381             </div>13351382           </div>13361383 13371384           <div class="peach-settings-row peach-settings-row--meta peach-settings-row--stack">13381385             <span class="peach-sync-section-label">Trusted networks ({trustedNetworks().length})</span>1339-            {trustedNetworks().filter(n => !n.isTailscale).map((network) => (1340-              <div class="peach-sync-device-row">1341-                <span class="peach-sync-device-name">{network.ssid ?? "Unknown network"}</span>1342-                <button1343-                  class="peach-btn peach-btn--ghost peach-btn--sm peach-sync-remove-btn"1344-                  onClick={() => void handleRemoveNetwork(network.ssid, network.bssid, network.isTailscale)}1345-                >1346-                  Remove1347-                </button>1348-              </div>1349-            ))}1386+            <For each={trustedNetworks()}>1387+              {(network) => (1388+                <div class="peach-sync-device-row">1389+                  <span class="peach-sync-device-name">1390+                    {network.isTailscale ? "Tailscale" : (network.ssid ?? "Unknown network")}1391+                  </span>1392+                  <Show1393+                    when={!network.isTailscale}1394+                    fallback={<span class="peach-sync-device-meta">Always trusted</span>}1395+                  >1396+                    <button1397+                      class="peach-btn peach-btn--ghost peach-btn--sm peach-sync-remove-btn"1398+                      onClick={() => void handleRemoveNetwork(network.ssid, network.bssid, network.isTailscale)}1399+                    >1400+                      Remove1401+                    </button>1402+                  </Show>1403+                </div>1404+              )}1405+            </For>13501406             <div class="peach-settings-actions--row">13511407               <Show when={!isCurrentNetworkTrusted()}>13521408                 <button13531409                   class="peach-btn peach-btn--subtle peach-btn--sm"13541410                   onClick={() => void handleMarkNetworkTrusted()}1411+                  disabled={markingNetwork()}13551412                 >1356-                  Mark current network as trusted1413+                  {markingNetwork() ? "Marking…" : "Mark current network as trusted"}13571414                 </button>13581415               </Show>13591416             </div>1360-            <span class="peach-sync-device-meta">1361-              Tailscale is always trusted1362-            </span>13631417           </div>13641418 13651419           <div class="peach-settings-row peach-settings-row--meta peach-settings-row--stack">

view file