feat(home): surface TollGate status on the Network tile

Add a TollGate row (Enabled/Disabled/Not installed) to the Home
dashboard's Network tile, polling the existing openwrt.get-status RPC
on the same cadence as the other network rows. Only rendered once an
OpenWrt router is actually configured, so nodes without one aren't
cluttered with an always-"Not configured" row.

Also fixes the underlying reason this could never have worked: nothing
in the OpenWrt Gateway flow ever persisted the router's host/credentials
server-side — the "connect" form only kept them in local component
state, so any no-args openwrt.get-status call (this new tile, and even
the Gateway page's own reload) always failed with "No router
configured" despite a fully working, provisioned router. Now
handle_openwrt_get_status saves the connection to router_config.json
whenever a host is explicitly passed in and the connection succeeds.
This commit is contained in:
2026-07-01 13:25:43 +00:00
parent d6c1feca97
commit e497f8fed1
3 changed files with 71 additions and 0 deletions
+34
View File
@@ -49,10 +49,12 @@ export const useHomeStatusStore = defineStore('homeStatus', () => {
const bitcoinStale = ref(false)
const vpnLoadState = ref<LoadState>('idle')
const fipsLoadState = ref<LoadState>('idle')
const tollgateLoadState = 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 lastTollgateRefreshAt = ref<number | null>(null)
const vpnStatus = ref<{
connected: boolean | null
@@ -67,6 +69,9 @@ export const useHomeStatusStore = defineStore('homeStatus', () => {
authenticated_peer_count?: number
} | null>(null)
// null = no OpenWrt router configured at all (tile row shows "Not configured").
const tollgateStatus = ref<{ installed: boolean; enabled: boolean } | null>(null)
const systemStatsLoaded = computed(() => systemLoadState.value === 'ready')
const bitcoinKnown = computed(() => stats.bitcoinAvailable !== null)
const vpnKnown = computed(() => vpnStatus.value.connected !== null)
@@ -185,12 +190,37 @@ export const useHomeStatusStore = defineStore('homeStatus', () => {
}
}
async function refreshTollgate() {
tollgateLoadState.value = tollgateLoadState.value === 'ready' ? 'ready' : 'loading'
try {
const res = await rpcClient.call<{ tollgate: { installed: boolean; enabled?: boolean } }>({
method: 'openwrt.get-status',
timeout: 15000,
})
tollgateStatus.value = { installed: res.tollgate.installed, enabled: res.tollgate.enabled ?? false }
tollgateLoadState.value = 'ready'
lastTollgateRefreshAt.value = Date.now()
} catch (e) {
const msg = e instanceof Error ? e.message : String(e)
if (msg.includes('No router configured')) {
// Not an error — most nodes simply don't have an OpenWrt gateway set up.
tollgateStatus.value = null
tollgateLoadState.value = 'ready'
lastTollgateRefreshAt.value = Date.now()
} else {
// Transient failure (SSH hiccup, router rebooting) — keep last-known state.
tollgateLoadState.value = tollgateStatus.value ? 'ready' : 'error'
}
}
}
async function refresh(packages: Record<string, PackageDataEntry>) {
await Promise.all([
refreshSystemStats(),
refreshBitcoin(packages),
refreshVpn(packages),
refreshFips(),
refreshTollgate(),
])
}
@@ -201,19 +231,23 @@ export const useHomeStatusStore = defineStore('homeStatus', () => {
bitcoinStale,
vpnLoadState,
fipsLoadState,
tollgateLoadState,
systemStatsLoaded,
bitcoinKnown,
vpnKnown,
vpnStatus,
fipsStatus,
tollgateStatus,
lastSystemRefreshAt,
lastBitcoinRefreshAt,
lastVpnRefreshAt,
lastFipsRefreshAt,
lastTollgateRefreshAt,
refresh,
refreshSystemStats,
refreshBitcoin,
refreshVpn,
refreshFips,
refreshTollgate,
}
})
+20
View File
@@ -173,6 +173,10 @@
<div class="flex items-center gap-3"><div class="w-2 h-2 rounded-full" :class="fipsDotClass"></div><span class="text-sm text-white/80">FIPS</span></div>
<span class="text-sm font-medium" :class="fipsTextClass">{{ fipsStatusLabel }}</span>
</div>
<div v-if="homeStatus.tollgateStatus" 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="tollgateDotClass"></div><span class="text-sm text-white/80">TollGate</span></div>
<span class="text-sm font-medium" :class="tollgateTextClass">{{ tollgateStatusLabel }}</span>
</div>
</div>
<div class="home-card-buttons flex gap-2 mt-auto pt-4 shrink-0">
<RouterLink to="/dashboard/server" class="home-card-btn flex-1 px-4 py-2 glass-button rounded-lg text-sm font-medium text-center transition-colors">{{ t('home.manageNetwork') }}</RouterLink>
@@ -445,6 +449,22 @@ const fipsStatusLabel = computed(() => {
const peers = s.authenticated_peer_count ?? 0
return peers === 1 ? 'Active · 1 peer' : `Active · ${peers} peers`
})
const tollgateDotClass = computed(() => {
const s = homeStatus.tollgateStatus
if (!s || !s.installed) return 'bg-white/40'
return s.enabled ? 'bg-green-400' : 'bg-yellow-400'
})
const tollgateTextClass = computed(() => {
const s = homeStatus.tollgateStatus
if (!s || !s.installed) return 'text-white/40'
return s.enabled ? 'text-green-400' : 'text-yellow-400'
})
const tollgateStatusLabel = computed(() => {
const s = homeStatus.tollgateStatus
if (!s) return homeStatus.tollgateLoadState === 'loading' ? 'Checking…' : 'Not configured'
if (!s.installed) return 'Not installed'
return s.enabled ? 'Enabled' : 'Disabled'
})
const bitcoinSyncDisplay = computed(() => {
if (homeStatus.stats.bitcoinAvailable === null) return 'Checking…'
if (!homeStatus.stats.bitcoinAvailable) return 'Not running'