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 >
< 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-03-11 10:44:56 +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 10:44:56 +00:00
< span class = "text-white/80 text-sm" > VPN < / span >
< / div >
< span class = "text-sm" : class = "networkData.vpnConnected ? 'text-green-400' : 'text-white/40'" >
{ { networkData . vpnConnected ? ` ${ networkData . vpnProvider } ( ${ networkData . vpnIp } ) ` : 'Not Connected' } }
< / span >
< / 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-03-20 02:59:29 +00:00
< div class = "grid grid-cols-1 2xl:grid-cols-2 gap-6 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
<!-- 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" >
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 = "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"
>
Scan WiFi
< / button >
2026-03-11 00:35:55 +00:00
< / div >
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
< 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' } } & 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 >
2026-03-11 00:35:55 +00:00
< / div >
< / div >
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
< 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 >
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
< / template >
2026-03-13 23:13:50 +00:00
< / div >
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"
: 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" > & times ; < / 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' ,
vpnConnected : false , vpnProvider : '' , vpnIp : '' , vpnHostname : '' , vpnPeers : 0 ,
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' : '' } ` }
if ( vpnRes . status === 'fulfilled' ) { networkData . value . vpnConnected = vpnRes . value . connected ; networkData . value . vpnProvider = vpnRes . value . provider ? ? '' ; networkData . value . vpnIp = vpnRes . value . ip _address ? ? '' }
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-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 }
2026-03-11 00:35:55 +00:00
}
2026-03-21 03:01:38 +00:00
function selectWifi ( ssid : string ) { wifiSelectedSsid . value = ssid ; wifiPassword . value = '' ; wifiConnecting . value = true }
2026-03-11 00:35:55 +00:00
2026-03-21 03:01:38 +00:00
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 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 ( ) {
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
}
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-03-21 03:01:38 +00:00
onMounted ( ( ) => { checkTorStatus ( ) ; loadNetworkData ( ) ; loadInterfaces ( ) ; loadDiskStatus ( ) ; loadTorServices ( ) } )
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 >