diff --git a/crates/core/src/node.rs b/crates/core/src/node.rs index 8dd0d46..0450ab8 100644 --- a/crates/core/src/node.rs +++ b/crates/core/src/node.rs @@ -3442,7 +3442,12 @@ impl Node { pub async fn get_redundancy_summary(&self) -> anyhow::Result<(usize, usize, usize, usize)> { let storage = self.storage.get().await; - storage.get_redundancy_summary(&self.node_id, 3_600_000) + // Posts are authored by posting identities (personas), not the + // network NodeId. Use every persona on this device so the + // summary counts all of my posts across personas. + let author_ids: Vec = storage.list_posting_identities()? + .into_iter().map(|p| p.node_id).collect(); + storage.get_redundancy_summary(&author_ids, 3_600_000) } // ---- Networking ---- diff --git a/crates/core/src/storage.rs b/crates/core/src/storage.rs index cee97e8..a330986 100644 --- a/crates/core/src/storage.rs +++ b/crates/core/src/storage.rs @@ -3129,17 +3129,30 @@ impl Storage { /// Get a summary of redundancy across all our authored posts. /// Returns (total, zero_replicas, one_replica, two_plus_replicas). + /// Redundancy summary across every post authored by ANY of the + /// device's posting identities (personas). Pre-v0.6.0 this matched + /// on the device's network NodeId, but the network/posting-ID + /// split moved authorship to the posting identity — so the old + /// query returned 0 of my posts and the UI showed 0 redundancy + /// for new posts. pub fn get_redundancy_summary( &self, - our_node_id: &NodeId, + author_ids: &[NodeId], staleness_ms: u64, ) -> anyhow::Result<(usize, usize, usize, usize)> { + if author_ids.is_empty() { + return Ok((0, 0, 0, 0)); + } let cutoff = now_ms() - staleness_ms as i64; - let mut stmt = self.conn.prepare( - "SELECT p.id FROM posts p WHERE p.author = ?1", - )?; + let placeholders: Vec<&str> = (0..author_ids.len()).map(|_| "?").collect(); + let sql = format!( + "SELECT p.id FROM posts p WHERE p.author IN ({})", + placeholders.join(","), + ); + let mut stmt = self.conn.prepare(&sql)?; + let params_iter = rusqlite::params_from_iter(author_ids.iter().map(|n| n.to_vec())); let post_ids: Vec = { - let mut rows = stmt.query(params![our_node_id.as_slice()])?; + let mut rows = stmt.query(params_iter)?; let mut ids = Vec::new(); while let Some(row) = rows.next()? { ids.push(blob_to_postid(row.get(0)?)?); diff --git a/frontend/app.js b/frontend/app.js index 705cb18..5fc2a2b 100644 --- a/frontend/app.js +++ b/frontend/app.js @@ -3543,11 +3543,11 @@ if (exportKeyBtn) exportKeyBtn.addEventListener('click', async () => { const key = await invoke('export_identity'); try { await navigator.clipboard.writeText(key); - toast('Identity key copied to clipboard. KEEP IT SECRET!'); + toast('Device address key copied to clipboard. KEEP IT SECRET!'); } catch (clipErr) { // Clipboard API may fail in some webview contexts — show the key instead console.error('Clipboard write failed:', clipErr); - prompt('Copy your identity key (KEEP IT SECRET!):', key); + prompt('Copy your device address key (KEEP IT SECRET!):', key); } } catch (e) { console.error('export_identity failed:', e); @@ -3586,14 +3586,16 @@ document.querySelectorAll('.text-size-opt').forEach(btn => { }); }); -// --- Identity management --- +// --- Device address management (formerly "Identity") --- +// The underlying Tauri commands keep their `identity` names for now — +// only the user-facing labels rename. Backend rename can follow. async function loadIdentities() { const list = $('#identities-list'); if (!list) return; try { const identities = await invoke('list_identities'); if (identities.length === 0) { - list.innerHTML = '

No identities

'; + list.innerHTML = '

No device addresses

'; return; } list.innerHTML = identities.map(id => { @@ -3616,7 +3618,7 @@ async function loadIdentities() { btn.textContent = 'Switching...'; try { await invoke('switch_identity', { nodeIdHex: btn.dataset.id }); - toast('Identity switched — reloading...'); + toast('Device address switched — reloading...'); setTimeout(() => location.reload(), 1000); } catch (e) { toast('Error: ' + e); btn.disabled = false; btn.textContent = 'Switch'; } }); @@ -3625,10 +3627,10 @@ async function loadIdentities() { // Wire delete buttons list.querySelectorAll('.delete-id-btn').forEach(btn => { btn.addEventListener('click', async () => { - if (!confirm('Delete this identity? This cannot be undone.')) return; + if (!confirm('Delete this device address? This cannot be undone.')) return; try { await invoke('delete_identity', { nodeIdHex: btn.dataset.id }); - toast('Identity deleted'); + toast('Device address deleted'); loadIdentities(); } catch (e) { toast('Error: ' + e); } }); @@ -3644,8 +3646,9 @@ $('#create-identity-btn').addEventListener('click', () => { overlay.style.cursor = 'default'; overlay.innerHTML = `
-

New Identity

- +

New Device Address

+

A new QUIC network endpoint for this device. Your personas are unaffected.

+
@@ -3654,10 +3657,10 @@ $('#create-identity-btn').addEventListener('click', () => { document.body.appendChild(overlay); overlay.querySelector('#new-id-create').addEventListener('click', async () => { const name = overlay.querySelector('#new-id-name').value.trim(); - if (!name) { toast('Name is required'); return; } + if (!name) { toast('Label is required'); return; } try { const nodeId = await invoke('create_identity', { name }); - toast(`Identity created: ${nodeId.substring(0, 12)}`); + toast(`Device address created: ${nodeId.substring(0, 12)}`); overlay.remove(); loadIdentities(); } catch (e) { toast('Error: ' + e); } @@ -3672,10 +3675,10 @@ $('#import-identity-btn').addEventListener('click', () => { overlay.style.cursor = 'default'; overlay.innerHTML = `
-

Import Identity

-

Paste the 64-character hex key from an identity export.

- - +

Import Device Address Key

+

Paste a 64-character hex key from a previous device-address export. This is NOT how you move your personas — use Export/Import personas above for that.

+ +
@@ -3688,7 +3691,7 @@ $('#import-identity-btn').addEventListener('click', () => { if (keyHex.length !== 64) { toast('Key must be 64 hex characters'); return; } try { const nodeId = await invoke('import_identity_key', { keyHex, name }); - toast(`Identity imported: ${nodeId.substring(0, 12)}`); + toast(`Device address imported: ${nodeId.substring(0, 12)}`); overlay.remove(); loadIdentities(); } catch (e) { toast('Error: ' + e); } @@ -4406,7 +4409,7 @@ async function init() {

How would you like to get started?

- +
`; document.body.appendChild(chooser); diff --git a/frontend/index.html b/frontend/index.html index 4c02f53..7abd8f2 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -236,7 +236,7 @@

Personas are who you are to peers — the keys you post and message with. Most people only need one. To move your account to a new device, you export your personas from this device and import them on the new one.

- Identities below are this device's own network address — usually not what you want to move. Leave them alone unless you know why you're touching them. + Device Address below is this device's own network endpoint — usually not what you want to move. Leave it alone unless you know why you're touching it.

@@ -259,14 +259,14 @@
-

Identities (advanced)

+

Device Address (advanced)

- This device's network address. Changing this is rarely useful — it lets you move the device's QUIC endpoint, NOT your posting identity. + This device's network endpoint — the QUIC address peers use to reach you. Changing this rotates the device's network identifier but does NOT change your posting identity (personas). Rarely useful.

- - + +
@@ -316,7 +316,7 @@

Danger Zone

-

Delete all local data. Identity key preserved.

+

Delete all local data. Device address key preserved.