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 -- >
security+feat: v1.3.0 — pentest remediation, container reliability, UI overhaul
Security (33 pentest findings addressed):
- CRITICAL: backend binds 127.0.0.1, path traversal in tor.rs/dwn fixed
- HIGH: federation requires signatures, XSS login redirect, RBAC viewer restricted
- HIGH: tar slip prevention, S3 SSRF validation, backup ID validation
- MEDIUM: remember-me random secret, TOTP session rotation, password re-auth
- LOW: CSP unsafe-inline removed, CORS dev-only, onion/webhook validation
Container reliability:
- Memory limits on all 37 containers (OOM prevention)
- Exited vs stopped state distinction with health-aware status badges
- Crash recovery coordination (no more restart cascade)
- User-stopped tracking survives reboots
- Tiered boot recovery (databases → core → services → apps)
UI:
- Wallet TransactionsModal, health-aware app status badges
- Restart button on containers, exited/crashed red state
- Mesh view overhaul, glass button updates, BaseModal/ToggleSwitch
- Apps sticky header removed, dev faucet, mutable mock wallet
Infrastructure:
- LND REST port 8080 exposed over Tor (LND Connect fix)
- Nginx cookie_session fix, deploy script Tor config updated
- Dev environment: podman auto-start, boot mode simulation
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
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 >
2026-06-11 00:24:40 -04:00
< div v-if = "networkRefreshing" class="p-2 text-center text-white/45 text-xs flex items-center justify-center gap-2" >
< svg class = "animate-spin h-3.5 w-3.5" fill = "none" viewBox = "0 0 24 24" >
< circle class = "opacity-25" cx = "12" cy = "12" r = "10" stroke = "currentColor" stroke -width = " 4 " > < / circle >
< path class = "opacity-75" fill = "currentColor" d = "M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z" > < / path >
< / svg >
Refreshing network ...
< / 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 = "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-04-19 00:42:56 -04: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 = "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 >
< span class = "text-white/80 text-sm" > WiFi < / span >
2026-03-11 00:04:26 +00:00
< / div >
2026-04-19 00:42:56 -04:00
< span class = "text-sm" : class = "networkData.wifiSsid ? 'text-green-400' : 'text-white/40'" > { { networkData . wifiSsid || 'Not connected' } } < / 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'" >
2026-04-18 11:07:08 -04:00
{ { networkData . vpnConnected ? 'WireGuard' : 'Not Connected' } }
2026-04-08 15:00:00 +02:00
< / 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-04-19 00:42:56 -04: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 = "M13 10V3L4 14h7v7l9-11h-7z" / > < / svg >
< span class = "text-white/80 text-sm" > FIPS Mesh < / span >
< / div >
< span class = "text-sm" :class = "fipsRowTextClass" > { { fipsRowLabel } } < / span >
< / div >
2026-03-11 00:04:26 +00:00
< / template >
2026-01-24 22:59:20 +00:00
< / div >
< / div >
2026-03-11 00:35:55 +00:00
2026-04-19 00:42:56 -04:00
< FipsNetworkCard / >
< / div >
2026-04-18 11:07:08 -04:00
< div class = "grid grid-cols-1 xl:grid-cols-2 gap-6 mb-6" >
2026-04-08 15:00:00 +02:00
<!-- VPN Card -- >
2026-06-11 00:24:40 -04:00
< div class = "glass-card p-6 flex flex-col transition-all hover:-translate-y-1" >
2026-04-08 15:00:00 +02:00
< 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 >
2026-04-18 11:07:08 -04:00
< p class = "text-xs text-white/50" > Standalone WireGuard VPN < / p >
2026-04-08 15:00:00 +02:00
< / div >
< / div >
2026-06-11 00:24:40 -04:00
< button @ click = "showAddDeviceModal = true; showingNewDevice = true" class = "responsive-card-actions-top glass-button px-4 py-2 text-sm" > Add Device < / button >
2026-04-08 15:00:00 +02:00
< / div >
2026-04-18 11:07:08 -04:00
<!-- WireGuard Status -- >
< div class = "mb-4 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" > Server Address < / span >
2026-04-08 15:00:00 +02:00
< / div >
2026-04-19 17:13:58 -04:00
< span class = "text-sm font-mono" : class = "networkData.wgIp ? 'text-white' : 'text-white/30'" > { { networkData . wgIp || 'Not configured' } } < / span >
2026-04-18 11:07:08 -04:00
< span v-if = "networkData.wgPubkey" class="block text-xs font-mono text-white/30 mt-1 truncate" > {{ networkData.wgPubkey }} < / span >
2026-04-08 15:00:00 +02:00
< / 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" >
2026-04-18 11:07:08 -04:00
< div v-for = "peer in vpnPeers" :key="peer.name" class="flex items-center justify-between text-xs py-1.5 px-2 bg-white/5 rounded" >
2026-04-08 15:00:00 +02:00
< div class = "flex items-center gap-2" >
2026-04-18 11:07:08 -04:00
< span class = "px-1 py-0.5 rounded text-[10px] font-medium bg-blue-500/20 text-blue-300" > WG < / span >
< button @click ="showPeerConfig(peer.name)" class = "text-white/70 hover:text-white transition-colors cursor-pointer" > { { peer . name } } < / button >
2026-04-08 15:00:00 +02:00
< / div >
< div class = "flex items-center gap-2" >
< span class = "text-white/40 font-mono" > { { peer . ip ? . replace ( /\/\d+$/ , '' ) || '' } } < / span >
2026-04-18 11:07:08 -04:00
< button @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" >
2026-04-08 15:00:00 +02:00
< 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 >
2026-06-11 00:24:40 -04:00
< button @ click = "showAddDeviceModal = true; showingNewDevice = true" class = "responsive-card-actions-bottom mt-4 mobile-card-action glass-button rounded-lg text-sm font-medium" >
Add Device
< / button >
2026-04-08 15:00:00 +02:00
< / div >
2026-04-18 11:07:08 -04:00
<!-- Network Interfaces ( second column on desktop ) -- >
2026-06-11 00:24:40 -04:00
< div data -controller -container tabindex = "0" class = "glass-card p-6 flex flex-col transition-all hover:-translate-y-1" >
2026-04-18 11:07:08 -04: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"
2026-06-11 00:24:40 -04:00
class = "responsive-card-actions-top px-3 py-1.5 glass-button rounded text-xs font-medium text-white/90 hover:text-white transition-colors"
2026-04-18 11:07:08 -04: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" >
2026-06-11 00:24:40 -04:00
< div v-if = "interfacesRefreshing" class="p-2 text-center text-white/45 text-xs flex items-center justify-center gap-2" >
< svg class = "animate-spin h-3.5 w-3.5" fill = "none" viewBox = "0 0 24 24" >
< circle class = "opacity-25" cx = "12" cy = "12" r = "10" stroke = "currentColor" stroke -width = " 4 " > < / circle >
< path class = "opacity-75" fill = "currentColor" d = "M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z" > < / path >
< / svg >
Refreshing interfaces ...
< / div >
2026-04-18 11:07:08 -04:00
< 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 text-white font-medium" > { { iface . name } } < / p >
< p class = "text-xs text-white/50" > { { iface . type === 'wifi' ? 'WiFi' : 'Ethernet' } } & middot ; { { 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 >
< / div >
< / div >
< p v-if = "physicalInterfaces.length === 0" class="text-sm text-white/50 text-center py-4" > No physical interfaces detected < / p >
< / div >
< / template >
2026-06-11 00:24:40 -04:00
< button
v - if = "wifiAvailable"
@ click = "showWifiModal = true"
class = "responsive-card-actions-bottom mt-4 mobile-card-action glass-button rounded-lg text-sm font-medium text-white/90 hover:text-white transition-colors"
>
Scan WiFi
< / button >
2026-04-18 11:07:08 -04:00
< / div >
< / div > <!-- close VPN + Network 2 - col grid -- >
2026-04-08 15:00:00 +02:00
<!-- 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 >
2026-04-18 11:07:08 -04:00
<!-- New device : WireGuard config -- >
2026-04-08 15:00:00 +02:00
< div v-else >
2026-04-18 11:07:08 -04:00
< div v-if = "peerQrData" >
< div class = "text-center" >
2026-04-08 15:00:00 +02:00
< 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 >
2026-04-18 11:07:08 -04:00
< / div >
< div v-else >
< div >
< p class = "text-sm text-white/50 mb-3" > Generate a WireGuard config for the standard WireGuard app . < / p >
2026-04-08 15:00:00 +02:00
< 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-04-18 11:07:08 -04:00
< div class = "mb-6" >
security+feat: v1.3.0 — pentest remediation, container reliability, UI overhaul
Security (33 pentest findings addressed):
- CRITICAL: backend binds 127.0.0.1, path traversal in tor.rs/dwn fixed
- HIGH: federation requires signatures, XSS login redirect, RBAC viewer restricted
- HIGH: tar slip prevention, S3 SSRF validation, backup ID validation
- MEDIUM: remember-me random secret, TOTP session rotation, password re-auth
- LOW: CSP unsafe-inline removed, CORS dev-only, onion/webhook validation
Container reliability:
- Memory limits on all 37 containers (OOM prevention)
- Exited vs stopped state distinction with health-aware status badges
- Crash recovery coordination (no more restart cascade)
- User-stopped tracking survives reboots
- Tiered boot recovery (databases → core → services → apps)
UI:
- Wallet TransactionsModal, health-aware app status badges
- Restart button on containers, exited/crashed red state
- Mesh view overhaul, glass button updates, BaseModal/ToggleSwitch
- Apps sticky header removed, dev faucet, mutable mock wallet
Infrastructure:
- LND REST port 8080 exposed over Tor (LND Connect fix)
- Nginx cookie_session fix, deploy script Tor config updated
- Dev environment: podman auto-start, boot mode simulation
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
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-11 00:35:55 +00:00
< / div >
2026-03-11 10:44:56 +00:00
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"
2026-06-12 03:00:15 -04:00
: wifi - scan - error = "wifiScanError"
2026-03-21 03:01:38 +00:00
: 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"
2026-06-12 03:00:15 -04:00
@ scan - wifi = "scanWifi"
2026-03-21 03:01:38 +00:00
@ 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" > & times ; < / button >
< / div >
< / div >
< / Transition >
2026-01-24 22:59:20 +00:00
< / div >
< / template >
< script setup lang = "ts" >
2026-04-10 04:48:08 -04:00
import { ref , computed , onMounted , onUnmounted , 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'
2026-04-19 00:42:56 -04:00
import FipsNetworkCard from './server/FipsNetworkCard.vue'
2026-03-21 03:01:38 +00:00
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 )
2026-06-11 00:24:40 -04:00
const networkRefreshing = ref ( false )
const networkHasLoaded = ref ( false )
2026-03-11 00:04:26 +00:00
const networkData = ref ( {
2026-04-19 00:42:56 -04:00
wifiCount : 'N/A' , wifiSsid : null as string | null , torConnected : false , forwardCount : 'N/A' ,
2026-04-18 11:07:08 -04:00
vpnConnected : false , vpnProvider : '' , vpnIp : '' , wgIp : '' , wgPubkey : '' , 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
} )
2026-04-19 00:42:56 -04:00
// FIPS status row for the Local Network card. Full FIPS card lives below.
2026-04-20 13:46:03 -04:00
const fipsSummary = ref < { installed : boolean ; service _active : boolean ; key _present : boolean ; anchor _connected ? : boolean ; authenticated _peer _count ? : number } | null > ( null )
2026-04-19 00:42:56 -04:00
const fipsRowLabel = computed ( ( ) => {
const s = fipsSummary . value
if ( ! s ) return '…'
if ( ! s . installed ) return 'Not installed'
2026-04-20 13:46:03 -04:00
if ( ! s . service _active ) {
if ( ! s . key _present ) return 'Awaiting seed'
return 'Inactive'
}
// Service is active — reflect anchor reachability so the row flips in
// sync with the full FIPS card below.
if ( s . anchor _connected === false ) return 'No anchor'
const peers = s . authenticated _peer _count ? ? 0
return peers === 1 ? 'Active · 1 peer' : ` Active · ${ peers } peers `
2026-04-19 00:42:56 -04:00
} )
const fipsRowTextClass = computed ( ( ) => {
const s = fipsSummary . value
if ( ! s || ! s . installed ) return 'text-white/40'
2026-04-20 13:46:03 -04:00
if ( ! s . service _active ) return 'text-white/60'
if ( s . anchor _connected === false ) return 'text-orange-400'
return 'text-green-400'
2026-04-19 00:42:56 -04:00
} )
async function loadFipsSummary ( ) {
try {
2026-04-20 13:46:03 -04:00
fipsSummary . value = await rpcClient . call < { installed : boolean ; service _active : boolean ; key _present : boolean ; anchor _connected ? : boolean ; authenticated _peer _count ? : number } > ( { method : 'fips.status' } )
2026-04-19 00:42:56 -04:00
} catch { /* backend too old */ }
}
2026-03-11 00:04:26 +00:00
async function loadNetworkData ( ) {
2026-06-11 00:24:40 -04:00
const initialLoad = ! networkHasLoaded . value
networkLoading . value = initialLoad
networkRefreshing . value = ! initialLoad
2026-03-11 00:04:26 +00:00
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-04-19 00:42:56 -04: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' ; networkData . value . wifiSsid = ( diagRes . value as { wifi _ssid ? : string | null } ) . wifi _ssid ? ? null }
2026-03-21 03:01:38 +00:00
if ( fwdRes . status === 'fulfilled' ) { const c = fwdRes . value . forwards ? . length ? ? 0 ; networkData . value . forwardCount = ` ${ c } rule ${ c !== 1 ? 's' : '' } ` }
2026-04-18 11:07:08 -04: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 ? ? '' ; networkData . value . wgPubkey = ( vpnRes . value as Record < string , unknown > ) . wg _pubkey as string ? ? '' }
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 }
2026-06-11 00:24:40 -04:00
} catch { /* keep existing/default values */ } finally {
networkHasLoaded . value = true
networkLoading . value = false
networkRefreshing . value = false
}
2026-03-11 00:04:26 +00:00
}
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 showingNewDevice = ref ( false )
function closeDeviceModal ( ) {
showAddDeviceModal . value = false
peerQrData . value = null
newPeerName . value = ''
peerError . value = ''
showingNewDevice . value = false
}
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 )
2026-06-11 00:24:40 -04:00
const interfacesRefreshing = ref ( false )
const interfacesHaveLoaded = ref ( false )
2026-03-11 00:35:55 +00:00
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-06-12 03:00:15 -04:00
const wifiScanError = ref ( '' )
2026-03-11 10:44:56 +00:00
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 ( ) {
2026-06-11 00:24:40 -04:00
const initialLoad = ! interfacesHaveLoaded . value
const hadInterfaces = allInterfaces . value . length > 0
interfacesLoading . value = initialLoad
interfacesRefreshing . value = ! initialLoad
try { const res = await rpcClient . call < { interfaces : NetworkInterface [ ] } > ( { method : 'network.list-interfaces' } ) ; allInterfaces . value = res . interfaces } catch { if ( ! hadInterfaces ) allInterfaces . value = [ ] } finally { interfacesHaveLoaded . value = true ; interfacesLoading . value = false ; interfacesRefreshing . value = false }
2026-03-11 00:35:55 +00:00
}
2026-06-12 03:00:15 -04:00
function wifiRequiresPassword ( network : WifiNetwork | undefined ) : boolean {
const security = ( network ? . security || '' ) . trim ( ) . toLowerCase ( )
return security . length > 0 && security !== '--' && security !== 'none' && security !== 'open'
}
2026-03-11 00:35:55 +00:00
async function scanWifi ( ) {
2026-06-12 03:00:15 -04:00
wifiScanning . value = true ; wifiNetworks . value = [ ] ; wifiScanError . value = '' ; wifiError . value = ''
try {
const res = await rpcClient . call < { networks : WifiNetwork [ ] } > ( { method : 'network.scan-wifi' } )
wifiNetworks . value = res . networks
} catch ( e ) {
wifiNetworks . value = [ ]
wifiScanError . value = e instanceof Error ? e . message : 'WiFi scan failed.'
} finally { wifiScanning . value = false }
2026-03-11 00:35:55 +00:00
}
2026-06-12 03:00:15 -04:00
function selectWifi ( network : WifiNetwork ) {
wifiSelectedSsid . value = network . ssid ; wifiPassword . value = '' ; wifiError . value = ''
if ( wifiRequiresPassword ( network ) ) {
wifiConnecting . value = true
} else {
connectToWifi ( '' )
}
}
2026-03-11 00:35:55 +00:00
2026-03-21 03:01:38 +00:00
async function connectToWifi ( password : string ) {
2026-06-12 03:00:15 -04:00
if ( ! wifiSelectedSsid . value ) return
2026-03-21 03:01:38 +00:00
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 10:44:56 +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 00:35:55 +00:00
}
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 )
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 ( ) {
2026-06-11 00:24:40 -04:00
const hadServices = torServices . value . length > 0
2026-03-13 23:13:50 +00:00
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 }
2026-06-11 00:24:40 -04:00
catch { if ( ! hadServices ) { torServices . value = [ ] ; torDaemonRunning . value = false } } finally { torServicesLoading . value = false }
2026-03-13 23:13:50 +00:00
}
fix: overhaul container lifecycle — recovery, health, uninstall, UI state
Container recovery:
- Health monitor: MAX_RESTART_ATTEMPTS 3→10, interval 60s→120s
- Dependency-aware restarts: won't restart services before their deps
- Reset dependent counters when a dependency recovers
- Handle "created" state containers (were invisible to health monitor)
- Added IndeedHub, mempool-api, mysql to tier system
- Crash recovery: podman start timeout 30s→120s with retry
- Podman client: socket timeout 5s→30s, added restart policy
UI state representation:
- Exit code 0 shows "stopped" (gray), not "crashed" (red)
- Exit code 137 shows "killed (OOM)"
- Non-zero exit shows "crashed" (red)
- Added exit_code field to PackageDataEntry
Install/uninstall fixes:
- Install returns error when container doesn't start (was silent success)
- Post-install hooks awaited instead of fire-and-forget tokio::spawn
- Uninstall: graceful rm before force, volume prune, network cleanup
- Uninstall returns error on partial failure (was 200 OK)
Config consistency:
- DB passwords read from /var/lib/archipelago/secrets/ (was hardcoded)
- Bitcoin: added ZMQ ports 28332/28333 for LND block notifications
- IndeedHub port 7777→8190 (was conflicting with strfry)
- Marketplace versions: LND 0.17.4→0.18.4, Mempool 2.5.0→3.0.0
Performance:
- Metrics collector interval 60s→300s (was duplicating health monitor)
- Podman client: proper error propagation instead of unwrap_or_default
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
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-19 16:38:11 +00:00
}
2026-04-19 00:42:56 -04:00
onMounted ( ( ) => { checkTorStatus ( ) ; loadNetworkData ( ) ; loadInterfaces ( ) ; loadDiskStatus ( ) ; loadTorServices ( ) ; loadVpnPeers ( ) ; loadFipsSummary ( ) } )
2026-04-10 04:48:08 -04:00
// Poll VPN status every 15s so IP updates after pairing
const vpnPollInterval = setInterval ( async ( ) => {
try {
const vpnRes = await rpcClient . vpnStatus ( )
networkData . value . vpnConnected = vpnRes . connected
networkData . value . vpnProvider = vpnRes . provider ? ? ''
networkData . value . vpnIp = ( vpnRes . ip _address ? ? '' ) . replace ( /\/\d+$/ , '' )
networkData . value . wgIp = vpnRes . wg _ip ? ? ''
} catch { /* ignore */ }
} , 15000 )
onUnmounted ( ( ) => clearInterval ( vpnPollInterval ) )
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-06-11 00:24:40 -04:00
defineExpose ( {
allInterfaces ,
interfacesRefreshing ,
loadInterfaces ,
loadNetworkData ,
loadTorServices ,
networkData ,
networkRefreshing ,
torServices ,
torServicesLoading ,
} )
2026-01-24 22:59:20 +00:00
< / script >