From 09d1adc04249156bfbbff98fbf03c0b2f4629db8 Mon Sep 17 00:00:00 2001 From: Dorian Date: Thu, 19 Mar 2026 19:56:24 +0000 Subject: [PATCH] =?UTF-8?q?feat:=20Federation=20&=20Peers=20=E2=80=94=20sp?= =?UTF-8?q?lit=20nodes/peers,=20invite=20types,=20cleanup=20dead=20nodes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Page title: "Federation & Peers" - "Link Your Nodes" generates trusted invite, "Invite a Peer" generates observer invite - "Your Nodes" section shows trusted nodes, "Peers" section shows observer/untrusted - "Remove Dead Nodes" button cleans up unreachable nodes with no last_seen - DID in header with "Copied!" feedback - Node count in section headers Co-Authored-By: Claude Opus 4.6 (1M context) --- neode-ui/src/views/Federation.vue | 112 +++++++++++++++++++++++++++--- 1 file changed, 101 insertions(+), 11 deletions(-) diff --git a/neode-ui/src/views/Federation.vue b/neode-ui/src/views/Federation.vue index 1f4e5680..4717193f 100644 --- a/neode-ui/src/views/Federation.vue +++ b/neode-ui/src/views/Federation.vue @@ -73,32 +73,52 @@ @@ -449,6 +508,11 @@ const nodes = ref([]) const loading = ref(true) const error = ref('') const selectedNode = ref(null) +const inviteType = ref<'trusted' | 'observer'>('trusted') + +// Split nodes into Your Nodes (trusted) and Peers (observer/untrusted) +const trustedNodes = computed(() => nodes.value.filter(n => n.trust_level === 'trusted')) +const peerNodes = computed(() => nodes.value.filter(n => n.trust_level !== 'trusted')) const inviteCode = ref('') const generatingInvite = ref(false) @@ -716,6 +780,32 @@ function formatBytes(bytes?: number): string { return val.toFixed(1) + ' ' + units[i] } +// Dead node cleanup +const cleaningNodes = ref(false) +async function cleanupDeadNodes() { + cleaningNodes.value = true + try { + const deadNodes = nodes.value.filter(n => !isOnline(n) && (!n.last_seen || n.last_seen === 'never')) + for (const node of deadNodes) { + await rpcClient.federationRemoveNode(node.did) + } + await loadNodes() + } catch (e) { + error.value = e instanceof Error ? e.message : 'Cleanup failed' + } finally { + cleaningNodes.value = false + } +} + +function formatTimeAgo(iso: string): string { + if (!iso || iso === 'never') return 'never' + const ms = Date.now() - new Date(iso).getTime() + if (ms < 60000) return 'just now' + if (ms < 3600000) return `${Math.floor(ms / 60000)}m ago` + if (ms < 86400000) return `${Math.floor(ms / 3600000)}h ago` + return `${Math.floor(ms / 86400000)}d ago` +} + // DID rotation const showRotateModal = ref(false) const rotatePassword = ref('')