From e497f8fed10e6b93044af60adf76deecdd1048a3 Mon Sep 17 00:00:00 2001 From: ssmithx Date: Wed, 1 Jul 2026 13:25:43 +0000 Subject: [PATCH] feat(home): surface TollGate status on the Network tile MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- core/archipelago/src/api/rpc/openwrt.rs | 17 +++++++++++++ neode-ui/src/stores/homeStatus.ts | 34 +++++++++++++++++++++++++ neode-ui/src/views/Home.vue | 20 +++++++++++++++ 3 files changed, 71 insertions(+) diff --git a/core/archipelago/src/api/rpc/openwrt.rs b/core/archipelago/src/api/rpc/openwrt.rs index d96f1747..664335de 100644 --- a/core/archipelago/src/api/rpc/openwrt.rs +++ b/core/archipelago/src/api/rpc/openwrt.rs @@ -53,6 +53,7 @@ impl RpcHandler { ) -> Result { let saved = net_router::load_router_config(&self.config.data_dir).await?; let p = params.unwrap_or_default(); + let host_from_params = p.get("host").and_then(|v| v.as_str()).is_some(); let host = p .get("host") @@ -78,6 +79,22 @@ impl RpcHandler { let router = Router::connect_password(&host, 22, &ssh_user, &ssh_password)?; 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 let release = router.run_ok("cat /etc/openwrt_release").unwrap_or_default(); let hostname = router diff --git a/neode-ui/src/stores/homeStatus.ts b/neode-ui/src/stores/homeStatus.ts index 435d78e2..4c39cd8a 100644 --- a/neode-ui/src/stores/homeStatus.ts +++ b/neode-ui/src/stores/homeStatus.ts @@ -49,10 +49,12 @@ export const useHomeStatusStore = defineStore('homeStatus', () => { const bitcoinStale = ref(false) const vpnLoadState = ref('idle') const fipsLoadState = ref('idle') + const tollgateLoadState = ref('idle') const lastSystemRefreshAt = ref(null) const lastBitcoinRefreshAt = ref(null) const lastVpnRefreshAt = ref(null) const lastFipsRefreshAt = ref(null) + const lastTollgateRefreshAt = ref(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) { 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, } }) diff --git a/neode-ui/src/views/Home.vue b/neode-ui/src/views/Home.vue index 81468a21..d733404e 100644 --- a/neode-ui/src/views/Home.vue +++ b/neode-ui/src/views/Home.vue @@ -173,6 +173,10 @@
FIPS
{{ fipsStatusLabel }} +
+
TollGate
+ {{ tollgateStatusLabel }} +
{{ t('home.manageNetwork') }} @@ -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'