ux+fix: rename Network Identity → Device Address (UI) + redundancy authors
#6: UI rename — Network Identity → Device Address. Just labels/wording, no backend changes. Tauri command names (list_identities, create_identity, etc.) and DTO fields stay as-is; the rename is purely user-facing. Settings labels, lightbox titles, toasts, danger-zone text, and welcome-screen "Import an Identity" button all updated. Distinguishes more clearly from Personas (which ARE the posting identity peers see). #7: Bug fix — redundancy showed 0 for new posts. Root cause: get_redundancy_summary queried SELECT p.id FROM posts WHERE p.author = ?1 with ?1 = self.node_id (the device's network NodeId). After the v0.6.0 network/posting-ID split, posts are authored by posting identities (personas), not the network NodeId — so the query returned 0 of my posts and the redundancy panel showed all zeros. Fix: storage::get_redundancy_summary now takes &[NodeId] and uses WHERE author IN (?, ?, ...) over every persona on the device. Node::get_redundancy_summary gathers them via list_posting_identities() before delegating. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
83fd30753f
commit
100ea55a15
4 changed files with 50 additions and 29 deletions
|
|
@ -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<NodeId> = storage.list_posting_identities()?
|
||||
.into_iter().map(|p| p.node_id).collect();
|
||||
storage.get_redundancy_summary(&author_ids, 3_600_000)
|
||||
}
|
||||
|
||||
// ---- Networking ----
|
||||
|
|
|
|||
|
|
@ -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<PostId> = {
|
||||
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)?)?);
|
||||
|
|
|
|||
|
|
@ -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 = '<p class="empty-hint">No identities</p>';
|
||||
list.innerHTML = '<p class="empty-hint">No device addresses</p>';
|
||||
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 = `
|
||||
<div style="background:#1a1a2e;border:1px solid #333;border-radius:12px;padding:1.5rem;max-width:350px;width:90%;text-align:center">
|
||||
<h3 style="color:#7fdbca;margin:0 0 0.75rem">New Identity</h3>
|
||||
<input id="new-id-name" type="text" placeholder="Display name" maxlength="50" style="width:100%;margin-bottom:0.75rem" />
|
||||
<h3 style="color:#7fdbca;margin:0 0 0.5rem">New Device Address</h3>
|
||||
<p style="font-size:0.72rem;color:#888;margin-bottom:0.75rem">A new QUIC network endpoint for this device. Your personas are unaffected.</p>
|
||||
<input id="new-id-name" type="text" placeholder="Label (your reference, not shared)" maxlength="50" style="width:100%;margin-bottom:0.75rem" />
|
||||
<div style="display:flex;gap:0.5rem;justify-content:center">
|
||||
<button class="btn btn-primary btn-sm" id="new-id-create">Create</button>
|
||||
<button class="btn btn-ghost btn-sm" id="new-id-cancel">Cancel</button>
|
||||
|
|
@ -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 = `
|
||||
<div style="background:#1a1a2e;border:1px solid #333;border-radius:12px;padding:1.5rem;max-width:400px;width:90%;text-align:center">
|
||||
<h3 style="color:#7fdbca;margin:0 0 0.75rem">Import Identity</h3>
|
||||
<p style="font-size:0.75rem;color:#888;margin-bottom:0.5rem">Paste the 64-character hex key from an identity export.</p>
|
||||
<input id="import-id-key" type="text" placeholder="Identity key (64 hex chars)" maxlength="64" style="width:100%;margin-bottom:0.5rem;font-family:monospace;font-size:0.7rem" />
|
||||
<input id="import-id-name" type="text" placeholder="Display name" maxlength="50" style="width:100%;margin-bottom:0.75rem" />
|
||||
<h3 style="color:#7fdbca;margin:0 0 0.5rem">Import Device Address Key</h3>
|
||||
<p style="font-size:0.75rem;color:#888;margin-bottom:0.5rem">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.</p>
|
||||
<input id="import-id-key" type="text" placeholder="Device address key (64 hex chars)" maxlength="64" style="width:100%;margin-bottom:0.5rem;font-family:monospace;font-size:0.7rem" />
|
||||
<input id="import-id-name" type="text" placeholder="Label (your reference, not shared)" maxlength="50" style="width:100%;margin-bottom:0.75rem" />
|
||||
<div style="display:flex;gap:0.5rem;justify-content:center">
|
||||
<button class="btn btn-primary btn-sm" id="import-id-go">Import</button>
|
||||
<button class="btn btn-ghost btn-sm" id="import-id-cancel">Cancel</button>
|
||||
|
|
@ -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() {
|
|||
<p style="color:#888;font-size:0.85rem;margin-bottom:1.5rem">How would you like to get started?</p>
|
||||
<div style="display:flex;flex-direction:column;gap:0.75rem">
|
||||
<button id="first-run-new" class="btn btn-primary" style="padding:0.75rem">Start Fresh</button>
|
||||
<button id="first-run-import" class="btn btn-ghost" style="padding:0.75rem">Import an Identity</button>
|
||||
<button id="first-run-import" class="btn btn-ghost" style="padding:0.75rem">Import from another device</button>
|
||||
</div>
|
||||
</div>`;
|
||||
document.body.appendChild(chooser);
|
||||
|
|
|
|||
|
|
@ -236,7 +236,7 @@
|
|||
<p class="empty-hint" style="margin-bottom:0.5rem;font-size:0.78rem;line-height:1.5">
|
||||
<strong style="color:#7fdbca">Personas</strong> 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 <em>export your personas</em> from this device and <em>import them</em> on the new one.
|
||||
<br><br>
|
||||
<strong style="color:#888">Identities</strong> 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.
|
||||
<strong style="color:#888">Device Address</strong> 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.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
|
|
@ -259,14 +259,14 @@
|
|||
</div>
|
||||
|
||||
<div class="section-card" style="text-align:center">
|
||||
<h3 style="margin-bottom:0.4rem;font-size:0.85rem;color:#888">Identities (advanced)</h3>
|
||||
<h3 style="margin-bottom:0.4rem;font-size:0.85rem;color:#888">Device Address (advanced)</h3>
|
||||
<p class="empty-hint" style="margin-bottom:0.5rem;font-size:0.72rem">
|
||||
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.
|
||||
</p>
|
||||
<div id="identities-list" style="margin-bottom:0.5rem"></div>
|
||||
<div style="display:flex;gap:0.5rem;justify-content:center;flex-wrap:wrap">
|
||||
<button id="create-identity-btn" class="btn btn-ghost btn-sm">New Identity</button>
|
||||
<button id="import-identity-btn" class="btn btn-ghost btn-sm">Import Key</button>
|
||||
<button id="create-identity-btn" class="btn btn-ghost btn-sm">New Device Address</button>
|
||||
<button id="import-identity-btn" class="btn btn-ghost btn-sm">Import Address Key</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
@ -316,7 +316,7 @@
|
|||
|
||||
<div class="section-card">
|
||||
<h3>Danger Zone</h3>
|
||||
<p class="empty-hint">Delete all local data. Identity key preserved.</p>
|
||||
<p class="empty-hint">Delete all local data. Device address key preserved.</p>
|
||||
<button id="reset-data-btn" class="btn btn-danger btn-full">Reset All Data</button>
|
||||
</div>
|
||||
</section>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue