From 85c29cd928c62279866ae62dae40c322d77a8be6 Mon Sep 17 00:00:00 2001 From: archipelago Date: Tue, 14 Jul 2026 06:45:15 -0400 Subject: [PATCH] feat(ui): Node Visibility = one public-discoverability switch + inline node list MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The old tri-state (hidden/discoverable/public) was confusing — its 'discoverable' level implied discovery by already-connected federated peers, which is meaningless. The card is now a single Enable switch (global ToggleSwitch style) that drives nostr presence publishing: ON = public — anyone can find the node and request a connection (requests join as Peer/observer on approval, never trusted). The card also lists nostr-discoverable nodes inline with Request-to-Peer buttons (same handshake.connect flow as the Federation Discover modal, which stays available in Federation & Peers). Co-Authored-By: Claude Fable 5 --- .../src/views/web5/Web5NodeVisibility.vue | 183 +++++++++++++----- 1 file changed, 139 insertions(+), 44 deletions(-) diff --git a/neode-ui/src/views/web5/Web5NodeVisibility.vue b/neode-ui/src/views/web5/Web5NodeVisibility.vue index 5c2c157f..b2f848af 100644 --- a/neode-ui/src/views/web5/Web5NodeVisibility.vue +++ b/neode-ui/src/views/web5/Web5NodeVisibility.vue @@ -8,9 +8,13 @@ -
+

{{ t('web5.nodeVisibility') }}

-

{{ t('web5.nodeVisibilityDesc') }}

+

+ Make your node publicly discoverable. When enabled, anyone on the Nostr + network can find your node and request a connection — requests always + wait for your approval and join as a Peer, never trusted. +

@@ -20,32 +24,24 @@
- -
- + aria-label="Enable public discoverability" + @update:model-value="toggleDiscoverable" + />
- -
+ +

{{ t('web5.yourTorAddress') }}

@@ -59,8 +55,48 @@
+ +
+
+

Discoverable nodes

+ +
+
+ Searching relays… +
+
+ No discoverable nodes found yet. Nodes appear here as relays gossip their presence. +
+
+
+
+
{{ shortNpub(node.nostr_npub) }}
+
{{ node.did }}
+
version {{ node.version || '?' }}
+
+ +
+
+
+ -

+

{{ t('web5.discoverableWarning') }}

@@ -70,6 +106,7 @@ import { ref } from 'vue' import { useI18n } from 'vue-i18n' import { rpcClient } from '@/api/rpc-client' +import ToggleSwitch from '@/components/ToggleSwitch.vue' import { safeClipboardWrite } from './utils' import type { VisibilityLevel } from './types' @@ -87,37 +124,65 @@ const nodeVisibility = ref('hidden') const nodeOnionAddress = ref(null) const visibilityLoading = ref(false) const settingVisibility = ref(false) +const discoverEnabled = ref(false) -const visibilityOptions = [ - { value: 'hidden' as VisibilityLevel, label: 'Hidden', description: 'Your node is not discoverable by others' }, - { value: 'discoverable' as VisibilityLevel, label: 'Discoverable', description: 'Federated peers can find and connect to your node' }, - { value: 'public' as VisibilityLevel, label: 'Public', description: 'Accepting connections from any Archipelago node' }, -] +interface DiscoverableNode { + nostr_pubkey: string + nostr_npub: string + did: string + version: string +} + +const discoveredNodes = ref([]) +const discovering = ref(false) +const requestingPeer = ref(null) +const requestedPeers = ref(new Set()) + +function shortNpub(npub: string): string { + if (!npub) return 'unknown' + return npub.length > 21 ? `${npub.slice(0, 12)}…${npub.slice(-6)}` : npub +} async function loadVisibility() { visibilityLoading.value = true try { - const res = await rpcClient.call<{ visibility: string; onion_address?: string }>({ method: 'network.get-visibility' }) - nodeVisibility.value = (res.visibility as VisibilityLevel) || 'hidden' - nodeOnionAddress.value = res.onion_address || null + // Nostr discovery is the functional flag: when on, a presence event + // (DID + npub — never the onion) is published to public relays so + // ANYONE can find this node and request a connection. The legacy + // network.get-visibility tri-state is only read for the onion display. + const [disc, vis] = await Promise.all([ + rpcClient.nostrDiscoveryStatus(), + rpcClient + .call<{ visibility: string; onion_address?: string; tor_address?: string }>({ method: 'network.get-visibility' }) + .catch(() => null), + ]) + discoverEnabled.value = !!disc.enabled + nodeVisibility.value = (vis?.visibility as VisibilityLevel) || 'hidden' + nodeOnionAddress.value = vis?.onion_address || vis?.tor_address || null + if (discoverEnabled.value) void discoverNodes() } catch { - nodeVisibility.value = 'hidden' + discoverEnabled.value = false } finally { visibilityLoading.value = false } } -async function setVisibility(level: VisibilityLevel) { - if (settingVisibility.value || nodeVisibility.value === level) return +async function toggleDiscoverable(enabled: boolean) { + if (settingVisibility.value) return settingVisibility.value = true try { - const res = await rpcClient.call<{ visibility: string; onion_address?: string }>({ - method: 'network.set-visibility', - params: { visibility: level }, - }) - nodeVisibility.value = (res.visibility as VisibilityLevel) || level - nodeOnionAddress.value = res.onion_address || nodeOnionAddress.value - emit('toast', t('web5.visibilitySetTo', { level })) + // Public means public: the switch drives nostr presence publishing. + const res = await rpcClient.nostrSetDiscovery(enabled) + discoverEnabled.value = !!res.enabled + // Keep the legacy visibility string in sync (cosmetic; best-effort). + const level: VisibilityLevel = enabled ? 'public' : 'hidden' + rpcClient + .call({ method: 'network.set-visibility', params: { visibility: level } }) + .then(() => { nodeVisibility.value = level }) + .catch(() => {}) + emit('toast', enabled ? 'Node is now publicly discoverable' : 'Node hidden from discovery') + if (enabled) void discoverNodes() + else discoveredNodes.value = [] } catch { emit('toast', t('web5.failedToUpdateVisibility')) } finally { @@ -125,6 +190,36 @@ async function setVisibility(level: VisibilityLevel) { } } +async function discoverNodes() { + if (discovering.value) return + discovering.value = true + try { + const res = await rpcClient.handshakeDiscover() + discoveredNodes.value = res.nodes || [] + } catch { + // keep whatever we had; the empty-state copy explains relay gossip lag + } finally { + discovering.value = false + } +} + +async function requestToPeer(node: DiscoverableNode) { + if (requestingPeer.value) return + requestingPeer.value = node.nostr_pubkey + try { + // Connection requests always land as Peer (observer) on approval — + // never trusted — so a mistaken request can't hand over fleet access. + await rpcClient.handshakeConnect(node.nostr_pubkey) + requestedPeers.value.add(node.nostr_pubkey) + requestedPeers.value = new Set(requestedPeers.value) + emit('toast', 'Peer request sent — awaiting their approval') + } catch (e) { + emit('toast', e instanceof Error ? e.message : 'Failed to send peer request') + } finally { + requestingPeer.value = null + } +} + function copyOnionAddress() { if (!nodeOnionAddress.value) return safeClipboardWrite(nodeOnionAddress.value)