feat(ui): FIPS network + seed-anchor cards render from cached resources (B4)
Some checks failed
Demo images / Build & push demo images (push) Failing after 2m6s

The two FIPS containers on the Server page were the last network cards
still fetching-on-mount into local refs — every visit was a blank card
until fips.status / fips.list-seed-anchors answered. Both now ride the
cached-resource layer: FipsNetworkCard shares the server.fips-summary
key with the Local Network card's FIPS row (one fetch, never disagree),
seed anchors cache under server.fips-seed-anchors, and mutations
write the RPC's authoritative result straight into the cache. The 15s
status poll skips hidden tabs — revalidate-on-focus covers the return.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
archipelago 2026-07-28 10:48:34 -04:00
parent 2971623145
commit 537c52d11c
2 changed files with 30 additions and 28 deletions

View File

@ -121,6 +121,7 @@
<script setup lang="ts">
import { computed, onMounted, onUnmounted, ref } from 'vue'
import { rpcClient } from '@/api/rpc-client'
import { useCachedResource } from '@/composables/useCachedResource'
import { safeClipboardWrite } from '@/views/web5/utils'
import FipsSeedAnchorsCard from './FipsSeedAnchorsCard.vue'
@ -136,7 +137,14 @@ interface FipsStatus {
anchor_connected?: boolean
}
const status = ref<FipsStatus>({
// Shares `server.fips-summary` with the Local Network card's FIPS row, so
// both paint from the same cache instantly on revisit and never disagree.
const statusRes = useCachedResource<FipsStatus>({
key: 'server.fips-summary',
ttlMs: 15_000,
fetcher: (signal) => rpcClient.call<FipsStatus>({ method: 'fips.status', signal, dedup: true, maxRetries: 1 }),
})
const status = computed<FipsStatus>(() => statusRes.data.value ?? {
installed: false,
version: null,
service_state: 'unknown',
@ -199,18 +207,11 @@ function flash(msg: string, isError = false) {
setTimeout(() => { statusMessage.value = '' }, 6000)
}
async function loadStatus() {
try {
status.value = await rpcClient.call<FipsStatus>({ method: 'fips.status' })
} catch (e) {
if (import.meta.env.DEV) console.warn('fips.status failed', e)
}
}
async function installAndActivate() {
installing.value = true
try {
status.value = await rpcClient.call<FipsStatus>({ method: 'fips.install' })
const next = await rpcClient.call<FipsStatus>({ method: 'fips.install' })
statusRes.optimistic(() => next) // confirmed server state, not a guess
flash('FIPS started')
} catch (e: unknown) {
const msg = e instanceof Error ? e.message : String(e)
@ -235,7 +236,7 @@ async function reconnectAnchor() {
}>({ method: 'fips.reconnect', timeout: 60_000 })
// Update the card with the post-reconnect status returned by the
// backend avoids an extra status fetch race.
status.value = { ...status.value, ...res.after }
statusRes.optimistic((cur) => ({ ...(cur ?? status.value), ...res.after }))
if (res.recovered) {
flash('Anchor reconnected.')
} else if (res.likely_cause === 'connected') {
@ -258,8 +259,10 @@ async function reconnectAnchor() {
// stuck showing whatever anchor state existed at mount time forever.
let statusInterval: ReturnType<typeof setInterval> | null = null
onMounted(() => {
loadStatus()
statusInterval = setInterval(loadStatus, 15000)
statusInterval = setInterval(() => {
if (document.hidden) return
void statusRes.refresh()
}, 15000)
})
onUnmounted(() => {
if (statusInterval) clearInterval(statusInterval)

View File

@ -88,8 +88,9 @@
</template>
<script setup lang="ts">
import { onMounted, reactive, ref } from 'vue'
import { computed, reactive, ref } from 'vue'
import { rpcClient } from '@/api/rpc-client'
import { useCachedResource } from '@/composables/useCachedResource'
defineProps<{ closable?: boolean }>()
defineEmits<{ (e: 'close'): void }>()
@ -107,7 +108,16 @@ interface ApplyResult {
message: string
}
const anchors = ref<SeedAnchor[]>([])
const anchorsRes = useCachedResource<SeedAnchor[]>({
key: 'server.fips-seed-anchors',
fetcher: async (signal) => {
const res = await rpcClient.call<{ seed_anchors: SeedAnchor[] }>({
method: 'fips.list-seed-anchors', signal, dedup: true, maxRetries: 1,
})
return res.seed_anchors
},
})
const anchors = computed(() => anchorsRes.data.value ?? [])
const adding = ref(false)
const applying = ref(false)
const statusMessage = ref('')
@ -126,15 +136,6 @@ function flash(msg: string, isError = false) {
setTimeout(() => { statusMessage.value = '' }, 6000)
}
async function load() {
try {
const res = await rpcClient.call<{ seed_anchors: SeedAnchor[] }>({ method: 'fips.list-seed-anchors' })
anchors.value = res.seed_anchors
} catch (e: unknown) {
if (import.meta.env.DEV) console.warn('fips.list-seed-anchors failed', e)
}
}
async function addAnchor() {
if (!draft.npub.trim() || !draft.address.trim()) return
adding.value = true
@ -148,7 +149,7 @@ async function addAnchor() {
label: draft.label.trim(),
},
})
anchors.value = res.seed_anchors
anchorsRes.optimistic(() => res.seed_anchors) // authoritative post-add list
draft.npub = ''
draft.address = ''
draft.label = ''
@ -168,7 +169,7 @@ async function removeAnchor(npub: string) {
method: 'fips.remove-seed-anchor',
params: { npub },
})
anchors.value = res.seed_anchors
anchorsRes.optimistic(() => res.seed_anchors)
flash('Anchor removed.')
} catch (e: unknown) {
const msg = e instanceof Error ? e.message : String(e)
@ -189,6 +190,4 @@ async function applyAll() {
applying.value = false
}
}
onMounted(load)
</script>