diff --git a/.github/skills/code-standards/SKILL.md b/.github/skills/code-standards/SKILL.md index c81db6414..c24d0ca22 100644 --- a/.github/skills/code-standards/SKILL.md +++ b/.github/skills/code-standards/SKILL.md @@ -98,6 +98,15 @@ from db_test_helpers import make_db, DummyDB, insert_device, minutes_ago If a helper you need doesn't exist yet, add it to `db_test_helpers.py` — not locally in the test file. +## Stubbing Modules in Standalone-Capable Tests + +If a test stubs NetAlertX modules into `sys.modules` so a script can be imported +outside the container (see `test/plugins/test_ntfy_custom_headers.py`), pop each +stubbed name back out of `sys.modules` right after the one-time import that needed +it. Otherwise the fake module leaks into every other test file collected in the +same pytest session and shadows the real module (see `testing-workflow` skill for +the full pattern and reproduction steps). + ## MAC Literals in Tests — ALWAYS Lowercase **MANDATORY:** Every MAC address literal used in test fixtures, parametrize decorators, assertions, or comments must be lowercase hex: diff --git a/.github/skills/testing-workflow/SKILL.md b/.github/skills/testing-workflow/SKILL.md index e369021dd..bb2e942e0 100644 --- a/.github/skills/testing-workflow/SKILL.md +++ b/.github/skills/testing-workflow/SKILL.md @@ -59,3 +59,49 @@ docker buildx build -t netalertx-test . ``` This takes ~30 seconds unless venv stage changes (~90s). + +## Pitfall: `sys.modules` Stubbing Leaks Across Test Files + +Some plugin tests (e.g. `test/plugins/test_ntfy_custom_headers.py`) stub NetAlertX +modules (`conf`, `helper`, `models.notification_instance`, etc.) via +`sys.modules[name] = fake_module` so the plugin script can be imported standalone, +outside the container. Because `sys.modules` is a single process-wide cache shared +by the whole pytest session, a fake module inserted by one test file silently +shadows the real module for every other test file collected afterwards — pytest +imports all test files during collection, before any test runs, so this can happen +regardless of alphabetical/directory order. + +Symptom: `AttributeError: does not have +the attribute 'get_setting_value'` (or similar) in an unrelated test file, where +the module repr has no `from ''` suffix — a giveaway that a stub, not the +real module, was resolved. + +Fix pattern: track which module names your stub actually inserted, and pop them +back out of `sys.modules` immediately after the one-time import that needed them +(the already-imported script keeps its bound names regardless): + +```python +_stubbed_module_names = [] + +def _stub(name, **attrs): + if name not in sys.modules: + mod = types.ModuleType(name) + for k, v in attrs.items(): + setattr(mod, k, v) + sys.modules[name] = mod + _stubbed_module_names.append(name) + +# ... _stub(...) calls, then the one-time import ... +import ntfy + +for _name in _stubbed_module_names: + sys.modules.pop(_name, None) +``` + +Reproduce cross-file pollution locally by running the suspect file together with +the affected one in a single pytest invocation (order matters less than you'd +think — collection happens for all files first): + +```bash +pytest test/plugins/test_ntfy_custom_headers.py test/backend/test_notification_templates.py -v +``` diff --git a/README.md b/README.md index 7546966f5..6fb9140d5 100755 --- a/README.md +++ b/README.md @@ -27,7 +27,6 @@ NetAlertX gives you a real-time source of truth for connected devices, helps ide Use NetAlertX to spot shadow IT, unauthorized hardware, IPAM drift, and other changes that matter to service teams. With multi-site sync, reporting, workflows, and webhooks, it helps MSPs stay ahead of problems without the overhead of a full NMS or SIEM. - ## Table of Contents - [Quick Start](#quick-start) diff --git a/front/deviceDetailsEdit.php b/front/deviceDetailsEdit.php index 684ed8396..61ef88fe0 100755 --- a/front/deviceDetailsEdit.php +++ b/front/deviceDetailsEdit.php @@ -110,7 +110,7 @@ function getDeviceData() { // columns to hide hiddenFields = ["NEWDEV_devScan", "NEWDEV_devPresentLastScan"] // columns to disable/readonly - conditional depending if a new dummy device is created - disabledFields = mac == "new" ? ["NEWDEV_devLastNotification", "NEWDEV_devFirstConnection", "NEWDEV_devLastConnection"] : ["NEWDEV_devLastNotification", "NEWDEV_devFirstConnection", "NEWDEV_devLastConnection", "NEWDEV_devMac", "NEWDEV_devLastIP", "NEWDEV_devPrimaryIPv6", "NEWDEV_devPrimaryIPv4", "NEWDEV_devSyncHubNode", "NEWDEV_devFQDN"]; + disabledFields = mac == "new" ? ["NEWDEV_devLastNotification", "NEWDEV_devFirstConnection", "NEWDEV_devLastConnection", "NEWDEV_devFQDN", "NEWDEV_devPrimaryIPv4", "NEWDEV_devPrimaryIPv6", "NEWDEV_devSyncHubNode"] : ["NEWDEV_devLastNotification", "NEWDEV_devFirstConnection", "NEWDEV_devLastConnection", "NEWDEV_devMac", "NEWDEV_devLastIP", "NEWDEV_devPrimaryIPv6", "NEWDEV_devPrimaryIPv4", "NEWDEV_devSyncHubNode", "NEWDEV_devFQDN"]; // Fields that are tracked by authoritative handler and can be locked/unlocked const trackedFields = { diff --git a/front/js/network-tabs.js b/front/js/network-tabs.js index 708af6839..c7e6db026 100644 --- a/front/js/network-tabs.js +++ b/front/js/network-tabs.js @@ -13,7 +13,7 @@ function renderNetworkTabs(nodes) { (node.devAlertDown == 1 ? "text-red" : "text-gray50")); const portLabel = node.node_ports_count ? ` (${node.node_ports_count})` : ''; - const icon = atob(node.devIcon); + const icon = safeAtob(node.devIcon); const id = node.devMac.replace(/:/g, '_'); html += ` diff --git a/front/js/scan_control.js b/front/js/scan_control.js new file mode 100644 index 000000000..beee93303 --- /dev/null +++ b/front/js/scan_control.js @@ -0,0 +1,46 @@ +//-------------------------------------------------------------- +// Pause / Resume automatic scans button +// Default pause duration (minutes) used for the single-click header button +function renderPauseResumeButton(pauseUntil) { + const icon = document.getElementById('pause-resume-icon'); + const link = document.getElementById('pause-resume-button'); + if (!icon || !link) return; + + const isPaused = !!pauseUntil; + icon.className = isPaused ? 'fa-solid fa-play' : 'fa-solid fa-pause'; + link.title = isPaused + ? getString('Header_ResumeScans_Tooltip') + : getString('Header_PauseScans_Tooltip'); +} + +// Updated whenever the SSE state manager receives a state_update event (see sse_manager.js) +document.addEventListener('nax:pauseStateUpdate', (e) => { + renderPauseResumeButton(e.detail.pauseUntil); +}); + +function togglePauseScans() { + const PAUSE_SCANS_DEFAULT_MINUTES = getSetting("UI_SCAN_PAUSE"); + const icon = document.getElementById('pause-resume-icon'); + const isPaused = icon && icon.classList.contains('fa-play'); + const apiBase = getApiBase(); + const apiToken = getSetting("API_TOKEN"); + const endpoint = isPaused ? '/scan/resume' : '/scan/pause'; + const success_msg = isPaused ? getString("Scans_Resumed") : getString("Scans_Paused"); + const payload = isPaused ? {} : { minutes: PAUSE_SCANS_DEFAULT_MINUTES }; + + $.ajax({ + url: `${apiBase}${endpoint}`, + method: "POST", + contentType: "application/json", + headers: { "Authorization": `Bearer ${apiToken}` }, + data: JSON.stringify(payload), + error: function(xhr, status, error) { + console.error("[Header] Error toggling scan pause:", status, error); + showMessage(error, 5000, "modal_red"); + }, + success:function() { + showMessage(success_msg); + }, + }); +} + diff --git a/front/js/sse_manager.js b/front/js/sse_manager.js index ae9c69039..c8536a2a5 100644 --- a/front/js/sse_manager.js +++ b/front/js/sse_manager.js @@ -186,6 +186,13 @@ class NetAlertXStateManager { })); } + // 6. Dispatch pause state update for the header Pause/Resume button + if (appState["pause_until"] !== undefined) { + document.dispatchEvent(new CustomEvent('nax:pauseStateUpdate', { + detail: { pauseUntil: appState["pause_until"] } + })); + } + // console.log("[NetAlertX State] UI updated via jQuery"); } catch (e) { console.error("[NetAlertX State] Failed to update state display:", e); diff --git a/front/js/ui_components.js b/front/js/ui_components.js index 1614d4d6a..4fcf96fe5 100755 --- a/front/js/ui_components.js +++ b/front/js/ui_components.js @@ -971,6 +971,9 @@ function renderDeviceLink(data, container, useName = false) { // Build and return badge parts const badge = badgeFromDevice(device); + // Decode once (with a safe fallback) and reuse for both the chip and hover preview + const decodedIcon = safeAtob(device.devIcon); + // badge class and hover-info class to container $(container) .addClass(`${badge.cssClass} hover-node-info`) @@ -989,14 +992,14 @@ function renderDeviceLink(data, container, useName = false) { 'data-alertdown': device.devAlertDown, 'data-sleeping': device.devIsSleeping || 0, 'data-archived': device.devIsArchived || 0, - 'data-isnew': device.devIsNew || 0, - 'data-icon': device.devIcon + 'data-isnew': device.devIsNew || 0, + 'data-icon': decodedIcon }); return ` - ${atob(device.devIcon)} + ${decodedIcon} ${useName ? encodeSpecialChars(device.devName) : data.text} (${badge.iconHtml}) @@ -1006,6 +1009,17 @@ function renderDeviceLink(data, container, useName = false) { `; } +// ------------------------------------------ +// Base64-decode a devIcon value, tolerating missing/empty/malformed input +function safeAtob(value) { + if (!value) return ''; + try { + return atob(value); + } catch (e) { + return ''; + } +} + // ------------------------------------------ // Display device info on hover (attach only once) function initHoverNodeInfo() { @@ -1063,7 +1077,7 @@ function initHoverNodeInfo() { const html = `
-
${atob(icon)}
${encodeSpecialChars(name)}
+
${icon || ''}
${encodeSpecialChars(name)}

diff --git a/front/php/templates/header.php b/front/php/templates/header.php index 0337c08f7..c606c24c2 100755 --- a/front/php/templates/header.php +++ b/front/php/templates/header.php @@ -54,6 +54,7 @@ + @@ -208,11 +209,17 @@ function update_servertime() {
  • + +
  • + + + +
  • 0 -
  • +
  • @@ -482,16 +489,14 @@ function update_servertime() { function toggleFullscreen() { - if (document.fullscreenElement) { - document.exitFullscreen(); + if (document.fullscreenElement) { + document.exitFullscreen(); + } + else { + document.documentElement.requestFullscreen(); } - else { - document.documentElement.requestFullscreen(); - } } - //-------------------------------------------------------------- - // Update server time in the header update_servertime() diff --git a/front/php/templates/language/ar_ar.json b/front/php/templates/language/ar_ar.json index de0545632..664f530c6 100644 --- a/front/php/templates/language/ar_ar.json +++ b/front/php/templates/language/ar_ar.json @@ -387,6 +387,8 @@ "HRS_TO_KEEP_NEWDEV_name": "مدة الاحتفاظ بالأجهزة الجديدة", "HRS_TO_KEEP_OFFDEV_description": "عدد الساعات للاحتفاظ بالأجهزة غير المتصلة", "HRS_TO_KEEP_OFFDEV_name": "مدة الاحتفاظ بالأجهزة غير المتصلة", + "Header_PauseScans_Tooltip": "", + "Header_ResumeScans_Tooltip": "", "LOADED_PLUGINS_description": "المكونات الإضافية المحملة", "LOADED_PLUGINS_name": "المكونات الإضافية المحملة", "LOG_LEVEL_description": "مستوى السجلات", @@ -645,6 +647,8 @@ "SCAN_SUBNETS_description": "الشبكات الفرعية للفحص", "SCAN_SUBNETS_name": "شبكات الفحص", "SYSTEM_TITLE": "عنوان النظام", + "Scans_Paused": "", + "Scans_Resumed": "", "Setting_Override": "تجاوز الإعدادات", "Setting_Override_Description": "وصف تجاوز الإعدادات", "Settings_Metadata_Toggle": "إظهار/إخفاء البيانات الوصفية للإعداد المحدد.", diff --git a/front/php/templates/language/ca_ca.json b/front/php/templates/language/ca_ca.json index d1996b0b6..4cbd14ab3 100644 --- a/front/php/templates/language/ca_ca.json +++ b/front/php/templates/language/ca_ca.json @@ -387,6 +387,8 @@ "HRS_TO_KEEP_NEWDEV_name": "Eliminar nous dispositius després de", "HRS_TO_KEEP_OFFDEV_description": "Això és un paràmetre de manteniment ELIMINANT dispositius. Si s'activa (0 està desactivat), els dispositius que estan Offline i el seu temps Last Offline es més vell que les hores especificades en aquest paràmetre, s'esborraran. Faci servir aquest paràmetre si vol auto-eliminar Dispositius Offline després de X hores sense connexió.", "HRS_TO_KEEP_OFFDEV_name": "Eliminar dispositius fora de línia després", + "Header_PauseScans_Tooltip": "", + "Header_ResumeScans_Tooltip": "", "LOADED_PLUGINS_description": "Quins Plugins carregar. Afegir plugins podria alentir l'aplicació. Llegir més sobre quins connectors necessiten estar habilitats, els tipus, o les opcions d'escaneig dins del documents de connectors. Els connectors descarregats perdran els vostres paràmetres. Només desactivats es poden eliminar els connectors.", "LOADED_PLUGINS_name": "Connectors carregats", "LOG_LEVEL_description": "Aquest paràmetre permetrà un registre més detallat. Útil per a la depuració d'esdeveniments d'escriptura a la base de dades.", @@ -645,6 +647,8 @@ "SCAN_SUBNETS_description": "La majoria dels escàners en xarxa (ARP-SCAN, NMAP, NSLOOKUP, DIG) es basen en l'exploració d'interfícies de xarxa específiques i subxarxes. Comproveu la documentació de subxarxes per ajudar en aquesta configuració, especialment VLANs, i quines VLANs són compatibles, o com esbrinar la màscara de xarxa i la seva interfície.

    Una alternativa als escàners en xarxa és activar alguns altres escàners / importadors de dispositius que no requereixin NetAlertX per tenir accés a la xarxa (UNIFI, dhcp. leases, PiHole, etc.).

    Nota: El temps d'exploració en si mateix depèn del nombre d'adreces IP per verificar, així que s'ha establir amb cura amb la màscara i la interfície de xarxa adequats.", "SCAN_SUBNETS_name": "Xarxes per escanejar", "SYSTEM_TITLE": "Informació de sistema", + "Scans_Paused": "", + "Scans_Resumed": "", "Setting_Override": "Valor de sobreescriptura", "Setting_Override_Description": "Activant aquesta opció anul·larà un valor predeterminat de l'aplicació amb el valor especificat.", "Settings_Metadata_Toggle": "Mostrar/amagar metadades per a la configuració donada.", diff --git a/front/php/templates/language/cs_cz.json b/front/php/templates/language/cs_cz.json index 3344c396f..f9d7afd6d 100644 --- a/front/php/templates/language/cs_cz.json +++ b/front/php/templates/language/cs_cz.json @@ -387,6 +387,8 @@ "HRS_TO_KEEP_NEWDEV_name": "Odstranit nová zařízení po", "HRS_TO_KEEP_OFFDEV_description": "Toto je nastavení údržby ODSTRANĚNÍ zařízení. Pokud je povoleno (0 zakázáno), zařízení Offline a data jejich Posledního připojení starší, než uvedené hodiny v tomto nastavení, budou odstraněna. Toto nastavení použijte, pokud chcete automaticky mazat Offline zařízení po uplynutí X hodin offline.", "HRS_TO_KEEP_OFFDEV_name": "Odstranit offline zařízení po", + "Header_PauseScans_Tooltip": "", + "Header_ResumeScans_Tooltip": "", "LOADED_PLUGINS_description": "Které zásuvné moduly načíst. Přidávání modulů může aplikaci zpomalit. Přečtěte si více o tom, které, které je třeba, aby byly povolené, o jejich typech nebo o předvolbách skenování v dokumentaci k zásuvným modulům. Odpojené moduly ztratí vaše nastavení. Odpojit je možné pouze deaktivované moduly.", "LOADED_PLUGINS_name": "Načtené moduly", "LOG_LEVEL_description": "Toto nastavení zapne podrobnější zaznamenávání událostí. To je užitečné pro ladění událostí zapisujících do databáze.", @@ -645,6 +647,8 @@ "SCAN_SUBNETS_description": "Většina skenerů sítí (ARP-SCAN, NMAP, NSLOOKUP, DIG) spoléhá na skenování konkrétních síťových rozhraní a podsítí. Podívejte se do dokumentace k podsítím ohledně pokynů k tomuto uspořádání, zejména VLAN sítím, ohledně toho, které VLAN sítě jsou podporovány nebo jak nastavit masku sítě na svém rozhraní.

    Alternativou ke skenerům na sítích je zapnout nějaké jiné skenery/importéry rozhraní, které nezávisí na tom, aby NetAlertX mělo přístup k síti (UNIFI, dhcp.leases, PiHole, atd.).

    Pozn.: Doba skenování jako taková závisí na počtu IP adres, které zkontrolovat, takže toto nastavte pečlivě s příslušnou maskou sítě a rozhraním.", "SCAN_SUBNETS_name": "Sítě ke skenování", "SYSTEM_TITLE": "Informace o systému", + "Scans_Paused": "", + "Scans_Resumed": "", "Setting_Override": "Přebít hodnotu", "Setting_Override_Description": "Zapnutí této předvolby přebije výchozí hodnotu z aplikace hodnotou, uvedenou výše.", "Settings_Metadata_Toggle": "Zobrazit/skrýt metadata pro dané nastavení.", diff --git a/front/php/templates/language/de_de.json b/front/php/templates/language/de_de.json index f5816b8a0..d8c756616 100644 --- a/front/php/templates/language/de_de.json +++ b/front/php/templates/language/de_de.json @@ -391,6 +391,8 @@ "HRS_TO_KEEP_NEWDEV_name": "Neue Geräte löschen nach", "HRS_TO_KEEP_OFFDEV_description": "", "HRS_TO_KEEP_OFFDEV_name": "Offline-Geräte löschen nach", + "Header_PauseScans_Tooltip": "", + "Header_ResumeScans_Tooltip": "", "LOADED_PLUGINS_description": "", "LOADED_PLUGINS_name": "Geladene Plugins", "LOG_LEVEL_description": "Diese Einstellung aktiviert die erweiterte Protokollierung. Nützlich fürs Debuggen von in die Datenbank geschriebenen Events.", @@ -706,6 +708,8 @@ "SMTP_USER_description": "The user name used to login into the SMTP server (sometimes a full email address).", "SMTP_USER_name": "SMTP user", "SYSTEM_TITLE": "Systeminformationen", + "Scans_Paused": "", + "Scans_Resumed": "", "Setting_Override": "Wert überschreiben", "Setting_Override_Description": "", "Settings_Metadata_Toggle": "Metadaten für die angegebene Einstellung anzeigen/ausblenden.", diff --git a/front/php/templates/language/en_us.json b/front/php/templates/language/en_us.json index 097a96a98..26fa2ddec 100755 --- a/front/php/templates/language/en_us.json +++ b/front/php/templates/language/en_us.json @@ -387,6 +387,8 @@ "HRS_TO_KEEP_NEWDEV_name": "Delete new devices after", "HRS_TO_KEEP_OFFDEV_description": "This is a maintenance setting DELETING devices. If enabled (0 is disabled), devices that are Offline and their Last Connection date time is older than the specified hours in this setting, will be deleted. Use this setting if you want to auto-delete Offline devices after X hours being offline.", "HRS_TO_KEEP_OFFDEV_name": "Delete offline devices after", + "Header_PauseScans_Tooltip": "Pause automatic scans", + "Header_ResumeScans_Tooltip": "Resume automatic scans", "LOADED_PLUGINS_description": "Which Plugins to load. Adding plugins might slow the application. Read more about which plugins need to be enabled, types, or scanning options in the plugins docs. Unloaded plugins will lose your settings. Only disabled plugins can be unloaded.", "LOADED_PLUGINS_name": "Loaded plugins", "LOG_LEVEL_description": "This setting will enable more verbose logging. Useful for debugging events writing into the database.", @@ -645,6 +647,8 @@ "SCAN_SUBNETS_description": "Most on-network scanners (ARP-SCAN, NMAP, NSLOOKUP, DIG) rely on scanning specific network interfaces and subnets. Check the subnets documentation for help on this setting, especially VLANs, what VLANs are supported, or how to figure out the network mask and your interface.

    An alternative to on-network scanners is to enable some other device scanners/importers that don't rely on NetAlertX having access to the network (UNIFI, dhcp.leases, PiHole, etc.).

    Note: The scan time itself depends on the number of IP addresses to check, so set this up carefully with the appropriate network mask and interface.", "SCAN_SUBNETS_name": "Networks to scan", "SYSTEM_TITLE": "System Information", + "Scans_Paused": "Scans paused", + "Scans_Resumed": "Scans resumed", "Setting_Override": "Override value", "Setting_Override_Description": "Enabling this option will override an App supplied default value with the value specified above.", "Settings_Metadata_Toggle": "Show/hide metadata for the given setting.", diff --git a/front/php/templates/language/es_es.json b/front/php/templates/language/es_es.json index 317f2a3f8..33f9583a7 100644 --- a/front/php/templates/language/es_es.json +++ b/front/php/templates/language/es_es.json @@ -389,6 +389,8 @@ "HRS_TO_KEEP_NEWDEV_name": "Eliminar nuevos dispositivos después", "HRS_TO_KEEP_OFFDEV_description": "Esta es una configuración de mantenimiento BORRAR dispositivos. Si está activado (0 está desactivado), los dispositivos que están Sin Conexión y su fecha de Última Conexión es anterior a las horas especificadas en este ajuste se eliminarán. Use este ajuste si desea eliminar automáticamente los dispositivos sin conexión después de que el X horas esté sin conexión.", "HRS_TO_KEEP_OFFDEV_name": "Borrar dispositivos sin conexión después de", + "Header_PauseScans_Tooltip": "", + "Header_ResumeScans_Tooltip": "", "LOADED_PLUGINS_description": "¿Qué plugins cargar?. Agregar plugins puede ralentizar la aplicación. Obtén más información sobre los complementos que deben habilitarse, los tipos o las opciones de escaneo en los documentos de plugins. Los plugins descargados perderán tu configuración. Solo se pueden descargar los complementos deshabilitados.", "LOADED_PLUGINS_name": "Plugins cargados", "LOG_LEVEL_description": "Esto hará que el registro tenga más información. Util para depurar que eventos se van guardando en la base de datos.", @@ -704,6 +706,8 @@ "SMTP_USER_description": "El nombre de usuario utilizado para iniciar sesión en el servidor SMTP (a veces, una dirección de correo electrónico completa).", "SMTP_USER_name": "Nombre de usuario SMTP", "SYSTEM_TITLE": "Información del sistema", + "Scans_Paused": "", + "Scans_Resumed": "", "Setting_Override": "Sobreescribir el valor", "Setting_Override_Description": "Habilitar esta opción anulará un valor predeterminado proporcionado por la aplicación con el valor especificado anteriormente.", "Settings_Metadata_Toggle": "Mostrar/ocultar los metadatos de la configuración.", diff --git a/front/php/templates/language/fa_fa.json b/front/php/templates/language/fa_fa.json index 0835e9dad..53fb364b2 100644 --- a/front/php/templates/language/fa_fa.json +++ b/front/php/templates/language/fa_fa.json @@ -387,6 +387,8 @@ "HRS_TO_KEEP_NEWDEV_name": "", "HRS_TO_KEEP_OFFDEV_description": "", "HRS_TO_KEEP_OFFDEV_name": "", + "Header_PauseScans_Tooltip": "", + "Header_ResumeScans_Tooltip": "", "LOADED_PLUGINS_description": "", "LOADED_PLUGINS_name": "", "LOG_LEVEL_description": "", @@ -645,6 +647,8 @@ "SCAN_SUBNETS_description": "", "SCAN_SUBNETS_name": "", "SYSTEM_TITLE": "", + "Scans_Paused": "", + "Scans_Resumed": "", "Setting_Override": "", "Setting_Override_Description": "", "Settings_Metadata_Toggle": "", diff --git a/front/php/templates/language/fi_fi.json b/front/php/templates/language/fi_fi.json index 3e01e76d6..abbeaad76 100644 --- a/front/php/templates/language/fi_fi.json +++ b/front/php/templates/language/fi_fi.json @@ -387,6 +387,8 @@ "HRS_TO_KEEP_NEWDEV_name": "", "HRS_TO_KEEP_OFFDEV_description": "", "HRS_TO_KEEP_OFFDEV_name": "", + "Header_PauseScans_Tooltip": "", + "Header_ResumeScans_Tooltip": "", "LOADED_PLUGINS_description": "", "LOADED_PLUGINS_name": "", "LOG_LEVEL_description": "", @@ -645,6 +647,8 @@ "SCAN_SUBNETS_description": "", "SCAN_SUBNETS_name": "", "SYSTEM_TITLE": "", + "Scans_Paused": "", + "Scans_Resumed": "", "Setting_Override": "", "Setting_Override_Description": "", "Settings_Metadata_Toggle": "", diff --git a/front/php/templates/language/fr_fr.json b/front/php/templates/language/fr_fr.json index 300815994..41221ac23 100644 --- a/front/php/templates/language/fr_fr.json +++ b/front/php/templates/language/fr_fr.json @@ -387,6 +387,8 @@ "HRS_TO_KEEP_NEWDEV_name": "Supprimer les nouveaux appareils après", "HRS_TO_KEEP_OFFDEV_description": "Il s'agit d'un paramètre de maintenance SUPPRIMER des appareils. Si cette option est activée (0 est désactivé), les appareils qui sont Hors ligne et dont la dernière connexion est plus ancienne que les heures spécifiées dans ce paramètre. Utilisez ce paramètre si vous souhaitez supprimer automatiquement Appareils hors ligne après X heures de déconnexion.", "HRS_TO_KEEP_OFFDEV_name": "Supprimez les appareils hors ligne après", + "Header_PauseScans_Tooltip": "", + "Header_ResumeScans_Tooltip": "", "LOADED_PLUGINS_description": "Affiche les plugins chargés. Ajouter des plugins peut ralentir l'application. Obtenez plus d'informations dur quels plugins dont à activer, ou les options de scan dans la documentation des plugins. Décharger des plugins leur fait perdre leurs paramètres. Seuls les plugins désactivés peuvent être déchargés.", "LOADED_PLUGINS_name": "Plugins chargés", "LOG_LEVEL_description": "Ce paramètre active une journalisation dans les logs plus verbeuse. Cela est utile pour identifier les événements écrivant dans la base de données.", @@ -645,6 +647,8 @@ "SCAN_SUBNETS_description": "La plupart des scanners sur le réseau (scan ARP, NMAP, Nslookup, DIG) se base sur le scan d'une partie spécifique des interfaces réseau ou de sous-réseau. Consulter la documentation des sous-réseaux pour plus d'aide sur ce paramètre, notamment pour des VLAN, lesquels sont supportés ou sur comment identifier le masque réseau et votre interface réseau.

    Une alternative à ces scanner sur le réseau et d'activer d'autres scanners d'appareils ou des importe, qui ne dépendent pas du fait de laisser NetAlertX accéder au réseau (Unifié, baux DHCP, Pi-hole, etc.).

    Remarque : la durée du scan en lui-même dépend du nombre d'adresses IP à scanner, renseignez donc soigneusement avec le bon masque réseau et la bonne interface réseau.", "SCAN_SUBNETS_name": "Réseaux à scanner", "SYSTEM_TITLE": "Informations système", + "Scans_Paused": "", + "Scans_Resumed": "", "Setting_Override": "Remplacer la valeur", "Setting_Override_Description": "Activer cette option va remplacer la valeur fournie par défaut par une application par la valeur renseignée au-dessus.", "Settings_Metadata_Toggle": "Afficher/masquer les méta données pour le paramètre sélectionné.", diff --git a/front/php/templates/language/he_il.json b/front/php/templates/language/he_il.json index 3e01e76d6..abbeaad76 100644 --- a/front/php/templates/language/he_il.json +++ b/front/php/templates/language/he_il.json @@ -387,6 +387,8 @@ "HRS_TO_KEEP_NEWDEV_name": "", "HRS_TO_KEEP_OFFDEV_description": "", "HRS_TO_KEEP_OFFDEV_name": "", + "Header_PauseScans_Tooltip": "", + "Header_ResumeScans_Tooltip": "", "LOADED_PLUGINS_description": "", "LOADED_PLUGINS_name": "", "LOG_LEVEL_description": "", @@ -645,6 +647,8 @@ "SCAN_SUBNETS_description": "", "SCAN_SUBNETS_name": "", "SYSTEM_TITLE": "", + "Scans_Paused": "", + "Scans_Resumed": "", "Setting_Override": "", "Setting_Override_Description": "", "Settings_Metadata_Toggle": "", diff --git a/front/php/templates/language/id_id.json b/front/php/templates/language/id_id.json index 3e01e76d6..abbeaad76 100644 --- a/front/php/templates/language/id_id.json +++ b/front/php/templates/language/id_id.json @@ -387,6 +387,8 @@ "HRS_TO_KEEP_NEWDEV_name": "", "HRS_TO_KEEP_OFFDEV_description": "", "HRS_TO_KEEP_OFFDEV_name": "", + "Header_PauseScans_Tooltip": "", + "Header_ResumeScans_Tooltip": "", "LOADED_PLUGINS_description": "", "LOADED_PLUGINS_name": "", "LOG_LEVEL_description": "", @@ -645,6 +647,8 @@ "SCAN_SUBNETS_description": "", "SCAN_SUBNETS_name": "", "SYSTEM_TITLE": "", + "Scans_Paused": "", + "Scans_Resumed": "", "Setting_Override": "", "Setting_Override_Description": "", "Settings_Metadata_Toggle": "", diff --git a/front/php/templates/language/it_it.json b/front/php/templates/language/it_it.json index 70b20dbba..10fe3728a 100644 --- a/front/php/templates/language/it_it.json +++ b/front/php/templates/language/it_it.json @@ -387,6 +387,8 @@ "HRS_TO_KEEP_NEWDEV_name": "Elimina nuovi dispositivi dopo", "HRS_TO_KEEP_OFFDEV_description": "Questa è un'impostazione di manutenzione che ELIMINA dispositivi. Se abilitata (0 è disabilitata), i dispositivi Offline la cui data e ora di Ultima connessione sono antecedenti alle ore specificate in questa impostazione, verranno eliminati. Utilizza questa impostazione se vuoi eliminare automaticamente i Dispositivi offline dopo X ore trascorse offline.", "HRS_TO_KEEP_OFFDEV_name": "Elimina dispositivi offline dopo", + "Header_PauseScans_Tooltip": "", + "Header_ResumeScans_Tooltip": "", "LOADED_PLUGINS_description": "Quali Plugin caricare. L'aggiunta di plugin potrebbe rallentare l'applicazione. Leggi di più su quali plugin necessitano di essere abilitati, tipi e opzioni di scansione nella documentazione plugin. I plugin disinstallati perdono la loro configurazione. Solo i plugin disabilitati possono essere disinstallati.", "LOADED_PLUGINS_name": "Plugin caricati", "LOG_LEVEL_description": "Questa impostazione abilita un log più dettagliato. Utile per il debug degli eventi salvati nel database.", @@ -645,6 +647,8 @@ "SCAN_SUBNETS_description": "La maggior parte degli scanner di rete (ARP-SCAN, NMAP, NSLOOKUP, DIG) si basano sulla scansione di interfacce di rete e sottoreti specifiche. Consulta la documentazione sulle sottoreti per assistenza su questa impostazione, in particolare VLAN, quali VLAN sono supportate o come individuare la maschera di rete e l'interfaccia.

    Un'alternativa agli scanner in rete è abilitare altri scanner/importatori di dispositivi che non si affidano a NetAlertX che hanno accesso alla rete (UNIFI, dhcp.leases , PiHole, ecc.).

    Nota: il tempo di scansione stesso dipende dal numero di indirizzi IP da controllare, quindi impostalo attentamente con la maschera di rete e l'interfaccia appropriate.", "SCAN_SUBNETS_name": "Reti da scansionare", "SYSTEM_TITLE": "Informazioni sistema", + "Scans_Paused": "", + "Scans_Resumed": "", "Setting_Override": "Sovrascrivi valore", "Setting_Override_Description": "L'abilitazione di questa opzione sovrascriverà il valore predefinito fornito dall'app con il valore specificato sopra.", "Settings_Metadata_Toggle": "Mostra/nascondi i metadati per l'impostazione specificata.", diff --git a/front/php/templates/language/ja_jp.json b/front/php/templates/language/ja_jp.json index 033cd4cb6..c553a6c9c 100644 --- a/front/php/templates/language/ja_jp.json +++ b/front/php/templates/language/ja_jp.json @@ -387,6 +387,8 @@ "HRS_TO_KEEP_NEWDEV_name": "新規デバイスの削除", "HRS_TO_KEEP_OFFDEV_description": "これは デバイスを削除 するメンテナンス設定です。有効にした場合(0 で無効)、オフライン 状態のデバイスの内、最終接続日時 が指定された時間より古いものは削除されます。オフラインデバイスX 時間経過後に自動削除したい場合に使用してください。", "HRS_TO_KEEP_OFFDEV_name": "オフラインデバイスを削除する", + "Header_PauseScans_Tooltip": "", + "Header_ResumeScans_Tooltip": "", "LOADED_PLUGINS_description": "読み込まれたプラグイン。プラグインの追加はアプリケーションの速度を低下させる可能性があります。有効化が必要なプラグインの種類やスキャンオプションについては、プラグインのドキュメント を参照してください。読み込まれなかったプラグインの設定は失われます。読み込まない設定にできるのは 無効化 されたプラグインのみです。", "LOADED_PLUGINS_name": "読み込まれたプラグイン", "LOG_LEVEL_description": "この設定により、より詳細なログ出力が有効になります。データベースへのイベント書き込みのデバッグに有用です。", @@ -645,6 +647,8 @@ "SCAN_SUBNETS_description": "ほとんどのネットワーク内スキャナー(ARP-SCAN、NMAP、NSLOOKUP、DIG)は、特定のネットワークインターフェースとサブネットをスキャンすることに依存しています。この設定に関するヘルプについては、サブネットのドキュメント を確認してください。特にVLAN、サポートされているVLANの種類、ネットワークマスクとインターフェースの確認方法についてです。

    ネットワーク内スキャナーの代替手段として、NetAlertX がネットワークにアクセスする必要のない他のデバイススキャナー/インポーター(UNIFI、dhcp.leases、PiHoleなど)を有効化できます。

    注:スキャン時間自体は確認するIPアドレス数に依存するため、適切なネットワークマスクとインターフェースで慎重に設定してください。", "SCAN_SUBNETS_name": "スキャン対象ネットワーク", "SYSTEM_TITLE": "システム情報", + "Scans_Paused": "", + "Scans_Resumed": "", "Setting_Override": "上書き値", "Setting_Override_Description": "このオプションを有効にすると、アプリが提供するデフォルト値が上記で指定された値で上書きされます。", "Settings_Metadata_Toggle": "指定された設定のメタデータを表示/非表示にする。", diff --git a/front/php/templates/language/nb_no.json b/front/php/templates/language/nb_no.json index 6c4872ebe..a84f21a00 100644 --- a/front/php/templates/language/nb_no.json +++ b/front/php/templates/language/nb_no.json @@ -387,6 +387,8 @@ "HRS_TO_KEEP_NEWDEV_name": "Behold nye enheter for", "HRS_TO_KEEP_OFFDEV_description": "", "HRS_TO_KEEP_OFFDEV_name": "", + "Header_PauseScans_Tooltip": "", + "Header_ResumeScans_Tooltip": "", "LOADED_PLUGINS_description": "Hvilke plugins som skal lastes. Å legge til plugins kan gjøre programmet tregere. Les mer om hvilke plugins som må aktiveres, typer eller skannealternativer i plugin dokumentasjonen. Ulastede plugins vil miste innstillingene sine. Bare deaktiverte plugins kan lastes ut.", "LOADED_PLUGINS_name": "Lastede plugins", "LOG_LEVEL_description": "Denne innstillingen vil aktivere mer detaljert logging. Nyttig for feilsøking av hendelser som skrives inn i databasen.", @@ -645,6 +647,8 @@ "SCAN_SUBNETS_description": "De fleste skannere på nettet (ARP-Scan, NMAP, NSlookup, Dig) er avhengige av å skanne spesifikke nettverksgrensesnitt og undernett. Sjekk subnett dokumentasjonen for hjelp på denne innstillingen, spesielt VLAN-er, hvilke VLAN-er som støttes, eller hvordan du kan finne ut nettverksmasken og grensesnittet ditt.

    Et alternativ til skannere på nettet er å aktivere noen andre enhetsskannere/importører som ikke er avhengige av NetalertX med tilgang til nettverket (UniFi, DHCP-Leaser, Pihole, osv.).

    Merk: Selve skanningstiden avhenger av antall IP -adresser som skal sjekkes, så sett dette opp nøye med riktig nettverksmaske og grensesnitt.", "SCAN_SUBNETS_name": "", "SYSTEM_TITLE": "Systeminformasjon", + "Scans_Paused": "", + "Scans_Resumed": "", "Setting_Override": "Overstyr verdi", "Setting_Override_Description": "Aktivering av dette alternativet vil overstyre en App som leveres standard-verdi med verdien som er spesifisert ovenfor.", "Settings_Metadata_Toggle": "Vis/skjul metadata for den gitte innstillingen.", diff --git a/front/php/templates/language/pl_pl.json b/front/php/templates/language/pl_pl.json index 76bb705e5..647da4bf5 100644 --- a/front/php/templates/language/pl_pl.json +++ b/front/php/templates/language/pl_pl.json @@ -387,6 +387,8 @@ "HRS_TO_KEEP_NEWDEV_name": "Usuń nowe urządzenia po", "HRS_TO_KEEP_OFFDEV_description": "To ustawienie konserwacyjne dotyczące USUWANIA urządzeń. Jeśli jest włączone (0 oznacza wyłączone), urządzenia, które są Offline i których ostatnie połączenie miało miejsce wcześniej niż określona liczba godzin w tym ustawieniu, zostaną usunięte. Skorzystaj z tej opcji, jeśli chcesz automatycznie usuwać urządzenia offline po X godzinach braku aktywności.", "HRS_TO_KEEP_OFFDEV_name": "Usuń urządzenia niedostępne po", + "Header_PauseScans_Tooltip": "", + "Header_ResumeScans_Tooltip": "", "LOADED_PLUGINS_description": "Które wtyczki mają zostać załadowane. Dodanie wtyczek może spowolnić działanie aplikacji. Więcej informacji o tym, które wtyczki należy włączyć, jakie są ich typy oraz dostępne opcje skanowania znajdziesz w dokumentacji wtyczek. Wtyczki, które nie zostaną załadowane, utracą swoje ustawienia. Tylko wtyczki oznaczone jako disabled mogą zostać pominięte przy ładowaniu.", "LOADED_PLUGINS_name": "Załadowane wtyczki", "LOG_LEVEL_description": "To ustawienie włącza bardziej szczegółowe logowanie. Przydatne do debugowania zdarzeń zapisywanych w bazie danych.", @@ -645,6 +647,8 @@ "SCAN_SUBNETS_description": "Większość skanerów sieciowych (ARP-SCAN, NMAP, NSLOOKUP, DIG) polega na skanowaniu określonych interfejsów sieciowych i podsieci. Zapoznaj się z dokumentacją podsieci, aby uzyskać pomoc w konfiguracji tego ustawienia, szczególnie w kontekście VLAN-ów, jakie VLAN-y są obsługiwane, lub jak ustalić maskę sieciową i interfejs.

    Alternatywą dla skanerów sieciowych jest włączenie innych skanerów/importerów urządzeń, które nie wymagają, aby NetAlertX miał dostęp do sieci (np. UNIFI, dhcp.leases, PiHole itp.).

    Uwaga: Czas skanowania zależy od liczby adresów IP do sprawdzenia, dlatego skonfiguruj to ostrożnie, ustawiając odpowiednią maskę sieciową i interfejs.", "SCAN_SUBNETS_name": "Sieci do zeskanowania", "SYSTEM_TITLE": "Informacje o systemie", + "Scans_Paused": "", + "Scans_Resumed": "", "Setting_Override": "Nadpisz wartość", "Setting_Override_Description": "Włączenie tej opcji spowoduje nadpisanie domyślnej wartości dostarczonej przez aplikację wartością określoną powyżej.", "Settings_Metadata_Toggle": "Pokaż/ukryj metadane dla danego ustawienia.", diff --git a/front/php/templates/language/pt_br.json b/front/php/templates/language/pt_br.json index 1fd679e73..cd8da014a 100644 --- a/front/php/templates/language/pt_br.json +++ b/front/php/templates/language/pt_br.json @@ -387,6 +387,8 @@ "HRS_TO_KEEP_NEWDEV_name": "Manter novos dispositivos por", "HRS_TO_KEEP_OFFDEV_description": "Esta é uma configuração de manutenção EXCLUINDO dispositivos. Se habilitado (0 está desabilitado), dispositivos que estão Offline e sua data e hora Last Offline são mais antigas que as horas especificadas nesta configuração, serão deletados. Use esta configuração se você quiser remover automaticamente Dispositivos Offline após X horas offline.", "HRS_TO_KEEP_OFFDEV_name": "Eliminar dispositivos offline após", + "Header_PauseScans_Tooltip": "", + "Header_ResumeScans_Tooltip": "", "LOADED_PLUGINS_description": "Quais plugins carregar. Adicionar plugins pode deixar o aplicativo lento. Leia mais sobre quais plugins precisam ser habilitados, tipos ou opções de escaneamento na documentação de plugins. Plugins descarregados perderão as suas configurações. Somente plugins desabilitados podem ser descarregados.", "LOADED_PLUGINS_name": "Plugins carregados", "LOG_LEVEL_description": "Esta definição permite um registo mais detalhado. Útil para depurar eventos gravados na base de dados.", @@ -645,6 +647,8 @@ "SCAN_SUBNETS_description": "", "SCAN_SUBNETS_name": "", "SYSTEM_TITLE": "", + "Scans_Paused": "", + "Scans_Resumed": "", "Setting_Override": "", "Setting_Override_Description": "", "Settings_Metadata_Toggle": "", diff --git a/front/php/templates/language/pt_pt.json b/front/php/templates/language/pt_pt.json index 931bd9d1d..4ba807744 100644 --- a/front/php/templates/language/pt_pt.json +++ b/front/php/templates/language/pt_pt.json @@ -387,6 +387,8 @@ "HRS_TO_KEEP_NEWDEV_name": "Remover novos dispostivos depois", "HRS_TO_KEEP_OFFDEV_description": "Isto é uma definição de manutenção ELIMINAR dispositivos. Se ativado (0 é desativado), dispositivos que estão Offline e a sua data de Última conexão foi mais antigo que as horas especificadas nesta definição, será eliminado. Use esta definição se quer auto-eliminar Dispositivos Offline após X horas de estarem offline.", "HRS_TO_KEEP_OFFDEV_name": "Apagar dispositivos offline após", + "Header_PauseScans_Tooltip": "", + "Header_ResumeScans_Tooltip": "", "LOADED_PLUGINS_description": "Quais plugins carregar. Adicionar plugins pode deixar a aplicação lenta. Leia mais sobre quais plugins precisam ser ativados, tipos ou opções de escaneamento na documentação de plugins. Plugins descarregados perderão as suas configurações. Somente plugins desativados podem ser descarregados.", "LOADED_PLUGINS_name": "Plugins carregados", "LOG_LEVEL_description": "Esta definição permite um registo mais detalhado. Útil para depurar eventos gravados na base de dados.", @@ -645,6 +647,8 @@ "SCAN_SUBNETS_description": "A maior parte dos scanners on-network (ARP-SCAN, NMAP, NSLOOKUP, DIG) baseiam-se em scanear interfaces de rede específicas e subredes. Veja a documentação de subredes para ajudar com esta definição, especialmente VLANs, quais VLANs são suportadas, ou como descobrir a máscara de rede e a sua interface.

    Uma alternativa a scanners on-network é ativar outro scanner de dispositivos/importadores que não dependam do NetAlertX tenha acesso à rede (UNIFI, dhcp.leases, PiHole, etc.).

    Nota: O tempo de scaneamento em si depende do número de endereços de IP a verificar, por isso configure isto com cuidado com a máscara e interface de rede apropriadas.", "SCAN_SUBNETS_name": "Redes a scanear", "SYSTEM_TITLE": "Informação de Sistema", + "Scans_Paused": "", + "Scans_Resumed": "", "Setting_Override": "Sobrescrever valor", "Setting_Override_Description": "Ativar esta opção irá sobrescrever o valor predefinido pela App com o valor especificado acima.", "Settings_Metadata_Toggle": "Mostrar/esconder metadados para definição especificada.", diff --git a/front/php/templates/language/ru_ru.json b/front/php/templates/language/ru_ru.json index a4a488703..96da60e80 100644 --- a/front/php/templates/language/ru_ru.json +++ b/front/php/templates/language/ru_ru.json @@ -387,6 +387,8 @@ "HRS_TO_KEEP_NEWDEV_name": "Удалить новые устройства после", "HRS_TO_KEEP_OFFDEV_description": "Это настройка обслуживания УДАЛЕНИЕ устройств. Если этот параметр включен (0 отключен), устройства, которые находятся в Offline и их дата и время последнего подключения старше, чем часы, указанные в этом параметре. Используйте этот параметр, если вы хотите автоматически удалять Offline устройства после X часов отсутствия в сети.", "HRS_TO_KEEP_OFFDEV_name": "Удалить устройства Offline после", + "Header_PauseScans_Tooltip": "", + "Header_ResumeScans_Tooltip": "", "LOADED_PLUGINS_description": "Какие плагины загружать. Добавление плагинов может замедлить работу приложения. Подробнее о том, какие плагины необходимо включить, их типах или параметрах сканирования, читайте в Документация по плагинам. Выгруженные плагины потеряют ваши настройки. Можно выгрузить только отключенные плагины.", "LOADED_PLUGINS_name": "Загруженные плагины", "LOG_LEVEL_description": "Этот параметр включит более подробное ведение журнала. Полезно для отладки записи событий в базу данных.", @@ -645,6 +647,8 @@ "SCAN_SUBNETS_description": "Большинство сетевых сканеров (ARP-SCAN, NMAP, NSLOOKUP, DIG) полагаются на сканирование определенных сетевых интерфейсов и подсетей. Дополнительную информацию по этому параметру можно найти в документации по подсетям, особенно VLAN, какие VLAN поддерживаются или как разобраться в маске сети и своем интерфейсе.

    Альтернативой сетевым сканерам является включение некоторых других сканеров/импортеров устройств, которые не полагаются на NetAlertX, имеющий доступ к сети (UNIFI, dhcp.leases , PiHole и др.).

    Примечание. Само время сканирования зависит от количества проверяемых IP-адресов, поэтому тщательно настройте его, указав соответствующую маску сети и интерфейс.", "SCAN_SUBNETS_name": "Сети для сканирования", "SYSTEM_TITLE": "Системная информация", + "Scans_Paused": "", + "Scans_Resumed": "", "Setting_Override": "Переопределить значение", "Setting_Override_Description": "Включение этой опции приведет к переопределению значения по умолчанию, предоставленного приложением, на значение, указанное выше.", "Settings_Metadata_Toggle": "Показать/скрыть метаданные для данного параметра.", diff --git a/front/php/templates/language/sv_sv.json b/front/php/templates/language/sv_sv.json index 3e01e76d6..abbeaad76 100644 --- a/front/php/templates/language/sv_sv.json +++ b/front/php/templates/language/sv_sv.json @@ -387,6 +387,8 @@ "HRS_TO_KEEP_NEWDEV_name": "", "HRS_TO_KEEP_OFFDEV_description": "", "HRS_TO_KEEP_OFFDEV_name": "", + "Header_PauseScans_Tooltip": "", + "Header_ResumeScans_Tooltip": "", "LOADED_PLUGINS_description": "", "LOADED_PLUGINS_name": "", "LOG_LEVEL_description": "", @@ -645,6 +647,8 @@ "SCAN_SUBNETS_description": "", "SCAN_SUBNETS_name": "", "SYSTEM_TITLE": "", + "Scans_Paused": "", + "Scans_Resumed": "", "Setting_Override": "", "Setting_Override_Description": "", "Settings_Metadata_Toggle": "", diff --git a/front/php/templates/language/tr_tr.json b/front/php/templates/language/tr_tr.json index 15ccc2f35..af888909a 100644 --- a/front/php/templates/language/tr_tr.json +++ b/front/php/templates/language/tr_tr.json @@ -387,6 +387,8 @@ "HRS_TO_KEEP_NEWDEV_name": "Yeni Cihazları Silmeden Önce", "HRS_TO_KEEP_OFFDEV_description": "Bu bir bakım ayarıdır Cihazları SİLME. Etkinleştirildiğinde (0 devre dışıdır), Çevrimdışı olan ve Son Çevrimdışı tarihi belirtilen saatten daha eski olan cihazlar silinecektir. Bu ayarı, X saat çevrimdışı olduktan sonra Çevrimdışı Cihazları otomatik olarak silmek için kullanabilirsiniz.", "HRS_TO_KEEP_OFFDEV_name": "Çevrimdışı Cihazları Silmeden Önce", + "Header_PauseScans_Tooltip": "", + "Header_ResumeScans_Tooltip": "", "LOADED_PLUGINS_description": "Hangi Eklentilerin Yükleneceği. Eklenti eklemek, uygulamanın hızını yavaşlatabilir. Hangi eklentilerin etkinleştirilmesi gerektiği, türler veya tarama seçenekleri hakkında daha fazla bilgi için eklentiler belgelerini okuyun. Yüklenmeyen eklentiler, ayarlarınızı kaybedecektir. Sadece devre dışı bırakılmış eklentiler yüklenebilir.", "LOADED_PLUGINS_name": "Yüklenen Eklentiler", "LOG_LEVEL_description": "Bu ayar, daha ayrıntılı günlüklemeyi etkinleştirecektir. Veritabanına yazılan olayları hata ayıklamak için faydalıdır.", @@ -645,6 +647,8 @@ "SCAN_SUBNETS_description": "", "SCAN_SUBNETS_name": "", "SYSTEM_TITLE": "", + "Scans_Paused": "", + "Scans_Resumed": "", "Setting_Override": "", "Setting_Override_Description": "", "Settings_Metadata_Toggle": "", diff --git a/front/php/templates/language/uk_ua.json b/front/php/templates/language/uk_ua.json index bfd006715..ee5ea9901 100644 --- a/front/php/templates/language/uk_ua.json +++ b/front/php/templates/language/uk_ua.json @@ -387,6 +387,8 @@ "HRS_TO_KEEP_NEWDEV_name": "Видаліть нові пристрої після", "HRS_TO_KEEP_OFFDEV_description": "Це налаштування обслуговування ВИДАЛЕННЯ пристроїв. Якщо ввімкнено (0 вимкнено), пристрої, які офлайн, та їх Останнє підключення дата та час старіші за вказані години в цьому налаштуванні, будуть видалені. Використовуйте це налаштування, якщо ви хочете автоматично видаляти офлайн-пристрої після X годин перебування в мережі.", "HRS_TO_KEEP_OFFDEV_name": "Видаліть офлайн-пристрої після", + "Header_PauseScans_Tooltip": "", + "Header_ResumeScans_Tooltip": "", "LOADED_PLUGINS_description": "Які плагіни завантажити. Додавання плагінів може уповільнити роботу програми. Дізнайтеся більше про те, які плагіни потрібно ввімкнути, типи чи параметри сканування в документи плагінів. Вивантажені плагіни втратять налаштування. Лише вимкнені плагіни можна вивантажити.", "LOADED_PLUGINS_name": "Завантажені плагіни", "LOG_LEVEL_description": "Цей параметр увімкне докладніше журналювання. Корисно для налагодження запису подій у базу даних.", @@ -645,6 +647,8 @@ "SCAN_SUBNETS_description": "Більшість мережевих сканерів (ARP-SCAN, NMAP, NSLOOKUP, DIG) покладаються на сканування конкретних мережевих інтерфейсів і підмереж. Перегляньте документацію підмереж, щоб отримати допомогу щодо цього налаштування, особливо VLAN, які VLAN підтримуються або як визначити маску мережі та ваш інтерфейс.

    Альтернативою мережевим сканерам є ввімкнення деяких інших сканерів/імпортерів пристроїв, які не покладаються на доступ NetAlertX до мережі (UNIFI, dhcp.leases , PiHole тощо).

    Примітка. Сам час сканування залежить від кількості IP-адрес, які потрібно перевірити, тому ретельно налаштуйте це за допомогою відповідної маски мережі та інтерфейсу.", "SCAN_SUBNETS_name": "Мережі для сканування", "SYSTEM_TITLE": "Інформація Про систему", + "Scans_Paused": "", + "Scans_Resumed": "", "Setting_Override": "Перевизначати значення", "Setting_Override_Description": "Якщо ввімкнути цю опцію, значення за умовчанням, надане програмою, буде замінено значенням, указаним вище.", "Settings_Metadata_Toggle": "Показати/сховати метадані для вказаного параметра.", diff --git a/front/php/templates/language/vi_vn.json b/front/php/templates/language/vi_vn.json index 3e01e76d6..abbeaad76 100644 --- a/front/php/templates/language/vi_vn.json +++ b/front/php/templates/language/vi_vn.json @@ -387,6 +387,8 @@ "HRS_TO_KEEP_NEWDEV_name": "", "HRS_TO_KEEP_OFFDEV_description": "", "HRS_TO_KEEP_OFFDEV_name": "", + "Header_PauseScans_Tooltip": "", + "Header_ResumeScans_Tooltip": "", "LOADED_PLUGINS_description": "", "LOADED_PLUGINS_name": "", "LOG_LEVEL_description": "", @@ -645,6 +647,8 @@ "SCAN_SUBNETS_description": "", "SCAN_SUBNETS_name": "", "SYSTEM_TITLE": "", + "Scans_Paused": "", + "Scans_Resumed": "", "Setting_Override": "", "Setting_Override_Description": "", "Settings_Metadata_Toggle": "", diff --git a/front/php/templates/language/zh_cn.json b/front/php/templates/language/zh_cn.json index 0442ef059..a93417e07 100644 --- a/front/php/templates/language/zh_cn.json +++ b/front/php/templates/language/zh_cn.json @@ -387,6 +387,8 @@ "HRS_TO_KEEP_NEWDEV_name": "小时后删除新设备", "HRS_TO_KEEP_OFFDEV_description": "这是删除设备的维护设置。如果启用了这个设置(0是禁用),任何上次连接时间比设置里存的指定时间长的离线设备都会被删除。要是您想在X小时后自动删除离线设备,请用这个设置。", "HRS_TO_KEEP_OFFDEV_name": "保留离线设备", + "Header_PauseScans_Tooltip": "", + "Header_ResumeScans_Tooltip": "", "LOADED_PLUGINS_description": "加载哪些插件。添加插件可能会降低应用程序的速度。在插件文档中详细了解需要启用哪些插件、插件类型或扫描选项。卸载插件将丢失您的设置。只有已禁用的插件才能卸载。", "LOADED_PLUGINS_name": "已加载插件", "LOG_LEVEL_description": "此设置将启用更详细的日志记录。对于调试写入数据库的事件很有用。", @@ -645,6 +647,8 @@ "SCAN_SUBNETS_description": "大多数网络扫描器(ARP-SCAN、NMAP、NSLOOKUP、DIG)依赖于扫描特定的网络接口和子网。查看子网文档以获取有关此设置的帮助,尤其是 VLAN、支持哪些 VLAN,或者如何确定网络掩码和接口。

    网络扫描器的替代方法是启用一些其他不依赖于 NetAlertX 访问网络的设备扫描器/导入器(UNIFI、dhcp.leases、PiHole 等)。

    注意:扫描时间本身取决于要检查的 IP 地址数量,因此请使用适当的网络掩码和接口仔细设置。", "SCAN_SUBNETS_name": "待扫描网络", "SYSTEM_TITLE": "系统信息", + "Scans_Paused": "", + "Scans_Resumed": "", "Setting_Override": "覆盖值", "Setting_Override_Description": "启用此选项将用上面指定的值覆盖应用程序提供的默认值。", "Settings_Metadata_Toggle": "显示/隐藏给定设置的元数据。", diff --git a/server/__main__.py b/server/__main__.py index 4534f9112..9f367e268 100755 --- a/server/__main__.py +++ b/server/__main__.py @@ -18,6 +18,7 @@ import sys import time import datetime +import math from pathlib import Path # Register NetAlertX modules @@ -25,7 +26,7 @@ from const import fullConfPath, sql_new_devices from logger import mylog from helper import filePermissions -from utils.datetime_utils import timeNowUTC +from utils.datetime_utils import timeNowUTC, is_datetime_future, normalizeTimeStamp from app_state import updateState from api import update_api, check_activity, update_GUI_port from scan.session_events import process_scan @@ -97,6 +98,10 @@ def main(): all_plugins = None pm = None + # Tracks the last "remaining minutes" value broadcast while paused, so we only + # call updateState() when the displayed countdown minute actually changes. + last_paused_minute_broadcast = None + # -- SETTINGS BACKWARD COMPATIBILITY START -- # rename settings that have changed names due to code cleanup or migration to plugins renameSettings(Path(fullConfPath)) @@ -122,113 +127,130 @@ def main(): # Update API endpoints update_api(db, all_plugins, False) - # proceed if 1 minute passed - if conf.last_scan_run + datetime.timedelta(minutes=1) < conf.loop_start_time: - # last time any scan or maintenance/upkeep was run - conf.last_scan_run = loop_start_time - - # Header (also broadcasts last_scan_run to frontend via SSE / app_state.json) - updateState("Process: Start", - last_scan_run=loop_start_time.replace(microsecond=0).isoformat(), - next_scan_time="") - - # Timestamp - startTime = loop_start_time - startTime = startTime.replace(microsecond=0) - - # Check if any plugins need to run on schedule - pm.run_plugin_scripts("schedule") - - # Compute the next scheduled run time AFTER schedule check (which updates last_next_schedule) - # Only device_scanner plugins have meaningful next_scan times for user display - scanner_prefixes = {p["unique_prefix"] for p in all_plugins if p.get("plugin_type") == "device_scanner"} - scanner_next = [s.last_next_schedule for s in conf.mySchedules if s.service in scanner_prefixes] - - # Get the earliest next scan time across all device scanners and broadcast. - # updateState validates the value is in the future before storing/broadcasting. - if scanner_next: - next_scan_dt = min(scanner_next) - updateState(next_scan_time=next_scan_dt.replace(microsecond=0).isoformat()) - - # determine run/scan type based on passed time - # -------------------------------------------- - - # Runs plugin scripts which are set to run every time after a scans finished - pm.run_plugin_scripts("always_after_scan") - - # process all the scanned data into new devices - processScan = updateState("Check scan").processScan - mylog("debug", [f"[MAIN] processScan: {processScan}"]) - - if processScan is True: - mylog("debug", "[MAIN] start processing scan results") - process_scan(db) - updateState("Scan processed", None, None, None, None, False) - - # Name resolution - # -------------------------------------------- - - # Check if new devices found (created by process_scan) - sql.execute(sql_new_devices) - newDevices = sql.fetchall() - db.commitDB() - - # If new devices were found, run all plugins registered to be run when new devices are found - # Run these before name resolution so plugins like NSLOOKUP that are configured - # for `on_new_device` can populate names used in the notifications below. - if len(newDevices) > 0: - pm.run_plugin_scripts("on_new_device") - - # run plugins before notification processing (e.g. Plugins to discover device names) - pm.run_plugin_scripts("before_name_updates") - - # Resolve devices names (will pick up results from on_new_device plugins above) - mylog("debug", "[Main] Resolve devices names") - update_devices_names(pm) - - # Notification handling - # ---------------------------------------- - - # send all configured notifications - final_json = get_notifications(db) - - # Write the notifications into the DB - notification = NotificationInstance(db) - notificationObj = notification.create(final_json, "") - - # ------------------------------------------------------------------------------ - # Run all enabled publisher gateways (notification delivery) - # ------------------------------------------------------------------------------ - # Design notes: - # - The eve_PendingAlertEmail flag is only cleared *after* a notification is sent. - # - If no notification is sent (HasNotifications == False), the flag stays set, - # meaning the event may still trigger alerts later depending on user settings - # (e.g. down-event reporting, delay timers, plugin conditions). - # - A pending flag means “still under evaluation,” not “missed.” - # It will clear automatically once its event is included in a sent alert. - # ------------------------------------------------------------------------------ - if notificationObj.HasNotifications: - pm.run_plugin_scripts("on_notification") - notification.setAllProcessed() - - # Only clear pending email flags and plugins_events once notifications are sent. - notification.clearPendingEmailFlag() + # Pause gate: skip the automatic scheduled-scan block below while paused. + # Manually-triggered scans (handled by check_and_run_user_event() above) are unaffected. + pause_until_dt = normalizeTimeStamp(updateState().pause_until) - else: - # If there are no notifications to process, - # we still need to clear all plugin events to prevent database growth if - # no notification gateways are configured - notification.clearPluginEvents() - mylog("verbose", ["[Notification] No changes to report"]) + if pause_until_dt and is_datetime_future(pause_until_dt): + remaining_minutes = math.ceil((pause_until_dt - timeNowUTC(as_string=False)).total_seconds() / 60) - # Commit SQL - db.commitDB() + if remaining_minutes != last_paused_minute_broadcast: + updateState(f"Process: Paused for {remaining_minutes} min") + last_paused_minute_broadcast = remaining_minutes - mylog("verbose", ["[MAIN] Process: Idle"]) else: - # do something - # mylog('verbose', ['[MAIN] Waiting to start next loop']) - updateState("Process: Idle") + if last_paused_minute_broadcast is not None: + # Pause expired naturally (not via /scan/resume) - clear it and resume normal state + updateState("Process: Idle", pause_until="") + last_paused_minute_broadcast = None + + # proceed if 1 minute passed + if conf.last_scan_run + datetime.timedelta(minutes=1) < conf.loop_start_time: + # last time any scan or maintenance/upkeep was run + conf.last_scan_run = loop_start_time + + # Header (also broadcasts last_scan_run to frontend via SSE / app_state.json) + updateState("Process: Start", + last_scan_run=loop_start_time.replace(microsecond=0).isoformat(), + next_scan_time="") + + # Timestamp + startTime = loop_start_time + startTime = startTime.replace(microsecond=0) + + # Check if any plugins need to run on schedule + pm.run_plugin_scripts("schedule") + + # Compute the next scheduled run time AFTER schedule check (which updates last_next_schedule) + # Only device_scanner plugins have meaningful next_scan times for user display + scanner_prefixes = {p["unique_prefix"] for p in all_plugins if p.get("plugin_type") == "device_scanner"} + scanner_next = [s.last_next_schedule for s in conf.mySchedules if s.service in scanner_prefixes] + + # Get the earliest next scan time across all device scanners and broadcast. + # updateState validates the value is in the future before storing/broadcasting. + if scanner_next: + next_scan_dt = min(scanner_next) + updateState(next_scan_time=next_scan_dt.replace(microsecond=0).isoformat()) + + # determine run/scan type based on passed time + # -------------------------------------------- + + # Runs plugin scripts which are set to run every time after a scans finished + pm.run_plugin_scripts("always_after_scan") + + # process all the scanned data into new devices + processScan = updateState("Check scan").processScan + mylog("debug", [f"[MAIN] processScan: {processScan}"]) + + if processScan is True: + mylog("debug", "[MAIN] start processing scan results") + process_scan(db) + updateState("Scan processed", None, None, None, None, False) + + # Name resolution + # -------------------------------------------- + + # Check if new devices found (created by process_scan) + sql.execute(sql_new_devices) + newDevices = sql.fetchall() + db.commitDB() + + # If new devices were found, run all plugins registered to be run when new devices are found + # Run these before name resolution so plugins like NSLOOKUP that are configured + # for `on_new_device` can populate names used in the notifications below. + if len(newDevices) > 0: + pm.run_plugin_scripts("on_new_device") + + # run plugins before notification processing (e.g. Plugins to discover device names) + pm.run_plugin_scripts("before_name_updates") + + # Resolve devices names (will pick up results from on_new_device plugins above) + mylog("debug", "[Main] Resolve devices names") + update_devices_names(pm) + + # Notification handling + # ---------------------------------------- + + # send all configured notifications + final_json = get_notifications(db) + + # Write the notifications into the DB + notification = NotificationInstance(db) + notificationObj = notification.create(final_json, "") + + # ------------------------------------------------------------------------------ + # Run all enabled publisher gateways (notification delivery) + # ------------------------------------------------------------------------------ + # Design notes: + # - The eve_PendingAlertEmail flag is only cleared *after* a notification is sent. + # - If no notification is sent (HasNotifications == False), the flag stays set, + # meaning the event may still trigger alerts later depending on user settings + # (e.g. down-event reporting, delay timers, plugin conditions). + # - A pending flag means “still under evaluation,” not “missed.” + # It will clear automatically once its event is included in a sent alert. + # ------------------------------------------------------------------------------ + if notificationObj.HasNotifications: + pm.run_plugin_scripts("on_notification") + notification.setAllProcessed() + + # Only clear pending email flags and plugins_events once notifications are sent. + notification.clearPendingEmailFlag() + + else: + # If there are no notifications to process, + # we still need to clear all plugin events to prevent database growth if + # no notification gateways are configured + notification.clearPluginEvents() + mylog("verbose", ["[Notification] No changes to report"]) + + # Commit SQL + db.commitDB() + + mylog("verbose", ["[MAIN] Process: Idle"]) + else: + # do something + # mylog('verbose', ['[MAIN] Waiting to start next loop']) + updateState("Process: Idle") # WORKFLOWS handling # ---------------------------------------- diff --git a/server/api_server/api_server_start.py b/server/api_server/api_server_start.py index 24b55c717..c214463d4 100755 --- a/server/api_server/api_server_start.py +++ b/server/api_server/api_server_start.py @@ -1,6 +1,7 @@ import threading import sys import os +from datetime import timedelta # flake8: noqa: E402 @@ -18,6 +19,7 @@ from helper import get_setting_value, get_env_setting_value, getBuildTimeStampAndVersion # noqa: E402 [flake8 lint suppression] from db.db_helper import get_date_from_period # noqa: E402 [flake8 lint suppression] from app_state import updateState # noqa: E402 [flake8 lint suppression] +from utils.datetime_utils import timeNowUTC # noqa: E402 [flake8 lint suppression] from .graphql_endpoint import devicesSchema # noqa: E402 [flake8 lint suppression] from .history_endpoint import delete_online_history # noqa: E402 [flake8 lint suppression] @@ -82,6 +84,7 @@ DeviceImportResponse, UpdateDeviceColumnRequest, LockDeviceFieldRequest, UnlockDeviceFieldsRequest, CopyDeviceRequest, TriggerScanRequest, + PauseScanRequest, PauseScanResponse, ResumeScanResponse, OpenPortsRequest, OpenPortsResponse, WakeOnLanRequest, WakeOnLanResponse, TracerouteRequest, @@ -1168,6 +1171,44 @@ def api_trigger_scan(payload=None): return jsonify({"success": True, "message": f"Scan triggered for type: {scan_type}"}), 200 +@app.route("/scan/pause", methods=["POST"]) +@validate_request( + operation_id="pause_scan_scheduler", + summary="Pause Scan Scheduler", + description="Pause the automatic scheduled scan loop for a number of minutes. " + "Manually-triggered scans (e.g. /nettools/trigger-scan) are not affected.", + request_model=PauseScanRequest, + response_model=PauseScanResponse, + tags=["nettools"], + validation_error_code=400, + auth_callable=is_authorized +) +def api_pause_scan(payload=None): + minutes = payload.minutes + + pause_until = (timeNowUTC(as_string=False) + timedelta(minutes=minutes)).replace(microsecond=0).isoformat() + + updateState(f"Process: Paused for {minutes} min", pause_until=pause_until) + + return jsonify({"success": True, "message": f"Scans paused for {minutes} minutes", "pause_until": pause_until}), 200 + + +@app.route("/scan/resume", methods=["POST"]) +@validate_request( + operation_id="resume_scan_scheduler", + summary="Resume Scan Scheduler", + description="Clear any active scan pause and resume the automatic scan scheduler. Idempotent — " + "succeeds even if scans were not paused.", + response_model=ResumeScanResponse, + tags=["nettools"], + auth_callable=is_authorized +) +def api_resume_scan(payload=None): + updateState("Process: Idle", pause_until="") + + return jsonify({"success": True, "message": "Scans resumed", "pause_until": ""}), 200 + + # def trigger_scan(scan_type): # """Trigger a network scan by adding it to the execution queue.""" # if scan_type not in ["ARPSCAN", "NMAPDEV", "NMAP"]: diff --git a/server/api_server/openapi/schemas.py b/server/api_server/openapi/schemas.py index 86baa66ee..8103a383b 100644 --- a/server/api_server/openapi/schemas.py +++ b/server/api_server/openapi/schemas.py @@ -519,6 +519,26 @@ class TriggerScanResponse(BaseResponse): scan_type: Optional[str] = Field(None, description="Type of scan that was triggered") +class PauseScanRequest(BaseModel): + """Request to pause the automatic scan scheduler for a number of minutes.""" + minutes: int = Field( + ..., + ge=1, + le=1440, + description="Number of minutes to pause automatic scans for (1-1440)" + ) + + +class PauseScanResponse(BaseResponse): + """Response for pausing the automatic scan scheduler.""" + pause_until: Optional[str] = Field(None, description="ISO timestamp scans are paused until") + + +class ResumeScanResponse(BaseResponse): + """Response for resuming the automatic scan scheduler.""" + pause_until: Optional[str] = Field(None, description="Always empty; confirms the pause was cleared") + + class OpenPortsRequest(BaseModel): """Request for getting open ports.""" target: str = Field( diff --git a/server/app_state.py b/server/app_state.py index aab57cda7..ba1bef4e0 100755 --- a/server/app_state.py +++ b/server/app_state.py @@ -45,7 +45,8 @@ def __init__( appVersion=None, buildTimestamp=None, last_scan_run=None, - next_scan_time=None + next_scan_time=None, + pause_until=None ): """ Initialize the application state, optionally overwriting previous values. @@ -93,6 +94,7 @@ def __init__( self.buildTimestamp = previousState.get("buildTimestamp", "") self.last_scan_run = previousState.get("last_scan_run", "") self.next_scan_time = previousState.get("next_scan_time", "") + self.pause_until = previousState.get("pause_until", "") else: # init first time values self.settingsSaved = 0 self.settingsImported = 0 @@ -107,6 +109,7 @@ def __init__( self.buildTimestamp = "" self.last_scan_run = "" self.next_scan_time = "" + self.pause_until = "" # Overwrite with provided parameters if supplied if settingsSaved is not None: @@ -148,6 +151,9 @@ def __init__( self.next_scan_time = next_scan_time else: self.next_scan_time = "" + # "" explicitly clears the pause (resume); a truthy value sets/extends it + if pause_until is not None: + self.pause_until = pause_until # check for new version every hour and if currently not running new version if self.isNewVersion is False and self.isNewVersionChecked + 3600 < int( timeNowUTC(as_string=False).timestamp() @@ -182,7 +188,8 @@ def __init__( appVersion=self.appVersion, buildTimestamp=self.buildTimestamp, last_scan_run=self.last_scan_run, - next_scan_time=self.next_scan_time + next_scan_time=self.next_scan_time, + pause_until=self.pause_until ) except Exception as e: mylog("none", [f"[app_state] SSE broadcast: {e}"]) @@ -202,7 +209,8 @@ def updateState(newState = None, appVersion=None, buildTimestamp=None, last_scan_run=None, - next_scan_time=None): + next_scan_time = None, + pause_until = None): """ Convenience method to create or update the app state. @@ -218,6 +226,7 @@ def updateState(newState = None, buildTimestamp (str, optional): Build timestamp. last_scan_run (str, optional): ISO timestamp of last backend scan run. next_scan_time (str, optional): ISO timestamp of next scheduled device_scanner run. + pause_until (str, optional): ISO timestamp scans are paused until; "" clears the pause. Returns: app_state_class: Updated state object. @@ -233,7 +242,8 @@ def updateState(newState = None, appVersion, buildTimestamp, last_scan_run, - next_scan_time + next_scan_time, + pause_until ) diff --git a/server/database.py b/server/database.py index 63f14233b..4e63ad435 100755 --- a/server/database.py +++ b/server/database.py @@ -16,6 +16,8 @@ ensure_Settings, ensure_Indexes, ensure_mac_lowercase_triggers, + ensure_dangling_parentmac_cleanup_trigger, + cleanup_existing_dangling_parentmac, migrate_to_camelcase, migrate_timestamps_to_utc, ) @@ -225,6 +227,9 @@ def initDB(self): # Normalization triggers ensure_mac_lowercase_triggers(self.sql) + # Prevent/repair dangling devParentMAC references left by deleted devices + cleanup_existing_dangling_parentmac(self.sql) + # Device history table + audit triggers ensure_deviceshistory_table(self.sql) ensure_deviceshistory_triggers(self.sql) @@ -240,9 +245,10 @@ def initDB(self): AppEvent_obj(self) # AppEvent_obj.drop_all_triggers() wipes every trigger in the DB - # (including trg_devhist_*) as part of its clean-start routine. - # Re-create the device history audit triggers here so they survive. + # (including trg_devhist_* and trg_clear_dangling_parentmac_on_delete) + # as part of its clean-start routine. Re-create them here so they survive. ensure_deviceshistory_triggers(self.sql) + ensure_dangling_parentmac_cleanup_trigger(self.sql) self.commitDB() def get_table_as_json(self, sqlQuery, parameters=None): diff --git a/server/db/db_upgrade.py b/server/db/db_upgrade.py index 2adac94bc..f2e9b55f0 100755 --- a/server/db/db_upgrade.py +++ b/server/db/db_upgrade.py @@ -146,6 +146,74 @@ def ensure_mac_lowercase_triggers(sql): return False +# Sentinel devParentMAC values that are never actual device references +PARENT_MAC_SENTINELS = ("", "internet", "null") + + +def ensure_dangling_parentmac_cleanup_trigger(sql): + """ + Ensures a trigger exists that clears devParentMAC/devParentMACSource on any + device that referenced a device MAC which was just deleted, preventing + dangling Parent Node references. + + Note: this intentionally does NOT touch the NEWDEV_devParentMAC setting. + Settings are sourced from app.conf and get re-imported verbatim on every + restart (see importConfigs()), so a DB-only fix here would be silently + reverted. Stale NEWDEV_devParentMAC values are instead guarded against at + the point of use in create_new_devices() (server/scan/device_handling.py). + """ + try: + sql.execute( + "SELECT name FROM sqlite_master WHERE type='trigger' AND name='trg_clear_dangling_parentmac_on_delete'" + ) + if not sql.fetchone(): + mylog("verbose", ["[db_upgrade] Creating trigger 'trg_clear_dangling_parentmac_on_delete'"]) + sql.execute(""" + CREATE TRIGGER trg_clear_dangling_parentmac_on_delete + AFTER DELETE ON Devices + FOR EACH ROW + WHEN OLD.devMac IS NOT NULL AND OLD.devMac != '' + BEGIN + UPDATE Devices + SET devParentMAC = '', devParentMACSource = '' + WHERE LOWER(devParentMAC) = LOWER(OLD.devMac); + END; + """) + + return True + + except Exception as e: + mylog("none", [f"[db_upgrade] ERROR while ensuring dangling parentMAC trigger: {e}"]) + return False + + +def cleanup_existing_dangling_parentmac(sql) -> bool: + """ + One-time/idempotent cleanup for installations that already have devParentMAC + values pointing to a MAC no longer present in Devices. The delete trigger + only prevents new dangling references going forward, so this repairs data + left over from before the trigger existed. + """ + try: + sentinel_list = ", ".join(f"'{v}'" for v in PARENT_MAC_SENTINELS) + + sql.execute(f""" + UPDATE Devices + SET devParentMAC = '', devParentMACSource = '' + WHERE devParentMAC IS NOT NULL + AND LOWER(devParentMAC) NOT IN ({sentinel_list}) + AND LOWER(devParentMAC) NOT IN (SELECT LOWER(devMac) FROM Devices) + """) + if sql.rowcount > 0: + mylog("verbose", [f"[db_upgrade] Cleared {sql.rowcount} dangling devParentMAC reference(s)"]) + + return True + + except Exception as e: + mylog("none", [f"[db_upgrade] ERROR while cleaning up dangling parentMAC references: {e}"]) + return False + + def ensure_views(sql) -> bool: """ Ensures required views exist. diff --git a/server/models/__init__.py b/server/models/__init__.py new file mode 100644 index 000000000..ead537bd0 --- /dev/null +++ b/server/models/__init__.py @@ -0,0 +1,5 @@ +""" +NetAlertX models package. + +Contains domain models and instances for notifications, devices, events, etc. +""" diff --git a/server/plugins/ui_settings/config.json b/server/plugins/ui_settings/config.json index df80b8a31..ac71ded21 100755 --- a/server/plugins/ui_settings/config.json +++ b/server/plugins/ui_settings/config.json @@ -219,6 +219,35 @@ } ] }, + { + "function": "SCAN_PAUSE", + "type": { + "dataType": "integer", + "elements": [ + { + "elementType": "input", + "elementOptions": [{ "type": "number" }], + "transformers": [] + } + ] + }, + "maxLength": 50, + "default_value": 30, + "options": [], + "localized": [], + "name": [ + { + "language_code": "en_us", + "string": "Scan pause duration" + } + ], + "description": [ + { + "language_code": "en_us", + "string": "How long (in minutes) should scans be paused when the user clicks the Pause button. Accepts values from 1 to 1440." + } + ] + }, { "function": "REFRESH", "type": { @@ -271,7 +300,7 @@ "description": [ { "language_code": "en_us", - "string": "Default number of items shown in tables per page, for example in teh Devices lists." + "string": "Default number of items shown in tables per page, for example in the Devices lists." } ] }, diff --git a/server/scan/device_handling.py b/server/scan/device_handling.py index 1d4fa8993..59e5149ce 100755 --- a/server/scan/device_handling.py +++ b/server/scan/device_handling.py @@ -10,6 +10,7 @@ from scan.name_resolution import NameResolver from scan.device_heuristics import guess_icon, guess_type from db.db_helper import sanitize_SQL_input, list_to_where, safe_int +from db.db_upgrade import PARENT_MAC_SENTINELS from db.authoritative_handler import ( get_overwrite_sql_clause, can_overwrite_field, @@ -730,6 +731,22 @@ def create_new_devices(db): mylog("debug", f"[New Devices] Collecting New Devices Query: {query}") current_scan_data = sql.execute(query).fetchall() + # Resolve the default Parent Node setting once and guard against it pointing + # to a MAC that no longer exists (e.g. that device was since deleted) - + # falling back to unset rather than seeding new devices with a dangling reference. + default_parent_mac_setting = get_setting_value("NEWDEV_devParentMAC") + if default_parent_mac_setting and default_parent_mac_setting.lower() not in PARENT_MAC_SENTINELS: + existing_device_macs = { + str(row[0]).lower() for row in sql.execute("SELECT devMac FROM Devices").fetchall() if row[0] + } + if default_parent_mac_setting.lower() not in existing_device_macs: + mylog( + "verbose", + f"[New Devices] NEWDEV_devParentMAC '{default_parent_mac_setting}' no longer " + "exists in Devices - treating as unset", + ) + default_parent_mac_setting = "" + for row in current_scan_data: ( scanMac, @@ -771,7 +788,7 @@ def create_new_devices(db): scanParentMAC if scanParentMAC and scanMac.lower() != "internet" else ( - get_setting_value("NEWDEV_devParentMAC") + default_parent_mac_setting if scanMac.lower() != "internet" else "null" ) diff --git a/test/api_endpoints/test_scan_pause_endpoints.py b/test/api_endpoints/test_scan_pause_endpoints.py new file mode 100644 index 000000000..470225067 --- /dev/null +++ b/test/api_endpoints/test_scan_pause_endpoints.py @@ -0,0 +1,116 @@ +import pytest +from unittest.mock import patch, MagicMock + +from api_server.api_server_start import app +from helper import get_setting_value + + +@pytest.fixture(scope="session") +def api_token(): + return get_setting_value("API_TOKEN") + + +@pytest.fixture +def client(): + with app.test_client() as client: + yield client + + +def auth_headers(token): + return {"Authorization": f"Bearer {token}"} + + +# --- /scan/pause --- + + +@patch("api_server.api_server_start.updateState") +def test_pause_scan_success(mock_update_state, client, api_token): + """Valid minutes value pauses scans and returns a future pause_until timestamp.""" + mock_update_state.return_value = MagicMock() + + response = client.post("/scan/pause", json={"minutes": 10}, headers=auth_headers(api_token)) + + assert response.status_code == 200 + data = response.get_json() + assert data["success"] is True + assert "pause_until" in data and data["pause_until"] + + mock_update_state.assert_called_once() + args, kwargs = mock_update_state.call_args + assert args[0] == "Process: Paused for 10 min" + assert kwargs["pause_until"] == data["pause_until"] + + +@patch("api_server.api_server_start.updateState") +def test_pause_scan_default_minutes_used(mock_update_state, client, api_token): + """The header button's default 10-minute pause request is accepted.""" + mock_update_state.return_value = MagicMock() + + response = client.post("/scan/pause", json={"minutes": 10}, headers=auth_headers(api_token)) + + assert response.status_code == 200 + assert response.get_json()["success"] is True + + +@pytest.mark.parametrize("minutes", [0, -5, 1441, "ten"]) +def test_pause_scan_invalid_minutes(client, api_token, minutes): + """Out-of-bounds or non-integer minutes values are rejected with a 400.""" + response = client.post("/scan/pause", json={"minutes": minutes}, headers=auth_headers(api_token)) + + assert response.status_code == 400 + data = response.get_json() + assert data["success"] is False + + +def test_pause_scan_missing_minutes(client, api_token): + """Missing 'minutes' field is rejected with a 400.""" + response = client.post("/scan/pause", json={}, headers=auth_headers(api_token)) + + assert response.status_code == 400 + assert response.get_json()["success"] is False + + +def test_pause_scan_requires_auth(client): + """Unauthenticated requests are rejected.""" + response = client.post("/scan/pause", json={"minutes": 10}) + + assert response.status_code == 403 + + +# --- /scan/resume --- + + +@patch("api_server.api_server_start.updateState") +def test_resume_scan_success(mock_update_state, client, api_token): + """Resume clears the pause and reports pause_until as empty.""" + mock_update_state.return_value = MagicMock() + + response = client.post("/scan/resume", headers=auth_headers(api_token)) + + assert response.status_code == 200 + data = response.get_json() + assert data["success"] is True + assert data["pause_until"] == "" + + mock_update_state.assert_called_once_with("Process: Idle", pause_until="") + + +@patch("api_server.api_server_start.updateState") +def test_resume_scan_idempotent_when_not_paused(mock_update_state, client, api_token): + """Calling resume when scans are not paused still succeeds (idempotent).""" + mock_update_state.return_value = MagicMock() + + response = client.post("/scan/resume", headers=auth_headers(api_token)) + response2 = client.post("/scan/resume", headers=auth_headers(api_token)) + + assert response.status_code == 200 + assert response2.status_code == 200 + assert response.get_json()["success"] is True + assert response2.get_json()["success"] is True + + +def test_resume_scan_requires_auth(client): + """Unauthenticated requests are rejected.""" + response = client.post("/scan/resume") + + assert response.status_code == 403 diff --git a/test/db/test_dangling_parentmac_cleanup.py b/test/db/test_dangling_parentmac_cleanup.py new file mode 100644 index 000000000..527d50d2b --- /dev/null +++ b/test/db/test_dangling_parentmac_cleanup.py @@ -0,0 +1,172 @@ +""" +Unit tests for dangling devParentMAC cleanup. + +Tests verify that: +- Deleting a device clears devParentMAC/devParentMACSource on devices that + referenced it as their Parent Node. +- Sentinel values ('', 'internet', 'null') are never touched. +- Valid parent references are left untouched. +- The one-time migration repairs pre-existing dangling data and is idempotent. + +Note: the NEWDEV_devParentMAC *setting* is intentionally NOT handled here. +Settings are sourced from app.conf and get re-imported verbatim on every +restart, so a DB-only fix would be silently reverted. That case is instead +guarded against at the point of use in create_new_devices() — see +test/scan/test_field_lock_scan_integration.py. +""" + +import sys +import os +import pytest +import sqlite3 +import tempfile + +INSTALL_PATH = os.getenv('NETALERTX_APP', '/app') +sys.path.extend([f"{INSTALL_PATH}/server/plugins", f"{INSTALL_PATH}/server"]) + +from db.db_upgrade import ( # noqa: E402 + ensure_dangling_parentmac_cleanup_trigger, + cleanup_existing_dangling_parentmac, +) + + +@pytest.fixture +def temp_db(): + """Create a temporary database for testing""" + fd, db_path = tempfile.mkstemp(suffix='.db') + os.close(fd) + + conn = sqlite3.connect(db_path) + cursor = conn.cursor() + + cursor.execute(""" + CREATE TABLE Devices ( + devMac TEXT PRIMARY KEY COLLATE NOCASE, + devParentMAC TEXT, + devParentMACSource TEXT + ) + """) + + conn.commit() + + yield cursor, conn + + conn.close() + os.unlink(db_path) + + +class TestDanglingParentMacTrigger: + """Test suite for the AFTER DELETE cleanup trigger""" + + def test_trigger_clears_dependent_devices_on_delete(self, temp_db): + cursor, conn = temp_db + assert ensure_dangling_parentmac_cleanup_trigger(cursor) is True + + cursor.execute( + "INSERT INTO Devices (devMac, devParentMAC, devParentMACSource) VALUES (?, ?, ?)", + ("aa:bb:cc:dd:ee:01", "", ""), + ) + cursor.execute( + "INSERT INTO Devices (devMac, devParentMAC, devParentMACSource) VALUES (?, ?, ?)", + ("aa:bb:cc:dd:ee:02", "aa:bb:cc:dd:ee:01", "NEWDEV"), + ) + conn.commit() + + cursor.execute("DELETE FROM Devices WHERE devMac = ?", ("aa:bb:cc:dd:ee:01",)) + conn.commit() + + cursor.execute( + "SELECT devParentMAC, devParentMACSource FROM Devices WHERE devMac = ?", + ("aa:bb:cc:dd:ee:02",), + ) + row = cursor.fetchone() + assert row == ("", "") + + def test_trigger_ignores_unrelated_deletes(self, temp_db): + cursor, conn = temp_db + ensure_dangling_parentmac_cleanup_trigger(cursor) + + cursor.execute( + "INSERT INTO Devices (devMac, devParentMAC) VALUES (?, ?)", + ("aa:bb:cc:dd:ee:01", "internet"), + ) + cursor.execute( + "INSERT INTO Devices (devMac, devParentMAC) VALUES (?, ?)", + ("aa:bb:cc:dd:ee:02", ""), + ) + conn.commit() + + cursor.execute("DELETE FROM Devices WHERE devMac = ?", ("aa:bb:cc:dd:ee:02",)) + conn.commit() + + cursor.execute( + "SELECT devParentMAC FROM Devices WHERE devMac = ?", ("aa:bb:cc:dd:ee:01",) + ) + assert cursor.fetchone() == ("internet",) + + +class TestCleanupExistingDanglingParentMac: + """Test suite for the one-time/idempotent data repair migration""" + + def test_cleanup_clears_dangling_reference(self, temp_db): + cursor, conn = temp_db + + cursor.execute( + "INSERT INTO Devices (devMac, devParentMAC, devParentMACSource) VALUES (?, ?, ?)", + ("aa:bb:cc:dd:ee:02", "aa:bb:cc:dd:ee:99", "NEWDEV"), + ) + conn.commit() + + assert cleanup_existing_dangling_parentmac(cursor) is True + + cursor.execute( + "SELECT devParentMAC, devParentMACSource FROM Devices WHERE devMac = ?", + ("aa:bb:cc:dd:ee:02",), + ) + assert cursor.fetchone() == ("", "") + + def test_cleanup_preserves_valid_and_sentinel_values(self, temp_db): + cursor, conn = temp_db + + cursor.execute( + "INSERT INTO Devices (devMac, devParentMAC) VALUES (?, ?)", + ("aa:bb:cc:dd:ee:01", ""), + ) + cursor.execute( + "INSERT INTO Devices (devMac, devParentMAC) VALUES (?, ?)", + ("aa:bb:cc:dd:ee:02", "aa:bb:cc:dd:ee:01"), + ) + cursor.execute( + "INSERT INTO Devices (devMac, devParentMAC) VALUES (?, ?)", + ("aa:bb:cc:dd:ee:03", "internet"), + ) + conn.commit() + + cleanup_existing_dangling_parentmac(cursor) + + cursor.execute( + "SELECT devParentMAC FROM Devices WHERE devMac = ?", ("aa:bb:cc:dd:ee:02",) + ) + assert cursor.fetchone() == ("aa:bb:cc:dd:ee:01",) + + cursor.execute( + "SELECT devParentMAC FROM Devices WHERE devMac = ?", ("aa:bb:cc:dd:ee:03",) + ) + assert cursor.fetchone() == ("internet",) + + def test_cleanup_is_idempotent(self, temp_db): + cursor, conn = temp_db + + cursor.execute( + "INSERT INTO Devices (devMac, devParentMAC) VALUES (?, ?)", + ("aa:bb:cc:dd:ee:02", "aa:bb:cc:dd:ee:99"), + ) + conn.commit() + + assert cleanup_existing_dangling_parentmac(cursor) is True + assert cleanup_existing_dangling_parentmac(cursor) is True + + cursor.execute( + "SELECT devParentMAC FROM Devices WHERE devMac = ?", ("aa:bb:cc:dd:ee:02",) + ) + assert cursor.fetchone() == ("",) diff --git a/test/plugins/test_ntfy_custom_headers.py b/test/plugins/test_ntfy_custom_headers.py index d0e542ea6..8fc8863cd 100644 --- a/test/plugins/test_ntfy_custom_headers.py +++ b/test/plugins/test_ntfy_custom_headers.py @@ -16,11 +16,16 @@ # --------------------------------------------------------------------------- # Stub NetAlertX-specific modules so tests can run outside the container. -# sys.modules.setdefault() is a no-op when the real module is already loaded, -# so this is safe to run inside the container too. +# These stubs are only placeholders for the duration of the `import ntfy` +# below - they are popped from sys.modules again right after, so they don't +# leak into other test files sharing the same pytest session (which would +# otherwise shadow the real modules, e.g. models.notification_instance, for +# every subsequent test). # --------------------------------------------------------------------------- _tmp_log = tempfile.mkdtemp() +_stubbed_module_names = [] + def _stub(name: str, **attrs): if name not in sys.modules: @@ -28,6 +33,7 @@ def _stub(name: str, **attrs): for k, v in attrs.items(): setattr(mod, k, v) sys.modules[name] = mod + _stubbed_module_names.append(name) _stub("pytz", timezone=lambda tz: tz) @@ -57,6 +63,13 @@ def _stub(name: str, **attrs): import ntfy # noqa: E402 from ntfy import build_custom_headers # noqa: E402 +# `ntfy` has already resolved its module-level `from x import y` bindings at +# this point, so removing these fake entries from sys.modules doesn't affect +# it - it just stops them from shadowing the real modules for other test +# files collected later in the same pytest session. +for _name in _stubbed_module_names: + sys.modules.pop(_name, None) + BUILT_IN = {"Title": "NetAlertX Notification", "Authorization": "Bearer secret"} diff --git a/test/scan/test_field_lock_scan_integration.py b/test/scan/test_field_lock_scan_integration.py index 8036ad225..77bd47c2a 100644 --- a/test/scan/test_field_lock_scan_integration.py +++ b/test/scan/test_field_lock_scan_integration.py @@ -231,6 +231,66 @@ def get_setting_value_side_effect(key): assert row["devVlanSource"] == "NEWDEV" +def test_create_new_devices_ignores_dangling_newdev_parentmac(scan_db_for_new_devices): + """A stale NEWDEV_devParentMAC pointing to a since-deleted device is treated as unset, + instead of seeding the new device with another dangling Parent Node reference.""" + cur = scan_db_for_new_devices.cursor() + cur.execute( + """ + INSERT INTO CurrentScan ( + scanMac, scanName, scanVendor, scanSourcePlugin, scanLastIP, + scanSyncHubNode, scanParentMAC, scanParentPort, + scanSite, scanSSID, scanType + ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + """, + ( + "aa:bb:cc:dd:ee:11", + "DeviceTwo", + "AcmeVendor", + "ARPSCAN", + "192.168.1.11", + "", + "", # no parent reported by the scan itself + "", + "", + "", + "", + ), + ) + scan_db_for_new_devices.commit() + + settings = { + "NEWDEV_devType": "default-type", + # points to a MAC that does not (and never did, in this test) exist in Devices + "NEWDEV_devParentMAC": "99:99:99:99:99:99", + "NEWDEV_devOwner": "owner", + "NEWDEV_devGroup": "group", + "NEWDEV_devComments": "", + "NEWDEV_devLocation": "", + "NEWDEV_devCustomProps": "", + "NEWDEV_devParentRelType": "uplink", + "SYNC_node_name": "SYNCNODE", + } + + db = Mock() + db.sql_connection = scan_db_for_new_devices + db.sql = cur + db.commitDB = scan_db_for_new_devices.commit + + with patch.multiple( + device_handling, + get_setting_value=Mock(side_effect=lambda key: settings.get(key, "")), + safe_int=Mock(return_value=0), + ): + device_handling.create_new_devices(db) + + row = cur.execute( + "SELECT devParentMAC FROM Devices WHERE devMac = ?", ("aa:bb:cc:dd:ee:11",) + ).fetchone() + + assert row["devParentMAC"] == "" + + def test_scan_updates_newdev_device_name(scan_db, mock_device_handlers): """Scanner discovers name for device with NEWDEV source.""" cur = scan_db.cursor()