From ee09fe9a2f9d9700755bda830abb0a2650cb5e29 Mon Sep 17 00:00:00 2001 From: Massimo Pissarello Date: Sat, 22 Aug 2026 04:42:48 +0200 Subject: [PATCH 01/16] Translated using Weblate (Italian) Currently translated at 100.0% (832 of 832 strings) Translation: NetAlertX/core Translate-URL: https://hosted.weblate.org/projects/pialert/core/it/ --- front/php/templates/language/it_it.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/front/php/templates/language/it_it.json b/front/php/templates/language/it_it.json index 70b20dbba..6d9ea0f8c 100644 --- a/front/php/templates/language/it_it.json +++ b/front/php/templates/language/it_it.json @@ -233,7 +233,7 @@ "Device_Shortcut_OnlineChart": "Presenza dispositivo", "Device_Shortcut_Unstable": "Instabile", "Device_TableHead_AlertDown": "Avviso disconnessione", - "Device_TableHead_Comments": "", + "Device_TableHead_Comments": "Commenti", "Device_TableHead_Connected_Devices": "Connessioni", "Device_TableHead_CustomProps": "Proprietà/Azioni", "Device_TableHead_FQDN": "FQDN", @@ -831,4 +831,4 @@ "settings_system_label": "Sistema", "settings_update_item_warning": "Aggiorna il valore qui sotto. Fai attenzione a seguire il formato precedente. La convalida non viene eseguita.", "test_event_tooltip": "Salva le modifiche prima di provare le nuove impostazioni." -} \ No newline at end of file +} From 8c1cf9911d262020ef90a685017a74194b379759 Mon Sep 17 00:00:00 2001 From: Ettore Atalan Date: Sat, 22 Aug 2026 14:33:22 +0200 Subject: [PATCH 02/16] Translated using Weblate (German) Currently translated at 88.1% (737 of 836 strings) Translation: NetAlertX/core Translate-URL: https://hosted.weblate.org/projects/pialert/core/de/ --- front/php/templates/language/de_de.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/front/php/templates/language/de_de.json b/front/php/templates/language/de_de.json index d8c756616..64023855b 100644 --- a/front/php/templates/language/de_de.json +++ b/front/php/templates/language/de_de.json @@ -237,7 +237,7 @@ "Device_Shortcut_OnlineChart": "Gerätepräsenz im Laufe der Zeit", "Device_Shortcut_Unstable": "Instabil", "Device_TableHead_AlertDown": "Alarm aus", - "Device_TableHead_Comments": "", + "Device_TableHead_Comments": "Kommentare", "Device_TableHead_Connected_Devices": "Verbindungen", "Device_TableHead_CustomProps": "Eigenschaften / Aktionen", "Device_TableHead_FQDN": "FQDN", @@ -908,4 +908,4 @@ "settings_system_label": "System", "settings_update_item_warning": "", "test_event_tooltip": "Speichere die Änderungen, bevor Sie die Einstellungen testen." -} \ No newline at end of file +} From fa37284574a6e60aca1e8fe21fac041078be53e7 Mon Sep 17 00:00:00 2001 From: Sylvain Pichon Date: Sat, 22 Aug 2026 22:00:55 +0200 Subject: [PATCH 03/16] Translated using Weblate (French) Currently translated at 100.0% (836 of 836 strings) Translation: NetAlertX/core Translate-URL: https://hosted.weblate.org/projects/pialert/core/fr/ --- front/php/templates/language/fr_fr.json | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/front/php/templates/language/fr_fr.json b/front/php/templates/language/fr_fr.json index 41221ac23..3cd152cec 100644 --- a/front/php/templates/language/fr_fr.json +++ b/front/php/templates/language/fr_fr.json @@ -233,7 +233,7 @@ "Device_Shortcut_OnlineChart": "Présence de l'appareil", "Device_Shortcut_Unstable": "Instable", "Device_TableHead_AlertDown": "Alerter si En panne", - "Device_TableHead_Comments": "", + "Device_TableHead_Comments": "Commentaires", "Device_TableHead_Connected_Devices": "Connexions", "Device_TableHead_CustomProps": "Champs / Actions", "Device_TableHead_FQDN": "Nom de domaine FQDN", @@ -387,8 +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": "", + "Header_PauseScans_Tooltip": "Mettre en pause les scans automatiques", + "Header_ResumeScans_Tooltip": "Reprendre les scans automatiques", "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.", @@ -647,8 +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": "", + "Scans_Paused": "Scans mis en pause", + "Scans_Resumed": "Scans repris", "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é.", @@ -835,4 +835,4 @@ "settings_system_label": "Système", "settings_update_item_warning": "Mettre à jour la valeur ci-dessous. Veillez à bien suivre le même format qu'auparavant. Il n'y a pas de pas de contrôle.", "test_event_tooltip": "Enregistrer d'abord vos modifications avant de tester vôtre paramétrage." -} \ No newline at end of file +} From a0270c76a4cde0fad3feedfddc42487be04c9ee7 Mon Sep 17 00:00:00 2001 From: Massimo Pissarello Date: Sat, 22 Aug 2026 23:24:40 +0200 Subject: [PATCH 04/16] Translated using Weblate (Italian) Currently translated at 100.0% (836 of 836 strings) Translation: NetAlertX/core Translate-URL: https://hosted.weblate.org/projects/pialert/core/it/ --- front/php/templates/language/it_it.json | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/front/php/templates/language/it_it.json b/front/php/templates/language/it_it.json index 66770cf67..8f84b1fdf 100644 --- a/front/php/templates/language/it_it.json +++ b/front/php/templates/language/it_it.json @@ -387,8 +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": "", + "Header_PauseScans_Tooltip": "Scansioni automatiche in pausa", + "Header_ResumeScans_Tooltip": "Riprendi scansioni automatiche", "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.", @@ -647,8 +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": "", + "Scans_Paused": "Scansioni in pausa", + "Scans_Resumed": "Scansioni riprese", "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.", From ece9788b890771ba16b030e8d73af21731163963 Mon Sep 17 00:00:00 2001 From: Aditya Raj Singh Date: Wed, 26 Aug 2026 02:47:22 +0530 Subject: [PATCH 05/16] fix: keep MikroTik scans running past MAC-less leases --- server/plugins/mikrotik_scan/mikrotik.py | 9 +-- test/plugins/test_mikrotik_scan.py | 83 ++++++++++++++++++++++++ 2 files changed, 88 insertions(+), 4 deletions(-) create mode 100644 test/plugins/test_mikrotik_scan.py diff --git a/server/plugins/mikrotik_scan/mikrotik.py b/server/plugins/mikrotik_scan/mikrotik.py index 1bb566c8a..3a6ca1363 100755 --- a/server/plugins/mikrotik_scan/mikrotik.py +++ b/server/plugins/mikrotik_scan/mikrotik.py @@ -7,7 +7,7 @@ INSTALL_PATH = os.getenv('NETALERTX_APP', '/app') sys.path.extend([f"{INSTALL_PATH}/server/plugins", f"{INSTALL_PATH}/server"]) -from plugin_helper import Plugin_Objects # noqa: E402 [flake8 lint suppression] +from plugin_helper import Plugin_Objects, normalize_mac # noqa: E402 [flake8 lint suppression] from logger import mylog, Logger # noqa: E402 [flake8 lint suppression] from helper import get_setting_value # noqa: E402 [flake8 lint suppression] from const import logPath # noqa: E402 [flake8 lint suppression] @@ -64,16 +64,17 @@ def get_entries(plugin_objects: Plugin_Objects) -> Plugin_Objects: for lease in leases: lease_id = lease.get('.id') address = lease.get('address') - mac_address = lease.get('mac-address').lower() + raw_mac_address = lease.get('mac-address') host_name = lease.get('host-name') comment = lease.get('comment') last_seen = lease.get('last-seen') status = lease.get('status') device_name = comment or host_name or "(unknown)" - mylog('verbose', f"ID: {lease_id}, Address: {address}, MAC: {mac_address}, Host Name: {host_name}, Comment: {comment}, Last Seen: {last_seen}, Status: {status}") + mylog('verbose', f"ID: {lease_id}, Address: {address}, MAC: {raw_mac_address}, Host Name: {host_name}, Comment: {comment}, Last Seen: {last_seen}, Status: {status}") - if (status == "bound"): + if status == "bound" and raw_mac_address: + mac_address = normalize_mac(raw_mac_address) plugin_objects.add_object( primaryId = mac_address, secondaryId = address, diff --git a/test/plugins/test_mikrotik_scan.py b/test/plugins/test_mikrotik_scan.py new file mode 100644 index 000000000..f7d20505a --- /dev/null +++ b/test/plugins/test_mikrotik_scan.py @@ -0,0 +1,83 @@ +"""Tests for the MikroTik DHCP lease scanner.""" + +import importlib.util +import sys +import types +from pathlib import Path +from unittest.mock import MagicMock, patch + + +def _load_mikrotik_module(): + stubbed_module_names = [] + + def stub(name, **attributes): + module = types.ModuleType(name) + for attribute, value in attributes.items(): + setattr(module, attribute, value) + sys.modules[name] = module + stubbed_module_names.append(name) + + class TrapError(Exception): + pass + + stub( + "plugin_helper", + Plugin_Objects=MagicMock, + normalize_mac=lambda mac: mac.strip().lower(), + ) + stub("logger", mylog=MagicMock(), Logger=MagicMock()) + stub("helper", get_setting_value=MagicMock(return_value="UTC")) + stub("const", logPath="/tmp") + stub("conf", tz=None) + stub("pytz", timezone=MagicMock(return_value="UTC")) + stub("librouteros", connect=MagicMock()) + stub("librouteros.exceptions", TrapError=TrapError) + + module_path = Path(__file__).resolve().parents[2] / "server" / "plugins" / "mikrotik_scan" / "mikrotik.py" + spec = importlib.util.spec_from_file_location("mikrotik_scan", module_path) + module = importlib.util.module_from_spec(spec) + spec.loader.exec_module(module) + + for name in stubbed_module_names: + sys.modules.pop(name, None) + + return module + + +mikrotik = _load_mikrotik_module() + + +def _lease(lease_id, address, mac_address, status="bound"): + lease = { + ".id": lease_id, + "address": address, + "host-name": f"device-{lease_id}", + "comment": "", + "last-seen": "1m", + "status": status, + } + if mac_address is not None: + lease["mac-address"] = mac_address + return lease + + +def test_disabled_lease_without_mac_does_not_abort_remaining_leases(): + leases = [ + _lease("*1", "192.168.1.2", "aa:bb:cc:dd:ee:01"), + _lease("*2", "192.168.1.5", None, status="waiting"), + _lease("*3", "192.168.1.8", "aa:bb:cc:dd:ee:03"), + ] + api = MagicMock(return_value=leases) + plugin_objects = MagicMock() + + mikrotik.MT_USER = "user" + mikrotik.MT_PASS = "password" + mikrotik.MT_HOST = "192.168.1.1" + mikrotik.MT_PORT = 8728 + + with patch.object(mikrotik, "connect", return_value=api): + result = mikrotik.get_entries(plugin_objects) + + assert result is plugin_objects + assert plugin_objects.add_object.call_count == 2 + assert [call.kwargs["primaryId"] for call in plugin_objects.add_object.call_args_list] == ["aa:bb:cc:dd:ee:01", "aa:bb:cc:dd:ee:03"] From ea7afd6c835280435559c4feac381b419f0fdb76 Mon Sep 17 00:00:00 2001 From: Aditya Raj Singh Date: Wed, 26 Aug 2026 02:59:20 +0530 Subject: [PATCH 06/16] test: isolate MikroTik scanner module stubs --- test/plugins/test_mikrotik_scan.py | 25 +++++++++++++++---------- 1 file changed, 15 insertions(+), 10 deletions(-) diff --git a/test/plugins/test_mikrotik_scan.py b/test/plugins/test_mikrotik_scan.py index f7d20505a..e42c07900 100644 --- a/test/plugins/test_mikrotik_scan.py +++ b/test/plugins/test_mikrotik_scan.py @@ -8,14 +8,15 @@ def _load_mikrotik_module(): - stubbed_module_names = [] + missing_module = object() + previous_modules = {} def stub(name, **attributes): + previous_modules[name] = sys.modules.get(name, missing_module) module = types.ModuleType(name) for attribute, value in attributes.items(): setattr(module, attribute, value) sys.modules[name] = module - stubbed_module_names.append(name) class TrapError(Exception): pass @@ -23,7 +24,7 @@ class TrapError(Exception): stub( "plugin_helper", Plugin_Objects=MagicMock, - normalize_mac=lambda mac: mac.strip().lower(), + normalize_mac=lambda mac: mac.strip().lower().replace("-", ":"), ) stub("logger", mylog=MagicMock(), Logger=MagicMock()) stub("helper", get_setting_value=MagicMock(return_value="UTC")) @@ -36,10 +37,14 @@ class TrapError(Exception): module_path = Path(__file__).resolve().parents[2] / "server" / "plugins" / "mikrotik_scan" / "mikrotik.py" spec = importlib.util.spec_from_file_location("mikrotik_scan", module_path) module = importlib.util.module_from_spec(spec) - spec.loader.exec_module(module) - - for name in stubbed_module_names: - sys.modules.pop(name, None) + try: + spec.loader.exec_module(module) + finally: + for name, previous_module in previous_modules.items(): + if previous_module is missing_module: + sys.modules.pop(name, None) + else: + sys.modules[name] = previous_module return module @@ -63,15 +68,15 @@ def _lease(lease_id, address, mac_address, status="bound"): def test_disabled_lease_without_mac_does_not_abort_remaining_leases(): leases = [ - _lease("*1", "192.168.1.2", "aa:bb:cc:dd:ee:01"), + _lease("*1", "192.168.1.2", "aa-bb-cc-dd-ee-01"), _lease("*2", "192.168.1.5", None, status="waiting"), - _lease("*3", "192.168.1.8", "aa:bb:cc:dd:ee:03"), + _lease("*3", "192.168.1.8", "aa-bb-cc-dd-ee-03"), ] api = MagicMock(return_value=leases) plugin_objects = MagicMock() mikrotik.MT_USER = "user" - mikrotik.MT_PASS = "password" + mikrotik.MT_PASS = None mikrotik.MT_HOST = "192.168.1.1" mikrotik.MT_PORT = 8728 From bcd44381c656fba66cfe81f977ddc3e21a7885c4 Mon Sep 17 00:00:00 2001 From: Aditya Raj Singh Date: Wed, 26 Aug 2026 03:05:02 +0530 Subject: [PATCH 07/16] test: cover bound MikroTik leases without MACs --- test/plugins/test_mikrotik_scan.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/test/plugins/test_mikrotik_scan.py b/test/plugins/test_mikrotik_scan.py index e42c07900..e79f217f4 100644 --- a/test/plugins/test_mikrotik_scan.py +++ b/test/plugins/test_mikrotik_scan.py @@ -66,11 +66,12 @@ def _lease(lease_id, address, mac_address, status="bound"): return lease -def test_disabled_lease_without_mac_does_not_abort_remaining_leases(): +def test_leases_without_mac_do_not_abort_remaining_leases(): leases = [ _lease("*1", "192.168.1.2", "aa-bb-cc-dd-ee-01"), _lease("*2", "192.168.1.5", None, status="waiting"), - _lease("*3", "192.168.1.8", "aa-bb-cc-dd-ee-03"), + _lease("*3", "192.168.1.6", None), + _lease("*4", "192.168.1.8", "aa-bb-cc-dd-ee-04"), ] api = MagicMock(return_value=leases) plugin_objects = MagicMock() @@ -85,4 +86,4 @@ def test_disabled_lease_without_mac_does_not_abort_remaining_leases(): assert result is plugin_objects assert plugin_objects.add_object.call_count == 2 - assert [call.kwargs["primaryId"] for call in plugin_objects.add_object.call_args_list] == ["aa:bb:cc:dd:ee:01", "aa:bb:cc:dd:ee:03"] + assert [call.kwargs["primaryId"] for call in plugin_objects.add_object.call_args_list] == ["aa:bb:cc:dd:ee:01", "aa:bb:cc:dd:ee:04"] From 988d0a47688661a595c3150a17fc322bbdefb5ab Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=8F=AB=E6=88=91Romeo?= <38833268@qq.com> Date: Thu, 27 Aug 2026 04:13:40 +0200 Subject: [PATCH 08/16] Translated using Weblate (Chinese (Simplified Han script)) Currently translated at 100.0% (836 of 836 strings) Translation: NetAlertX/core Translate-URL: https://hosted.weblate.org/projects/pialert/core/zh_Hans/ --- front/php/templates/language/zh_cn.json | 82 ++++++++++++------------- 1 file changed, 41 insertions(+), 41 deletions(-) diff --git a/front/php/templates/language/zh_cn.json b/front/php/templates/language/zh_cn.json index a93417e07..dd0462cbb 100644 --- a/front/php/templates/language/zh_cn.json +++ b/front/php/templates/language/zh_cn.json @@ -66,12 +66,12 @@ "CustProps_cant_remove": "无法移除,至少需要保留一个属性。", "DAYS_TO_KEEP_EVENTS_description": "这是维护设置。它指定将保留的事件条目的天数。所有较旧的事件将被定期删除。也适用于插件事件历史记录。", "DAYS_TO_KEEP_EVENTS_name": "删除早于", - "DEEP_SLEEP_description": "", + "DEEP_SLEEP_description": "通过延长处理周期之间的空闲等待时间来降低 CPU 使用率。启用后,扫描任务可能会延迟最多 1 分钟,且用户界面(UI)的响应速度可能会降低", "DEEP_SLEEP_name": "深度睡眠", - "DEV_HIST_DAYS_description": "", - "DEV_HIST_DAYS_name": "", - "DEV_HIST_TRACKED_description": "", - "DEV_HIST_TRACKED_name": "", + "DEV_HIST_DAYS_description": "保留设备字段变更历史的天数,设置为 0 可完全禁用审计引擎并减少资源使用", + "DEV_HIST_DAYS_name": "列变更保留期限", + "DEV_HIST_TRACKED_description": "监控变更的设备列,添加或移除字段名称以控制变更日志中审核的内容", + "DEV_HIST_TRACKED_name": "跟踪列更改", "DISCOVER_PLUGINS_description": "禁用此选项可加快初始化和设置保存的速度。当禁用时,插件不会被发现,并且您无法将新插件添加到 LOADED_PLUGINS设置中。", "DISCOVER_PLUGINS_name": "发现插件", "DevDetail_Children_Title": "子代关系", @@ -209,10 +209,10 @@ "Device_MultiEdit_MassActions": "谨慎操作:", "Device_MultiEdit_No_Devices": "未选择设备。", "Device_MultiEdit_Tooltip": "小心。 单击此按钮会将左侧的值应用到上面选择的所有设备。", - "Device_NextScan_Imminent": "", + "Device_NextScan_Imminent": "即将发生的", "Device_NextScan_In": "下一次扫描将大致进行于 ", - "Device_NoData_Help": "", - "Device_NoData_Scanning": "", + "Device_NoData_Help": "如果扫描后设备未出现,请检查您的SCAN_SUBNETS设置以及 documentation.", + "Device_NoData_Scanning": "等待首次扫描 - 这在初始设置后可能需要几分钟时间.", "Device_NoData_Title": "没有找到设备", "Device_NoMatch_Title": "没有设备符合当前过滤选项", "Device_Save_Failed": "保存设备失败", @@ -231,15 +231,15 @@ "Device_Shortcut_Favorites": "收藏", "Device_Shortcut_NewDevices": "新设备", "Device_Shortcut_OnlineChart": "设备统计", - "Device_Shortcut_Unstable": "", + "Device_Shortcut_Unstable": "不稳定", "Device_TableHead_AlertDown": "提醒宕机", - "Device_TableHead_Comments": "", + "Device_TableHead_Comments": "注释", "Device_TableHead_Connected_Devices": "链接", "Device_TableHead_CustomProps": "属性", "Device_TableHead_FQDN": "FQDN", "Device_TableHead_Favorite": "收藏", "Device_TableHead_FirstSession": "加入", - "Device_TableHead_Flapping": "", + "Device_TableHead_Flapping": "扑动", "Device_TableHead_GUID": "GUID", "Device_TableHead_Group": "组", "Device_TableHead_IPv4": "IPv4", @@ -341,17 +341,17 @@ "Gen_Delete": "删除", "Gen_DeleteAll": "全部删除", "Gen_Description": "描述", - "Gen_Down": "", + "Gen_Down": "下", "Gen_Error": "错误", "Gen_Filter": "筛选", - "Gen_Flapping": "", + "Gen_Flapping": "跳动", "Gen_Generate": "生成", "Gen_InvalidMac": "无效的 Mac 地址。", "Gen_Invalid_Value": "输入了无效的值", "Gen_LockedDB": "错误 - DB 可能被锁定 - 检查 F12 开发工具 -> 控制台或稍后重试。", "Gen_NetworkMask": "网络掩码", - "Gen_New": "", - "Gen_No_Data": "", + "Gen_New": "新的", + "Gen_No_Data": "无数据", "Gen_Offline": "离线", "Gen_Okay": "Ok", "Gen_Online": "在线", @@ -369,7 +369,7 @@ "Gen_SelectIcon": "", "Gen_SelectToPreview": "选择预览", "Gen_Selected_Devices": "选定的设备:", - "Gen_Sleeping": "", + "Gen_Sleeping": "睡眠", "Gen_Subnet": "子网", "Gen_Switch": "交换", "Gen_Upd": "已成功更新", @@ -387,8 +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": "", + "Header_PauseScans_Tooltip": "暂停自动扫描", + "Header_ResumeScans_Tooltip": "恢复自动扫描", "LOADED_PLUGINS_description": "加载哪些插件。添加插件可能会降低应用程序的速度。在插件文档中详细了解需要启用哪些插件、插件类型或扫描选项。卸载插件将丢失您的设置。只有已禁用的插件才能卸载。", "LOADED_PLUGINS_name": "已加载插件", "LOG_LEVEL_description": "此设置将启用更详细的日志记录。对于调试写入数据库的事件很有用。", @@ -451,9 +451,9 @@ "Maintenance_Tool_backup_noti_text": "您确定要执行数据库备份吗?请确保当前没有正在运行的扫描。", "Maintenance_Tool_backup_text": "数据库备份以 zip 存档形式位于数据库目录中,以创建日期命名。备份数量没有上限。", "Maintenance_Tool_check_visible": "取消选择隐藏列。", - "Maintenance_Tool_clearSourceFields_selected": "", - "Maintenance_Tool_clearSourceFields_selected_noti": "", - "Maintenance_Tool_clearSourceFields_selected_text": "", + "Maintenance_Tool_clearSourceFields_selected": "清除选定源字段", + "Maintenance_Tool_clearSourceFields_selected_noti": "清除源", + "Maintenance_Tool_clearSourceFields_selected_text": "这将清除所选设备的所有源字段.此操作无法撤销", "Maintenance_Tool_darkmode": "切换模式(暗/亮)", "Maintenance_Tool_darkmode_noti": "切换模式", "Maintenance_Tool_darkmode_noti_text": "主题切换后,页面会尝试重新加载以激活更改。如有必要,必须清除缓存。", @@ -532,7 +532,7 @@ "NETWORK_DEVICE_TYPES_name": "网络设备类型", "Navigation_About": "关于", "Navigation_AppEvents": "应用事件", - "Navigation_ChangeHistory": "", + "Navigation_ChangeHistory": "变更日志", "Navigation_Devices": "设备", "Navigation_Donations": "捐款", "Navigation_Events": "事件", @@ -602,8 +602,8 @@ "PIALERT_WEB_PROTECTION_name": "启用登录", "PLUGINS_KEEP_HIST_description": "应保留多少个插件历史扫描结果条目(每个插件,而不是特定于设备)。", "PLUGINS_KEEP_HIST_name": "插件历史", - "PRAGMA_JOURNAL_SIZE_LIMIT_description": "", - "PRAGMA_JOURNAL_SIZE_LIMIT_name": "", + "PRAGMA_JOURNAL_SIZE_LIMIT_description": "SQLite WAL(预写日志)在触发自动检查点之前的最大大小(以MB为单位)。较低的值(10-20 MB)可减少磁盘/存储使用,但在扫描过程中会增加CPU使用率。较高的值(50-100 MB)可在操作过程中减少CPU峰值,但可能会占用更多RAM和磁盘空间。默认值50 MB兼顾了两者。适用于资源受限的系统,如带有SD卡的NAS设备。保存设置后,需要重新启动服务器以使更改生效。", + "PRAGMA_JOURNAL_SIZE_LIMIT_name": "预写入日志大小限制(MB)", "Plugins_DeleteAll": "全部删除(忽略过滤器)", "Plugins_Filters_Mac": "Mac 过滤器", "Plugins_History": "事件历史", @@ -647,8 +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": "", + "Scans_Paused": "扫描暂停", + "Scans_Resumed": "恢复扫描", "Setting_Override": "覆盖值", "Setting_Override_Description": "启用此选项将用上面指定的值覆盖应用程序提供的默认值。", "Settings_Metadata_Toggle": "显示/隐藏给定设置的元数据。", @@ -752,9 +752,9 @@ "VERSION_name": "本号或时间戳", "WF_Action_Add": "添加动作", "WF_Action_field": "字段", - "WF_Action_target": "", - "WF_Action_target_conditions": "", - "WF_Action_token_hint": "", + "WF_Action_target": "应用动作到", + "WF_Action_target_conditions": "目标设备条件", + "WF_Action_token_hint": "使用{{trigger.COLUMN}}来引用触发设备(e.g. {{trigger.devLastIP}}, {{trigger.devMac}})", "WF_Action_type": "类型", "WF_Action_value": "值", "WF_Actions": "动作", @@ -782,19 +782,19 @@ "add_icon_event_tooltip": "添加新图标", "add_option_event_tooltip": "添加新值", "copy_icons_event_tooltip": "覆盖所有同类设备的图标", - "device_history_col_changes": "", - "device_history_col_dropdown": "", - "device_history_col_source": "", - "device_history_col_time": "", - "device_history_empty_state": "", - "device_history_tab_title": "", + "device_history_col_changes": "选择通过以下方式跟踪哪些列 DEV_HIST_TRACKED设置", + "device_history_col_dropdown": "列", + "device_history_col_source": "被XX更改", + "device_history_col_time": "时间戳", + "device_history_empty_state": "在配置的保留窗口内未记录任何属性更改", + "device_history_tab_title": "变更日志", "device_history_table_title_changes": "更改", "devices_old": "刷新中…", - "gen_actions": "", - "gen_content": "", - "gen_device": "", - "gen_level": "", - "gen_read": "", + "gen_actions": "动作", + "gen_content": "内容", + "gen_device": "设备", + "gen_level": "等级", + "gen_read": "读取", "gen_timestamp": "时间戳", "general_event_description": "您触发的事件可能需要一段时间才能完成后台进程。一旦以下执行队列清空,执行就会结束(如果遇到问题,请检查错误日志)。

执行队列:", "general_event_title": "执行自组织网络事件", @@ -835,4 +835,4 @@ "settings_system_label": "系统", "settings_update_item_warning": "更新下面的值。请注意遵循先前的格式。未执行验证。", "test_event_tooltip": "在测试设置之前,请先保存更改。" -} \ No newline at end of file +} From af195d0bd1c0ebff6735d59080c454f339cba878 Mon Sep 17 00:00:00 2001 From: Hosted Weblate user 54392 Date: Fri, 28 Aug 2026 02:28:25 +0200 Subject: [PATCH 09/16] Translated using Weblate (Chinese (Simplified Han script)) Currently translated at 100.0% (836 of 836 strings) Translation: NetAlertX/core Translate-URL: https://hosted.weblate.org/projects/pialert/core/zh_Hans/ --- front/php/templates/language/zh_cn.json | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/front/php/templates/language/zh_cn.json b/front/php/templates/language/zh_cn.json index dd0462cbb..994fff21a 100644 --- a/front/php/templates/language/zh_cn.json +++ b/front/php/templates/language/zh_cn.json @@ -66,11 +66,11 @@ "CustProps_cant_remove": "无法移除,至少需要保留一个属性。", "DAYS_TO_KEEP_EVENTS_description": "这是维护设置。它指定将保留的事件条目的天数。所有较旧的事件将被定期删除。也适用于插件事件历史记录。", "DAYS_TO_KEEP_EVENTS_name": "删除早于", - "DEEP_SLEEP_description": "通过延长处理周期之间的空闲等待时间来降低 CPU 使用率。启用后,扫描任务可能会延迟最多 1 分钟,且用户界面(UI)的响应速度可能会降低", + "DEEP_SLEEP_description": "通过延长处理周期之间的空闲等待时间来降低 CPU 使用率。启用后,扫描任务可能会延迟最多 1 分钟,且用户界面(UI)的响应速度可能会降低。", "DEEP_SLEEP_name": "深度睡眠", - "DEV_HIST_DAYS_description": "保留设备字段变更历史的天数,设置为 0 可完全禁用审计引擎并减少资源使用", + "DEV_HIST_DAYS_description": "保留设备字段变更历史的天数,设置为 0 可完全禁用审计引擎并减少资源使用。", "DEV_HIST_DAYS_name": "列变更保留期限", - "DEV_HIST_TRACKED_description": "监控变更的设备列,添加或移除字段名称以控制变更日志中审核的内容", + "DEV_HIST_TRACKED_description": "监控变更的设备列,添加或移除字段名称控制变更日志中审核的内容。", "DEV_HIST_TRACKED_name": "跟踪列更改", "DISCOVER_PLUGINS_description": "禁用此选项可加快初始化和设置保存的速度。当禁用时,插件不会被发现,并且您无法将新插件添加到 LOADED_PLUGINS设置中。", "DISCOVER_PLUGINS_name": "发现插件", @@ -209,7 +209,7 @@ "Device_MultiEdit_MassActions": "谨慎操作:", "Device_MultiEdit_No_Devices": "未选择设备。", "Device_MultiEdit_Tooltip": "小心。 单击此按钮会将左侧的值应用到上面选择的所有设备。", - "Device_NextScan_Imminent": "即将发生的", + "Device_NextScan_Imminent": "即将发生的…", "Device_NextScan_In": "下一次扫描将大致进行于 ", "Device_NoData_Help": "如果扫描后设备未出现,请检查您的SCAN_SUBNETS设置以及 documentation.", "Device_NoData_Scanning": "等待首次扫描 - 这在初始设置后可能需要几分钟时间.", @@ -453,7 +453,7 @@ "Maintenance_Tool_check_visible": "取消选择隐藏列。", "Maintenance_Tool_clearSourceFields_selected": "清除选定源字段", "Maintenance_Tool_clearSourceFields_selected_noti": "清除源", - "Maintenance_Tool_clearSourceFields_selected_text": "这将清除所选设备的所有源字段.此操作无法撤销", + "Maintenance_Tool_clearSourceFields_selected_text": "这将清除所选设备的所有源字段,此操作无法撤销。", "Maintenance_Tool_darkmode": "切换模式(暗/亮)", "Maintenance_Tool_darkmode_noti": "切换模式", "Maintenance_Tool_darkmode_noti_text": "主题切换后,页面会尝试重新加载以激活更改。如有必要,必须清除缓存。", @@ -754,7 +754,7 @@ "WF_Action_field": "字段", "WF_Action_target": "应用动作到", "WF_Action_target_conditions": "目标设备条件", - "WF_Action_token_hint": "使用{{trigger.COLUMN}}来引用触发设备(e.g. {{trigger.devLastIP}}, {{trigger.devMac}})", + "WF_Action_token_hint": "用 {{trigger.COLUMN}} 来引用触发设备(比如, {{trigger.devLastIP}}{{trigger.devMac}})", "WF_Action_type": "类型", "WF_Action_value": "值", "WF_Actions": "动作", @@ -782,11 +782,11 @@ "add_icon_event_tooltip": "添加新图标", "add_option_event_tooltip": "添加新值", "copy_icons_event_tooltip": "覆盖所有同类设备的图标", - "device_history_col_changes": "选择通过以下方式跟踪哪些列 DEV_HIST_TRACKED设置", + "device_history_col_changes": "选择通过 DEV_HIST_TRACKED 设置追踪的列。", "device_history_col_dropdown": "列", - "device_history_col_source": "被XX更改", + "device_history_col_source": "修改方", "device_history_col_time": "时间戳", - "device_history_empty_state": "在配置的保留窗口内未记录任何属性更改", + "device_history_empty_state": "在配置的保留窗口内未记录到属性更改。", "device_history_tab_title": "变更日志", "device_history_table_title_changes": "更改", "devices_old": "刷新中…", From 18b8f7456345fff57dd62919768c8518f573083f Mon Sep 17 00:00:00 2001 From: Vaibhav Srivastava Date: Sat, 29 Aug 2026 14:11:58 +0530 Subject: [PATCH 10/16] docs: fix typo shoudl -> should Signed-off-by: Vaibhav Srivastava --- docs/ADVISORY_EYES_ON_GLASS.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/ADVISORY_EYES_ON_GLASS.md b/docs/ADVISORY_EYES_ON_GLASS.md index 41803b762..c97fb2afc 100644 --- a/docs/ADVISORY_EYES_ON_GLASS.md +++ b/docs/ADVISORY_EYES_ON_GLASS.md @@ -24,7 +24,7 @@ Static dashboards are the enemy of real-time response. NetAlertX allows you to f An MSP's focus is on what is *broken*, not what is working. Hide the noise to increase reaction speed. * **Hide Unnecessary Blocks:** Under UI Settings, disable dashboard blocks that don't provide immediate utility, such as **Online presence** or **Tiles**. -* **Hide virtual connections:** You can specify which relationships shoudl be hidden from the main view to remove any virtual devices that are not essential from your views. +* **Hide virtual connections:** You can specify which relationships should be hidden from the main view to remove any virtual devices that are not essential from your views. * **Browser Full-Screen:** Use the built-in "Full Screen" toggle in the top bar to remove browser chrome (URL bars/tabs) for a cleaner "Wallboard" look. ### 3. Creating Custom NOC Views From 8d5eab41b5266ef5e304f85beb58e3bc6f451919 Mon Sep 17 00:00:00 2001 From: mauricio-camayo Date: Sat, 29 Aug 2026 22:28:37 -0500 Subject: [PATCH 11/16] Add pihole_monitor plugin: combined Pi-hole device import + query anomaly detection Does two jobs against the same Pi-hole connection(s), instead of two separately configured plugins: 1. Device import - same job as the official PIHOLEAPI (pihole_api_scan) plugin, but supports an optional secondary/failover Pi-hole natively (accepts two sets of credentials instead of forking the official plugin, which hardcodes its settings-key prefix and doesn't support multiple instances). 2. Query anomaly detection - flags a device whose blocked-query count spikes well above its own recent rolling average (signature of malware/a compromised device beaconing out), keyed by MAC address (not IP, which changes under DHCP) and combined across both Pi-hole instances so a compromised device can't evade detection by switching resolvers. Notifications are delegated entirely to NetAlertX's own Watched/Report on mechanism - the plugin never calls a notification service directly. Live-tested against a two-Pi-hole home setup (v26.8.5) for several days, including two real bugs found and fixed during that testing (an offline-filtered device losing its MAC and falling back to a bare-IP identifier, and a boolean-expression flake8 style fix). --- server/plugins/pihole_monitor/README.md | 118 ++++ server/plugins/pihole_monitor/config.json | 553 ++++++++++++++++++ .../plugins/pihole_monitor/pihole_monitor.py | 451 ++++++++++++++ 3 files changed, 1122 insertions(+) create mode 100644 server/plugins/pihole_monitor/README.md create mode 100644 server/plugins/pihole_monitor/config.json create mode 100644 server/plugins/pihole_monitor/pihole_monitor.py diff --git a/server/plugins/pihole_monitor/README.md b/server/plugins/pihole_monitor/README.md new file mode 100644 index 000000000..ac9278751 --- /dev/null +++ b/server/plugins/pihole_monitor/README.md @@ -0,0 +1,118 @@ +## Overview - PIHOLEMON Plugin — Pi-hole Monitor + +The **PIHOLEMON** plugin does two jobs against the same Pi-hole connection(s): + +1. **Device import** - same job as the official **PIHOLEAPI** (`pihole_api_scan`) plugin: pulls the device list from Pi-hole and feeds it into NetAlertX's normal device-scanner pipeline, so a device Pi-hole knows about but NetAlertX doesn't gets created automatically. +2. **Query anomaly detection** - flags a device whose *blocked*-query count spikes well above its own recent average, the classic signature of malware or a compromised device beaconing out to blocklisted domains. This is deliberately not the same thing as device discovery: it's about a device you already know suddenly behaving differently, not a new device showing up. + +Both share one login per Pi-hole instance and one settings page, instead of being two separately configured pieces that happen to need the same credentials. + +Two design choices worth knowing about: + +* **Both a primary and an optional secondary/failover Pi-hole are checked, and their results combined.** Watching only one leaves an obvious blind spot for the anomaly detection - a device can simply point at the other resolver and never show up. Leave the secondary URL blank if you only run one Pi-hole; most setups do. +* **The anomaly baseline is keyed by MAC address, not IP.** DHCP-assigned IPs change; since this plugin already has the device list from Pi-hole itself, it resolves IP to MAC from that same data - no separate lookup needed for that part. A device owner, if you use NetAlertX's `devOwner` field, is looked up via NetAlertX's own GraphQL API purely to make the anomaly label friendlier; it's optional and never blocks import or detection if unavailable. + +### Why not just run two copies of PIHOLEAPI for two Pi-holes? + +We looked into this first. `pihole_api_scan.py` hardcodes its settings-key prefix (`PIHOLEAPI_URL`, `PIHOLEAPI_PASSWORD`, ...) as literal strings throughout the script rather than reading it from `config.json`. Duplicating the plugin folder gives you two copies that both read and write the *same* settings keys - not two independent instances - and NetAlertX's own plugin docs don't describe an officially supported way to run multiple instances of one plugin. Making a real second instance means forking the script and renaming every occurrence of the prefix by hand, which then has to be kept in sync manually with any upstream changes. This plugin exists so that isn't necessary: it accepts two sets of credentials natively and was designed to support that from the start - for both the device-import half and the anomaly-detection half. + +### Quick setup guide + +* You are running **Pi-hole v6** or newer on every instance you configure (this plugin uses `/api/auth`, `/api/network/devices`, and `/api/stats/top_clients`, none of which exist in v5). +* An **App Password** is generated on each Pi-hole (`Settings → Web Interface / API → App Password`) - recommended over using the admin login password directly. + +### Usage + +- Head to **Settings** > **Pi-hole Monitor** to fill in the values below. + +| Setting Key | Description | +| ---------------------------------- | --------------------------------------------------------------------------------------------------- | +| **PIHOLEMON_PRIMARY_URL** | Required. URL to your primary Pi-hole, e.g. `http://192.168.1.10/`. | +| **PIHOLEMON_PRIMARY_PASSWORD** | App Password (or admin password) for the primary Pi-hole. | +| **PIHOLEMON_SECONDARY_URL** | Optional. URL to a secondary/failover Pi-hole, e.g. `http://192.168.1.11:8080/`. Leave blank if you only run one. | +| **PIHOLEMON_SECONDARY_PASSWORD** | Only used if a secondary URL is set. | +| **PIHOLEMON_GET_OFFLINE** | Import devices even if not recently seen. Default off. | +| **PIHOLEMON_CONSIDER_ONLINE** | Seconds since last seen to still count a device as online. Default `300`. | +| **PIHOLEMON_VERIFY_SSL** | Verify TLS certificates. Disable for self-signed certs. Default off. | +| **PIHOLEMON_API_MAXCLIENTS** | Maximum devices requested **per instance**'s device list. Default `500`. | +| **PIHOLEMON_FAKE_MAC** | Generate a fake MAC from the IP for devices with a non-standard hardware address. Default off. | +| **PIHOLEMON_GRAPHQL_URL** | NetAlertX's own GraphQL endpoint, for the optional owner lookup. Defaults to `http://127.0.0.1:20212/graphql` (this plugin runs inside the NetAlertX container). | +| **PIHOLEMON_GRAPHQL_TOKEN** | API token for the GraphQL endpoint, if one is configured. | +| **PIHOLEMON_MULTIPLIER** | Flag a device when its blocked-query count exceeds this many times its own recent average. Default `4`. | +| **PIHOLEMON_MIN_BLOCKED** | Ignore devices below this absolute blocked-query count, even if the multiplier is exceeded. Default `20`. | +| **PIHOLEMON_HISTORY_LENGTH** | How many recent runs to keep per device for the rolling baseline. Default `28` (one week at the default 6-hour schedule). | +| **Watched** *(standard NetAlertX setting)* | Which columns count as "changed" for notification purposes. Defaults to `watchedValue4` (the anomaly/normal flag) only - the raw blocked-query count changes every run by design. | +| **Report on** *(standard NetAlertX setting)* | Which statuses actually notify. Defaults to `watched-changed` only, so you hear about it exactly when a device flips into (or out of) an anomaly. | + +This plugin does **not** send notifications on its own - it relies on NetAlertX's own core, which diffs the columns picked in **Watched** between runs and, on a match against **Report on**, dispatches through whichever publisher(s) you already have enabled under **Settings → Notifications** (ntfy, Apprise, email, Telegram, ...). + +One extra setting is required for that to actually reach you: NetAlertX's **Notification Processing** plugin (`NTFPRCS`) has its own **"Notify on"** setting (`NTFPRCS_INCLUDED_SECTIONS`), and its default value - `new_devices`, `down_devices`, `events` - does **not** include `plugins`. Without `plugins` in that list, this plugin's Watched/Report on matches are recorded correctly but never make it into a notification. Add `plugins` to `NTFPRCS_INCLUDED_SECTIONS` once, and it also covers any other plugin using the same mechanism, not just this one. + +The default text notification for the `plugins` section is a generic vertical `Header: Value` dump. For something more readable, set `NTFPRCS`'s **"Text Template: Plugins"** (`NTFPRCS_TEXT_TEMPLATE_plugins`) to something like: + +``` +{objectPrimaryId} [{watchedValue2}] @ {objectSecondaryId} → {watchedValue4} ({watchedValue3} blocked) +``` + +Only the columns the `plugins` section actually selects are available as placeholders (`{plugin}`, `{objectPrimaryId}`, `{objectSecondaryId}`, `{dateTimeChanged}`, `{watchedValue1-4}`, `{status}`) - `extra` (where this plugin's `owner`/`ratio` detail lives) isn't one of them, so that richer text is only visible on the device's page in NetAlertX, not in the notification itself. This template setting is global to `NTFPRCS`, so it affects any plugin using Watched/Report on, not just this one. + +If the same device (same MAC) is seen on both instances, the entry with the more recent "last seen" timestamp wins - it isn't imported twice, and its blocked-query counts from both instances are summed, not compared separately. + +### Testing the notification pipeline end-to-end + +Waiting for real, organic beaconing traffic to confirm notifications actually arrive isn't practical. The clean way to force a `normal` → `anomaly` transition on demand, without touching any internal state file by hand: + +1. Temporarily set `PIHOLEMON_MULTIPLIER` to something like `1.01` and `PIHOLEMON_MIN_BLOCKED` to `1`. +2. Run the plugin (wait for its schedule, or trigger it manually from the UI). +3. Any device with a baseline and *any* blocked traffic should now flip to `anomaly` - confirming the whole chain: Watched/Report on match → `Plugins_Events` row → NetAlertX's Notification Processing (`NTFPRCS`, needs `plugins` in `NTFPRCS_INCLUDED_SECTIONS`, see above) → your configured publisher. +4. Set `PIHOLEMON_MULTIPLIER`/`PIHOLEMON_MIN_BLOCKED` back to their real values afterward - left at the test values, everything with any traffic at all reads as an anomaly. + +### ⚠️ Troubleshooting + +--- + +#### ❌ Authentication failed / no data from a Pi-hole instance + +* Confirm the URL includes the scheme (`http://`/`https://`) and, if not on the default port, the port too - e.g. `http://192.168.1.10/` ✔, `http://192.168.1.10/admin` ❌. +* Confirm that instance is running **Pi-hole v6**, not v5. +* SSL verification matches your setup (disable for self-signed certificates). +* Try the App Password by hand first: `curl -X POST /api/auth -d '{"password":""}'` should return a `session.sid`. +* Check the plugin log for `[PIHOLEMON]