Keep the Mounts tab in place when an LXC modal opens

- Apply the static mount list as soon as it arrives instead of waiting
  for the df/stat runtime probe.
- Refill a guest's mount seed after a lifecycle event rather than
  leaving it empty until the next modal open.
- Retry the bulk modal cache while the server is still warming guests.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
MacRimi
2026-09-25 22:11:23 +02:00
co-authored by Claude Opus 5.5
parent 53b415b127
commit c0fa75f404
+70 -43
View File
@@ -1153,6 +1153,7 @@ export function VirtualMachines() {
cache.details.delete(vm.vmid) cache.details.delete(vm.vmid)
cache.backups.delete(vm.vmid) cache.backups.delete(vm.vmid)
cache.mountPoints.delete(vm.vmid) cache.mountPoints.delete(vm.vmid)
if (vm.type === "lxc") reseedMountPoints(vm.vmid)
cache.schedule.delete(vm.vmid) cache.schedule.delete(vm.vmid)
cache.firewall.delete(vm.vmid) cache.firewall.delete(vm.vmid)
dockerInventoryRequestedRef.current.delete(vm.vmid) dockerInventoryRequestedRef.current.delete(vm.vmid)
@@ -1355,7 +1356,8 @@ export function VirtualMachines() {
useEffect(() => { useEffect(() => {
if (!vmData || vmData.length === 0) return if (!vmData || vmData.length === 0) return
let cancelled = false let cancelled = false
fetchApi<{ let retryTimer: ReturnType<typeof setTimeout> | undefined
const hydrate = (attempt: number) => fetchApi<{
guests: Array<{ guests: Array<{
vmid: number vmid: number
type: "qemu" | "lxc" type: "qemu" | "lxc"
@@ -1370,6 +1372,10 @@ export function VirtualMachines() {
.then((payload) => { .then((payload) => {
if (cancelled || !payload?.guests) return if (cancelled || !payload?.guests) return
const cache = vmModalCacheRef.current const cache = vmModalCacheRef.current
// Guests the server has not warmed yet come back null; ask again
// shortly so their mount tab is seeded before the modal opens.
const pending = payload.guests.some((g) => g.type === "lxc" && !g.mount_points)
if (pending && attempt < 3) retryTimer = setTimeout(() => hydrate(attempt + 1), 20000)
for (const g of payload.guests) { for (const g of payload.guests) {
if (g.details) cache.details.set(g.vmid, g.details) if (g.details) cache.details.set(g.vmid, g.details)
if (g.backups?.backups) { if (g.backups?.backups) {
@@ -1403,7 +1409,11 @@ export function VirtualMachines() {
// Silent — modal open handlers fall back to individual // Silent — modal open handlers fall back to individual
// fetches if the ref cache is empty. // fetches if the ref cache is empty.
}) })
return () => { cancelled = true } hydrate(0)
return () => {
cancelled = true
if (retryTimer) clearTimeout(retryTimer)
}
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
}, [vmidsKey]) }, [vmidsKey])
@@ -1431,49 +1441,66 @@ export function VirtualMachines() {
// user perceives no lag. // user perceives no lag.
const hasSeed = mountPoints.length > 0 const hasSeed = mountPoints.length > 0
if (!hasSeed) setLoadingMounts(true) if (!hasSeed) setLoadingMounts(true)
try { // Each half is applied as soon as it lands: the static list decides
const [staticResp, runtimeResp] = await Promise.all([ // whether the tab exists and must not wait for df/stat.
fetchApi<{ const staticReq = fetchApi<{
ok: boolean ok: boolean
mount_points: LxcMountPoint[] mount_points: LxcMountPoint[]
ad_hoc_hint_count?: number ad_hoc_hint_count?: number
}>(`/api/lxc/${vmid}/mount-points`).catch((e) => { }>(`/api/lxc/${vmid}/mount-points`)
console.error("Error fetching static mount points:", e) .then((staticResp) => {
return null if (staticResp?.ok) {
}), const mp = staticResp.mount_points || []
fetchApi<{ const hint = staticResp.ad_hoc_hint_count ?? 0
ok: boolean setMountPoints(mp)
running: boolean setMountsAdHocHint(hint)
runtime: Record<string, Partial<LxcMountPoint>> vmModalCacheRef.current.mountPoints.set(vmid, { mount_points: mp, ad_hoc_hint_count: hint })
ad_hoc: LxcMountPoint[] } else if (!hasSeed) {
}>(`/api/lxc/${vmid}/mount-points/runtime`).catch((e) => { setMountPoints([])
console.error("Error fetching runtime mount points:", e) }
return null })
}), .catch((e) => {
]) console.error("Error fetching static mount points:", e)
if (staticResp?.ok) { if (!hasSeed) setMountPoints([])
const mp = staticResp.mount_points || [] })
const hint = staticResp.ad_hoc_hint_count ?? 0 .finally(() => setLoadingMounts(false))
setMountPoints(mp) const runtimeReq = fetchApi<{
setMountsAdHocHint(hint) ok: boolean
vmModalCacheRef.current.mountPoints.set(vmid, { mount_points: mp, ad_hoc_hint_count: hint }) running: boolean
} else if (!hasSeed) { runtime: Record<string, Partial<LxcMountPoint>>
setMountPoints([]) ad_hoc: LxcMountPoint[]
} }>(`/api/lxc/${vmid}/mount-points/runtime`)
if (runtimeResp?.ok) { .then((runtimeResp) => {
setMountPointsRuntime(runtimeResp.runtime || {}) if (runtimeResp?.ok) {
setAdHocMounts(runtimeResp.ad_hoc || []) setMountPointsRuntime(runtimeResp.runtime || {})
} else { setAdHocMounts(runtimeResp.ad_hoc || [])
} else {
setMountPointsRuntime({})
setAdHocMounts([])
}
})
.catch((e) => {
console.error("Error fetching runtime mount points:", e)
setMountPointsRuntime({}) setMountPointsRuntime({})
setAdHocMounts([]) setAdHocMounts([])
} })
} catch (error) { await Promise.all([staticReq, runtimeReq])
console.error("Error fetching LXC mount points:", error) }
if (!hasSeed) setMountPoints([])
setAdHocMounts([]) // Background refill of the static mount list after a lifecycle event,
} finally { // so the next open of that guest renders its tab bar complete.
setLoadingMounts(false) const reseedMountPoints = (vmid: number) => {
} fetchApi<{ ok: boolean; mount_points: LxcMountPoint[]; ad_hoc_hint_count?: number }>(
`/api/lxc/${vmid}/mount-points`,
)
.then((r) => {
if (!r?.ok) return
vmModalCacheRef.current.mountPoints.set(vmid, {
mount_points: r.mount_points || [],
ad_hoc_hint_count: r.ad_hoc_hint_count ?? 0,
})
})
.catch(() => {})
} }
const handleMetricsClick = () => { const handleMetricsClick = () => {