5bc0f257164c
Settings — Vault History group (10 snapshots, restore + lock) + Sync Now feedback states (v2.14.8)
- time
- 2026-04-30 20:58 UTC · 6 months ago
- machine
- koh2
- parent
- 2b121cc936d6
↓ peach-browser-5bc0f257.tar.gzatlas at this save
files changed5 files
manifest.firefox.json+1 −1manifest.json+1 −1package.json+1 −1src/popup/styles/popup.css+69 −0src/popup/views/Settings.tsx+160 −5manifest.firefox.json+1 −1
@@ -1,7 +1,7 @@11 {22 "manifest_version": 3,33 "name": "Peach",4- "version": "2.14.7",4+ "version": "2.14.8",55 "description": "Zero-knowledge password manager",66 "browser_specific_settings": {77 "gecko": {manifest.json+1 −1
@@ -1,7 +1,7 @@11 {22 "manifest_version": 3,33 "name": "Peach",4- "version": "2.14.7",4+ "version": "2.14.8",55 "description": "Zero-knowledge password manager",66 "action": {77 "default_popup": "src/popup/index.html",package.json+1 −1
@@ -1,6 +1,6 @@11 {22 "name": "peach",3- "version": "2.14.7",3+ "version": "2.14.8",44 "description": "Zero-knowledge password manager",55 "type": "module",66 "scripts": {src/popup/styles/popup.css+69 −0
@@ -6964,3 +6964,72 @@69646964 color: #9a8b7a;69656965 font-size: 14px;69666966 }6967+6968+/* Vault History — Settings → Vault sub-view */6969+6970+.peach-history-list {6971+ border: 1px solid var(--peach-border);6972+ border-radius: 8px;6973+ overflow: hidden;6974+ margin: 8px 0 4px;6975+}6976+6977+.peach-history-row {6978+ display: flex;6979+ align-items: center;6980+ justify-content: space-between;6981+ padding: 9px 12px;6982+ gap: 10px;6983+ border-bottom: 1px solid var(--peach-border);6984+}6985+6986+.peach-history-row:last-child {6987+ border-bottom: none;6988+}6989+6990+.peach-history-meta {6991+ display: flex;6992+ flex-direction: column;6993+ gap: 2px;6994+ min-width: 0;6995+ flex: 1;6996+}6997+6998+.peach-history-time {6999+ font-size: 12px;7000+ font-family: 'IBM Plex Mono', ui-monospace, monospace;7001+ color: var(--peach-t0);7002+ white-space: nowrap;7003+ overflow: hidden;7004+ text-overflow: ellipsis;7005+}7006+7007+.peach-history-sub {7008+ font-size: 10px;7009+ color: var(--peach-t3);7010+ letter-spacing: 0.06em;7011+ text-transform: uppercase;7012+ white-space: nowrap;7013+ overflow: hidden;7014+ text-overflow: ellipsis;7015+}7016+7017+.peach-history-btn {7018+ flex-shrink: 0;7019+ white-space: nowrap;7020+ min-width: 56px;7021+ text-align: center;7022+}7023+7024+.peach-history-btn.is-busy {7025+ opacity: 0.65;7026+}7027+7028+.peach-history-footer {7029+ font-size: 10px;7030+ color: var(--peach-t3);7031+ letter-spacing: 0.05em;7032+ text-align: right;7033+ margin: 0;7034+ padding: 2px 0 0;7035+}src/popup/views/Settings.tsx+160 −5
@@ -6,7 +6,12 @@66 import { biometricAvailable } from "../../shared/biometric";77 import type { Canary } from "../../shared/types";88 import { ACCENT_COLORS } from "../../shared/types";9-import type { AccentColorName, PasskeyProviderState, ProFeature } from "../../shared/types";9+import type {10+ AccentColorName,11+ PasskeyProviderState,12+ ProFeature,13+ VaultSnapshot,14+} from "../../shared/types";1015 import type { PairedDevice, TrustedNetwork, SyncStatus } from "../../shared/sync-types";1116 import {1217 DEFAULT_BINDINGS,@@ -121,6 +126,9 @@121126 const [syncProStatus, setSyncProStatus] = createSignal(false);122127 const isPro = () => props.isPro ?? syncProStatus();123128 const [syncingNow, setSyncingNow] = createSignal(false);129+ const [syncFeedback, setSyncFeedback] = createSignal<130+ { kind: "ok" | "err"; text: string } | null131+ >(null);124132 const [passkeyState, setPasskeyState] = createSignal<PasskeyProviderState | null>(null);125133 const [passkeySaving, setPasskeySaving] = createSignal(false);126134 @@ -130,6 +138,64 @@130138 131139 const [declinedCardCount, setDeclinedCardCount] = createSignal(0);132140 141+ // Vault history (peach_vault_history) — last 10 snapshots, snapshotted142+ // BEFORE every real-mode vault write so the user can roll back if a sync143+ // or import overwrites the wrong state.144+ const [historySnapshots, setHistorySnapshots] = createSignal<VaultSnapshot[]>([]);145+ const [historyLoading, setHistoryLoading] = createSignal(false);146+ const [historyRestoring, setHistoryRestoring] = createSignal<string | null>(null);147+148+ async function loadVaultHistory() {149+ setHistoryLoading(true);150+ try {151+ const res = await sendMessage({ type: "GET_VAULT_HISTORY" });152+ const list = (res as { ok: boolean; data?: VaultSnapshot[] }).data ?? [];153+ setHistorySnapshots(list);154+ } finally {155+ setHistoryLoading(false);156+ }157+ }158+159+ async function handleRestoreVaultSnapshot(id: string) {160+ if (historyRestoring()) return;161+ setHistoryRestoring(id);162+ const res = await sendMessage({163+ type: "RESTORE_VAULT_SNAPSHOT",164+ payload: { snapshotId: id },165+ });166+ setHistoryRestoring(null);167+ if ((res as { ok: boolean }).ok) {168+ // Vault is now locked — let the parent close Settings; App.tsx's169+ // VAULT_LOCKED listener will route back to the lock screen.170+ props.onBack();171+ }172+ }173+174+ function formatHistoryTime(savedAt: number): string {175+ const diff = Date.now() - savedAt;176+ if (diff < 60_000) return "Just now";177+ if (diff < 3_600_000) return `${Math.floor(diff / 60_000)}m ago`;178+ const d = new Date(savedAt);179+ const isToday = new Date().toDateString() === d.toDateString();180+ const isYest =181+ new Date(Date.now() - 86_400_000).toDateString() === d.toDateString();182+ const t = d.toLocaleTimeString([], { hour: "numeric", minute: "2-digit" });183+ if (isToday) return `Today ${t}`;184+ if (isYest) return `Yesterday ${t}`;185+ return `${d.toLocaleDateString([], { month: "short", day: "numeric" })} ${t}`;186+ }187+188+ function reasonLabel(reason: VaultSnapshot["reason"]): string {189+ return (190+ {191+ local_save: "saved",192+ sync_received: "synced",193+ import: "imported",194+ restore: "restored",195+ } as const196+ )[reason] ?? reason;197+ }198+133199 const [showChangePw, setShowChangePw] = createSignal(false);134200 const [changePwCurrent, setChangePwCurrent] = createSignal("");135201 const [changePwNew, setChangePwNew] = createSignal("");@@ -182,6 +248,10 @@182248 sendMessage({ type: "GET_LICENSE_KEY" }),183249 ]);184250 251+ // Vault history loads in the background — the Vault sub-view picks it up252+ // when the user navigates in. Loading it lazily keeps Settings boot fast.253+ void loadVaultHistory();254+185255 if ((licenseRes as { ok: boolean }).ok) {186256 setLicenseInfo((licenseRes as { ok: boolean; data?: LicenseInfo }).data ?? null);187257 }@@ -339,8 +409,28 @@339409 async function handleSyncNow() {340410 if (syncStatus() === "not_trusted") return;341411 setSyncingNow(true);342- await sendMessage({ type: "TRIGGER_SYNC" });343- setTimeout(() => setSyncingNow(false), 3000);412+ setSyncFeedback(null);413+ const res = await sendMessage({ type: "TRIGGER_SYNC" });414+ const ok = (res as { ok: boolean }).ok;415+ if (ok) {416+ const data = (res as { data?: { syncedCount?: number; totalReachable?: number } }).data;417+ const count = data?.syncedCount ?? 0;418+ setSyncFeedback({ kind: "ok", text: count > 0 ? `Synced with ${count} ${count === 1 ? "device" : "devices"}` : "Up to date" });419+ setLastSyncAt(Date.now());420+ } else {421+ const error = (res as { error?: string }).error;422+ const map: Record<string, string> = {423+ no_paired_devices: "No paired devices",424+ no_devices_reachable: "No devices found",425+ vault_locked: "Vault is locked",426+ pro_required: "Sync requires Pro",427+ };428+ setSyncFeedback({ kind: "err", text: map[error ?? ""] ?? "Sync failed" });429+ }430+ setTimeout(() => {431+ setSyncingNow(false);432+ setSyncFeedback(null);433+ }, 3500);344434 }345435 346436 async function handleRemoveDevice(deviceId: string) {@@ -1017,6 +1107,61 @@10171107 open10181108 </button>10191109 </div>1110+1111+ <p class="peach-settings-sub-label">VAULT HISTORY</p>1112+ <Show when={historyLoading()}>1113+ <p class="peach-settings-hint">Loading…</p>1114+ </Show>1115+ <Show when={!historyLoading() && historySnapshots().length === 0}>1116+ <p class="peach-settings-hint">1117+ No snapshots yet. Peach saves automatically before every change —1118+ saves, syncs, and imports.1119+ </p>1120+ </Show>1121+ <Show when={!historyLoading() && historySnapshots().length > 0}>1122+ <p class="peach-settings-hint">1123+ Restoring locks your vault. Unlock with the password that1124+ encrypted that snapshot to confirm.1125+ </p>1126+ <div class="peach-history-list">1127+ <For each={historySnapshots()}>1128+ {(snap) => (1129+ <div class="peach-history-row">1130+ <div class="peach-history-meta">1131+ <span class="peach-history-time">1132+ {formatHistoryTime(snap.savedAt)}1133+ </span>1134+ <span class="peach-history-sub">1135+ {[1136+ snap.entryCount !== null1137+ ? `${snap.entryCount} ${snap.entryCount === 1 ? "entry" : "entries"}`1138+ : null,1139+ reasonLabel(snap.reason),1140+ ]1141+ .filter(Boolean)1142+ .join(" · ")}1143+ </span>1144+ </div>1145+ <button1146+ class={`peach-btn peach-btn--subtle peach-btn--sm peach-history-btn${1147+ historyRestoring() === snap.id ? " is-busy" : ""1148+ }`}1149+ onClick={() => void handleRestoreVaultSnapshot(snap.id)}1150+ disabled={historyRestoring() !== null}1151+ >1152+ <Show when={historyRestoring() === snap.id} fallback="Restore">1153+ …1154+ </Show>1155+ </button>1156+ </div>1157+ )}1158+ </For>1159+ </div>1160+ <p class="peach-history-footer">1161+ {historySnapshots().length} of 10 snapshots1162+ </p>1163+ </Show>1164+10201165 <div class="peach-danger-card">10211166 <div class="peach-danger-card-head">10221167 <span class="peach-danger-card-glyph" aria-hidden="true">⟳</span>@@ -1139,11 +1284,21 @@11391284 <button11401285 class="peach-btn peach-btn--subtle peach-btn--sm"11411286 onClick={() => void handleSyncNow()}1142- disabled={syncStatus() === "not_trusted" || syncStatus() === "syncing"}1287+ disabled={syncStatus() === "not_trusted" || syncingNow()}11431288 >1144- {syncingNow() ? "Syncing..." : "Sync now"}1289+ <Switch>1290+ <Match when={syncingNow()}>Syncing…</Match>1291+ <Match when={syncFeedback()?.kind === "ok"}>✓ {syncFeedback()!.text}</Match>1292+ <Match when={syncFeedback()?.kind === "err"}>{syncFeedback()!.text}</Match>1293+ <Match when={true}>Sync now</Match>1294+ </Switch>11451295 </button>11461296 </div>1297+ <p class="peach-settings-hint">1298+ If you edit on multiple devices before syncing, the most recently1299+ changed device wins. Use Vault History above to recover older1300+ versions if needed.1301+ </p>11471302 11481303 <div class="peach-settings-row peach-settings-row--meta peach-settings-row--stack">11491304 <span class="peach-sync-section-label">Paired devices ({pairedDevices().length})</span>