From b5a2a3f52bf83e2fb9e998138b9679d12acc83c0 Mon Sep 17 00:00:00 2001 From: VAIO73 <50487331+Vaso73@users.noreply.github.com> Date: Sun, 18 Oct 2026 16:15:09 +0000 Subject: [PATCH 1/2] fix: explain why native discovery cannot scan stopped containers --- AppImage/components/lxc-app-panel.tsx | 6 ++- AppImage/messages/en/common.json | 1 + AppImage/messages/sk/common.json | 1 + .../tests/test_native_app_discovery.py | 21 ++++++++++ .../test_native_app_discovery_ui.cjs | 38 ++++++++++++++++++- 5 files changed, 64 insertions(+), 3 deletions(-) diff --git a/AppImage/components/lxc-app-panel.tsx b/AppImage/components/lxc-app-panel.tsx index 22154ef6..25fc0461 100644 --- a/AppImage/components/lxc-app-panel.tsx +++ b/AppImage/components/lxc-app-panel.tsx @@ -125,6 +125,7 @@ interface PortDetail { interface NativeDiscovery { vmid: number status: "not_scanned" | "complete" | "partial" | "error" | "busy" + issues?: string[] stale: boolean candidates: DetectedApp[] } @@ -804,7 +805,10 @@ export function LxcAppPanel({ vmid, ctIp, onChange, managed, initialData, oci }: if (!isCurrent()) return setNativeDiscovery(native) if (native.status === "error" || native.status === "busy") { - setDetectionNotice({ found: false, text: t("vmLxc.appEditor.nativeScanFailed") }) + const stopped = native.status === "error" && Array.isArray(native.issues) + && native.issues.includes("guest_stopped") + setDetectionNotice({ found: false, text: t(stopped + ? "vmLxc.appEditor.nativeScanStopped" : "vmLxc.appEditor.nativeScanFailed") }) return } const result: Suggestions = await fetchApi(`/api/vms/${vmid}/apps/suggestions`, { diff --git a/AppImage/messages/en/common.json b/AppImage/messages/en/common.json index 5f144106..050cd5ab 100644 --- a/AppImage/messages/en/common.json +++ b/AppImage/messages/en/common.json @@ -1746,6 +1746,7 @@ "nativeEvidenceDirectory": "Working directory", "nativeEvidenceService": "Service definition", "nativeScanFailed": "Application search could not finish. Previous evidence may be outdated; try again.", + "nativeScanStopped": "This container is stopped. Application search is only available while the container is running.", "nativeScanPartial": "Application search is incomplete. Review the available evidence and try again.", "nativeVersionUnknown": "Installed version unknown", "nativeEvidenceStale": "Evidence may be outdated or incomplete.", diff --git a/AppImage/messages/sk/common.json b/AppImage/messages/sk/common.json index af9535b0..e2156568 100644 --- a/AppImage/messages/sk/common.json +++ b/AppImage/messages/sk/common.json @@ -1690,6 +1690,7 @@ "nativeEvidenceDirectory": "Pracovný priečinok", "nativeEvidenceService": "Definícia služby", "nativeScanFailed": "Vyhľadávanie aplikácií sa nepodarilo dokončiť. Predchádzajúce dôkazy môžu byť zastarané; skús to znova.", + "nativeScanStopped": "Kontajner je vypnutý. Vyhľadávanie aplikácií je dostupné iba v spustenom kontajneri.", "nativeScanPartial": "Vyhľadávanie aplikácií je neúplné. Skontroluj dostupné dôkazy a skús to znova.", "nativeVersionUnknown": "Nainštalovaná verzia nie je známa", "nativeEvidenceStale": "Dôkazy môžu byť zastarané alebo neúplné.", diff --git a/AppImage/scripts/tests/test_native_app_discovery.py b/AppImage/scripts/tests/test_native_app_discovery.py index dc24424a..3d92533d 100644 --- a/AppImage/scripts/tests/test_native_app_discovery.py +++ b/AppImage/scripts/tests/test_native_app_discovery.py @@ -746,6 +746,27 @@ class Routes(unittest.TestCase): scan.assert_not_called() read.assert_not_called() + def test_stopped_scan_route_exposes_reason_and_keeps_cached_evidence(self): + item = {'discovery_id': 'native-retained', 'name': 'Retained app', + 'installation_path': '/opt/retained/server'} + previous = {'vmid': 101, 'status': 'complete', 'stale': False, + 'scanned_at': time.time(), 'candidates': [item]} + native._cache[101] = copy.deepcopy(previous) + with patch.object(native, 'scan', side_effect=native.ProbeError('guest_stopped')), \ + patch.object(lxc_apps, 'load_sidecar', return_value={'apps': []}), \ + patch.object(lxc_apps, 'get_suggestions') as suggestions, \ + patch.object(lxc_apps, '_write_sidecar') as write: + response = self.client.post('/api/vms/101/apps/native-discovery') + self.assertEqual(response.status_code, 200) + result = response.get_json() + self.assertEqual(result['status'], 'error') + self.assertEqual(result['issues'], ['guest_stopped']) + self.assertTrue(result['stale']) + self.assertEqual(result['candidates'], [item]) + self.assertEqual(result['scanned_at'], previous['scanned_at']) + suggestions.assert_not_called() + write.assert_not_called() + def test_actual_lifecycle_marks_last_known_stale_without_scanning(self): native._cache[101] = {'vmid': 101, 'status': 'complete', 'stale': False, 'candidates': [], 'scanned_at': time.time()} diff --git a/tests/lxc_updates/test_native_app_discovery_ui.cjs b/tests/lxc_updates/test_native_app_discovery_ui.cjs index 2ffa802e..3cfc5a09 100644 --- a/tests/lxc_updates/test_native_app_discovery_ui.cjs +++ b/tests/lxc_updates/test_native_app_discovery_ui.cjs @@ -27,7 +27,7 @@ function fixture({nativeCandidate = candidate} = {}) { let cursor = 0, tree, props, effects = [], states = [], refs = [], effectStates = [] const calls = [], cachedWrites = [] let native = {vmid: 101, status: 'not_scanned', candidates: [], stale: false} - let holdScan, scanGate, holdSave, saveGate, scanStatus = 'complete' + let holdScan, scanGate, holdSave, saveGate, scanStatus = 'complete', scanIssues let cacheGate, resolveCache, cachePending = false const seed = {sidecar: {vmid: 101, apps: []}, suggestions: {extras: [], port_suggestions: []}} const react = { @@ -56,7 +56,7 @@ function fixture({nativeCandidate = candidate} = {}) { } if (options.method === 'POST') { if (scanGate) await scanGate - native = {...native, status: scanStatus, candidates: [nativeCandidate]} + native = {...native, status: scanStatus, issues: scanIssues, candidates: [nativeCandidate]} } return structuredClone(native) } @@ -102,6 +102,7 @@ function fixture({nativeCandidate = candidate} = {}) { holdInitialCache: () => {cachePending = true; cacheGate = new Promise(resolve => {resolveCache = resolve})}, resolveInitialCache: () => resolveCache(), setScanStatus: value => {scanStatus = value}, + setScanIssues: value => {scanIssues = value}, holdScan: () => {scanGate = new Promise(resolve => {holdScan = resolve})}, resolveScan: () => holdScan(), holdSave: () => {saveGate = new Promise(resolve => {holdSave = resolve})}, resolveSave: () => holdSave(), } @@ -162,6 +163,39 @@ async function main() { assert.ok(text(failed.tree()).includes('nativeScanFailed')) assert.equal(failed.calls.filter(c => c.url.endsWith('/apps/suggestions') && c.method === 'POST').length, 0) + // Match the collector's public response, including retained stale evidence. + // A busy reply may carry an earlier guest_stopped issue from its cache. + for (const [status, issues, expected] of [ + ['error', ['guest_stopped'], 'nativeScanStopped'], + ['error', ['status_unknown'], 'nativeScanFailed'], + ['error', ['timeout'], 'nativeScanFailed'], + ['error', [], 'nativeScanFailed'], + ['error', undefined, 'nativeScanFailed'], + ['error', 'guest_stopped', 'nativeScanFailed'], + ['busy', ['guest_stopped'], 'nativeScanFailed'], + ]) { + const denied = fixture() + denied.setNative({vmid: 101, status: 'complete', stale: true, candidates: [candidate]}) + denied.render(); await denied.settle() + denied.setScanStatus(status); denied.setScanIssues(issues) + await denied.button('searchApplications').props.onClick(); denied.render() + assert.ok(text(denied.tree()).includes(expected), `${status}/${JSON.stringify(issues)} uses ${expected}`) + assert.ok(!text(denied.tree()).includes(expected === 'nativeScanStopped' ? 'nativeScanFailed' : 'nativeScanStopped')) + assert.ok(text(denied.tree()).includes('/opt/handmade/server'), 'failure retains prior evidence') + assert.equal(denied.calls.filter(c => c.method === 'POST').length, 1, 'only the explicit discovery POST, no suggestion, save or control call') + assert.equal(denied.cachedWrites.length, 0, 'failure cannot overwrite registrations') + assert.equal(denied.button('searchApplications').props.disabled, false, 'search settles and can be retried') + } + + const stoppedLate = fixture() + stoppedLate.render(); await stoppedLate.settle() + stoppedLate.setScanStatus('error'); stoppedLate.setScanIssues(['guest_stopped']) + stoppedLate.holdScan() + const stoppedRequest = stoppedLate.button('searchApplications').props.onClick() + stoppedLate.render({vmid: 102}); await stoppedLate.settle() + stoppedLate.resolveScan(); await stoppedRequest; stoppedLate.render() + assert.ok(!text(stoppedLate.tree()).includes('nativeScanStopped'), 'late stopped response cannot label another CT') + const editorSwitch = fixture() editorSwitch.render(); await editorSwitch.settle() await editorSwitch.button('searchApplications').props.onClick(); editorSwitch.render() From e52d194b9a3bb796949f58cd26aedd448d6e09d0 Mon Sep 17 00:00:00 2001 From: MacRimi <123239993+MacRimi@users.noreply.github.com> Date: Sun, 18 Oct 2026 20:09:24 +0200 Subject: [PATCH 2/2] Translate the stopped-container search notice --- AppImage/messages/de/common.json | 1 + AppImage/messages/es/common.json | 1 + AppImage/messages/fr/common.json | 1 + AppImage/messages/it/common.json | 1 + AppImage/messages/pt/common.json | 1 + AppImage/messages/sv/common.json | 1 + 6 files changed, 6 insertions(+) diff --git a/AppImage/messages/de/common.json b/AppImage/messages/de/common.json index 6c9a5fb6..129e270e 100644 --- a/AppImage/messages/de/common.json +++ b/AppImage/messages/de/common.json @@ -1744,6 +1744,7 @@ "nativeEvidenceDirectory": "Arbeitsverzeichnis", "nativeEvidenceService": "Dienstdefinition", "nativeScanFailed": "Die Anwendungssuche konnte nicht abgeschlossen werden. Frühere Ergebnisse sind möglicherweise veraltet; versuchen Sie es erneut.", + "nativeScanStopped": "Dieser Container ist gestoppt. Die Anwendungssuche ist nur verfügbar, während der Container läuft.", "nativeScanPartial": "Die Anwendungssuche ist unvollständig. Prüfen Sie die gefundenen Hinweise und versuchen Sie es erneut.", "nativeVersionUnknown": "Installierte Version unbekannt", "nativeEvidenceStale": "Die Hinweise sind möglicherweise veraltet oder unvollständig.", diff --git a/AppImage/messages/es/common.json b/AppImage/messages/es/common.json index b4eefbb2..102b62df 100644 --- a/AppImage/messages/es/common.json +++ b/AppImage/messages/es/common.json @@ -1729,6 +1729,7 @@ "nativeEvidenceDirectory": "Directorio de trabajo", "nativeEvidenceService": "Definición del servicio", "nativeScanFailed": "La búsqueda de aplicaciones no ha podido terminar. Lo detectado antes puede estar desactualizado; vuelve a intentarlo.", + "nativeScanStopped": "Este contenedor está detenido. La búsqueda de aplicaciones solo está disponible con el contenedor en marcha.", "nativeScanPartial": "La búsqueda de aplicaciones está incompleta. Revisa lo que se ha encontrado y vuelve a intentarlo.", "nativeVersionUnknown": "Versión instalada desconocida", "nativeEvidenceStale": "Lo detectado puede estar desactualizado o incompleto.", diff --git a/AppImage/messages/fr/common.json b/AppImage/messages/fr/common.json index c42e18fb..de2f0de3 100644 --- a/AppImage/messages/fr/common.json +++ b/AppImage/messages/fr/common.json @@ -1744,6 +1744,7 @@ "nativeEvidenceDirectory": "Répertoire de travail", "nativeEvidenceService": "Définition du service", "nativeScanFailed": "La recherche d'applications n'a pas pu se terminer. Les résultats précédents peuvent être obsolètes ; réessayez.", + "nativeScanStopped": "Ce conteneur est arrêté. La recherche d'applications n'est disponible que lorsque le conteneur est en cours d'exécution.", "nativeScanPartial": "La recherche d'applications est incomplète. Vérifiez les éléments trouvés et réessayez.", "nativeVersionUnknown": "Version installée inconnue", "nativeEvidenceStale": "Les éléments trouvés peuvent être obsolètes ou incomplets.", diff --git a/AppImage/messages/it/common.json b/AppImage/messages/it/common.json index 88fb45a7..7c13401b 100644 --- a/AppImage/messages/it/common.json +++ b/AppImage/messages/it/common.json @@ -1744,6 +1744,7 @@ "nativeEvidenceDirectory": "Directory di lavoro", "nativeEvidenceService": "Definizione del servizio", "nativeScanFailed": "La ricerca delle applicazioni non è stata completata. I risultati precedenti potrebbero non essere aggiornati; riprova.", + "nativeScanStopped": "Questo contenitore è arrestato. La ricerca delle applicazioni è disponibile solo con il contenitore in esecuzione.", "nativeScanPartial": "La ricerca delle applicazioni è incompleta. Controlla gli elementi trovati e riprova.", "nativeVersionUnknown": "Versione installata sconosciuta", "nativeEvidenceStale": "Gli elementi trovati potrebbero non essere aggiornati o essere incompleti.", diff --git a/AppImage/messages/pt/common.json b/AppImage/messages/pt/common.json index 2af0c313..6eef895c 100644 --- a/AppImage/messages/pt/common.json +++ b/AppImage/messages/pt/common.json @@ -1744,6 +1744,7 @@ "nativeEvidenceDirectory": "Diretório de trabalho", "nativeEvidenceService": "Definição do serviço", "nativeScanFailed": "A pesquisa de aplicativos não pôde ser concluída. Os resultados anteriores podem estar desatualizados; tente novamente.", + "nativeScanStopped": "Este contentor está parado. A pesquisa de aplicações só está disponível com o contentor em execução.", "nativeScanPartial": "A pesquisa de aplicativos está incompleta. Revise os itens encontrados e tente novamente.", "nativeVersionUnknown": "Versão instalada desconhecida", "nativeEvidenceStale": "Os itens encontrados podem estar desatualizados ou incompletos.", diff --git a/AppImage/messages/sv/common.json b/AppImage/messages/sv/common.json index 9d1c2f62..f622bbb9 100644 --- a/AppImage/messages/sv/common.json +++ b/AppImage/messages/sv/common.json @@ -1744,6 +1744,7 @@ "nativeEvidenceDirectory": "Arbetskatalog", "nativeEvidenceService": "Tjänstdefinition", "nativeScanFailed": "Programsökningen kunde inte slutföras. Tidigare resultat kan vara inaktuella; försök igen.", + "nativeScanStopped": "Den här containern är stoppad. Programsökningen är bara tillgänglig medan containern körs.", "nativeScanPartial": "Programsökningen är ofullständig. Granska det som hittades och försök igen.", "nativeVersionUnknown": "Installerad version okänd", "nativeEvidenceStale": "Det som hittades kan vara inaktuellt eller ofullständigt.",