mirror of
https://github.com/MacRimi/ProxMenux.git
synced 2026-11-02 19:02:31 +00:00
fix: preserve native discovery results after delayed cache reads
This commit is contained in:
@@ -475,6 +475,7 @@ export function LxcAppPanel({ vmid, ctIp, onChange, managed, initialData, oci }:
|
|||||||
const [sidecar, setSidecar] = useState<SidecarResponse | null>(seed?.sidecar ?? null)
|
const [sidecar, setSidecar] = useState<SidecarResponse | null>(seed?.sidecar ?? null)
|
||||||
const [suggestions, setSuggestions] = useState<Suggestions | null>(seed?.suggestions ?? null)
|
const [suggestions, setSuggestions] = useState<Suggestions | null>(seed?.suggestions ?? null)
|
||||||
const [nativeDiscovery, setNativeDiscovery] = useState<NativeDiscovery | null>(null)
|
const [nativeDiscovery, setNativeDiscovery] = useState<NativeDiscovery | null>(null)
|
||||||
|
const nativeRequest = useRef(0)
|
||||||
const targetRef = useRef(vmid)
|
const targetRef = useRef(vmid)
|
||||||
targetRef.current = vmid
|
targetRef.current = vmid
|
||||||
const targetGeneration = useRef(0)
|
const targetGeneration = useRef(0)
|
||||||
@@ -485,6 +486,7 @@ export function LxcAppPanel({ vmid, ctIp, onChange, managed, initialData, oci }:
|
|||||||
}
|
}
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const generation = targetGeneration.current
|
const generation = targetGeneration.current
|
||||||
|
const request = ++nativeRequest.current
|
||||||
setNativeDiscovery(null)
|
setNativeDiscovery(null)
|
||||||
setEditing(null)
|
setEditing(null)
|
||||||
setDetectionNotice(null)
|
setDetectionNotice(null)
|
||||||
@@ -492,7 +494,8 @@ export function LxcAppPanel({ vmid, ctIp, onChange, managed, initialData, oci }:
|
|||||||
setSaving(false)
|
setSaving(false)
|
||||||
if (managed) return
|
if (managed) return
|
||||||
fetchApi<NativeDiscovery>(`/api/vms/${vmid}/apps/native-discovery`).then(result => {
|
fetchApi<NativeDiscovery>(`/api/vms/${vmid}/apps/native-discovery`).then(result => {
|
||||||
if (targetRef.current === vmid && targetGeneration.current === generation) setNativeDiscovery(result)
|
if (targetRef.current === vmid && targetGeneration.current === generation
|
||||||
|
&& nativeRequest.current === request) setNativeDiscovery(result)
|
||||||
}).catch(() => { /* cached metadata is optional; never initiate a scan */ })
|
}).catch(() => { /* cached metadata is optional; never initiate a scan */ })
|
||||||
return () => { targetGeneration.current += 1 }
|
return () => { targetGeneration.current += 1 }
|
||||||
}, [vmid, managed])
|
}, [vmid, managed])
|
||||||
@@ -787,7 +790,9 @@ export function LxcAppPanel({ vmid, ctIp, onChange, managed, initialData, oci }:
|
|||||||
|
|
||||||
const searchInstalledApplications = async () => {
|
const searchInstalledApplications = async () => {
|
||||||
const generation = targetGeneration.current
|
const generation = targetGeneration.current
|
||||||
|
const request = ++nativeRequest.current
|
||||||
const isCurrent = () => targetRef.current === vmid && targetGeneration.current === generation
|
const isCurrent = () => targetRef.current === vmid && targetGeneration.current === generation
|
||||||
|
&& nativeRequest.current === request
|
||||||
const before = new Set([...visibleDetected, ...visibleWorkloads].map((item) => item.slug))
|
const before = new Set([...visibleDetected, ...visibleWorkloads].map((item) => item.slug))
|
||||||
setSearchingApplications(true)
|
setSearchingApplications(true)
|
||||||
setDetectionNotice(null)
|
setDetectionNotice(null)
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ function fixture({nativeCandidate = candidate} = {}) {
|
|||||||
const calls = [], cachedWrites = []
|
const calls = [], cachedWrites = []
|
||||||
let native = {vmid: 101, status: 'not_scanned', candidates: [], stale: false}
|
let native = {vmid: 101, status: 'not_scanned', candidates: [], stale: false}
|
||||||
let holdScan, scanGate, holdSave, saveGate, scanStatus = 'complete'
|
let holdScan, scanGate, holdSave, saveGate, scanStatus = 'complete'
|
||||||
|
let cacheGate, resolveCache, cachePending = false
|
||||||
const seed = {sidecar: {vmid: 101, apps: []}, suggestions: {extras: [], port_suggestions: []}}
|
const seed = {sidecar: {vmid: 101, apps: []}, suggestions: {extras: [], port_suggestions: []}}
|
||||||
const react = {
|
const react = {
|
||||||
useState(initial) {
|
useState(initial) {
|
||||||
@@ -47,6 +48,12 @@ function fixture({nativeCandidate = candidate} = {}) {
|
|||||||
const api = async (url, options = {}) => {
|
const api = async (url, options = {}) => {
|
||||||
calls.push({url, ...options})
|
calls.push({url, ...options})
|
||||||
if (url.endsWith('/native-discovery')) {
|
if (url.endsWith('/native-discovery')) {
|
||||||
|
if (options.method !== 'POST' && cachePending) {
|
||||||
|
cachePending = false
|
||||||
|
const oldSnapshot = structuredClone(native)
|
||||||
|
await cacheGate
|
||||||
|
return oldSnapshot
|
||||||
|
}
|
||||||
if (options.method === 'POST') {
|
if (options.method === 'POST') {
|
||||||
if (scanGate) await scanGate
|
if (scanGate) await scanGate
|
||||||
native = {...native, status: scanStatus, candidates: [nativeCandidate]}
|
native = {...native, status: scanStatus, candidates: [nativeCandidate]}
|
||||||
@@ -92,6 +99,8 @@ function fixture({nativeCandidate = candidate} = {}) {
|
|||||||
}
|
}
|
||||||
return {render, settle, calls, cachedWrites, button, tree: () => tree,
|
return {render, settle, calls, cachedWrites, button, tree: () => tree,
|
||||||
setNative: value => {native = value},
|
setNative: value => {native = value},
|
||||||
|
holdInitialCache: () => {cachePending = true; cacheGate = new Promise(resolve => {resolveCache = resolve})},
|
||||||
|
resolveInitialCache: () => resolveCache(),
|
||||||
setScanStatus: value => {scanStatus = value},
|
setScanStatus: value => {scanStatus = value},
|
||||||
holdScan: () => {scanGate = new Promise(resolve => {holdScan = resolve})}, resolveScan: () => holdScan(),
|
holdScan: () => {scanGate = new Promise(resolve => {holdScan = resolve})}, resolveScan: () => holdScan(),
|
||||||
holdSave: () => {saveGate = new Promise(resolve => {holdSave = resolve})}, resolveSave: () => holdSave(),
|
holdSave: () => {saveGate = new Promise(resolve => {holdSave = resolve})}, resolveSave: () => holdSave(),
|
||||||
@@ -133,6 +142,15 @@ async function main() {
|
|||||||
const hestiaPayload = JSON.parse(hestia.calls.find(c => /\/apps$/.test(c.url) && c.method === 'POST').body)
|
const hestiaPayload = JSON.parse(hestia.calls.find(c => /\/apps$/.test(c.url) && c.method === 'POST').body)
|
||||||
assert.deepEqual(hestiaPayload.ports, [{port: 8083, scheme: 'https', web_path: ''}],
|
assert.deepEqual(hestiaPayload.ports, [{port: 8083, scheme: 'https', web_path: ''}],
|
||||||
'a verified Hestia panel pre-fills only its fixed HTTPS endpoint')
|
'a verified Hestia panel pre-fills only its fixed HTTPS endpoint')
|
||||||
|
const delayedCache = fixture()
|
||||||
|
delayedCache.holdInitialCache()
|
||||||
|
delayedCache.render(); await delayedCache.settle()
|
||||||
|
await delayedCache.button('searchApplications').props.onClick(); delayedCache.render()
|
||||||
|
assert.ok(text(delayedCache.tree()).includes('/opt/handmade/server'))
|
||||||
|
delayedCache.resolveInitialCache(); await delayedCache.settle()
|
||||||
|
assert.ok(text(delayedCache.tree()).includes('/opt/handmade/server'),
|
||||||
|
'a delayed initial cache read must not erase a completed explicit search')
|
||||||
|
|
||||||
const partial = fixture()
|
const partial = fixture()
|
||||||
partial.render(); await partial.settle(); partial.setScanStatus('partial')
|
partial.render(); await partial.settle(); partial.setScanStatus('partial')
|
||||||
await partial.button('searchApplications').props.onClick(); partial.render()
|
await partial.button('searchApplications').props.onClick(); partial.render()
|
||||||
|
|||||||
Reference in New Issue
Block a user