351f84bfff6e
feat: collapse sync alert toasts by type with counter + flip + dismiss-all; strip mesh diag (v4.8.0)
- time
- 2026-08-01 17:49 UTC · 2 months ago
- machine
- koh2
- parent
- de56d93b26ce
↓ peach-browser-351f84bf.tar.gzatlas at this save
files changed6 files
manifest.firefox.json+1 −1manifest.json+1 −1package.json+1 −1src/background/sync-vault-adapter.ts+0 −34src/popup/App.tsx+55 −15src/popup/styles/popup.css+23 −1manifest.firefox.json+1 −1
@@ -1,7 +1,7 @@11 {22 "manifest_version": 3,33 "name": "Peach Password Manager",4- "version": "4.7.9",4+ "version": "4.8.0",55 "description": "Zero-knowledge password manager",66 "browser_specific_settings": {77 "gecko": {manifest.json+1 −1
@@ -2,7 +2,7 @@22 "manifest_version": 3,33 "minimum_chrome_version": "116",44 "name": "Peach Password Manager",5- "version": "4.7.9",5+ "version": "4.8.0",66 "description": "Zero-knowledge password manager",77 "action": {88 "default_popup": "src/popup/index.html",package.json+1 −1
@@ -1,6 +1,6 @@11 {22 "name": "peach",3- "version": "4.7.9",3+ "version": "4.8.0",44 "description": "Zero-knowledge password manager",55 "license": "GPL-3.0-only",66 "type": "module",src/background/sync-vault-adapter.ts+0 −34
@@ -292,15 +292,6 @@292292 !issuer.signingPublicKeyHex ||293293 !/^[0-9a-f]{64}$/.test(issuer.signingPublicKeyHex)294294 ) {295- // ponytail: temporary mesh-propagation diagnostic — strip once root cause is confirmed296- console.info("[Peach diag] issuer gate rejected", {297- authenticatedPubkeyHex,298- claimedIssuerId: issuerId,299- found: !!issuer,300- foundId: issuer?.deviceId,301- trustSource: issuer?.trustSource,302- hasSigningKey: !!issuer?.signingPublicKeyHex,303- });304295 return;305296 }306297 @@ -324,31 +315,14 @@324315 );325316 body = await verifyDeviceManifest(manifest, derived.publicKeyRaw);326317 } catch (derivedError) {327- // ponytail: temporary mesh-propagation diagnostic — strip once root cause is confirmed328- console.info("[Peach diag] manifest signature rejected (stored + derived)", {329- issuerId: issuer.deviceId,330- storedError: String(storedError),331- derivedError: String(derivedError),332- });333318 return;334319 } finally {335320 masterForIssuer.fill(0);336321 }337322 }338323 if (body.issuerDeviceId !== issuer.deviceId) {339- // ponytail: temporary mesh-propagation diagnostic — strip once root cause is confirmed340- console.info("[Peach diag] issuerDeviceId mismatch", {341- body: body.issuerDeviceId,342- expected: issuer.deviceId,343- });344324 return;345325 }346- // ponytail: temporary mesh-propagation diagnostic — strip once root cause is confirmed347- console.info("[Peach diag] manifest verified", {348- issuer: issuer.deviceId,349- devices: body.devices.map((d) => ({ id: d.deviceId, relayUrl: d.relayUrl ?? null })),350- revoked: body.revoked.map((r) => r.deviceId),351- });352326 353327 const ourDeviceId = await getOrCreateDeviceId();354328 const masterKey = requireMasterKey();@@ -394,14 +368,6 @@394368 localKeys.publicKey,395369 entry,396370 );397- // ponytail: temporary mesh-propagation diagnostic — strip once root cause is confirmed398- console.info("[Peach diag] entry", {399- deviceId: entry.deviceId,400- hasRelayUrl: !!entry.relayUrl,401- revoked: await isDeviceRevoked(entry.deviceId, entry.longTermPublicKeyHex),402- existing: !!existing,403- hasMeshRoute: !!meshRoute,404- });405371 406372 // A v3 manifest from an admitted, KK-authenticated issuer proves407373 // same-vault membership and carries enough route material for the twosrc/popup/App.tsx+55 −15
@@ -159,6 +159,8 @@159159 timestamp: number;160160 }161161 const [syncAlerts, setSyncAlerts] = createSignal<SyncAlert[]>([]);162+ // Per-group flip index: which alert within a type-group is currently shown.163+ const [alertFlipIdx, setAlertFlipIdx] = createSignal<Record<string, number>>({});162164 async function loadSyncAlerts() {163165 const r = (await sendMessage({ type: "GET_SYNC_ALERTS" })) as {164166 ok: boolean;@@ -171,9 +173,27 @@171173 void sendMessage({ type: "GET_SYNC_ALERTS", payload: { consume: true } });172174 }173175 }174- function dismissSyncAlert(index: number) {175- setSyncAlerts((arr) => arr.filter((_, i) => i !== index));176+ function dismissAlertGroup(type: string) {177+ setSyncAlerts((arr) => arr.filter((a) => a.type !== type));178+ setAlertFlipIdx((m) => { const n = { ...m }; delete n[type]; return n; });176179 }180+ function flipAlert(type: string, dir: 1 | -1) {181+ const group = syncAlerts().filter((a) => a.type === type);182+ if (group.length <= 1) return;183+ setAlertFlipIdx((m) => {184+ const cur = m[type] ?? 0;185+ return { ...m, [type]: (cur + dir + group.length) % group.length };186+ });187+ }188+ /** Grouped alert types in display order, each with its alerts. */189+ const alertGroups = createMemo(() => {190+ const map = new Map<string, SyncAlert[]>();191+ for (const a of syncAlerts()) {192+ const g = map.get(a.type);193+ if (g) g.push(a); else map.set(a.type, [a]);194+ }195+ return [...map.entries()];196+ });177197 function syncAlertText(a: SyncAlert): string {178198 if (a.type === "DEVICE_REVOKED") {179199 return `${a.deviceName} revoked a paired device — review your devices in Settings`;@@ -1702,20 +1722,40 @@17021722 via Portal below at the document root so its position:fixed17031723 isn't trapped by .peach-shell's transform-based animations. */}17041724 {/* Rare device-trust alerts; normal sync changes are silent. */}1705- <Show when={syncAlerts().length > 0}>1725+ <Show when={alertGroups().length > 0}>17061726 <div class="peach-sync-alerts">1707- <For each={syncAlerts()}>1708- {(alert, i) => (1709- <div class="peach-sync-alert">1710- <span class="peach-sync-alert-text">⚠ {syncAlertText(alert)}</span>1711- <button1712- class="peach-sync-alert-dismiss"1713- type="button"1714- aria-label="Dismiss"1715- onClick={() => dismissSyncAlert(i())}1716- >×</button>1717- </div>1718- )}1727+ <For each={alertGroups()}>1728+ {([type, group]) => {1729+ const idx = () => Math.min(alertFlipIdx()[type] ?? 0, group.length - 1);1730+ return (1731+ <div class="peach-sync-alert">1732+ <Show when={group.length > 1}>1733+ <button1734+ class="peach-sync-alert-flip"1735+ type="button"1736+ aria-label="Previous"1737+ onClick={() => flipAlert(type, -1)}1738+ >‹</button>1739+ </Show>1740+ <span class="peach-sync-alert-text">⚠ {syncAlertText(group[idx()])}</span>1741+ <Show when={group.length > 1}>1742+ <span class="peach-sync-alert-count">{idx() + 1}/{group.length}</span>1743+ <button1744+ class="peach-sync-alert-flip"1745+ type="button"1746+ aria-label="Next"1747+ onClick={() => flipAlert(type, 1)}1748+ >›</button>1749+ </Show>1750+ <button1751+ class="peach-sync-alert-dismiss"1752+ type="button"1753+ aria-label={group.length > 1 ? "Dismiss all" : "Dismiss"}1754+ onClick={() => dismissAlertGroup(type)}1755+ >×</button>1756+ </div>1757+ );1758+ }}17191759 </For>17201760 </div>17211761 </Show>src/popup/styles/popup.css+23 −1
@@ -674,7 +674,7 @@674674 }675675 .peach-sync-alert {676676 display: flex;677- align-items: flex-start;677+ align-items: center;678678 gap: 8px;679679 padding: 8px 10px;680680 border-radius: 7px;@@ -687,6 +687,28 @@687687 font-size: 10px;688688 line-height: 1.45;689689 color: var(--peach-t0);690+ overflow: hidden;691+ text-overflow: ellipsis;692+ white-space: nowrap;693+}694+.peach-sync-alert-flip {695+ flex-shrink: 0;696+ background: none;697+ border: none;698+ color: var(--peach-t2);699+ font-size: 14px;700+ line-height: 1;701+ cursor: pointer;702+ padding: 0 2px;703+}704+.peach-sync-alert-flip:hover { color: var(--peach-t0); }705+.peach-sync-alert-count {706+ flex-shrink: 0;707+ font-family: "IBM Plex Mono", "SFMono-Regular", monospace;708+ font-size: 9px;709+ color: var(--peach-t2);710+ min-width: 24px;711+ text-align: center;690712 }691713 .peach-sync-alert-dismiss {692714 flex-shrink: 0;