From 5b60ae373614d9ea322332519a5fa6cb225d5d7a Mon Sep 17 00:00:00 2001 From: Avi Date: Mon, 28 Sep 2026 12:03:35 -0500 Subject: [PATCH 1/8] feat(profiles): persistent identity backfill for generic pairing labels Pairing-time kind-0 enrichment gives up after ~4 attempts/2 minutes; a fresh Amber account that has not published metadata yet (or publishes later) then keeps the generic 'Amber'/'Remote Signer' label forever. serve() now spawns a slow-cadence backfill: every pass it re-scans for Nip46Client rows still wearing a GENERIC_PAIRING_LABELS placeholder and re-fetches kind-0, upgrading label/picture/nip05 when metadata appears. User renames are never overwritten (placeholder check gates the write); locked encrypted vaults are skipped; network runs off-lock with a 90s budget per identity. --- src/ipc.rs | 103 ++++++++++++++++++++++++++++++++++++++++++++++++ src/profiles.rs | 27 +++++++++++++ 2 files changed, 130 insertions(+) diff --git a/src/ipc.rs b/src/ipc.rs index dcb07a1..cd6a09a 100644 --- a/src/ipc.rs +++ b/src/ipc.rs @@ -5,6 +5,8 @@ use serde::{Deserialize, Serialize}; use serde_json::json; use tokio::sync::Mutex; +use nostr_sdk::prelude::PublicKey; + use crate::app::{App, Nip46ClientSignerHandle}; use crate::errors::AppError; use crate::feed; @@ -331,6 +333,107 @@ pub async fn serve() -> Result<(), AppError> { let mut tasks = JoinSet::new(); + // Persistent identity backfill. Pairing-time enrichment tries for ~2 + // minutes and stops; if the account's kind-0 was never published (or was + // published later), the row would keep its generic "Amber"/"Remote + // Signer" placeholder forever — which is what a new user with a fresh + // Amber account sees, with no way to know the app resolved nothing. + // This loop retries on a slow cadence while the backend runs: every pass + // collects remote rows still wearing a generic placeholder (only once + // the vault is unlocked — an encrypted vault hides them otherwise), + // fetches kind-0 from the enabled relays, and upgrades the row. A user + // rename always wins: is_generic_pairing_label is the sole gate on the + // label write. Cheap when nothing is pending (one vault scan per pass). + { + let app = app.clone(); + tasks.spawn(async move { + loop { + // Collect pending identities without holding the lock + // across network work. + let pending: Vec<(String, PublicKey)> = { + let guard = app.lock().await; + if guard.is_locked() { + Vec::new() + } else { + guard + .vault + .profiles + .iter() + .filter(|p| { + p.signer_mode == SignerMode::Nip46Client + && profiles::is_generic_pairing_label(&p.label) + }) + .filter_map(|p| { + PublicKey::parse(&p.public_key) + .ok() + .map(|key| (p.public_key.clone(), key)) + }) + .collect() + } + }; + if pending.is_empty() { + tokio::time::sleep(Duration::from_secs(300)).await; + continue; + } + let relay_urls = { + let guard = app.lock().await; + relays::enabled_urls(&guard.settings) + }; + for (npub, identity) in pending { + let found = tokio::time::timeout( + Duration::from_secs(90), + tokio::task::spawn_blocking({ + let relay_urls = relay_urls.clone(); + move || profiles::fetch_profile_metadata(&identity, &relay_urls) + }), + ) + .await + .ok() + .and_then(|join| join.ok()) + .flatten(); + let Some(meta) = found else { continue }; + let mut guard = app.lock().await; + let mut changed = false; + // Re-resolve by pubkey string: another request (rename, + // a pairing-time enrichment) may have edited the row + // while this fetch was in flight. + if let Some(row) = guard + .vault + .profiles + .iter_mut() + .find(|p| p.public_key == npub) + { + if row.picture.is_none() && meta.picture.is_some() { + row.picture = meta.picture.clone(); + changed = true; + } + if row.nip05.is_none() && meta.nip05.is_some() { + row.nip05 = meta.nip05.clone(); + changed = true; + } + let real_name = meta + .display_name + .as_deref() + .or(meta.name.as_deref()) + .map(str::trim) + .filter(|name| !name.is_empty()); + if let Some(name) = real_name { + if profiles::is_generic_pairing_label(&row.label) { + row.label = name.to_string(); + changed = true; + } + } + } + if changed { + let _ = guard.save_vault(); + eprintln!("[backfill] resolved profile identity via kind-0: {npub}"); + } + } + tokio::time::sleep(Duration::from_secs(120)).await; + } + }); + } + while let Some(line) = line_rx.recv().await { if line.trim().is_empty() { continue; diff --git a/src/profiles.rs b/src/profiles.rs index d5b8dfc..2da0bf1 100644 --- a/src/profiles.rs +++ b/src/profiles.rs @@ -553,6 +553,19 @@ pub fn find_stored_profile<'a>( .ok_or_else(|| AppError::profile_not_found(npub)) } +/// Labels the UI assigns when pairing without a user-supplied name +/// (CreateProfileModal defaults to "Amber"; Signer Mode to "Remote +/// Signer"). A remote-signer profile row still carrying one of these has +/// never resolved its real identity from the network, so it stays a +/// candidate for automatic name backfill on every launch. A user rename +/// always falls outside this list and is therefore never overwritten. +pub const GENERIC_PAIRING_LABELS: &[&str] = &["Amber", "Remote Signer"]; + +/// True when `label` is one of the generic pairing placeholders. +pub fn is_generic_pairing_label(label: &str) -> bool { + GENERIC_PAIRING_LABELS.contains(&label) +} + /// Create or refresh the vault profile for a remote (NIP-46) identity. /// /// When a NIP-46 client connection is established the identity lives on the @@ -930,6 +943,20 @@ pub fn delete_profile(vault: &mut Vault, npub: &str) -> Result Vault { From f905cbcdad02d282a67d0db1c68e8ad1eabfb16c Mon Sep 17 00:00:00 2001 From: Avi Date: Mon, 28 Sep 2026 20:13:07 -0500 Subject: [PATCH 2/8] =?UTF-8?q?docs(checkpoint):=20Sep=2028=20evening=20?= =?UTF-8?q?=E2=80=94=20pairing=20resolved=20(live=20vault=20evidence),=20b?= =?UTF-8?q?ackfill=20+=20stdin=20fix=20verified=20green?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CHECKPOINT-encryption.md | 61 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 61 insertions(+) diff --git a/CHECKPOINT-encryption.md b/CHECKPOINT-encryption.md index 2271d5c..d5dd9fd 100644 --- a/CHECKPOINT-encryption.md +++ b/CHECKPOINT-encryption.md @@ -1,3 +1,64 @@ +# Checkpoint — stdin EAGAIN fix, accent theme, identity backfill (2026-09-28 evening) + +## Where things are +- Project: `/home/avi/Projects/Keynctr`, branch `master` @ **`5b60ae3`** + ("feat(profiles): persistent identity backfill for generic pairing labels"). + Previous: `a6182e3` (iOS blue accent for Workshop Dark + custom accent + setting), `9770f46` (stdin blocking-thread reader — EAGAIN crash fix), + `6bff188` (docs), `dcc701f` (no-restart self-update). +- Working tree: clean for tracked files. Untracked intentionally NOT + committed: `COSMIC_THEME.md`, `KeynectrAppIconPossibility02.jpeg`, + `deferred/SignerConnectionPanel.tsx.wip/`. +- Release binary rebuilt 2026-09-28 12:02 (from the 5b60ae3 tree). +- origin/master is at `a6182e3`; local is 1 commit ahead — push still blocked + (no Forgejo token stored for this session; HTTPS asks for credentials). + +## Pairing problem status — RESOLVED (live evidence) +- `~/.local/share/keynectr/profiles_vault.json` now carries **3 persisted + `nip46_connections`** rows with matching `signer_mode: nip46_client` profile + rows (created Sep 25, Sep 27, and **Sep 28 10:23**). The Sep 28 pairing + happened on the current build: Amber's connect event parses, the handshake + completes, and the profile row persists — the original + "Amber says connected but no row appears" failure no longer reproduces. +- The Sep 28 row is the **active profile**. Its label is still the generic + "Amber" because the paired account has not published kind-0 yet; `5b60ae3` + backfill re-checks every ~2-5 min while the backend runs and will rename it + when metadata appears. No further parse-side work outstanding. + +## What was completed (this checkpoint's commits) +1. **`9770f46` stdin EAGAIN crash fix** — `keynectr serve` treated a transient + EAGAIN on the Electron pipe as fatal ("Rust backend exited unexpectedly + code 1"). Dedicated blocking-thread reader + mpsc; verified live with a + 200-line request stream. +2. **`a6182e3` custom accent color** — iOS blue accent for Workshop Dark plus + a user-settable accent color. +3. **`5b60ae3` persistent identity backfill** — slow-cadence loop in `serve()` + re-fetches kind-0 for Nip46Client rows still wearing generic + "Amber"/"Remote Signer" placeholders; user renames never overwritten; + locked vaults skipped; network off-lock, 90 s budget per identity. + +## Verification (2026-09-28 evening cron pass) +- `cargo test` → 221 unit + 6 e2e passed, 0 failed. +- `cargo clippy --all-targets` → 0 warnings. `cargo fmt --check` → clean. +- `frontend`: `npm test` → 139 passed (19 files); `typecheck`, `lint`, + `format:check` all clean. + +## How to resume +- GUI: `cd ~/Projects/Keynctr/frontend && npx vite --port 5173` then + `NOSTR_GUI_DEV_URL=http://localhost:5173 KEYNCTR_ENABLE_GPU=1 npx electron .` +- CLI backend: `target/release/keynectr serve` (spawned by Electron). +- Push pending commit when credentials are available: `git push origin master`. + +## Outstanding / next steps +- Live confirmation of the backfill rename: pair/keep a fresh Amber account + with no kind-0, publish kind-0 from Amber, confirm the Keynctr row renames + itself within ~5 min without restarting the app. +- Step 4 permissions UI follow-ups, Step 5 KDF, Step 7 rename/hygiene + (unchanged from previous checkpoint). +- `package.json` `homepage` still points at github.com/avi/Keynctr (Step 7). + +--- + # Checkpoint — permissions UI + updater fix + Workshop theme (2026-09-27 night) ## Where things are From 4d0df31726359bbedb754127c55d74944638cbf3 Mon Sep 17 00:00:00 2001 From: Avi Date: Wed, 30 Sep 2026 09:11:55 -0500 Subject: [PATCH 3/8] =?UTF-8?q?feat(theme):=20Archipelago=20=E2=80=94=20sy?= =?UTF-8?q?nthwave=20sunset=20sea=20from=20reference=20art?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Teal-navy dusk, coral scanline sun (subtle repeating-linear horizon stripes + low-center sun glow + pink cloud banks, fixed attachment), glassmorphic cards/sidebar (blur, translucent surfaces), coral primary accent, white logo filter on the dark canvas. Parsed as 'archipelago' in settings; Settings -> Appearance option 'Archipelago - Sunset Sea'. Verified tokens + all 6 rules compute in the built bundle; 221+6 Rust, 139 frontend tests, clippy 0, electron:build green. --- frontend/src/lib/types.ts | 3 +- frontend/src/screens/SettingsScreen.tsx | 1 + frontend/src/styles.css | 81 +++++++++++++++++++++++++ src/settings.rs | 5 ++ 4 files changed, 89 insertions(+), 1 deletion(-) diff --git a/frontend/src/lib/types.ts b/frontend/src/lib/types.ts index 1b2db24..9d71707 100644 --- a/frontend/src/lib/types.ts +++ b/frontend/src/lib/types.ts @@ -7,7 +7,8 @@ export type Theme = | 'impeccable-dark' | 'cosmic' | 'workshop' - | 'workshop-dark'; + | 'workshop-dark' + | 'archipelago'; /** Active signer mode. */ export type SignerMode = 'embedded' | 'nip46_bunker' | 'nip46_client'; diff --git a/frontend/src/screens/SettingsScreen.tsx b/frontend/src/screens/SettingsScreen.tsx index fdfd396..428c2ee 100644 --- a/frontend/src/screens/SettingsScreen.tsx +++ b/frontend/src/screens/SettingsScreen.tsx @@ -148,6 +148,7 @@ export function SettingsScreen() { +