feat: deploy-to-target supports .253 + mesh/federation/VPN updates

- Add deploy_secondary() function for deploying to multiple LAN nodes
- --both now deploys to .198 and .253 (previously .198 only)
- Fleet deploy updated for 3 LAN nodes
- Mesh DM fixes: protocol frame format, DM-via-channel routing
- Federation pending requests, discover modal
- VPN status UI improvements
- Image versions and container specs updates

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Dorian
2026-04-18 11:07:08 -04:00
co-authored by Claude Opus 4.6
parent e210376e05
commit 9dd802998c
38 changed files with 3773 additions and 697 deletions
+143
View File
@@ -45,6 +45,54 @@
@clear-invite="inviteCode = ''"
/>
<!-- Nostr discoverability strip: opt-in toggle + Discover button.
Renders inline so the Federation page is the single place a user
manages everything related to peering. The toggle directly mutates
the `nostr_discovery_enabled` config flag backend defaults to OFF
and nothing is published until the user explicitly turns it on. -->
<div class="glass-card p-4 mb-6 flex flex-col sm:flex-row sm:items-center sm:justify-between gap-3">
<div class="min-w-0">
<div class="flex items-center gap-2">
<span class="text-sm font-medium text-white">Nostr discoverability</span>
<span
class="inline-block px-2 py-0.5 text-[10px] uppercase tracking-wide rounded"
:class="discoveryEnabled ? 'bg-green-500/20 text-green-300' : 'bg-white/10 text-white/50'"
>{{ discoveryEnabled ? 'On' : 'Off' }}</span>
</div>
<p class="text-xs text-white/60 mt-1">
When on, this node publishes a presence event (DID + npub only never an onion)
so other nodes can find you and request to peer. Inbound requests land in the
panel below for your approval. Off by default.
</p>
</div>
<div class="flex items-center gap-2 shrink-0">
<button
class="px-3 py-1.5 glass-button glass-button-sm rounded text-xs text-white/90 hover:text-white disabled:opacity-50"
:disabled="discoveryToggling"
@click="toggleDiscovery"
>
{{ discoveryToggling ? '…' : (discoveryEnabled ? 'Disable' : 'Enable') }}
</button>
<button
class="px-3 py-1.5 glass-button glass-button-sm rounded text-xs text-white/90 hover:text-white disabled:opacity-50"
:disabled="!discoveryEnabled"
@click="showDiscoverModal = true"
>
Discover Nodes
</button>
</div>
</div>
<div v-if="discoveryError" class="mb-4 text-xs text-red-400">{{ discoveryError }}</div>
<PendingRequestsPanel
:requests="pendingRequests"
:polling="pollingHandshake"
:busy-id="pendingBusyId"
@poll="pollHandshake"
@approve="approvePending"
@reject="rejectPending"
/>
<NodeList
:nodes="nodes"
:loading="loading"
@@ -82,6 +130,13 @@
@close="showJoinModal = false"
@join="joinFederation"
/>
<DiscoverModal
:visible="showDiscoverModal"
:outbound-sent="pendingRequests"
@close="showDiscoverModal = false"
@sent="loadPendingRequests"
/>
</div>
</template>
@@ -97,7 +152,10 @@ import QuickActions from './federation/QuickActions.vue'
import NodeList from './federation/NodeList.vue'
import NodeDetailModal from './federation/NodeDetailModal.vue'
import JoinModal from './federation/JoinModal.vue'
import PendingRequestsPanel from './federation/PendingRequestsPanel.vue'
import DiscoverModal from './federation/DiscoverModal.vue'
import type { FederatedNode, DwnStatus, SyncResult } from './federation/types'
import type { PendingPeerRequest } from '@/api/rpc-client'
import { nodeName, timeAgo } from './federation/utils'
const transportStore = useTransportStore()
@@ -205,6 +263,89 @@ const rotateSuccess = ref('')
// Dead node cleanup
const cleaningNodes = ref(false)
// Nostr discoverability + pending peer requests
const discoveryEnabled = ref(false)
const discoveryToggling = ref(false)
const discoveryError = ref('')
const showDiscoverModal = ref(false)
const pendingRequests = ref<PendingPeerRequest[]>([])
const pollingHandshake = ref(false)
const pendingBusyId = ref<string | null>(null)
async function loadDiscoveryState() {
try {
const result = await rpcClient.nostrDiscoveryStatus()
discoveryEnabled.value = !!result.enabled
} catch {
discoveryEnabled.value = false
}
}
async function toggleDiscovery() {
discoveryToggling.value = true
discoveryError.value = ''
const next = !discoveryEnabled.value
try {
const result = await rpcClient.nostrSetDiscovery(next)
discoveryEnabled.value = !!result.enabled
} catch (e: unknown) {
discoveryError.value = e instanceof Error ? e.message : 'Failed to toggle discoverability'
} finally {
discoveryToggling.value = false
}
}
async function loadPendingRequests() {
try {
const result = await rpcClient.federationListPendingRequests()
pendingRequests.value = result.requests
} catch (e: unknown) {
discoveryError.value = e instanceof Error ? e.message : 'Failed to load pending requests'
}
}
async function pollHandshake() {
pollingHandshake.value = true
discoveryError.value = ''
try {
await rpcClient.handshakePoll()
await loadPendingRequests()
// If a poll applied a PeerInvite, the federation node list also changed.
await loadNodes()
} catch (e: unknown) {
discoveryError.value = e instanceof Error ? e.message : 'Poll failed'
} finally {
pollingHandshake.value = false
}
}
async function approvePending(id: string) {
pendingBusyId.value = id
discoveryError.value = ''
try {
await rpcClient.federationApproveRequest(id)
await loadPendingRequests()
await loadNodes()
} catch (e: unknown) {
discoveryError.value = e instanceof Error ? e.message : 'Approve failed'
} finally {
pendingBusyId.value = null
}
}
async function rejectPending(id: string) {
pendingBusyId.value = id
discoveryError.value = ''
try {
await rpcClient.federationRejectRequest(id)
await loadPendingRequests()
} catch (e: unknown) {
discoveryError.value = e instanceof Error ? e.message : 'Reject failed'
} finally {
pendingBusyId.value = null
}
}
function isOnlineCheck(node: FederatedNode): boolean {
if (!node.last_seen) return false
const lastSeen = new Date(node.last_seen).getTime()
@@ -382,6 +523,8 @@ async function rotateDid(password: string) {
onMounted(async () => {
loadNodes()
loadDwnStatus()
loadDiscoveryState()
loadPendingRequests()
transportStore.fetchPeers()
try {
const result = await rpcClient.getNodeDid()