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(emptyStats()) const systemLoadState = ref('idle') const bitcoinLoadState = ref('idle') const vpnLoadState = ref('idle') const fipsLoadState = ref('idle') const lastSystemRefreshAt = ref(null) const lastBitcoinRefreshAt = ref(null) const lastVpnRefreshAt = ref(null) const lastFipsRefreshAt = ref(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) { 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) { 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) { 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, } })