archy/neode-ui/src/stores/homeStatus.ts
2026-05-18 11:47:12 -04:00

207 lines
6.4 KiB
TypeScript

import { defineStore } from 'pinia'
import { computed, reactive, ref } from 'vue'
import { rpcClient } from '@/api/rpc-client'
import { PackageState, type PackageDataEntry } from '@/types/api'
type LoadState = 'idle' | 'loading' | 'ready' | 'error'
interface SystemStatsSnapshot {
cpuPercent: number
memUsed: number
memTotal: number
memPercent: number
diskUsed: number
diskTotal: number
diskPercent: number
uptimeSecs: number
loadAvg1: number
loadAvg5: number
loadAvg15: number
bitcoinSyncPercent: number
bitcoinBlockHeight: number
bitcoinAvailable: boolean | null
}
const emptyStats = (): SystemStatsSnapshot => ({
cpuPercent: 0,
memUsed: 0,
memTotal: 0,
memPercent: 0,
diskUsed: 0,
diskTotal: 0,
diskPercent: 0,
uptimeSecs: 0,
loadAvg1: 0,
loadAvg5: 0,
loadAvg15: 0,
bitcoinSyncPercent: 0,
bitcoinBlockHeight: 0,
bitcoinAvailable: null,
})
export const useHomeStatusStore = defineStore('homeStatus', () => {
const stats = reactive<SystemStatsSnapshot>(emptyStats())
const systemLoadState = ref<LoadState>('idle')
const bitcoinLoadState = ref<LoadState>('idle')
const vpnLoadState = ref<LoadState>('idle')
const fipsLoadState = ref<LoadState>('idle')
const lastSystemRefreshAt = ref<number | null>(null)
const lastBitcoinRefreshAt = ref<number | null>(null)
const lastVpnRefreshAt = ref<number | null>(null)
const lastFipsRefreshAt = ref<number | null>(null)
const vpnStatus = ref<{
connected: boolean | null
provider: string
}>({ connected: null, provider: '' })
const fipsStatus = ref<{
installed: boolean
service_active: boolean
key_present: boolean
anchor_connected?: boolean
authenticated_peer_count?: number
} | null>(null)
const systemStatsLoaded = computed(() => systemLoadState.value === 'ready')
const bitcoinKnown = computed(() => stats.bitcoinAvailable !== null)
const vpnKnown = computed(() => vpnStatus.value.connected !== null)
async function refreshSystemStats() {
systemLoadState.value = systemLoadState.value === 'ready' ? 'ready' : 'loading'
try {
const res = await rpcClient.call<{
cpu_usage_percent: number
mem_used_bytes: number
mem_total_bytes: number
disk_used_bytes: number
disk_total_bytes: number
uptime_secs: number
load_avg_1?: number
load_avg_5?: number
load_avg_15?: number
}>({ method: 'system.stats' })
stats.cpuPercent = res.cpu_usage_percent
stats.memUsed = res.mem_used_bytes
stats.memTotal = res.mem_total_bytes
stats.memPercent = res.mem_total_bytes > 0 ? (res.mem_used_bytes / res.mem_total_bytes) * 100 : 0
stats.diskUsed = res.disk_used_bytes
stats.diskTotal = res.disk_total_bytes
stats.diskPercent = res.disk_total_bytes > 0 ? (res.disk_used_bytes / res.disk_total_bytes) * 100 : 0
stats.uptimeSecs = res.uptime_secs
stats.loadAvg1 = res.load_avg_1 ?? 0
stats.loadAvg5 = res.load_avg_5 ?? 0
stats.loadAvg15 = res.load_avg_15 ?? 0
systemLoadState.value = 'ready'
lastSystemRefreshAt.value = Date.now()
} catch {
systemLoadState.value = stats.uptimeSecs > 0 ? 'ready' : 'error'
}
}
async function refreshBitcoin(packages: Record<string, PackageDataEntry>) {
bitcoinLoadState.value = bitcoinLoadState.value === 'ready' ? 'ready' : 'loading'
try {
const btc = await rpcClient.call<{ block_height: number; sync_progress: number }>({
method: 'bitcoin.getinfo',
timeout: 5000,
})
stats.bitcoinSyncPercent = (btc.sync_progress ?? 0) * 100
stats.bitcoinBlockHeight = btc.block_height ?? 0
stats.bitcoinAvailable = true
bitcoinLoadState.value = 'ready'
lastBitcoinRefreshAt.value = Date.now()
} catch {
const btcPkg = packages['bitcoin-knots'] || packages['bitcoin-core'] || packages.bitcoin
if (btcPkg?.state === PackageState.Running) {
stats.bitcoinAvailable = true
bitcoinLoadState.value = 'ready'
lastBitcoinRefreshAt.value = Date.now()
return
}
if (btcPkg && (btcPkg.state === PackageState.Stopped || btcPkg.state === PackageState.Exited)) {
stats.bitcoinAvailable = false
bitcoinLoadState.value = 'ready'
lastBitcoinRefreshAt.value = Date.now()
return
}
// No authoritative package data yet. Keep the previous known value
// rather than flashing "Not running" during route changes/scans.
bitcoinLoadState.value = stats.bitcoinAvailable === null ? 'error' : 'ready'
}
}
async function refreshVpn(packages: Record<string, PackageDataEntry>) {
vpnLoadState.value = vpnLoadState.value === 'ready' ? 'ready' : 'loading'
try {
const status = await rpcClient.vpnStatus()
vpnStatus.value = {
connected: status.connected,
provider: status.provider ?? status.configured_provider ?? '',
}
vpnLoadState.value = 'ready'
lastVpnRefreshAt.value = Date.now()
} catch {
const tailscale = packages.tailscale
if (tailscale?.state === PackageState.Running) {
vpnStatus.value = { connected: true, provider: 'tailscale' }
vpnLoadState.value = 'ready'
lastVpnRefreshAt.value = Date.now()
return
}
vpnLoadState.value = vpnStatus.value.connected === null ? 'error' : 'ready'
}
}
async function refreshFips() {
fipsLoadState.value = fipsLoadState.value === 'ready' ? 'ready' : 'loading'
try {
const status = await rpcClient.call<{
installed: boolean
service_active: boolean
key_present: boolean
anchor_connected?: boolean
authenticated_peer_count?: number
}>({ method: 'fips.status' })
fipsStatus.value = status
fipsLoadState.value = 'ready'
lastFipsRefreshAt.value = Date.now()
} catch {
fipsLoadState.value = fipsStatus.value ? 'ready' : 'error'
}
}
async function refresh(packages: Record<string, PackageDataEntry>) {
await Promise.all([
refreshSystemStats(),
refreshBitcoin(packages),
refreshVpn(packages),
refreshFips(),
])
}
return {
stats,
systemLoadState,
bitcoinLoadState,
vpnLoadState,
fipsLoadState,
systemStatsLoaded,
bitcoinKnown,
vpnKnown,
vpnStatus,
fipsStatus,
lastSystemRefreshAt,
lastBitcoinRefreshAt,
lastVpnRefreshAt,
lastFipsRefreshAt,
refresh,
refreshSystemStats,
refreshBitcoin,
refreshVpn,
refreshFips,
}
})