diff --git a/AppImage/scripts/native_app_discovery.py b/AppImage/scripts/native_app_discovery.py index a807ead1..5ade84bd 100644 --- a/AppImage/scripts/native_app_discovery.py +++ b/AppImage/scripts/native_app_discovery.py @@ -460,11 +460,21 @@ def candidate(vmid, record, footprint): {'kind': 'working_directory', 'value': record.get('process_root', record['root'])}]) ports = sorted(set(record.get('listening_ports') or [])) + # Hestia's control panel is a narrow, verified exception to generic port + # inference: its own executable serves the panel on fixed HTTPS port 8083. + # Keep this declaration on the exact runtime signature so an unrelated + # application merely listening on 8083 never receives an HTTPS guess. + hestia_panel = (record['name'] == 'Hestia' + and record['path'] == _HESTIA_NGINX + and record['root'] == _HESTIA_ROOT + and _HESTIA_PANEL_PORT in ports) evidence.extend({'kind': 'tcp_listener', 'value': str(port)} for port in ports) return {'slug': identity, 'discovery_id': identity, 'source': 'native_metadata', 'name': record['name'], 'installed_version': version, 'evidence': evidence, - 'installation_path': record['path'], 'default_ports': [], + 'installation_path': record['path'], + 'default_ports': [_HESTIA_PANEL_PORT] if hestia_panel else [], 'listening_ports': ports, + **({'scheme': 'https'} if hestia_panel else {}), 'metadata_issue': metadata_issue, 'tracking_suggestion': ({'installed_via': 'manual', 'installed_version': version, 'detected_version': version, 'detector_verified': True} diff --git a/AppImage/scripts/tests/test_native_app_discovery.py b/AppImage/scripts/tests/test_native_app_discovery.py index f47c33c9..d1432b65 100644 --- a/AppImage/scripts/tests/test_native_app_discovery.py +++ b/AppImage/scripts/tests/test_native_app_discovery.py @@ -153,9 +153,18 @@ class DiscoveryFixtures(unittest.TestCase): self.assertEqual(item['name'], 'Hestia') self.assertEqual(item['installation_path'], hestia) self.assertEqual(item['listening_ports'], [8083]) + self.assertEqual(item['default_ports'], [8083]) + self.assertEqual(item['scheme'], 'https') self.assertIn({'kind': 'working_directory', 'value': '/'}, item['evidence']) self.assertIn('metadata_limit', result['issues']) + unrelated = native.candidate(101, { + 'name': 'other-panel', 'path': '/opt/other-panel/server', 'root': '/opt/other-panel', + 'process': '/opt/other-panel/server', 'listening_ports': [8083], + }, 'FILE\n') + self.assertEqual(unrelated['default_ports'], []) + self.assertNotIn('scheme', unrelated) + self.calls.clear() result = native.scan(101, self.runner( services='END\n', processes='LIMIT\n', ports='END\n')) diff --git a/tests/lxc_updates/test_native_app_discovery_ui.cjs b/tests/lxc_updates/test_native_app_discovery_ui.cjs index 3bb55dc4..06a77f2e 100644 --- a/tests/lxc_updates/test_native_app_discovery_ui.cjs +++ b/tests/lxc_updates/test_native_app_discovery_ui.cjs @@ -23,7 +23,7 @@ const candidate = { evidence: [{kind: 'installation_file', value: '/opt/handmade/server'}], default_ports: [], } -function fixture() { +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} @@ -49,7 +49,7 @@ function fixture() { if (url.endsWith('/native-discovery')) { if (options.method === 'POST') { if (scanGate) await scanGate - native = {...native, status: scanStatus, candidates: [candidate]} + native = {...native, status: scanStatus, candidates: [nativeCandidate]} } return structuredClone(native) } @@ -123,6 +123,16 @@ async function main() { assert.equal(payload.repo, '') assert.equal(payload.upstream_type, '') + const hestia = fixture({nativeCandidate: { + ...candidate, name: 'Hestia', default_ports: [8083], scheme: 'https', + }}) + hestia.render(); await hestia.settle() + await hestia.button('searchApplications').props.onClick(); hestia.render() + await hestia.button('registerButton').props.onClick(); hestia.render() + await hestia.button('saveButton').props.onClick(); hestia.render() + 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: ''}], + 'a verified Hestia panel pre-fills only its fixed HTTPS endpoint') const partial = fixture() partial.render(); await partial.settle(); partial.setScanStatus('partial') await partial.button('searchApplications').props.onClick(); partial.render()