351f84bfff6e

feat: collapse sync alert toasts by type with counter + flip + dismiss-all; strip mesh diag (v4.8.0)

Mesh propagation confirmed working: Firefox now appears in Chrome's paired-devices list after v4.7.4+v4.7.8+v4.7.9 fixed three layers of the wsPort null/zero validation gap. Toast UX overhaul: alerts grouped by type (DEVICE_REVOKED / DEVICE_KEY_ROTATED). Each group shows ONE compact one-line toast with ‹ › flip arrows (when count > 1), current/total counter, and × dismiss-all. At most 2 toasts visible instead of N cards flooding the popup. Also stripped all 5 [Peach diag] console.info lines from applyVerifiedPeerManifest — mesh propagation confirmed, diagnostics retired. Gates: tsc RC=0, adapter+manifest 46/46, build clean.

time
2026-08-01 17:49 UTC · 2 months ago
machine
koh2
parent
de56d93b26ce
6 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 −1
manifest.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": {

view file

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",

view file

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",

view file

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 two

view file

src/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>

view file

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;

view file