Merge pull request #1835 from netalertx/main

sync
This commit is contained in:
Jokob @NetAlertX authored and GitHub committed 2026-10-05 08:44:18 +11:00
commit 735f51027c
7 files changed
+212 -26

No files matched your search

+3 -3
View File
@@ -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).
+12 -6
View File
@@ -1944,6 +1944,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;
@@ -1966,7 +1968,7 @@ textarea[readonly],
}
#networkTree .netNodeText
{
position: absolute;
position: static;
}
#networkTree .netPort
{
@@ -1977,7 +1979,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;
@@ -2054,8 +2057,8 @@ textarea[readonly],
#networkTree .netCollapse
{
position: absolute;
right: 0;
position: static;
float: right;
margin-right: -3px;
}
@@ -2078,9 +2081,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;
}
+1 -1
View File
@@ -281,7 +281,7 @@ function initTree(myHierarchy)
// generate +/- icon if node has children nodes
collapseExpandHtml = nodeData.data.hasChildren ?
`<div class="netCollapse"
style="font-size:${nodeHeightPx/2}px;top:${Math.floor(nodeHeightPx / 4)}px"
style="font-size:${nodeHeightPx/2}px;margin-top:-${nodeHeightPx - Math.floor(nodeHeightPx / 4)}px"
data-mytreepath="${nodeData.data.path}"
data-mytreemac="${nodeData.data.devMac}">
<i class="fa fa-${collapseExpandIcon} pointer"></i>
+16 -16
View File
@@ -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": "<i class=\"fa-solid fa-chevron-right \"></i>",
"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": "Устройства обычно обнаруживаются с помощью <a target=\"_blank\" href=\"https://docs.netalertx.com/PLUGINS\">плагинов</a>. Однако в некоторых случаях вам может потребоваться добавить устройства вручную. Для изучения конкретных сценариев ознакомьтесь с документацией <a target=\"_blank\" href=\"https://docs.netalertx.com/REMOTE_NETWORKS\">Remote Networks</a>.",
@@ -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": "Сбор истории ресурсов в настоящее время отключен. <a onclick=\"toggleAllSettings()\" href=\"#MAINT_PERF_DAYS\">Включите его в настройках</a>.",
"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": "Имя службы",
@@ -854,4 +854,4 @@
"settings_system_label": "Система",
"settings_update_item_warning": "Обновить значение ниже. Будьте осторожны, следуя предыдущему формату. <b>Проверка не выполняется.</b>",
"test_event_tooltip": "Сначала сохраните изменения, прежде чем проверять настройки."
}
}
+20
View File
@@ -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.
+41
View File
@@ -0,0 +1,41 @@
<!doctype html>
<html lang="en">
<meta charset="utf-8">
<title>NetAlertX network topology regression fixture</title>
<link rel="stylesheet" href="/front/lib/bootstrap/bootstrap.min.css">
<link rel="stylesheet" href="/front/lib/AdminLTE/dist/css/AdminLTE.css">
<link rel="stylesheet" href="/front/lib/font-awesome/all.min.css">
<link rel="stylesheet" href="/front/css/app.css">
<body>
<style>body { background: #fff; }</style>
<h1>Network topology — discussion #1379</h1>
<div class="networkTable"><div id="networkTree"></div></div>
<script src="/front/lib/jquery/jquery.min.js"></script>
<script src="/front/lib/treeviz/treeviz.iife.js"></script>
<script src="/front/js/network-tree.js"></script>
<script>
var emptyArr = [null, undefined, ""];
var clicks = [];
/** Provide a neutral device badge for fixed fixture data. */
function badgeFromDevice() { return {cssText: ""}; }
/** Return the fixed, trusted fixture labels. */
function encodeSpecialChars(value) { return String(value); }
/** Keep fixture timestamps independent of locale. */
function localizeTimestamp(value) { return value; }
/** Provide a fixed link color for visual assertions. */
function getRelationshipConf() { return {color: "#336c87"}; }
/** Attach the production collapse handlers after initialization. */
function hideSpinner() { attachTreeEvents(); }
/** Record node clicks for pointer interaction assertions. */
function handleNodeClick(node) { clicks.push(node.dataset.mac); }
/** Remove the collapsed MAC using the application helper contract. */
function removeItemFromArray(items, item) { items.splice(items.indexOf(item), 1); }
deviceListGlobal = [
{devMac: "internet", devName: "Internet", devParentMAC: "", devIsNetworkNodeDynamic: 1},
{devMac: "aa:bb:cc:dd:ee:01", devName: "Router", devParentMAC: "internet", devIsNetworkNodeDynamic: 1},
{devMac: "aa:bb:cc:dd:ee:02", devName: "Laptop", devParentMAC: "aa:bb:cc:dd:ee:01", devParentPort: "1"},
{devMac: "aa:bb:cc:dd:ee:03", devName: "Phone", devParentMAC: "aa:bb:cc:dd:ee:01", devSSID: "Home Wi-Fi"}
].map(device => ({devIcon: btoa('<i class="fa fa-desktop"></i>'), devSSID: "", devParentPort: "", devLastIP: "192.0.2.1", devVendor: "Test", devType: "Device", devParentRelType: "wired", devVlan: "", ...device}));
initTree(getHierarchy());
</script>
</body></html>
+119
View File
@@ -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; });