From 70d9f789fd021f1282f4d016ab507e1d501117c1 Mon Sep 17 00:00:00 2001 From: Safeguard Date: Thu, 1 Oct 2026 17:53:29 +0200 Subject: [PATCH 1/4] Translated using Weblate (Russian) Currently translated at 100.0% (853 of 853 strings) Translation: NetAlertX/core Translate-URL: https://hosted.weblate.org/projects/pialert/core/ru/ --- front/php/templates/language/ru_ru.json | 32 ++++++++++++------------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/front/php/templates/language/ru_ru.json b/front/php/templates/language/ru_ru.json index 84c199428..efb0021a1 100644 --- a/front/php/templates/language/ru_ru.json +++ b/front/php/templates/language/ru_ru.json @@ -233,8 +233,8 @@ "Device_Shortcut_OnlineChart": "Присутствие устройств", "Device_Shortcut_Unstable": "Нестабильный", "Device_TableHead_AlertDown": "Оповещение о сост. ВЫКЛ", - "Device_TableHead_AlertEvents": "", - "Device_TableHead_CanSleep": "", + "Device_TableHead_AlertEvents": "Оповещения о событиях", + "Device_TableHead_CanSleep": "Может спать", "Device_TableHead_Comments": "Комментарии", "Device_TableHead_Connected_Devices": "Соединения", "Device_TableHead_CustomProps": "Свойства / Действия", @@ -265,7 +265,7 @@ "Device_TableHead_Rowid": "ID строки", "Device_TableHead_SSID": "SSID", "Device_TableHead_SourcePlugin": "Исходный плагин", - "Device_TableHead_StaticIP": "", + "Device_TableHead_StaticIP": "Статический IP", "Device_TableHead_Status": "Статус", "Device_TableHead_SyncHubNodeName": "Узел синхронизации", "Device_TableHead_Type": "Тип", @@ -341,7 +341,7 @@ "Gen_Copy": "Запустить", "Gen_CopyToClipboard": "Копировать в буфер обмена", "Gen_DataUpdatedUITakesTime": "ОК - Обновление UI может занять некоторое время, если сканирование выполняется.", - "Gen_Day": "", + "Gen_Day": "День", "Gen_Delete": "Удалить", "Gen_DeleteAll": "Удалить все", "Gen_Description": "Описание", @@ -350,11 +350,11 @@ "Gen_Filter": "Фильтр", "Gen_Flapping": "Нестабильный", "Gen_Generate": "Генерировать", - "Gen_Hour": "", + "Gen_Hour": "Час", "Gen_InvalidMac": "Неверный Mac-адрес.", "Gen_Invalid_Value": "Введено некорректное значение", "Gen_LockedDB": "ОШИБКА - Возможно, база данных заблокирована. Проверьте инструменты разработчика F12 -> Консоль или повторите попытку позже.", - "Gen_Month": "", + "Gen_Month": "Месяц", "Gen_NetworkMask": "Маска сети", "Gen_New": "Новый", "Gen_Next": "Следующий", @@ -386,7 +386,7 @@ "Gen_Update_Value": "Обновить значение", "Gen_ValidIcon": "", "Gen_Warning": "Предупреждение", - "Gen_Week": "", + "Gen_Week": "Неделя", "Gen_Work_In_Progress": "Работа продолжается, самое время оставить отзыв на https://github.com/netalertx/NetAlertX/issues", "Gen_create_new_device": "Новое устройство", "Gen_create_new_device_info": "Устройства обычно обнаруживаются с помощью плагинов. Однако в некоторых случаях вам может потребоваться добавить устройства вручную. Для изучения конкретных сценариев ознакомьтесь с документацией Remote Networks.", @@ -716,14 +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_Performance": "Производительность", + "Systeminfo_Performance_CPU": "CPU %", + "Systeminfo_Performance_Disabled": "Сбор истории ресурсов в настоящее время отключен. Включите его в настройках.", + "Systeminfo_Performance_Duration": "Продолжительность сканирования (сек.)", + "Systeminfo_Performance_IO_Caption": "Только ввод/вывод прямого процесса NetAlertX - подпроцесс ввода/вывода плагина (например, сканер, записывающий свой собственный файл результатов) не включен.", + "Systeminfo_Performance_IO_Read": "Чтение ввода-вывода (Мб)", + "Systeminfo_Performance_IO_Write": "Запись ввода-вывода (Мб)", + "Systeminfo_Performance_Memory": "Использование памяти (Мб)", "Systeminfo_Services": "Службы", "Systeminfo_Services_Description": "Описание службы", "Systeminfo_Services_Name": "Имя службы", @@ -852,4 +852,4 @@ "settings_system_label": "Система", "settings_update_item_warning": "Обновить значение ниже. Будьте осторожны, следуя предыдущему формату. Проверка не выполняется.", "test_event_tooltip": "Сначала сохраните изменения, прежде чем проверять настройки." -} \ No newline at end of file +} From 5a060fb779dd5a2d15fd337de66f5162d31c5e74 Mon Sep 17 00:00:00 2001 From: Luis Perez Date: Sat, 3 Oct 2026 13:41:41 -0700 Subject: [PATCH 2/4] Fix spelling errors in DEVICE_MANAGEMENT.md Corrected spelling errors for 'address' in the Main Info section. --- docs/DEVICE_MANAGEMENT.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/DEVICE_MANAGEMENT.md b/docs/DEVICE_MANAGEMENT.md index 1f2616e05..e17c8d6a6 100755 --- a/docs/DEVICE_MANAGEMENT.md +++ b/docs/DEVICE_MANAGEMENT.md @@ -4,15 +4,15 @@ The Main Info section is where most of the device identifiable information is st > [!NOTE] > -> You can multi-edit devices by selecting them in the main Devices view, from the Mainetence section, or via the CSV Export functionality under Maintenance. More info can be found in the [Devices Bulk-editing docs](./DEVICES_BULK_EDITING.md). +> You can multi-edit devices by selecting them in the main Devices view, from the Maintenance section, or via the CSV Export functionality under Maintenance. More info can be found in the [Devices Bulk-editing docs](./DEVICES_BULK_EDITING.md). ![Main Info](./img/DEVICE_MANAGEMENT/DeviceManagement_MainInfo.png) ## Main Info - - **MAC**: MAC addres of the device. Not editable, unless creating a new dummy device. - - **Last IP**: IP addres of the device. Not editable, unless creating a new dummy device. + - **MAC**: MAC address of the device. Not editable, unless creating a new dummy device. + - **Last IP**: IP address of the device. Not editable, unless creating a new dummy device. - **Name**: Friendly device name. Autodetected via various 🆎 Name discovery [plugins](https://docs.netalertx.com/PLUGINS_OVERVIEW). The app attaches `(IP match)` if the name is discovered via an IP match and not MAC match which could mean the name could be incorrect as IPs might change. - **Icon**: Partially autodetected. Select an existing or [add a custom icon](./ICONS.md). You can also auto-apply the same icon on all devices of the same type. - **Owner**: Device owner (The list is self-populated with existing owners and you can add custom values). From 071a4dded4596a46892696abf875565ab1312206 Mon Sep 17 00:00:00 2001 From: Alexander Shevchenko Date: Sun, 4 Oct 2026 16:45:18 +0200 Subject: [PATCH 3/4] fix(frontend): restore network topology rendering in Safari WebKit paints positioned HTML and opacity layers inside SVG foreignObject at the SVG origin. Device cards, labels and icons overlap there while their connections remain at the expected coordinates. Keep node content in normal flow, place collapse controls with a float and height-derived negative margin, and use alpha colors for muted icons. Scope all style changes to the network tree. Related: #1116 Discussion: https://github.com/netalertx/NetAlertX/discussions/1379 --- front/css/app.css | 18 ++++++++++++------ front/js/network-tree.js | 2 +- 2 files changed, 13 insertions(+), 7 deletions(-) diff --git a/front/css/app.css b/front/css/app.css index fdc2afbfc..b4fe82bc7 100755 --- a/front/css/app.css +++ b/front/css/app.css @@ -1924,6 +1924,8 @@ textarea[readonly], #networkTree .box { + /* Positioned HTML inside SVG foreignObject is misplaced by WebKit. */ + position:static; border-width:1px; border-top-color:grey; padding:0px; @@ -1946,7 +1948,7 @@ textarea[readonly], } #networkTree .netNodeText { - position: absolute; + position: static; } #networkTree .netPort { @@ -1957,7 +1959,8 @@ textarea[readonly], #networkTree .portBckgIcon { - opacity: 0.3; + color: var(--color-gray); + color: color-mix(in srgb, currentColor 30%, transparent); display: initial; float: left; width: 1em; @@ -2034,8 +2037,8 @@ textarea[readonly], #networkTree .netCollapse { - position: absolute; - right: 0; + position: static; + float: right; margin-right: -3px; } @@ -2058,9 +2061,12 @@ textarea[readonly], #networkTree .network-hw-icon { - position: absolute; + position: static; + display: inline-block; margin-left: -0.4em; - opacity: 0.3; + /* Alpha in the color avoids WebKit's foreignObject opacity layer bug. */ + color: var(--color-gray); + color: color-mix(in srgb, currentColor 30%, transparent); margin-top: 0.1em; } diff --git a/front/js/network-tree.js b/front/js/network-tree.js index 26ebcce59..7dc6fcc83 100644 --- a/front/js/network-tree.js +++ b/front/js/network-tree.js @@ -281,7 +281,7 @@ function initTree(myHierarchy) // generate +/- icon if node has children nodes collapseExpandHtml = nodeData.data.hasChildren ? `
From 1aceeae2183ffcfac84d7f54d371ae00abfb105e Mon Sep 17 00:00:00 2001 From: Alexander Shevchenko Date: Sun, 4 Oct 2026 16:45:29 +0200 Subject: [PATCH 4/4] test(frontend): cover network topology rendering and interactions Add a standalone fixture using the real topology renderer and styles. Check painted pixels in WebKit, Chromium and Firefox because DOM bounds can remain correct when WebKit paints foreignObject content incorrectly. Exercise node clicks, collapse/expand, wheel zoom and drag pan, including assertions that pointer gestures change the SVG transform. Document dependency installation and execution separately from pytest. The regression fails with the original frontend and passes with the fix. --- test/ui/TESTING_GUIDE.md | 20 +++++ test/ui/fixtures/network_topology.html | 41 +++++++++ test/ui/test_network_topology.cjs | 119 +++++++++++++++++++++++++ 3 files changed, 180 insertions(+) create mode 100644 test/ui/fixtures/network_topology.html create mode 100644 test/ui/test_network_topology.cjs diff --git a/test/ui/TESTING_GUIDE.md b/test/ui/TESTING_GUIDE.md index 6afef9b1f..66632ab09 100644 --- a/test/ui/TESTING_GUIDE.md +++ b/test/ui/TESTING_GUIDE.md @@ -413,4 +413,24 @@ Tests that the settings form submits correctly to `php/server/util.php` with `fu Verifies that all settings are preserved when saved (no data loss during save operation). **Key Coverage**: Form submission flow → PHP `saveSettings()` → Config file generation with Python-compatible formatting +# Network topology rendering in WebKit, Chromium and Firefox + +The standalone regression uses the real Treeviz bundle, network renderer and styles +with four fixture devices. No running NetAlertX backend or database is required. +It checks painted pixels, node clicks, collapse/expand, wheel zoom and drag pan. +Pixel checks are necessary because WebKit may return correct DOM bounds while +painting positioned or translucent HTML at the SVG origin instead. + +Run from the repository root (Node.js required): + +```sh +npm install --prefix /tmp/netalertx-browser-tests playwright pngjs +/tmp/netalertx-browser-tests/node_modules/.bin/playwright install --with-deps webkit chromium firefox +NODE_PATH=/tmp/netalertx-browser-tests/node_modules node test/ui/test_network_topology.cjs +``` + +Screenshots are written to `/tmp/netalertx-topology`, or to the directory specified +by `NETALERTX_TOPOLOGY_ARTIFACTS`. This regression runs separately from pytest. +Also verify the fixture in native Safari when a Mac is available; Playwright +WebKit is useful regression coverage but is not the Safari application. diff --git a/test/ui/fixtures/network_topology.html b/test/ui/fixtures/network_topology.html new file mode 100644 index 000000000..fe7a4bf90 --- /dev/null +++ b/test/ui/fixtures/network_topology.html @@ -0,0 +1,41 @@ + + + +NetAlertX network topology regression fixture + + + + + + +

Network topology — discussion #1379

+
+ + + + + diff --git a/test/ui/test_network_topology.cjs b/test/ui/test_network_topology.cjs new file mode 100644 index 000000000..6ecc5053a --- /dev/null +++ b/test/ui/test_network_topology.cjs @@ -0,0 +1,119 @@ +// Standalone browser regression for SVG foreignObject rendering; see TESTING_GUIDE.md. +const assert = require('node:assert/strict'); +const fs = require('node:fs'); +const http = require('node:http'); +const os = require('node:os'); +const path = require('node:path'); +const {chromium, firefox, webkit} = require('playwright'); +const {PNG} = require('pngjs'); + +/** Verify actual painted pixels, since WebKit can report correct bounds for misplaced HTML. */ +async function assertPainted(page, selector) { + const items = await page.locator(selector).all(); + assert(items.length > 0, selector + ' is missing'); + for (const item of items) { + const rect = await item.boundingBox(); + assert(rect && rect.width > 0 && rect.height > 0, selector + ' has no bounds'); + const png = PNG.sync.read(await page.screenshot({clip: { + x: Math.ceil(rect.x + 1), y: Math.ceil(rect.y + 2), + width: Math.max(1, Math.floor(rect.width - 2)), height: Math.max(1, Math.floor(rect.height - 4)) + }})); + let pixels = 0; + for (let i = 0; i < png.data.length; i += 4) { + if (png.data[i] < 240 && png.data[i + 1] < 240 && png.data[i + 2] < 240) pixels++; + } + assert(pixels >= 3, selector + ' is not painted at its SVG coordinates'); + } +} + +/** Serve the fixture and repository assets on an ephemeral loopback port. */ +function createServer() { + const root = path.resolve(__dirname, '../..'); + return http.createServer((request, response) => { + const pathname = new URL(request.url, 'http://localhost').pathname; + const file = path.resolve(root, '.' + pathname); + if (!file.startsWith(root + path.sep)) { + response.writeHead(403).end(); + return; + } + fs.readFile(file, (error, data) => { + if (error) { + response.writeHead(404).end(); + return; + } + const types = {'.html': 'text/html', '.css': 'text/css', '.js': 'text/javascript'}; + response.writeHead(200, {'Content-Type': types[path.extname(file)] || 'application/octet-stream'}); + response.end(data); + }); + }); +} + +/** Check painting and real pointer interactions before and after tree updates and transforms. */ +async function checkBrowser(name, engine, url, artifacts) { + const browser = await engine.launch(); + try { + const page = await browser.newPage({viewport: {width: 1200, height: 800}}); + const errors = []; + page.on('pageerror', error => errors.push(error.message)); + await page.goto(url); + await page.waitForFunction(() => document.querySelectorAll('#networkTree g.node').length === 4); + // Treeviz transitions take 600 ms; wait for both them and icon fonts to settle. + await page.evaluate(() => document.fonts.ready); + await page.waitForTimeout(900); + await page.screenshot({path: path.join(artifacts, name + '-initial.png')}); + for (const selector of ['.spanNetworkTree', '.netIcon', '.portBckgIcon', '.network-hw-icon', '.netCollapse']) { + await assertPainted(page, '#networkTree ' + selector); + } + const laptop = page.locator('.node-inner[data-mac="aa:bb:cc:dd:ee:02"] .spanNetworkTree'); + await laptop.click(); + assert.deepEqual(await page.evaluate(() => clicks), ['aa:bb:cc:dd:ee:02']); + const collapse = page.locator('.netCollapse[data-mytreemac="aa:bb:cc:dd:ee:01"]'); + await collapse.click(); + await page.waitForFunction(() => document.querySelectorAll('#networkTree g.node').length === 2); + await page.waitForTimeout(700); + await collapse.click(); + await page.waitForFunction(() => document.querySelectorAll('#networkTree g.node').length === 4); + await page.waitForTimeout(700); + await assertPainted(page, '#networkTree .spanNetworkTree'); + // Exercise Treeviz/D3's actual wheel zoom and drag pan handlers. + const canvas = page.locator('#networkTree svg > g'); + const beforeZoom = await canvas.getAttribute('transform'); + await page.mouse.move(600, 400); + await page.mouse.wheel(0, 300); + await page.waitForTimeout(500); + const afterZoom = await canvas.getAttribute('transform'); + assert.notEqual(afterZoom, beforeZoom, 'Wheel zoom did not change the SVG transform'); + await page.mouse.move(600, 400); + await page.mouse.down(); + await page.mouse.move(650, 440, {steps: 5}); + await page.mouse.up(); + assert.notEqual(await canvas.getAttribute('transform'), afterZoom, 'Drag pan did not change the SVG transform'); + await assertPainted(page, '#networkTree .spanNetworkTree'); + await laptop.click(); + assert.equal((await page.evaluate(() => clicks)).length, 2); + await page.screenshot({path: path.join(artifacts, name + '-zoom-pan.png')}); + // The bundled Treeviz demo expects #tree, which network.php also does not provide. + assert.deepEqual(errors.filter(error => error !== 'Cannot find dom element with id:tree'), []); + console.log(name + ': PASS — painting, clicks, collapse/expand, zoom and pan'); + } finally { + await browser.close(); + } +} + +/** Run the same rendering checks in WebKit, Chromium and Firefox without an app backend. */ +async function main() { + const artifacts = process.env.NETALERTX_TOPOLOGY_ARTIFACTS || path.join(os.tmpdir(), 'netalertx-topology'); + fs.mkdirSync(artifacts, {recursive: true}); + const server = createServer(); + await new Promise(resolve => server.listen(0, '127.0.0.1', resolve)); + const url = `http://127.0.0.1:${server.address().port}/test/ui/fixtures/network_topology.html`; + try { + for (const [name, engine] of Object.entries({webkit, chromium, firefox})) { + await checkBrowser(name, engine, url, artifacts); + } + } finally { + await new Promise(resolve => server.close(resolve)); + } +} + +main().catch(error => { console.error(error); process.exitCode = 1; });