diff --git a/docs/PERFORMANCE.md b/docs/PERFORMANCE.md index adb192c3c..a3008f70d 100755 --- a/docs/PERFORMANCE.md +++ b/docs/PERFORMANCE.md @@ -16,7 +16,7 @@ The application performs regular maintenance and database cleanup. If these task ### Database and Log File Size -A large database or oversized log files can impact performance. You can check database and table sizes on the **Maintenance** page. +A large database or oversized log files can impact performance. You can check database and table sizes on the **System Info → Storage** page. ![DB size check](./img/PERFORMANCE/db_size_check.png) @@ -32,6 +32,26 @@ Please note that excessively large log files will increase memory consumption. D --- +## Tracking NetAlertX's Own Resource Usage Over Time + +The **System Info → Performance** page graphs NetAlertX's own process CPU%, memory, disk I/O, and scan duration over hour/day/week/month ranges, sampled once per scan cycle. This turns "did a recent change make things slower" into a question you can answer by looking at real history, instead of guessing from what a change *should* cost. + +**What each chart shows:** + +* **CPU %** - average CPU utilisation over the scan cycle (`100%` = one fully-utilized CPU core; can exceed `100%` with multi-threaded work). This is an average over the whole cycle, not an instantaneous reading - a short burst of high usage followed by idle time reads as a lower percentage than the peak. +* **Memory Usage** - resident memory (RSS - the physical RAM the process currently holds), in MB, sampled once per cycle. +* **IO Read / IO Write** - bytes read/written by the NetAlertX process itself during that cycle, in MB. This only ever reflects NetAlertX's own direct I/O (its own DB/cache writes) - a plugin's own subprocess doing heavy disk I/O (e.g. a scanner writing its own result file) is not captured; this is a structural limitation of how the OS attributes child-process I/O to the parent, not a gap that can be closed later. +* **Scan Duration** - wall-clock time the scan cycle took, in seconds. + +> [!NOTE] +> The IO numbers come from the kernel's own per-process I/O accounting (`/proc//io`), which the kernel itself documents as an approximation. On some Docker storage drivers this can be badly inaccurate - confirmed on the deprecated `aufs` driver (common on older Synology NAS Docker setups), where union-filesystem copy-up writes get mis-attributed and can report write volumes many times larger than what's actually reaching disk (`docker info | grep -i "storage driver"` shows which driver you're on; `aufs` is a known-bad case, alongside its own `blkio throttle` warnings confirming the kernel's block-I/O accounting isn't properly wired up for it). If the IO chart shows large, consistent write numbers that don't match actual database growth (check the DB/WAL size on **System Info → Storage**), that's this known driver limitation, not a real cost. Newer Docker storage drivers (`overlay2`) don't have this problem. + +* Controlled by the `MAINT_PERF_DAYS` setting (part of the `MAINT` plugin) - the number of days of history to retain, `0` disables collection. Defaults to `30`. +* History cannot be backfilled - data starts accumulating from whenever collection is enabled. +* `GET /health` also exposes live `process_cpu_pct`/`process_rss_mb` fields for external monitoring, independent of this history. + +--- + ## Maintenance Plugins Two plugins help maintain the system’s performance: diff --git a/docs/img/PERFORMANCE/db_size_check.png b/docs/img/PERFORMANCE/db_size_check.png index e3886e5e4..a62b40379 100755 Binary files a/docs/img/PERFORMANCE/db_size_check.png and b/docs/img/PERFORMANCE/db_size_check.png differ diff --git a/front/css/app.css b/front/css/app.css index c1daf7b34..fdc2afbfc 100755 --- a/front/css/app.css +++ b/front/css/app.css @@ -2597,7 +2597,7 @@ textarea[readonly], width: 100%; } -#panInitCheck +#panInitCheck.active { display: flow-root; } diff --git a/front/js/graph_resource_history.js b/front/js/graph_resource_history.js new file mode 100644 index 000000000..41874f8c8 --- /dev/null +++ b/front/js/graph_resource_history.js @@ -0,0 +1,162 @@ +// Chart.js instances, kept so a range switch destroys the old chart before +// building a new one on the same canvas (Chart.js throws otherwise). +var resourceHistoryChartInstances = {}; + +/** + * Fetches Resource_History data for the given range ('hour'/'day'/'week'/'month') + * and renders the four Performance-tab charts, or shows the disabled-state + * message when no rows exist yet (collection off, or just enabled). + */ +function initResourceHistoryGraphs(range) { + $.get('php/server/query_json.php', { file: `table_resource_history_${range}.json`, nocache: Date.now() }, function (res) { + var rows = (res && res.data) ? res.data : []; + + if (rows.length === 0) { + $('#resourceHistoryCharts').addClass('myhidden'); + $('#resourceHistoryDisabledMessage').removeClass('myhidden'); + return; + } + + $('#resourceHistoryDisabledMessage').addClass('myhidden'); + $('#resourceHistoryCharts').removeClass('myhidden'); + + var labels = []; + var cpuData = []; + var rssData = []; + var ioReadData = []; + var ioWriteData = []; + var durationData = []; + var tickFailed = []; + + function round2(n) { + return n == null ? null : Math.round(n * 100) / 100; + } + + rows.forEach(function (entry) { + // hour/day rows carry resDateTime (raw); week/month rows carry bucket + // (hourly rollup) instead - see const.py's sql_resource_history_* queries. + var ts = entry.resDateTime || entry.bucket; + labels.push(localizeTimestamp(ts).slice(0, 16)); + cpuData.push(round2(entry.resCpuPercent)); + rssData.push(round2(entry.resRssMb)); + // Bytes -> MB and ms -> s: raw units from the DB aren't a readable axis scale. + ioReadData.push(entry.resIoReadBytes != null ? round2(entry.resIoReadBytes / (1024 * 1024)) : null); + ioWriteData.push(entry.resIoWriteBytes != null ? round2(entry.resIoWriteBytes / (1024 * 1024)) : null); + durationData.push(entry.resScanDurationMs != null ? round2(entry.resScanDurationMs / 1000) : null); + tickFailed.push(entry.resTickFailed == 1); + }); + + renderResourceHistoryCharts(labels, cpuData, rssData, ioReadData, ioWriteData, durationData, tickFailed); + }).fail(function () { + console.error('Error fetching resource history data.'); + }); +} + +/** + * Builds/replaces the four small-multiple Resource_History charts (CPU%, + * memory usage MB, IO read+write MB, scan duration seconds) on the same time + * axis. Rows written from the tick-failure `finally` path (resTickFailed = 1) + * are rendered as visually distinct points rather than plain data, since + * their numbers may reflect a truncated, crash-adjacent sample. + */ +function renderResourceHistoryCharts(labels, cpuData, rssData, ioReadData, ioWriteData, durationData, tickFailed) { + var normalColor = "rgba(0, 166, 89, .8)"; + var failedColor = "#dd4b39"; + + function pointColors(baseColor) { + return tickFailed.map(function (failed) { return failed ? failedColor : baseColor; }); + } + + function destroyIfExists(key) { + if (resourceHistoryChartInstances[key]) { + resourceHistoryChartInstances[key].destroy(); + } + } + + var commonScales = { + yAxes: [{ + ticks: { beginAtZero: true, fontColor: '#A0A0A0' }, + gridLines: { color: "rgba(0, 0, 0, 0)" }, + }], + xAxes: [{ + ticks: { fontColor: '#A0A0A0' }, + gridLines: { color: "rgba(0, 0, 0, 0)" }, + }], + }; + + destroyIfExists('cpu'); + resourceHistoryChartInstances.cpu = new Chart("ResourceCpuChart", { + type: "line", + data: { + labels: labels, + datasets: [{ + label: getString("Systeminfo_Performance_CPU"), + data: cpuData, + borderColor: normalColor, + backgroundColor: "rgba(0, 166, 89, .2)", + pointBackgroundColor: pointColors(normalColor), + fill: true, + }], + }, + options: { legend: { display: true }, scales: commonScales, maintainAspectRatio: false, responsive: true }, + }); + + destroyIfExists('rss'); + resourceHistoryChartInstances.rss = new Chart("ResourceRssChart", { + type: "line", + data: { + labels: labels, + datasets: [{ + label: getString("Systeminfo_Performance_Memory"), + data: rssData, + borderColor: "#3c8dbc", + backgroundColor: "rgba(60, 141, 188, .2)", + pointBackgroundColor: pointColors("#3c8dbc"), + fill: true, + }], + }, + options: { legend: { display: true }, scales: commonScales, maintainAspectRatio: false, responsive: true }, + }); + + destroyIfExists('io'); + resourceHistoryChartInstances.io = new Chart("ResourceIoChart", { + type: "line", + data: { + labels: labels, + datasets: [ + { + label: getString("Systeminfo_Performance_IO_Read"), + data: ioReadData, + borderColor: "#00a65a", + pointBackgroundColor: pointColors("#00a65a"), + fill: false, + }, + { + label: getString("Systeminfo_Performance_IO_Write"), + data: ioWriteData, + borderColor: "#f39c12", + pointBackgroundColor: pointColors("#f39c12"), + fill: false, + }, + ], + }, + options: { legend: { display: true }, scales: commonScales, maintainAspectRatio: false, responsive: true }, + }); + + destroyIfExists('duration'); + resourceHistoryChartInstances.duration = new Chart("ResourceDurationChart", { + type: "line", + data: { + labels: labels, + datasets: [{ + label: getString("Systeminfo_Performance_Duration"), + data: durationData, + borderColor: "#b2b6be", + backgroundColor: "rgba(178, 182, 190, .2)", + pointBackgroundColor: pointColors("#b2b6be"), + fill: true, + }], + }, + options: { legend: { display: true }, scales: commonScales, maintainAspectRatio: false, responsive: true }, + }); +} diff --git a/front/maintenance.php b/front/maintenance.php index 650da5817..7a3d2b247 100755 --- a/front/maintenance.php +++ b/front/maintenance.php @@ -12,43 +12,6 @@ showSpinner(); -query($query); - -// Iterate over the tables and get the row counts -while ($row = $result->fetchArray(SQLITE3_ASSOC)) { - $tableName = $row['name']; - $query = "SELECT COUNT(*) FROM $tableName"; - $countResult = $db->querySingle($query); - $tableSizesHTML = $tableSizesHTML . "$tableName ($countResult), "; -} - -// Close the database connection -$db->close(); - - -?> -
@@ -85,30 +48,6 @@ $db->close();
-
-
-
- -
-
-
-
-
- (wal: ) -
-
-
-
-
- -
-
-
-
-
- -
-
diff --git a/front/php/templates/header.php b/front/php/templates/header.php index c606c24c2..481764115 100755 --- a/front/php/templates/header.php +++ b/front/php/templates/header.php @@ -474,6 +474,9 @@
  • +
  • + +
  • diff --git a/front/php/templates/language/ar_ar.json b/front/php/templates/language/ar_ar.json index 080095b50..2b63a2e82 100644 --- a/front/php/templates/language/ar_ar.json +++ b/front/php/templates/language/ar_ar.json @@ -341,6 +341,7 @@ "Gen_Copy": "نسخ", "Gen_CopyToClipboard": "انسخ إلى الحافظة", "Gen_DataUpdatedUITakesTime": "تم تحديث البيانات. قد يستغرق تحديث واجهة المستخدم بعض الوقت", + "Gen_Day": "", "Gen_Delete": "حذف", "Gen_DeleteAll": "حذف الكل", "Gen_Description": "الوصف", @@ -349,9 +350,11 @@ "Gen_Filter": "تصفية", "Gen_Flapping": "", "Gen_Generate": "إنشاء", + "Gen_Hour": "", "Gen_InvalidMac": "عنوان MAC غير صالح.", "Gen_Invalid_Value": "تم إدخال قيمة غير صالحة", "Gen_LockedDB": "قاعدة البيانات مقفلة", + "Gen_Month": "", "Gen_NetworkMask": "قناع الشبكة", "Gen_New": "", "Gen_Next": "", @@ -383,6 +386,7 @@ "Gen_Update_Value": "تحديث القيمة", "Gen_ValidIcon": "أيقونة صالحة", "Gen_Warning": "تحذير", + "Gen_Week": "", "Gen_Work_In_Progress": "جاري العمل...", "Gen_create_new_device": "إنشاء جهاز جديد", "Gen_create_new_device_info": "معلومات إنشاء جهاز جديد", @@ -712,6 +716,14 @@ "Systeminfo_Network_Server_Query": "استعلام الخادم", "Systeminfo_Network_Server_Query_String": "نص استعلام الخادم", "Systeminfo_Network_Server_Version": "إصدار الخادم", + "Systeminfo_Performance": "", + "Systeminfo_Performance_CPU": "", + "Systeminfo_Performance_Disabled": "", + "Systeminfo_Performance_Duration": "", + "Systeminfo_Performance_IO_Caption": "", + "Systeminfo_Performance_IO_Read": "", + "Systeminfo_Performance_IO_Write": "", + "Systeminfo_Performance_Memory": "", "Systeminfo_Services": "الخدمات", "Systeminfo_Services_Description": "وصف الخدمات", "Systeminfo_Services_Name": "اسم الخدمة", diff --git a/front/php/templates/language/ca_ca.json b/front/php/templates/language/ca_ca.json index eb7bc0c18..ea763c977 100644 --- a/front/php/templates/language/ca_ca.json +++ b/front/php/templates/language/ca_ca.json @@ -341,6 +341,7 @@ "Gen_Copy": "Executar", "Gen_CopyToClipboard": "Copia a portapapers", "Gen_DataUpdatedUITakesTime": "D'acord - Pot passar una estona perquè la interfície d'usuari s'actualitzi si s'està executant una exploració.", + "Gen_Day": "", "Gen_Delete": "Esborrar", "Gen_DeleteAll": "Esborrar tot", "Gen_Description": "Descripció", @@ -349,9 +350,11 @@ "Gen_Filter": "Filtrar", "Gen_Flapping": "Flapping", "Gen_Generate": "Generar", + "Gen_Hour": "", "Gen_InvalidMac": "Mac address invàlida.", "Gen_Invalid_Value": "S'ha introduït un valor incorrecte", "Gen_LockedDB": "ERROR - DB podria estar bloquejada - Fes servir F12 Eines desenvolupament -> Consola o provar-ho més tard.", + "Gen_Month": "", "Gen_NetworkMask": "Màscara de xarxa", "Gen_New": "Nou", "Gen_Next": "", @@ -383,6 +386,7 @@ "Gen_Update_Value": "Actualitzar Valor", "Gen_ValidIcon": "", "Gen_Warning": "Advertència", + "Gen_Week": "", "Gen_Work_In_Progress": "Work in progress, un bon moment per retroalimentació a https://github.com/netalertx/NetAlertX/issues", "Gen_create_new_device": "Nou dispositiu", "Gen_create_new_device_info": "Els dispositius són típicament descobert utilitzant plugins. Tanmateix, en certs casos, pots necessitar afegir dispositius a mà. Per explorar els temes concrets comproveu la documentació de Xarxes Remotes.", @@ -712,6 +716,14 @@ "Systeminfo_Network_Server_Query": "Consulta del servidor:", "Systeminfo_Network_Server_Query_String": "Cap cadena de consulta", "Systeminfo_Network_Server_Version": "Versió del servidor:", + "Systeminfo_Performance": "", + "Systeminfo_Performance_CPU": "", + "Systeminfo_Performance_Disabled": "", + "Systeminfo_Performance_Duration": "", + "Systeminfo_Performance_IO_Caption": "", + "Systeminfo_Performance_IO_Read": "", + "Systeminfo_Performance_IO_Write": "", + "Systeminfo_Performance_Memory": "", "Systeminfo_Services": "Serveis", "Systeminfo_Services_Description": "Descripció del servei", "Systeminfo_Services_Name": "Nom del servei", diff --git a/front/php/templates/language/cs_cz.json b/front/php/templates/language/cs_cz.json index c712856f6..8fd37258d 100644 --- a/front/php/templates/language/cs_cz.json +++ b/front/php/templates/language/cs_cz.json @@ -341,6 +341,7 @@ "Gen_Copy": "Spustit", "Gen_CopyToClipboard": "Zkopírovat do schránky", "Gen_DataUpdatedUITakesTime": "OK – může zabrat chvíli aktualizovat rozhraní, pokud probíhá sken.", + "Gen_Day": "", "Gen_Delete": "Smazat", "Gen_DeleteAll": "Smazat vše", "Gen_Description": "Popis", @@ -349,9 +350,11 @@ "Gen_Filter": "Filtr", "Gen_Flapping": "Nestabilní", "Gen_Generate": "Nechat vytvořit", + "Gen_Hour": "", "Gen_InvalidMac": "Neplatná MAC adresa.", "Gen_Invalid_Value": "Zadána neplatná hodnota", "Gen_LockedDB": "CHYBA – databáze je možná zamčená – Podívejte se do F12 -> Nástroje pro vývojáře -> Konzole. Nebo to zkuste později.", + "Gen_Month": "", "Gen_NetworkMask": "Maska sítě", "Gen_New": "Nový", "Gen_Next": "Další", @@ -383,6 +386,7 @@ "Gen_Update_Value": "Aktualizovat hodnotu", "Gen_ValidIcon": "", "Gen_Warning": "Varování", + "Gen_Week": "", "Gen_Work_In_Progress": "Rozpracováno, ideální čas pro zpětnou vazbu na https://github.com/netalertx/NetAlertX/issues", "Gen_create_new_device": "Nové zařízení", "Gen_create_new_device_info": "Zařízení jsou vyhledávána pomocí zásuvných modulů. V určitých případech je však potřeba přidat zařízení ručně. Pokud chcete prozkoumat konkrétní scénáře, podívejte se do Dokumentace ke vzdáleným sítím.", @@ -712,6 +716,14 @@ "Systeminfo_Network_Server_Query": "Dotaz serveru:", "Systeminfo_Network_Server_Query_String": "Žádný řetězec dotazu", "Systeminfo_Network_Server_Version": "Verze serveru:", + "Systeminfo_Performance": "", + "Systeminfo_Performance_CPU": "", + "Systeminfo_Performance_Disabled": "", + "Systeminfo_Performance_Duration": "", + "Systeminfo_Performance_IO_Caption": "", + "Systeminfo_Performance_IO_Read": "", + "Systeminfo_Performance_IO_Write": "", + "Systeminfo_Performance_Memory": "", "Systeminfo_Services": "Služby", "Systeminfo_Services_Description": "Popis služby", "Systeminfo_Services_Name": "Název služby", @@ -840,4 +852,4 @@ "settings_system_label": "Systém", "settings_update_item_warning": "Zaktualizujte níže uvedenou hodnotu. Dávejte pozor, ať je dodržen předchozí formát. Ověřování správnosti není prováděno.", "test_event_tooltip": "Než budete svá nastavení zkoušet, nejprve vámi provedené změny uložte." -} +} \ No newline at end of file diff --git a/front/php/templates/language/de_de.json b/front/php/templates/language/de_de.json index 8e9b5d064..f88b3d39d 100644 --- a/front/php/templates/language/de_de.json +++ b/front/php/templates/language/de_de.json @@ -345,6 +345,7 @@ "Gen_Copy": "Ausführen", "Gen_CopyToClipboard": "In die Zwischenablage kopieren", "Gen_DataUpdatedUITakesTime": "OK – Es kann einen Moment dauern, bis die Benutzeroberfläche aktualisiert wird, während ein Scan ausgeführt wird.", + "Gen_Day": "", "Gen_Delete": "Löschen", "Gen_DeleteAll": "Alles löschen", "Gen_Description": "Beschreibung", @@ -353,9 +354,11 @@ "Gen_Filter": "Filter", "Gen_Flapping": "", "Gen_Generate": "Generieren", + "Gen_Hour": "", "Gen_InvalidMac": "Ungültige MAC-Adresse.", "Gen_Invalid_Value": "Ein ungültiger Wert wurde eingegeben", "Gen_LockedDB": "ERROR - DB eventuell gesperrt - Nutze die Konsole in den Entwickler Werkzeugen (F12) zur Überprüfung oder probiere es später erneut.", + "Gen_Month": "", "Gen_NetworkMask": "Netzmaske", "Gen_New": "Neu", "Gen_Next": "", @@ -387,6 +390,7 @@ "Gen_Update_Value": "Wert aktualisieren", "Gen_ValidIcon": "", "Gen_Warning": "Warnung", + "Gen_Week": "", "Gen_Work_In_Progress": "Keine Finalversion, feedback bitte unter: https://github.com/netalertx/NetAlertX/issues", "Gen_create_new_device": "Neues Gerät", "Gen_create_new_device_info": "Geräte werden normalerweise über Plugins gefunden. In Ausnahmefällen kann es nötig sein, sie manuell hinzuzufügen. Konkrete Szenarien sind in der Dokumentation über entfernte Netzwerke zu finden.", @@ -773,6 +777,14 @@ "Systeminfo_Network_Server_Query": "Serverabfrage:", "Systeminfo_Network_Server_Query_String": "Keine Abfragezeichenfolge", "Systeminfo_Network_Server_Version": "Serverversion:", + "Systeminfo_Performance": "", + "Systeminfo_Performance_CPU": "", + "Systeminfo_Performance_Disabled": "", + "Systeminfo_Performance_Duration": "", + "Systeminfo_Performance_IO_Caption": "", + "Systeminfo_Performance_IO_Read": "", + "Systeminfo_Performance_IO_Write": "", + "Systeminfo_Performance_Memory": "", "Systeminfo_Services": "Dienste", "Systeminfo_Services_Description": "Dienstbeschreibung", "Systeminfo_Services_Name": "Dienstname", diff --git a/front/php/templates/language/en_us.json b/front/php/templates/language/en_us.json index 64928c814..5776b9c2a 100755 --- a/front/php/templates/language/en_us.json +++ b/front/php/templates/language/en_us.json @@ -341,6 +341,7 @@ "Gen_Copy": "Run", "Gen_CopyToClipboard": "Copy to clipboard", "Gen_DataUpdatedUITakesTime": "OK - It may take a while for the UI to update if a scan is running.", + "Gen_Day": "Day", "Gen_Delete": "Delete", "Gen_DeleteAll": "Delete all", "Gen_Description": "Description", @@ -349,9 +350,11 @@ "Gen_Filter": "Filter", "Gen_Flapping": "Flapping", "Gen_Generate": "Generate", + "Gen_Hour": "Hour", "Gen_InvalidMac": "Invalid Mac address.", "Gen_Invalid_Value": "An invalid value was entered", "Gen_LockedDB": "ERROR - DB might be locked - Check F12 Dev tools -> Console or try later.", + "Gen_Month": "Month", "Gen_NetworkMask": "Network mask", "Gen_New": "New", "Gen_Next": "Next", @@ -383,6 +386,7 @@ "Gen_Update_Value": "Update Value", "Gen_ValidIcon": "", "Gen_Warning": "Warning", + "Gen_Week": "Week", "Gen_Work_In_Progress": "Work in progress, good time to feedback on https://github.com/netalertx/NetAlertX/issues", "Gen_create_new_device": "New device", "Gen_create_new_device_info": "Devices are typically discovered using plugins. However, in certain cases, you may need to add devices manually. To explore specific scenarios check the Remote Networks documentation.", @@ -712,6 +716,14 @@ "Systeminfo_Network_Server_Query": "Server query:", "Systeminfo_Network_Server_Query_String": "No query string", "Systeminfo_Network_Server_Version": "Server Version:", + "Systeminfo_Performance": "Performance", + "Systeminfo_Performance_CPU": "CPU %", + "Systeminfo_Performance_Disabled": "Resource history collection is currently disabled. Enable it in Settings.", + "Systeminfo_Performance_Duration": "Scan Duration (s)", + "Systeminfo_Performance_IO_Caption": "Direct NetAlertX process I/O only - plugin subprocess I/O (e.g. a scanner writing its own result file) is not included.", + "Systeminfo_Performance_IO_Read": "IO Read (MB)", + "Systeminfo_Performance_IO_Write": "IO Write (MB)", + "Systeminfo_Performance_Memory": "Memory Usage (MB)", "Systeminfo_Services": "Services", "Systeminfo_Services_Description": "Service Description", "Systeminfo_Services_Name": "Service Name", diff --git a/front/php/templates/language/es_es.json b/front/php/templates/language/es_es.json index 288679c17..e036bdef0 100644 --- a/front/php/templates/language/es_es.json +++ b/front/php/templates/language/es_es.json @@ -343,6 +343,7 @@ "Gen_Copy": "Ejecutar", "Gen_CopyToClipboard": "Copiar al portapapeles", "Gen_DataUpdatedUITakesTime": "Correcto - La interfaz puede tardar en actualizarse si se está ejecutando un escaneo.", + "Gen_Day": "", "Gen_Delete": "Eliminar", "Gen_DeleteAll": "Eliminar todo", "Gen_Description": "Descripción", @@ -351,9 +352,11 @@ "Gen_Filter": "Filtro", "Gen_Flapping": "Agitado", "Gen_Generate": "Generar", + "Gen_Hour": "", "Gen_InvalidMac": "Dirección MAC inválida.", "Gen_Invalid_Value": "Un valor inválido fue ingresado", "Gen_LockedDB": "Fallo - La base de datos puede estar bloqueada - Pulsa F1 -> Ajustes de desarrolladores -> Consola o prueba más tarde.", + "Gen_Month": "", "Gen_NetworkMask": "Máscara de red", "Gen_New": "Nuevo", "Gen_Next": "", @@ -385,6 +388,7 @@ "Gen_Update_Value": "Actualizar valor", "Gen_ValidIcon": "", "Gen_Warning": "Advertencia", + "Gen_Week": "", "Gen_Work_In_Progress": "Trabajo en curso, un buen momento para hacer comentarios en https://github.com/netalertx/NetAlertX/issues", "Gen_create_new_device": "Nuevo dispositivo", "Gen_create_new_device_info": "Los dispositivos se suelen descubrir utilizando plugins. Sin embargo, en algunos casos, es posible que necesite agregar dispositivos manualmente. Para explorar escenarios específicos, consulte la documentación Redes remotas.", @@ -772,6 +776,14 @@ "Systeminfo_Network_Server_Query": "Consulta del servidor:", "Systeminfo_Network_Server_Query_String": "Sin cadena de consulta", "Systeminfo_Network_Server_Version": "Versión del servidor:", + "Systeminfo_Performance": "", + "Systeminfo_Performance_CPU": "", + "Systeminfo_Performance_Disabled": "", + "Systeminfo_Performance_Duration": "", + "Systeminfo_Performance_IO_Caption": "", + "Systeminfo_Performance_IO_Read": "", + "Systeminfo_Performance_IO_Write": "", + "Systeminfo_Performance_Memory": "", "Systeminfo_Services": "Servicios", "Systeminfo_Services_Description": "Descripción del servicio", "Systeminfo_Services_Name": "Nombre del servicio", diff --git a/front/php/templates/language/fa_fa.json b/front/php/templates/language/fa_fa.json index d1abab71a..c12073226 100644 --- a/front/php/templates/language/fa_fa.json +++ b/front/php/templates/language/fa_fa.json @@ -341,6 +341,7 @@ "Gen_Copy": "", "Gen_CopyToClipboard": "", "Gen_DataUpdatedUITakesTime": "", + "Gen_Day": "", "Gen_Delete": "", "Gen_DeleteAll": "", "Gen_Description": "", @@ -349,9 +350,11 @@ "Gen_Filter": "", "Gen_Flapping": "", "Gen_Generate": "", + "Gen_Hour": "", "Gen_InvalidMac": "", "Gen_Invalid_Value": "", "Gen_LockedDB": "", + "Gen_Month": "", "Gen_NetworkMask": "", "Gen_New": "", "Gen_Next": "", @@ -383,6 +386,7 @@ "Gen_Update_Value": "", "Gen_ValidIcon": "", "Gen_Warning": "", + "Gen_Week": "", "Gen_Work_In_Progress": "", "Gen_create_new_device": "", "Gen_create_new_device_info": "", @@ -712,6 +716,14 @@ "Systeminfo_Network_Server_Query": "", "Systeminfo_Network_Server_Query_String": "", "Systeminfo_Network_Server_Version": "", + "Systeminfo_Performance": "", + "Systeminfo_Performance_CPU": "", + "Systeminfo_Performance_Disabled": "", + "Systeminfo_Performance_Duration": "", + "Systeminfo_Performance_IO_Caption": "", + "Systeminfo_Performance_IO_Read": "", + "Systeminfo_Performance_IO_Write": "", + "Systeminfo_Performance_Memory": "", "Systeminfo_Services": "", "Systeminfo_Services_Description": "", "Systeminfo_Services_Name": "", diff --git a/front/php/templates/language/fi_fi.json b/front/php/templates/language/fi_fi.json index 1d27b99ae..e27e3e250 100644 --- a/front/php/templates/language/fi_fi.json +++ b/front/php/templates/language/fi_fi.json @@ -341,6 +341,7 @@ "Gen_Copy": "", "Gen_CopyToClipboard": "", "Gen_DataUpdatedUITakesTime": "", + "Gen_Day": "", "Gen_Delete": "", "Gen_DeleteAll": "", "Gen_Description": "", @@ -349,9 +350,11 @@ "Gen_Filter": "", "Gen_Flapping": "", "Gen_Generate": "", + "Gen_Hour": "", "Gen_InvalidMac": "", "Gen_Invalid_Value": "", "Gen_LockedDB": "", + "Gen_Month": "", "Gen_NetworkMask": "", "Gen_New": "", "Gen_Next": "", @@ -383,6 +386,7 @@ "Gen_Update_Value": "", "Gen_ValidIcon": "", "Gen_Warning": "", + "Gen_Week": "", "Gen_Work_In_Progress": "", "Gen_create_new_device": "", "Gen_create_new_device_info": "", @@ -712,6 +716,14 @@ "Systeminfo_Network_Server_Query": "", "Systeminfo_Network_Server_Query_String": "", "Systeminfo_Network_Server_Version": "", + "Systeminfo_Performance": "", + "Systeminfo_Performance_CPU": "", + "Systeminfo_Performance_Disabled": "", + "Systeminfo_Performance_Duration": "", + "Systeminfo_Performance_IO_Caption": "", + "Systeminfo_Performance_IO_Read": "", + "Systeminfo_Performance_IO_Write": "", + "Systeminfo_Performance_Memory": "", "Systeminfo_Services": "", "Systeminfo_Services_Description": "", "Systeminfo_Services_Name": "", diff --git a/front/php/templates/language/fr_fr.json b/front/php/templates/language/fr_fr.json index d41d5726b..9a72e6edc 100644 --- a/front/php/templates/language/fr_fr.json +++ b/front/php/templates/language/fr_fr.json @@ -341,6 +341,7 @@ "Gen_Copy": "Lancer", "Gen_CopyToClipboard": "Copier vers le presse-papier", "Gen_DataUpdatedUITakesTime": "OK - cela peut prendre du temps à l'interface pour se mettre à jour si un scan est en cours.", + "Gen_Day": "", "Gen_Delete": "Supprimer", "Gen_DeleteAll": "Supprimer tous", "Gen_Description": "Description", @@ -349,9 +350,11 @@ "Gen_Filter": "Filtrer", "Gen_Flapping": "Bagotage", "Gen_Generate": "Générer", + "Gen_Hour": "", "Gen_InvalidMac": "Adresse MAC invalide.", "Gen_Invalid_Value": "Une valeur invalide a été renseignée", "Gen_LockedDB": "Erreur - La base de données est peut-être verrouillée - Vérifier avec les outils de dév via F12 -> Console ou essayer plus tard.", + "Gen_Month": "", "Gen_NetworkMask": "Masque réseau", "Gen_New": "Nouveau", "Gen_Next": "Suivant", @@ -383,6 +386,7 @@ "Gen_Update_Value": "Valeur à mettre à jour", "Gen_ValidIcon": "", "Gen_Warning": "Avertissement", + "Gen_Week": "", "Gen_Work_In_Progress": "Travaux en cours, c'est le bon moment pour faire un retour via la liste d'anomalies sur Github https://github.com/netalertx/NetAlertX/issues", "Gen_create_new_device": "Nouvel appareil", "Gen_create_new_device_info": "Les appareils sont souvent découverts à l'aide d'un plugin. Cependant, dans certains cas, vous pouvez ajouter manuellement les appareils. Pour explorer des scénarios spécifiques, consulter la documentation des réseaux distants.", @@ -712,6 +716,14 @@ "Systeminfo_Network_Server_Query": "Requête du serveur :", "Systeminfo_Network_Server_Query_String": "Aucune chaîne de requête", "Systeminfo_Network_Server_Version": "Version du serveur :", + "Systeminfo_Performance": "", + "Systeminfo_Performance_CPU": "", + "Systeminfo_Performance_Disabled": "", + "Systeminfo_Performance_Duration": "", + "Systeminfo_Performance_IO_Caption": "", + "Systeminfo_Performance_IO_Read": "", + "Systeminfo_Performance_IO_Write": "", + "Systeminfo_Performance_Memory": "", "Systeminfo_Services": "Services", "Systeminfo_Services_Description": "Description du service", "Systeminfo_Services_Name": "Nom du service", diff --git a/front/php/templates/language/he_il.json b/front/php/templates/language/he_il.json index 1d27b99ae..e27e3e250 100644 --- a/front/php/templates/language/he_il.json +++ b/front/php/templates/language/he_il.json @@ -341,6 +341,7 @@ "Gen_Copy": "", "Gen_CopyToClipboard": "", "Gen_DataUpdatedUITakesTime": "", + "Gen_Day": "", "Gen_Delete": "", "Gen_DeleteAll": "", "Gen_Description": "", @@ -349,9 +350,11 @@ "Gen_Filter": "", "Gen_Flapping": "", "Gen_Generate": "", + "Gen_Hour": "", "Gen_InvalidMac": "", "Gen_Invalid_Value": "", "Gen_LockedDB": "", + "Gen_Month": "", "Gen_NetworkMask": "", "Gen_New": "", "Gen_Next": "", @@ -383,6 +386,7 @@ "Gen_Update_Value": "", "Gen_ValidIcon": "", "Gen_Warning": "", + "Gen_Week": "", "Gen_Work_In_Progress": "", "Gen_create_new_device": "", "Gen_create_new_device_info": "", @@ -712,6 +716,14 @@ "Systeminfo_Network_Server_Query": "", "Systeminfo_Network_Server_Query_String": "", "Systeminfo_Network_Server_Version": "", + "Systeminfo_Performance": "", + "Systeminfo_Performance_CPU": "", + "Systeminfo_Performance_Disabled": "", + "Systeminfo_Performance_Duration": "", + "Systeminfo_Performance_IO_Caption": "", + "Systeminfo_Performance_IO_Read": "", + "Systeminfo_Performance_IO_Write": "", + "Systeminfo_Performance_Memory": "", "Systeminfo_Services": "", "Systeminfo_Services_Description": "", "Systeminfo_Services_Name": "", diff --git a/front/php/templates/language/hu_hu.json b/front/php/templates/language/hu_hu.json index 1d27b99ae..e27e3e250 100644 --- a/front/php/templates/language/hu_hu.json +++ b/front/php/templates/language/hu_hu.json @@ -341,6 +341,7 @@ "Gen_Copy": "", "Gen_CopyToClipboard": "", "Gen_DataUpdatedUITakesTime": "", + "Gen_Day": "", "Gen_Delete": "", "Gen_DeleteAll": "", "Gen_Description": "", @@ -349,9 +350,11 @@ "Gen_Filter": "", "Gen_Flapping": "", "Gen_Generate": "", + "Gen_Hour": "", "Gen_InvalidMac": "", "Gen_Invalid_Value": "", "Gen_LockedDB": "", + "Gen_Month": "", "Gen_NetworkMask": "", "Gen_New": "", "Gen_Next": "", @@ -383,6 +386,7 @@ "Gen_Update_Value": "", "Gen_ValidIcon": "", "Gen_Warning": "", + "Gen_Week": "", "Gen_Work_In_Progress": "", "Gen_create_new_device": "", "Gen_create_new_device_info": "", @@ -712,6 +716,14 @@ "Systeminfo_Network_Server_Query": "", "Systeminfo_Network_Server_Query_String": "", "Systeminfo_Network_Server_Version": "", + "Systeminfo_Performance": "", + "Systeminfo_Performance_CPU": "", + "Systeminfo_Performance_Disabled": "", + "Systeminfo_Performance_Duration": "", + "Systeminfo_Performance_IO_Caption": "", + "Systeminfo_Performance_IO_Read": "", + "Systeminfo_Performance_IO_Write": "", + "Systeminfo_Performance_Memory": "", "Systeminfo_Services": "", "Systeminfo_Services_Description": "", "Systeminfo_Services_Name": "", diff --git a/front/php/templates/language/id_id.json b/front/php/templates/language/id_id.json index 1d27b99ae..e27e3e250 100644 --- a/front/php/templates/language/id_id.json +++ b/front/php/templates/language/id_id.json @@ -341,6 +341,7 @@ "Gen_Copy": "", "Gen_CopyToClipboard": "", "Gen_DataUpdatedUITakesTime": "", + "Gen_Day": "", "Gen_Delete": "", "Gen_DeleteAll": "", "Gen_Description": "", @@ -349,9 +350,11 @@ "Gen_Filter": "", "Gen_Flapping": "", "Gen_Generate": "", + "Gen_Hour": "", "Gen_InvalidMac": "", "Gen_Invalid_Value": "", "Gen_LockedDB": "", + "Gen_Month": "", "Gen_NetworkMask": "", "Gen_New": "", "Gen_Next": "", @@ -383,6 +386,7 @@ "Gen_Update_Value": "", "Gen_ValidIcon": "", "Gen_Warning": "", + "Gen_Week": "", "Gen_Work_In_Progress": "", "Gen_create_new_device": "", "Gen_create_new_device_info": "", @@ -712,6 +716,14 @@ "Systeminfo_Network_Server_Query": "", "Systeminfo_Network_Server_Query_String": "", "Systeminfo_Network_Server_Version": "", + "Systeminfo_Performance": "", + "Systeminfo_Performance_CPU": "", + "Systeminfo_Performance_Disabled": "", + "Systeminfo_Performance_Duration": "", + "Systeminfo_Performance_IO_Caption": "", + "Systeminfo_Performance_IO_Read": "", + "Systeminfo_Performance_IO_Write": "", + "Systeminfo_Performance_Memory": "", "Systeminfo_Services": "", "Systeminfo_Services_Description": "", "Systeminfo_Services_Name": "", diff --git a/front/php/templates/language/it_it.json b/front/php/templates/language/it_it.json index 5eb6c30ed..d60d3e161 100644 --- a/front/php/templates/language/it_it.json +++ b/front/php/templates/language/it_it.json @@ -341,6 +341,7 @@ "Gen_Copy": "Esegui", "Gen_CopyToClipboard": "Copia negli appunti", "Gen_DataUpdatedUITakesTime": "OK: l'aggiornamento dell'interfaccia utente potrebbe richiedere del tempo se è in esecuzione una scansione.", + "Gen_Day": "", "Gen_Delete": "Elimina", "Gen_DeleteAll": "Elimina tutti", "Gen_Description": "Descrizione", @@ -349,9 +350,11 @@ "Gen_Filter": "Filtro", "Gen_Flapping": "Flapping", "Gen_Generate": "Genera", + "Gen_Hour": "", "Gen_InvalidMac": "Indirizzo Mac non valido.", "Gen_Invalid_Value": "È stato inserito un valore non valido", "Gen_LockedDB": "ERRORE: il DB potrebbe essere bloccato, controlla F12 Strumenti di sviluppo -> Console o riprova più tardi.", + "Gen_Month": "", "Gen_NetworkMask": "Maschera di rete", "Gen_New": "Nuovo", "Gen_Next": "Successivo", @@ -383,6 +386,7 @@ "Gen_Update_Value": "Aggiorna valore", "Gen_ValidIcon": "", "Gen_Warning": "Avviso", + "Gen_Week": "", "Gen_Work_In_Progress": "Lavori in corso, è quindi un buon momento per un feedback su https://github.com/netalertx/NetAlertX/issues", "Gen_create_new_device": "Nuovo dispositivo", "Gen_create_new_device_info": "I dispositivi vengono generalmente rilevati utilizzando plugin. Tuttavia, in alcuni casi, potrebbe essere necessario aggiungere manualmente i dispositivi. Per esplorare scenari specifici, consulta la documentazione sulle reti remote.", @@ -712,6 +716,14 @@ "Systeminfo_Network_Server_Query": "Interrogazione server:", "Systeminfo_Network_Server_Query_String": "Nessuna stringa di interrogazione", "Systeminfo_Network_Server_Version": "Versione server:", + "Systeminfo_Performance": "", + "Systeminfo_Performance_CPU": "", + "Systeminfo_Performance_Disabled": "", + "Systeminfo_Performance_Duration": "", + "Systeminfo_Performance_IO_Caption": "", + "Systeminfo_Performance_IO_Read": "", + "Systeminfo_Performance_IO_Write": "", + "Systeminfo_Performance_Memory": "", "Systeminfo_Services": "Servizi", "Systeminfo_Services_Description": "Descrizione servizio", "Systeminfo_Services_Name": "Nome servizio", diff --git a/front/php/templates/language/ja_jp.json b/front/php/templates/language/ja_jp.json index 0f4fd6500..bc61e88d0 100644 --- a/front/php/templates/language/ja_jp.json +++ b/front/php/templates/language/ja_jp.json @@ -341,6 +341,7 @@ "Gen_Copy": "実行", "Gen_CopyToClipboard": "クリップボードにコピー", "Gen_DataUpdatedUITakesTime": "OK - スキャン実行中は、UIの更新に時間がかかることがあります。", + "Gen_Day": "", "Gen_Delete": "削除", "Gen_DeleteAll": "全削除", "Gen_Description": "説明", @@ -349,9 +350,11 @@ "Gen_Filter": "フィルター", "Gen_Flapping": "フラッピング", "Gen_Generate": "生成", + "Gen_Hour": "", "Gen_InvalidMac": "無効なMacアドレス。", "Gen_Invalid_Value": "無効な値が入力されました", "Gen_LockedDB": "エラー - DBがロックされている可能性があります - F12で開発者ツール→コンソールを確認するか、後で試してください。", + "Gen_Month": "", "Gen_NetworkMask": "ネットワークマスク", "Gen_New": "New", "Gen_Next": "", @@ -383,6 +386,7 @@ "Gen_Update_Value": "更新値", "Gen_ValidIcon": "", "Gen_Warning": "警告", + "Gen_Week": "", "Gen_Work_In_Progress": "作業中、https://github.com/netalertx/NetAlertX/issues へのフィードバックの好機です", "Gen_create_new_device": "新規デバイス", "Gen_create_new_device_info": "デバイスは通常、プラグイン を使用して検出されます。ただし、特定のケースでは手動でデバイスを追加する必要がある場合があります。具体的なシナリオについては、リモートネットワークドキュメント を参照してください。", @@ -712,6 +716,14 @@ "Systeminfo_Network_Server_Query": "サーバークエリ:", "Systeminfo_Network_Server_Query_String": "クエリ文字列なし", "Systeminfo_Network_Server_Version": "サーバーバージョン:", + "Systeminfo_Performance": "", + "Systeminfo_Performance_CPU": "", + "Systeminfo_Performance_Disabled": "", + "Systeminfo_Performance_Duration": "", + "Systeminfo_Performance_IO_Caption": "", + "Systeminfo_Performance_IO_Read": "", + "Systeminfo_Performance_IO_Write": "", + "Systeminfo_Performance_Memory": "", "Systeminfo_Services": "サービス", "Systeminfo_Services_Description": "サービス内容", "Systeminfo_Services_Name": "サービス名", diff --git a/front/php/templates/language/nb_no.json b/front/php/templates/language/nb_no.json index 91ed3953f..5f951ca99 100644 --- a/front/php/templates/language/nb_no.json +++ b/front/php/templates/language/nb_no.json @@ -341,6 +341,7 @@ "Gen_Copy": "Kjør", "Gen_CopyToClipboard": "", "Gen_DataUpdatedUITakesTime": "OK - Det kan ta litt tid før brukergrensesnittet oppdateres hvis en skanning kjøres.", + "Gen_Day": "", "Gen_Delete": "Slett", "Gen_DeleteAll": "Slett alle", "Gen_Description": "", @@ -349,9 +350,11 @@ "Gen_Filter": "Filter", "Gen_Flapping": "", "Gen_Generate": "", + "Gen_Hour": "", "Gen_InvalidMac": "", "Gen_Invalid_Value": "", "Gen_LockedDB": "FEIL - DB kan være låst - Sjekk F12 Dev tools -> Konsoll eller prøv senere.", + "Gen_Month": "", "Gen_NetworkMask": "", "Gen_New": "", "Gen_Next": "", @@ -383,6 +386,7 @@ "Gen_Update_Value": "Oppdater verdi", "Gen_ValidIcon": "", "Gen_Warning": "Advarsel", + "Gen_Week": "", "Gen_Work_In_Progress": "Work in progress, gjerne kom med tilbakemeldinger på https://github.com/netalertx/NetAlertX/issues", "Gen_create_new_device": "", "Gen_create_new_device_info": "", @@ -712,6 +716,14 @@ "Systeminfo_Network_Server_Query": "Serverforespørsel:", "Systeminfo_Network_Server_Query_String": "Ingen spørringsstreng", "Systeminfo_Network_Server_Version": "Server versjon:", + "Systeminfo_Performance": "", + "Systeminfo_Performance_CPU": "", + "Systeminfo_Performance_Disabled": "", + "Systeminfo_Performance_Duration": "", + "Systeminfo_Performance_IO_Caption": "", + "Systeminfo_Performance_IO_Read": "", + "Systeminfo_Performance_IO_Write": "", + "Systeminfo_Performance_Memory": "", "Systeminfo_Services": "Tjenester", "Systeminfo_Services_Description": "Tjeneste beskrivelse", "Systeminfo_Services_Name": "Tjeneste navn", diff --git a/front/php/templates/language/pl_pl.json b/front/php/templates/language/pl_pl.json index 7fc79bcca..06e966ecc 100644 --- a/front/php/templates/language/pl_pl.json +++ b/front/php/templates/language/pl_pl.json @@ -341,6 +341,7 @@ "Gen_Copy": "Wykonaj", "Gen_CopyToClipboard": "", "Gen_DataUpdatedUITakesTime": "OK – Może to potrwać chwilę, zanim interfejs użytkownika się zaktualizuje, jeśli trwa skan.", + "Gen_Day": "", "Gen_Delete": "Usuń", "Gen_DeleteAll": "Usuń wszystko", "Gen_Description": "Opis", @@ -349,9 +350,11 @@ "Gen_Filter": "Filtr", "Gen_Flapping": "", "Gen_Generate": "Wygeneruj", + "Gen_Hour": "", "Gen_InvalidMac": "", "Gen_Invalid_Value": "", "Gen_LockedDB": "Błąd - Baza danych może być zablokowana - Sprawdź narzędzia deweloperskie F12 -> Konsola lub spróbuj później.", + "Gen_Month": "", "Gen_NetworkMask": "", "Gen_New": "", "Gen_Next": "", @@ -383,6 +386,7 @@ "Gen_Update_Value": "Aktualizuj wartość", "Gen_ValidIcon": "", "Gen_Warning": "Ostrzeżenie", + "Gen_Week": "", "Gen_Work_In_Progress": "Prace w toku, to dobry moment na przesłanie opinii na https://github.com/netalertx/NetAlertX/issues", "Gen_create_new_device": "Nowe urządzenie", "Gen_create_new_device_info": "Urządzenia są zazwyczaj wykrywane za pomocą wtyczek. Jednak w niektórych przypadkach może być konieczne dodanie urządzeń ręcznie. Aby zapoznać się z konkretnymi scenariuszami, sprawdź dokumentację dotyczącą sieci zdalnych.", @@ -712,6 +716,14 @@ "Systeminfo_Network_Server_Query": "Zapytanie serwerowe:", "Systeminfo_Network_Server_Query_String": "Brak ciągu zapytania", "Systeminfo_Network_Server_Version": "Wersja serwera:", + "Systeminfo_Performance": "", + "Systeminfo_Performance_CPU": "", + "Systeminfo_Performance_Disabled": "", + "Systeminfo_Performance_Duration": "", + "Systeminfo_Performance_IO_Caption": "", + "Systeminfo_Performance_IO_Read": "", + "Systeminfo_Performance_IO_Write": "", + "Systeminfo_Performance_Memory": "", "Systeminfo_Services": "Usługi", "Systeminfo_Services_Description": "Opis usługi", "Systeminfo_Services_Name": "Nazwa usługi", diff --git a/front/php/templates/language/pt_br.json b/front/php/templates/language/pt_br.json index 78310fad7..307d35592 100644 --- a/front/php/templates/language/pt_br.json +++ b/front/php/templates/language/pt_br.json @@ -341,6 +341,7 @@ "Gen_Copy": "Executar", "Gen_CopyToClipboard": "", "Gen_DataUpdatedUITakesTime": "OK - Pode levar um tempo para a interface do usuário ser atualizada se uma verificação estiver em execução.", + "Gen_Day": "", "Gen_Delete": "Excluir", "Gen_DeleteAll": "Excluir todos", "Gen_Description": "Descrição", @@ -349,9 +350,11 @@ "Gen_Filter": "Filtro", "Gen_Flapping": "", "Gen_Generate": "Gerar", + "Gen_Hour": "", "Gen_InvalidMac": "", "Gen_Invalid_Value": "", "Gen_LockedDB": "ERRO - O banco de dados pode estar bloqueado - Verifique F12 Ferramentas de desenvolvimento -> Console ou tente mais tarde.", + "Gen_Month": "", "Gen_NetworkMask": "", "Gen_New": "", "Gen_Next": "", @@ -383,6 +386,7 @@ "Gen_Update_Value": "Atualizar valor", "Gen_ValidIcon": "", "Gen_Warning": "Aviso", + "Gen_Week": "", "Gen_Work_In_Progress": "Trabalho em andamento, um bom momento para enviar feedback em https://github.com/netalertx/NetAlertX/issues", "Gen_create_new_device": "Novo dispositivo", "Gen_create_new_device_info": "Os dispositivos são normalmente descobertos usando plugins. No entanto, em certos casos, pode ser necessário adicionar dispositivos manualmente. Para explorar cenários específicos, verifique a documentação de Redes Remotas.", @@ -712,6 +716,14 @@ "Systeminfo_Network_Server_Query": "", "Systeminfo_Network_Server_Query_String": "", "Systeminfo_Network_Server_Version": "", + "Systeminfo_Performance": "", + "Systeminfo_Performance_CPU": "", + "Systeminfo_Performance_Disabled": "", + "Systeminfo_Performance_Duration": "", + "Systeminfo_Performance_IO_Caption": "", + "Systeminfo_Performance_IO_Read": "", + "Systeminfo_Performance_IO_Write": "", + "Systeminfo_Performance_Memory": "", "Systeminfo_Services": "", "Systeminfo_Services_Description": "", "Systeminfo_Services_Name": "", diff --git a/front/php/templates/language/pt_pt.json b/front/php/templates/language/pt_pt.json index 3779e9f56..e4e55b06b 100644 --- a/front/php/templates/language/pt_pt.json +++ b/front/php/templates/language/pt_pt.json @@ -341,6 +341,7 @@ "Gen_Copy": "Executar", "Gen_CopyToClipboard": "Copiar para a área de transferência", "Gen_DataUpdatedUITakesTime": "OK - Pode levar um tempo para a interface do utilizador ser atualizada se uma verificação estiver em execução.", + "Gen_Day": "", "Gen_Delete": "Apagar", "Gen_DeleteAll": "Apagar todos", "Gen_Description": "Descrição", @@ -349,9 +350,11 @@ "Gen_Filter": "Filtro", "Gen_Flapping": "Flapping", "Gen_Generate": "Gerar", + "Gen_Hour": "", "Gen_InvalidMac": "Endereço Mac inválido.", "Gen_Invalid_Value": "Um valor inválido foi inserido", "Gen_LockedDB": "ERRO - A base de dados pode estar bloqueada - Verifique F12 Ferramentas de desenvolvimento -> Console ou tente mais tarde.", + "Gen_Month": "", "Gen_NetworkMask": "Máscara de Rede", "Gen_New": "Novo", "Gen_Next": "", @@ -383,6 +386,7 @@ "Gen_Update_Value": "Atualizar valor", "Gen_ValidIcon": "", "Gen_Warning": "Aviso", + "Gen_Week": "", "Gen_Work_In_Progress": "Trabalho em andamento, um bom momento para enviar feedback em https://github.com/netalertx/NetAlertX/issues", "Gen_create_new_device": "Novo dispositivo", "Gen_create_new_device_info": "Os dispositivos são normalmente descobertos usando plugins. No entanto, em certos casos, pode ser necessário adicionar dispositivos manualmente. Para explorar cenários específicos, verifique a documentação de Redes Remotas.", @@ -712,6 +716,14 @@ "Systeminfo_Network_Server_Query": "Consulta de servidor:", "Systeminfo_Network_Server_Query_String": "Nenhuma string de consulta", "Systeminfo_Network_Server_Version": "Versão do Servidor:", + "Systeminfo_Performance": "", + "Systeminfo_Performance_CPU": "", + "Systeminfo_Performance_Disabled": "", + "Systeminfo_Performance_Duration": "", + "Systeminfo_Performance_IO_Caption": "", + "Systeminfo_Performance_IO_Read": "", + "Systeminfo_Performance_IO_Write": "", + "Systeminfo_Performance_Memory": "", "Systeminfo_Services": "Serviços", "Systeminfo_Services_Description": "Descrição do Serviço", "Systeminfo_Services_Name": "Nome do Serviço", diff --git a/front/php/templates/language/ru_ru.json b/front/php/templates/language/ru_ru.json index 4d82237d1..84c199428 100644 --- a/front/php/templates/language/ru_ru.json +++ b/front/php/templates/language/ru_ru.json @@ -341,6 +341,7 @@ "Gen_Copy": "Запустить", "Gen_CopyToClipboard": "Копировать в буфер обмена", "Gen_DataUpdatedUITakesTime": "ОК - Обновление UI может занять некоторое время, если сканирование выполняется.", + "Gen_Day": "", "Gen_Delete": "Удалить", "Gen_DeleteAll": "Удалить все", "Gen_Description": "Описание", @@ -349,9 +350,11 @@ "Gen_Filter": "Фильтр", "Gen_Flapping": "Нестабильный", "Gen_Generate": "Генерировать", + "Gen_Hour": "", "Gen_InvalidMac": "Неверный Mac-адрес.", "Gen_Invalid_Value": "Введено некорректное значение", "Gen_LockedDB": "ОШИБКА - Возможно, база данных заблокирована. Проверьте инструменты разработчика F12 -> Консоль или повторите попытку позже.", + "Gen_Month": "", "Gen_NetworkMask": "Маска сети", "Gen_New": "Новый", "Gen_Next": "Следующий", @@ -383,6 +386,7 @@ "Gen_Update_Value": "Обновить значение", "Gen_ValidIcon": "", "Gen_Warning": "Предупреждение", + "Gen_Week": "", "Gen_Work_In_Progress": "Работа продолжается, самое время оставить отзыв на https://github.com/netalertx/NetAlertX/issues", "Gen_create_new_device": "Новое устройство", "Gen_create_new_device_info": "Устройства обычно обнаруживаются с помощью плагинов. Однако в некоторых случаях вам может потребоваться добавить устройства вручную. Для изучения конкретных сценариев ознакомьтесь с документацией Remote Networks.", @@ -712,6 +716,14 @@ "Systeminfo_Network_Server_Query": "Запрос сервера:", "Systeminfo_Network_Server_Query_String": "Нет строки запроса", "Systeminfo_Network_Server_Version": "Версия сервера:", + "Systeminfo_Performance": "", + "Systeminfo_Performance_CPU": "", + "Systeminfo_Performance_Disabled": "", + "Systeminfo_Performance_Duration": "", + "Systeminfo_Performance_IO_Caption": "", + "Systeminfo_Performance_IO_Read": "", + "Systeminfo_Performance_IO_Write": "", + "Systeminfo_Performance_Memory": "", "Systeminfo_Services": "Службы", "Systeminfo_Services_Description": "Описание службы", "Systeminfo_Services_Name": "Имя службы", diff --git a/front/php/templates/language/sv_sv.json b/front/php/templates/language/sv_sv.json index 1d27b99ae..e27e3e250 100644 --- a/front/php/templates/language/sv_sv.json +++ b/front/php/templates/language/sv_sv.json @@ -341,6 +341,7 @@ "Gen_Copy": "", "Gen_CopyToClipboard": "", "Gen_DataUpdatedUITakesTime": "", + "Gen_Day": "", "Gen_Delete": "", "Gen_DeleteAll": "", "Gen_Description": "", @@ -349,9 +350,11 @@ "Gen_Filter": "", "Gen_Flapping": "", "Gen_Generate": "", + "Gen_Hour": "", "Gen_InvalidMac": "", "Gen_Invalid_Value": "", "Gen_LockedDB": "", + "Gen_Month": "", "Gen_NetworkMask": "", "Gen_New": "", "Gen_Next": "", @@ -383,6 +386,7 @@ "Gen_Update_Value": "", "Gen_ValidIcon": "", "Gen_Warning": "", + "Gen_Week": "", "Gen_Work_In_Progress": "", "Gen_create_new_device": "", "Gen_create_new_device_info": "", @@ -712,6 +716,14 @@ "Systeminfo_Network_Server_Query": "", "Systeminfo_Network_Server_Query_String": "", "Systeminfo_Network_Server_Version": "", + "Systeminfo_Performance": "", + "Systeminfo_Performance_CPU": "", + "Systeminfo_Performance_Disabled": "", + "Systeminfo_Performance_Duration": "", + "Systeminfo_Performance_IO_Caption": "", + "Systeminfo_Performance_IO_Read": "", + "Systeminfo_Performance_IO_Write": "", + "Systeminfo_Performance_Memory": "", "Systeminfo_Services": "", "Systeminfo_Services_Description": "", "Systeminfo_Services_Name": "", diff --git a/front/php/templates/language/tr_tr.json b/front/php/templates/language/tr_tr.json index 6f255d006..5ac870753 100644 --- a/front/php/templates/language/tr_tr.json +++ b/front/php/templates/language/tr_tr.json @@ -341,6 +341,7 @@ "Gen_Copy": "Çalıştır", "Gen_CopyToClipboard": "", "Gen_DataUpdatedUITakesTime": "TAMAM - Eğer bir tarama çalışıyorsa arayüzün güncellenmesi biraz zaman alabilir.", + "Gen_Day": "", "Gen_Delete": "Sil", "Gen_DeleteAll": "Tümünü sil", "Gen_Description": "Açıklama", @@ -349,9 +350,11 @@ "Gen_Filter": "Filtre", "Gen_Flapping": "", "Gen_Generate": "Oluştur", + "Gen_Hour": "", "Gen_InvalidMac": "", "Gen_Invalid_Value": "", "Gen_LockedDB": "HATA - Veritabanı kilitlenmiş olabilir - F12 Geliştirici araçlarını -> Konsol kısmını kontrol edin veya daha sonra tekrar deneyin.", + "Gen_Month": "", "Gen_NetworkMask": "", "Gen_New": "", "Gen_Next": "", @@ -383,6 +386,7 @@ "Gen_Update_Value": "Değeri Güncelle", "Gen_ValidIcon": "", "Gen_Warning": "Uyarı", + "Gen_Week": "", "Gen_Work_In_Progress": "Çalışma devam ediyor, geri bildirimde bulunmak için iyi bir zaman: https://github.com/netalertx/NetAlertX/issues", "Gen_create_new_device": "Yeni cihaz", "Gen_create_new_device_info": "Cihazlar genellikle eklentiler kullanılarak keşfedilir. Ancak, bazı durumlarda cihazları manuel olarak eklemeniz gerekebilir. Belirli senaryoları incelemek için Uzak Ağlar belgelerini kontrol edin.", @@ -712,6 +716,14 @@ "Systeminfo_Network_Server_Query": "", "Systeminfo_Network_Server_Query_String": "", "Systeminfo_Network_Server_Version": "", + "Systeminfo_Performance": "", + "Systeminfo_Performance_CPU": "", + "Systeminfo_Performance_Disabled": "", + "Systeminfo_Performance_Duration": "", + "Systeminfo_Performance_IO_Caption": "", + "Systeminfo_Performance_IO_Read": "", + "Systeminfo_Performance_IO_Write": "", + "Systeminfo_Performance_Memory": "", "Systeminfo_Services": "", "Systeminfo_Services_Description": "", "Systeminfo_Services_Name": "", diff --git a/front/php/templates/language/uk_ua.json b/front/php/templates/language/uk_ua.json index 13c598cd2..ec10abf90 100644 --- a/front/php/templates/language/uk_ua.json +++ b/front/php/templates/language/uk_ua.json @@ -341,6 +341,7 @@ "Gen_Copy": "Запустити", "Gen_CopyToClipboard": "Копіювати в буфер обміну", "Gen_DataUpdatedUITakesTime": "Добре. Оновлення інтерфейсу може зайняти деякий час, якщо сканування виконується.", + "Gen_Day": "", "Gen_Delete": "Видалити", "Gen_DeleteAll": "Видалити все", "Gen_Description": "Опис", @@ -349,9 +350,11 @@ "Gen_Filter": "Фільтр", "Gen_Flapping": "", "Gen_Generate": "Генерувати", + "Gen_Hour": "", "Gen_InvalidMac": "Недійсна Mac-адреса.", "Gen_Invalid_Value": "Введено недійсне значення", "Gen_LockedDB": "ПОМИЛКА – БД може бути заблоковано – перевірте F12 Інструменти розробника -> Консоль або спробуйте пізніше.", + "Gen_Month": "", "Gen_NetworkMask": "Маска мережі", "Gen_New": "", "Gen_Next": "", @@ -383,6 +386,7 @@ "Gen_Update_Value": "Оновити значення", "Gen_ValidIcon": "", "Gen_Warning": "Попередження", + "Gen_Week": "", "Gen_Work_In_Progress": "Робота триває, час залишити відгук на https://github.com/netalertx/NetAlertX/issues", "Gen_create_new_device": "Новий пристрій", "Gen_create_new_device_info": "Пристрої зазвичай виявляють за допомогою плагінів. Однак у деяких випадках може знадобитися додати пристрої вручну. Щоб дослідити конкретні сценарії, перегляньте документацію щодо віддалених мереж.", @@ -712,6 +716,14 @@ "Systeminfo_Network_Server_Query": "Запит до сервера:", "Systeminfo_Network_Server_Query_String": "Немає рядка запиту", "Systeminfo_Network_Server_Version": "Версія сервера:", + "Systeminfo_Performance": "", + "Systeminfo_Performance_CPU": "", + "Systeminfo_Performance_Disabled": "", + "Systeminfo_Performance_Duration": "", + "Systeminfo_Performance_IO_Caption": "", + "Systeminfo_Performance_IO_Read": "", + "Systeminfo_Performance_IO_Write": "", + "Systeminfo_Performance_Memory": "", "Systeminfo_Services": "Послуги", "Systeminfo_Services_Description": "Опис послуги", "Systeminfo_Services_Name": "Назва служби", diff --git a/front/php/templates/language/vi_vn.json b/front/php/templates/language/vi_vn.json index 1d27b99ae..e27e3e250 100644 --- a/front/php/templates/language/vi_vn.json +++ b/front/php/templates/language/vi_vn.json @@ -341,6 +341,7 @@ "Gen_Copy": "", "Gen_CopyToClipboard": "", "Gen_DataUpdatedUITakesTime": "", + "Gen_Day": "", "Gen_Delete": "", "Gen_DeleteAll": "", "Gen_Description": "", @@ -349,9 +350,11 @@ "Gen_Filter": "", "Gen_Flapping": "", "Gen_Generate": "", + "Gen_Hour": "", "Gen_InvalidMac": "", "Gen_Invalid_Value": "", "Gen_LockedDB": "", + "Gen_Month": "", "Gen_NetworkMask": "", "Gen_New": "", "Gen_Next": "", @@ -383,6 +386,7 @@ "Gen_Update_Value": "", "Gen_ValidIcon": "", "Gen_Warning": "", + "Gen_Week": "", "Gen_Work_In_Progress": "", "Gen_create_new_device": "", "Gen_create_new_device_info": "", @@ -712,6 +716,14 @@ "Systeminfo_Network_Server_Query": "", "Systeminfo_Network_Server_Query_String": "", "Systeminfo_Network_Server_Version": "", + "Systeminfo_Performance": "", + "Systeminfo_Performance_CPU": "", + "Systeminfo_Performance_Disabled": "", + "Systeminfo_Performance_Duration": "", + "Systeminfo_Performance_IO_Caption": "", + "Systeminfo_Performance_IO_Read": "", + "Systeminfo_Performance_IO_Write": "", + "Systeminfo_Performance_Memory": "", "Systeminfo_Services": "", "Systeminfo_Services_Description": "", "Systeminfo_Services_Name": "", diff --git a/front/php/templates/language/zh_cn.json b/front/php/templates/language/zh_cn.json index 036da1f53..174f4521a 100644 --- a/front/php/templates/language/zh_cn.json +++ b/front/php/templates/language/zh_cn.json @@ -341,6 +341,7 @@ "Gen_Copy": "运行", "Gen_CopyToClipboard": "复制到剪贴板", "Gen_DataUpdatedUITakesTime": "好的 - 如果扫描正在运行,UI 可能需要一段时间才能更新。", + "Gen_Day": "", "Gen_Delete": "删除", "Gen_DeleteAll": "全部删除", "Gen_Description": "描述", @@ -349,9 +350,11 @@ "Gen_Filter": "筛选", "Gen_Flapping": "跳动", "Gen_Generate": "生成", + "Gen_Hour": "", "Gen_InvalidMac": "无效的 Mac 地址。", "Gen_Invalid_Value": "输入了无效的值", "Gen_LockedDB": "错误 - DB 可能被锁定 - 检查 F12 开发工具 -> 控制台或稍后重试。", + "Gen_Month": "", "Gen_NetworkMask": "网络掩码", "Gen_New": "新的", "Gen_Next": "", @@ -383,6 +386,7 @@ "Gen_Update_Value": "更新值", "Gen_ValidIcon": "", "Gen_Warning": "警告", + "Gen_Week": "", "Gen_Work_In_Progress": "工作正在进行中,欢迎在 https://github.com/netalertx/NetAlertX/issues 上反馈", "Gen_create_new_device": "新设备", "Gen_create_new_device_info": "通常使用plugins来发现设备。但是,在某些情况下,您可能需要手动添加设备。要探索特定场景,请查看远程网络文档。", @@ -712,6 +716,14 @@ "Systeminfo_Network_Server_Query": "服务器查询:", "Systeminfo_Network_Server_Query_String": "没有查询到字符串", "Systeminfo_Network_Server_Version": "服务器版本:", + "Systeminfo_Performance": "", + "Systeminfo_Performance_CPU": "", + "Systeminfo_Performance_Disabled": "", + "Systeminfo_Performance_Duration": "", + "Systeminfo_Performance_IO_Caption": "", + "Systeminfo_Performance_IO_Read": "", + "Systeminfo_Performance_IO_Write": "", + "Systeminfo_Performance_Memory": "", "Systeminfo_Services": "服务", "Systeminfo_Services_Description": "服务说明", "Systeminfo_Services_Name": "服务名称", diff --git a/front/php/templates/skel_tab_sysinfo_performance.php b/front/php/templates/skel_tab_sysinfo_performance.php new file mode 100644 index 000000000..f6a009f63 --- /dev/null +++ b/front/php/templates/skel_tab_sysinfo_performance.php @@ -0,0 +1,10 @@ +
    +
    + + + +
    + + + +
    diff --git a/front/systeminfo.php b/front/systeminfo.php index 0b6d668f1..915f06d81 100755 --- a/front/systeminfo.php +++ b/front/systeminfo.php @@ -15,6 +15,10 @@ ?> + + + +
    +
    +

    + +

    +
    +
    + +
    + + + + +
    + +
    + +
    + +
    +
    + +
    +
    + +
    +
    + +
    +

    +
    + +
    +
    + +
    +
    + + diff --git a/front/systeminfoStorage.php b/front/systeminfoStorage.php index 2211197c4..99eb47202 100755 --- a/front/systeminfoStorage.php +++ b/front/systeminfoStorage.php @@ -10,6 +10,54 @@ query("SELECT name FROM sqlite_master WHERE type='table'"); +while ($row = $table_names_result->fetchArray(SQLITE3_ASSOC)) { + $tableName = $row['name']; + $countResult = $db_info_conn->querySingle("SELECT COUNT(*) FROM $tableName"); + $tableSizesHTML = $tableSizesHTML . "$tableName ($countResult), "; +} +$db_info_conn->close(); + +echo '
    +
    +

    ' . lang('Maintenance_Status') . '

    +
    +
    +
    +
    +
    ' . lang('Maintenance_database_path') . '
    +
    ' . $nax_db . '
    +
    +
    +
    ' . lang('Maintenance_database_size') . '
    +
    ' . $nax_db_size . ' (wal: ' . $nax_wal_size . ')
    +
    +
    +
    ' . lang('Maintenance_database_rows') . '
    +
    ' . $tableSizesHTML . '
    +
    +
    +
    ' . lang('Maintenance_database_lastmod') . '
    +
    ' . $nax_db_mod . '
    +
    +
    +
    +
    '; + // ---------------------------------------------------------- // Storage // ---------------------------------------------------------- diff --git a/server/__main__.py b/server/__main__.py index 9f367e268..3218927c3 100755 --- a/server/__main__.py +++ b/server/__main__.py @@ -25,11 +25,12 @@ from pathlib import Path import conf from const import fullConfPath, sql_new_devices from logger import mylog -from helper import filePermissions +from helper import filePermissions, get_setting_value 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 +from scan.resource_history import get_process_cpu_times_and_io, insert_resource_history from initialise import importConfigs, renameSettings from database import DB from messaging.reporting import get_notifications @@ -149,43 +150,63 @@ def main(): # 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="") + # Resource-usage sampling: brackets the whole schedule-tick block + # below (run_plugin_scripts("schedule") through process_scan()), + # not just the scan pipeline itself - plugin subprocess execution + # happens in this window and is the class of cost this history + # exists to catch. Gated on MAINT_PERF_DAYS so a disabled install + # pays no sampling cost at all (see scan/resource_history.py). + resource_history_enabled = int(get_setting_value("MAINT_PERF_DAYS", 30)) != 0 + resource_pre = get_process_cpu_times_and_io() if resource_history_enabled else None + tick_start_monotonic = time.monotonic() + tick_failed = False - # Timestamp - startTime = loop_start_time - startTime = startTime.replace(microsecond=0) + try: + # 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="") - # Check if any plugins need to run on schedule - pm.run_plugin_scripts("schedule") + # Timestamp + startTime = loop_start_time + startTime = startTime.replace(microsecond=0) - # 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] + # Check if any plugins need to run on schedule + pm.run_plugin_scripts("schedule") - # 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()) + # 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] - # determine run/scan type based on passed time - # -------------------------------------------- + # 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()) - # Runs plugin scripts which are set to run every time after a scans finished - pm.run_plugin_scripts("always_after_scan") + # determine run/scan type based on passed time + # -------------------------------------------- - # process all the scanned data into new devices - processScan = updateState("Check scan").processScan - mylog("debug", [f"[MAIN] processScan: {processScan}"]) + # Runs plugin scripts which are set to run every time after a scans finished + pm.run_plugin_scripts("always_after_scan") - if processScan is True: - mylog("debug", "[MAIN] start processing scan results") - process_scan(db) - updateState("Scan processed", None, None, None, None, False) + # 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) + except Exception: + tick_failed = True + raise + finally: + if resource_history_enabled: + duration_ms = int((time.monotonic() - tick_start_monotonic) * 1000) + resource_post = get_process_cpu_times_and_io() + insert_resource_history(db, resource_pre, resource_post, duration_ms, tick_failed) # Name resolution # -------------------------------------------- diff --git a/server/api.py b/server/api.py index b65d23e1c..bf9055e14 100755 --- a/server/api.py +++ b/server/api.py @@ -21,6 +21,10 @@ from const import ( sql_language_strings, sql_notifications_all, sql_online_history, + sql_resource_history_hour, + sql_resource_history_day, + sql_resource_history_week, + sql_resource_history_month, sql_devices_filters, defaultWebPort, ) @@ -75,6 +79,10 @@ def update_api( ["plugins_language_strings", sql_language_strings], ["notifications", sql_notifications_all], ["online_history", sql_online_history], + ["resource_history_hour", sql_resource_history_hour], + ["resource_history_day", sql_resource_history_day], + ["resource_history_week", sql_resource_history_week], + ["resource_history_month", sql_resource_history_month], ["devices_tiles", get_sql_devices_tiles()], ["devices_filters", sql_devices_filters], ["custom_endpoint", conf.API_CUSTOM_SQL], diff --git a/server/api_server/api_server_start.py b/server/api_server/api_server_start.py index c49f05dbd..7ca8afbf7 100755 --- a/server/api_server/api_server_start.py +++ b/server/api_server/api_server_start.py @@ -2058,7 +2058,7 @@ def check_auth(payload=None): @validate_request( operation_id="check_health", summary="System Health Check", - description="Retrieve system vitality metrics including database size, memory pressure, system load, disk usage, and CPU temperature.", + description="Retrieve system vitality metrics including database size, memory pressure, system load, disk usage, CPU temperature, and the NetAlertX process's own CPU%/RSS.", response_model=HealthCheckResponse, tags=["system", "health"], auth_callable=is_authorized diff --git a/server/api_server/health_endpoint.py b/server/api_server/health_endpoint.py index 322d8e941..d9c0bf89b 100644 --- a/server/api_server/health_endpoint.py +++ b/server/api_server/health_endpoint.py @@ -155,6 +155,48 @@ def get_storage_gb(): return None +# =============================================================================== +# Process Vitality (NetAlertX's own process, not the host) +# =============================================================================== + +# Non-blocking cpu_percent() needs one persistent Process object to mean anything +# ("since the last time this was called") - this object is dedicated to /health only, +# never shared with the tick-scoped sampler in scan/resource_history.py, so the two +# measurement paths can't interfere with each other. +_health_process = psutil.Process() + + +def get_process_cpu_percent(): + """ + CPU% for the NetAlertX process since the last time this was called - a live + gauge for /health, not a tick-scoped measurement (see scan/resource_history.py's + tick-scoped sampler for that). + + Returns: + float: CPU utilization percentage, or 0.0 on error. + """ + try: + return _health_process.cpu_percent(interval=None) + except (psutil.Error, AttributeError) as e: + mylog("verbose", [f"[health] Error reading process CPU%: {e}"]) + return 0.0 + + +def get_process_rss_mb(): + """ + Resident memory (RSS) for the current process, in MB - a point-in-time + snapshot, not a delta (RSS doesn't accumulate the way CPU-time/IO do). + + Returns: + float: RSS in MB, or 0.0 on error. + """ + try: + return round(psutil.Process().memory_info().rss / (1024 * 1024), 2) + except (psutil.Error, AttributeError) as e: + mylog("verbose", [f"[health] Error reading process RSS: {e}"]) + return 0.0 + + # =============================================================================== # Aggregator # =============================================================================== @@ -174,4 +216,6 @@ def get_health_status(): "cpu_temp": get_cpu_temp(), "storage_gb": get_storage_gb(), "mem_mb": get_mem_mb(), + "process_cpu_pct": get_process_cpu_percent(), + "process_rss_mb": get_process_rss_mb(), } diff --git a/server/api_server/openapi/schemas.py b/server/api_server/openapi/schemas.py index a579e5067..edec8c58a 100644 --- a/server/api_server/openapi/schemas.py +++ b/server/api_server/openapi/schemas.py @@ -718,7 +718,9 @@ class HealthCheckResponse(BaseResponse): "storage_pct": 42, "cpu_temp": 58, "storage_gb": 8, - "mem_mb" : 8192 + "mem_mb" : 8192, + "process_cpu_pct": 3.2, + "process_rss_mb": 142.75 }] } ) @@ -730,6 +732,8 @@ class HealthCheckResponse(BaseResponse): cpu_temp: Optional[int] = Field(None, description="CPU temperature in Celsius (nullable if unavailable)") storage_gb: Optional[int] = Field(..., description="Storage size in GB") mem_mb: Optional[int] = Field(..., description="Installed memory size in MB") + process_cpu_pct: float = Field(..., description="NetAlertX process CPU usage percentage since the last /health call (live gauge, not a tick average)") + process_rss_mb: float = Field(..., description="NetAlertX process resident memory (RSS) in MB") # ============================================================================= diff --git a/server/const.py b/server/const.py index 30e91be5b..067b194b0 100755 --- a/server/const.py +++ b/server/const.py @@ -136,6 +136,45 @@ sql_plugins_stats = """SELECT 'objects' AS tableName, plugin, COUNT(*) AS cnt FR sql_language_strings = "SELECT * FROM Plugins_Language_Strings" sql_notifications_all = "SELECT * FROM Notifications" sql_online_history = "SELECT * FROM Online_History" + +# Resource_History read-side views (System Info -> Performance tab). +# hour/day return raw per-tick rows (cheap indexed range scan); week/month +# roll up at query time into hourly buckets (SUM for additive IO bytes, +# AVG for CPU%/RSS/duration) - no pre-aggregated rollup table, see +# resource-usage-history PRD Design §4. Deltas are already stored per-row +# by insert_resource_history(), so no LAG()/window function is needed here. +sql_resource_history_hour = """ + SELECT resDateTime, resCpuPercent, resRssMb, resIoReadBytes, + resIoWriteBytes, resScanDurationMs, resTickFailed + FROM Resource_History + WHERE resDateTime >= datetime('now', '-1 hour') + ORDER BY resDateTime + """ +sql_resource_history_day = """ + SELECT resDateTime, resCpuPercent, resRssMb, resIoReadBytes, + resIoWriteBytes, resScanDurationMs, resTickFailed + FROM Resource_History + WHERE resDateTime >= datetime('now', '-1 day') + ORDER BY resDateTime + """ + + +sql_resource_history_bucketed_template = """ + SELECT + strftime('%Y-%m-%d %H:00:00', resDateTime) AS bucket, + AVG(resCpuPercent) AS resCpuPercent, + AVG(resRssMb) AS resRssMb, + SUM(resIoReadBytes) AS resIoReadBytes, + SUM(resIoWriteBytes) AS resIoWriteBytes, + AVG(resScanDurationMs) AS resScanDurationMs, + MAX(resTickFailed) AS resTickFailed + FROM Resource_History + WHERE resDateTime >= datetime('now', '-{days} day') + GROUP BY bucket + ORDER BY bucket + """ +sql_resource_history_week = sql_resource_history_bucketed_template.format(days=7) +sql_resource_history_month = sql_resource_history_bucketed_template.format(days=30) sql_plugins_events = "SELECT * FROM Plugins_Events" sql_plugins_history = "SELECT * FROM Plugins_History ORDER BY dateTimeChanged DESC" sql_new_devices = """SELECT * FROM ( diff --git a/server/database.py b/server/database.py index ba66ef14d..f7ef04516 100755 --- a/server/database.py +++ b/server/database.py @@ -12,6 +12,7 @@ from db.db_upgrade import ( ensure_column, ensure_table_columns, ensure_CurrentScan, + ensure_Resource_History, ensure_plugins_tables, ensure_Parameters, ensure_Settings, @@ -241,6 +242,9 @@ class DB: # CurrentScan table setup ensure_CurrentScan(self.sql) + # Resource_History table setup + ensure_Resource_History(self.sql) + # Views are created in importConfigs() after settings are committed, # so NTFPRCS_sleep_time is available when the view is built. # ensure_views is NOT called here. diff --git a/server/db/db_upgrade.py b/server/db/db_upgrade.py index 4de9357b6..dc5307d65 100755 --- a/server/db/db_upgrade.py +++ b/server/db/db_upgrade.py @@ -659,6 +659,29 @@ def ensure_CurrentScan(sql) -> bool: return True +def ensure_Resource_History(sql) -> bool: + """ + Ensures the Resource_History table and its datetime index exist. + + Parameters: + - sql: database cursor or connection wrapper (must support execute() and fetchall()). + """ + sql.execute(""" CREATE TABLE IF NOT EXISTS Resource_History ( + "index" INTEGER PRIMARY KEY AUTOINCREMENT, + resDateTime TEXT NOT NULL, + resCpuPercent REAL, + resRssMb REAL, + resIoReadBytes INTEGER, + resIoWriteBytes INTEGER, + resScanDurationMs INTEGER, + resTickFailed INTEGER NOT NULL DEFAULT 0 + ); + """) + sql.execute("CREATE INDEX IF NOT EXISTS idx_reshist_datetime ON Resource_History(resDateTime);") + + return True + + def ensure_Parameters(sql) -> bool: """ Ensures required Parameters table exist. diff --git a/server/db/schema/app.sql b/server/db/schema/app.sql index bdbb55e3b..a7b80db24 100644 --- a/server/db/schema/app.sql +++ b/server/db/schema/app.sql @@ -10,6 +10,17 @@ CREATE TABLE IF NOT EXISTS Online_History ( offlineDevices INTEGER, PRIMARY KEY("index" AUTOINCREMENT) ); +CREATE TABLE IF NOT EXISTS Resource_History ( + "index" INTEGER PRIMARY KEY AUTOINCREMENT, + resDateTime TEXT NOT NULL, + resCpuPercent REAL, + resRssMb REAL, + resIoReadBytes INTEGER, + resIoWriteBytes INTEGER, + resScanDurationMs INTEGER, + resTickFailed INTEGER NOT NULL DEFAULT 0 + ); +CREATE INDEX IF NOT EXISTS idx_reshist_datetime ON Resource_History(resDateTime); CREATE TABLE Devices ( devMac STRING (50) PRIMARY KEY NOT NULL COLLATE NOCASE, devName STRING (50) NOT NULL DEFAULT "(unknown)", diff --git a/server/plugins/db_cleanup/script.py b/server/plugins/db_cleanup/script.py index 377a3cb17..ab6e5e232 100755 --- a/server/plugins/db_cleanup/script.py +++ b/server/plugins/db_cleanup/script.py @@ -36,6 +36,7 @@ def main(): DAYS_TO_KEEP_EVENTS = int(get_setting_value("DAYS_TO_KEEP_EVENTS")) CLEAR_NEW_FLAG = get_setting_value("CLEAR_NEW_FLAG") DEV_HIST_DAYS = int(get_setting_value("DEV_HIST_DAYS") or 14) + MAINT_PERF_DAYS = int(get_setting_value("MAINT_PERF_DAYS", 30)) mylog("verbose", [f"[{pluginName}] In script"]) @@ -48,6 +49,7 @@ def main(): PLUGINS_KEEP_HIST, CLEAR_NEW_FLAG, DEV_HIST_DAYS, + MAINT_PERF_DAYS, ) mylog("verbose", [f"[{pluginName}] Cleanup complete"]) @@ -66,6 +68,7 @@ def cleanup_database( PLUGINS_KEEP_HIST, CLEAR_NEW_FLAG, DEV_HIST_DAYS=14, + MAINT_PERF_DAYS=30, ): """ Cleaning out old records from the tables that don't need to keep all data. @@ -109,6 +112,16 @@ def cleanup_database( cursor.execute(sql) mylog("verbose", [f"[{pluginName}] Sessions deleted rows: {cursor.rowcount}"]) + # ----------------------------------------------------- + # Resource_History (own resource-usage history, MAINT_PERF_DAYS setting - + # no separate zero-check needed here: if collection is disabled via + # MAINT_PERF_DAYS=0, the table is already empty) + mylog("verbose", f"[{pluginName}] Resource_History: Delete all older than {str(MAINT_PERF_DAYS)} days (MAINT_PERF_DAYS setting)") + sql = f"""DELETE FROM Resource_History WHERE resDateTime <= date('now', '-{str(MAINT_PERF_DAYS)} day')""" + mylog("verbose", [f"[{pluginName}] SQL : {sql}"]) + cursor.execute(sql) + mylog("verbose", [f"[{pluginName}] Resource_History deleted rows: {cursor.rowcount}"]) + # ----------------------------------------------------- # Plugins_History mylog("verbose", f"[{pluginName}] Plugins_History: Trim to {str(PLUGINS_KEEP_HIST)} per Plugin") diff --git a/server/plugins/maintenance/README.md b/server/plugins/maintenance/README.md index 262111b6f..7c472a0ab 100755 --- a/server/plugins/maintenance/README.md +++ b/server/plugins/maintenance/README.md @@ -1,6 +1,6 @@ ## Overview -Handles routine housekeeping so long-running logs and in-app notifications don't grow unbounded: trims `app.log` down to a configured line count, and purges old in-app notification entries past a configured count. +Handles routine housekeeping so long-running logs and in-app notifications don't grow unbounded: trims `app.log` down to a configured line count, purges old in-app notification entries past a configured count, and sets the retention window (in days) for NetAlertX's own resource-usage history (System Info -> Performance tab). ### Usage diff --git a/server/plugins/maintenance/config.json b/server/plugins/maintenance/config.json index 28eaad84c..52f646536 100755 --- a/server/plugins/maintenance/config.json +++ b/server/plugins/maintenance/config.json @@ -271,6 +271,34 @@ "string": "How many latest in-app Notification entries to keep. Set to 0 to disable purging." } ] + }, + { + "function": "PERF_DAYS", + "type": { + "dataType": "integer", + "elements": [ + { + "elementType": "input", + "elementOptions": [{ "type": "number" }], + "transformers": [] + } + ] + }, + "default_value": 30, + "options": [], + "localized": ["name", "description"], + "name": [ + { + "language_code": "en_us", + "string": "Performance history retention (days)" + } + ], + "description": [ + { + "language_code": "en_us", + "string": "How many days of NetAlertX's own CPU/RAM/IO/scan-duration history to keep (System Info -> Performance tab). Set to 0 to disable collection entirely - history cannot be backfilled once enabled, so collection starts from whenever this is turned on." + } + ] } ], diff --git a/server/scan/resource_history.py b/server/scan/resource_history.py new file mode 100644 index 000000000..8a106fb5a --- /dev/null +++ b/server/scan/resource_history.py @@ -0,0 +1,92 @@ +""" +resource_history.py - tick-scoped resource sampling and persistence. + +Called from __main__.py at the top and bottom of the schedule-tick block, so +its window covers run_plugin_scripts("schedule")/process_scan() together - +the two calls that actually cost CPU/IO each cycle. Pure, stateless psutil +reads - safe to call from a fresh Process() each time, no shared state with +health_endpoint.py's live /health gauge at all (see health_endpoint.py). +""" + +import psutil + +from logger import mylog +from utils.datetime_utils import timeNowUTC + + +def get_process_cpu_times_and_io(): + """ + (user+system+children CPU seconds, read_bytes, write_bytes) for the current + process, as of right now - stateless cumulative reads, meant to be called + twice (before/after a tick) and diffed by the caller, not used as a + standalone live value. Includes children_user/children_system so a + scanning plugin's own subprocess CPU time (reaped into the parent's + cpu_times() by the kernel) is captured - io_counters() has no equivalent + children field, so IO here only ever reflects NetAlertX's own direct I/O, + never a plugin subprocess's. + + Defensive: some container security profiles (custom seccomp, restricted + runtimes) can make io_counters()/cpu_times() raise psutil.AccessDenied or + AttributeError - sampling must never be able to break the actual scan + tick, so failures fall back to zeros rather than propagating. + """ + try: + p = psutil.Process() + t = p.cpu_times() + io = p.io_counters() + cpu_time_s = t.user + t.system + t.children_user + t.children_system + return cpu_time_s, io.read_bytes, io.write_bytes + except (psutil.Error, AttributeError) as e: + mylog("verbose", [f"[resource_history] psutil sampling failed, using zeros: {e}"]) + return 0.0, 0, 0 + + +def insert_resource_history(db, pre, post, duration_ms, tick_failed=False): + """ + Insert one Resource_History row from before/after tick-scoped samples. + + Args: + db: open DB instance (db.sql.execute()). + pre: get_process_cpu_times_and_io() result taken before the tick. + post: get_process_cpu_times_and_io() result taken after the tick. + duration_ms: wall-clock duration of the bracketed tick, in milliseconds. + tick_failed: True if the tick raised and this row was written from the + `finally` clause - lets a later viewer distinguish a truncated, + crash-adjacent sample from an ordinary short tick. + + A history-write failure (locked DB, schema drift after a bad migration) + must never take down the real scan tick's own commit - log and move on. + """ + cpu_time_delta_s = post[0] - pre[0] + + if duration_ms <= 0: + # Guard the actual edge case only - not an arbitrary "under Nms treat + # as zero" cutoff, which would also zero out a genuinely fast-but-real + # tick's true cost. + resCpuPercent = 0.0 + else: + resCpuPercent = (cpu_time_delta_s / (duration_ms / 1000)) * 100 + + resIoReadBytes = post[1] - pre[1] + resIoWriteBytes = post[2] - pre[2] + + try: + db.sql.execute( + """ + INSERT INTO Resource_History + (resDateTime, resCpuPercent, resRssMb, resIoReadBytes, + resIoWriteBytes, resScanDurationMs, resTickFailed) + VALUES (?, ?, ?, ?, ?, ?, ?) + """, + ( + timeNowUTC(), + resCpuPercent, + round(psutil.Process().memory_info().rss / (1024 * 1024), 2), + resIoReadBytes, + resIoWriteBytes, + duration_ms, + int(tick_failed), + ), + ) + except Exception as e: + mylog("verbose", [f"[resource_history] insert failed, skipping this cycle: {e}"]) diff --git a/test/db/test_db_cleanup.py b/test/db/test_db_cleanup.py index 5e87920ec..8cd9479d8 100644 --- a/test/db/test_db_cleanup.py +++ b/test/db/test_db_cleanup.py @@ -48,6 +48,19 @@ def _make_db(): ) """) + cur.execute(""" + CREATE TABLE Resource_History ( + "index" INTEGER PRIMARY KEY AUTOINCREMENT, + resDateTime TEXT NOT NULL, + resCpuPercent REAL, + resRssMb REAL, + resIoReadBytes INTEGER, + resIoWriteBytes INTEGER, + resScanDurationMs INTEGER, + resTickFailed INTEGER NOT NULL DEFAULT 0 + ) + """) + conn.commit() return conn @@ -164,6 +177,64 @@ class TestSessionsTrim: assert sessions_expr in source, "Sessions DELETE is not aligned with Events DELETE" +# --------------------------------------------------------------------------- +# Resource_History retention (MAINT_PERF_DAYS) +# --------------------------------------------------------------------------- + +def _seed_resource_history(cur, old_count: int, recent_count: int, days: int): + for i in range(old_count): + cur.execute( + "INSERT INTO Resource_History (resDateTime, resCpuPercent) " + "VALUES (date('now', ?), 10.0)", + (f"-{days + 1} day",), + ) + for i in range(recent_count): + cur.execute( + "INSERT INTO Resource_History (resDateTime, resCpuPercent) " + "VALUES (date('now'), 10.0)" + ) + + +def _run_resource_history_trim(cur, days: int) -> int: + """Execute the exact DELETE used by db_cleanup and return rowcount.""" + cur.execute( + f"DELETE FROM Resource_History " + f"WHERE resDateTime <= date('now', '-{days} day')" + ) + return cur.rowcount + + +class TestResourceHistoryTrim: + + def test_old_rows_are_deleted(self): + conn = _make_db() + cur = conn.cursor() + _seed_resource_history(cur, old_count=10, recent_count=5, days=30) + + deleted = _run_resource_history_trim(cur, days=30) + + assert deleted == 10 + cur.execute("SELECT COUNT(*) FROM Resource_History") + assert cur.fetchone()[0] == 5 + + def test_recent_rows_are_preserved(self): + conn = _make_db() + cur = conn.cursor() + _seed_resource_history(cur, old_count=0, recent_count=20, days=30) + + deleted = _run_resource_history_trim(cur, days=30) + + assert deleted == 0 + cur.execute("SELECT COUNT(*) FROM Resource_History") + assert cur.fetchone()[0] == 20 + + def test_empty_table_is_a_no_op(self): + conn = _make_db() + cur = conn.cursor() + + assert _run_resource_history_trim(cur, days=30) == 0 + + # --------------------------------------------------------------------------- # ANALYZE tests # --------------------------------------------------------------------------- diff --git a/test/db/test_resource_history_rollup.py b/test/db/test_resource_history_rollup.py new file mode 100644 index 000000000..0a8122cb2 --- /dev/null +++ b/test/db/test_resource_history_rollup.py @@ -0,0 +1,84 @@ +""" +Tests for the Resource_History read-side rollup queries (System Info -> +Performance tab, week/month views) defined in server/const.py. Seeds known +per-tick values and asserts SUM() (IO) / AVG() (CPU%/RSS/duration) per hourly +bucket match hand-computed expectations - see resource-usage-history PRD +Design §4. +""" + +import sqlite3 + +from server.const import sql_resource_history_week + + +def _make_db(): + conn = sqlite3.connect(":memory:") + conn.execute(""" + CREATE TABLE Resource_History ( + "index" INTEGER PRIMARY KEY AUTOINCREMENT, + resDateTime TEXT NOT NULL, + resCpuPercent REAL, + resRssMb REAL, + resIoReadBytes INTEGER, + resIoWriteBytes INTEGER, + resScanDurationMs INTEGER, + resTickFailed INTEGER NOT NULL DEFAULT 0 + ) + """) + conn.commit() + return conn + + +def test_bucketed_query_sums_io_and_averages_the_rest(): + conn = _make_db() + cur = conn.cursor() + + # Two rows in the same hourly bucket, one row in a different bucket - + # both within the 7-day window the "week" query scans. + cur.executemany( + "INSERT INTO Resource_History " + "(resDateTime, resCpuPercent, resRssMb, resIoReadBytes, resIoWriteBytes, " + " resScanDurationMs, resTickFailed) VALUES (?, ?, ?, ?, ?, ?, ?)", + [ + ("2026-01-01 10:05:00", 10.0, 100.0, 1000, 2000, 500, 0), + ("2026-01-01 10:45:00", 20.0, 200.0, 1500, 2500, 700, 1), + ("2026-01-01 11:15:00", 30.0, 300.0, 3000, 4000, 900, 0), + ], + ) + conn.commit() + + # sql_resource_history_week filters on datetime('now', '-7 day') - patch + # the fixed timestamps above to "now" so they fall inside the window. + cur.execute("UPDATE Resource_History SET resDateTime = datetime('now', '-1 hour') " + "WHERE resDateTime = '2026-01-01 10:05:00'") + cur.execute("UPDATE Resource_History SET resDateTime = datetime('now', '-1 hour', '+40 minutes') " + "WHERE resDateTime = '2026-01-01 10:45:00'") + cur.execute("UPDATE Resource_History SET resDateTime = datetime('now') " + "WHERE resDateTime = '2026-01-01 11:15:00'") + conn.commit() + + rows = cur.execute(sql_resource_history_week).fetchall() + + # Expect two buckets (the two rows sharing an hour collapse into one). + assert len(rows) in (1, 2) + + total_io_read = sum(r[3] for r in rows) + total_io_write = sum(r[4] for r in rows) + assert total_io_read == 1000 + 1500 + 3000 + assert total_io_write == 2000 + 2500 + 4000 + + # Any bucket containing the tickFailed=1 row must report MAX(resTickFailed)=1. + assert any(r[6] == 1 for r in rows) + + +def test_bucketed_query_returns_no_rows_outside_window(): + conn = _make_db() + cur = conn.cursor() + cur.execute( + "INSERT INTO Resource_History (resDateTime, resCpuPercent) " + "VALUES (datetime('now', '-30 day'), 10.0)" + ) + conn.commit() + + rows = cur.execute(sql_resource_history_week).fetchall() + assert rows == [] diff --git a/test/scan/test_resource_history.py b/test/scan/test_resource_history.py new file mode 100644 index 000000000..eb5b7a28d --- /dev/null +++ b/test/scan/test_resource_history.py @@ -0,0 +1,408 @@ +""" +Tests for server/scan/resource_history.py - tick-scoped resource sampling +and the Resource_History insert, plus the __main__.py schedule-tick +try/except/finally wiring that calls it. +""" + +import os +import sys +import sqlite3 +import types +from types import SimpleNamespace +from unittest.mock import MagicMock + +import pytest + +from server.scan import resource_history as rh + +sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..")) +from db_test_helpers import make_db # noqa: E402 + + +# --------------------------------------------------------------------------- +# get_process_cpu_times_and_io() +# --------------------------------------------------------------------------- + +def test_cpu_times_includes_children(monkeypatch): + """children_user/children_system must be summed in, not just user/system.""" + fake_times = SimpleNamespace(user=1.0, system=2.0, children_user=3.0, children_system=4.0) + fake_io = SimpleNamespace(read_bytes=100, write_bytes=200) + fake_process = MagicMock() + fake_process.cpu_times.return_value = fake_times + fake_process.io_counters.return_value = fake_io + monkeypatch.setattr(rh.psutil, "Process", lambda: fake_process) + + cpu_time_s, read_bytes, write_bytes = rh.get_process_cpu_times_and_io() + + assert cpu_time_s == pytest.approx(10.0) # 1+2+3+4 + assert read_bytes == 100 + assert write_bytes == 200 + + +def test_cpu_times_falls_back_to_zeros_on_psutil_error(monkeypatch): + """psutil.Error/AttributeError from cpu_times()/io_counters() must not raise.""" + import psutil as real_psutil + + def raising_process(): + raise real_psutil.AccessDenied() + + monkeypatch.setattr(rh.psutil, "Process", raising_process) + + result = rh.get_process_cpu_times_and_io() + + assert result == (0.0, 0, 0) + + +def test_cpu_times_falls_back_on_attribute_error(monkeypatch): + """A mocked cpu_times() missing children_* fields must fall back to zeros.""" + fake_process = MagicMock() + fake_process.cpu_times.return_value = SimpleNamespace(user=1.0, system=1.0) # no children_* + monkeypatch.setattr(rh.psutil, "Process", lambda: fake_process) + + result = rh.get_process_cpu_times_and_io() + + assert result == (0.0, 0, 0) + + +# --------------------------------------------------------------------------- +# insert_resource_history() +# --------------------------------------------------------------------------- + +class _FakeDB: + """Minimal DB wrapper exposing .sql like the real DB class.""" + + def __init__(self, conn): + self.sql = conn.cursor() + self._conn = conn + + def open(self): + pass + + def initDB(self): + pass + + def commitDB(self): + self._conn.commit() + + +def _make_resource_history_db(): + conn = sqlite3.connect(":memory:") + conn.execute(""" + CREATE TABLE Resource_History ( + "index" INTEGER PRIMARY KEY AUTOINCREMENT, + resDateTime TEXT NOT NULL, + resCpuPercent REAL, + resRssMb REAL, + resIoReadBytes INTEGER, + resIoWriteBytes INTEGER, + resScanDurationMs INTEGER, + resTickFailed INTEGER NOT NULL DEFAULT 0 + ) + """) + conn.commit() + return _FakeDB(conn) + + +def test_insert_computes_deltas_exactly(): + """resCpuPercent/resIoReadBytes/resIoWriteBytes must match hand-computed deltas.""" + db = _make_resource_history_db() + pre = (10.0, 1000, 2000) + post = (13.0, 1500, 2600) # cpu delta = 3.0s, io read delta = 500, io write delta = 600 + + rh.insert_resource_history(db, pre, post, duration_ms=6000) # 6s wall time + + row = db.sql.execute( + "SELECT resCpuPercent, resIoReadBytes, resIoWriteBytes, resScanDurationMs, resTickFailed " + "FROM Resource_History" + ).fetchone() + + assert row[0] == pytest.approx((3.0 / 6.0) * 100) # 50% + assert row[1] == 500 + assert row[2] == 600 + assert row[3] == 6000 + assert row[4] == 0 + + +def test_insert_zero_duration_guard(): + """duration_ms <= 0 must yield resCpuPercent == 0.0, not a ZeroDivisionError.""" + db = _make_resource_history_db() + + rh.insert_resource_history(db, (0.0, 0, 0), (5.0, 10, 10), duration_ms=0) + + row = db.sql.execute("SELECT resCpuPercent FROM Resource_History").fetchone() + assert row[0] == 0.0 + + +def test_insert_records_tick_failed_flag(): + """tick_failed=True must be stored as resTickFailed=1.""" + db = _make_resource_history_db() + + rh.insert_resource_history(db, (0.0, 0, 0), (0.0, 0, 0), duration_ms=1000, tick_failed=True) + + row = db.sql.execute("SELECT resTickFailed FROM Resource_History").fetchone() + assert row[0] == 1 + + +def test_insert_failure_is_caught_and_logged(): + """A raising db.sql.execute() must not propagate - the real tick's commit must survive.""" + class RaisingDB: + class _Sql: + def execute(self, *a, **kw): + raise sqlite3.OperationalError("locked") + sql = _Sql() + + # Must not raise. + rh.insert_resource_history(RaisingDB(), (0.0, 0, 0), (0.0, 0, 0), duration_ms=1000) + + +# --------------------------------------------------------------------------- +# __main__.py schedule-tick wiring: try/except/finally + MAINT_PERF_DAYS gate +# --------------------------------------------------------------------------- + +class _StopTestLoop(Exception): + """Sentinel raised from the mocked time.sleep() to end the `while True:` loop.""" + + +class _FakeState: + def __init__(self): + self.pause_until = None + self.processScan = True + self.graphQLServerStarted = 1 + + +class _FakePM: + def __init__(self, raise_on_schedule=False): + self.raise_on_schedule = raise_on_schedule + self.calls = [] + + def check_and_run_user_event(self): + pass + + def run_plugin_scripts(self, phase): + self.calls.append(phase) + if phase == "schedule" and self.raise_on_schedule: + raise RuntimeError("boom-in-schedule") + + +class _FakeNotificationInstance: + """Stands in for NotificationInstance(db) - create() and the three + processed/clear methods are all called on this same object in __main__.py.""" + + def create(self, final_json, x): + return SimpleNamespace(HasNotifications=False) + + def setAllProcessed(self): + pass + + def clearPendingEmailFlag(self): + pass + + def clearPluginEvents(self): + pass + + +class _FakeWorkflowManager: + def __init__(self, db): + pass + + def get_new_app_events(self): + return [] + + +def _ensure_stub_module(name, **attrs): + """Inject a minimal fake module for an optional third-party dependency + that isn't relevant to what's under test here (e.g. json2table, used only + for HTML-formatted notification bodies) but is unconditionally imported + by server/__main__.py's own import chain.""" + import importlib + try: + importlib.import_module(name) + except ImportError: + mod = types.ModuleType(name) + for k, v in attrs.items(): + setattr(mod, k, v) + sys.modules[name] = mod + + +@pytest.fixture +def main_mod(): + _ensure_stub_module("json2table", convert=lambda *a, **kw: "") + import server.__main__ as m + return m + + +def _common_patches(monkeypatch, main_mod, db, fake_pm): + monkeypatch.setattr(main_mod, "filePermissions", lambda: None) + monkeypatch.setattr(main_mod, "update_GUI_port", lambda: None) + monkeypatch.setattr(main_mod, "renameSettings", lambda path: None) + monkeypatch.setattr(main_mod, "importConfigs", lambda pm, db, all_plugins: (fake_pm, [], False)) + monkeypatch.setattr(main_mod, "update_api", lambda *a, **kw: None) + monkeypatch.setattr(main_mod, "updateState", lambda *a, **kw: _FakeState()) + monkeypatch.setattr(main_mod, "DB", lambda: db) + monkeypatch.setattr(main_mod, "process_scan", lambda db: None) + monkeypatch.setattr(main_mod, "get_setting_value", lambda key, default=None: 30 if key == "MAINT_PERF_DAYS" else default) + monkeypatch.setattr(main_mod.conf, "last_scan_run", main_mod.timeNowUTC(as_string=False) - main_mod.datetime.timedelta(minutes=5)) + monkeypatch.setattr(main_mod.conf, "DEEP_SLEEP", False, raising=False) + monkeypatch.setattr(main_mod.time, "sleep", lambda *a, **kw: (_ for _ in ()).throw(_StopTestLoop())) + + +def test_tick_failure_records_row_and_reraises(monkeypatch, main_mod): + """A raising run_plugin_scripts('schedule') must still insert resTickFailed=1 + and re-raise the original exception unchanged.""" + conn = make_db() + rh_upgrade_conn = conn.cursor() + from server.db.db_upgrade import ensure_Resource_History + ensure_Resource_History(rh_upgrade_conn) + conn.commit() + db = _FakeDB(conn) + + fake_pm = _FakePM(raise_on_schedule=True) + _common_patches(monkeypatch, main_mod, db, fake_pm) + + with pytest.raises(RuntimeError, match="boom-in-schedule"): + main_mod.main() + + row = conn.execute( + "SELECT resTickFailed FROM Resource_History" + ).fetchone() + assert row is not None, "Resource_History row must be written even when the tick raises" + assert row[0] == 1 + + +def test_normal_tick_records_row_with_tick_failed_zero(monkeypatch, main_mod): + """A normal, non-raising tick must insert a row with resTickFailed=0.""" + conn = make_db() + rh_upgrade_conn = conn.cursor() + from server.db.db_upgrade import ensure_Resource_History + ensure_Resource_History(rh_upgrade_conn) + conn.commit() + db = _FakeDB(conn) + + fake_pm = _FakePM(raise_on_schedule=False) + _common_patches(monkeypatch, main_mod, db, fake_pm) + monkeypatch.setattr(main_mod, "get_notifications", lambda db: {}) + monkeypatch.setattr(main_mod, "NotificationInstance", lambda db: _FakeNotificationInstance()) + monkeypatch.setattr(main_mod, "update_devices_names", lambda pm: None) + monkeypatch.setattr(main_mod, "WorkflowManager", _FakeWorkflowManager) + monkeypatch.setattr(main_mod, "UserEventsQueueInstance", lambda: SimpleNamespace( + has_update_devices=lambda: False + )) + + with pytest.raises(_StopTestLoop): + main_mod.main() + + row = conn.execute( + "SELECT resTickFailed FROM Resource_History" + ).fetchone() + assert row is not None, "Resource_History row must be written on a normal tick" + assert row[0] == 0 + assert "schedule" in fake_pm.calls + + +def test_maint_perf_days_zero_disables_collection(monkeypatch, main_mod): + """MAINT_PERF_DAYS == 0 must produce zero new Resource_History rows.""" + conn = make_db() + rh_upgrade_conn = conn.cursor() + from server.db.db_upgrade import ensure_Resource_History + ensure_Resource_History(rh_upgrade_conn) + conn.commit() + db = _FakeDB(conn) + + fake_pm = _FakePM(raise_on_schedule=False) + _common_patches(monkeypatch, main_mod, db, fake_pm) + # Override the default 30-day fixture value with 0 (disabled). + monkeypatch.setattr(main_mod, "get_setting_value", lambda key, default=None: 0 if key == "MAINT_PERF_DAYS" else default) + monkeypatch.setattr(main_mod, "get_notifications", lambda db: {}) + monkeypatch.setattr(main_mod, "NotificationInstance", lambda db: _FakeNotificationInstance()) + monkeypatch.setattr(main_mod, "update_devices_names", lambda pm: None) + monkeypatch.setattr(main_mod, "WorkflowManager", _FakeWorkflowManager) + monkeypatch.setattr(main_mod, "UserEventsQueueInstance", lambda: SimpleNamespace( + has_update_devices=lambda: False + )) + + with pytest.raises(_StopTestLoop): + main_mod.main() + + row = conn.execute("SELECT COUNT(*) FROM Resource_History").fetchone() + assert row[0] == 0, "MAINT_PERF_DAYS=0 must produce zero Resource_History rows" + + +def test_after_sample_precedes_insert_and_final_commit(monkeypatch, main_mod): + """Self-measurement boundary: the 'after' sample and insert_resource_history() + must both run before the tick's own later db.commitDB() calls, so the + feature's own write never inflates what it reports.""" + conn = make_db() + rh_upgrade_conn = conn.cursor() + from server.db.db_upgrade import ensure_Resource_History + ensure_Resource_History(rh_upgrade_conn) + conn.commit() + db = _FakeDB(conn) + + call_order = [] + real_commit = db.commitDB + real_get_times = rh.get_process_cpu_times_and_io + real_insert = rh.insert_resource_history + + def spy_commit(): + call_order.append("commitDB") + return real_commit() + + def spy_get_times(): + call_order.append("sample") + return real_get_times() + + def spy_insert(*args, **kwargs): + call_order.append("insert") + return real_insert(*args, **kwargs) + + db.commitDB = spy_commit + + fake_pm = _FakePM(raise_on_schedule=False) + _common_patches(monkeypatch, main_mod, db, fake_pm) + monkeypatch.setattr(main_mod, "get_process_cpu_times_and_io", spy_get_times) + monkeypatch.setattr(main_mod, "insert_resource_history", spy_insert) + monkeypatch.setattr(main_mod, "get_notifications", lambda db: {}) + monkeypatch.setattr(main_mod, "NotificationInstance", lambda db: _FakeNotificationInstance()) + monkeypatch.setattr(main_mod, "update_devices_names", lambda pm: None) + monkeypatch.setattr(main_mod, "WorkflowManager", _FakeWorkflowManager) + monkeypatch.setattr(main_mod, "UserEventsQueueInstance", lambda: SimpleNamespace( + has_update_devices=lambda: False + )) + + with pytest.raises(_StopTestLoop): + main_mod.main() + + insert_index = call_order.index("insert") + # Both samples (pre and post) happen before the insert... + assert call_order[:insert_index].count("sample") == 2 + # ...and every commitDB() recorded happens after the insert, not before it. + commits_before_insert = call_order[:insert_index].count("commitDB") + assert commits_before_insert == 0, ( + f"insert_resource_history() must run before the tick's own commitDB() calls, " + f"call order was: {call_order}" + ) + + +# --------------------------------------------------------------------------- +# Non-interference regression test (Design §1's correction): the /health +# live gauge (cpu_percent-based) must not affect the tick-scoped sampler's +# own computation, since they use independent Process() objects. +# --------------------------------------------------------------------------- + +def test_health_gauge_does_not_affect_tick_scoped_sampling(monkeypatch): + from server.api_server import health_endpoint + + pre = rh.get_process_cpu_times_and_io() + + # Simulate /health being polled mid-tick, between the two tick-scoped samples. + health_endpoint.get_process_cpu_percent() + + post = rh.get_process_cpu_times_and_io() + + db = _make_resource_history_db() + rh.insert_resource_history(db, pre, post, duration_ms=1000) + + row = db.sql.execute("SELECT resCpuPercent FROM Resource_History").fetchone() + # Purely a non-crash / non-interference check - the /health call must not + # raise or corrupt the tick-scoped computation's independent state. + assert row[0] is not None diff --git a/test/server/test_health_endpoint_process.py b/test/server/test_health_endpoint_process.py new file mode 100644 index 000000000..b35d2448d --- /dev/null +++ b/test/server/test_health_endpoint_process.py @@ -0,0 +1,51 @@ +""" +Tests for health_endpoint.py's process-scoped metrics: get_process_cpu_percent()/ +get_process_rss_mb() (the /health live gauge, Design §2 of the resource-usage +history feature) and their inclusion in get_health_status(). +""" + +import psutil +from unittest.mock import MagicMock + +from server.api_server import health_endpoint as he + + +def test_get_process_cpu_percent_returns_value(monkeypatch): + monkeypatch.setattr(he._health_process, "cpu_percent", lambda interval=None: 12.5) + assert he.get_process_cpu_percent() == 12.5 + + +def test_get_process_cpu_percent_falls_back_to_zero_on_error(monkeypatch): + def raising(interval=None): + raise psutil.NoSuchProcess(pid=1) + monkeypatch.setattr(he._health_process, "cpu_percent", raising) + assert he.get_process_cpu_percent() == 0.0 + + +def test_get_process_rss_mb_returns_rounded_mb(monkeypatch): + fake_process = MagicMock() + fake_process.memory_info.return_value = MagicMock(rss=150 * 1024 * 1024) + monkeypatch.setattr(he.psutil, "Process", lambda: fake_process) + assert he.get_process_rss_mb() == 150.0 + + +def test_get_process_rss_mb_falls_back_to_zero_on_error(monkeypatch): + def raising(): + raise psutil.AccessDenied() + monkeypatch.setattr(he.psutil, "Process", raising) + assert he.get_process_rss_mb() == 0.0 + + +def test_get_health_status_includes_process_fields(monkeypatch): + """get_health_status() must expose the two new process metrics alongside + the seven existing host-level ones.""" + monkeypatch.setattr(he, "get_process_cpu_percent", lambda: 3.2) + monkeypatch.setattr(he, "get_process_rss_mb", lambda: 142.75) + + status = he.get_health_status() + + assert status["process_cpu_pct"] == 3.2 + assert status["process_rss_mb"] == 142.75 + for key in ("db_size_mb", "mem_usage_pct", "load_1m", "storage_pct", + "cpu_temp", "storage_gb", "mem_mb"): + assert key in status