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:
parent
d6c1feca97
commit
e497f8fed1
@ -53,6 +53,7 @@ impl RpcHandler {
|
|||||||
) -> Result<serde_json::Value> {
|
) -> Result<serde_json::Value> {
|
||||||
let saved = net_router::load_router_config(&self.config.data_dir).await?;
|
let saved = net_router::load_router_config(&self.config.data_dir).await?;
|
||||||
let p = params.unwrap_or_default();
|
let p = params.unwrap_or_default();
|
||||||
|
let host_from_params = p.get("host").and_then(|v| v.as_str()).is_some();
|
||||||
|
|
||||||
let host = p
|
let host = p
|
||||||
.get("host")
|
.get("host")
|
||||||
@ -78,6 +79,22 @@ impl RpcHandler {
|
|||||||
let router = Router::connect_password(&host, 22, &ssh_user, &ssh_password)?;
|
let router = Router::connect_password(&host, 22, &ssh_user, &ssh_password)?;
|
||||||
router.verify_openwrt()?;
|
router.verify_openwrt()?;
|
||||||
|
|
||||||
|
// Persist the connection so other views (e.g. the Home dashboard's
|
||||||
|
// Network tile) can poll `openwrt.get-status` with no params instead
|
||||||
|
// of every caller needing to carry host/credentials around. Only do
|
||||||
|
// this when the host actually came from params — otherwise every
|
||||||
|
// no-args poll would re-save the same thing it just read.
|
||||||
|
if host_from_params {
|
||||||
|
let _ = net_router::configure_router(
|
||||||
|
&self.config.data_dir,
|
||||||
|
net_router::RouterType::OpenWrt,
|
||||||
|
&host,
|
||||||
|
None,
|
||||||
|
Some(&ssh_user),
|
||||||
|
Some(&ssh_password),
|
||||||
|
).await;
|
||||||
|
}
|
||||||
|
|
||||||
// System info
|
// System info
|
||||||
let release = router.run_ok("cat /etc/openwrt_release").unwrap_or_default();
|
let release = router.run_ok("cat /etc/openwrt_release").unwrap_or_default();
|
||||||
let hostname = router
|
let hostname = router
|
||||||
|
|||||||
@ -49,10 +49,12 @@ export const useHomeStatusStore = defineStore('homeStatus', () => {
|
|||||||
const bitcoinStale = ref(false)
|
const bitcoinStale = ref(false)
|
||||||
const vpnLoadState = ref<LoadState>('idle')
|
const vpnLoadState = ref<LoadState>('idle')
|
||||||
const fipsLoadState = ref<LoadState>('idle')
|
const fipsLoadState = ref<LoadState>('idle')
|
||||||
|
const tollgateLoadState = ref<LoadState>('idle')
|
||||||
const lastSystemRefreshAt = ref<number | null>(null)
|
const lastSystemRefreshAt = ref<number | null>(null)
|
||||||
const lastBitcoinRefreshAt = ref<number | null>(null)
|
const lastBitcoinRefreshAt = ref<number | null>(null)
|
||||||
const lastVpnRefreshAt = ref<number | null>(null)
|
const lastVpnRefreshAt = ref<number | null>(null)
|
||||||
const lastFipsRefreshAt = ref<number | null>(null)
|
const lastFipsRefreshAt = ref<number | null>(null)
|
||||||
|
const lastTollgateRefreshAt = ref<number | null>(null)
|
||||||
|
|
||||||
const vpnStatus = ref<{
|
const vpnStatus = ref<{
|
||||||
connected: boolean | null
|
connected: boolean | null
|
||||||
@ -67,6 +69,9 @@ export const useHomeStatusStore = defineStore('homeStatus', () => {
|
|||||||
authenticated_peer_count?: number
|
authenticated_peer_count?: number
|
||||||
} | null>(null)
|
} | 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 systemStatsLoaded = computed(() => systemLoadState.value === 'ready')
|
||||||
const bitcoinKnown = computed(() => stats.bitcoinAvailable !== null)
|
const bitcoinKnown = computed(() => stats.bitcoinAvailable !== null)
|
||||||
const vpnKnown = computed(() => vpnStatus.value.connected !== 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>) {
|
async function refresh(packages: Record<string, PackageDataEntry>) {
|
||||||
await Promise.all([
|
await Promise.all([
|
||||||
refreshSystemStats(),
|
refreshSystemStats(),
|
||||||
refreshBitcoin(packages),
|
refreshBitcoin(packages),
|
||||||
refreshVpn(packages),
|
refreshVpn(packages),
|
||||||
refreshFips(),
|
refreshFips(),
|
||||||
|
refreshTollgate(),
|
||||||
])
|
])
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -201,19 +231,23 @@ export const useHomeStatusStore = defineStore('homeStatus', () => {
|
|||||||
bitcoinStale,
|
bitcoinStale,
|
||||||
vpnLoadState,
|
vpnLoadState,
|
||||||
fipsLoadState,
|
fipsLoadState,
|
||||||
|
tollgateLoadState,
|
||||||
systemStatsLoaded,
|
systemStatsLoaded,
|
||||||
bitcoinKnown,
|
bitcoinKnown,
|
||||||
vpnKnown,
|
vpnKnown,
|
||||||
vpnStatus,
|
vpnStatus,
|
||||||
fipsStatus,
|
fipsStatus,
|
||||||
|
tollgateStatus,
|
||||||
lastSystemRefreshAt,
|
lastSystemRefreshAt,
|
||||||
lastBitcoinRefreshAt,
|
lastBitcoinRefreshAt,
|
||||||
lastVpnRefreshAt,
|
lastVpnRefreshAt,
|
||||||
lastFipsRefreshAt,
|
lastFipsRefreshAt,
|
||||||
|
lastTollgateRefreshAt,
|
||||||
refresh,
|
refresh,
|
||||||
refreshSystemStats,
|
refreshSystemStats,
|
||||||
refreshBitcoin,
|
refreshBitcoin,
|
||||||
refreshVpn,
|
refreshVpn,
|
||||||
refreshFips,
|
refreshFips,
|
||||||
|
refreshTollgate,
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
@ -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>
|
<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>
|
<span class="text-sm font-medium" :class="fipsTextClass">{{ fipsStatusLabel }}</span>
|
||||||
</div>
|
</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>
|
||||||
<div class="home-card-buttons flex gap-2 mt-auto pt-4 shrink-0">
|
<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>
|
<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
|
const peers = s.authenticated_peer_count ?? 0
|
||||||
return peers === 1 ? 'Active · 1 peer' : `Active · ${peers} peers`
|
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(() => {
|
const bitcoinSyncDisplay = computed(() => {
|
||||||
if (homeStatus.stats.bitcoinAvailable === null) return 'Checking…'
|
if (homeStatus.stats.bitcoinAvailable === null) return 'Checking…'
|
||||||
if (!homeStatus.stats.bitcoinAvailable) return 'Not running'
|
if (!homeStatus.stats.bitcoinAvailable) return 'Not running'
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user