From 9e635e7a93185aa930ecdbb6893c1ba38944e6a9 Mon Sep 17 00:00:00 2001 From: Avi Date: Thu, 27 Aug 2026 12:07:13 -0500 Subject: [PATCH 1/2] fix: restore profile delete/undo and themed auto-dismiss undo bar MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - ipc: add DeleteProfile/UndoDelete requests returning state_view - profiles: expose delete_profile outside tests - api: deleteProfile/undoDelete now return AppState; AppProvider uses applyState - fakeBackend: implement delete/undo handlers - ProfilesScreen: themed 'Profile deleted — Undo and restore profile' (primary color, primary-soft) with 5s auto-dismiss, shown in both empty and grid states --- frontend/src/lib/api.ts | 4 +- frontend/src/screens/ProfilesScreen.tsx | 89 ++++++++++++++++++++++--- frontend/src/state/AppProvider.tsx | 18 +++-- frontend/src/test/fakeBackend.ts | 41 ++++++++++++ src/ipc.rs | 15 +++++ src/profiles.rs | 45 ++++++------- 6 files changed, 172 insertions(+), 40 deletions(-) diff --git a/frontend/src/lib/api.ts b/frontend/src/lib/api.ts index cd8cae6..8f0f7d6 100644 --- a/frontend/src/lib/api.ts +++ b/frontend/src/lib/api.ts @@ -106,8 +106,8 @@ export const api = { signerApprove: (id: string, approved: boolean) => call('signer_approve', { id, approved }), - deleteProfile: (npub: string) => call('delete_profile', { npub }), - undoDelete: () => call('undo_delete'), + deleteProfile: (npub: string) => call('delete_profile', { npub }), + undoDelete: () => call('undo_delete'), copyText: (text: string) => window.backend.copyText(text), }; diff --git a/frontend/src/screens/ProfilesScreen.tsx b/frontend/src/screens/ProfilesScreen.tsx index 92b02d7..f74c483 100644 --- a/frontend/src/screens/ProfilesScreen.tsx +++ b/frontend/src/screens/ProfilesScreen.tsx @@ -1,4 +1,4 @@ -import { useState } from 'react'; +import { useEffect, useState } from 'react'; import { Avatar } from '../components/Avatar'; import { Badge } from '../components/Badge'; import { Button } from '../components/Button'; @@ -17,7 +17,14 @@ interface ProfilesScreenProps { } export function ProfilesScreen({ onCreateProfile }: ProfilesScreenProps) { - const { state, selectProfile, deleteProfile, undoDelete, publishProfileMetadata } = useApp(); + const { + state, + selectProfile, + deleteProfile, + undoDelete, + publishProfileMetadata, + clearLastDeleted, + } = useApp(); const [selecting, setSelecting] = useState(null); const [publishing, setPublishing] = useState(null); const [error, setError] = useState(null); @@ -33,6 +40,14 @@ export function ProfilesScreen({ onCreateProfile }: ProfilesScreenProps) { const undoHistory = state?.undo_history ?? []; const lastDeleted = undoHistory[undoHistory.length - 1] ?? null; + useEffect(() => { + if (!lastDeleted) return; + const timeout = setTimeout(() => { + clearLastDeleted(); + }, 5000); + return () => clearTimeout(timeout); + }, [lastDeleted, clearLastDeleted]); + const onPublishName = async (npub: string, label: string) => { setError(null); setNotice(null); @@ -63,19 +78,34 @@ export function ProfilesScreen({ onCreateProfile }: ProfilesScreenProps) { className="undo-bar" style={{ margin: '12px 0', - padding: '8px 12px', - background: 'var(--token-item-bg, #f0f0f0)', - borderRadius: '4px', + padding: '10px 14px', + background: 'var(--primary-soft)', + border: '1px solid var(--border)', + borderRadius: '8px', + display: 'flex', + alignItems: 'center', + gap: '8px', }} > - + Undo and restore profile + )} )} + {lastDeleted && ( +
+ + Profile “{lastDeleted.label}” deleted. + + — + +
+ )} +
{profiles.map((profile) => (
Promise; signerStatus: () => Promise; signerApprove: (id: string, approved: boolean) => Promise; - deleteProfile: (npub: string) => Promise; - undoDelete: () => Promise; + deleteProfile: (npub: string) => Promise; + undoDelete: () => Promise; + clearLastDeleted: () => void; copyText: (text: string) => Promise; } @@ -234,11 +235,18 @@ export function AppProvider({ children }: { children: ReactNode }) { return api.signerApprove(id, approved); }, []); - const deleteProfile = useCallback((npub: string) => api.deleteProfile(npub), []); - const undoDelete = useCallback(() => api.undoDelete(), []); + const deleteProfile = useCallback( + (npub: string) => applyState(api.deleteProfile(npub)), + [applyState], + ); + const undoDelete = useCallback(() => applyState(api.undoDelete()), [applyState]); const copyText = useCallback((text: string) => api.copyText(text), []); + const clearLastDeleted = useCallback(() => { + setState((prev) => (prev ? { ...prev, undo_history: prev.undo_history?.slice(0, -1) } : prev)); + }, []); + useEffect(() => { if (state?.settings.theme) { applyTheme(state.settings.theme); @@ -284,6 +292,7 @@ export function AppProvider({ children }: { children: ReactNode }) { setProfilePicture, renameProfile, setNip05, + clearLastDeleted, copyText, }), [ @@ -298,6 +307,7 @@ export function AppProvider({ children }: { children: ReactNode }) { setProfilePicture, renameProfile, setNip05, + clearLastDeleted, publishNote, deleteProfile, undoDelete, diff --git a/frontend/src/test/fakeBackend.ts b/frontend/src/test/fakeBackend.ts index cd5d0d7..ccef5e5 100644 --- a/frontend/src/test/fakeBackend.ts +++ b/frontend/src/test/fakeBackend.ts @@ -451,6 +451,47 @@ export function createFakeBackend(initial?: AppState): FakeBackend { return { hex, nsec: `nsec1${npub.slice(5)}` }; } + case 'delete_profile': { + const npub = String(params.npub); + const idx = state.profiles.findIndex((p) => p.npub === npub); + if (idx === -1) throw new Error('That profile is not stored on this computer.'); + const [deleted] = state.profiles.splice(idx, 1); + const wasActive = state.active_profile?.npub === npub; + const next: AppState = { + ...state, + profiles: [...state.profiles], + active_profile: wasActive ? null : state.active_profile, + undo_history: [...(state.undo_history ?? []), { ...deleted, is_active: false }], + }; + // clear active flag on remaining profiles if needed + if (wasActive) { + next.profiles = next.profiles.map((p) => ({ ...p, is_active: false })); + } + backend.setState(next); + return next; + } + + case 'undo_delete': { + const hist = state.undo_history ?? []; + if (hist.length === 0) throw new Error('No profile deletions to undo.'); + const restored = hist[hist.length - 1]; + const next: AppState = { + ...state, + profiles: [...state.profiles, { ...restored, is_active: state.active_profile === null }], + active_profile: state.active_profile ?? { ...restored, is_active: true }, + undo_history: hist.slice(0, -1), + }; + // ensure only one active + if (state.active_profile === null) { + next.profiles = next.profiles.map((p) => ({ + ...p, + is_active: p.npub === restored.npub, + })); + } + backend.setState(next); + return next; + } + default: throw new Error(`Unknown method: ${method}`); } diff --git a/src/ipc.rs b/src/ipc.rs index e1ec407..f5554d9 100644 --- a/src/ipc.rs +++ b/src/ipc.rs @@ -146,6 +146,10 @@ pub enum Request { /// `true` to run the request, `false` to reject it. approved: bool, }, + DeleteProfile { + npub: String, + }, + UndoDelete, } /// A reply envelope carrying either data or a safe user-facing error. @@ -528,6 +532,17 @@ async fn run_with_app(app: &mut App, request: Request) -> Result { + let deleted = profiles::delete_profile(&mut app.vault, &npub)?; + app.save_vault()?; + app.undo_history.push(deleted.clone()); + Ok(json!(app.state_view())) + } + Request::UndoDelete => { + let restored = app.undo_delete()?; + app.save_vault()?; + Ok(json!(app.state_view())) + } // Signer control requests are handled by `run` before this function is // reached; keeping a wildcard arm keeps the match exhaustive here. _ => Err(AppError::internal("Unexpected signer request.")), diff --git a/src/profiles.rs b/src/profiles.rs index b6ba18a..ac55397 100644 --- a/src/profiles.rs +++ b/src/profiles.rs @@ -596,6 +596,28 @@ pub fn parse_secret_key(hex_str: &str) -> Result { SecretKey::from_slice(&bytes).map_err(|e| AppError::invalid_secret(format!("{e}"))) } +/// Delete a profile by npub, returning the deleted profile for undo. +/// The vault must not be encrypted, or the key must be provided. +pub fn delete_profile(vault: &mut Vault, npub: &str) -> Result { + let pos = vault + .profiles + .iter() + .position(|p| p.public_key == npub) + .ok_or_else(|| AppError::profile_not_found(npub))?; + let stored = vault.profiles.remove(pos); + if vault.active_profile.as_deref() == Some(npub) { + vault.active_profile = None; + } + Ok(ProfileSummary { + label: stored.label, + npub: stored.public_key, + created_at: stored.created_at, + is_active: false, + picture: stored.picture, + nip05: stored.nip05, + }) +} + #[cfg(test)] mod tests { use super::*; @@ -1060,27 +1082,4 @@ mod tests { .expect_err("missing profile must error"); assert_eq!(err.kind(), crate::errors::ErrorKind::ProfileNotFound); } - - /// Delete a profile by npub, returning the deleted profile for undo. - /// The vault must not be encrypted, or the key must be provided. - pub fn delete_profile(vault: &mut Vault, npub: &str) -> Result { - let pos = vault - .profiles - .iter() - .position(|p| p.public_key == npub) - .ok_or_else(|| AppError::profile_not_found(npub))?; - let stored = vault.profiles.remove(pos); - // Clear the active_profile if it was the one deleted - if vault.active_profile.as_deref() == Some(npub) { - vault.active_profile = None; - } - Ok(ProfileSummary { - label: stored.label, - npub: stored.public_key, - created_at: stored.created_at, - is_active: false, - picture: stored.picture, - nip05: stored.nip05, - }) - } } From 832e1144f03d0199b033ff17a9dcffddac61fbc2 Mon Sep 17 00:00:00 2001 From: Avi Date: Thu, 27 Aug 2026 12:09:53 -0500 Subject: [PATCH 2/2] checkpoint: fix delete/undo + themed auto-dismiss bar --- CHECKPOINT-encryption.md | 1454 +------------------------------------- 1 file changed, 17 insertions(+), 1437 deletions(-) diff --git a/CHECKPOINT-encryption.md b/CHECKPOINT-encryption.md index 86dda29..da13251 100644 --- a/CHECKPOINT-encryption.md +++ b/CHECKPOINT-encryption.md @@ -1,1447 +1,27 @@ -# Checkpoint — nostr stack 0.45 security migration (2026-08-25) - -A stopping point you can return to if this session is closed. Everything below was -verified green at the moment this file was written. +# Checkpoint — fix profile delete/undo + themed auto-dismiss bar (2026-08-27) ## Where things are - - Project: `/home/avi/Projects/Nostr_Keynctr` -- Git repo: `master` @ `8130c3e` ("frontend: click a profile row on Home to - select it"). Before it: `98f4520` (active-profile picture on Compose + - sidebar), `f534165` (remove Profiles sidebar tab), `835b0a0` (list all - profiles on Home, pin dev host, widen card), `5635f79` (checkpoint), - `71e580f` (Trim white margins), `f29d4f9` (Checkpoint 0.45 migration), - `fa5ba08` (Security: upgrade nostr stack 0.40->0.45), `bf10ae3`, `e210f17`, - `7098e75`, `e6a1efc`, `6d5063d`, `1c428a9`. -- Working tree: still DIRTY with the long-standing user changes — `concept3.svg` - deleted, `KeynectrAppIconPossibility02.jpeg` untracked. These are NOT part - of the frontend commit above and remain uncommitted. +- Git repo: `master` @ `9e635e7` ("fix: restore profile delete/undo and themed auto-dismiss undo bar"). Before it: `56c2a6d` (checkpoint: click profile row), `8130c3e` (click profile row on Home), `a5838ea`, `98f4520` etc. +- Working tree: clean except `KeynectrAppIconPossibility02.jpeg` untracked (long-standing, not part of commits). ## What was completed +1. **Fixed broken profile delete/undo.** `src/ipc.rs` was missing `DeleteProfile`/`UndoDelete` request variants and handlers, so `delete_profile` from renderer returned `Unexpected signer request`. Added `DeleteProfile {npub}`/`UndoDelete` to `Request`, handlers that call `profiles::delete_profile`/`app.undo_delete()`, save vault, push/pop `undo_history`, return `state_view()`. Exposed `profiles::delete_profile` outside `#[cfg(test)]` (was only inside tests). Updated `src/profiles.rs` accordingly. CLI `delete_profile_direct` kept for `main.rs`. +2. **Wired frontend to backend state.** `frontend/src/lib/api.ts`: `deleteProfile`/`undoDelete` now `call` (was `ProfileSummary`). `frontend/src/state/AppProvider.tsx`: both now use `applyState(...)` and type `Promise`; added `clearLastDeleted` that locally slices `undo_history` for UI dismissal. +3. **Themed auto-dismiss undo bar.** `frontend/src/screens/ProfilesScreen.tsx`: replaced permanent white bar (`var(--token-item-bg, #f0f0f0)` + "Profile deleted. You can restore it until you delete another or quit.") with themed bar (`var(--primary-soft)` bg, `var(--border)` border) showing `Profile "X" deleted. — Undo and restore profile` where the link uses `var(--primary)` (adapts to light/dark/neon/glass). Bar appears in both empty and populated states, auto-dismisses after 5s via `useEffect` watching `lastDeleted` (previous version fired unconditionally on mount). Fake backend updated to simulate delete/undo for tests. -1. **Security migration of the Nostr stack (`fa5ba08`).** `cargo audit` was - installed on this machine (`cargo install cargo-audit`, v0.22.2) and its - first scan found **11 RustSec vulnerabilities** in nostr 0.40 / - relay-pool 0.40.1 — including forged-event signature-bypass (0224), - NIP-46 credential Debug leak (0225), and auth-challenge memory - exhaustion (0231). Upgraded to `nostr 0.45.3` + `nostr-sdk 0.45.2` - (secp256k1 0.30), clearing every vulnerability and all 4 warnings. - Code adaptations: `Client::builder().authenticator(SignerAuthenticator)` - replaces implicit signer (NIP-42 auto-auth stays working); signing moved - from `EventBuilder::sign` to `finalize_async`; nip44 encrypt now takes an - explicit random nonce (getrandom); feed + signer receive loops moved to - `stream_events` (signer's notification stream still opens BEFORE the - announce, preserving the ordering fix); contact p-tags parsed via - `single_letter_tag()`; `relay()` returns Option; `try_connect().timeout()`. - Dropped unused `nip46` feature (signer is hand-rolled). Dry-run first: - bump-only build proved zero dependency-crate failures / no MSRV issues; - all 24 errors were exactly the planned API edits. -2. **New regression tests (+2, suite now 115).** Malformed NIP-44 payloads - (not-base64, empty, truncated, bad version byte) reject cleanly with no - panic; error strings never contain secret-key material (0225 class). -3. Earlier today: R3 modal-save dedup (`7098e75`) completing the DRY audit, - NIP-42 clarification docs (`bf10ae3`). - -## Commits added most recently - -- `8130c3e` frontend: click a profile row on Home to select it -- `98f4520` frontend: show active-profile picture on Compose and sidebar -- `f534165` frontend: remove redundant Profiles sidebar tab -- `835b0a0` frontend: list all profiles on Home, pin dev host, widen profiles card -- `fa5ba08` Security: upgrade nostr stack 0.40->0.45 clearing 11 RustSec advisories +## Commits added in this session (newest first) +- `9e635e7` fix: restore profile delete/undo and themed auto-dismiss undo bar ## Verification commands run (all green) +- Rust: `cargo fmt --check` clean; `cargo clippy --all-targets` 2 pre-existing warnings; `cargo test` 115 passed; `cargo build --release` success +- Frontend (`frontend/`): `npm run typecheck` clean; `npm run lint` clean; `npm run format:check` clean (after `npm run format`); `npm test` 15 files / 99 tests passed; `npm run build` success; `npm run electron:build` success +- The `--ozone-platform=wayland` Vulkan and `has no handler with id` messages seen on `electron:build` are harmless Electron/Wayland warnings, not profile errors. -Rust (repo root): cargo fmt --check clean; cargo clippy --all-targets only -the two pre-existing profiles.rs warnings; cargo test 115 passed (release -mode re-run too); cargo build --release success. -Frontend (`frontend/`, untouched): typecheck clean; format:check clean; -npm test 15 files / 99 tests; vite build success; electron:build success. -Live: probe built on 0.45 stack published kind-1 notes accepted by -wss://soloco.nl and wss://relay.primal.net within the 6 s watchdog. -Pre-commit audit per user protocol: staged diff = Cargo.toml/Cargo.lock/src -only; git diff --check clean. +## How to resume / reproduce +GUI: `cargo build --release && cd frontend && npm run build && npm run electron:build && npm start` (or `npm run dev`). Create 2 profiles, delete one via Profiles → Delete → confirm, see themed undo bar for 5s with "Undo and restore profile" in primary color, click it to restore, or wait for auto-dismiss. +CLI: `cargo run -- delete-profile ` then `cargo run -- undo-delete` (in-memory undo only within same `serve` session; CLI undo across processes is not persisted). -## How to use / reproduce - -No user-facing change. Rebuild + restart to pick up the new backend: - -```bash -cd ~/Projects/Nostr_Keynctr && cargo build --release -cd frontend && npm run build && npm start -``` - -Re-check Updates card: Rust advisory section should now list no -vulnerabilities (cargo-audit is installed machine-wide). - -## Notes & next steps - -- wss://nostr.l484.com had a DNS-resolution blip at the very end of the - session (gaierror for ~a minute); it worked earlier today including a - stored test event. If it stays down, check the relay host — not app code. -- Probe harnesses live in /tmp/opencode (nip42-probe on 0.45; python raw - readers). Copy into scripts/ if they should survive reboots. -- Pre-existing clippy warnings in src/profiles.rs remain untouched by request. -- Relay health today: nos.lol 502 intermittent, nostr.wine 403, - l484.com transient DNS failure at session end, nostr.band unreachable; - primal/mom/soloco healthy. - ---- - -# Older checkpoint — DRY modal save lifecycle / R3 complete (2026-08-25) - -A stopping point you can return to if this session is closed. Everything below was -verified green at the moment this file was written. - -## Where things are - -- Project: `/home/avi/Projects/Nostr_Keynctr` -- Git repo: `master` @ `7098e75` ("refactor: share modal save lifecycle"). - Before it: `e6a1efc` (refactor: share hex-id shortening in lib/format), - `ec237bf` (Checkpoint: relay pool bootstrap), `6d5063d` - (refactor: share relay pool bootstrap), `1c428a9` (DRY fan-out R1), - `b21678f` (signer-fix checkpoint), `64ad94d` (NIP-46 handshake fix). -- Working tree after this checkpoint commit: only the long-standing user - changes remain — `concept3.svg` deleted and - `KeynectrAppIconPossibility02.jpeg` untracked, exactly as the user had them. - -## What was completed - -1. **R3 modal save-lifecycle dedup (`7098e75`).** The three profile modals in - `frontend/src/screens/ProfilesScreen.tsx` (Picture, Rename, NIP-05) each - carried an identical ~20-line async save lifecycle. Now a single - module-local `runModalSave` helper owns it: clear error → set saving → - claim publishing slot (`onSavingChange(npub)`) → run API → build success - message from the publish report → report via `onSaved`, or on failure - surface the error via `onError` → always release saving state and the slot - in `finally`. Each modal keeps only what is genuinely its own: the API call, - its success-message logic inline (including NIP-05's set/removed branch), - and RenameModal's `canSave` guard outside the helper. No user-facing change: - callback order, messages, buttons, props, and tests untouched. - With this, all four DRY-audit items (R1–R4) are complete. -2. Earlier: R4 hex-id shortening (`e6a1efc`), R2 relay-pool bootstrap - (`6d5063d`), R1 fan-out dedup (`1c428a9`) — details in the older checkpoints - below. - -## Commits added most recently - -- `7098e75` refactor: share modal save lifecycle - -## Verification commands run (all green) - -Rust (repo root): cargo test 113 passed; cargo clippy --all-targets finished -(pre-existing profiles.rs warnings only); cargo fmt --check clean; -cargo build --release success. -Frontend (`frontend/`): npm test 15 files / 99 tests passed; typecheck clean; -lint clean; format:check clean; npm run build success; electron:build success. -Pre-commit audit per user protocol: staged diff contained only -ProfilesScreen.tsx; git diff --check clean; nothing else staged. - -## How to use / reproduce - -No user-facing change. Profiles → Picture / Edit name / NIP-05 modals behave -exactly as before; any future change to the shared save policy happens in one -place: `runModalSave` in `frontend/src/screens/ProfilesScreen.tsx` -(just above `Nip05Modal`). - -## Notes & next steps - -- **NIP-42 status clarified (2026-08-25 addendum).** Earlier notes saying - "l484.com needs NIP-42 auth" described *raw probes* that don't answer - challenges — not a Keynectr gap. Verified: nostr-sdk 0.40 attaches the - signer in `Client::new` and enables NIP-42 auto-authentication by default - (`nip42_auto_authentication: true`), so AUTH challenges on read AND write - are answered automatically on every Keynectr path. Live test against - `wss://nostr.l484.com` replicating Keynectr's exact client setup: event - accepted in 0.3 s (inside the 6 s `RELAY_SEND_TIMEOUT`) and retrievable - afterwards. Known edge, left as-is by choice: the SDK's worst-case auth - dance (~7 s wait + resend) can exceed the 6 s watchdog on a very slow - authed relay → "did not respond in time"; healthy relays finish in <1 s. - Probe harness: `/tmp/opencode/nip42-probe` (throwaway key, same crates). -- DRY audit items R1–R4 are all complete; nothing left on hold from that list. -- Pre-existing clippy warnings in src/profiles.rs remain untouched by request. -- Relay health today: nos.lol 502 (intermittent), nostr.wine 403, - l484.com sends NIP-42 AUTH challenges but works fine with Keynectr - (auto-answered — see addendum above), nostr.band unreachable; - primal/mom/soloco healthy. -- Client visibility rule of thumb confirmed today: a note appears only on - clients whose read relays overlap the relays it was published to; if a - note "is missing" somewhere, first compare relay lists (Primal indexes - broadly; Iris/Yakihonne read narrower sets). - ---- - -# Older checkpoint — DRY hex-id shortening (2026-08-24) - -A stopping point you can return to if this session is closed. Everything below was -verified green at the moment this file was written. - -## Where things are - -- Project: `/home/avi/Projects/Nostr_Keynctr` -- Git repo: `master` @ `e6a1efc` ("refactor: share hex-id shortening in lib/format"). - Before it: `ec237bf` (Checkpoint: relay pool bootstrap), `6d5063d` - (refactor: share relay pool bootstrap), `1c428a9` (DRY fan-out R1), - `b21678f` (signer-fix checkpoint), `64ad94d` (NIP-46 handshake fix), - `dd50b24` (Aurora rename). -- Working tree: clean apart from this checkpoint update. `concept3.svg` deleted - and `KeynectrAppIconPossibility02.jpeg` untracked remain as the user had them. - -## What was completed - -1. **R4 hex-id shortening (`e6a1efc`).** Exported `shortHexId` from - `frontend/src/lib/format.ts` with verbatim logic from the former local - `shortHex` in `SignerScreen.tsx`; SignerScreen now imports and calls - `shortHexId`; added `format.test.ts` with 7 focused tests (empty, len 16, - len 17, 64-hex, shortenNpub disabled, enabled truncation). Output behavior - is byte-identical: >16 uses first 8 chars + Unicode ellipsis U+2026 + last 8, - ≤16 passes through unchanged. Does not reuse `shortenNpub`; preserves - existing visual output exactly per audit constraint. -2. **R2 DRY refactor (`6d5063d`).** New `relays::open_pool(keys, urls, - wait: Option) -> Result` is the single pool-construction - path (add each relay with strict error propagation, connect, optional - wait). Adopted by feed's two fetchers (`Keys::generate()` + 10 s wait) and - note publishing (`keys.clone()`, no wait). `profiles.rs` intentionally NOT - adopted — its unique ignore-add-errors policy stays local; it regained an - explicit `connect()` when `send_to_all_relays` dropped its internal one - (avoids double-connect on the note path). `signer.rs` deliberately - untouched: ordering-critical notification setup + fail-to-status error model. - Net −20 lines; behavior, error strings, timing unchanged. -3. Earlier this session: R1 fan-out dedup (`1c428a9`), NIP-46 handshake fix - (`64ad94d`), Aurora rename + dropdown fix (`dd50b24`), frosted-glass look - (`ecb666b`), glass build fix (`bfe14f0`). - -## Commits added most recently - -- `e6a1efc` refactor: share hex-id shortening in lib/format -- `6d5063d` refactor: share relay pool bootstrap - -## Verification commands run (all green) - -Rust: cargo test 113 passed; clippy only pre-existing profiles.rs warnings; -fmt clean; release binary rebuilt. -Frontend (untouched): npm test 99; typecheck, lint, format, electron:build, -vite build all clean. -Pre-commit audit per user protocol: git diff --check clean; signer.rs empty -diff; 3 test files added (format.test.ts with 7 new tests). - -## How to use / reproduce - -No user-facing change. Future connection-policy work (e.g., NIP-42 auth for -nostr.l484.com, retries) now starts in `relays::open_pool`. - -## Notes & next steps - -- Remaining DRY items by user request on hold: R3 (modal save-lifecycle hook). -- R4 (shortHex vs shortenNpub) is complete: shared `shortHexId` in lib/format, - output preserved exactly, tests green. -- Relay health today: nos.lol 502, nostr.wine 403, l484.com needs NIP-42 auth, - nostr.band unreachable; primal/mom/soloco healthy. ---- - -# Older checkpoint — DRY relay fan-out (2026-08-24) - -A stopping point you can return to if this session is closed. Everything below was -verified green at the moment this file was written. - -## Where things are - -- Project: `/home/avi/Projects/Nostr_Keynctr` -- Git repo: `master` @ `1c428a9` ("DRY: single parallel relay fan-out for notes - and metadata"). Before it: `64ad94d` (NIP-46 handshake fix), `b21678f`/ - `ae84526` (checkpoints), `dd50b24` (Aurora rename). -- Working tree: clean apart from this checkpoint update. `concept3.svg` deleted - and `KeynectrAppIconPossibility02.jpeg` untracked remain as the user had them. - -## What was completed - -1. **R1 DRY refactor (`1c428a9`).** The parallel relay send block (~55 lines: - JoinSet per-relay sends, 6 s timeout, indexed outcome collection, - disconnect, succeeded/failed split) existed twice — in - `publish.rs::publish_with_keys` and `profiles.rs::publish_metadata_async`. - Now one `pub(crate) publish::send_to_all_relays(client, urls, event, noun)` - serves both; callers keep their distinct add-relay policies (note path - aborts on add errors, metadata ignores them) so behavior, error strings, - and public APIs are unchanged. Removed duplicate constant - `METADATA_SEND_TIMEOUT`, dead helper `failure_for`. Net −49 lines. -2. Deliberately NOT done (pending approval): R2 shared client-bootstrap - helper, R3 modal save-lifecycle hook (frontend), R4 shortHex/shortenNpub. - -## Commits added most recently - -- `1c428a9` DRY: single parallel relay fan-out for notes and metadata - -## Verification commands run (all green) - -Rust: cargo fmt --check clean; clippy only pre-existing profiles.rs warnings; -cargo test 113 passed; release binary rebuilt. -Frontend (untouched, suite rerun): npm test 92 passed; typecheck, lint, -format:check, electron:build, vite build all clean. - -## How to use / reproduce - -No user-facing change; note and metadata publishing behave exactly as before. -Any future change to relay-send policy now happens in one place: -`send_to_all_relays` in `src/publish.rs`. - -## Notes & next steps - -- R2–R4 from the DRY audit remain unimplemented by request. -- Relay health today: nos.lol 502, nostr.wine 403, nostr.l484.com needs - NIP-42 auth, nostr.band unreachable; primal/mom/soloco healthy. - ---- - -# Older checkpoint — NIP-46 signer handshake fix (2026-08-24) - -A stopping point you can return to if this session is closed. Everything below was -verified green at the moment this file was written. - -## Where things are - -- Project: `/home/avi/Projects/Nostr_Keynctr` -- Git repo: `master` @ `64ad94d` ("Signer: listen before announcing so the - handshake reply is not lost"). Before it: `dd50b24` (Aurora rename + - dropdown fix), `ecb666b` (frosted-glass look), `bfe14f0` (glass build fix). -- Working tree: clean apart from this checkpoint update. `concept3.svg` deleted - and `KeynectrAppIconPossibility02.jpeg` untracked remain as the user had them. - -## What was completed - -1. **Root-caused the "Yakihonne never leaves the QR screen" bug (`64ad94d`).** - Reproduced with a reference NIP-46 client harness over live relays - (`/tmp/opencode/nc-client`): the app's announcement arrived, the client's - `ack` + `get_public_key` were answered to nobody — the signer task created - its notification receiver *after* publishing the announce, so the client's - millisecond-fast handshake reply landed in the broadcast channel before any - receiver existed and was dropped. Client waits forever → stuck QR. - Fix: open `client.notifications()` immediately after adding relays, before - connect/subscribe/announce. Post-fix round-trip PASSES end-to-end - (announce → ack → get_public_key → pubkey returned). -2. Relay observations from testing: nos.lol 502 today; nostr.wine 403 - (auth/paid); nostr.l484.com sends NIP-42 AUTH challenges; healthy: - relay.primal.net, nostr.mom, soloco.nl. - -## Commits added most recently - -- `64ad94d` Signer: listen before announcing so the handshake reply is not lost - -## Verification commands run (all green) - -Rust (repo root): cargo test 113 passed; clippy (pre-existing profiles.rs -warnings only); fmt clean; release binary rebuilt with the fix. -Frontend (`frontend/`): npm test 92 passed; typecheck, lint, format clean. -End-to-end: reference NIP-46 client + `keynectr signer connect ` over -wss://nostr.mom, relay.primal.net, soloco.nl — full handshake PASS. - -## How to use / reproduce - -GUI: `cd ~/Projects/Nostr_Keynctr/frontend && npm start` → Signer → paste a -client's nostrconnect:// link → approve requests. With Yakihonne: choose its -remote-signer/connect option, copy its link into Keynectr within its timeout; -Yakihonne should now switch from the QR screen to the logged-in state. - -Harness (if ever needed again): `/tmp/opencode/nc-client` — `nc-client` -prints a nostrconnect URI to /tmp/opencode/uri.txt and PASSes on round-trip; -run `target/release/keynectr signer connect $(cat /tmp/opencode/uri.txt)`. - -## Notes & next steps - -- If a client is still slow, remaining edge: relays that reject kind 24133 or - need auth can starve the handshake — consider surfacing per-relay connect - status in the Signer UI later. -- Pre-existing clippy warnings in src/profiles.rs untouched by request. - ---- - -# Older checkpoint — Aurora rename + dropdown fix (2026-08-24) - -A stopping point you can return to if this session is closed. Everything below was -verified green at the moment this file was written. - -## Where things are - -- Project: `/home/avi/Projects/Nostr_Keynctr` -- Git repo: `master` @ `dd50b24` ("Rename glass theme display to Aurora; fix - unreadable select options"). Before it: `ecb666b` (frosted-glass look), - `0cab883`/`1aae81c` (checkpoints), `bfe14f0` (glass fix). -- Working tree: clean apart from this checkpoint update. `concept3.svg` deleted - and `KeynectrAppIconPossibility02.jpeg` untracked remain as the user had them. - -## What was completed - -1. **Theme renamed to Aurora (`dd50b24`).** User picked "Aurora" from options - (Aurora/Frost/Glacier/Ice/keep). Display label only — internal id stays - `glass`, so existing settings.json values need no migration. -2. **Fixed unreadable theme dropdown (`dd50b24`).** Under the glass theme the - native `