5d22740c27d6

feat: UI-1 sync alert banners — popup fetches GET_SYNC_ALERTS on open, renders dismissible REMOTE_PASSWORD_CHANGE/REMOTE_DELETE/DEVICE_KEY_ROTATED banners, acks with consume

time
2026-06-10 03:43 UTC · 4 months ago
machine
koh2
agent
claude-opus-4-8
session
audit fix integration
parent
800e374c624b
2 files src/popup/App.tsx+57 −1src/popup/styles/popup.css+35 −0
src/popup/App.tsx+57 −1
@@ -1,4 +1,4 @@1-import { createEffect, createMemo, createSignal, lazy, onCleanup, onMount, Show, Suspense } from "solid-js";1+import { createEffect, createMemo, createSignal, For, lazy, onCleanup, onMount, Show, Suspense } from "solid-js";22 import { Portal } from "solid-js/web";33 import { storageGet, storageSet } from "../shared/storage";44 import { sendMessage } from "../shared/bridge";@@ -131,6 +131,41 @@131131   const [entries, setEntries] = createSignal<VaultEntry[]>([]);132132   const [selectedEntry, setSelectedEntry] = createSignal<VaultEntry | null>(null);133133   const [editEntry, setEditEntry] = createSignal<VaultEntry | undefined>(undefined);134+135+  // UI-1 / HV-1b + MV-8d: remote-mutation alerts queued by the sync merge,136+  // shown as dismissible banners on popup open.137+  interface SyncAlert {138+    type: "REMOTE_PASSWORD_CHANGE" | "REMOTE_DELETE" | "DEVICE_KEY_ROTATED";139+    entryId: string;140+    entryTitle: string;141+    deviceName: string;142+    timestamp: number;143+  }144+  const [syncAlerts, setSyncAlerts] = createSignal<SyncAlert[]>([]);145+  async function loadSyncAlerts() {146+    const r = (await sendMessage({ type: "GET_SYNC_ALERTS" })) as {147+      ok: boolean;148+      data?: SyncAlert[];149+    };150+    if (r.ok && (r.data?.length ?? 0) > 0) {151+      setSyncAlerts(r.data ?? []);152+      // Acknowledge so a crash between read and render doesn't lose them —153+      // they remain on screen until the user dismisses each.154+      void sendMessage({ type: "GET_SYNC_ALERTS", payload: { consume: true } });155+    }156+  }157+  function dismissSyncAlert(index: number) {158+    setSyncAlerts((arr) => arr.filter((_, i) => i !== index));159+  }160+  function syncAlertText(a: SyncAlert): string {161+    if (a.type === "REMOTE_PASSWORD_CHANGE") {162+      return `${a.deviceName} changed the password for ${a.entryTitle}`;163+    }164+    if (a.type === "REMOTE_DELETE") {165+      return `${a.deviceName} deleted ${a.entryTitle}`;166+    }167+    return `${a.deviceName}'s sync key changed — if you didn't reinstall Peach on that device, rotate your sync identity`;168+  }134169   const [accentColor, setAccentColor] = createSignal<AccentColorName>("peach");135170   const [theme, setTheme] = createSignal<"dark" | "light">("dark");136171   const [textSize, setTextSize] = createSignal<"small" | "medium" | "large">("medium");@@ -334,6 +369,8 @@334369         type: "TRIGGER_SYNC",335370         payload: { reason: "foreground" },336371       });372+      // UI-1 / HV-1b: surface any remote-mutation alerts the sync merge queued.373+      void loadSyncAlerts();337374     }338375 339376     // If the user created a vault but hasn't yet confirmed their recovery@@ -1363,6 +1400,25 @@13631400             {/* Desktop: shown on demand via Show. Native sheet is rendered13641401                 via Portal below at the document root so its position:fixed13651402                 isn't trapped by .peach-shell's transform-based animations. */}1403+            {/* UI-1 / HV-1b: dismissible remote-mutation alert banners. */}1404+            <Show when={syncAlerts().length > 0}>1405+              <div class="peach-sync-alerts">1406+                <For each={syncAlerts()}>1407+                  {(alert, i) => (1408+                    <div class="peach-sync-alert">1409+                      <span class="peach-sync-alert-text">⚠ {syncAlertText(alert)}</span>1410+                      <button1411+                        class="peach-sync-alert-dismiss"1412+                        type="button"1413+                        aria-label="Dismiss"1414+                        onClick={() => dismissSyncAlert(i())}1415+                      >×</button>1416+                    </div>1417+                  )}1418+                </For>1419+              </div>1420+            </Show>1421+13661422             <Show when={!isNativePlatform() && view() === "list"}>13671423               <VaultView13681424                 entries={entries()}

view file

src/popup/styles/popup.css+35 −0
@@ -648,6 +648,41 @@648648   color: var(--peach-t2);649649   margin-bottom: 2px;650650 }651+/* UI-1 / HV-1b: dismissible sync alert banners at the top of the vault view. */652+.peach-sync-alerts {653+  display: flex;654+  flex-direction: column;655+  gap: 6px;656+  padding: 8px 12px 0;657+}658+.peach-sync-alert {659+  display: flex;660+  align-items: flex-start;661+  gap: 8px;662+  padding: 8px 10px;663+  border-radius: 7px;664+  background: color-mix(in srgb, #C8A840 14%, transparent);665+  border: 1px solid color-mix(in srgb, #C8A840 38%, transparent);666+}667+.peach-sync-alert-text {668+  flex: 1;669+  font-family: "IBM Plex Mono", "SFMono-Regular", monospace;670+  font-size: 10px;671+  line-height: 1.45;672+  color: var(--peach-t0);673+}674+.peach-sync-alert-dismiss {675+  flex-shrink: 0;676+  background: none;677+  border: none;678+  color: var(--peach-t2);679+  font-size: 14px;680+  line-height: 1;681+  cursor: pointer;682+  padding: 0 2px;683+}684+.peach-sync-alert-dismiss:hover { color: var(--peach-t0); }685+651686 /* UI-8 / LV-2: non-blocking homograph warning under the URL editor. */652687 .peach-homograph-warning {653688   margin: 4px 0 2px;

view file