2026-01-24 22:59:20 +00:00
< template >
2026-03-14 17:12:41 +00:00
< div class = "pb-6" >
2026-01-24 22:59:20 +00:00
2026-03-30 16:35:06 +01:00
<!-- LUKS Encryption Badge -->
< div v-if = "diskEncrypted" class="mb-4 px-4 py-2.5 rounded-xl border bg-green-500/5 border-green-500/20 flex items-center gap-2.5" >
< svg xmlns = "http://www.w3.org/2000/svg" class = "h-4 w-4 text-green-400 shrink-0" fill = "none" viewBox = "0 0 24 24" stroke = "currentColor" >< path stroke -linecap =" round " stroke -linejoin =" round " stroke -width =" 2 " d = "M12 15v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 002 2zm10-10V7a4 4 0 00-8 0v4h8z" /></ svg >
< span class = "text-xs text-green-300/80 font-medium" > LUKS2 Encrypted Storage </ span >
</ div >
2026-03-11 10:44:56 +00:00
<!-- Disk Space Warning Banner -->
< div
v-if = "diskWarning"
class = "mb-6 p-4 rounded-xl border flex items-center justify-between"
: class = "diskWarning.level === 'critical'
? 'bg-red-500/10 border-red-500/30'
: 'bg-yellow-500/10 border-yellow-500/30'"
>
< div class = "flex items-center gap-3" >
< svg xmlns = "http://www.w3.org/2000/svg" class = "h-5 w-5 shrink-0" : class = "diskWarning.level === 'critical' ? 'text-red-400' : 'text-yellow-400'" fill = "none" viewBox = "0 0 24 24" stroke = "currentColor" >
< path stroke -linecap =" round " stroke -linejoin =" round " stroke -width =" 2 " d = "M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-2.5L13.732 4.5c-.77-.833-2.694-.833-3.464 0L3.34 16.5c-.77.833.192 2.5 1.732 2.5z" />
</ svg >
< div >
< p class = "text-sm font-medium" : class = "diskWarning.level === 'critical' ? 'text-red-300' : 'text-yellow-300'" >
{{ diskWarning . level === 'critical' ? 'Disk Space Critical' : 'Disk Space Warning' }}
</ p >
< p class = "text-xs text-white/60" >
{{ diskWarning . used_percent . toFixed ( 1 ) }} % used — {{ formatBytes ( diskWarning . free_bytes ) }} remaining
</ p >
</ div >
</ div >
< button
class = "glass-button glass-button-sm px-3 py-1.5 text-xs font-medium rounded"
:disabled = "diskCleaning"
@click ="runDiskCleanup"
>
{{ diskCleaning ? 'Cleaning...' : 'Clean Up' }}
</ button >
</ div >
2026-03-21 03:01:38 +00:00
<!-- Quick Actions -->
< QuickActionsCard
:services-running = "servicesRunning"
:restarting = "restarting"
:tor-status-label = "torStatusLabel"
:tor-status-color = "torStatusColor"
:checking-tor = "checkingTor"
:auto-sync-enabled = "autoSyncEnabled"
:log-count = "logCount"
@ restart -services =" restartServices "
@ check -tor =" checkTorStatus "
@ update : auto -sync-enabled =" autoSyncEnabled = $ event "
@ view -logs =" viewLogs "
/>
2026-01-24 22:59:20 +00:00
<!-- Overview Cards -->
2026-03-19 12:44:31 +00:00
< div class = "grid grid-cols-1 xl:grid-cols-2 gap-6 mb-8" >
2026-01-24 22:59:20 +00:00
<!-- Local Network Card -->
2026-03-30 13:35:02 +01:00
< div data -controller-container tabindex = "0" class = "glass-card p-6 flex flex-col transition-all hover:-translate-y-1" >
2026-02-17 15:03:34 +00:00
< div class = "flex items-start gap-4 mb-4 shrink-0" >
2026-01-24 22:59:20 +00:00
< div class = "flex-shrink-0 w-12 h-12 rounded-lg bg-white/10 flex items-center justify-center" >
< svg class = "w-6 h-6 text-white/80" fill = "none" stroke = "currentColor" viewBox = "0 0 24 24" >
< path stroke -linecap =" round " stroke -linejoin =" round " stroke -width =" 2 " d = "M8.111 16.404a5.5 5.5 0 017.778 0M12 20h.01m-7.08-7.071c3.904-3.905 10.236-3.905 14.141 0M1.394 9.393c5.857-5.857 15.355-5.857 21.213 0" />
</ svg >
</ div >
< div class = "flex-1" >
< h2 class = "text-xl font-semibold text-white mb-2" > Local Network </ h2 >
< p class = "text-white/70 text-sm mb-4" > OpenWRT - integrated router and network management </ p >
</ div >
</ div >
2026-02-17 15:03:34 +00:00
< div class = "space-y-3 flex-1 min-h-0" >
2026-03-11 00:04:26 +00:00
< template v-if = "networkLoading" >
< div v-for = "i in 4" :key="i" class="flex items-center justify-between p-3 bg-white/5 rounded-lg animate-pulse" >
< div class = "flex items-center gap-3" >
< div class = "w-5 h-5 bg-white/10 rounded" ></ div >
< div class = "w-24 h-4 bg-white/10 rounded" ></ div >
</ div >
< div class = "w-16 h-4 bg-white/10 rounded" ></ div >
2026-01-24 22:59:20 +00:00
</ div >
2026-03-11 00:04:26 +00:00
</ template >
2026-01-24 22:59:20 +00:00
2026-03-11 00:04:26 +00:00
< template v-else >
< div class = "flex items-center justify-between p-3 bg-white/5 rounded-lg" >
< div class = "flex items-center gap-3" >
2026-03-21 03:01:38 +00:00
< svg class = "w-5 h-5 text-white/60" fill = "none" stroke = "currentColor" viewBox = "0 0 24 24" >< path stroke -linecap =" round " stroke -linejoin =" round " stroke -width =" 2 " d = "M9 12l2 2 4-4m5.618-4.016A11.955 11.955 0 0112 2.944a11.955 11.955 0 01-8.618 3.04A12.02 12.02 0 003 9c0 5.591 3.824 10.29 9 11.622 5.176-1.332 9-6.03 9-11.622 0-1.042-.133-2.052-.382-3.016z" /></ svg >
2026-03-11 00:04:26 +00:00
< span class = "text-white/80 text-sm" > Firewall Active </ span >
</ div >
< span class = "text-green-400 text-sm font-medium" > Protected </ span >
2026-01-24 22:59:20 +00:00
</ div >
2026-03-11 00:04:26 +00:00
< div class = "flex items-center justify-between p-3 bg-white/5 rounded-lg" >
< div class = "flex items-center gap-3" >
2026-03-21 03:01:38 +00:00
< svg class = "w-5 h-5 text-white/60" fill = "none" stroke = "currentColor" viewBox = "0 0 24 24" >< path stroke -linecap =" round " stroke -linejoin =" round " stroke -width =" 2 " d = "M12 15v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 002 2zm10-10V7a4 4 0 00-8 0v4h8z" /></ svg >
2026-03-11 00:04:26 +00:00
< span class = "text-white/80 text-sm" > WiFi Networks </ span >
</ div >
< span class = "text-white/60 text-sm" >{{ networkData . wifiCount }}</ span >
2026-01-24 22:59:20 +00:00
</ div >
2026-03-11 00:04:26 +00:00
< div class = "flex items-center justify-between p-3 bg-white/5 rounded-lg" >
< div class = "flex items-center gap-3" >
2026-03-21 03:01:38 +00:00
< svg class = "w-5 h-5 text-white/60" fill = "none" stroke = "currentColor" viewBox = "0 0 24 24" >< path stroke -linecap =" round " stroke -linejoin =" round " stroke -width =" 2 " d = "M21 12a9 9 0 01-9 9m9-9a9 9 0 00-9-9m9 9H3m9 9a9 9 0 01-9-9m9 9c1.657 0 3-4.03 3-9s-1.343-9-3-9m0 18c-1.657 0-3-4.03-3-9s1.343-9 3-9m-9 9a9 9 0 019-9" /></ svg >
2026-03-19 16:44:46 +00:00
< span class = "text-white/80 text-sm" > Tor </ span >
2026-03-11 00:04:26 +00:00
</ div >
2026-03-19 22:56:37 +00:00
< span class = "text-sm" : class = "torStatusLabel === 'running' ? 'text-green-400' : 'text-white/60'" >{{ torStatusLabel === 'running' ? 'Connected' : torStatusLabel === 'checking' ? 'Checking...' : 'Stopped' }}</ span >
2026-01-24 22:59:20 +00:00
</ div >
2026-03-11 00:04:26 +00:00
< div class = "flex items-center justify-between p-3 bg-white/5 rounded-lg" >
< div class = "flex items-center gap-3" >
2026-03-21 03:01:38 +00:00
< svg class = "w-5 h-5 text-white/60" fill = "none" stroke = "currentColor" viewBox = "0 0 24 24" >< path stroke -linecap =" round " stroke -linejoin =" round " stroke -width =" 2 " d = "M13 10V3L4 14h7v7l9-11h-7z" /></ svg >
2026-03-11 00:04:26 +00:00
< span class = "text-white/80 text-sm" > Port Forwarding </ span >
</ div >
< span class = "text-white/60 text-sm" >{{ networkData . forwardCount }}</ span >
</ div >
2026-04-08 15:00:00 +02:00
< div class = "flex items-center justify-between p-3 bg-white/5 rounded-lg" >
< div class = "flex items-center gap-3" >
< svg class = "w-5 h-5 text-white/60" fill = "none" stroke = "currentColor" viewBox = "0 0 24 24" >< path stroke -linecap =" round " stroke -linejoin =" round " stroke -width =" 2 " d = "M12 15v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 002 2zm10-10V7a4 4 0 00-8 0v4h8z" /></ svg >
< span class = "text-white/80 text-sm" > VPN </ span >
2026-03-11 10:44:56 +00:00
</ div >
2026-04-08 15:00:00 +02:00
< span class = "text-sm" : class = "networkData.vpnConnected ? 'text-green-400' : 'text-white/40'" >
{{ networkData . vpnConnected ? 'WireGuard / NostrVPN' : 'Not Connected' }}
</ span >
2026-03-11 10:44:56 +00:00
</ div >
< button class = "w-full flex items-center justify-between p-3 bg-white/5 rounded-lg hover:bg-white/10 transition-colors text-left" @click ="showDnsModal = true" >
< div class = "flex items-center gap-3" >
2026-03-21 03:01:38 +00:00
< svg class = "w-5 h-5 text-white/60" fill = "none" stroke = "currentColor" viewBox = "0 0 24 24" >< path stroke -linecap =" round " stroke -linejoin =" round " stroke -width =" 2 " d = "M21 12a9 9 0 01-9 9m9-9a9 9 0 00-9-9m9 9H3m9 9a9 9 0 01-9-9m9 9c1.657 0 3-4.03 3-9s-1.343-9-3-9m0 18c-1.657 0-3-4.03-3-9s1.343-9 3-9" /></ svg >
2026-03-11 10:44:56 +00:00
< span class = "text-white/80 text-sm" > DNS </ span >
</ div >
< span class = "text-sm" : class = "networkData.dnsProvider !== 'system' ? 'text-green-400' : 'text-white/60'" >
{{ dnsDisplayLabel }}
</ span >
</ button >
2026-03-11 00:04:26 +00:00
</ template >
2026-01-24 22:59:20 +00:00
</ div >
2026-03-14 17:12:41 +00:00
< button disabled title = "Coming Soon" class = "mt-4 w-full min-h-[44px] glass-button rounded-lg text-sm font-medium opacity-50 cursor-not-allowed flex items-center justify-center" >
2026-01-24 22:59:20 +00:00
Manage Local Network
</ button >
</ div >
<!-- Web3 Card -->
2026-03-30 13:35:02 +01:00
< div data -controller-container tabindex = "0" class = "glass-card p-6 flex flex-col transition-all hover:-translate-y-1" >
2026-02-17 15:03:34 +00:00
< div class = "flex items-start gap-4 mb-4 shrink-0" >
2026-01-24 22:59:20 +00:00
< div class = "flex-shrink-0 w-12 h-12 rounded-lg bg-white/10 flex items-center justify-center" >
2026-03-21 03:01:38 +00:00
< svg class = "w-6 h-6 text-white/80" fill = "none" stroke = "currentColor" viewBox = "0 0 24 24" >< path stroke -linecap =" round " stroke -linejoin =" round " stroke -width =" 2 " d = "M21 12a9 9 0 01-9 9m9-9a9 9 0 00-9-9m9 9H3m9 9a9 9 0 01-9-9m9 9c1.657 0 3-4.03 3-9s-1.343-9-3-9m0 18c-1.657 0-3-4.03-3-9s1.343-9 3-9m-9 9a9 9 0 019-9" /></ svg >
2026-01-24 22:59:20 +00:00
</ div >
< div class = "flex-1" >
< h2 class = "text-xl font-semibold text-white mb-2" > Web3 </ h2 >
< p class = "text-white/70 text-sm mb-4" > Decentralized web hosting and services </ p >
</ div >
</ div >
2026-02-17 15:03:34 +00:00
< div class = "space-y-3 flex-1 min-h-0" >
2026-03-21 03:01:38 +00:00
< div v-for = "item in ['Hosted Websites', 'SSL Certificates', 'IPFS Storage', 'ENS Domains']" :key="item" class="flex items-center justify-between p-3 bg-white/5 rounded-lg" >
2026-01-24 22:59:20 +00:00
< div class = "flex items-center gap-3" >
2026-03-21 03:01:38 +00:00
< svg class = "w-5 h-5 text-white/60" fill = "none" stroke = "currentColor" viewBox = "0 0 24 24" >< path stroke -linecap =" round " stroke -linejoin =" round " stroke -width =" 2 " d = "M21 12a9 9 0 01-9 9m9-9a9 9 0 00-9-9m9 9H3m9 9a9 9 0 01-9-9m9 9c1.657 0 3-4.03 3-9s-1.343-9-3-9m0 18c-1.657 0-3-4.03-3-9s1.343-9 3-9m-9 9a9 9 0 019-9" /></ svg >
< span class = "text-white/80 text-sm" >{{ item }}</ span >
2026-01-24 22:59:20 +00:00
</ div >
2026-03-11 00:05:12 +00:00
< span class = "text-white/40 text-xs px-2 py-0.5 bg-white/5 rounded-full" > Coming Soon </ span >
2026-01-24 22:59:20 +00:00
</ div >
</ div >
2026-03-14 17:12:41 +00:00
< button disabled title = "Coming Soon" class = "mt-4 w-full min-h-[44px] glass-button rounded-lg text-sm font-medium opacity-50 cursor-not-allowed flex items-center justify-center" >
2026-01-24 22:59:20 +00:00
Manage Web3 Services
</ button >
</ div >
</ div >
2026-03-11 00:35:55 +00:00
2026-04-08 15:00:00 +02:00
<!-- VPN Card -->
< div class = "glass-card p-6 mb-6 transition-all hover:-translate-y-1" >
< div class = "flex items-center justify-between mb-4" >
< div class = "flex items-center gap-3" >
< div class = "flex-shrink-0 w-10 h-10 rounded-lg bg-white/10 flex items-center justify-center" >
< svg class = "w-5 h-5 text-white/80" fill = "none" stroke = "currentColor" viewBox = "0 0 24 24" >< path stroke -linecap =" round " stroke -linejoin =" round " stroke -width =" 2 " d = "M12 15v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 002 2zm10-10V7a4 4 0 00-8 0v4h8z" /></ svg >
</ div >
< div >
< h2 class = "text-lg font-semibold text-white" > VPN </ h2 >
< p class = "text-xs text-white/50" > WireGuard + NostrVPN mesh </ p >
</ div >
</ div >
< button @click ="showAddDeviceModal = true; showingNewDevice = true" class = "glass-button px-4 py-2 text-sm" > Add Device </ button >
</ div >
<!-- Node npub for sharing -->
< div v-if = "nodeNpub" class="mb-3 px-3 py-2 bg-white/5 rounded-lg flex items-center justify-between" >
< div class = "flex items-center gap-2 min-w-0" >
< span class = "text-xs text-white/40 shrink-0" > npub </ span >
< span class = "text-xs font-mono text-white/60 truncate" >{{ nodeNpub }}</ span >
</ div >
< button @click ="copyNpub" class = "text-xs text-white/40 hover:text-white shrink-0 ml-2" >{{ copiedNpub ? 'Copied' : 'Copy' }}</ button >
</ div >
<!-- Private relay URLs for mesh VPN peer discovery -->
< div v-if = "relayOnion" class="mb-3 px-3 py-2 bg-white/5 rounded-lg flex items-center justify-between" >
< div class = "flex items-center gap-2 min-w-0" >
< span class = "text-xs text-purple-400/70 shrink-0" > relay ( tor )</ span >
< span class = "text-xs font-mono text-white/60 truncate" >{{ relayOnion }}</ span >
</ div >
< button @click ="copyText(relayOnion, 'onion')" class = "text-xs text-white/40 hover:text-white shrink-0 ml-2" >{{ copiedField === 'onion' ? 'Copied' : 'Copy' }}</ button >
</ div >
< div v-if = "relayDirect" class="mb-3 px-3 py-2 bg-white/5 rounded-lg flex items-center justify-between" >
< div class = "flex items-center gap-2 min-w-0" >
< span class = "text-xs text-white/40 shrink-0" > relay ( direct )</ span >
< span class = "text-xs font-mono text-white/60 truncate" >{{ relayDirect }}</ span >
</ div >
< button @click ="copyText(relayDirect, 'direct')" class = "text-xs text-white/40 hover:text-white shrink-0 ml-2" >{{ copiedField === 'direct' ? 'Copied' : 'Copy' }}</ button >
</ div >
<!-- VPN IPs -->
< div class = "grid grid-cols-1 sm:grid-cols-2 gap-3 mb-4" >
< div class = "p-3 bg-white/5 rounded-lg" >
< div class = "flex items-center gap-2 mb-1" >
< div class = "w-2 h-2 rounded-full" : class = "networkData.wgIp ? 'bg-green-400' : 'bg-white/20'" ></ div >
< span class = "text-xs text-white/50" > WireGuard </ span >
</ div >
< span class = "text-sm font-mono" : class = "networkData.wgIp ? 'text-white' : 'text-white/30'" >{{ networkData . wgIp || 'Not active' }}</ span >
</ div >
< div class = "p-3 bg-white/5 rounded-lg" >
< div class = "flex items-center gap-2 mb-1" >
< div class = "w-2 h-2 rounded-full" : class = "networkData.vpnIp ? 'bg-green-400' : 'bg-white/20'" ></ div >
< span class = "text-xs text-white/50" > NostrVPN </ span >
</ div >
< span class = "text-sm font-mono" : class = "networkData.vpnIp ? 'text-white' : 'text-white/30'" >{{ networkData . vpnIp || 'Not active' }}</ span >
</ div >
</ div >
<!-- Connected Devices -->
< div class = "border-t border-white/10 pt-3" >
< div class = "flex items-center justify-between mb-2" >
< span class = "text-xs text-white/50" > Connected Devices </ span >
< span class = "text-xs text-white/30" >{{ vpnPeers . length }} device {{ vpnPeers . length !== 1 ? 's' : '' }}</ span >
</ div >
< div v-if = "vpnPeers.length" class="space-y-1" >
< div v-for = "peer in vpnPeers" :key="peer.name + (peer.npub || '')" class="flex items-center justify-between text-xs py-1.5 px-2 bg-white/5 rounded" >
< div class = "flex items-center gap-2" >
< span class = "px-1 py-0.5 rounded text-[10px] font-medium" : class = "peer.type === 'nostrvpn' ? 'bg-purple-500/20 text-purple-300' : 'bg-blue-500/20 text-blue-300'" >{{ peer . type === 'nostrvpn' ? 'NVP' : 'WG' }}</ span >
< button v-if = "peer.type !== 'nostrvpn'" @click="showPeerConfig(peer.name)" class="text-white/70 hover:text-white transition-colors cursor-pointer" >{{ peer.name }}</ button >
< span v-else class = "text-white/70" >{{ peer . name }}</ span >
</ div >
< div class = "flex items-center gap-2" >
< span class = "text-white/40 font-mono" >{{ peer . ip ? . replace ( /\/\d+$/ , '' ) || '' }}</ span >
< button v-if = "peer.type !== 'nostrvpn'" @click="removePeer(peer.name)" :disabled="removingPeer === peer.name" class="p-0.5 rounded hover:bg-white/10 text-white/30 hover:text-red-400 transition-colors" :title="'Remove ' + peer.name" >
< svg v-if = "removingPeer === peer.name" class="w-3.5 h-3.5 animate-spin" fill="none" viewBox="0 0 24 24"><circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4" /><path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z" /></ svg >
< svg v-else class = "w-3.5 h-3.5" fill = "none" stroke = "currentColor" viewBox = "0 0 24 24" >< path stroke -linecap =" round " stroke -linejoin =" round " stroke -width =" 2 " d = "M6 18L18 6M6 6l12 12" /></ svg >
</ button >
</ div >
</ div >
</ div >
< div v-else class = "text-xs text-white/30 py-2" > No devices added yet </ div >
</ div >
</ div >
<!-- Add Device Modal -->
< Teleport to = "body" >
< Transition name = "modal" >
< div v-if = "showAddDeviceModal" class="fixed inset-0 z-[3000] flex items-center justify-center p-4" @click="closeDeviceModal" >
< div class = "absolute inset-0 bg-black/60 backdrop-blur-sm" ></ div >
< div @click.stop class = "glass-card p-6 max-w-md w-full relative z-10" >
< div class = "flex items-center justify-between mb-4" >
< h3 class = "text-lg font-semibold text-white" > Connect Device </ h3 >
< button @click ="closeDeviceModal" class = "p-1 rounded hover:bg-white/10 text-white/60" >< svg class = "w-5 h-5" fill = "none" stroke = "currentColor" viewBox = "0 0 24 24" >< path stroke -linecap =" round " stroke -linejoin =" round " stroke -width =" 2 " d = "M6 18L18 6M6 6l12 12" /></ svg ></ button >
</ div >
<!-- Loading state ( for existing peer config ) -->
< div v-if = "loadingPeerConfig" class="text-center py-8" >
< svg class = "w-6 h-6 animate-spin text-white/40 mx-auto" fill = "none" viewBox = "0 0 24 24" >< circle class = "opacity-25" cx = "12" cy = "12" r = "10" stroke = "currentColor" stroke -width =" 4 " />< path class = "opacity-75" fill = "currentColor" d = "M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z" /></ svg >
</ div >
<!-- Existing peer QR view -->
< div v-else-if = "peerQrData && !showingNewDevice" class="text-center" >
< div class = "bg-white rounded-xl p-4 mb-4 inline-block" v-html = "peerQrData.qr_svg" ></ div >
< p class = "text-sm text-white/70 mb-2" > Scan with the < strong > WireGuard </ strong > app </ p >
< p class = "text-xs text-white/40 font-mono mb-4" >{{ peerQrData . peer_ip }}</ p >
< div class = "flex gap-2" >
< button @click ="copyPeerConfig" class = "flex-1 glass-button py-2 text-xs" >{{ copiedConfig ? 'Copied!' : 'Copy Config' }}</ button >
< button @click ="closeDeviceModal" class = "flex-1 glass-button py-2 text-xs" > Done </ button >
</ div >
</ div >
<!-- New device : tab selection -->
< div v-else >
< div v-if = "!peerQrData && !inviteData" class="flex gap-1 mb-4 bg-white/5 rounded-lg p-1" >
< button @click ="deviceTab = 'nvpn'" : class = "deviceTab === 'nvpn' ? 'bg-white/10 text-white' : 'text-white/40 hover:text-white/60'" class = "flex-1 py-1.5 text-xs font-medium rounded-md transition-colors" > NostrVPN App </ button >
< button @click ="deviceTab = 'wg'" : class = "deviceTab === 'wg' ? 'bg-white/10 text-white' : 'text-white/40 hover:text-white/60'" class = "flex-1 py-1.5 text-xs font-medium rounded-md transition-colors" > WireGuard App </ button >
</ div >
< div v-if = "deviceTab === 'nvpn'" >
2026-04-08 19:04:53 +02:00
<!-- Step 2 : QR + mesh details -- >
2026-04-08 15:00:00 +02:00
< div v-if = "inviteData" class="text-center" >
2026-04-08 19:04:53 +02:00
< p class = "text-xs text-white/40 mb-3" > Step 2 of 2 — Join the mesh </ p >
2026-04-08 15:00:00 +02:00
< div class = "bg-white rounded-xl p-4 mb-4 inline-block" v-html = "inviteData.qr_svg" ></ div >
2026-04-08 19:04:53 +02:00
< p class = "text-sm text-white/70 mb-4" > Scan with the < strong > NostrVPN </ strong > app </ p >
<!-- Manual entry details -->
< div class = "border-t border-white/10 pt-3 mb-4 text-left" >
< button @click ="showMeshDetails = !showMeshDetails" class = "text-xs text-white/40 hover:text-white/60 transition-colors mb-2 flex items-center gap-1" >
< svg class = "w-3 h-3 transition-transform" : class = "showMeshDetails ? 'rotate-90' : ''" fill = "none" stroke = "currentColor" viewBox = "0 0 24 24" >< path stroke -linecap =" round " stroke -linejoin =" round " stroke -width =" 2 " d = "M9 5l7 7-7 7" /></ svg >
Or enter manually in the app
</ button >
< div v-if = "showMeshDetails" class="space-y-2" >
< div class = "flex items-center justify-between p-2 bg-white/5 rounded-lg" >
< div class = "min-w-0" >< span class = "text-[10px] text-white/40 block" > Network </ span >< span class = "text-xs font-mono text-white/70 truncate block" >{{ inviteData . network_id }}</ span ></ div >
< button @click ="copyText(inviteData.network_id, 'net')" class = "text-[10px] text-white/40 hover:text-white shrink-0 ml-2" >{{ copiedField === 'net' ? 'Copied' : 'Copy' }}</ button >
</ div >
< div class = "flex items-center justify-between p-2 bg-white/5 rounded-lg" >
< div class = "min-w-0" >< span class = "text-[10px] text-white/40 block" > Node npub </ span >< span class = "text-xs font-mono text-white/70 truncate block" >{{ inviteData . npub }}</ span ></ div >
< button @click ="copyText(inviteData.npub, 'invnpub')" class = "text-[10px] text-white/40 hover:text-white shrink-0 ml-2" >{{ copiedField === 'invnpub' ? 'Copied' : 'Copy' }}</ button >
</ div >
< div v-for = "(relay, i) in (inviteData.relays || [])" :key="relay" class="flex items-center justify-between p-2 bg-white/5 rounded-lg" >
< div class = "min-w-0" >< span class = "text-[10px] text-white/40 block" > Relay {{ ( inviteData . relays ? . length || 0 ) > 1 ? i + 1 : '' }}</ span >< span class = "text-xs font-mono text-white/70 truncate block" >{{ relay }}</ span ></ div >
< button @click ="copyText(relay, 'relay' + i)" class = "text-[10px] text-white/40 hover:text-white shrink-0 ml-2" >{{ copiedField === 'relay' + i ? 'Copied' : 'Copy' }}</ button >
</ div >
</ div >
</ div >
2026-04-08 15:00:00 +02:00
< div class = "flex gap-2" >
2026-04-08 19:04:53 +02:00
< button @click ="copyInvite" class = "flex-1 glass-button py-2 text-xs" >{{ copiedInvite ? 'Copied!' : 'Copy Invite Link' }}</ button >
2026-04-08 15:00:00 +02:00
< button @click ="closeDeviceModal" class = "flex-1 glass-button py-2 text-xs" > Done </ button >
</ div >
</ div >
2026-04-08 19:04:53 +02:00
<!-- Step 1 : Enter phone npub -->
2026-04-08 15:00:00 +02:00
< div v-else >
2026-04-08 19:04:53 +02:00
< p class = "text-xs text-white/40 mb-3" > Step 1 of 2 — Enter your phone 's npub</p>
<p class="text-sm text-white/50 mb-3">Open the <strong class="text-white/70">NostrVPN</strong> app on your phone, go to <strong class="text-white/70">Settings</strong>, and copy your npub.</p>
<input v-model="participantNpub" type="text" placeholder="npub1..." class="w-full bg-white/5 border border-white/10 rounded-lg px-4 py-2.5 text-sm text-white placeholder-white/30 focus:outline-none focus:border-white/30 font-mono mb-3" @keyup.enter="generateInviteWithNpub" />
<button @click="generateInviteWithNpub" :disabled="generatingInvite || !participantNpub.trim().startsWith(' npub1 ')" class="w-full glass-button py-2.5 text-sm font-medium disabled:opacity-30">{{ generatingInvite ? ' Setting up ... ' : ' Next → ' }}</button>
2026-04-08 15:00:00 +02:00
</div>
</div>
<div v-if="deviceTab === ' wg '">
<div v-if="peerQrData" class="text-center">
<div class="bg-white rounded-xl p-4 mb-4 inline-block" v-html="peerQrData.qr_svg"></div>
<p class="text-sm text-white/70 mb-2">Scan with the <strong>WireGuard</strong> app</p>
<p class="text-xs text-white/40 font-mono mb-4">{{ peerQrData.peer_ip }}</p>
<div class="flex gap-2">
<button @click="copyPeerConfig" class="flex-1 glass-button py-2 text-xs">{{ copiedConfig ? ' Copied ! ' : ' Copy Config ' }}</button>
<button @click="closeDeviceModal" class="flex-1 glass-button py-2 text-xs">Done</button>
</div>
</div>
<div v-else>
<p class="text-sm text-white/50 mb-3">Generate a static WireGuard config for the standard WireGuard app.</p>
<input v-model="newPeerName" type="text" placeholder="Device name (e.g. iPhone)" class="w-full bg-white/5 border border-white/10 rounded-lg px-4 py-2.5 text-sm text-white placeholder-white/30 focus:outline-none focus:border-white/30 mb-3" @keyup.enter="createPeer" />
<button @click="createPeer" :disabled="creatingPeer || !newPeerName.trim()" class="w-full glass-button py-2.5 text-sm font-medium disabled:opacity-30">{{ creatingPeer ? ' Generating ... ' : ' Generate QR Code ' }}</button>
</div>
</div>
</div>
<p v-if="peerError" class="text-sm text-red-400 mt-2">{{ peerError }}</p>
</div>
</div>
</Transition>
</Teleport>
2026-03-20 02:59:29 +00:00
<div class="grid grid-cols-1 2xl:grid-cols-2 gap-6 mb-6">
2026-03-19 12:44:31 +00:00
<!-- Network Interfaces -->
2026-03-30 13:35:02 +01:00
<div data-controller-container tabindex="0" class="glass-card p-6 transition-all hover:-translate-y-1">
2026-03-19 12:44:31 +00:00
<div class="flex items-center justify-between mb-4">
<div>
<h2 class="text-xl font-semibold text-white mb-1">Network Interfaces</h2>
<p class="text-sm text-white/60">Detected hardware and virtual interfaces</p>
</div>
<button
v-if="wifiAvailable"
@click="showWifiModal = true"
class="px-3 py-1.5 glass-button rounded text-xs font-medium text-white/90 hover:text-white transition-colors"
2026-03-11 00:35:55 +00:00
>
2026-03-19 12:44:31 +00:00
Scan WiFi
</button>
</div>
<template v-if="interfacesLoading">
<div class="space-y-3">
<div v-for="i in 3" :key="i" class="p-3 bg-white/5 rounded-lg animate-pulse h-14"></div>
</div>
</template>
<template v-else>
<div class="space-y-3">
<div
v-for="iface in physicalInterfaces"
:key="iface.name"
class="flex items-center justify-between p-3 bg-white/5 rounded-lg"
>
<div class="flex items-center gap-3">
<div class="w-2 h-2 rounded-full" :class="iface.state === ' up ' ? ' bg - green - 400 ' : ' bg - white / 30 '"></div>
<div>
<p class="text-sm font-medium text-white">{{ iface.name }}</p>
<p class="text-xs text-white/50">{{ iface.type === ' wifi ' ? ' WiFi ' : ' Ethernet ' }} · {{ iface.mac }}</p>
</div>
</div>
<div class="text-right">
<p v-if="iface.ipv4.length > 0" class="text-sm text-white/80">{{ iface.ipv4[0] }}</p>
<p v-else class="text-sm text-white/40">No IP</p>
2026-03-11 00:35:55 +00:00
</div>
</div>
2026-03-19 12:44:31 +00:00
<p v-if="physicalInterfaces.length === 0" class="text-sm text-white/50 text-center py-4">No physical interfaces detected</p>
2026-03-11 00:35:55 +00:00
</div>
2026-03-19 12:44:31 +00:00
</template>
2026-03-13 23:13:50 +00:00
</div>
2026-03-19 12:44:31 +00:00
<!-- Tor Services -->
2026-03-21 03:01:38 +00:00
<TorServicesCard
:tor-services="torServices"
:tor-services-loading="torServicesLoading"
:tor-daemon-running="torDaemonRunning"
:tor-restarting="torRestarting"
:tor-rotating="torRotating"
:tor-deleting="torDeleting"
@restart-tor="restartTor"
@show-add-service="showAddServiceModal = true"
@copy-address="copyTorAddress"
@rotate-service="rotateService"
@delete-service="deleteService"
@toggle-app="toggleTorApp"
/>
2026-03-13 23:13:50 +00:00
</div>
2026-03-21 03:01:38 +00:00
<!-- Modals -->
<ServerModals
:show-add-service-modal="showAddServiceModal"
:show-wifi-modal="showWifiModal"
:show-dns-modal="showDnsModal"
:available-apps-for-tor="availableAppsForTor"
:adding-service="addingService"
:add-service-error="addServiceError"
:wifi-scanning="wifiScanning"
:wifi-networks="wifiNetworks"
:wifi-connecting="wifiConnecting"
:wifi-submitting="wifiSubmitting"
:wifi-selected-ssid="wifiSelectedSsid"
:wifi-error="wifiError"
:dns-selected-provider="dnsSelectedProvider"
:dns-servers="networkData.dnsServers"
:dns-applying="dnsApplying"
:dns-error="dnsError"
:dns-provider-options="dnsProviderOptions"
@close-add-service="showAddServiceModal = false"
@create-service-for-app="createServiceForApp"
@create-service="createService"
@close-wifi="showWifiModal = false"
@select-wifi="selectWifi"
@connect-wifi="connectToWifi"
@cancel-wifi-connect="wifiConnecting = false; wifiPassword = ''; wifiError = ''"
@close-dns="showDnsModal = false; dnsError = ''"
@select-dns-provider="(v: string) => { dnsSelectedProvider = v }"
@apply-dns="applyDnsConfig"
/>
2026-03-11 10:44:56 +00:00
<!-- Logs info toast -->
<Transition name="fade">
<div v-if="logsToast" class="fixed bottom-20 left-1/2 -translate-x-1/2 z-50 max-w-md w-full px-4">
<div class="bg-white/10 border border-white/20 backdrop-blur-sm rounded-lg px-4 py-3 text-white/80 text-sm flex items-center justify-between gap-3">
<span>{{ logsToast }}</span>
<button @click="logsToast = ''" class="text-white/50 hover:text-white shrink-0">×</button>
</div>
</div>
</Transition>
2026-01-24 22:59:20 +00:00
</div>
</template>
<script setup lang="ts">
2026-03-11 00:35:55 +00:00
import { ref, computed, onMounted, watch } from ' vue '
2026-03-04 05:23:42 +00:00
import { rpcClient } from ' @ / api / rpc - client '
2026-03-20 02:59:29 +00:00
import { useAppStore } from ' @ / stores / app '
2026-03-21 03:01:38 +00:00
import QuickActionsCard from ' . / server / QuickActionsCard . vue '
import TorServicesCard from ' . / server / TorServicesCard . vue '
import ServerModals from ' . / server / ServerModals . vue '
import type { TorServiceInfo } from ' . / server / TorServicesCard . vue '
2026-01-24 22:59:20 +00:00
2026-03-20 02:59:29 +00:00
const appStore = useAppStore()
2026-01-24 22:59:20 +00:00
// Service status
const servicesRunning = ref(true)
const restarting = ref(false)
2026-03-19 16:44:46 +00:00
// Tor status
const torStatusLabel = ref<' running ' | ' stopped ' | ' checking '>(' checking ')
const checkingTor = ref(false)
const torStatusColor = computed(() => {
if (torStatusLabel.value === ' running ') return ' bg - green - 400 '
if (torStatusLabel.value === ' checking ') return ' bg - yellow - 400 '
return ' bg - red - 400 '
})
2026-01-24 22:59:20 +00:00
2026-03-21 03:01:38 +00:00
// Auto-sync, logs
2026-01-24 22:59:20 +00:00
const autoSyncEnabled = ref(true)
2026-03-11 00:04:26 +00:00
const logCount = ref(0)
// Network data
const networkLoading = ref(true)
const networkData = ref({
2026-03-21 03:01:38 +00:00
wifiCount: ' N / A ', torConnected: false, forwardCount: ' N / A ',
2026-04-08 15:00:00 +02:00
vpnConnected: false, vpnProvider: '', vpnIp: '', wgIp: '', vpnHostname: '', vpnPeers: 0,
2026-03-21 03:01:38 +00:00
dnsProvider: ' system ', dnsServers: [] as string[], dnsDoH: false,
2026-03-11 00:04:26 +00:00
})
async function loadNetworkData() {
networkLoading.value = true
try {
2026-03-11 10:44:56 +00:00
const [diagRes, fwdRes, vpnRes, dnsRes] = await Promise.allSettled([
2026-03-11 00:04:26 +00:00
rpcClient.call<{ wan_ip: string | null; nat_type: string; upnp_available: boolean; tor_connected: boolean; wifi_count?: number }>({ method: ' network . diagnostics ' }),
rpcClient.call<{ forwards: unknown[] }>({ method: ' router . list - forwards ' }),
2026-03-11 10:44:56 +00:00
rpcClient.vpnStatus(),
rpcClient.dnsStatus(),
2026-03-11 00:04:26 +00:00
])
2026-03-21 03:01:38 +00:00
if (diagRes.status === ' fulfilled ') { networkData.value.torConnected = diagRes.value.tor_connected; networkData.value.wifiCount = diagRes.value.wifi_count !== undefined ? `${diagRes.value.wifi_count} configured` : ' N / A ' }
if (fwdRes.status === ' fulfilled ') { const c = fwdRes.value.forwards?.length ?? 0; networkData.value.forwardCount = `${c} rule${c !== 1 ? ' s ' : ''}` }
2026-04-08 15:00:00 +02:00
if (vpnRes.status === ' fulfilled ') { networkData.value.vpnConnected = vpnRes.value.connected; networkData.value.vpnProvider = vpnRes.value.provider ?? ''; networkData.value.vpnIp = (vpnRes.value.ip_address ?? '').replace(/\/\d+$/, ''); networkData.value.wgIp = vpnRes.value.wg_ip ?? ''; nodeNpub.value = vpnRes.value.node_npub ?? ''; relayOnion.value = vpnRes.value.relay_onion ?? ''; relayDirect.value = vpnRes.value.relay_direct ?? '' }
2026-03-21 03:01:38 +00:00
if (dnsRes.status === ' fulfilled ') { networkData.value.dnsProvider = dnsRes.value.provider; networkData.value.dnsServers = dnsRes.value.resolv_conf_servers ?? []; networkData.value.dnsDoH = dnsRes.value.doh_enabled }
} catch { /* keep defaults */ } finally { networkLoading.value = false }
2026-03-11 00:04:26 +00:00
}
2026-04-08 15:00:00 +02:00
// Node npub for NostrVPN
const nodeNpub = ref('')
const copiedNpub = ref(false)
async function copyNpub() {
if (!nodeNpub.value) return
try { await navigator.clipboard.writeText(nodeNpub.value) } catch { /* fallback */ }
copiedNpub.value = true
setTimeout(() => { copiedNpub.value = false }, 2000)
}
// Private relay URLs
const relayOnion = ref('')
const relayDirect = ref('')
const copiedField = ref('')
async function copyText(text: string, field: string) {
if (!text) return
try { await navigator.clipboard.writeText(text) } catch { /* fallback */ }
copiedField.value = field
setTimeout(() => { copiedField.value = '' }, 2000)
}
2026-04-07 19:44:00 +01:00
// VPN peer management
const showAddDeviceModal = ref(false)
const newPeerName = ref('')
const creatingPeer = ref(false)
const peerQrData = ref<{ qr_svg: string; config: string; peer_ip: string } | null>(null)
const peerError = ref('')
const copiedConfig = ref(false)
2026-04-08 15:00:00 +02:00
const vpnPeers = ref<{ name: string; ip: string; type?: string; npub?: string }[]>([])
2026-04-07 19:44:00 +01:00
async function loadVpnPeers() {
try {
const res = await rpcClient.call<{ peers: { name: string; ip: string }[] }>({ method: ' vpn . list - peers ' })
vpnPeers.value = res.peers || []
} catch { /* no peers */ }
}
async function createPeer() {
if (!newPeerName.value.trim()) return
creatingPeer.value = true
peerError.value = ''
try {
const res = await rpcClient.call<{ qr_svg: string; config: string; peer_ip: string }>({
method: ' vpn . create - peer ',
params: { name: newPeerName.value.trim() },
})
peerQrData.value = res
loadVpnPeers()
} catch (e) {
peerError.value = e instanceof Error ? e.message : ' Failed to create peer '
} finally {
creatingPeer.value = false
}
}
2026-04-08 15:00:00 +02:00
const loadingPeerConfig = ref(false)
async function showPeerConfig(name: string) {
showAddDeviceModal.value = true
loadingPeerConfig.value = true
peerError.value = ''
try {
const res = await rpcClient.call<{ qr_svg: string; config: string; peer_ip: string }>({
method: ' vpn . peer - config ',
params: { name },
})
peerQrData.value = res
} catch (e) {
peerError.value = e instanceof Error ? e.message : ' Failed to load config '
} finally {
loadingPeerConfig.value = false
}
}
const removingPeer = ref('')
async function removePeer(name: string) {
removingPeer.value = name
try {
await rpcClient.call({ method: ' vpn . remove - peer ', params: { name } })
vpnPeers.value = vpnPeers.value.filter(p => p.name !== name)
} catch { /* ignore */ }
finally { removingPeer.value = '' }
}
const deviceTab = ref<' nvpn ' | ' wg '>(' nvpn ')
const showingNewDevice = ref(false)
2026-04-08 19:04:53 +02:00
const showMeshDetails = ref(false)
const inviteData = ref<{ invite_url: string; qr_svg: string; npub: string; network_id: string; relays?: string[] } | null>(null)
2026-04-08 15:00:00 +02:00
const generatingInvite = ref(false)
const copiedInvite = ref(false)
2026-04-08 19:04:53 +02:00
const participantNpub = ref('')
2026-04-08 15:00:00 +02:00
function closeDeviceModal() {
showAddDeviceModal.value = false
peerQrData.value = null
inviteData.value = null
newPeerName.value = ''
peerError.value = ''
showingNewDevice.value = false
2026-04-08 19:04:53 +02:00
showMeshDetails.value = false
participantNpub.value = ''
2026-04-08 15:00:00 +02:00
}
2026-04-08 19:04:53 +02:00
async function generateInviteWithNpub() {
const npub = participantNpub.value.trim()
if (!npub.startsWith(' npub1 ')) return
2026-04-08 15:00:00 +02:00
generatingInvite.value = true
peerError.value = ''
try {
2026-04-08 19:04:53 +02:00
const res = await rpcClient.call<{ invite_url: string; qr_svg: string; npub: string; network_id: string; relays?: string[] }>({
method: ' vpn . invite ',
params: { npub },
})
2026-04-08 15:00:00 +02:00
inviteData.value = res
2026-04-08 19:04:53 +02:00
// Add to device list immediately
const short = npub.length > 20 ? `${npub.slice(0, 12)}...${npub.slice(-6)}` : npub
vpnPeers.value.push({ name: short, ip: ' mesh ', type: ' nostrvpn ', npub })
loadVpnPeers()
2026-04-08 15:00:00 +02:00
} catch (e) {
peerError.value = e instanceof Error ? e.message : ' Failed to generate invite '
} finally {
generatingInvite.value = false
}
}
async function copyInvite() {
if (!inviteData.value?.invite_url) return
try { await navigator.clipboard.writeText(inviteData.value.invite_url) } catch { /* fallback */ }
copiedInvite.value = true
setTimeout(() => { copiedInvite.value = false }, 2000)
}
2026-04-07 19:44:00 +01:00
async function copyPeerConfig() {
if (!peerQrData.value?.config) return
try { await navigator.clipboard.writeText(peerQrData.value.config) } catch { /* fallback */ }
copiedConfig.value = true
setTimeout(() => { copiedConfig.value = false }, 2000)
}
2026-03-11 00:35:55 +00:00
// Network interfaces
2026-03-21 03:01:38 +00:00
interface NetworkInterface { name: string; type: string; state: string; mac: string; ipv4: string[] }
interface WifiNetwork { ssid: string; signal: number; security: string }
2026-03-11 00:35:55 +00:00
const interfacesLoading = ref(true)
const allInterfaces = ref<NetworkInterface[]>([])
2026-03-21 03:01:38 +00:00
const physicalInterfaces = computed(() => allInterfaces.value.filter(i => i.type === ' ethernet ' || i.type === ' wifi '))
const wifiAvailable = computed(() => allInterfaces.value.some(i => i.type === ' wifi '))
2026-03-11 00:35:55 +00:00
const showWifiModal = ref(false)
const wifiScanning = ref(false)
const wifiNetworks = ref<WifiNetwork[]>([])
const wifiConnecting = ref(false)
2026-03-11 10:49:26 +00:00
const wifiSubmitting = ref(false)
2026-03-11 00:35:55 +00:00
const wifiSelectedSsid = ref('')
const wifiPassword = ref('')
2026-03-11 10:44:56 +00:00
const wifiError = ref('')
2026-03-21 03:01:38 +00:00
// DNS
2026-03-11 10:44:56 +00:00
const showDnsModal = ref(false)
const dnsSelectedProvider = ref(' system ')
const dnsApplying = ref(false)
const dnsError = ref('')
const dnsProviderOptions = [
{ value: ' system ', label: ' System Default ', description: ' DHCP - assigned DNS servers ', doh: false },
{ value: ' cloudflare ', label: ' Cloudflare ', description: ' 1.1.1.1 / 1.0.0.1 ', doh: true },
{ value: ' google ', label: ' Google ', description: ' 8.8.8.8 / 8.8.4.4 ', doh: true },
{ value: ' quad9 ', label: ' Quad9 ', description: ' 9.9.9.9 / 149.112.112.112 ', doh: true },
{ value: ' mullvad ', label: ' Mullvad ', description: ' 194.242.2.2 ( no logging ) ', doh: true },
{ value: ' custom ', label: ' Custom ', description: ' Enter your own DNS servers ', doh: false },
]
type DnsProviderValue = ' system ' | ' cloudflare ' | ' google ' | ' quad9 ' | ' mullvad ' | ' custom '
const dnsDisplayLabel = computed(() => {
const p = networkData.value.dnsProvider
const opt = dnsProviderOptions.find(o => o.value === p)
2026-03-21 03:01:38 +00:00
if (opt && p !== ' system ') return `${opt.label}${networkData.value.dnsDoH ? ' ( DoH ) ' : ''}`
if (networkData.value.dnsServers.length > 0) return networkData.value.dnsServers.slice(0, 2).join(' , ')
2026-03-11 10:44:56 +00:00
return ' System Default '
})
2026-03-21 03:01:38 +00:00
async function applyDnsConfig(customServers: string) {
dnsApplying.value = true; dnsError.value = ''
2026-03-11 10:44:56 +00:00
try {
const provider = dnsSelectedProvider.value as DnsProviderValue
const params: { provider: DnsProviderValue; servers?: string[] } = { provider }
2026-03-21 03:01:38 +00:00
if (provider === ' custom ') { params.servers = customServers.split(' , ').map(s => s.trim()).filter(s => s.length > 0) }
2026-03-11 10:44:56 +00:00
const res = await rpcClient.configureDns(params)
2026-03-21 03:01:38 +00:00
networkData.value.dnsProvider = res.provider; networkData.value.dnsServers = res.servers; networkData.value.dnsDoH = res.doh_enabled
2026-03-11 10:44:56 +00:00
showDnsModal.value = false
2026-03-21 03:01:38 +00:00
} catch (e) { dnsError.value = e instanceof Error ? e.message : ' DNS configuration failed . ' } finally { dnsApplying.value = false }
2026-03-11 10:44:56 +00:00
}
2026-03-11 00:35:55 +00:00
async function loadInterfaces() {
interfacesLoading.value = true
2026-03-21 03:01:38 +00:00
try { const res = await rpcClient.call<{ interfaces: NetworkInterface[] }>({ method: ' network . list - interfaces ' }); allInterfaces.value = res.interfaces } catch { allInterfaces.value = [] } finally { interfacesLoading.value = false }
2026-03-11 00:35:55 +00:00
}
async function scanWifi() {
2026-03-21 03:01:38 +00:00
wifiScanning.value = true; wifiNetworks.value = []
try { const res = await rpcClient.call<{ networks: WifiNetwork[] }>({ method: ' network . scan - wifi ' }); wifiNetworks.value = res.networks } catch { wifiNetworks.value = [] } finally { wifiScanning.value = false }
}
function selectWifi(ssid: string) { wifiSelectedSsid.value = ssid; wifiPassword.value = ''; wifiConnecting.value = true }
async function connectToWifi(password: string) {
if (!password || !wifiSelectedSsid.value) return
wifiError.value = ''; wifiSubmitting.value = true
2026-03-11 00:35:55 +00:00
try {
2026-03-21 03:01:38 +00:00
await rpcClient.call({ method: ' network . configure - wifi ', params: { ssid: wifiSelectedSsid.value, password } })
showWifiModal.value = false; wifiConnecting.value = false; wifiPassword.value = ''
logsToast.value = ' WiFi connected successfully '; setTimeout(() => { logsToast.value = '' }, 4000); loadInterfaces()
} catch (e) { wifiError.value = e instanceof Error ? e.message : ' WiFi connection failed . ' } finally { wifiSubmitting.value = false }
2026-03-11 00:35:55 +00:00
}
2026-03-21 03:01:38 +00:00
// Disk space
const diskWarning = ref<{ level: ' warning ' | ' critical '; used_percent: number; free_bytes: number } | null>(null)
2026-03-30 16:35:06 +01:00
const diskEncrypted = ref(false)
2026-03-11 10:44:56 +00:00
const diskCleaning = ref(false)
async function loadDiskStatus() {
2026-03-30 16:35:06 +01:00
try {
const res = await rpcClient.diskStatus()
diskEncrypted.value = !!(res as Record<string, unknown>).encrypted
if (res.level === ' warning ' || res.level === ' critical ') {
diskWarning.value = { level: res.level, used_percent: res.used_percent, free_bytes: res.free_bytes }
} else { diskWarning.value = null }
} catch { /* non-critical */ }
2026-03-11 10:44:56 +00:00
}
async function runDiskCleanup() {
diskCleaning.value = true
2026-03-21 03:01:38 +00:00
try { await rpcClient.diskCleanup(); await loadDiskStatus(); logsToast.value = ' Disk cleanup completed '; setTimeout(() => { logsToast.value = '' }, 4000) }
catch (e) { logsToast.value = `Disk cleanup failed: ${e instanceof Error ? e.message : ' Unknown error '}`; setTimeout(() => { logsToast.value = '' }, 6000) }
finally { diskCleaning.value = false }
2026-03-11 10:44:56 +00:00
}
function formatBytes(bytes: number): string {
2026-03-21 03:01:38 +00:00
const gb = 1024 * 1024 * 1024; const mb = 1024 * 1024
2026-03-11 10:44:56 +00:00
if (bytes >= gb) return `${(bytes / gb).toFixed(1)} GB`
if (bytes >= mb) return `${(bytes / mb).toFixed(0)} MB`
return `${(bytes / 1024).toFixed(0)} KB`
}
2026-03-21 03:01:38 +00:00
// Tor Services
2026-03-13 23:13:50 +00:00
const torServices = ref<TorServiceInfo[]>([])
const torServicesLoading = ref(false)
2026-03-20 02:59:29 +00:00
const torDaemonRunning = ref(false)
const torRestarting = ref(false)
const torRotating = ref<string | false>(false)
const torDeleting = ref<string | false>(false)
2026-03-19 16:38:11 +00:00
2026-03-20 02:59:29 +00:00
const showAddServiceModal = ref(false)
const addingService = ref(false)
const addServiceError = ref('')
const availableAppsForTor = computed(() => {
const existingNames = new Set(torServices.value.map(s => s.name))
2026-03-21 03:01:38 +00:00
return Object.entries(appStore.packages)
2026-03-20 02:59:29 +00:00
.filter(([id]) => !existingNames.has(id))
2026-03-21 03:01:38 +00:00
.map(([id, pkg]) => ({ id, title: (pkg as { manifest?: { title?: string } })?.manifest?.title || id }))
2026-03-20 02:59:29 +00:00
.sort((a, b) => a.title.localeCompare(b.title))
})
2026-03-13 23:13:50 +00:00
async function loadTorServices() {
torServicesLoading.value = true
2026-03-21 03:01:38 +00:00
try { 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 }
2026-03-13 23:13:50 +00:00
}
2026-03-31 07:03:57 +01:00
async function copyTorAddress(address: string) {
try {
if (navigator.clipboard?.writeText) {
await navigator.clipboard.writeText(address)
} else {
const ta = document.createElement(' textarea ')
ta.value = address
ta.style.position = ' fixed '
ta.style.opacity = ' 0 '
document.body.appendChild(ta)
ta.select()
document.execCommand(' copy ')
document.body.removeChild(ta)
}
logsToast.value = ' Onion address copied to clipboard '
} catch {
logsToast.value = ' Failed to copy address '
}
setTimeout(() => { logsToast.value = '' }, 3000)
}
2026-03-13 23:13:50 +00:00
2026-03-21 03:01:38 +00:00
async function toggleTorApp(appId: string, enabled: boolean) { try { await rpcClient.call({ method: ' tor . toggle - app ', params: { app_id: appId, enabled }, timeout: 90000 }); await loadTorServices() } catch { /* handled */ } }
async function rotateService(name: string) { torRotating.value = name; try { await rpcClient.call({ method: ' tor . rotate - service ', params: { name }, timeout: 90000 }); await loadTorServices() } catch { /* handled */ } finally { torRotating.value = false } }
async function restartTor() { torRestarting.value = true; try { await rpcClient.call({ method: ' tor . restart ', timeout: 90000 }); await loadTorServices(); logsToast.value = ' Tor restarted successfully '; setTimeout(() => { logsToast.value = '' }, 3000) } catch { logsToast.value = ' Failed to restart Tor '; setTimeout(() => { logsToast.value = '' }, 5000) } finally { torRestarting.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 { /* handled */ } finally { torDeleting.value = false } }
2026-03-20 02:59:29 +00:00
async function createServiceForApp(appId: string) {
2026-03-21 03:01:38 +00:00
addServiceError.value = ''; addingService.value = true
try { 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 }
2026-03-20 02:59:29 +00:00
}
2026-03-21 03:01:38 +00:00
async function createService(name: string, port: number | null) {
2026-03-20 02:59:29 +00:00
if (!name || !port) return
2026-03-21 03:01:38 +00:00
addServiceError.value = ''; addingService.value = true
try { await rpcClient.call({ method: ' tor . create - service ', params: { name, local_port: port }, timeout: 90000 }); showAddServiceModal.value = false; 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 }
2026-03-20 02:59:29 +00:00
}
2026-04-07 19:44:00 +01:00
onMounted(() => { checkTorStatus(); loadNetworkData(); loadInterfaces(); loadDiskStatus(); loadTorServices(); loadVpnPeers() })
2026-03-21 03:01:38 +00:00
watch(showWifiModal, (open) => { if (open) scanWifi() })
watch(showDnsModal, (open) => { if (open) { dnsSelectedProvider.value = networkData.value.dnsProvider || ' system '; dnsError.value = '' } })
2026-03-11 10:44:56 +00:00
2026-03-04 05:23:42 +00:00
async function restartServices() {
2026-03-21 03:01:38 +00:00
restarting.value = true; servicesRunning.value = false
try { await rpcClient.restartServer(); logsToast.value = ' Services restarting ... '; setTimeout(() => { logsToast.value = '' }, 4000) }
catch (e) { logsToast.value = `Restart failed: ${e instanceof Error ? e.message : ' Unknown error '}`; setTimeout(() => { logsToast.value = '' }, 6000) }
2026-03-13 23:13:50 +00:00
const pollHealth = async (retries: number) => {
for (let i = 0; i < retries; i++) {
await new Promise(r => setTimeout(r, 2000))
2026-03-21 03:01:38 +00:00
try { await rpcClient.call({ method: ' server . health ', params: {} }); servicesRunning.value = true; restarting.value = false; return } catch { /* still restarting */ }
2026-03-13 23:13:50 +00:00
}
2026-03-21 03:01:38 +00:00
restarting.value = false; servicesRunning.value = false; torStatusLabel.value = ' stopped '
2026-03-13 23:13:50 +00:00
}
pollHealth(15)
2026-01-24 22:59:20 +00:00
}
2026-03-19 16:44:46 +00:00
async function checkTorStatus() {
2026-03-21 03:01:38 +00:00
checkingTor.value = true; torStatusLabel.value = ' checking '
try { const res = await rpcClient.call<{ services: TorServiceInfo[] }>({ method: ' tor . list - services ' }); torServices.value = res.services || []; torStatusLabel.value = torServices.value.some(s => s.onion_address) ? ' running ' : ' stopped ' }
catch { torStatusLabel.value = ' stopped ' } finally { checkingTor.value = false }
2026-01-24 22:59:20 +00:00
}
2026-03-11 10:44:56 +00:00
const logsToast = ref('')
2026-03-21 03:01:38 +00:00
function viewLogs() { logCount.value = 0; logsToast.value = ' Server logs are available via SSH : journalctl - u archipelago - f '; setTimeout(() => { logsToast.value = ' ' }, 6000 ) }
2026-01-24 22:59:20 +00:00
</ script >