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
↓ peach-browser-5d22740c.tar.gzatlas at this save
files changed2 files
src/popup/App.tsx+57 −1src/popup/styles/popup.css+35 −0src/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()}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;