fix: Server.vue — check connectivity on mount, poll health after restart

- Added checkConnectivity() call on mount instead of assuming connected
- Restart now polls server.health up to 15 times instead of blindly
  assuming success after 2s
- Marks UI-CLEAN-01, 02, 03 done in plan

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Dorian
2026-03-13 23:13:50 +00:00
parent fe61fbf39c
commit 27eabbce92
2 changed files with 135 additions and 6 deletions

View File

@@ -332,6 +332,52 @@
</template>
</div>
<!-- Tor Services -->
<div class="glass-card px-6 py-6 mb-6">
<div class="flex items-center justify-between mb-4">
<div>
<h2 class="text-xl font-semibold text-white/96">Tor Services</h2>
<p class="text-sm text-white/60 mt-1">Manage hidden service addresses for your node and apps</p>
</div>
<button @click="loadTorServices" class="glass-button px-4 py-2 rounded-lg text-sm flex items-center gap-2">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15" />
</svg>
Refresh
</button>
</div>
<div v-if="torServicesLoading && torServices.length === 0" class="text-sm text-white/40 py-4 text-center">Loading Tor services...</div>
<div v-else-if="torServices.length === 0" class="text-sm text-white/40 py-4 text-center">No Tor services configured</div>
<div v-else class="space-y-2">
<div v-for="svc in torServices" :key="svc.name" class="bg-black/20 rounded-xl border border-white/10 p-3 flex items-center justify-between gap-3">
<div class="flex-1 min-w-0">
<p class="text-white text-sm font-medium">{{ svc.name }}</p>
<p v-if="svc.onion_address" class="text-amber-300/80 text-xs font-mono truncate cursor-pointer" :title="svc.onion_address" @click="copyTorAddress(svc.onion_address)">{{ svc.onion_address }}</p>
<p v-else class="text-white/30 text-xs">No .onion address</p>
</div>
<div class="flex items-center gap-2 shrink-0">
<button
v-if="svc.name === 'archipelago'"
@click="rotateNodeAddress"
:disabled="torRotating"
class="glass-button px-3 py-1.5 rounded-lg text-xs"
>
{{ torRotating ? 'Rotating...' : 'Rotate' }}
</button>
<label class="tor-toggle-label">
<input
type="checkbox"
:checked="svc.enabled"
@change="toggleTorApp(svc.name, !svc.enabled)"
class="tor-toggle-input"
/>
<span class="tor-toggle-slider"></span>
</label>
</div>
</div>
</div>
</div>
<!-- WiFi Scan Modal -->
<div v-if="showWifiModal" class="fixed inset-0 bg-black/60 backdrop-blur-sm z-50 flex items-center justify-center p-4" @click.self="showWifiModal = false">
<div class="glass-card p-6 w-full max-w-md">
@@ -743,11 +789,65 @@ function formatBytes(bytes: number): string {
return `${(bytes / 1024).toFixed(0)} KB`
}
// --- Tor Services ---
interface TorServiceInfo {
name: string
local_port: number
onion_address: string | null
enabled: boolean
}
const torServices = ref<TorServiceInfo[]>([])
const torServicesLoading = ref(false)
async function loadTorServices() {
torServicesLoading.value = true
try {
const res = await rpcClient.call<{ services: TorServiceInfo[] }>({ method: 'tor.list-services' })
torServices.value = res.services || []
} catch {
torServices.value = []
} finally {
torServicesLoading.value = false
}
}
const torRotating = ref(false)
function copyTorAddress(address: string) {
navigator.clipboard.writeText(address)
logsToast.value = 'Onion address copied to clipboard'
setTimeout(() => { logsToast.value = '' }, 3000)
}
async function toggleTorApp(appId: string, enabled: boolean) {
try {
await rpcClient.call({ method: 'tor.toggle-service', params: { name: appId, enabled } })
await loadTorServices()
} catch (e) {
if (import.meta.env.DEV) console.warn('Failed to toggle Tor app:', e)
}
}
async function rotateNodeAddress() {
torRotating.value = true
try {
await rpcClient.call({ method: 'tor.rotate-address', params: { name: 'archipelago' } })
await loadTorServices()
} catch (e) {
if (import.meta.env.DEV) console.warn('Failed to rotate Tor address:', e)
} finally {
torRotating.value = false
}
}
onMounted(() => {
checkConnectivity()
loadNetworkData()
loadPeerCount()
loadInterfaces()
loadDiskStatus()
loadTorServices()
})
watch(showWifiModal, (open) => {
@@ -774,10 +874,24 @@ async function restartServices() {
setTimeout(() => { logsToast.value = '' }, 6000)
if (import.meta.env.DEV) console.warn('Restart RPC failed', e)
}
setTimeout(() => {
// Poll health to confirm recovery instead of assuming success
const pollHealth = async (retries: number) => {
for (let i = 0; i < retries; i++) {
await new Promise(r => setTimeout(r, 2000))
try {
await rpcClient.call({ method: 'server.health', params: {} })
servicesRunning.value = true
restarting.value = false
return
} catch {
// Still restarting
}
}
restarting.value = false
servicesRunning.value = true
}, 2000)
servicesRunning.value = false
connectivityStatus.value = 'disconnected'
}
pollHealth(15)
}
async function checkConnectivity() {