fix: Tor management system, bug fixes, federation name sync
Major changes: - Full Tor hidden service management via systemd path unit pattern (tor-helper.sh + archipelago-tor-helper.path/service) — respects NoNewPrivileges=yes, no sudo needed from backend - Container doctor: prefer system Tor over container, remove archy-tor - Deploy script: fix torrc generation (read correct services.json path), web apps map port 80→local port, enable both tor and tor@default - Federation: server rename pushes name to peers via background sync - Server name: fix root-owned file, optimistic store update - Mesh: local echo for sent messages, sendingArch loading state - Web5: Message button → Mesh redirect, node name lookup in messages - PeerFiles: show DID not onion in header - Connected Nodes: flex-1 instead of fixed max-h - Toast notifications route to Mesh - Deploy script: fix single-quote syntax in SSH block Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -60,9 +60,12 @@ async function loadArchMessages() {
|
||||
} catch { /* silent */ }
|
||||
}
|
||||
|
||||
const sendingArch = ref(false)
|
||||
|
||||
async function sendArchMessage() {
|
||||
if (!messageText.value.trim()) return
|
||||
sendError.value = ''
|
||||
sendingArch.value = true
|
||||
try {
|
||||
// Broadcast to all federated peers over Tor
|
||||
const nodes = await rpcClient.federationListNodes()
|
||||
@@ -74,12 +77,20 @@ async function sendArchMessage() {
|
||||
sent++
|
||||
} catch { /* some peers may be offline */ }
|
||||
}
|
||||
// Local echo — show the sent message immediately
|
||||
archMessages.value.push({
|
||||
from_pubkey: 'me',
|
||||
message: msg,
|
||||
timestamp: new Date().toISOString(),
|
||||
})
|
||||
messageText.value = ''
|
||||
if (sent === 0) sendError.value = 'No peers reachable'
|
||||
// Reload to see the message
|
||||
setTimeout(loadArchMessages, 2000)
|
||||
if (sent === 0) sendError.value = 'No peers reachable — message may arrive when they come online'
|
||||
// Also reload in background to pick up any replies
|
||||
setTimeout(loadArchMessages, 5000)
|
||||
} catch (e) {
|
||||
sendError.value = e instanceof Error ? e.message : 'Send failed'
|
||||
} finally {
|
||||
sendingArch.value = false
|
||||
}
|
||||
}
|
||||
|
||||
@@ -314,8 +325,8 @@ const chatMessages = computed(() => {
|
||||
return archMessages.value.map((m, i) => ({
|
||||
id: i,
|
||||
peer_contact_id: -99,
|
||||
peer_name: m.from_pubkey.slice(0, 12) + '...',
|
||||
direction: 'received' as const,
|
||||
peer_name: m.from_pubkey === 'me' ? 'You' : (m.from_pubkey.slice(0, 12) + '...'),
|
||||
direction: (m.from_pubkey === 'me' ? 'sent' : 'received') as 'sent' | 'received',
|
||||
plaintext: m.message,
|
||||
timestamp: m.timestamp,
|
||||
delivered: true,
|
||||
@@ -747,10 +758,10 @@ function truncatePubkey(hex: string | null): string {
|
||||
/>
|
||||
<button
|
||||
class="glass-button mesh-chat-send-btn"
|
||||
:disabled="!messageText.trim() || mesh.sending"
|
||||
:disabled="!messageText.trim() || mesh.sending || sendingArch"
|
||||
@click="handleSendMessage"
|
||||
>
|
||||
{{ mesh.sending ? '...' : 'Send' }}
|
||||
{{ (mesh.sending || sendingArch) ? '...' : 'Send' }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -31,7 +31,8 @@
|
||||
</div>
|
||||
<div class="hidden md:block">
|
||||
<h1 class="text-2xl font-bold text-white">{{ peerDisplayName }}</h1>
|
||||
<p class="text-sm text-white/50">{{ currentPeer?.onion || 'Peer files' }}</p>
|
||||
<p v-if="currentPeer?.did" class="text-sm text-white/50 font-mono truncate max-w-md" :title="currentPeer.did">{{ currentPeer.did }}</p>
|
||||
<p v-else class="text-sm text-white/50">Peer files</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -275,7 +275,7 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 xl:grid-cols-2 gap-6 mb-6">
|
||||
<div class="grid grid-cols-1 2xl:grid-cols-2 gap-6 mb-6">
|
||||
<!-- Network Interfaces -->
|
||||
<div class="glass-card p-6">
|
||||
<div class="flex items-center justify-between mb-4">
|
||||
@@ -326,8 +326,8 @@
|
||||
<div class="flex items-center justify-between mb-4">
|
||||
<div class="flex items-center gap-3">
|
||||
<div class="relative shrink-0">
|
||||
<div class="w-3 h-3 rounded-full" :class="torRunning ? 'bg-green-400' : 'bg-red-400'"></div>
|
||||
<div v-if="torRunning" class="absolute inset-0 w-3 h-3 rounded-full bg-green-400 animate-ping opacity-75"></div>
|
||||
<div class="w-3 h-3 rounded-full" :class="torDaemonRunning ? 'bg-green-400' : 'bg-red-400'"></div>
|
||||
<div v-if="torDaemonRunning" class="absolute inset-0 w-3 h-3 rounded-full bg-green-400 animate-ping opacity-75"></div>
|
||||
</div>
|
||||
<div>
|
||||
<h2 class="text-xl font-semibold text-white/96">Tor Services</h2>
|
||||
@@ -335,17 +335,17 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<button @click="cleanupRotatedServices" :disabled="torCleaning" class="glass-button px-4 py-2 rounded-lg text-sm flex items-center gap-2">
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" />
|
||||
</svg>
|
||||
{{ torCleaning ? 'Cleaning...' : 'Cleanup Old' }}
|
||||
</button>
|
||||
<button @click="loadTorServices" class="glass-button px-4 py-2 rounded-lg text-sm flex items-center gap-2">
|
||||
<button @click="restartTor" :disabled="torRestarting" class="glass-button px-3 py-2 rounded-lg text-sm flex items-center gap-2">
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15" />
|
||||
</svg>
|
||||
Refresh
|
||||
{{ torRestarting ? 'Restarting...' : 'Restart Tor' }}
|
||||
</button>
|
||||
<button @click="showAddServiceModal = true" class="glass-button px-3 py-2 rounded-lg text-sm flex items-center gap-2">
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4v16m8-8H4" />
|
||||
</svg>
|
||||
Add Service
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -354,9 +354,16 @@
|
||||
<div v-else class="space-y-2">
|
||||
<div v-for="svc in torServices" :key="svc.name" class="bg-black/20 rounded-xl border border-white/10 p-3 flex items-center justify-between gap-3">
|
||||
<div class="flex-1 min-w-0">
|
||||
<p class="text-white text-sm font-medium">{{ svc.name }}</p>
|
||||
<div class="flex items-center gap-2">
|
||||
<p class="text-white text-sm font-medium">{{ svc.name }}</p>
|
||||
<span class="text-white/30 text-xs">:{{ svc.local_port }}</span>
|
||||
<span v-if="svc.protocol" class="text-xs px-1.5 py-0.5 rounded bg-blue-500/20 text-blue-400">protocol</span>
|
||||
<span v-else-if="!svc.unauthenticated" class="text-xs px-1.5 py-0.5 rounded bg-green-500/20 text-green-400">auth</span>
|
||||
<span v-else class="text-xs px-1.5 py-0.5 rounded bg-amber-500/20 text-amber-400">open</span>
|
||||
</div>
|
||||
<p v-if="svc.onion_address" class="text-amber-300/80 text-xs font-mono truncate cursor-pointer" :title="svc.onion_address" @click="copyTorAddress(svc.onion_address)">{{ svc.onion_address }}</p>
|
||||
<p v-else class="text-white/30 text-xs">No .onion address</p>
|
||||
<p v-else-if="svc.enabled" class="text-white/30 text-xs">Waiting for .onion address...</p>
|
||||
<p v-else class="text-white/30 text-xs">Disabled</p>
|
||||
</div>
|
||||
<div class="flex items-center gap-2 shrink-0">
|
||||
<button
|
||||
@@ -367,11 +374,116 @@
|
||||
>
|
||||
{{ torRotating === svc.name ? 'Rotating...' : 'Rotate' }}
|
||||
</button>
|
||||
<button
|
||||
v-if="svc.name !== 'archipelago'"
|
||||
@click="deleteService(svc.name)"
|
||||
:disabled="torDeleting === svc.name"
|
||||
class="glass-button px-2 py-1.5 rounded-lg text-xs text-red-400 hover:text-red-300"
|
||||
:title="'Delete ' + svc.name + ' hidden service'"
|
||||
>
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" />
|
||||
</svg>
|
||||
</button>
|
||||
<ToggleSwitch :model-value="svc.enabled" @update:model-value="toggleTorApp(svc.name, $event)" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Add Tor Service Modal -->
|
||||
<Teleport to="body">
|
||||
<div v-if="showAddServiceModal" class="fixed inset-0 z-50 flex items-center justify-center p-4 bg-black/60 backdrop-blur-md" @click.self="showAddServiceModal = false" @keydown.escape="showAddServiceModal = false">
|
||||
<div class="glass-card p-6 max-w-md w-full">
|
||||
<h3 class="text-lg font-semibold text-white mb-4">Add Tor Hidden Service</h3>
|
||||
|
||||
<!-- Tabs: Installed Apps | Manual -->
|
||||
<div class="flex gap-1 mb-4 border-b border-white/10">
|
||||
<button
|
||||
@click="addServiceTab = 'apps'"
|
||||
class="px-4 py-2 text-sm font-medium rounded-t-lg transition-colors"
|
||||
:class="addServiceTab === 'apps' ? 'bg-white/10 text-white' : 'text-white/60 hover:text-white/80 hover:bg-white/5'"
|
||||
>
|
||||
Installed Apps
|
||||
</button>
|
||||
<button
|
||||
@click="addServiceTab = 'manual'"
|
||||
class="px-4 py-2 text-sm font-medium rounded-t-lg transition-colors"
|
||||
:class="addServiceTab === 'manual' ? 'bg-white/10 text-white' : 'text-white/60 hover:text-white/80 hover:bg-white/5'"
|
||||
>
|
||||
Manual
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Installed Apps tab -->
|
||||
<div v-if="addServiceTab === 'apps'">
|
||||
<p class="text-white/60 text-sm mb-3">Select an installed app to create a .onion address for it.</p>
|
||||
<div v-if="availableAppsForTor.length === 0" class="p-4 text-center text-white/40 text-sm">
|
||||
All installed apps already have Tor services.
|
||||
</div>
|
||||
<div v-else class="space-y-2 max-h-64 overflow-y-auto">
|
||||
<button
|
||||
v-for="app in availableAppsForTor"
|
||||
:key="app.id"
|
||||
@click="createServiceForApp(app.id)"
|
||||
:disabled="addingService"
|
||||
class="w-full flex items-center justify-between p-3 bg-white/5 rounded-lg hover:bg-white/10 transition-colors text-left"
|
||||
>
|
||||
<div class="min-w-0">
|
||||
<p class="text-sm font-medium text-white">{{ app.title }}</p>
|
||||
<p class="text-xs text-white/40">{{ app.id }}</p>
|
||||
</div>
|
||||
<span class="text-xs text-orange-400 shrink-0">+ Add</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Manual tab -->
|
||||
<div v-if="addServiceTab === 'manual'">
|
||||
<p class="text-white/60 text-sm mb-3">Create a .onion address for any local service.</p>
|
||||
<div class="space-y-4">
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-white/80 mb-1">Service Name</label>
|
||||
<input
|
||||
v-model="newServiceName"
|
||||
class="w-full px-3 py-2 rounded-lg bg-white/10 text-white border border-white/20 focus:border-orange-500 focus:ring-1 focus:ring-orange-500"
|
||||
placeholder="my-app"
|
||||
maxlength="64"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-white/80 mb-1">Local Port</label>
|
||||
<input
|
||||
v-model.number="newServicePort"
|
||||
type="number"
|
||||
min="1"
|
||||
max="65535"
|
||||
class="w-full px-3 py-2 rounded-lg bg-white/10 text-white border border-white/20 focus:border-orange-500 focus:ring-1 focus:ring-orange-500"
|
||||
placeholder="8080"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex gap-3 mt-6">
|
||||
<button
|
||||
@click="createService"
|
||||
:disabled="!newServiceName.trim() || !newServicePort || addingService"
|
||||
class="flex-1 px-4 py-2 rounded-lg bg-orange-500 text-white font-medium hover:bg-orange-600 disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
|
||||
>
|
||||
{{ addingService ? 'Creating...' : 'Create Service' }}
|
||||
</button>
|
||||
<button
|
||||
@click="showAddServiceModal = false"
|
||||
class="px-4 py-2 rounded-lg bg-white/10 text-white font-medium hover:bg-white/20 transition-colors"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p v-if="addServiceError" class="mt-3 text-sm text-red-400">{{ addServiceError }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</Teleport>
|
||||
</div>
|
||||
|
||||
<!-- WiFi Scan Modal -->
|
||||
@@ -511,8 +623,11 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, onMounted, watch } from 'vue'
|
||||
import { rpcClient } from '@/api/rpc-client'
|
||||
import { useAppStore } from '@/stores/app'
|
||||
import ToggleSwitch from '@/components/ToggleSwitch.vue'
|
||||
|
||||
const appStore = useAppStore()
|
||||
|
||||
// Connected nodes
|
||||
const connectedNodes = ref(0)
|
||||
|
||||
@@ -797,28 +912,52 @@ interface TorServiceInfo {
|
||||
local_port: number
|
||||
onion_address: string | null
|
||||
enabled: boolean
|
||||
unauthenticated: boolean
|
||||
protocol: boolean
|
||||
}
|
||||
|
||||
const torServices = ref<TorServiceInfo[]>([])
|
||||
const torServicesLoading = ref(false)
|
||||
const torCleaning = ref(false)
|
||||
const torDaemonRunning = ref(false)
|
||||
const torRestarting = ref(false)
|
||||
const torRotating = ref<string | false>(false)
|
||||
const torDeleting = ref<string | false>(false)
|
||||
|
||||
const torRunning = computed(() => torServices.value.length > 0 && torServices.value.some(s => s.onion_address))
|
||||
// Add service modal
|
||||
const showAddServiceModal = ref(false)
|
||||
const addServiceTab = ref<'apps' | 'manual'>('apps')
|
||||
const newServiceName = ref('')
|
||||
const newServicePort = ref<number | null>(null)
|
||||
const addingService = ref(false)
|
||||
const addServiceError = ref('')
|
||||
|
||||
// Installed apps that don't already have a Tor service
|
||||
const availableAppsForTor = computed(() => {
|
||||
const existingNames = new Set(torServices.value.map(s => s.name))
|
||||
const pkgs = appStore.packages
|
||||
return Object.entries(pkgs)
|
||||
.filter(([id]) => !existingNames.has(id))
|
||||
.map(([id, pkg]) => ({
|
||||
id,
|
||||
title: (pkg as { manifest?: { title?: string } })?.manifest?.title || id,
|
||||
}))
|
||||
.sort((a, b) => a.title.localeCompare(b.title))
|
||||
})
|
||||
|
||||
async function loadTorServices() {
|
||||
torServicesLoading.value = true
|
||||
try {
|
||||
const res = await rpcClient.call<{ services: TorServiceInfo[] }>({ method: 'tor.list-services' })
|
||||
const res = await rpcClient.call<{ services: TorServiceInfo[]; tor_running: boolean }>({ method: 'tor.list-services' })
|
||||
torServices.value = res.services || []
|
||||
torDaemonRunning.value = res.tor_running ?? false
|
||||
} catch {
|
||||
torServices.value = []
|
||||
torDaemonRunning.value = false
|
||||
} finally {
|
||||
torServicesLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const torRotating = ref<string | false>(false)
|
||||
|
||||
function copyTorAddress(address: string) {
|
||||
navigator.clipboard.writeText(address)
|
||||
logsToast.value = 'Onion address copied to clipboard'
|
||||
@@ -827,7 +966,7 @@ function copyTorAddress(address: string) {
|
||||
|
||||
async function toggleTorApp(appId: string, enabled: boolean) {
|
||||
try {
|
||||
await rpcClient.call({ method: 'tor.toggle-app', params: { app_id: appId, enabled } })
|
||||
await rpcClient.call({ method: 'tor.toggle-app', params: { app_id: appId, enabled }, timeout: 90000 })
|
||||
await loadTorServices()
|
||||
} catch (e) {
|
||||
if (import.meta.env.DEV) console.warn('Failed to toggle Tor app:', e)
|
||||
@@ -837,7 +976,7 @@ async function toggleTorApp(appId: string, enabled: boolean) {
|
||||
async function rotateService(name: string) {
|
||||
torRotating.value = name
|
||||
try {
|
||||
await rpcClient.call({ method: 'tor.rotate-service', params: { name } })
|
||||
await rpcClient.call({ method: 'tor.rotate-service', params: { name }, timeout: 90000 })
|
||||
await loadTorServices()
|
||||
} catch (e) {
|
||||
if (import.meta.env.DEV) console.warn('Failed to rotate Tor address:', e)
|
||||
@@ -846,15 +985,71 @@ async function rotateService(name: string) {
|
||||
}
|
||||
}
|
||||
|
||||
async function cleanupRotatedServices() {
|
||||
torCleaning.value = true
|
||||
async function restartTor() {
|
||||
torRestarting.value = true
|
||||
try {
|
||||
await rpcClient.call({ method: 'tor.cleanup-rotated' })
|
||||
await rpcClient.call({ method: 'tor.restart', timeout: 90000 })
|
||||
await loadTorServices()
|
||||
logsToast.value = 'Tor restarted successfully'
|
||||
setTimeout(() => { logsToast.value = '' }, 3000)
|
||||
} catch (e) {
|
||||
if (import.meta.env.DEV) console.warn('Failed to cleanup rotated Tor services:', e)
|
||||
logsToast.value = 'Failed to restart Tor'
|
||||
setTimeout(() => { logsToast.value = '' }, 5000)
|
||||
if (import.meta.env.DEV) console.warn('Failed to restart Tor:', e)
|
||||
} finally {
|
||||
torCleaning.value = false
|
||||
torRestarting.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function createServiceForApp(appId: string) {
|
||||
addServiceError.value = ''
|
||||
addingService.value = true
|
||||
try {
|
||||
// Backend knows the port from known_service_port() — pass 0 to let it auto-detect
|
||||
await rpcClient.call({ method: 'tor.create-service', params: { name: appId, local_port: 0 }, timeout: 90000 })
|
||||
showAddServiceModal.value = false
|
||||
await loadTorServices()
|
||||
logsToast.value = `Tor service for "${appId}" created`
|
||||
setTimeout(() => { logsToast.value = '' }, 3000)
|
||||
} catch (e) {
|
||||
addServiceError.value = e instanceof Error ? e.message : 'Failed to create service'
|
||||
} finally {
|
||||
addingService.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function createService() {
|
||||
const name = newServiceName.value.trim()
|
||||
const port = newServicePort.value
|
||||
if (!name || !port) return
|
||||
addServiceError.value = ''
|
||||
addingService.value = true
|
||||
try {
|
||||
await rpcClient.call({ method: 'tor.create-service', params: { name, local_port: port }, timeout: 90000 })
|
||||
showAddServiceModal.value = false
|
||||
newServiceName.value = ''
|
||||
newServicePort.value = null
|
||||
await loadTorServices()
|
||||
logsToast.value = `Tor service "${name}" created`
|
||||
setTimeout(() => { logsToast.value = '' }, 3000)
|
||||
} catch (e) {
|
||||
addServiceError.value = e instanceof Error ? e.message : 'Failed to create service'
|
||||
} finally {
|
||||
addingService.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function deleteService(name: string) {
|
||||
torDeleting.value = name
|
||||
try {
|
||||
await rpcClient.call({ method: 'tor.delete-service', params: { name }, timeout: 90000 })
|
||||
await loadTorServices()
|
||||
logsToast.value = `Tor service "${name}" deleted`
|
||||
setTimeout(() => { logsToast.value = '' }, 3000)
|
||||
} catch (e) {
|
||||
if (import.meta.env.DEV) console.warn('Failed to delete Tor service:', e)
|
||||
} finally {
|
||||
torDeleting.value = false
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1125,6 +1125,8 @@ async function saveServerName() {
|
||||
}
|
||||
try {
|
||||
await rpcClient.call({ method: 'server.set-name', params: { name } })
|
||||
// Optimistically update the store so UI reflects the change immediately
|
||||
store.updateServerName(name)
|
||||
} catch (e) {
|
||||
if (import.meta.env.DEV) console.error('Failed to rename server:', e)
|
||||
}
|
||||
|
||||
@@ -156,7 +156,7 @@
|
||||
Nodes
|
||||
</button>
|
||||
<button
|
||||
@click="showSendMessageModal = true"
|
||||
@click="router.push('/dashboard/mesh')"
|
||||
class="flex-1 px-3 py-1.5 glass-button glass-button-sm rounded text-xs font-medium text-white/90 hover:text-white transition-colors"
|
||||
>
|
||||
Message
|
||||
@@ -678,7 +678,7 @@
|
||||
</div>
|
||||
|
||||
<!-- Peers tab -->
|
||||
<div v-show="nodesContainerTab === 'peers'" class="space-y-2 max-h-48 overflow-y-auto">
|
||||
<div v-show="nodesContainerTab === 'peers'" class="space-y-2 flex-1 overflow-y-auto">
|
||||
<div v-if="peers.length === 0" class="p-4 text-center text-white/60 text-sm">
|
||||
{{ t('web5.noPeers') }}
|
||||
</div>
|
||||
@@ -695,7 +695,7 @@
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
@click="showSendMessageModal = true; sendMessageTo = p.onion"
|
||||
@click="router.push('/dashboard/mesh')"
|
||||
class="px-2 py-1 text-xs rounded bg-orange-500/20 text-orange-400 hover:bg-orange-500/30 transition-colors shrink-0"
|
||||
>
|
||||
{{ t('web5.message') }}
|
||||
@@ -704,7 +704,7 @@
|
||||
</div>
|
||||
|
||||
<!-- Messages tab -->
|
||||
<div v-show="nodesContainerTab === 'messages'" class="space-y-2 max-h-64 overflow-y-auto">
|
||||
<div v-show="nodesContainerTab === 'messages'" class="space-y-2 flex-1 overflow-y-auto">
|
||||
<div v-if="loadingMessages" class="p-4 text-center text-white/60 text-sm">
|
||||
{{ t('common.loading') }}
|
||||
</div>
|
||||
@@ -717,7 +717,7 @@
|
||||
class="p-3 bg-white/5 rounded-lg border-l-2 border-orange-500/50"
|
||||
>
|
||||
<div class="flex items-center justify-between gap-2 mb-1">
|
||||
<p class="text-xs font-mono text-white/60 truncate" :title="m.from_pubkey">{{ (m.from_pubkey || '').slice(0, 16) }}...</p>
|
||||
<p class="text-xs font-mono text-white/60 truncate" :title="m.from_pubkey">{{ peerNameFromPubkey(m.from_pubkey) }}</p>
|
||||
<span class="text-xs text-white/40 shrink-0">{{ formatMessageTime(m.timestamp) }}</span>
|
||||
</div>
|
||||
<p class="text-sm text-white/90 break-words">{{ m.message }}</p>
|
||||
@@ -725,7 +725,7 @@
|
||||
</div>
|
||||
|
||||
<!-- Requests tab -->
|
||||
<div v-show="nodesContainerTab === 'requests'" class="space-y-2 max-h-64 overflow-y-auto">
|
||||
<div v-show="nodesContainerTab === 'requests'" class="space-y-2 flex-1 overflow-y-auto">
|
||||
<div v-if="loadingRequests" class="p-4 text-center text-white/60 text-sm">
|
||||
{{ t('common.loading') }}
|
||||
</div>
|
||||
@@ -739,7 +739,7 @@
|
||||
>
|
||||
<div class="flex items-start justify-between gap-3">
|
||||
<div class="min-w-0 flex-1">
|
||||
<p class="text-xs font-mono text-white/70 truncate" :title="req.from_did">{{ req.from_did }}</p>
|
||||
<p class="text-xs font-mono text-white/70 truncate" :title="req.from_did">{{ peerNameFromPubkey(req.from_did) }}</p>
|
||||
<p v-if="req.message" class="text-sm text-white/80 mt-1 break-words">{{ req.message }}</p>
|
||||
<p class="text-xs text-white/40 mt-1">{{ formatMessageTime(req.created_at) }}</p>
|
||||
</div>
|
||||
@@ -2647,6 +2647,12 @@ const peerReachableLocal = ref<Record<string, boolean>>({})
|
||||
const peerReachable = computed(() => ({ ...appStore.peerHealth, ...peerReachableLocal.value }))
|
||||
const connectedNodesCount = computed(() => peers.value.length)
|
||||
|
||||
function peerNameFromPubkey(pubkey: string): string {
|
||||
const peer = peers.value.find(p => p.pubkey === pubkey || p.onion === pubkey)
|
||||
if (peer?.name) return peer.name
|
||||
return (pubkey || '').slice(0, 16) + '...'
|
||||
}
|
||||
|
||||
// Hardware wallet detection
|
||||
interface HwWalletDevice {
|
||||
type: string
|
||||
|
||||
Reference in New Issue
Block a user