feat(02-03): OpenWrt gateway status caches on repeat visits; CloudFolder left as-is
Demo images / Build & push demo images (push) Successful in 3m43s

- OpenWrtGateway.vue: manual resources-store usage (no TTL gating — every
  mount force-refetched) replaced with useCachedResource (key
  server.openwrt-status, no item id — one gateway per node, fixed route
  with no :id param). load() keeps its explicit force-refresh semantics
  for connect/tollgate actions via a pendingParams closure; onMounted now
  only force-fetches when the cache is missing or past its 30s TTL.
- Fixed a real bug this conversion exposed (Rule 1): `loading` conflated
  'refreshing' with 'loading', hiding the already-rendered status panels
  behind the full skeleton on every background revalidation. Every mount
  used to force a fetch, so this was previously masked — cached content
  never got a chance to render before the skeleton took over. Now only a
  true first-load (no data yet) blocks on the skeleton (D-07).
- secondaryScreenCache.test.ts: repeat-open call-count coverage for
  OpenWrtGateway (within-TTL: one fetch; after TTL: cached paint + one
  more fetch).
- CloudFolder.vue: left unchanged. See 02-03-SUMMARY.md for the
  cache-placement decision and why a clean useCachedResource conversion
  needs a cloud.ts change outside this plan's files_modified scope.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
archipelago
2026-07-30 08:21:53 -04:00
co-authored by Claude Fable 5
parent 7c6c487a03
commit ec89901fa9
2 changed files with 135 additions and 18 deletions
+55 -18
View File
@@ -2,7 +2,7 @@
import { ref, computed, onMounted } from 'vue'
import { useRouter } from 'vue-router'
import { rpcClient } from '@/api/rpc-client'
import { useResourcesStore } from '@/stores/resources'
import { useCachedResource } from '@/composables/useCachedResource'
import BackButton from '@/components/BackButton.vue'
const router = useRouter()
@@ -74,14 +74,38 @@ interface ScannedNetwork {
encryption: string
}
const status = computed(() => statusEntry().data)
// Router status is cached in the shared resources store so revisits paint
// the last-known state instantly while a fresh read runs behind it.
const resources = useResourcesStore()
const statusEntry = () => resources.entry<RouterStatus>('server.openwrt-status')
// Router status is cached via the shared useCachedResource hook so revisits
// paint the last-known state instantly and a mount inside the TTL issues no
// new RPC. No item id in this key — there is exactly one configured gateway
// per node, not a per-item collection (route is the fixed `server/openwrt`,
// no :id param). `load(params)` is the explicit force-refresh path (connect,
// tollgate provision/save all want an unconditional re-fetch); `pendingParams`
// lets the fetcher read the params `load()` was called with without changing
// the cache key.
let pendingParams: Record<string, string> | undefined
const routerResource = useCachedResource<RouterStatus>({
key: 'server.openwrt-status',
fetcher: (signal) => rpcClient.call<RouterStatus>({
method: 'openwrt.get-status',
params: pendingParams ?? {},
signal,
timeout: 30000,
dedup: true,
maxRetries: 1,
}),
ttlMs: 30_000,
immediate: false, // initial fetch is gated explicitly in onMounted below
})
const status = computed(() => routerResource.data.value)
// 'refreshing' deliberately excluded (D-07, keep-last-value): a background
// TTL revalidation must not hide the already-rendered status panels behind
// the full loading skeleton — only a true first-load (no data at all yet)
// blocks on it. Previously this conflated the two, so a cache hit would
// still briefly show the skeleton on every mount instead of painting
// instantly.
const loading = computed(() => {
const s = statusEntry().loadState
return s === 'loading' || s === 'refreshing' || (s === 'idle' && statusEntry().data === null)
const s = routerResource.loadState.value
return routerResource.data.value === null && (s === 'loading' || s === 'idle')
})
const error = ref('')
const host = ref('')
@@ -122,17 +146,14 @@ const dhcpStart = ref(100)
const dhcpLimit = ref(150)
const masqEnabled = ref(true)
// Force-fetch: always refetches regardless of TTL. Used by every explicit
// user action (connect, tollgate provision/save) that must reflect a fresh
// server-side state, and by onMounted below when the cache is missing/stale.
async function load(params?: Record<string, string>) {
error.value = ''
await resources.refresh<RouterStatus>('server.openwrt-status', () =>
rpcClient.call<RouterStatus>({
method: 'openwrt.get-status',
params: params ?? {},
timeout: 30000,
dedup: true,
maxRetries: 1,
}))
const err = statusEntry().error
pendingParams = params
await routerResource.refresh()
const err = routerResource.error.value
if (err) {
if (err.includes('No router configured')) {
showConnectForm.value = true
@@ -371,7 +392,23 @@ const boardName = computed(() => {
return status.value.release.openwrt_board_name || ''
})
onMounted(() => load())
// Only force-fetch when the cache is missing or past its TTL, so a repeat
// open inside the TTL paints from cache with no new RPC. On a within-TTL
// remount, still reconcile the connect-form/error UI state from the cached
// entry (mirrors what a completed load() call would have set).
onMounted(() => {
if (routerResource.data.value === null || routerResource.isStale.value) {
void load()
return
}
const err = routerResource.error.value
if (err) {
if (err.includes('No router configured')) showConnectForm.value = true
else error.value = err
} else {
showConnectForm.value = false
}
})
</script>
<template>