mirror of
https://github.com/jokob-sk/NetAlertX.git
synced 2026-03-31 15:22:26 -07:00
Compare commits
45 Commits
feat-proxm
...
v26.3.15
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
bff87f4d61 | ||
|
|
6f7d2c3253 | ||
|
|
0766fb2de6 | ||
|
|
d19cb3d679 | ||
|
|
9b71c210b2 | ||
|
|
c9cb1f3fba | ||
|
|
78a8030c6a | ||
|
|
b5b0bcc766 | ||
|
|
13515603e4 | ||
|
|
518608cffc | ||
|
|
df3ca50c5c | ||
|
|
93fc126da2 | ||
|
|
a60ec9ed3a | ||
|
|
e1d206ca74 | ||
|
|
2771a6e9c2 | ||
|
|
aba1ddd3df | ||
|
|
165c9d3baa | ||
|
|
0b0c88f712 | ||
|
|
d49abd9d02 | ||
|
|
abf024d4d3 | ||
|
|
4eb5947ceb | ||
|
|
1d1a8045a0 | ||
|
|
f8c09d35a7 | ||
|
|
d8d090404e | ||
|
|
5a6de6d832 | ||
|
|
05b63cb730 | ||
|
|
2921614eac | ||
|
|
17d95d802f | ||
|
|
a0048980b8 | ||
|
|
89811cd133 | ||
|
|
b854206599 | ||
|
|
a532c98115 | ||
|
|
da23880eb1 | ||
|
|
c73ce839f2 | ||
|
|
5c0f29b97c | ||
|
|
f1496b483b | ||
|
|
ba26f34191 | ||
|
|
37f8a44cb3 | ||
|
|
76a259d9e5 | ||
|
|
8ab9d9f395 | ||
|
|
c1d53ff93f | ||
|
|
a329c5b541 | ||
|
|
0555105473 | ||
|
|
b0aa5d0e45 | ||
|
|
93df52f70c |
6
.github/skills/code-standards/SKILL.md
vendored
6
.github/skills/code-standards/SKILL.md
vendored
@@ -5,12 +5,12 @@ description: NetAlertX coding standards and conventions. Use this when writing c
|
||||
|
||||
# Code Standards
|
||||
|
||||
- ask me to review before going to each next step (mention n step out of x)
|
||||
- before starting, prepare implementation plan
|
||||
- ask me to review before going to each next step (mention n step out of x) (AI only)
|
||||
- before starting, prepare implementation plan (AI only)
|
||||
- ask me to review it and ask any clarifying questions first
|
||||
- add test creation as last step - follow repo architecture patterns - do not place in the root of /test
|
||||
- code has to be maintainable, no duplicate code
|
||||
- follow DRY principle
|
||||
- follow DRY principle - maintainability of code is more important than speed of implementation
|
||||
- code files should be less than 500 LOC for better maintainability
|
||||
|
||||
## File Length
|
||||
|
||||
16
.github/workflows/run-all-tests.yml
vendored
16
.github/workflows/run-all-tests.yml
vendored
@@ -3,6 +3,10 @@ name: 🧪 Manual Test Suite Selector
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
run_all:
|
||||
description: '✅ Run ALL tests (overrides individual selectors)'
|
||||
type: boolean
|
||||
default: false
|
||||
run_scan:
|
||||
description: '📂 scan/ (Scan, Logic, Locks, IPs)'
|
||||
type: boolean
|
||||
@@ -23,6 +27,10 @@ on:
|
||||
description: '📂 ui/ (Selenium & Dashboard)'
|
||||
type: boolean
|
||||
default: false
|
||||
run_plugins:
|
||||
description: '📂 plugins/ (Sync insert schema-aware logic)'
|
||||
type: boolean
|
||||
default: false
|
||||
run_root_files:
|
||||
description: '📄 Root Test Files (WOL, Atomicity, etc.)'
|
||||
type: boolean
|
||||
@@ -42,12 +50,20 @@ jobs:
|
||||
id: builder
|
||||
run: |
|
||||
PATHS=""
|
||||
|
||||
# run_all overrides everything
|
||||
if [ "${{ github.event.inputs.run_all }}" == "true" ]; then
|
||||
echo "final_paths=test/" >> $GITHUB_OUTPUT
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Folder Mapping with 'test/' prefix
|
||||
if [ "${{ github.event.inputs.run_scan }}" == "true" ]; then PATHS="$PATHS test/scan/"; fi
|
||||
if [ "${{ github.event.inputs.run_api }}" == "true" ]; then PATHS="$PATHS test/api_endpoints/ test/server/"; fi
|
||||
if [ "${{ github.event.inputs.run_backend }}" == "true" ]; then PATHS="$PATHS test/backend/ test/db/"; fi
|
||||
if [ "${{ github.event.inputs.run_docker_env }}" == "true" ]; then PATHS="$PATHS test/docker_tests/"; fi
|
||||
if [ "${{ github.event.inputs.run_ui }}" == "true" ]; then PATHS="$PATHS test/ui/"; fi
|
||||
if [ "${{ github.event.inputs.run_plugins }}" == "true" ]; then PATHS="$PATHS test/plugins/"; fi
|
||||
|
||||
# Root Files Mapping (files sitting directly in /test/)
|
||||
if [ "${{ github.event.inputs.run_root_files }}" == "true" ]; then
|
||||
|
||||
@@ -1,23 +1,23 @@
|
||||
# 🤝 Contributing to NetAlertX
|
||||
# Contributing to NetAlertX
|
||||
|
||||
First off, **thank you** for taking the time to contribute! NetAlertX is built and improved with the help of passionate people like you.
|
||||
|
||||
---
|
||||
|
||||
## 📂 Issues, Bugs, and Feature Requests
|
||||
## Issues, Bugs, and Feature Requests
|
||||
|
||||
Please use the [GitHub Issue Tracker](https://github.com/netalertx/NetAlertX/issues) for:
|
||||
- Bug reports 🐞
|
||||
- Feature requests 💡
|
||||
- Documentation feedback 📖
|
||||
- Bug reports
|
||||
- Feature requests
|
||||
- Documentation feedback
|
||||
|
||||
Before opening a new issue:
|
||||
- 🛑 [Check Common Issues & Debug Tips](https://docs.netalertx.com/DEBUG_TIPS#common-issues)
|
||||
- 🔍 [Search Closed Issues](https://github.com/netalertx/NetAlertX/issues?q=is%3Aissue+is%3Aclosed)
|
||||
- [Check Common Issues & Debug Tips](https://docs.netalertx.com/DEBUG_TIPS#common-issues)
|
||||
- [Search Closed Issues](https://github.com/netalertx/NetAlertX/issues?q=is%3Aissue+is%3Aclosed)
|
||||
|
||||
---
|
||||
|
||||
## 🚀 Submitting Pull Requests (PRs)
|
||||
## Submitting Pull Requests (PRs)
|
||||
|
||||
We welcome PRs to improve the code, docs, or UI!
|
||||
|
||||
@@ -29,9 +29,14 @@ Please:
|
||||
- If relevant, add or update tests and documentation
|
||||
- For plugins, refer to the [Plugin Dev Guide](https://docs.netalertx.com/PLUGINS_DEV)
|
||||
|
||||
|
||||
## Code quality
|
||||
|
||||
- read and follow the [code-standards](/.github/skills/code-standards/SKILL.md)
|
||||
|
||||
---
|
||||
|
||||
## 🌟 First-Time Contributors
|
||||
## First-Time Contributors
|
||||
|
||||
New to open source? Check out these resources:
|
||||
- [How to Fork and Submit a PR](https://opensource.guide/how-to-contribute/)
|
||||
@@ -39,15 +44,15 @@ New to open source? Check out these resources:
|
||||
|
||||
---
|
||||
|
||||
## 🔐 Code of Conduct
|
||||
## Code of Conduct
|
||||
|
||||
By participating, you agree to follow our [Code of Conduct](./CODE_OF_CONDUCT.md), which ensures a respectful and welcoming community.
|
||||
|
||||
---
|
||||
|
||||
## 📬 Contact
|
||||
## Contact
|
||||
|
||||
If you have more in-depth questions or want to discuss contributing in other ways, feel free to reach out at:
|
||||
📧 [jokob@duck.com](mailto:jokob@duck.com?subject=NetAlertX%20Contribution)
|
||||
[jokob.sk@gmail.com](mailto:jokob.sk@gmail.com?subject=NetAlertX%20Contribution)
|
||||
|
||||
We appreciate every contribution, big or small! 💙
|
||||
|
||||
@@ -19,6 +19,9 @@ services:
|
||||
- CHOWN # Required for root-entrypoint to chown /data + /tmp before dropping privileges
|
||||
- SETUID # Required for root-entrypoint to switch to non-root user
|
||||
- SETGID # Required for root-entrypoint to switch to non-root group
|
||||
sysctls: # ARP flux mitigation for host networking accuracy
|
||||
net.ipv4.conf.all.arp_ignore: 1
|
||||
net.ipv4.conf.all.arp_announce: 2
|
||||
volumes:
|
||||
|
||||
- type: volume # Persistent Docker-managed Named Volume for storage
|
||||
|
||||
@@ -30,6 +30,17 @@ services:
|
||||
- CHOWN # Required for root-entrypoint to chown /data + /tmp before dropping privileges
|
||||
- SETUID # Required for root-entrypoint to switch to non-root user
|
||||
- SETGID # Required for root-entrypoint to switch to non-root group
|
||||
# --- ARP FLUX MITIGATION ---
|
||||
# Note: When using `network_mode: host`, these sysctls require the
|
||||
# NET_ADMIN capability to be applied to the host namespace.
|
||||
#
|
||||
# If your environment restricts capabilities, or you prefer to configure
|
||||
# them on the Host OS, REMOVE the sysctls block below and apply via:
|
||||
# sudo sysctl -w net.ipv4.conf.all.arp_ignore=1 net.ipv4.conf.all.arp_announce=2
|
||||
# ---------------------------
|
||||
sysctls: # ARP flux mitigation (reduces duplicate/ambiguous ARP behavior on host networking)
|
||||
net.ipv4.conf.all.arp_ignore: 1
|
||||
net.ipv4.conf.all.arp_announce: 2
|
||||
|
||||
volumes:
|
||||
- type: volume # Persistent Docker-managed named volume for config + database
|
||||
|
||||
71
docs/docker-troubleshooting/arp-flux-sysctls.md
Normal file
71
docs/docker-troubleshooting/arp-flux-sysctls.md
Normal file
@@ -0,0 +1,71 @@
|
||||
# ARP Flux Sysctls Not Set
|
||||
|
||||
## Issue Description
|
||||
|
||||
NetAlertX detected that ARP flux protection sysctls are not set as expected:
|
||||
|
||||
- `net.ipv4.conf.all.arp_ignore=1`
|
||||
- `net.ipv4.conf.all.arp_announce=2`
|
||||
|
||||
## Security Ramifications
|
||||
|
||||
This is not a direct container breakout risk, but detection quality can degrade:
|
||||
|
||||
- Incorrect IP/MAC associations
|
||||
- Device state flapping
|
||||
- Unreliable topology or presence data
|
||||
|
||||
## Why You're Seeing This Issue
|
||||
|
||||
The running environment does not provide the expected kernel sysctl values. This is common in Docker setups where sysctls were not explicitly configured.
|
||||
|
||||
## How to Correct the Issue
|
||||
|
||||
### Option A: Via Docker (Standard Bridge Networking or `network_mode: host` with `NET_ADMIN`)
|
||||
|
||||
If you are using standard bridged networking, or `network_mode: host` and the container is granted the `NET_ADMIN` capability (as is the default recommendation), set these sysctls at container runtime.
|
||||
|
||||
- In `docker-compose.yml` (preferred):
|
||||
```yaml
|
||||
services:
|
||||
netalertx:
|
||||
sysctls:
|
||||
net.ipv4.conf.all.arp_ignore: 1
|
||||
net.ipv4.conf.all.arp_announce: 2
|
||||
```
|
||||
|
||||
- For `docker run`:
|
||||
```bash
|
||||
docker run \
|
||||
--sysctl net.ipv4.conf.all.arp_ignore=1 \
|
||||
--sysctl net.ipv4.conf.all.arp_announce=2 \
|
||||
ghcr.io/netalertx/netalertx:latest
|
||||
```
|
||||
|
||||
> **Note:** Setting `net.ipv4.conf.all.arp_ignore` and `net.ipv4.conf.all.arp_announce` may fail with "operation not permitted" unless the container is run with elevated privileges. To resolve this, you can:
|
||||
> - Use `--privileged` with `docker run`.
|
||||
> - Use the more restrictive `--cap-add=NET_ADMIN` (or `cap_add: [NET_ADMIN]` in `docker-compose` service definitions) to allow the sysctls to be applied at runtime.
|
||||
|
||||
### Option B: Via Host OS (Fallback for `network_mode: host`)
|
||||
|
||||
If you are running the container with `network_mode: host` and cannot grant the `NET_ADMIN` capability, or if your container runtime environment explicitly blocks sysctl overrides, applying these settings via the container configuration will fail. Attempting to do so without sufficient privileges typically results in an OCI runtime error: `sysctl "net.ipv4.conf.all.arp_announce" not allowed in host network namespace`.
|
||||
|
||||
In this scenario, you must apply the settings directly on your host operating system:
|
||||
|
||||
1. **Remove** the `sysctls` section from your `docker-compose.yml`.
|
||||
2. **Apply** on the host immediately:
|
||||
```bash
|
||||
sudo sysctl -w net.ipv4.conf.all.arp_ignore=1
|
||||
sudo sysctl -w net.ipv4.conf.all.arp_announce=2
|
||||
```
|
||||
3. **Make persistent** by adding the following lines to `/etc/sysctl.conf` on the host:
|
||||
```text
|
||||
net.ipv4.conf.all.arp_ignore=1
|
||||
net.ipv4.conf.all.arp_announce=2
|
||||
```
|
||||
|
||||
## Additional Resources
|
||||
|
||||
For broader Docker Compose guidance, see:
|
||||
|
||||
- [DOCKER_COMPOSE.md](https://docs.netalertx.com/DOCKER_COMPOSE)
|
||||
@@ -299,7 +299,7 @@ function updateChevrons(currentMac) {
|
||||
|
||||
showSpinner();
|
||||
|
||||
cacheDevices().then(() => {
|
||||
cacheDevices(true).then(() => {
|
||||
hideSpinner();
|
||||
|
||||
// Retry after re-caching
|
||||
@@ -507,7 +507,7 @@ function updateDevicePageName(mac) {
|
||||
if (mac != 'new' && (name === null|| owner === null)) {
|
||||
console.warn("Device not found in cache, retrying after re-cache:", mac);
|
||||
showSpinner();
|
||||
cacheDevices().then(() => {
|
||||
cacheDevices(true).then(() => {
|
||||
hideSpinner();
|
||||
// Retry after successful cache
|
||||
updateDevicePageName(mac);
|
||||
|
||||
@@ -53,7 +53,12 @@
|
||||
<div class="col-md-12">
|
||||
<div class="box" id="clients">
|
||||
<div class="box-header ">
|
||||
<h3 class="box-title col-md-12"><?= lang('Device_Shortcut_OnlineChart');?> </h3>
|
||||
<h3 class="box-title"><?= lang('Device_Shortcut_OnlineChart');?> </h3>
|
||||
<div class="box-tools pull-right">
|
||||
<button type="button" class="btn btn-box-tool" data-widget="collapse">
|
||||
<i class="fa fa-minus"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="box-body">
|
||||
<div class="chart">
|
||||
@@ -72,10 +77,15 @@
|
||||
<!-- Device Filters ------------------------------------------------------- -->
|
||||
<div class="box box-aqua hidden" id="columnFiltersWrap">
|
||||
<div class="box-header ">
|
||||
<h3 class="box-title col-md-12"><?= lang('Devices_Filters');?> </h3>
|
||||
<h3 class="box-title"><?= lang('Devices_Filters');?> </h3>
|
||||
<div class="box-tools pull-right">
|
||||
<button type="button" class="btn btn-box-tool" data-widget="collapse">
|
||||
<i class="fa fa-minus"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Placeholder ------------------------------------------------------- -->
|
||||
<div id="columnFilters" ></div>
|
||||
<div class="box-body" id="columnFilters"></div>
|
||||
</div>
|
||||
|
||||
<!-- datatable ------------------------------------------------------------- -->
|
||||
@@ -148,6 +158,20 @@
|
||||
// DEVICE_COLUMN_FIELDS, COL, NUMERIC_DEFAULTS, GRAPHQL_EXTRA_FIELDS, COLUMN_NAME_MAP
|
||||
// are all defined in js/device-columns.js — edit that file to add new columns.
|
||||
|
||||
// Collapse DevicePresence and Filters sections by default on small/mobile screens
|
||||
(function collapseOnMobile() {
|
||||
if (window.innerWidth < 768) {
|
||||
['#clients', '#columnFiltersWrap'].forEach(function(sel) {
|
||||
var $box = $(sel);
|
||||
if ($box.length) {
|
||||
$box.addClass('collapsed-box');
|
||||
$box.find('.box-body, .box-footer').hide();
|
||||
$box.find('[data-widget="collapse"] i').removeClass('fa-minus').addClass('fa-plus');
|
||||
}
|
||||
});
|
||||
}
|
||||
})();
|
||||
|
||||
// Read parameters & Initialize components
|
||||
callAfterAppInitialized(main)
|
||||
showSpinner();
|
||||
@@ -472,12 +496,9 @@ function renderFilters(customData) {
|
||||
// Collect filters
|
||||
const columnFilters = collectFilters();
|
||||
|
||||
// Update DataTable with the new filters or search value (if applicable)
|
||||
$('#tableDevices').DataTable().draw();
|
||||
|
||||
// Optionally, apply column filters (if using filters for individual columns)
|
||||
// Apply column filters then draw once (previously drew twice — bug fixed).
|
||||
const table = $('#tableDevices').DataTable();
|
||||
table.columnFilters = columnFilters; // Apply your column filters logic
|
||||
table.columnFilters = columnFilters;
|
||||
table.draw();
|
||||
});
|
||||
|
||||
@@ -603,6 +624,10 @@ function hasEnabledDeviceScanners() {
|
||||
// Update the title-bar ETA subtitle and the DataTables empty-state message.
|
||||
// Called on every nax:scanEtaUpdate; the inner ticker keeps the title bar live between events.
|
||||
function updateScanEtaDisplay(nextScanTime, currentState) {
|
||||
// Detect scan-finished transition BEFORE updating _currentStateAnchor.
|
||||
// justFinishedScanning is true only when the backend transitions scanning → idle.
|
||||
var justFinishedScanning = (currentState === 'Process: Idle') && isScanningState(_currentStateAnchor);
|
||||
|
||||
// Prefer the backend-computed values; keep previous anchors if not yet received.
|
||||
_nextScanTimeAnchor = nextScanTime || _nextScanTimeAnchor;
|
||||
_currentStateAnchor = currentState || _currentStateAnchor;
|
||||
@@ -636,12 +661,26 @@ function updateScanEtaDisplay(nextScanTime, currentState) {
|
||||
eta.style.display = '';
|
||||
}
|
||||
|
||||
// Update DataTables empty message once per SSE event — avoids AJAX spam on server-side tables.
|
||||
// Update DataTables empty message once per SSE event.
|
||||
// NOTE: Do NOT call dt.draw() here — on page load the SSE queue replays all
|
||||
// accumulated events at once, causing a draw() (= GraphQL AJAX call) per event.
|
||||
// Instead, update the visible empty-state DOM cell directly.
|
||||
var label = getEtaLabel();
|
||||
if ($.fn.DataTable.isDataTable('#tableDevices')) {
|
||||
var dt = $('#tableDevices').DataTable();
|
||||
dt.settings()[0].oLanguage.sEmptyTable = buildEmptyDeviceTableMessage(label);
|
||||
if (dt.page.info().recordsTotal === 0) { dt.draw(false); }
|
||||
var newEmptyMsg = buildEmptyDeviceTableMessage(label);
|
||||
dt.settings()[0].oLanguage.sEmptyTable = newEmptyMsg;
|
||||
if (dt.page.info().recordsTotal === 0) {
|
||||
// Patch the visible cell text without triggering a server-side AJAX reload.
|
||||
$('#tableDevices tbody .dataTables_empty').html(newEmptyMsg);
|
||||
}
|
||||
|
||||
// When scanning just finished and the table is still empty, reload data so
|
||||
// newly discovered devices appear automatically. Skip reload if there are
|
||||
// already rows — no need to disturb the user's current view.
|
||||
if (justFinishedScanning && dt.page.info().recordsTotal === 0) {
|
||||
dt.ajax.reload(null, false); // false = keep current page position
|
||||
}
|
||||
}
|
||||
|
||||
tickTitleBar();
|
||||
@@ -728,6 +767,7 @@ function initializeDatatable (status) {
|
||||
${_gqlFields}
|
||||
}
|
||||
count
|
||||
dbCount
|
||||
}
|
||||
}
|
||||
`;
|
||||
@@ -768,9 +808,10 @@ function initializeDatatable (status) {
|
||||
console.log("Raw response:", res);
|
||||
const json = res["data"];
|
||||
|
||||
// Set the total number of records for pagination at the *root level* so DataTables sees them
|
||||
res.recordsTotal = json.devices.count || 0;
|
||||
res.recordsFiltered = json.devices.count || 0;
|
||||
// recordsTotal = raw DB count (before filters/search) so DataTables uses emptyTable
|
||||
// only when the DB is genuinely empty, and zeroRecords when a filter returns nothing.
|
||||
res.recordsTotal = json.devices.dbCount || 0;
|
||||
res.recordsFiltered = json.devices.count || 0;
|
||||
|
||||
// console.log("recordsTotal:", res.recordsTotal, "recordsFiltered:", res.recordsFiltered);
|
||||
// console.log("tableRows:", tableRows);
|
||||
@@ -1010,7 +1051,8 @@ function initializeDatatable (status) {
|
||||
// Processing
|
||||
'processing' : true,
|
||||
'language' : {
|
||||
emptyTable: buildEmptyDeviceTableMessage(getString('Device_NextScan_Imminent')),
|
||||
emptyTable: buildEmptyDeviceTableMessage(getString('Device_NextScan_Imminent')),
|
||||
zeroRecords: "<?= lang('Device_NoMatch_Title');?>",
|
||||
"lengthMenu": "<?= lang('Device_Tablelenght');?>",
|
||||
"search": "<?= lang('Device_Searchbox');?>: ",
|
||||
"paginate": {
|
||||
|
||||
@@ -451,11 +451,23 @@ function getDevDataByMac(macAddress, dbColumn) {
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// Cache the devices as one JSON
|
||||
function cacheDevices()
|
||||
/**
|
||||
* Fetches the full device list from table_devices.json and stores it in
|
||||
* localStorage under CACHE_KEYS.DEVICES_ALL.
|
||||
*
|
||||
* On subsequent calls the fetch is skipped if the initFlag is already set,
|
||||
* unless forceRefresh is true. Pass forceRefresh = true whenever the caller
|
||||
* knows the cached list may be stale (e.g. a device was not found by MAC and
|
||||
* the page needs to recover without a full clearCache()).
|
||||
*
|
||||
* @param {boolean} [forceRefresh=false] - When true, bypasses the initFlag
|
||||
* guard and always fetches fresh data from the server.
|
||||
* @returns {Promise<void>} Resolves when the cache has been populated.
|
||||
*/
|
||||
function cacheDevices(forceRefresh = false)
|
||||
{
|
||||
return new Promise((resolve, reject) => {
|
||||
if(getCache(CACHE_KEYS.initFlag('cacheDevices')) === "true")
|
||||
if(!forceRefresh && getCache(CACHE_KEYS.initFlag('cacheDevices')) === "true")
|
||||
{
|
||||
// One-time migration: normalize legacy { data: [...] } wrapper to a plain array.
|
||||
// Old cache entries from prior versions stored the raw API envelope; re-write
|
||||
|
||||
@@ -208,6 +208,7 @@
|
||||
"Device_NoData_Help": "",
|
||||
"Device_NoData_Scanning": "",
|
||||
"Device_NoData_Title": "",
|
||||
"Device_NoMatch_Title": "",
|
||||
"Device_Save_Failed": "",
|
||||
"Device_Save_Unauthorized": "",
|
||||
"Device_Saved_Success": "",
|
||||
|
||||
@@ -208,6 +208,7 @@
|
||||
"Device_NoData_Help": "",
|
||||
"Device_NoData_Scanning": "",
|
||||
"Device_NoData_Title": "",
|
||||
"Device_NoMatch_Title": "",
|
||||
"Device_Save_Failed": "Problemes guardant el dispositiu",
|
||||
"Device_Save_Unauthorized": "Token invàlid - No autoritzat",
|
||||
"Device_Saved_Success": "S'ha guardat el dispositiu",
|
||||
|
||||
@@ -208,6 +208,7 @@
|
||||
"Device_NoData_Help": "",
|
||||
"Device_NoData_Scanning": "",
|
||||
"Device_NoData_Title": "",
|
||||
"Device_NoMatch_Title": "",
|
||||
"Device_Save_Failed": "",
|
||||
"Device_Save_Unauthorized": "",
|
||||
"Device_Saved_Success": "",
|
||||
|
||||
@@ -212,6 +212,7 @@
|
||||
"Device_NoData_Help": "",
|
||||
"Device_NoData_Scanning": "",
|
||||
"Device_NoData_Title": "",
|
||||
"Device_NoMatch_Title": "",
|
||||
"Device_Save_Failed": "",
|
||||
"Device_Save_Unauthorized": "",
|
||||
"Device_Saved_Success": "Gerät erfolgreich gespeichert",
|
||||
|
||||
@@ -208,6 +208,7 @@
|
||||
"Device_NoData_Help": "If devices don't appear after the scan, check your SCAN_SUBNETS setting and <a href=\"https://docs.netalertx.com/SUBNETS\" target=\"_blank\">documentation</a>.",
|
||||
"Device_NoData_Scanning": "Waiting for the first scan - this may take several minutes after the initial setup.",
|
||||
"Device_NoData_Title": "No devices found yet",
|
||||
"Device_NoMatch_Title": "No devices match the current filter",
|
||||
"Device_Save_Failed": "Failed to save device",
|
||||
"Device_Save_Unauthorized": "Unauthorized - invalid API token",
|
||||
"Device_Saved_Success": "Device saved successfully",
|
||||
|
||||
@@ -210,6 +210,7 @@
|
||||
"Device_NoData_Help": "",
|
||||
"Device_NoData_Scanning": "",
|
||||
"Device_NoData_Title": "",
|
||||
"Device_NoMatch_Title": "",
|
||||
"Device_Save_Failed": "Fallo al guardar el dispositivo",
|
||||
"Device_Save_Unauthorized": "No autorizado - Token de API inválido",
|
||||
"Device_Saved_Success": "Dispositivo guardado exitósamente",
|
||||
|
||||
@@ -208,6 +208,7 @@
|
||||
"Device_NoData_Help": "",
|
||||
"Device_NoData_Scanning": "",
|
||||
"Device_NoData_Title": "",
|
||||
"Device_NoMatch_Title": "",
|
||||
"Device_Save_Failed": "",
|
||||
"Device_Save_Unauthorized": "",
|
||||
"Device_Saved_Success": "",
|
||||
|
||||
@@ -139,7 +139,7 @@
|
||||
"DevDetail_SessionTable_Duration": "Durée",
|
||||
"DevDetail_SessionTable_IP": "IP",
|
||||
"DevDetail_SessionTable_Order": "Ordre",
|
||||
"DevDetail_Shortcut_CurrentStatus": "État actuel",
|
||||
"DevDetail_Shortcut_CurrentStatus": "État",
|
||||
"DevDetail_Shortcut_DownAlerts": "Alertes de panne",
|
||||
"DevDetail_Shortcut_Presence": "Présence",
|
||||
"DevDetail_Shortcut_Sessions": "Sessions",
|
||||
@@ -203,16 +203,17 @@
|
||||
"Device_MultiEdit_MassActions": "Actions en masse :",
|
||||
"Device_MultiEdit_No_Devices": "Aucun appareil sélectionné.",
|
||||
"Device_MultiEdit_Tooltip": "Attention. Ceci va appliquer la valeur de gauche à tous les appareils sélectionnés au-dessus.",
|
||||
"Device_NextScan_Imminent": "",
|
||||
"Device_NextScan_In": "",
|
||||
"Device_NoData_Help": "",
|
||||
"Device_NoData_Scanning": "",
|
||||
"Device_NoData_Title": "",
|
||||
"Device_NextScan_Imminent": "Imminent...",
|
||||
"Device_NextScan_In": "Prochain scan dans ",
|
||||
"Device_NoData_Help": "Si les appareils n'apparaissent pas après le scan, vérifiez vos paramètres SCAN_SUBNETS et la <a href=\"https://docs.netalertx.com/SUBNETS\" target=\"_blank\">documentation</a>.",
|
||||
"Device_NoData_Scanning": "En attente du premier scan - cela peut prendre quelques minutes après le premier paramétrage.",
|
||||
"Device_NoData_Title": "Aucun appareil trouvé pour le moment",
|
||||
"Device_NoMatch_Title": "",
|
||||
"Device_Save_Failed": "Erreur à l'enregistrement de l'appareil",
|
||||
"Device_Save_Unauthorized": "Non autorisé - Jeton d'API invalide",
|
||||
"Device_Saved_Success": "Appareil enregistré avec succès",
|
||||
"Device_Saved_Unexpected": "La mise à jour de l'appareil a renvoyé une réponse inattendue",
|
||||
"Device_Scanning": "",
|
||||
"Device_Scanning": "Scan en cours...",
|
||||
"Device_Searchbox": "Rechercher",
|
||||
"Device_Shortcut_AllDevices": "Mes appareils",
|
||||
"Device_Shortcut_AllNodes": "Tous les nœuds",
|
||||
@@ -322,7 +323,7 @@
|
||||
"Gen_AddDevice": "Ajouter un appareil",
|
||||
"Gen_Add_All": "Ajouter tous",
|
||||
"Gen_All_Devices": "Tous les appareils",
|
||||
"Gen_Archived": "",
|
||||
"Gen_Archived": "Archivés",
|
||||
"Gen_AreYouSure": "Êtes-vous sûr ?",
|
||||
"Gen_Backup": "Lancer la sauvegarde",
|
||||
"Gen_Cancel": "Annuler",
|
||||
@@ -333,7 +334,7 @@
|
||||
"Gen_Delete": "Supprimer",
|
||||
"Gen_DeleteAll": "Supprimer tous",
|
||||
"Gen_Description": "Description",
|
||||
"Gen_Down": "",
|
||||
"Gen_Down": "En panne",
|
||||
"Gen_Error": "Erreur",
|
||||
"Gen_Filter": "Filtrer",
|
||||
"Gen_Flapping": "",
|
||||
@@ -342,7 +343,7 @@
|
||||
"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_NetworkMask": "Masque réseau",
|
||||
"Gen_New": "",
|
||||
"Gen_New": "Nouveau",
|
||||
"Gen_Offline": "Hors ligne",
|
||||
"Gen_Okay": "OK",
|
||||
"Gen_Online": "En ligne",
|
||||
@@ -360,7 +361,7 @@
|
||||
"Gen_SelectIcon": "<i class=\"fa-solid fa-chevron-down fa-fade\"></i>",
|
||||
"Gen_SelectToPreview": "Sélectionnez pour prévisualiser",
|
||||
"Gen_Selected_Devices": "Appareils sélectionnés :",
|
||||
"Gen_Sleeping": "",
|
||||
"Gen_Sleeping": "En sommeil",
|
||||
"Gen_Subnet": "Sous-réseau",
|
||||
"Gen_Switch": "Basculer",
|
||||
"Gen_Upd": "Mise à jour réussie",
|
||||
@@ -804,4 +805,4 @@
|
||||
"settings_system_label": "Système",
|
||||
"settings_update_item_warning": "Mettre à jour la valeur ci-dessous. Veillez à bien suivre le même format qu'auparavant. <b>Il n'y a pas de pas de contrôle.</b>",
|
||||
"test_event_tooltip": "Enregistrer d'abord vos modifications avant de tester vôtre paramétrage."
|
||||
}
|
||||
}
|
||||
|
||||
808
front/php/templates/language/id_id.json
Normal file
808
front/php/templates/language/id_id.json
Normal file
@@ -0,0 +1,808 @@
|
||||
{
|
||||
"API_CUSTOM_SQL_description": "",
|
||||
"API_CUSTOM_SQL_name": "",
|
||||
"API_TOKEN_description": "",
|
||||
"API_TOKEN_name": "",
|
||||
"API_display_name": "",
|
||||
"API_icon": "",
|
||||
"About_Design": "",
|
||||
"About_Exit": "",
|
||||
"About_Title": "",
|
||||
"AppEvents_AppEventProcessed": "",
|
||||
"AppEvents_DateTimeCreated": "",
|
||||
"AppEvents_Extra": "",
|
||||
"AppEvents_GUID": "",
|
||||
"AppEvents_Helper1": "",
|
||||
"AppEvents_Helper2": "",
|
||||
"AppEvents_Helper3": "",
|
||||
"AppEvents_ObjectForeignKey": "",
|
||||
"AppEvents_ObjectIndex": "",
|
||||
"AppEvents_ObjectIsArchived": "",
|
||||
"AppEvents_ObjectIsNew": "",
|
||||
"AppEvents_ObjectPlugin": "",
|
||||
"AppEvents_ObjectPrimaryID": "",
|
||||
"AppEvents_ObjectSecondaryID": "",
|
||||
"AppEvents_ObjectStatus": "",
|
||||
"AppEvents_ObjectStatusColumn": "",
|
||||
"AppEvents_ObjectType": "",
|
||||
"AppEvents_Plugin": "",
|
||||
"AppEvents_Type": "",
|
||||
"BACKEND_API_URL_description": "",
|
||||
"BACKEND_API_URL_name": "",
|
||||
"BackDevDetail_Actions_Ask_Run": "",
|
||||
"BackDevDetail_Actions_Not_Registered": "",
|
||||
"BackDevDetail_Actions_Title_Run": "",
|
||||
"BackDevDetail_Copy_Ask": "",
|
||||
"BackDevDetail_Copy_Title": "",
|
||||
"BackDevDetail_Tools_WOL_error": "",
|
||||
"BackDevDetail_Tools_WOL_okay": "",
|
||||
"BackDevices_Arpscan_disabled": "",
|
||||
"BackDevices_Arpscan_enabled": "",
|
||||
"BackDevices_Backup_CopError": "",
|
||||
"BackDevices_Backup_Failed": "",
|
||||
"BackDevices_Backup_okay": "",
|
||||
"BackDevices_DBTools_DelDevError_a": "",
|
||||
"BackDevices_DBTools_DelDevError_b": "",
|
||||
"BackDevices_DBTools_DelDev_a": "",
|
||||
"BackDevices_DBTools_DelDev_b": "",
|
||||
"BackDevices_DBTools_DelEvents": "",
|
||||
"BackDevices_DBTools_DelEventsError": "",
|
||||
"BackDevices_DBTools_ImportCSV": "",
|
||||
"BackDevices_DBTools_ImportCSVError": "",
|
||||
"BackDevices_DBTools_ImportCSVMissing": "",
|
||||
"BackDevices_DBTools_Purge": "",
|
||||
"BackDevices_DBTools_UpdDev": "",
|
||||
"BackDevices_DBTools_UpdDevError": "",
|
||||
"BackDevices_DBTools_Upgrade": "",
|
||||
"BackDevices_DBTools_UpgradeError": "",
|
||||
"BackDevices_Device_UpdDevError": "",
|
||||
"BackDevices_Restore_CopError": "",
|
||||
"BackDevices_Restore_Failed": "",
|
||||
"BackDevices_Restore_okay": "",
|
||||
"BackDevices_darkmode_disabled": "",
|
||||
"BackDevices_darkmode_enabled": "",
|
||||
"CLEAR_NEW_FLAG_description": "",
|
||||
"CLEAR_NEW_FLAG_name": "",
|
||||
"CustProps_cant_remove": "",
|
||||
"DAYS_TO_KEEP_EVENTS_description": "",
|
||||
"DAYS_TO_KEEP_EVENTS_name": "",
|
||||
"DISCOVER_PLUGINS_description": "",
|
||||
"DISCOVER_PLUGINS_name": "",
|
||||
"DevDetail_Children_Title": "",
|
||||
"DevDetail_Copy_Device_Title": "",
|
||||
"DevDetail_Copy_Device_Tooltip": "",
|
||||
"DevDetail_CustomProperties_Title": "",
|
||||
"DevDetail_CustomProps_reset_info": "",
|
||||
"DevDetail_DisplayFields_Title": "",
|
||||
"DevDetail_EveandAl_AlertAllEvents": "",
|
||||
"DevDetail_EveandAl_AlertDown": "",
|
||||
"DevDetail_EveandAl_Archived": "",
|
||||
"DevDetail_EveandAl_NewDevice": "",
|
||||
"DevDetail_EveandAl_NewDevice_Tooltip": "",
|
||||
"DevDetail_EveandAl_RandomMAC": "",
|
||||
"DevDetail_EveandAl_ScanCycle": "",
|
||||
"DevDetail_EveandAl_ScanCycle_a": "",
|
||||
"DevDetail_EveandAl_ScanCycle_z": "",
|
||||
"DevDetail_EveandAl_Skip": "",
|
||||
"DevDetail_EveandAl_Title": "",
|
||||
"DevDetail_Events_CheckBox": "",
|
||||
"DevDetail_GoToNetworkNode": "",
|
||||
"DevDetail_Icon": "",
|
||||
"DevDetail_Icon_Descr": "",
|
||||
"DevDetail_Loading": "",
|
||||
"DevDetail_MainInfo_Comments": "",
|
||||
"DevDetail_MainInfo_Favorite": "",
|
||||
"DevDetail_MainInfo_Group": "",
|
||||
"DevDetail_MainInfo_Location": "",
|
||||
"DevDetail_MainInfo_Name": "",
|
||||
"DevDetail_MainInfo_Network": "",
|
||||
"DevDetail_MainInfo_Network_Port": "",
|
||||
"DevDetail_MainInfo_Network_Site": "",
|
||||
"DevDetail_MainInfo_Network_Title": "",
|
||||
"DevDetail_MainInfo_Owner": "",
|
||||
"DevDetail_MainInfo_SSID": "",
|
||||
"DevDetail_MainInfo_Title": "",
|
||||
"DevDetail_MainInfo_Type": "",
|
||||
"DevDetail_MainInfo_Vendor": "",
|
||||
"DevDetail_MainInfo_mac": "",
|
||||
"DevDetail_NavToChildNode": "",
|
||||
"DevDetail_Network_Node_hover": "",
|
||||
"DevDetail_Network_Port_hover": "",
|
||||
"DevDetail_Nmap_Scans": "",
|
||||
"DevDetail_Nmap_Scans_desc": "",
|
||||
"DevDetail_Nmap_buttonDefault": "",
|
||||
"DevDetail_Nmap_buttonDefault_text": "",
|
||||
"DevDetail_Nmap_buttonDetail": "",
|
||||
"DevDetail_Nmap_buttonDetail_text": "",
|
||||
"DevDetail_Nmap_buttonFast": "",
|
||||
"DevDetail_Nmap_buttonFast_text": "",
|
||||
"DevDetail_Nmap_buttonSkipDiscovery": "",
|
||||
"DevDetail_Nmap_buttonSkipDiscovery_text": "",
|
||||
"DevDetail_Nmap_resultsLink": "",
|
||||
"DevDetail_Owner_hover": "",
|
||||
"DevDetail_Periodselect_All": "",
|
||||
"DevDetail_Periodselect_LastMonth": "",
|
||||
"DevDetail_Periodselect_LastWeek": "",
|
||||
"DevDetail_Periodselect_LastYear": "",
|
||||
"DevDetail_Periodselect_today": "",
|
||||
"DevDetail_Run_Actions_Title": "",
|
||||
"DevDetail_Run_Actions_Tooltip": "",
|
||||
"DevDetail_SessionInfo_FirstSession": "",
|
||||
"DevDetail_SessionInfo_LastIP": "",
|
||||
"DevDetail_SessionInfo_LastSession": "",
|
||||
"DevDetail_SessionInfo_StaticIP": "",
|
||||
"DevDetail_SessionInfo_Status": "",
|
||||
"DevDetail_SessionInfo_Title": "",
|
||||
"DevDetail_SessionTable_Additionalinfo": "",
|
||||
"DevDetail_SessionTable_Connection": "",
|
||||
"DevDetail_SessionTable_Disconnection": "",
|
||||
"DevDetail_SessionTable_Duration": "",
|
||||
"DevDetail_SessionTable_IP": "",
|
||||
"DevDetail_SessionTable_Order": "",
|
||||
"DevDetail_Shortcut_CurrentStatus": "",
|
||||
"DevDetail_Shortcut_DownAlerts": "",
|
||||
"DevDetail_Shortcut_Presence": "",
|
||||
"DevDetail_Shortcut_Sessions": "",
|
||||
"DevDetail_Tab_Details": "",
|
||||
"DevDetail_Tab_Events": "",
|
||||
"DevDetail_Tab_EventsTableDate": "",
|
||||
"DevDetail_Tab_EventsTableEvent": "",
|
||||
"DevDetail_Tab_EventsTableIP": "",
|
||||
"DevDetail_Tab_EventsTableInfo": "",
|
||||
"DevDetail_Tab_Nmap": "",
|
||||
"DevDetail_Tab_NmapEmpty": "",
|
||||
"DevDetail_Tab_NmapTableExtra": "",
|
||||
"DevDetail_Tab_NmapTableHeader": "",
|
||||
"DevDetail_Tab_NmapTableIndex": "",
|
||||
"DevDetail_Tab_NmapTablePort": "",
|
||||
"DevDetail_Tab_NmapTableService": "",
|
||||
"DevDetail_Tab_NmapTableState": "",
|
||||
"DevDetail_Tab_NmapTableText": "",
|
||||
"DevDetail_Tab_NmapTableTime": "",
|
||||
"DevDetail_Tab_Plugins": "",
|
||||
"DevDetail_Tab_Presence": "",
|
||||
"DevDetail_Tab_Sessions": "",
|
||||
"DevDetail_Tab_Tools": "",
|
||||
"DevDetail_Tab_Tools_Internet_Info_Description": "",
|
||||
"DevDetail_Tab_Tools_Internet_Info_Error": "",
|
||||
"DevDetail_Tab_Tools_Internet_Info_Start": "",
|
||||
"DevDetail_Tab_Tools_Internet_Info_Title": "",
|
||||
"DevDetail_Tab_Tools_Nslookup_Description": "",
|
||||
"DevDetail_Tab_Tools_Nslookup_Error": "",
|
||||
"DevDetail_Tab_Tools_Nslookup_Start": "",
|
||||
"DevDetail_Tab_Tools_Nslookup_Title": "",
|
||||
"DevDetail_Tab_Tools_Speedtest_Description": "",
|
||||
"DevDetail_Tab_Tools_Speedtest_Start": "",
|
||||
"DevDetail_Tab_Tools_Speedtest_Title": "",
|
||||
"DevDetail_Tab_Tools_Traceroute_Description": "",
|
||||
"DevDetail_Tab_Tools_Traceroute_Error": "",
|
||||
"DevDetail_Tab_Tools_Traceroute_Start": "",
|
||||
"DevDetail_Tab_Tools_Traceroute_Title": "",
|
||||
"DevDetail_Tools_WOL": "",
|
||||
"DevDetail_Tools_WOL_noti": "",
|
||||
"DevDetail_Tools_WOL_noti_text": "",
|
||||
"DevDetail_Type_hover": "",
|
||||
"DevDetail_Vendor_hover": "",
|
||||
"DevDetail_WOL_Title": "",
|
||||
"DevDetail_button_AddIcon": "",
|
||||
"DevDetail_button_AddIcon_Help": "",
|
||||
"DevDetail_button_AddIcon_Tooltip": "",
|
||||
"DevDetail_button_Delete": "",
|
||||
"DevDetail_button_DeleteEvents": "",
|
||||
"DevDetail_button_DeleteEvents_Warning": "",
|
||||
"DevDetail_button_Delete_ask": "",
|
||||
"DevDetail_button_OverwriteIcons": "",
|
||||
"DevDetail_button_OverwriteIcons_Tooltip": "",
|
||||
"DevDetail_button_OverwriteIcons_Warning": "",
|
||||
"DevDetail_button_Reset": "",
|
||||
"DevDetail_button_Save": "",
|
||||
"DeviceEdit_ValidMacIp": "",
|
||||
"Device_MultiEdit": "",
|
||||
"Device_MultiEdit_Backup": "",
|
||||
"Device_MultiEdit_Fields": "",
|
||||
"Device_MultiEdit_MassActions": "",
|
||||
"Device_MultiEdit_No_Devices": "",
|
||||
"Device_MultiEdit_Tooltip": "",
|
||||
"Device_NextScan_Imminent": "",
|
||||
"Device_NextScan_In": "",
|
||||
"Device_NoData_Help": "",
|
||||
"Device_NoData_Scanning": "",
|
||||
"Device_NoData_Title": "",
|
||||
"Device_NoMatch_Title": "",
|
||||
"Device_Save_Failed": "",
|
||||
"Device_Save_Unauthorized": "",
|
||||
"Device_Saved_Success": "",
|
||||
"Device_Saved_Unexpected": "",
|
||||
"Device_Scanning": "",
|
||||
"Device_Searchbox": "",
|
||||
"Device_Shortcut_AllDevices": "",
|
||||
"Device_Shortcut_AllNodes": "",
|
||||
"Device_Shortcut_Archived": "",
|
||||
"Device_Shortcut_Connected": "",
|
||||
"Device_Shortcut_Devices": "",
|
||||
"Device_Shortcut_DownAlerts": "",
|
||||
"Device_Shortcut_DownOnly": "",
|
||||
"Device_Shortcut_Favorites": "",
|
||||
"Device_Shortcut_NewDevices": "",
|
||||
"Device_Shortcut_OnlineChart": "",
|
||||
"Device_Shortcut_Unstable": "",
|
||||
"Device_TableHead_AlertDown": "",
|
||||
"Device_TableHead_Connected_Devices": "",
|
||||
"Device_TableHead_CustomProps": "",
|
||||
"Device_TableHead_FQDN": "",
|
||||
"Device_TableHead_Favorite": "",
|
||||
"Device_TableHead_FirstSession": "",
|
||||
"Device_TableHead_Flapping": "",
|
||||
"Device_TableHead_GUID": "",
|
||||
"Device_TableHead_Group": "",
|
||||
"Device_TableHead_IPv4": "",
|
||||
"Device_TableHead_IPv6": "",
|
||||
"Device_TableHead_Icon": "",
|
||||
"Device_TableHead_LastIP": "",
|
||||
"Device_TableHead_LastIPOrder": "",
|
||||
"Device_TableHead_LastSession": "",
|
||||
"Device_TableHead_Location": "",
|
||||
"Device_TableHead_MAC": "",
|
||||
"Device_TableHead_MAC_full": "",
|
||||
"Device_TableHead_Name": "",
|
||||
"Device_TableHead_NetworkSite": "",
|
||||
"Device_TableHead_Owner": "",
|
||||
"Device_TableHead_ParentRelType": "",
|
||||
"Device_TableHead_Parent_MAC": "",
|
||||
"Device_TableHead_Port": "",
|
||||
"Device_TableHead_PresentLastScan": "",
|
||||
"Device_TableHead_ReqNicsOnline": "",
|
||||
"Device_TableHead_RowID": "",
|
||||
"Device_TableHead_Rowid": "",
|
||||
"Device_TableHead_SSID": "",
|
||||
"Device_TableHead_SourcePlugin": "",
|
||||
"Device_TableHead_Status": "",
|
||||
"Device_TableHead_SyncHubNodeName": "",
|
||||
"Device_TableHead_Type": "",
|
||||
"Device_TableHead_Vendor": "",
|
||||
"Device_TableHead_Vlan": "",
|
||||
"Device_Table_Not_Network_Device": "",
|
||||
"Device_Table_info": "",
|
||||
"Device_Table_nav_next": "",
|
||||
"Device_Table_nav_prev": "",
|
||||
"Device_Tablelenght": "",
|
||||
"Device_Tablelenght_all": "",
|
||||
"Device_Title": "",
|
||||
"Devices_Filters": "",
|
||||
"ENABLE_PLUGINS_description": "",
|
||||
"ENABLE_PLUGINS_name": "",
|
||||
"ENCRYPTION_KEY_description": "",
|
||||
"ENCRYPTION_KEY_name": "",
|
||||
"Email_display_name": "",
|
||||
"Email_icon": "",
|
||||
"Events_Loading": "",
|
||||
"Events_Periodselect_All": "",
|
||||
"Events_Periodselect_LastMonth": "",
|
||||
"Events_Periodselect_LastWeek": "",
|
||||
"Events_Periodselect_LastYear": "",
|
||||
"Events_Periodselect_today": "",
|
||||
"Events_Searchbox": "",
|
||||
"Events_Shortcut_AllEvents": "",
|
||||
"Events_Shortcut_DownAlerts": "",
|
||||
"Events_Shortcut_Events": "",
|
||||
"Events_Shortcut_MissSessions": "",
|
||||
"Events_Shortcut_NewDevices": "",
|
||||
"Events_Shortcut_Sessions": "",
|
||||
"Events_Shortcut_VoidSessions": "",
|
||||
"Events_TableHead_AdditionalInfo": "",
|
||||
"Events_TableHead_Connection": "",
|
||||
"Events_TableHead_Date": "",
|
||||
"Events_TableHead_Device": "",
|
||||
"Events_TableHead_Disconnection": "",
|
||||
"Events_TableHead_Duration": "",
|
||||
"Events_TableHead_DurationOrder": "",
|
||||
"Events_TableHead_EventType": "",
|
||||
"Events_TableHead_IP": "",
|
||||
"Events_TableHead_IPOrder": "",
|
||||
"Events_TableHead_Order": "",
|
||||
"Events_TableHead_Owner": "",
|
||||
"Events_TableHead_PendingAlert": "",
|
||||
"Events_Table_info": "",
|
||||
"Events_Table_nav_next": "",
|
||||
"Events_Table_nav_prev": "",
|
||||
"Events_Tablelenght": "",
|
||||
"Events_Tablelenght_all": "",
|
||||
"Events_Title": "",
|
||||
"FakeMAC_hover": "",
|
||||
"FieldLock_Error": "",
|
||||
"FieldLock_Lock_Tooltip": "",
|
||||
"FieldLock_Locked": "",
|
||||
"FieldLock_SaveBeforeLocking": "",
|
||||
"FieldLock_Source_Label": "",
|
||||
"FieldLock_Unlock_Tooltip": "",
|
||||
"FieldLock_Unlocked": "",
|
||||
"GRAPHQL_PORT_description": "",
|
||||
"GRAPHQL_PORT_name": "",
|
||||
"Gen_Action": "",
|
||||
"Gen_Add": "",
|
||||
"Gen_AddDevice": "",
|
||||
"Gen_Add_All": "",
|
||||
"Gen_All_Devices": "",
|
||||
"Gen_Archived": "",
|
||||
"Gen_AreYouSure": "",
|
||||
"Gen_Backup": "",
|
||||
"Gen_Cancel": "",
|
||||
"Gen_Change": "",
|
||||
"Gen_Copy": "",
|
||||
"Gen_CopyToClipboard": "",
|
||||
"Gen_DataUpdatedUITakesTime": "",
|
||||
"Gen_Delete": "",
|
||||
"Gen_DeleteAll": "",
|
||||
"Gen_Description": "",
|
||||
"Gen_Down": "",
|
||||
"Gen_Error": "",
|
||||
"Gen_Filter": "",
|
||||
"Gen_Flapping": "",
|
||||
"Gen_Generate": "",
|
||||
"Gen_InvalidMac": "",
|
||||
"Gen_Invalid_Value": "",
|
||||
"Gen_LockedDB": "",
|
||||
"Gen_NetworkMask": "",
|
||||
"Gen_New": "",
|
||||
"Gen_Offline": "",
|
||||
"Gen_Okay": "",
|
||||
"Gen_Online": "",
|
||||
"Gen_Purge": "",
|
||||
"Gen_ReadDocs": "",
|
||||
"Gen_Remove_All": "",
|
||||
"Gen_Remove_Last": "",
|
||||
"Gen_Reset": "",
|
||||
"Gen_Restore": "",
|
||||
"Gen_Run": "",
|
||||
"Gen_Save": "",
|
||||
"Gen_Saved": "",
|
||||
"Gen_Search": "",
|
||||
"Gen_Select": "",
|
||||
"Gen_SelectIcon": "",
|
||||
"Gen_SelectToPreview": "",
|
||||
"Gen_Selected_Devices": "",
|
||||
"Gen_Sleeping": "",
|
||||
"Gen_Subnet": "",
|
||||
"Gen_Switch": "",
|
||||
"Gen_Upd": "",
|
||||
"Gen_Upd_Fail": "",
|
||||
"Gen_Update": "",
|
||||
"Gen_Update_Value": "",
|
||||
"Gen_ValidIcon": "",
|
||||
"Gen_Warning": "",
|
||||
"Gen_Work_In_Progress": "",
|
||||
"Gen_create_new_device": "",
|
||||
"Gen_create_new_device_info": "",
|
||||
"General_display_name": "",
|
||||
"General_icon": "",
|
||||
"HRS_TO_KEEP_NEWDEV_description": "",
|
||||
"HRS_TO_KEEP_NEWDEV_name": "",
|
||||
"HRS_TO_KEEP_OFFDEV_description": "",
|
||||
"HRS_TO_KEEP_OFFDEV_name": "",
|
||||
"LOADED_PLUGINS_description": "",
|
||||
"LOADED_PLUGINS_name": "",
|
||||
"LOG_LEVEL_description": "",
|
||||
"LOG_LEVEL_name": "",
|
||||
"Loading": "",
|
||||
"Login_Box": "",
|
||||
"Login_Default_PWD": "",
|
||||
"Login_Info": "",
|
||||
"Login_Psw-box": "",
|
||||
"Login_Psw_alert": "",
|
||||
"Login_Psw_folder": "",
|
||||
"Login_Psw_new": "",
|
||||
"Login_Psw_run": "",
|
||||
"Login_Remember": "",
|
||||
"Login_Remember_small": "",
|
||||
"Login_Submit": "",
|
||||
"Login_Toggle_Alert_headline": "",
|
||||
"Login_Toggle_Info": "",
|
||||
"Login_Toggle_Info_headline": "",
|
||||
"Maint_PurgeLog": "",
|
||||
"Maint_RestartServer": "",
|
||||
"Maint_Restart_Server_noti_text": "",
|
||||
"Maintenance_InitCheck": "",
|
||||
"Maintenance_InitCheck_Checking": "",
|
||||
"Maintenance_InitCheck_QuickSetupGuide": "",
|
||||
"Maintenance_InitCheck_Success": "",
|
||||
"Maintenance_ReCheck": "",
|
||||
"Maintenance_Running_Version": "",
|
||||
"Maintenance_Status": "",
|
||||
"Maintenance_Title": "",
|
||||
"Maintenance_Tool_DownloadConfig": "",
|
||||
"Maintenance_Tool_DownloadConfig_text": "",
|
||||
"Maintenance_Tool_DownloadWorkflows": "",
|
||||
"Maintenance_Tool_DownloadWorkflows_text": "",
|
||||
"Maintenance_Tool_ExportCSV": "",
|
||||
"Maintenance_Tool_ExportCSV_noti": "",
|
||||
"Maintenance_Tool_ExportCSV_noti_text": "",
|
||||
"Maintenance_Tool_ExportCSV_text": "",
|
||||
"Maintenance_Tool_ImportCSV": "",
|
||||
"Maintenance_Tool_ImportCSV_noti": "",
|
||||
"Maintenance_Tool_ImportCSV_noti_text": "",
|
||||
"Maintenance_Tool_ImportCSV_text": "",
|
||||
"Maintenance_Tool_ImportConfig_noti": "",
|
||||
"Maintenance_Tool_ImportPastedCSV": "",
|
||||
"Maintenance_Tool_ImportPastedCSV_noti_text": "",
|
||||
"Maintenance_Tool_ImportPastedCSV_text": "",
|
||||
"Maintenance_Tool_ImportPastedConfig": "",
|
||||
"Maintenance_Tool_ImportPastedConfig_noti_text": "",
|
||||
"Maintenance_Tool_ImportPastedConfig_text": "",
|
||||
"Maintenance_Tool_UnlockFields": "",
|
||||
"Maintenance_Tool_UnlockFields_noti": "",
|
||||
"Maintenance_Tool_UnlockFields_noti_text": "",
|
||||
"Maintenance_Tool_UnlockFields_text": "",
|
||||
"Maintenance_Tool_arpscansw": "",
|
||||
"Maintenance_Tool_arpscansw_noti": "",
|
||||
"Maintenance_Tool_arpscansw_noti_text": "",
|
||||
"Maintenance_Tool_arpscansw_text": "",
|
||||
"Maintenance_Tool_backup": "",
|
||||
"Maintenance_Tool_backup_noti": "",
|
||||
"Maintenance_Tool_backup_noti_text": "",
|
||||
"Maintenance_Tool_backup_text": "",
|
||||
"Maintenance_Tool_check_visible": "",
|
||||
"Maintenance_Tool_clearSourceFields_selected": "",
|
||||
"Maintenance_Tool_clearSourceFields_selected_noti": "",
|
||||
"Maintenance_Tool_clearSourceFields_selected_text": "",
|
||||
"Maintenance_Tool_darkmode": "",
|
||||
"Maintenance_Tool_darkmode_noti": "",
|
||||
"Maintenance_Tool_darkmode_noti_text": "",
|
||||
"Maintenance_Tool_darkmode_text": "",
|
||||
"Maintenance_Tool_del_ActHistory": "",
|
||||
"Maintenance_Tool_del_ActHistory_noti": "",
|
||||
"Maintenance_Tool_del_ActHistory_noti_text": "",
|
||||
"Maintenance_Tool_del_ActHistory_text": "",
|
||||
"Maintenance_Tool_del_alldev": "",
|
||||
"Maintenance_Tool_del_alldev_noti": "",
|
||||
"Maintenance_Tool_del_alldev_noti_text": "",
|
||||
"Maintenance_Tool_del_alldev_text": "",
|
||||
"Maintenance_Tool_del_allevents": "",
|
||||
"Maintenance_Tool_del_allevents30": "",
|
||||
"Maintenance_Tool_del_allevents30_noti": "",
|
||||
"Maintenance_Tool_del_allevents30_noti_text": "",
|
||||
"Maintenance_Tool_del_allevents30_text": "",
|
||||
"Maintenance_Tool_del_allevents_noti": "",
|
||||
"Maintenance_Tool_del_allevents_noti_text": "",
|
||||
"Maintenance_Tool_del_allevents_text": "",
|
||||
"Maintenance_Tool_del_empty_macs": "",
|
||||
"Maintenance_Tool_del_empty_macs_noti": "",
|
||||
"Maintenance_Tool_del_empty_macs_noti_text": "",
|
||||
"Maintenance_Tool_del_empty_macs_text": "",
|
||||
"Maintenance_Tool_del_selecteddev": "",
|
||||
"Maintenance_Tool_del_selecteddev_text": "",
|
||||
"Maintenance_Tool_del_unknowndev": "",
|
||||
"Maintenance_Tool_del_unknowndev_noti": "",
|
||||
"Maintenance_Tool_del_unknowndev_noti_text": "",
|
||||
"Maintenance_Tool_del_unknowndev_text": "",
|
||||
"Maintenance_Tool_del_unlockFields_selecteddev_text": "",
|
||||
"Maintenance_Tool_displayed_columns_text": "",
|
||||
"Maintenance_Tool_drag_me": "",
|
||||
"Maintenance_Tool_order_columns_text": "",
|
||||
"Maintenance_Tool_purgebackup": "",
|
||||
"Maintenance_Tool_purgebackup_noti": "",
|
||||
"Maintenance_Tool_purgebackup_noti_text": "",
|
||||
"Maintenance_Tool_purgebackup_text": "",
|
||||
"Maintenance_Tool_restore": "",
|
||||
"Maintenance_Tool_restore_noti": "",
|
||||
"Maintenance_Tool_restore_noti_text": "",
|
||||
"Maintenance_Tool_restore_text": "",
|
||||
"Maintenance_Tool_unlockFields_selecteddev": "",
|
||||
"Maintenance_Tool_unlockFields_selecteddev_noti": "",
|
||||
"Maintenance_Tool_upgrade_database_noti": "",
|
||||
"Maintenance_Tool_upgrade_database_noti_text": "",
|
||||
"Maintenance_Tool_upgrade_database_text": "",
|
||||
"Maintenance_Tools_Tab_BackupRestore": "",
|
||||
"Maintenance_Tools_Tab_Logging": "",
|
||||
"Maintenance_Tools_Tab_Settings": "",
|
||||
"Maintenance_Tools_Tab_Tools": "",
|
||||
"Maintenance_Tools_Tab_UISettings": "",
|
||||
"Maintenance_arp_status": "",
|
||||
"Maintenance_arp_status_off": "",
|
||||
"Maintenance_arp_status_on": "",
|
||||
"Maintenance_built_on": "",
|
||||
"Maintenance_current_version": "",
|
||||
"Maintenance_database_backup": "",
|
||||
"Maintenance_database_backup_found": "",
|
||||
"Maintenance_database_backup_total": "",
|
||||
"Maintenance_database_lastmod": "",
|
||||
"Maintenance_database_path": "",
|
||||
"Maintenance_database_rows": "",
|
||||
"Maintenance_database_size": "",
|
||||
"Maintenance_lang_selector_apply": "",
|
||||
"Maintenance_lang_selector_empty": "",
|
||||
"Maintenance_lang_selector_lable": "",
|
||||
"Maintenance_lang_selector_text": "",
|
||||
"Maintenance_new_version": "",
|
||||
"Maintenance_themeselector_apply": "",
|
||||
"Maintenance_themeselector_empty": "",
|
||||
"Maintenance_themeselector_lable": "",
|
||||
"Maintenance_themeselector_text": "",
|
||||
"Maintenance_version": "",
|
||||
"NETWORK_DEVICE_TYPES_description": "",
|
||||
"NETWORK_DEVICE_TYPES_name": "",
|
||||
"Navigation_About": "",
|
||||
"Navigation_AppEvents": "",
|
||||
"Navigation_Devices": "",
|
||||
"Navigation_Donations": "",
|
||||
"Navigation_Events": "",
|
||||
"Navigation_Integrations": "",
|
||||
"Navigation_Maintenance": "",
|
||||
"Navigation_Monitoring": "",
|
||||
"Navigation_Network": "",
|
||||
"Navigation_Notifications": "",
|
||||
"Navigation_Plugins": "",
|
||||
"Navigation_Presence": "",
|
||||
"Navigation_Report": "",
|
||||
"Navigation_Settings": "",
|
||||
"Navigation_SystemInfo": "",
|
||||
"Navigation_Workflows": "",
|
||||
"Network_Assign": "",
|
||||
"Network_Cant_Assign": "",
|
||||
"Network_Cant_Assign_No_Node_Selected": "",
|
||||
"Network_Configuration_Error": "",
|
||||
"Network_Connected": "",
|
||||
"Network_Devices": "",
|
||||
"Network_ManageAdd": "",
|
||||
"Network_ManageAdd_Name": "",
|
||||
"Network_ManageAdd_Name_text": "",
|
||||
"Network_ManageAdd_Port": "",
|
||||
"Network_ManageAdd_Port_text": "",
|
||||
"Network_ManageAdd_Submit": "",
|
||||
"Network_ManageAdd_Type": "",
|
||||
"Network_ManageAdd_Type_text": "",
|
||||
"Network_ManageAssign": "",
|
||||
"Network_ManageDel": "",
|
||||
"Network_ManageDel_Name": "",
|
||||
"Network_ManageDel_Name_text": "",
|
||||
"Network_ManageDel_Submit": "",
|
||||
"Network_ManageDevices": "",
|
||||
"Network_ManageEdit": "",
|
||||
"Network_ManageEdit_ID": "",
|
||||
"Network_ManageEdit_ID_text": "",
|
||||
"Network_ManageEdit_Name": "",
|
||||
"Network_ManageEdit_Name_text": "",
|
||||
"Network_ManageEdit_Port": "",
|
||||
"Network_ManageEdit_Port_text": "",
|
||||
"Network_ManageEdit_Submit": "",
|
||||
"Network_ManageEdit_Type": "",
|
||||
"Network_ManageEdit_Type_text": "",
|
||||
"Network_ManageLeaf": "",
|
||||
"Network_ManageUnassign": "",
|
||||
"Network_NoAssignedDevices": "",
|
||||
"Network_NoDevices": "",
|
||||
"Network_Node": "",
|
||||
"Network_Node_Name": "",
|
||||
"Network_Parent": "",
|
||||
"Network_Root": "",
|
||||
"Network_Root_Not_Configured": "",
|
||||
"Network_Root_Unconfigurable": "",
|
||||
"Network_ShowArchived": "",
|
||||
"Network_ShowOffline": "",
|
||||
"Network_Table_Hostname": "",
|
||||
"Network_Table_IP": "",
|
||||
"Network_Table_State": "",
|
||||
"Network_Title": "",
|
||||
"Network_UnassignedDevices": "",
|
||||
"Notifications_All": "",
|
||||
"Notifications_Mark_All_Read": "",
|
||||
"PIALERT_WEB_PASSWORD_description": "",
|
||||
"PIALERT_WEB_PASSWORD_name": "",
|
||||
"PIALERT_WEB_PROTECTION_description": "",
|
||||
"PIALERT_WEB_PROTECTION_name": "",
|
||||
"PLUGINS_KEEP_HIST_description": "",
|
||||
"PLUGINS_KEEP_HIST_name": "",
|
||||
"PRAGMA_JOURNAL_SIZE_LIMIT_description": "",
|
||||
"PRAGMA_JOURNAL_SIZE_LIMIT_name": "",
|
||||
"Plugins_DeleteAll": "",
|
||||
"Plugins_Filters_Mac": "",
|
||||
"Plugins_History": "",
|
||||
"Plugins_Obj_DeleteListed": "",
|
||||
"Plugins_Objects": "",
|
||||
"Plugins_Out_of": "",
|
||||
"Plugins_Unprocessed_Events": "",
|
||||
"Plugins_no_control": "",
|
||||
"Presence_CalHead_day": "",
|
||||
"Presence_CalHead_lang": "",
|
||||
"Presence_CalHead_month": "",
|
||||
"Presence_CalHead_quarter": "",
|
||||
"Presence_CalHead_week": "",
|
||||
"Presence_CalHead_year": "",
|
||||
"Presence_CallHead_Devices": "",
|
||||
"Presence_Key_OnlineNow": "",
|
||||
"Presence_Key_OnlineNow_desc": "",
|
||||
"Presence_Key_OnlinePast": "",
|
||||
"Presence_Key_OnlinePastMiss": "",
|
||||
"Presence_Key_OnlinePastMiss_desc": "",
|
||||
"Presence_Key_OnlinePast_desc": "",
|
||||
"Presence_Loading": "",
|
||||
"Presence_Shortcut_AllDevices": "",
|
||||
"Presence_Shortcut_Archived": "",
|
||||
"Presence_Shortcut_Connected": "",
|
||||
"Presence_Shortcut_Devices": "",
|
||||
"Presence_Shortcut_DownAlerts": "",
|
||||
"Presence_Shortcut_Favorites": "",
|
||||
"Presence_Shortcut_NewDevices": "",
|
||||
"Presence_Title": "",
|
||||
"REFRESH_FQDN_description": "",
|
||||
"REFRESH_FQDN_name": "",
|
||||
"REPORT_DASHBOARD_URL_description": "",
|
||||
"REPORT_DASHBOARD_URL_name": "",
|
||||
"REPORT_ERROR": "",
|
||||
"REPORT_MAIL_description": "",
|
||||
"REPORT_MAIL_name": "",
|
||||
"REPORT_TITLE": "",
|
||||
"RandomMAC_hover": "",
|
||||
"Reports_Sent_Log": "",
|
||||
"SCAN_SUBNETS_description": "",
|
||||
"SCAN_SUBNETS_name": "",
|
||||
"SYSTEM_TITLE": "",
|
||||
"Setting_Override": "",
|
||||
"Setting_Override_Description": "",
|
||||
"Settings_Metadata_Toggle": "",
|
||||
"Settings_Show_Description": "",
|
||||
"Settings_device_Scanners_desync": "",
|
||||
"Settings_device_Scanners_desync_popup": "",
|
||||
"Speedtest_Results": "",
|
||||
"Systeminfo_AvailableIps": "",
|
||||
"Systeminfo_CPU": "",
|
||||
"Systeminfo_CPU_Cores": "",
|
||||
"Systeminfo_CPU_Name": "",
|
||||
"Systeminfo_CPU_Speed": "",
|
||||
"Systeminfo_CPU_Temp": "",
|
||||
"Systeminfo_CPU_Vendor": "",
|
||||
"Systeminfo_Client_Resolution": "",
|
||||
"Systeminfo_Client_User_Agent": "",
|
||||
"Systeminfo_General": "",
|
||||
"Systeminfo_General_Date": "",
|
||||
"Systeminfo_General_Date2": "",
|
||||
"Systeminfo_General_Full_Date": "",
|
||||
"Systeminfo_General_TimeZone": "",
|
||||
"Systeminfo_Memory": "",
|
||||
"Systeminfo_Memory_Total_Memory": "",
|
||||
"Systeminfo_Memory_Usage": "",
|
||||
"Systeminfo_Memory_Usage_Percent": "",
|
||||
"Systeminfo_Motherboard": "",
|
||||
"Systeminfo_Motherboard_BIOS": "",
|
||||
"Systeminfo_Motherboard_BIOS_Date": "",
|
||||
"Systeminfo_Motherboard_BIOS_Vendor": "",
|
||||
"Systeminfo_Motherboard_Manufactured": "",
|
||||
"Systeminfo_Motherboard_Name": "",
|
||||
"Systeminfo_Motherboard_Revision": "",
|
||||
"Systeminfo_Network": "",
|
||||
"Systeminfo_Network_Accept_Encoding": "",
|
||||
"Systeminfo_Network_Accept_Language": "",
|
||||
"Systeminfo_Network_Connection_Port": "",
|
||||
"Systeminfo_Network_HTTP_Host": "",
|
||||
"Systeminfo_Network_HTTP_Referer": "",
|
||||
"Systeminfo_Network_HTTP_Referer_String": "",
|
||||
"Systeminfo_Network_Hardware": "",
|
||||
"Systeminfo_Network_Hardware_Interface_Mask": "",
|
||||
"Systeminfo_Network_Hardware_Interface_Name": "",
|
||||
"Systeminfo_Network_Hardware_Interface_RX": "",
|
||||
"Systeminfo_Network_Hardware_Interface_TX": "",
|
||||
"Systeminfo_Network_IP": "",
|
||||
"Systeminfo_Network_IP_Connection": "",
|
||||
"Systeminfo_Network_IP_Server": "",
|
||||
"Systeminfo_Network_MIME": "",
|
||||
"Systeminfo_Network_Request_Method": "",
|
||||
"Systeminfo_Network_Request_Time": "",
|
||||
"Systeminfo_Network_Request_URI": "",
|
||||
"Systeminfo_Network_Secure_Connection": "",
|
||||
"Systeminfo_Network_Secure_Connection_String": "",
|
||||
"Systeminfo_Network_Server_Name": "",
|
||||
"Systeminfo_Network_Server_Name_String": "",
|
||||
"Systeminfo_Network_Server_Query": "",
|
||||
"Systeminfo_Network_Server_Query_String": "",
|
||||
"Systeminfo_Network_Server_Version": "",
|
||||
"Systeminfo_Services": "",
|
||||
"Systeminfo_Services_Description": "",
|
||||
"Systeminfo_Services_Name": "",
|
||||
"Systeminfo_Storage": "",
|
||||
"Systeminfo_Storage_Device": "",
|
||||
"Systeminfo_Storage_Mount": "",
|
||||
"Systeminfo_Storage_Size": "",
|
||||
"Systeminfo_Storage_Type": "",
|
||||
"Systeminfo_Storage_Usage": "",
|
||||
"Systeminfo_Storage_Usage_Free": "",
|
||||
"Systeminfo_Storage_Usage_Mount": "",
|
||||
"Systeminfo_Storage_Usage_Total": "",
|
||||
"Systeminfo_Storage_Usage_Used": "",
|
||||
"Systeminfo_System": "",
|
||||
"Systeminfo_System_AVG": "",
|
||||
"Systeminfo_System_Architecture": "",
|
||||
"Systeminfo_System_Kernel": "",
|
||||
"Systeminfo_System_OSVersion": "",
|
||||
"Systeminfo_System_Running_Processes": "",
|
||||
"Systeminfo_System_System": "",
|
||||
"Systeminfo_System_Uname": "",
|
||||
"Systeminfo_System_Uptime": "",
|
||||
"Systeminfo_This_Client": "",
|
||||
"Systeminfo_USB_Devices": "",
|
||||
"TICKER_MIGRATE_TO_NETALERTX": "",
|
||||
"TIMEZONE_description": "",
|
||||
"TIMEZONE_name": "",
|
||||
"UI_DEV_SECTIONS_description": "",
|
||||
"UI_DEV_SECTIONS_name": "",
|
||||
"UI_ICONS_description": "",
|
||||
"UI_ICONS_name": "",
|
||||
"UI_LANG_description": "",
|
||||
"UI_LANG_name": "",
|
||||
"UI_MY_DEVICES_description": "",
|
||||
"UI_MY_DEVICES_name": "",
|
||||
"UI_NOT_RANDOM_MAC_description": "",
|
||||
"UI_NOT_RANDOM_MAC_name": "",
|
||||
"UI_PRESENCE_description": "",
|
||||
"UI_PRESENCE_name": "",
|
||||
"UI_REFRESH_description": "",
|
||||
"UI_REFRESH_name": "",
|
||||
"VERSION_description": "",
|
||||
"VERSION_name": "",
|
||||
"WF_Action_Add": "",
|
||||
"WF_Action_field": "",
|
||||
"WF_Action_type": "",
|
||||
"WF_Action_value": "",
|
||||
"WF_Actions": "",
|
||||
"WF_Add": "",
|
||||
"WF_Add_Condition": "",
|
||||
"WF_Add_Group": "",
|
||||
"WF_Condition_field": "",
|
||||
"WF_Condition_operator": "",
|
||||
"WF_Condition_value": "",
|
||||
"WF_Conditions": "",
|
||||
"WF_Conditions_logic_rules": "",
|
||||
"WF_Duplicate": "",
|
||||
"WF_Enabled": "",
|
||||
"WF_Export": "",
|
||||
"WF_Export_Copy": "",
|
||||
"WF_Import": "",
|
||||
"WF_Import_Copy": "",
|
||||
"WF_Name": "",
|
||||
"WF_Remove": "",
|
||||
"WF_Remove_Copy": "",
|
||||
"WF_Save": "",
|
||||
"WF_Trigger": "",
|
||||
"WF_Trigger_event_type": "",
|
||||
"WF_Trigger_type": "",
|
||||
"add_icon_event_tooltip": "",
|
||||
"add_option_event_tooltip": "",
|
||||
"copy_icons_event_tooltip": "",
|
||||
"devices_old": "",
|
||||
"general_event_description": "",
|
||||
"general_event_title": "",
|
||||
"go_to_device_event_tooltip": "",
|
||||
"go_to_node_event_tooltip": "",
|
||||
"new_version_available": "",
|
||||
"report_guid": "",
|
||||
"report_guid_missing": "",
|
||||
"report_select_format": "",
|
||||
"report_time": "",
|
||||
"run_event_tooltip": "",
|
||||
"select_icon_event_tooltip": "",
|
||||
"settings_core_icon": "",
|
||||
"settings_core_label": "",
|
||||
"settings_device_scanners": "",
|
||||
"settings_device_scanners_icon": "",
|
||||
"settings_device_scanners_info": "",
|
||||
"settings_device_scanners_label": "",
|
||||
"settings_enabled": "",
|
||||
"settings_enabled_icon": "",
|
||||
"settings_expand_all": "",
|
||||
"settings_imported": "",
|
||||
"settings_imported_label": "",
|
||||
"settings_missing": "",
|
||||
"settings_missing_block": "",
|
||||
"settings_old": "",
|
||||
"settings_other_scanners": "",
|
||||
"settings_other_scanners_icon": "",
|
||||
"settings_other_scanners_label": "",
|
||||
"settings_publishers": "",
|
||||
"settings_publishers_icon": "",
|
||||
"settings_publishers_info": "",
|
||||
"settings_publishers_label": "",
|
||||
"settings_readonly": "",
|
||||
"settings_saved": "",
|
||||
"settings_system_icon": "",
|
||||
"settings_system_label": "",
|
||||
"settings_update_item_warning": "",
|
||||
"test_event_tooltip": ""
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"API_CUSTOM_SQL_description": "Puoi specificare una query SQL personalizzata che genererà un file JSON e quindi lo esporrà tramite l'<a href=\"/php/server/query_json.php?file=table_custom_endpoint.json\" target=\"_blank\"><code>table_custom_endpoint.json</code>endpoint del file</a>.",
|
||||
"API_CUSTOM_SQL_description": "Puoi specificare una query SQL personalizzata che genererà un file JSON e quindi lo esporrà tramite <a href=\"/php/server/query_json.php?file=table_custom_endpoint.json\" target=\"_blank\"><code>table_custom_endpoint.json</code> endpoint del file</a>.",
|
||||
"API_CUSTOM_SQL_name": "Endpoint personalizzato",
|
||||
"API_TOKEN_description": "Token API per comunicazioni sicure. Generane uno o inserisci un valore qualsiasi. Viene inviato nell'intestazione della richiesta e utilizzato nel plugin <code>SYNC</code>, nel server GraphQL e in altri endpoint API. Puoi utilizzare gli endpoint API per creare integrazioni personalizzate come descritto nella <a href=\"https://docs.netalertx.com/API\" target=\"_blank\">documentazione API</a>.",
|
||||
"API_TOKEN_name": "Token API",
|
||||
@@ -203,11 +203,12 @@
|
||||
"Device_MultiEdit_MassActions": "Azioni di massa:",
|
||||
"Device_MultiEdit_No_Devices": "Nessun dispositivo selezionato.",
|
||||
"Device_MultiEdit_Tooltip": "Attento. Facendo clic verrà applicato il valore sulla sinistra a tutti i dispositivi selezionati sopra.",
|
||||
"Device_NextScan_Imminent": "imminente",
|
||||
"Device_NextScan_In": "Prossima scansione in ",
|
||||
"Device_NextScan_Imminent": "Imminente...",
|
||||
"Device_NextScan_In": "Prossima scansione tra circa ",
|
||||
"Device_NoData_Help": "Se i dispositivi non vengono visualizzati dopo la scansione, controlla l'impostazione SCAN_SUBNETS e la <a href=\"https://docs.netalertx.com/SUBNETS\" target=\"_blank\">documentazione</a>.",
|
||||
"Device_NoData_Scanning": "In attesa della prima scansione: potrebbero volerci diversi minuti dopo la configurazione iniziale.",
|
||||
"Device_NoData_Title": "Ancora nessun dispositivo trovato",
|
||||
"Device_NoMatch_Title": "",
|
||||
"Device_Save_Failed": "Impossibile salvare il dispositivo",
|
||||
"Device_Save_Unauthorized": "Non autorizzato: token API non valido",
|
||||
"Device_Saved_Success": "Dispositivo salvato correttamente",
|
||||
@@ -567,13 +568,13 @@
|
||||
"Network_ManageEdit_Type_text": "-- Seleziona tipo --",
|
||||
"Network_ManageLeaf": "Gestisci assegnazione",
|
||||
"Network_ManageUnassign": "Annulla assegnazione",
|
||||
"Network_NoAssignedDevices": "A questo nodo di rete non sono assegnati dispositivi (nodi foglia). Assegnane uno dall'elenco qui sotto o vai alla scheda <b><i class=\"fa fa-info-circle\"></i>Dettagli</b> di qualsiasi dispositivo in <a href=\"devices.php\"><b > <i class=\"fa fa-laptop\"></i>Dispositivi</b></a> e assegnalo a un <b><i class=\"fa fa-server\"></i>Nodo di rete (MAC)</b> e una <b><i class=\"fa fa-ethernet\"></i>Porta</b>.",
|
||||
"Network_NoAssignedDevices": "A questo nodo di rete non sono assegnati dispositivi (nodi foglia). Assegnane uno dall'elenco qui sotto o vai alla scheda <b><i class=\"fa fa-info-circle\"></i> Dettagli</b> di qualsiasi dispositivo in <a href=\"devices.php\"><b > <i class=\"fa fa-laptop\"></i> Dispositivi</b></a> e assegnalo a un <b><i class=\"fa fa-server\"></i> Nodo di rete (MAC)</b> e una <b><i class=\"fa fa-ethernet\"></i> Porta</b>.",
|
||||
"Network_NoDevices": "Nessun dispositivo da configurare",
|
||||
"Network_Node": "Nodo di rete",
|
||||
"Network_Node_Name": "Nome nodo",
|
||||
"Network_Parent": "Dispositivo di rete principale",
|
||||
"Network_Root": "Nodo radice",
|
||||
"Network_Root_Not_Configured": "Seleziona un tipo di dispositivo di rete, ad esempio un <b>Gateway</b>, nel campo <b>Tipo</b> del <a href=\"deviceDetails.php?mac=Internet\">dispositivo root Internet</a> per iniziare a configurare questa schermata. <br/><br/> Ulteriore documentazione è disponibile nella guida <a href=\"https://docs.netalertx.com/NETWORK_TREE\" target=\"_blank\"> Come impostare la tua pagina di rete</a>",
|
||||
"Network_Root_Not_Configured": "Seleziona un tipo di dispositivo di rete, ad esempio un <b>Gateway</b>, nel campo <b>Tipo</b> del <a href=\"deviceDetails.php?mac=Internet\">dispositivo root Internet</a> per iniziare a configurare questa schermata. <br/><br/> Ulteriore documentazione è disponibile nella guida <a href=\"https://docs.netalertx.com/NETWORK_TREE\" target=\"_blank\">Come impostare la tua pagina di rete</a>",
|
||||
"Network_Root_Unconfigurable": "Radice non configurabile",
|
||||
"Network_ShowArchived": "Mostra archiviati",
|
||||
"Network_ShowOffline": "Mostra offline",
|
||||
@@ -584,7 +585,7 @@
|
||||
"Network_UnassignedDevices": "Dispositivi non assegnati",
|
||||
"Notifications_All": "Tutte le notifiche",
|
||||
"Notifications_Mark_All_Read": "Segna tutto come letto",
|
||||
"PIALERT_WEB_PASSWORD_description": "La password predefinita è <code>123456</code>. Per modificare la password esegui <code>/app/back/pialert-cli</code> nel contenitore o utilizza il <a onclick=\"toggleAllSettings()\" href=\"#SETPWD_RUN\"><code>SETPWD_RUN</code>plugin imposta password</a>.",
|
||||
"PIALERT_WEB_PASSWORD_description": "La password predefinita è <code>123456</code>. Per modificare la password esegui <code>/app/back/pialert-cli</code> nel contenitore o utilizza il <a onclick=\"toggleAllSettings()\" href=\"#SETPWD_RUN\"><code>SETPWD_RUN</code> plugin imposta password</a>.",
|
||||
"PIALERT_WEB_PASSWORD_name": "Password login",
|
||||
"PIALERT_WEB_PROTECTION_description": "Se abilitato, viene mostrata una finestra di login. Leggi attentamente qui sotto se rimani bloccato fuori dall'istanza.",
|
||||
"PIALERT_WEB_PROTECTION_name": "Abilita login",
|
||||
|
||||
@@ -203,11 +203,12 @@
|
||||
"Device_MultiEdit_MassActions": "大量のアクション:",
|
||||
"Device_MultiEdit_No_Devices": "デバイスが選択されていません。",
|
||||
"Device_MultiEdit_Tooltip": "注意。これをクリックすると、左側の値が上記で選択したすべてのデバイスに適用されます。",
|
||||
"Device_NextScan_Imminent": "まもなく",
|
||||
"Device_NextScan_In": "次のスキャン ",
|
||||
"Device_NextScan_Imminent": "まもなく...",
|
||||
"Device_NextScan_In": "次のスキャンまでおよそ ",
|
||||
"Device_NoData_Help": "スキャン後にデバイスが表示されない場合は、SCAN_SUBNETS設定と<a href=\"https://docs.netalertx.com/SUBNETS\" target=\"_blank\">ドキュメント</a>を確認してください。",
|
||||
"Device_NoData_Scanning": "最初のスキャンを待機中 - 初期設定後、数分かかる場合があります。",
|
||||
"Device_NoData_Title": "デバイスが見つかりません",
|
||||
"Device_NoMatch_Title": "",
|
||||
"Device_Save_Failed": "デバイスの保存に失敗しました",
|
||||
"Device_Save_Unauthorized": "許可されていない - 無効なAPIトークン",
|
||||
"Device_Saved_Success": "デバイスが正常に保存されました",
|
||||
|
||||
@@ -2,12 +2,13 @@
|
||||
|
||||
// ###################################
|
||||
// ## Languages
|
||||
// ## Look-up here: http://www.lingoes.net/en/translator/langcode.htm
|
||||
// ###################################
|
||||
|
||||
$defaultLang = "en_us";
|
||||
|
||||
// Load the canonical language list from languages.json — do not hardcode here.
|
||||
$_langJsonPath = dirname(__FILE__) . '/languages.json';
|
||||
$_langJsonPath = dirname(__FILE__) . '/language_definitions/languages.json';
|
||||
$_langJson = json_decode(file_get_contents($_langJsonPath), true);
|
||||
$allLanguages = array_column($_langJson['languages'], 'code');
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
{ "code": "en_us", "display": "English (en_us)" },
|
||||
{ "code": "es_es", "display": "Spanish (es_es)" },
|
||||
{ "code": "fa_fa", "display": "Farsi (fa_fa)" },
|
||||
{ "code": "id_id", "display": "Indonesian (id_id)" },
|
||||
{ "code": "fr_fr", "display": "French (fr_fr)" },
|
||||
{ "code": "it_it", "display": "Italian (it_it)" },
|
||||
{ "code": "ja_jp", "display": "Japanese (ja_jp)" },
|
||||
@@ -46,7 +46,7 @@ def load_language_codes(languages_json_path):
|
||||
if __name__ == "__main__":
|
||||
current_path = os.path.dirname(os.path.abspath(__file__))
|
||||
# language codes are loaded from languages.json — add a new language there
|
||||
languages_json = os.path.join(current_path, "languages.json")
|
||||
languages_json = os.path.join(current_path, "language_definitions/languages.json")
|
||||
codes = load_language_codes(languages_json)
|
||||
file_paths = [os.path.join(current_path, f"{code}.json") for code in codes]
|
||||
merge_translations(file_paths[0], file_paths[1:])
|
||||
|
||||
@@ -208,6 +208,7 @@
|
||||
"Device_NoData_Help": "",
|
||||
"Device_NoData_Scanning": "",
|
||||
"Device_NoData_Title": "",
|
||||
"Device_NoMatch_Title": "",
|
||||
"Device_Save_Failed": "",
|
||||
"Device_Save_Unauthorized": "",
|
||||
"Device_Saved_Success": "",
|
||||
|
||||
@@ -208,6 +208,7 @@
|
||||
"Device_NoData_Help": "",
|
||||
"Device_NoData_Scanning": "",
|
||||
"Device_NoData_Title": "",
|
||||
"Device_NoMatch_Title": "",
|
||||
"Device_Save_Failed": "",
|
||||
"Device_Save_Unauthorized": "",
|
||||
"Device_Saved_Success": "",
|
||||
|
||||
@@ -208,6 +208,7 @@
|
||||
"Device_NoData_Help": "",
|
||||
"Device_NoData_Scanning": "",
|
||||
"Device_NoData_Title": "",
|
||||
"Device_NoMatch_Title": "",
|
||||
"Device_Save_Failed": "",
|
||||
"Device_Save_Unauthorized": "",
|
||||
"Device_Saved_Success": "",
|
||||
|
||||
@@ -208,6 +208,7 @@
|
||||
"Device_NoData_Help": "",
|
||||
"Device_NoData_Scanning": "",
|
||||
"Device_NoData_Title": "",
|
||||
"Device_NoMatch_Title": "",
|
||||
"Device_Save_Failed": "",
|
||||
"Device_Save_Unauthorized": "",
|
||||
"Device_Saved_Success": "",
|
||||
|
||||
@@ -139,7 +139,7 @@
|
||||
"DevDetail_SessionTable_Duration": "Продолжительность",
|
||||
"DevDetail_SessionTable_IP": "IP",
|
||||
"DevDetail_SessionTable_Order": "Порядок",
|
||||
"DevDetail_Shortcut_CurrentStatus": "Текущий статус",
|
||||
"DevDetail_Shortcut_CurrentStatus": "Статус",
|
||||
"DevDetail_Shortcut_DownAlerts": "Оповещения о сбое",
|
||||
"DevDetail_Shortcut_Presence": "Присутствие",
|
||||
"DevDetail_Shortcut_Sessions": "Сеансы",
|
||||
@@ -203,16 +203,17 @@
|
||||
"Device_MultiEdit_MassActions": "Массовые действия:",
|
||||
"Device_MultiEdit_No_Devices": "Устройства не выбраны.",
|
||||
"Device_MultiEdit_Tooltip": "Осторожно. При нажатии на эту кнопку значение слева будет применено ко всем устройствам, выбранным выше.",
|
||||
"Device_NextScan_Imminent": "",
|
||||
"Device_NextScan_In": "",
|
||||
"Device_NoData_Help": "",
|
||||
"Device_NoData_Scanning": "",
|
||||
"Device_NoData_Title": "",
|
||||
"Device_NextScan_Imminent": "Скоро...",
|
||||
"Device_NextScan_In": "Следующее сканирование примерно через· ",
|
||||
"Device_NoData_Help": "Если устройства не отображаются после сканирования, проверьте настройку SCAN_SUBNETS и <a href=\"https://docs.netalertx.com/SUBNETS\" target=\"_blank\">документацию</a>.",
|
||||
"Device_NoData_Scanning": "Ожидание первого сканирования — это может занять несколько минут после первоначальной настройки.",
|
||||
"Device_NoData_Title": "Устройства пока не найдены",
|
||||
"Device_NoMatch_Title": "",
|
||||
"Device_Save_Failed": "Не удалось сохранить устройство",
|
||||
"Device_Save_Unauthorized": "Не авторизован - недействительный токен API",
|
||||
"Device_Saved_Success": "Устройство успешно сохранено",
|
||||
"Device_Saved_Unexpected": "Обновление устройства дало неожиданный ответ",
|
||||
"Device_Scanning": "",
|
||||
"Device_Scanning": "Сканирование...",
|
||||
"Device_Searchbox": "Поиск",
|
||||
"Device_Shortcut_AllDevices": "Мои устройства",
|
||||
"Device_Shortcut_AllNodes": "Все узлы",
|
||||
@@ -224,14 +225,14 @@
|
||||
"Device_Shortcut_Favorites": "Избранные",
|
||||
"Device_Shortcut_NewDevices": "Новые устройства",
|
||||
"Device_Shortcut_OnlineChart": "Присутствие устройств",
|
||||
"Device_Shortcut_Unstable": "",
|
||||
"Device_Shortcut_Unstable": "Нестабильный",
|
||||
"Device_TableHead_AlertDown": "Оповещение о сост. ВЫКЛ",
|
||||
"Device_TableHead_Connected_Devices": "Соединения",
|
||||
"Device_TableHead_CustomProps": "Свойства / Действия",
|
||||
"Device_TableHead_FQDN": "FQDN",
|
||||
"Device_TableHead_Favorite": "Избранное",
|
||||
"Device_TableHead_FirstSession": "Первый сеанс",
|
||||
"Device_TableHead_Flapping": "",
|
||||
"Device_TableHead_Flapping": "Нестабильный",
|
||||
"Device_TableHead_GUID": "GUID",
|
||||
"Device_TableHead_Group": "Группа",
|
||||
"Device_TableHead_IPv4": "IPv4",
|
||||
@@ -322,7 +323,7 @@
|
||||
"Gen_AddDevice": "Добавить устройство",
|
||||
"Gen_Add_All": "Добавить все",
|
||||
"Gen_All_Devices": "Все устройства",
|
||||
"Gen_Archived": "",
|
||||
"Gen_Archived": "Архивировано",
|
||||
"Gen_AreYouSure": "Вы уверены?",
|
||||
"Gen_Backup": "Запустить резервное копирование",
|
||||
"Gen_Cancel": "Отмена",
|
||||
@@ -333,16 +334,16 @@
|
||||
"Gen_Delete": "Удалить",
|
||||
"Gen_DeleteAll": "Удалить все",
|
||||
"Gen_Description": "Описание",
|
||||
"Gen_Down": "",
|
||||
"Gen_Down": "Лежит",
|
||||
"Gen_Error": "Ошибка",
|
||||
"Gen_Filter": "Фильтр",
|
||||
"Gen_Flapping": "",
|
||||
"Gen_Flapping": "Нестабильный",
|
||||
"Gen_Generate": "Генерировать",
|
||||
"Gen_InvalidMac": "Неверный Mac-адрес.",
|
||||
"Gen_Invalid_Value": "Введено некорректное значение",
|
||||
"Gen_LockedDB": "ОШИБКА - Возможно, база данных заблокирована. Проверьте инструменты разработчика F12 -> Консоль или повторите попытку позже.",
|
||||
"Gen_NetworkMask": "Маска сети",
|
||||
"Gen_New": "",
|
||||
"Gen_New": "Новый",
|
||||
"Gen_Offline": "Оффлайн",
|
||||
"Gen_Okay": "OK",
|
||||
"Gen_Online": "Онлайн",
|
||||
@@ -360,7 +361,7 @@
|
||||
"Gen_SelectIcon": "<i class=\"fa-solid fa-chevron-down fa-fade\"></i>",
|
||||
"Gen_SelectToPreview": "Выберите для предварительного просмотра",
|
||||
"Gen_Selected_Devices": "Выбранные устройства:",
|
||||
"Gen_Sleeping": "",
|
||||
"Gen_Sleeping": "Спящий",
|
||||
"Gen_Subnet": "Подсеть",
|
||||
"Gen_Switch": "Переключить",
|
||||
"Gen_Upd": "Успешное обновление",
|
||||
@@ -590,8 +591,8 @@
|
||||
"PIALERT_WEB_PROTECTION_name": "Включить вход",
|
||||
"PLUGINS_KEEP_HIST_description": "Сколько записей результатов сканирования истории плагинов следует хранить (для каждого плагина, а не для конкретного устройства).",
|
||||
"PLUGINS_KEEP_HIST_name": "История плагинов",
|
||||
"PRAGMA_JOURNAL_SIZE_LIMIT_description": "",
|
||||
"PRAGMA_JOURNAL_SIZE_LIMIT_name": "",
|
||||
"PRAGMA_JOURNAL_SIZE_LIMIT_description": "Максимальный размер SQLite WAL (журнал упреждающей записи) в МБ перед запуском автоматических контрольных точек. Более низкие значения (10–20 МБ) уменьшают использование диска/хранилища, но увеличивают загрузку ЦП во время сканирования. Более высокие значения (50–100 МБ) уменьшают нагрузку на процессор во время операций, но могут использовать больше оперативной памяти и дискового пространства. Значение по умолчанию <code>50 МБ</code> компенсирует и то, и другое. Полезно для систем с ограниченными ресурсами, таких как устройства NAS с SD-картами. Перезапустите сервер, чтобы изменения вступили в силу после сохранения настроек.",
|
||||
"PRAGMA_JOURNAL_SIZE_LIMIT_name": "Ограничение размера WAL (МБ)",
|
||||
"Plugins_DeleteAll": "Удалить все (фильтры игнорируются)",
|
||||
"Plugins_Filters_Mac": "Фильтр MAC-адреса",
|
||||
"Plugins_History": "История событий",
|
||||
@@ -804,4 +805,4 @@
|
||||
"settings_system_label": "Система",
|
||||
"settings_update_item_warning": "Обновить значение ниже. Будьте осторожны, следуя предыдущему формату. <b>Проверка не выполняется.</b>",
|
||||
"test_event_tooltip": "Сначала сохраните изменения, прежде чем проверять настройки."
|
||||
}
|
||||
}
|
||||
|
||||
@@ -208,6 +208,7 @@
|
||||
"Device_NoData_Help": "",
|
||||
"Device_NoData_Scanning": "",
|
||||
"Device_NoData_Title": "",
|
||||
"Device_NoMatch_Title": "",
|
||||
"Device_Save_Failed": "",
|
||||
"Device_Save_Unauthorized": "",
|
||||
"Device_Saved_Success": "",
|
||||
|
||||
@@ -208,6 +208,7 @@
|
||||
"Device_NoData_Help": "",
|
||||
"Device_NoData_Scanning": "",
|
||||
"Device_NoData_Title": "",
|
||||
"Device_NoMatch_Title": "",
|
||||
"Device_Save_Failed": "",
|
||||
"Device_Save_Unauthorized": "",
|
||||
"Device_Saved_Success": "",
|
||||
|
||||
@@ -208,6 +208,7 @@
|
||||
"Device_NoData_Help": "",
|
||||
"Device_NoData_Scanning": "",
|
||||
"Device_NoData_Title": "",
|
||||
"Device_NoMatch_Title": "",
|
||||
"Device_Save_Failed": "Не вдалося зберегти пристрій",
|
||||
"Device_Save_Unauthorized": "Неавторизовано – недійсний токен API",
|
||||
"Device_Saved_Success": "Пристрій успішно збережено",
|
||||
|
||||
@@ -208,6 +208,7 @@
|
||||
"Device_NoData_Help": "",
|
||||
"Device_NoData_Scanning": "",
|
||||
"Device_NoData_Title": "",
|
||||
"Device_NoMatch_Title": "",
|
||||
"Device_Save_Failed": "",
|
||||
"Device_Save_Unauthorized": "",
|
||||
"Device_Saved_Success": "",
|
||||
|
||||
@@ -208,6 +208,7 @@
|
||||
"Device_NoData_Help": "",
|
||||
"Device_NoData_Scanning": "",
|
||||
"Device_NoData_Title": "",
|
||||
"Device_NoMatch_Title": "",
|
||||
"Device_Save_Failed": "保存设备失败",
|
||||
"Device_Save_Unauthorized": "未授权 - API 令牌无效",
|
||||
"Device_Saved_Success": "设备保存成功",
|
||||
@@ -374,9 +375,9 @@
|
||||
"Gen_create_new_device_info": "通常使用<a target=\"_blank\" href=\"https://docs.netalertx.com/PLUGINS\">plugins</a>来发现设备。但是,在某些情况下,您可能需要手动添加设备。要探索特定场景,请查看<a target=\"_blank\" href=\"https://docs.netalertx.com/REMOTE_NETWORKS\">远程网络文档</a>。",
|
||||
"General_display_name": "通用",
|
||||
"General_icon": "<i class=\"fa fa-gears\"></i>",
|
||||
"HRS_TO_KEEP_NEWDEV_description": "这是一项维护设置<b>删除设备</b>。如果启用该设置(<code>0</code> 为禁用),则标记为<b>新设备</b>的设备(如果其<b>首次会话</b>时间早于此设置中指定的小时数)将被删除。如果您想在 <code>X</code> 小时后自动删除<b>新设备</b>,请使用此设置。",
|
||||
"HRS_TO_KEEP_NEWDEV_description": "这是一项 <b>删除设备</b> 的维护性设置。如启用(<code>0</code> 代表禁用),被标记为 <b>新设备</b> 的设备将被删除,如果它们的 <b>首个会话</b> 时间比这个设置中指定的时长要短。如果想在 <code>X</code> 个小时后自动删除 <b>新设备</b> 请使用本设置。",
|
||||
"HRS_TO_KEEP_NEWDEV_name": "小时后删除新设备",
|
||||
"HRS_TO_KEEP_OFFDEV_description": "这是维护设置<b>删除设备</b>。如果启用了这个设置(<code>0</code>是禁用),任何<b>上次链接</b>时间比设置里存的指定时间长的<b>离线</b>设备都会被删除。要是您想<code>X</code>小时以候自动处理<b>下线设备</b>,请用这个设置。",
|
||||
"HRS_TO_KEEP_OFFDEV_description": "这是<b>删除设备</b>的维护设置。如果启用了这个设置(<code>0</code>是禁用),任何<b>上次连接</b>时间比设置里存的指定时间长的<b>离线</b>设备都会被删除。要是您想在<code>X</code>小时后自动删除<b>离线设备</b>,请用这个设置。",
|
||||
"HRS_TO_KEEP_OFFDEV_name": "保留离线设备",
|
||||
"LOADED_PLUGINS_description": "加载哪些插件。添加插件可能会降低应用程序的速度。在<a target=\"_blank\" href=\"https://docs.netalertx.com/PLUGINS\">插件文档</a>中详细了解需要启用哪些插件、插件类型或扫描选项。卸载插件将丢失您的设置。只有<code>已禁用</code>的插件才能卸载。",
|
||||
"LOADED_PLUGINS_name": "已加载插件",
|
||||
@@ -804,4 +805,4 @@
|
||||
"settings_system_label": "系统",
|
||||
"settings_update_item_warning": "更新下面的值。请注意遵循先前的格式。<b>未执行验证。</b>",
|
||||
"test_event_tooltip": "在测试设置之前,请先保存更改。"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -222,27 +222,30 @@ def main():
|
||||
extra = '',
|
||||
foreignKey = device['devGUID'])
|
||||
|
||||
# Resolve the actual columns that exist in the Devices table once.
|
||||
# This automatically excludes computed/virtual fields (e.g. devStatus,
|
||||
# devIsSleeping) and 'rowid' without needing a maintained exclusion list.
|
||||
cursor.execute("PRAGMA table_info(Devices)")
|
||||
db_columns = {row[1] for row in cursor.fetchall()}
|
||||
|
||||
# Filter out existing devices
|
||||
new_devices = [device for device in device_data if device['devMac'] not in existing_mac_addresses]
|
||||
|
||||
# Remove 'rowid' key if it exists
|
||||
for device in new_devices:
|
||||
device.pop('rowid', None)
|
||||
device.pop('devStatus', None)
|
||||
|
||||
mylog('verbose', [f'[{pluginName}] All devices: "{len(device_data)}"'])
|
||||
mylog('verbose', [f'[{pluginName}] New devices: "{len(new_devices)}"'])
|
||||
|
||||
# Prepare the insert statement
|
||||
if new_devices:
|
||||
|
||||
# creating insert statement, removing 'rowid', 'devStatus' as handled on the target and devStatus is resolved on the fly
|
||||
columns = ', '.join(k for k in new_devices[0].keys() if k not in ['rowid', 'devStatus'])
|
||||
placeholders = ', '.join('?' for k in new_devices[0] if k not in ['rowid', 'devStatus'])
|
||||
# Only keep keys that are real columns in the target DB; computed
|
||||
# or unknown fields are silently dropped regardless of source schema.
|
||||
insert_cols = [k for k in new_devices[0].keys() if k in db_columns]
|
||||
columns = ', '.join(insert_cols)
|
||||
placeholders = ', '.join('?' for _ in insert_cols)
|
||||
sql = f'INSERT INTO Devices ({columns}) VALUES ({placeholders})'
|
||||
|
||||
# Extract values for the new devices
|
||||
values = [tuple(device.values()) for device in new_devices]
|
||||
# Extract only the whitelisted column values for each device
|
||||
values = [tuple(device.get(col) for col in insert_cols) for device in new_devices]
|
||||
|
||||
mylog('verbose', [f'[{pluginName}] Inserting Devices SQL : "{sql}"'])
|
||||
mylog('verbose', [f'[{pluginName}] Inserting Devices VALUES: "{values}"'])
|
||||
|
||||
@@ -13,6 +13,9 @@ services:
|
||||
- CHOWN
|
||||
- SETUID
|
||||
- SETGID
|
||||
sysctls:
|
||||
net.ipv4.conf.all.arp_ignore: 1
|
||||
net.ipv4.conf.all.arp_announce: 2
|
||||
volumes:
|
||||
- type: volume
|
||||
source: netalertx_data
|
||||
|
||||
@@ -13,6 +13,9 @@ services:
|
||||
- CHOWN
|
||||
- SETUID
|
||||
- SETGID
|
||||
sysctls:
|
||||
net.ipv4.conf.all.arp_ignore: 1
|
||||
net.ipv4.conf.all.arp_announce: 2
|
||||
volumes:
|
||||
- type: volume
|
||||
source: netalertx_data
|
||||
|
||||
33
install/production-filesystem/entrypoint.d/36-override-individual-settings.sh
Normal file → Executable file
33
install/production-filesystem/entrypoint.d/36-override-individual-settings.sh
Normal file → Executable file
@@ -9,28 +9,17 @@ if [ ! -f "${NETALERTX_CONFIG}/app.conf" ]; then
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Helper: set or append config key safely
|
||||
set_config_value() {
|
||||
_key="$1"
|
||||
_value="$2"
|
||||
|
||||
# Remove newlines just in case
|
||||
_value=$(printf '%s' "$_value" | tr -d '\n\r')
|
||||
|
||||
# Escape sed-sensitive chars
|
||||
_escaped=$(printf '%s\n' "$_value" | sed 's/[\/&]/\\&/g')
|
||||
|
||||
if grep -q "^${_key}=" "${NETALERTX_CONFIG}/app.conf"; then
|
||||
sed -i "s|^${_key}=.*|${_key}=${_escaped}|" "${NETALERTX_CONFIG}/app.conf"
|
||||
else
|
||||
echo "${_key}=${_value}" >> "${NETALERTX_CONFIG}/app.conf"
|
||||
fi
|
||||
}
|
||||
|
||||
# ------------------------------------------------------------
|
||||
# LOADED_PLUGINS override
|
||||
# ------------------------------------------------------------
|
||||
if [ -n "${LOADED_PLUGINS:-}" ]; then
|
||||
echo "[ENV] Applying LOADED_PLUGINS override"
|
||||
set_config_value "LOADED_PLUGINS" "$LOADED_PLUGINS"
|
||||
value=$(printf '%s' "$LOADED_PLUGINS" | tr -d '\n\r')
|
||||
# declare delimiter for sed and escape it along with / and &
|
||||
delim='|'
|
||||
escaped=$(printf '%s\n' "$value" | sed "s/[\/${delim}&]/\\&/g")
|
||||
|
||||
if grep -q '^LOADED_PLUGINS=' "${NETALERTX_CONFIG}/app.conf"; then
|
||||
# use same delimiter when substituting
|
||||
sed -i "s${delim}^LOADED_PLUGINS=.*${delim}LOADED_PLUGINS=${escaped}${delim}" "${NETALERTX_CONFIG}/app.conf"
|
||||
else
|
||||
echo "LOADED_PLUGINS=${value}" >> "${NETALERTX_CONFIG}/app.conf"
|
||||
fi
|
||||
fi
|
||||
|
||||
89
install/production-filesystem/entrypoint.d/37-host-optimization.sh
Normal file → Executable file
89
install/production-filesystem/entrypoint.d/37-host-optimization.sh
Normal file → Executable file
@@ -1,92 +1,35 @@
|
||||
#!/bin/sh
|
||||
|
||||
# 37-host-optimization.sh: Apply and validate network optimizations (ARP flux fix)
|
||||
# 37-host-optimization.sh: Detect ARP flux sysctl configuration.
|
||||
#
|
||||
# This script improves detection accuracy by ensuring proper ARP behavior.
|
||||
# It attempts to apply sysctl settings and warns if not possible.
|
||||
# This script does not change host/kernel settings.
|
||||
|
||||
# --- Color Codes ---
|
||||
RED=$(printf '\033[1;31m')
|
||||
YELLOW=$(printf '\033[1;33m')
|
||||
RESET=$(printf '\033[0m')
|
||||
|
||||
# --- Skip flag ---
|
||||
if [ -n "${SKIP_OPTIMIZATIONS:-}" ]; then
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# --- Helpers ---
|
||||
|
||||
get_sysctl() {
|
||||
sysctl -n "$1" 2>/dev/null || echo "unknown"
|
||||
}
|
||||
|
||||
set_sysctl_if_needed() {
|
||||
key="$1"
|
||||
expected="$2"
|
||||
|
||||
current="$(get_sysctl "$key")"
|
||||
|
||||
# Already correct
|
||||
if [ "$current" = "$expected" ]; then
|
||||
return 0
|
||||
fi
|
||||
|
||||
# Try to apply
|
||||
if sysctl -w "$key=$expected" >/dev/null 2>&1; then
|
||||
return 0
|
||||
fi
|
||||
|
||||
# Failed
|
||||
return 1
|
||||
}
|
||||
|
||||
# --- Apply Settings (best effort) ---
|
||||
|
||||
failed=0
|
||||
|
||||
set_sysctl_if_needed net.ipv4.conf.all.arp_ignore 1 || failed=1
|
||||
set_sysctl_if_needed net.ipv4.conf.all.arp_announce 2 || failed=1
|
||||
set_sysctl_if_needed net.ipv4.conf.default.arp_ignore 1 || failed=1
|
||||
set_sysctl_if_needed net.ipv4.conf.default.arp_announce 2 || failed=1
|
||||
[ "$(sysctl -n net.ipv4.conf.all.arp_ignore 2>/dev/null || echo unknown)" = "1" ] || failed=1
|
||||
[ "$(sysctl -n net.ipv4.conf.all.arp_announce 2>/dev/null || echo unknown)" = "2" ] || failed=1
|
||||
|
||||
# --- Validate final state ---
|
||||
|
||||
all_ignore="$(get_sysctl net.ipv4.conf.all.arp_ignore)"
|
||||
all_announce="$(get_sysctl net.ipv4.conf.all.arp_announce)"
|
||||
|
||||
# --- Warning Output ---
|
||||
|
||||
if [ "$all_ignore" != "1" ] || [ "$all_announce" != "2" ]; then
|
||||
if [ "$failed" -eq 1 ]; then
|
||||
>&2 printf "%s" "${YELLOW}"
|
||||
>&2 cat <<EOF
|
||||
>&2 cat <<'EOF'
|
||||
══════════════════════════════════════════════════════════════════════════════
|
||||
⚠️ ATTENTION: ARP flux protection not enabled.
|
||||
|
||||
NetAlertX relies on ARP for device detection. Your system currently allows
|
||||
ARP replies from incorrect interfaces (ARP flux), which may result in:
|
||||
|
||||
• False devices being detected
|
||||
• IP/MAC mismatches
|
||||
• Flapping device states
|
||||
• Incorrect network topology
|
||||
|
||||
This is common when running in Docker or multi-interface environments.
|
||||
|
||||
──────────────────────────────────────────────────────────────────────────
|
||||
Recommended fix (Docker Compose):
|
||||
|
||||
sysctls:
|
||||
net.ipv4.conf.all.arp_ignore: 1
|
||||
net.ipv4.conf.all.arp_announce: 2
|
||||
|
||||
──────────────────────────────────────────────────────────────────────────
|
||||
Alternatively, apply on the host:
|
||||
⚠️ WARNING: ARP flux sysctls are not set.
|
||||
|
||||
Expected values:
|
||||
net.ipv4.conf.all.arp_ignore=1
|
||||
net.ipv4.conf.all.arp_announce=2
|
||||
|
||||
Detection accuracy may be reduced until this is configured.
|
||||
Note: If using 'network_mode: host', setting these via docker-compose sysctls
|
||||
requires the NET_ADMIN capability. When granted, these sysctls will
|
||||
modify the host namespace. Otherwise, you must configure them directly
|
||||
on your host operating system instead.
|
||||
|
||||
Detection accuracy may be reduced until configured.
|
||||
|
||||
See: https://docs.netalertx.com/docker-troubleshooting/arp-flux-sysctls/
|
||||
══════════════════════════════════════════════════════════════════════════════
|
||||
EOF
|
||||
>&2 printf "%s" "${RESET}"
|
||||
|
||||
@@ -86,10 +86,11 @@ for script in "${ENTRYPOINT_CHECKS}"/*; do
|
||||
fi
|
||||
script_name=$(basename "$script" | sed 's/^[0-9]*-//;s/\.(sh|py)$//;s/-/ /g')
|
||||
echo "--> ${script_name} "
|
||||
if [ -n "${SKIP_STARTUP_CHECKS:-}" ] && echo "${SKIP_STARTUP_CHECKS}" | grep -q "\b${script_name}\b"; then
|
||||
printf "%sskip%s\n" "${GREY}" "${RESET}"
|
||||
continue
|
||||
fi
|
||||
if [ -n "${SKIP_STARTUP_CHECKS:-}" ] &&
|
||||
printf '%s' "${SKIP_STARTUP_CHECKS}" | grep -wFq -- "${script_name}"; then
|
||||
printf "%sskip%s\n" "${GREY}" "${RESET}"
|
||||
continue
|
||||
fi
|
||||
|
||||
"$script"
|
||||
NETALERTX_DOCKER_ERROR_CHECK=$?
|
||||
|
||||
@@ -48,11 +48,13 @@ else
|
||||
log_error "python /app/server is not running"
|
||||
fi
|
||||
|
||||
# 5. Check port 20211 is open and contains "netalertx"
|
||||
if curl -sf --max-time 10 "http://localhost:${PORT:-20211}" | grep -i "netalertx" > /dev/null; then
|
||||
log_success "Port ${PORT:-20211} is responding and contains 'netalertx'"
|
||||
# 5. Check port 20211 is open
|
||||
CHECK_ADDR="${LISTEN_ADDR:-127.0.0.1}"
|
||||
[ "${CHECK_ADDR}" == "0.0.0.0" ] && CHECK_ADDR="127.0.0.1"
|
||||
if timeout 10 bash -c "</dev/tcp/${CHECK_ADDR}/${PORT:-20211}" 2>/dev/null; then
|
||||
log_success "Port ${PORT:-20211} is responding"
|
||||
else
|
||||
log_error "Port ${PORT:-20211} is not responding or doesn't contain 'netalertx'"
|
||||
log_error "Port ${PORT:-20211} is not responding"
|
||||
fi
|
||||
|
||||
# NOTE: GRAPHQL_PORT might not be set and is initailized as a setting with a default value in the container. It can also be initialized via APP_CONF_OVERRIDE
|
||||
@@ -71,4 +73,4 @@ else
|
||||
echo "[HEALTHCHECK] ❌ One or more health checks failed"
|
||||
fi
|
||||
|
||||
exit $EXIT_CODE
|
||||
exit $EXIT_CODE
|
||||
|
||||
@@ -20,6 +20,7 @@ nav:
|
||||
- Docker Updates: UPDATES.md
|
||||
- Docker Maintenance: DOCKER_MAINTENANCE.md
|
||||
- Docker Startup Troubleshooting:
|
||||
- ARP flux sysctls: docker-troubleshooting/arp-flux-sysctls.md
|
||||
- Aufs capabilities: docker-troubleshooting/aufs-capabilities.md
|
||||
- Excessive capabilities: docker-troubleshooting/excessive-capabilities.md
|
||||
- File permissions: docker-troubleshooting/file-permissions.md
|
||||
|
||||
@@ -85,7 +85,7 @@ class Device(ObjectType):
|
||||
devStatus = String(description="Online/Offline status")
|
||||
devIsRandomMac = Int(description="Calculated: Is MAC address randomized?")
|
||||
devParentChildrenCount = Int(description="Calculated: Number of children attached to this parent")
|
||||
devIpLong = Int(description="Calculated: IP address in long format")
|
||||
devIpLong = String(description="Calculated: IP address in long format (returned as string to support the full unsigned 32-bit range)")
|
||||
devFilterStatus = String(description="Calculated: Device status for UI filtering")
|
||||
devFQDN = String(description="Fully Qualified Domain Name")
|
||||
devParentRelType = String(description="Relationship type to parent")
|
||||
@@ -108,6 +108,7 @@ class Device(ObjectType):
|
||||
class DeviceResult(ObjectType):
|
||||
devices = List(Device)
|
||||
count = Int()
|
||||
db_count = Int(description="Total device count in the database, before any status/filter/search is applied")
|
||||
|
||||
|
||||
# --- SETTINGS ---
|
||||
@@ -198,7 +199,14 @@ class Query(ObjectType):
|
||||
devices_data = json.load(f)["data"]
|
||||
except (FileNotFoundError, json.JSONDecodeError) as e:
|
||||
mylog("none", f"[graphql_schema] Error loading devices data: {e}")
|
||||
return DeviceResult(devices=[], count=0)
|
||||
return DeviceResult(devices=[], count=0, db_count=0)
|
||||
|
||||
# Int fields that may arrive from the DB as empty strings — coerce to None
|
||||
_INT_FIELDS = [
|
||||
"devFavorite", "devStaticIP", "devScan", "devLogEvents", "devAlertEvents",
|
||||
"devAlertDown", "devSkipRepeated", "devPresentLastScan", "devIsNew",
|
||||
"devIsArchived", "devReqNicsOnline", "devFlapping", "devCanSleep", "devIsSleeping",
|
||||
]
|
||||
|
||||
# Add dynamic fields to each device
|
||||
for device in devices_data:
|
||||
@@ -206,10 +214,20 @@ class Query(ObjectType):
|
||||
device["devParentChildrenCount"] = get_number_of_children(
|
||||
device["devMac"], devices_data
|
||||
)
|
||||
device["devIpLong"] = format_ip_long(device.get("devLastIP", ""))
|
||||
# Return as string — IPv4 long values can exceed Int's signed 32-bit max (2,147,483,647)
|
||||
device["devIpLong"] = str(format_ip_long(device.get("devLastIP", "")))
|
||||
|
||||
# Coerce empty strings to None so GraphQL Int serialisation doesn't fail
|
||||
for _field in _INT_FIELDS:
|
||||
if device.get(_field) == "":
|
||||
device[_field] = None
|
||||
|
||||
mylog("trace", f"[graphql_schema] devices_data: {devices_data}")
|
||||
|
||||
# Raw DB count — before any status, filter, or search is applied.
|
||||
# Used by the frontend to distinguish "no devices in DB" from "filter returned nothing".
|
||||
db_count = len(devices_data)
|
||||
|
||||
# initialize total_count
|
||||
total_count = len(devices_data)
|
||||
|
||||
@@ -426,7 +444,7 @@ class Query(ObjectType):
|
||||
# Convert dict objects to Device instances to enable field resolution
|
||||
devices = [Device(**device) for device in devices_data]
|
||||
|
||||
return DeviceResult(devices=devices, count=total_count)
|
||||
return DeviceResult(devices=devices, count=total_count, db_count=db_count)
|
||||
|
||||
# --- SETTINGS ---
|
||||
settings = Field(SettingResult, filters=List(FilterOptionsInput))
|
||||
|
||||
@@ -7,7 +7,7 @@ from logger import mylog
|
||||
|
||||
INSTALL_PATH = os.getenv("NETALERTX_APP", "/app")
|
||||
LANGUAGES_JSON_PATH = os.path.join(
|
||||
INSTALL_PATH, "front", "php", "templates", "language", "languages.json"
|
||||
INSTALL_PATH, "front", "php", "templates", "language", "language_definitions", "languages.json"
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -33,7 +33,7 @@ def get_device_conditions():
|
||||
"unknown": f"WHERE {base_active} AND devName IN ({NULL_EQUIVALENTS_SQL})",
|
||||
"known": f"WHERE {base_active} AND devName NOT IN ({NULL_EQUIVALENTS_SQL})",
|
||||
"favorites_offline": f"WHERE {base_active} AND devFavorite=1 AND devPresentLastScan=0",
|
||||
"new_online": f"WHERE {base_active} AND devIsNew=1 AND devPresentLastScan=0",
|
||||
"new_online": f"WHERE {base_active} AND devIsNew=1 AND devPresentLastScan=1",
|
||||
"unstable_devices": f"WHERE {base_active} AND devFlapping=1",
|
||||
"unstable_favorites": f"WHERE {base_active} AND devFavorite=1 AND devFlapping=1",
|
||||
"unstable_network_devices": f"WHERE {base_active} AND devType IN ({network_dev_types}) AND devFlapping=1",
|
||||
|
||||
@@ -27,7 +27,7 @@ from messaging.in_app import write_notification
|
||||
# ===============================================================================
|
||||
|
||||
_LANGUAGES_JSON = os.path.join(
|
||||
applicationPath, "front", "php", "templates", "language", "languages.json"
|
||||
applicationPath, "front", "php", "templates", "language", "language_definitions" ,"languages.json"
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -115,24 +115,19 @@ def is_datetime_future(dt, current_threshold=None):
|
||||
return dt > current_threshold
|
||||
|
||||
|
||||
def ensure_future_datetime(schedule_obj, current_threshold=None, max_retries=5):
|
||||
def ensure_future_datetime(schedule_obj, current_threshold=None):
|
||||
"""
|
||||
Ensure a schedule's next() call returns a datetime strictly in the future.
|
||||
|
||||
This is a defensive utility for cron/schedule libraries that should always return
|
||||
future times but may have edge cases. Validates and retries if needed.
|
||||
Keeps calling .next() until a future time is returned — never raises.
|
||||
|
||||
Args:
|
||||
schedule_obj: A schedule object with a .next() method (e.g., from croniter/APScheduler)
|
||||
current_threshold: datetime to compare against. If None, uses timeNowTZ(as_string=False)
|
||||
max_retries: Maximum times to call .next() if result is not in future (default: 5)
|
||||
|
||||
Returns:
|
||||
datetime.datetime: A guaranteed future datetime from schedule_obj.next()
|
||||
|
||||
Raises:
|
||||
RuntimeError: If max_retries exceeded without getting a future time
|
||||
|
||||
Examples:
|
||||
newSchedule = Cron(run_sch).schedule(start_date=timeNowUTC(as_string=False))
|
||||
next_time = ensure_future_datetime(newSchedule)
|
||||
@@ -141,17 +136,9 @@ def ensure_future_datetime(schedule_obj, current_threshold=None, max_retries=5):
|
||||
current_threshold = timeNowTZ(as_string=False)
|
||||
|
||||
next_time = schedule_obj.next()
|
||||
retries = 0
|
||||
|
||||
while next_time <= current_threshold and retries < max_retries:
|
||||
while next_time <= current_threshold:
|
||||
next_time = schedule_obj.next()
|
||||
retries += 1
|
||||
|
||||
if next_time <= current_threshold:
|
||||
raise RuntimeError(
|
||||
f"[ensure_future_datetime] Failed to get future time after {max_retries} retries. "
|
||||
f"Last attempt: {next_time}, Current time: {current_threshold}"
|
||||
)
|
||||
|
||||
return next_time
|
||||
|
||||
|
||||
@@ -43,6 +43,10 @@ def create_dummy(client, api_token, test_mac):
|
||||
client.post(f"/device/{test_mac}", json=payload, headers=auth_headers(api_token))
|
||||
|
||||
|
||||
def delete_dummy(client, api_token, test_mac):
|
||||
client.delete("/devices", json={"macs": [test_mac]}, headers=auth_headers(api_token))
|
||||
|
||||
|
||||
def test_get_all_devices(client, api_token, test_mac):
|
||||
# Ensure there is at least one device
|
||||
create_dummy(client, api_token, test_mac)
|
||||
@@ -149,53 +153,62 @@ def test_export_import_cycle_base64(client, api_token, test_mac):
|
||||
|
||||
|
||||
def test_devices_totals(client, api_token, test_mac):
|
||||
# 1. Create a dummy device
|
||||
create_dummy(client, api_token, test_mac)
|
||||
try:
|
||||
# 1. Call the totals endpoint
|
||||
resp = client.get("/devices/totals", headers=auth_headers(api_token))
|
||||
assert resp.status_code == 200
|
||||
|
||||
# 2. Call the totals endpoint
|
||||
resp = client.get("/devices/totals", headers=auth_headers(api_token))
|
||||
assert resp.status_code == 200
|
||||
# 2. Ensure the response is a JSON list
|
||||
data = resp.json
|
||||
assert isinstance(data, list)
|
||||
|
||||
# 3. Ensure the response is a JSON list
|
||||
data = resp.json
|
||||
assert isinstance(data, list)
|
||||
# 3. Dynamically get expected length
|
||||
conditions = get_device_conditions()
|
||||
expected_length = len(conditions)
|
||||
assert len(data) == expected_length
|
||||
|
||||
# 4. Dynamically get expected length
|
||||
conditions = get_device_conditions()
|
||||
expected_length = len(conditions)
|
||||
assert len(data) == expected_length
|
||||
|
||||
# 5. Check that at least 1 device exists
|
||||
assert data[0] >= 1 # 'devices' count includes the dummy device
|
||||
# 4. Check that at least 1 device exists when there are any conditions
|
||||
if expected_length > 0:
|
||||
assert data[0] >= 1 # 'devices' count includes the dummy device
|
||||
else:
|
||||
# no conditions defined; data should be an empty list
|
||||
assert data == []
|
||||
finally:
|
||||
delete_dummy(client, api_token, test_mac)
|
||||
|
||||
|
||||
def test_devices_by_status(client, api_token, test_mac):
|
||||
# 1. Create a dummy device
|
||||
create_dummy(client, api_token, test_mac)
|
||||
try:
|
||||
# 1. Request devices by a valid status
|
||||
resp = client.get("/devices/by-status?status=my", headers=auth_headers(api_token))
|
||||
assert resp.status_code == 200
|
||||
data = resp.json
|
||||
assert isinstance(data, list)
|
||||
assert any(d["id"] == test_mac for d in data)
|
||||
|
||||
# 2. Request devices by a valid status
|
||||
resp = client.get("/devices/by-status?status=my", headers=auth_headers(api_token))
|
||||
assert resp.status_code == 200
|
||||
data = resp.json
|
||||
assert isinstance(data, list)
|
||||
assert any(d["id"] == test_mac for d in data)
|
||||
# 2. Request devices with an invalid/unknown status
|
||||
resp_invalid = client.get("/devices/by-status?status=invalid_status", headers=auth_headers(api_token))
|
||||
# Strict validation now returns 422 for invalid status enum values
|
||||
assert resp_invalid.status_code == 422
|
||||
|
||||
# 3. Request devices with an invalid/unknown status
|
||||
resp_invalid = client.get("/devices/by-status?status=invalid_status", headers=auth_headers(api_token))
|
||||
# Strict validation now returns 422 for invalid status enum values
|
||||
assert resp_invalid.status_code == 422
|
||||
# 3. Check favorite formatting if devFavorite = 1
|
||||
# Update dummy device to favorite
|
||||
update_resp = client.post(
|
||||
f"/device/{test_mac}",
|
||||
json={"devFavorite": 1},
|
||||
headers=auth_headers(api_token)
|
||||
)
|
||||
assert update_resp.status_code == 200
|
||||
assert update_resp.json.get("success") is True
|
||||
|
||||
# 4. Check favorite formatting if devFavorite = 1
|
||||
# Update dummy device to favorite
|
||||
client.post(
|
||||
f"/device/{test_mac}",
|
||||
json={"devFavorite": 1},
|
||||
headers=auth_headers(api_token)
|
||||
)
|
||||
resp_fav = client.get("/devices/by-status?status=my", headers=auth_headers(api_token))
|
||||
fav_data = next((d for d in resp_fav.json if d["id"] == test_mac), None)
|
||||
assert fav_data is not None
|
||||
assert "★" in fav_data["title"]
|
||||
resp_fav = client.get("/devices/by-status?status=my", headers=auth_headers(api_token))
|
||||
fav_data = next((d for d in resp_fav.json if d["id"] == test_mac), None)
|
||||
assert fav_data is not None
|
||||
assert "★" in fav_data["title"]
|
||||
finally:
|
||||
delete_dummy(client, api_token, test_mac)
|
||||
|
||||
|
||||
def test_delete_test_devices(client, api_token):
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import pytest
|
||||
from unittest.mock import patch, MagicMock
|
||||
from datetime import datetime
|
||||
import random
|
||||
|
||||
from api_server.api_server_start import app
|
||||
from helper import get_setting_value
|
||||
@@ -21,6 +22,31 @@ def auth_headers(token):
|
||||
return {"Authorization": f"Bearer {token}"}
|
||||
|
||||
|
||||
def create_dummy(client, api_token, test_mac):
|
||||
payload = {
|
||||
"createNew": True,
|
||||
"devName": "Test Device MCP",
|
||||
"devOwner": "Unit Test",
|
||||
"devType": "Router",
|
||||
"devVendor": "TestVendor",
|
||||
}
|
||||
response = client.post(f"/device/{test_mac}", json=payload, headers=auth_headers(api_token))
|
||||
assert response.status_code in [200, 201], (
|
||||
f"Expected status 200/201 for device creation, got {response.status_code}. "
|
||||
f"Response body: {response.get_data(as_text=True)}"
|
||||
)
|
||||
return response
|
||||
|
||||
|
||||
def delete_dummy(client, api_token, test_mac):
|
||||
response = client.delete("/devices", json={"macs": [test_mac]}, headers=auth_headers(api_token))
|
||||
assert response.status_code == 200, (
|
||||
f"Expected status 200 for device deletion, got {response.status_code}. "
|
||||
f"Response body: {response.get_data(as_text=True)}"
|
||||
)
|
||||
return response
|
||||
|
||||
|
||||
# --- Device Search Tests ---
|
||||
|
||||
|
||||
@@ -350,25 +376,22 @@ def test_mcp_devices_import_json(mock_db_conn, client, api_token):
|
||||
# --- MCP Device Totals Tests ---
|
||||
|
||||
|
||||
@patch("database.get_temp_db_connection")
|
||||
def test_mcp_devices_totals(mock_db_conn, client, api_token):
|
||||
def test_mcp_devices_totals(client, api_token):
|
||||
"""Test MCP devices totals endpoint."""
|
||||
mock_conn = MagicMock()
|
||||
mock_sql = MagicMock()
|
||||
mock_execute_result = MagicMock()
|
||||
# Mock the getTotals method to return sample data
|
||||
mock_execute_result.fetchone.return_value = [10, 8, 2, 0, 1, 3] # devices, connected, favorites, new, down, archived
|
||||
mock_sql.execute.return_value = mock_execute_result
|
||||
mock_conn.cursor.return_value = mock_sql
|
||||
mock_db_conn.return_value = mock_conn
|
||||
test_mac = "aa:bb:cc:" + ":".join(f"{random.randint(0, 255):02X}" for _ in range(3)).lower()
|
||||
create_dummy(client, api_token, test_mac)
|
||||
|
||||
response = client.get("/devices/totals", headers=auth_headers(api_token))
|
||||
try:
|
||||
response = client.get("/devices/totals", headers=auth_headers(api_token))
|
||||
|
||||
assert response.status_code == 200
|
||||
data = response.get_json()
|
||||
# Should return device counts as array
|
||||
assert isinstance(data, list)
|
||||
assert len(data) >= 4 # At least online, offline, etc.
|
||||
assert response.status_code == 200
|
||||
data = response.get_json()
|
||||
# Should return device counts as array
|
||||
assert isinstance(data, list)
|
||||
assert len(data) >= 4 # At least online, offline, etc.
|
||||
assert data[0] >= 1
|
||||
finally:
|
||||
delete_dummy(client, api_token, test_mac)
|
||||
|
||||
|
||||
# --- MCP Traceroute Tests ---
|
||||
|
||||
@@ -5,7 +5,7 @@ Import from any test subdirectory with:
|
||||
|
||||
import sys, os
|
||||
sys.path.insert(0, os.path.join(os.path.dirname(__file__), ".."))
|
||||
from db_test_helpers import make_db, insert_device, minutes_ago, DummyDB, down_event_macs
|
||||
from db_test_helpers import make_db, insert_device, minutes_ago, DummyDB, down_event_macs, make_device_dict, sync_insert_devices
|
||||
"""
|
||||
|
||||
import sqlite3
|
||||
@@ -202,6 +202,125 @@ def insert_device(
|
||||
)
|
||||
|
||||
|
||||
def make_device_dict(mac: str = "aa:bb:cc:dd:ee:ff", **overrides) -> dict:
|
||||
"""
|
||||
Return a fully-populated Devices row dict with safe defaults.
|
||||
|
||||
Mirrors every column in CREATE_DEVICES so callers can be inserted
|
||||
directly via sync_insert_devices() or similar helpers. Pass keyword
|
||||
arguments to override any individual field.
|
||||
|
||||
Computed/view-only columns (devStatus, devIsSleeping, devFlapping,
|
||||
rowid, …) are intentionally absent — tests that need to verify they are
|
||||
dropped should add them after calling this function.
|
||||
"""
|
||||
base = {
|
||||
"devMac": mac,
|
||||
"devName": "Test Device",
|
||||
"devOwner": "",
|
||||
"devType": "",
|
||||
"devVendor": "Acme",
|
||||
"devFavorite": 0,
|
||||
"devGroup": "",
|
||||
"devComments": "",
|
||||
"devFirstConnection": "2024-01-01 00:00:00",
|
||||
"devLastConnection": "2024-01-02 00:00:00",
|
||||
"devLastIP": "192.168.1.10",
|
||||
"devPrimaryIPv4": "192.168.1.10",
|
||||
"devPrimaryIPv6": "",
|
||||
"devVlan": "",
|
||||
"devForceStatus": "",
|
||||
"devStaticIP": "",
|
||||
"devScan": 1,
|
||||
"devLogEvents": 1,
|
||||
"devAlertEvents": 1,
|
||||
"devAlertDown": 1,
|
||||
"devCanSleep": 0,
|
||||
"devSkipRepeated": 0,
|
||||
"devLastNotification": "",
|
||||
"devPresentLastScan": 1,
|
||||
"devIsNew": 0,
|
||||
"devLocation": "",
|
||||
"devIsArchived": 0,
|
||||
"devParentMAC": "",
|
||||
"devParentPort": "",
|
||||
"devIcon": "",
|
||||
"devGUID": "test-guid-1",
|
||||
"devSite": "",
|
||||
"devSSID": "",
|
||||
"devSyncHubNode": "node1",
|
||||
"devSourcePlugin": "",
|
||||
"devCustomProps": "",
|
||||
"devFQDN": "",
|
||||
"devParentRelType": "",
|
||||
"devReqNicsOnline": 0,
|
||||
"devMacSource": "",
|
||||
"devNameSource": "",
|
||||
"devFQDNSource": "",
|
||||
"devLastIPSource": "",
|
||||
"devVendorSource": "",
|
||||
"devSSIDSource": "",
|
||||
"devParentMACSource": "",
|
||||
"devParentPortSource": "",
|
||||
"devParentRelTypeSource": "",
|
||||
"devVlanSource": "",
|
||||
}
|
||||
base.update(overrides)
|
||||
return base
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Sync insert helper (shared by test/plugins/test_sync_insert.py and
|
||||
# test/plugins/test_sync_protocol.py — mirrors sync.py's insert block)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def sync_insert_devices(
|
||||
conn: sqlite3.Connection,
|
||||
device_data: list,
|
||||
existing_macs: set | None = None,
|
||||
) -> int:
|
||||
"""
|
||||
Schema-aware device INSERT mirroring sync.py's Mode-3 insert block.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
conn:
|
||||
In-memory (or real) SQLite connection with a Devices table.
|
||||
device_data:
|
||||
List of device dicts as received from table_devices.json or a node log.
|
||||
existing_macs:
|
||||
Set of MAC addresses already present in Devices. Rows whose devMac is
|
||||
in this set are skipped. Pass ``None`` (default) to insert everything.
|
||||
|
||||
Returns the number of rows actually inserted.
|
||||
"""
|
||||
if not device_data:
|
||||
return 0
|
||||
|
||||
cursor = conn.cursor()
|
||||
|
||||
candidates = (
|
||||
[d for d in device_data if d["devMac"] not in existing_macs]
|
||||
if existing_macs is not None
|
||||
else list(device_data)
|
||||
)
|
||||
|
||||
if not candidates:
|
||||
return 0
|
||||
|
||||
cursor.execute("PRAGMA table_info(Devices)")
|
||||
db_columns = {row[1] for row in cursor.fetchall()}
|
||||
|
||||
insert_cols = [k for k in candidates[0].keys() if k in db_columns]
|
||||
columns = ", ".join(insert_cols)
|
||||
placeholders = ", ".join("?" for _ in insert_cols)
|
||||
sql = f"INSERT INTO Devices ({columns}) VALUES ({placeholders})"
|
||||
values = [tuple(d.get(col) for col in insert_cols) for d in candidates]
|
||||
cursor.executemany(sql, values)
|
||||
conn.commit()
|
||||
return len(values)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Assertion helpers
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
@@ -317,14 +317,18 @@ def _select_custom_ports(exclude: set[int] | None = None) -> int:
|
||||
raise RuntimeError("Unable to locate a free high port for compose testing")
|
||||
|
||||
|
||||
def _make_port_check_hook(ports: tuple[int, ...]) -> Callable[[], None]:
|
||||
def _make_port_check_hook(
|
||||
ports: tuple[int, ...],
|
||||
settle_wait_seconds: int = COMPOSE_SETTLE_WAIT_SECONDS,
|
||||
port_wait_timeout: int = COMPOSE_PORT_WAIT_TIMEOUT,
|
||||
) -> Callable[[], None]:
|
||||
"""Return a callback that waits for the provided ports to accept TCP connections."""
|
||||
|
||||
def _hook() -> None:
|
||||
for port in ports:
|
||||
LAST_PORT_SUCCESSES.pop(port, None)
|
||||
time.sleep(COMPOSE_SETTLE_WAIT_SECONDS)
|
||||
_wait_for_ports(ports, timeout=COMPOSE_PORT_WAIT_TIMEOUT)
|
||||
time.sleep(settle_wait_seconds)
|
||||
_wait_for_ports(ports, timeout=port_wait_timeout)
|
||||
|
||||
return _hook
|
||||
|
||||
@@ -344,6 +348,7 @@ def _write_normal_startup_compose(
|
||||
|
||||
service_env = service.setdefault("environment", {})
|
||||
service_env.setdefault("NETALERTX_CHECK_ONLY", "1")
|
||||
service_env.setdefault("SKIP_STARTUP_CHECKS", "host optimization")
|
||||
|
||||
if env_overrides:
|
||||
service_env.update(env_overrides)
|
||||
@@ -852,12 +857,18 @@ def test_normal_startup_no_warnings_compose(tmp_path: pathlib.Path) -> None:
|
||||
default_project = "netalertx-normal-default"
|
||||
|
||||
default_compose_file = _write_normal_startup_compose(default_dir, default_project, default_env_overrides)
|
||||
port_check_timeout = 20
|
||||
settle_wait_seconds = 2
|
||||
default_result = _run_docker_compose(
|
||||
default_compose_file,
|
||||
default_project,
|
||||
timeout=8,
|
||||
detached=True,
|
||||
post_up=_make_port_check_hook(default_ports),
|
||||
post_up=_make_port_check_hook(
|
||||
default_ports,
|
||||
settle_wait_seconds=settle_wait_seconds,
|
||||
port_wait_timeout=port_check_timeout,
|
||||
),
|
||||
)
|
||||
# MANDATORY LOGGING - DO NOT REMOVE (see file header for reasoning)
|
||||
print("\n[compose output default]", default_result.output)
|
||||
@@ -885,9 +896,14 @@ def test_normal_startup_no_warnings_compose(tmp_path: pathlib.Path) -> None:
|
||||
f"Unexpected mount row values for /data: {data_parts[2:4]}"
|
||||
)
|
||||
|
||||
allowed_warning = "⚠️ WARNING: ARP flux sysctls are not set."
|
||||
|
||||
assert "Write permission denied" not in default_output
|
||||
assert "CRITICAL" not in default_output
|
||||
assert "⚠️" not in default_output
|
||||
assert all(
|
||||
"⚠️" not in line or allowed_warning in line
|
||||
for line in default_output.splitlines()
|
||||
), "Unexpected warning found in default output"
|
||||
|
||||
custom_http = _select_custom_ports({default_http_port})
|
||||
custom_graphql = _select_custom_ports({default_http_port, custom_http})
|
||||
@@ -913,7 +929,11 @@ def test_normal_startup_no_warnings_compose(tmp_path: pathlib.Path) -> None:
|
||||
custom_project,
|
||||
timeout=8,
|
||||
detached=True,
|
||||
post_up=_make_port_check_hook(custom_ports),
|
||||
post_up=_make_port_check_hook(
|
||||
custom_ports,
|
||||
settle_wait_seconds=settle_wait_seconds,
|
||||
port_wait_timeout=port_check_timeout,
|
||||
),
|
||||
)
|
||||
print("\n[compose output custom]", custom_result.output)
|
||||
custom_output = _assert_ports_ready(custom_result, custom_project, custom_ports)
|
||||
@@ -922,8 +942,16 @@ def test_normal_startup_no_warnings_compose(tmp_path: pathlib.Path) -> None:
|
||||
assert "❌" not in custom_output
|
||||
assert "Write permission denied" not in custom_output
|
||||
assert "CRITICAL" not in custom_output
|
||||
assert "⚠️" not in custom_output
|
||||
lowered_custom = custom_output.lower()
|
||||
assert all(
|
||||
"⚠️" not in line or allowed_warning in line
|
||||
for line in custom_output.splitlines()
|
||||
), "Unexpected warning found in custom output"
|
||||
custom_output_without_allowed_warning = "\n".join(
|
||||
line
|
||||
for line in custom_output.splitlines()
|
||||
if allowed_warning.lower() not in line.lower()
|
||||
)
|
||||
lowered_custom = custom_output_without_allowed_warning.lower()
|
||||
assert "arning" not in lowered_custom
|
||||
assert "rror" not in lowered_custom
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@ such as environment variable settings and check skipping.
|
||||
import subprocess
|
||||
import uuid
|
||||
import pytest
|
||||
import shutil
|
||||
|
||||
IMAGE = "netalertx-test"
|
||||
|
||||
@@ -85,8 +86,49 @@ def test_no_app_conf_override_when_no_graphql_port():
|
||||
|
||||
def test_skip_startup_checks_env_var():
|
||||
# If SKIP_STARTUP_CHECKS contains the human-readable name of a check (e.g. "mandatory folders"),
|
||||
# the entrypoint should skip that specific check. We check that the "Creating NetAlertX log directory."
|
||||
# the entrypoint should skip that specific check. We check that the "Creating NetAlertX log directory."
|
||||
# message (from the mandatory folders check) is not printed when skipped.
|
||||
result = _run_entrypoint(env={"SKIP_STARTUP_CHECKS": "mandatory folders"}, check_only=True)
|
||||
assert "Creating NetAlertX log directory" not in result.stdout
|
||||
assert result.returncode == 0
|
||||
|
||||
|
||||
@pytest.mark.docker
|
||||
@pytest.mark.feature_complete
|
||||
def test_host_optimization_warning_matches_sysctl():
|
||||
"""Validate host-optimization warning matches actual host sysctl values."""
|
||||
sysctl_bin = shutil.which("sysctl")
|
||||
if not sysctl_bin:
|
||||
pytest.skip("sysctl binary not found on host; skipping host-optimization warning check")
|
||||
|
||||
ignore_proc = subprocess.run(
|
||||
[sysctl_bin, "-n", "net.ipv4.conf.all.arp_ignore"],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
check=False,
|
||||
timeout=10,
|
||||
)
|
||||
announce_proc = subprocess.run(
|
||||
[sysctl_bin, "-n", "net.ipv4.conf.all.arp_announce"],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
check=False,
|
||||
timeout=10,
|
||||
)
|
||||
|
||||
if ignore_proc.returncode != 0 or announce_proc.returncode != 0:
|
||||
pytest.skip("sysctl values unavailable on host; skipping host-optimization warning check")
|
||||
|
||||
arp_ignore = ignore_proc.stdout.strip()
|
||||
arp_announce = announce_proc.stdout.strip()
|
||||
expected_warning = not (arp_ignore == "1" and arp_announce == "2")
|
||||
|
||||
result = _run_entrypoint(check_only=True)
|
||||
combined_output = result.stdout + result.stderr
|
||||
warning_present = "WARNING: ARP flux sysctls are not set." in combined_output
|
||||
|
||||
assert warning_present == expected_warning, (
|
||||
"host-optimization warning mismatch: "
|
||||
f"arp_ignore={arp_ignore}, arp_announce={arp_announce}, "
|
||||
f"expected_warning={expected_warning}, warning_present={warning_present}"
|
||||
)
|
||||
|
||||
0
test/plugins/__init__.py
Normal file
0
test/plugins/__init__.py
Normal file
130
test/plugins/test_sync_insert.py
Normal file
130
test/plugins/test_sync_insert.py
Normal file
@@ -0,0 +1,130 @@
|
||||
"""
|
||||
Tests for the SYNC plugin's schema-aware device insert logic.
|
||||
|
||||
The core invariant: only columns that actually exist in the Devices table
|
||||
are included in the INSERT statement. Computed/virtual fields (devStatus,
|
||||
devIsSleeping, devFlapping) and unknown future columns must be silently
|
||||
dropped — never cause an OperationalError.
|
||||
"""
|
||||
|
||||
import sys
|
||||
import os
|
||||
|
||||
import pytest
|
||||
|
||||
# Ensure shared helpers and server code are importable.
|
||||
sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", ".."))
|
||||
sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "..", "server"))
|
||||
|
||||
from db_test_helpers import make_db, make_device_dict, sync_insert_devices # noqa: E402
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Fixtures
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
@pytest.fixture
|
||||
def conn():
|
||||
"""Fresh in-memory DB with the Devices table and all views."""
|
||||
return make_db()
|
||||
|
||||
|
||||
class TestSyncInsertSchemaAware:
|
||||
|
||||
def test_clean_device_inserts_successfully(self, conn):
|
||||
"""Happy path: a well-formed device dict inserts without error."""
|
||||
device = make_device_dict()
|
||||
inserted = sync_insert_devices(conn, [device])
|
||||
assert inserted == 1
|
||||
|
||||
cur = conn.cursor()
|
||||
cur.execute("SELECT devMac FROM Devices WHERE devMac = ?", (device["devMac"],))
|
||||
row = cur.fetchone()
|
||||
assert row is not None
|
||||
|
||||
def test_computed_devStatus_is_silently_dropped(self, conn):
|
||||
"""devStatus is a computed view column — must NOT raise OperationalError."""
|
||||
device = make_device_dict()
|
||||
device["devStatus"] = "Online" # computed in DevicesView, not in Devices table
|
||||
|
||||
# Pre-fix this would raise: sqlite3.OperationalError: table Devices has no column named devStatus
|
||||
inserted = sync_insert_devices(conn, [device])
|
||||
assert inserted == 1
|
||||
|
||||
def test_computed_devIsSleeping_is_silently_dropped(self, conn):
|
||||
"""devIsSleeping is a CTE/view column — must NOT raise OperationalError."""
|
||||
device = make_device_dict()
|
||||
device["devIsSleeping"] = 0 # the exact field that triggered the original bug report
|
||||
|
||||
inserted = sync_insert_devices(conn, [device])
|
||||
assert inserted == 1
|
||||
|
||||
def test_computed_devFlapping_is_silently_dropped(self, conn):
|
||||
"""devFlapping is also computed in the view."""
|
||||
device = make_device_dict()
|
||||
device["devFlapping"] = 0
|
||||
|
||||
inserted = sync_insert_devices(conn, [device])
|
||||
assert inserted == 1
|
||||
|
||||
def test_rowid_is_silently_dropped(self, conn):
|
||||
"""rowid must never appear in an INSERT column list."""
|
||||
device = make_device_dict()
|
||||
device["rowid"] = 42
|
||||
|
||||
inserted = sync_insert_devices(conn, [device])
|
||||
assert inserted == 1
|
||||
|
||||
def test_all_computed_fields_at_once(self, conn):
|
||||
"""All known computed/virtual columns together — none should abort the insert."""
|
||||
device = make_device_dict()
|
||||
device["rowid"] = 99
|
||||
device["devStatus"] = "Online"
|
||||
device["devIsSleeping"] = 0
|
||||
device["devFlapping"] = 0
|
||||
device["totally_unknown_future_column"] = "ignored"
|
||||
|
||||
inserted = sync_insert_devices(conn, [device])
|
||||
assert inserted == 1
|
||||
|
||||
def test_batch_insert_multiple_devices(self, conn):
|
||||
"""Multiple devices with computed fields all insert correctly."""
|
||||
devices = []
|
||||
for i in range(3):
|
||||
d = make_device_dict(mac=f"aa:bb:cc:dd:ee:{i:02x}")
|
||||
d["devGUID"] = f"guid-{i}"
|
||||
d["devStatus"] = "Online" # computed
|
||||
d["devIsSleeping"] = 0 # computed
|
||||
devices.append(d)
|
||||
|
||||
inserted = sync_insert_devices(conn, devices)
|
||||
assert inserted == len(devices)
|
||||
|
||||
def test_values_aligned_with_columns_after_filtering(self, conn):
|
||||
"""Values must be extracted in the same order as insert_cols (alignment bug guard)."""
|
||||
device = make_device_dict()
|
||||
device["devStatus"] = "SHOULD_BE_DROPPED"
|
||||
device["devIsSleeping"] = 999
|
||||
|
||||
sync_insert_devices(conn, [device])
|
||||
|
||||
cur = conn.cursor()
|
||||
cur.execute("SELECT devName, devVendor, devLastIP FROM Devices WHERE devMac = ?", (device["devMac"],))
|
||||
row = cur.fetchone()
|
||||
assert row["devName"] == "Test Device"
|
||||
assert row["devVendor"] == "Acme"
|
||||
assert row["devLastIP"] == "192.168.1.10"
|
||||
|
||||
def test_unknown_column_does_not_prevent_insert(self, conn):
|
||||
"""A column that was added on the node but doesn't exist on the hub is dropped."""
|
||||
device = make_device_dict()
|
||||
device["devNewFeatureOnlyOnNode"] = "some_value"
|
||||
|
||||
# Must not raise — hub schema wins
|
||||
inserted = sync_insert_devices(conn, [device])
|
||||
assert inserted == 1
|
||||
|
||||
def test_empty_device_list_returns_zero(self, conn):
|
||||
"""Edge case: empty list should not raise and should return 0."""
|
||||
inserted = sync_insert_devices(conn, [])
|
||||
assert inserted == 0
|
||||
413
test/plugins/test_sync_protocol.py
Normal file
413
test/plugins/test_sync_protocol.py
Normal file
@@ -0,0 +1,413 @@
|
||||
"""
|
||||
Tests for SYNC plugin push/pull/receive behaviour.
|
||||
|
||||
Three modes exercised:
|
||||
Mode 1 – PUSH (NODE): send_data() POSTs encrypted device data to the hub.
|
||||
Mode 2 – PULL (HUB): get_data() GETs a base64 JSON blob from each node.
|
||||
Mode 3 – RECEIVE: hub parses decoded log files and upserts devices into DB.
|
||||
|
||||
sync.py is intentionally NOT imported here — its module-level code has side
|
||||
effects (reads live config, initialises logging). Instead, the pure logic
|
||||
under test is extracted into thin local mirrors that match the production
|
||||
implementation exactly, so any divergence will surface as a test failure.
|
||||
"""
|
||||
|
||||
import base64
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
import pytest
|
||||
import requests
|
||||
|
||||
# Make shared helpers + server packages importable from test/plugins/
|
||||
sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", ".."))
|
||||
sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "..", "server"))
|
||||
|
||||
from db_test_helpers import make_db, make_device_dict, sync_insert_devices # noqa: E402
|
||||
from utils.crypto_utils import encrypt_data, decrypt_data # noqa: E402
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Local mirrors of sync.py logic (no module-level side-effects on import)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
API_ENDPOINT = "/sync"
|
||||
|
||||
|
||||
def _send_data(api_token, file_content, encryption_key, file_path, node_name, pref, hub_url):
|
||||
"""Mirror of sync.send_data() — returns True on HTTP 200, False otherwise."""
|
||||
encrypted_data = encrypt_data(file_content, encryption_key)
|
||||
data = {
|
||||
"data": encrypted_data,
|
||||
"file_path": file_path,
|
||||
"plugin": pref,
|
||||
"node_name": node_name,
|
||||
}
|
||||
headers = {"Authorization": f"Bearer {api_token}"}
|
||||
try:
|
||||
response = requests.post(hub_url + API_ENDPOINT, data=data, headers=headers, timeout=5)
|
||||
return response.status_code == 200
|
||||
except requests.RequestException:
|
||||
return False
|
||||
|
||||
|
||||
def _get_data(api_token, node_url):
|
||||
"""Mirror of sync.get_data() — returns parsed JSON dict or '' on any failure."""
|
||||
headers = {"Authorization": f"Bearer {api_token}"}
|
||||
try:
|
||||
response = requests.get(node_url + API_ENDPOINT, headers=headers, timeout=5)
|
||||
if response.status_code == 200:
|
||||
try:
|
||||
return response.json()
|
||||
except json.JSONDecodeError:
|
||||
pass
|
||||
except requests.RequestException:
|
||||
pass
|
||||
return ""
|
||||
|
||||
|
||||
def _node_name_from_filename(file_name: str) -> str:
|
||||
"""Mirror of the node-name extraction in sync.main()."""
|
||||
parts = file_name.split(".")
|
||||
return parts[2] if ("decoded" in file_name or "encoded" in file_name) else parts[1]
|
||||
|
||||
|
||||
def _determine_mode(hub_url: str, send_devices: bool, plugins_to_sync: list, pull_nodes: list):
|
||||
"""Mirror of the is_hub / is_node detection block in sync.main()."""
|
||||
is_node = len(hub_url) > 0 and (send_devices or bool(plugins_to_sync))
|
||||
is_hub = len(pull_nodes) > 0
|
||||
return is_hub, is_node
|
||||
|
||||
|
||||
def _currentscan_candidates(device_data: list[dict]) -> list[dict]:
|
||||
"""
|
||||
Mirror of the plugin_objects.add_object() filter in sync.main().
|
||||
|
||||
Only online (devPresentLastScan=1) and non-internet devices are eligible
|
||||
to be written to the CurrentScan / plugin result file.
|
||||
"""
|
||||
return [
|
||||
d for d in device_data
|
||||
if d.get("devPresentLastScan") == 1 and str(d.get("devMac", "")).lower() != "internet"
|
||||
]
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Fixtures
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
ENCRYPTION_KEY = "test-secret-key"
|
||||
API_TOKEN = "tok_abc123"
|
||||
HUB_URL = "http://hub.local:20211"
|
||||
NODE_URL = "http://node.local:20211"
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def conn():
|
||||
"""Fresh in-memory DB with Devices table and all views."""
|
||||
return make_db()
|
||||
|
||||
|
||||
# ===========================================================================
|
||||
# Mode detection
|
||||
# ===========================================================================
|
||||
|
||||
class TestModeDetection:
|
||||
|
||||
def test_is_node_when_hub_url_and_send_devices(self):
|
||||
is_hub, is_node = _determine_mode(HUB_URL, send_devices=True, plugins_to_sync=[], pull_nodes=[])
|
||||
assert is_node is True
|
||||
assert is_hub is False
|
||||
|
||||
def test_is_node_when_hub_url_and_plugins_set(self):
|
||||
is_hub, is_node = _determine_mode(HUB_URL, send_devices=False, plugins_to_sync=["NMAP"], pull_nodes=[])
|
||||
assert is_node is True
|
||||
assert is_hub is False
|
||||
|
||||
def test_is_hub_when_pull_nodes_set(self):
|
||||
is_hub, is_node = _determine_mode("", send_devices=False, plugins_to_sync=[], pull_nodes=[NODE_URL])
|
||||
assert is_hub is True
|
||||
assert is_node is False
|
||||
|
||||
def test_is_both_hub_and_node(self):
|
||||
is_hub, is_node = _determine_mode(HUB_URL, send_devices=True, plugins_to_sync=[], pull_nodes=[NODE_URL])
|
||||
assert is_hub is True
|
||||
assert is_node is True
|
||||
|
||||
def test_neither_when_no_config(self):
|
||||
is_hub, is_node = _determine_mode("", send_devices=False, plugins_to_sync=[], pull_nodes=[])
|
||||
assert is_hub is False
|
||||
assert is_node is False
|
||||
|
||||
def test_no_hub_url_means_not_node_even_with_send_devices(self):
|
||||
is_hub, is_node = _determine_mode("", send_devices=True, plugins_to_sync=[], pull_nodes=[])
|
||||
assert is_node is False
|
||||
|
||||
|
||||
# ===========================================================================
|
||||
# send_data (Mode 1 – PUSH)
|
||||
# ===========================================================================
|
||||
|
||||
class TestSendData:
|
||||
|
||||
def _mock_post(self, status_code=200):
|
||||
resp = MagicMock()
|
||||
resp.status_code = status_code
|
||||
return patch("requests.post", return_value=resp)
|
||||
|
||||
def test_returns_true_on_http_200(self):
|
||||
with self._mock_post(200):
|
||||
result = _send_data(API_TOKEN, '{"data":[]}', ENCRYPTION_KEY,
|
||||
"/tmp/file.log", "node1", "SYNC", HUB_URL)
|
||||
assert result is True
|
||||
|
||||
def test_returns_false_on_non_200(self):
|
||||
for code in (400, 401, 403, 500, 503):
|
||||
with self._mock_post(code):
|
||||
result = _send_data(API_TOKEN, '{"data":[]}', ENCRYPTION_KEY,
|
||||
"/tmp/file.log", "node1", "SYNC", HUB_URL)
|
||||
assert result is False, f"Expected False for HTTP {code}"
|
||||
|
||||
def test_returns_false_on_connection_error(self):
|
||||
with patch("requests.post", side_effect=requests.ConnectionError("refused")):
|
||||
result = _send_data(API_TOKEN, '{"data":[]}', ENCRYPTION_KEY,
|
||||
"/tmp/file.log", "node1", "SYNC", HUB_URL)
|
||||
assert result is False
|
||||
|
||||
def test_returns_false_on_timeout(self):
|
||||
with patch("requests.post", side_effect=requests.Timeout("timed out")):
|
||||
result = _send_data(API_TOKEN, '{"data":[]}', ENCRYPTION_KEY,
|
||||
"/tmp/file.log", "node1", "SYNC", HUB_URL)
|
||||
assert result is False
|
||||
|
||||
def test_posts_to_correct_endpoint(self):
|
||||
resp = MagicMock()
|
||||
resp.status_code = 200
|
||||
with patch("requests.post", return_value=resp) as mock_post:
|
||||
_send_data(API_TOKEN, '{"data":[]}', ENCRYPTION_KEY,
|
||||
"/tmp/file.log", "node1", "SYNC", HUB_URL)
|
||||
url_called = mock_post.call_args[0][0]
|
||||
assert url_called == HUB_URL + "/sync"
|
||||
|
||||
def test_bearer_auth_header_sent(self):
|
||||
resp = MagicMock()
|
||||
resp.status_code = 200
|
||||
with patch("requests.post", return_value=resp) as mock_post:
|
||||
_send_data(API_TOKEN, '{"data":[]}', ENCRYPTION_KEY,
|
||||
"/tmp/file.log", "node1", "SYNC", HUB_URL)
|
||||
headers = mock_post.call_args[1]["headers"]
|
||||
assert headers["Authorization"] == f"Bearer {API_TOKEN}"
|
||||
|
||||
def test_payload_contains_expected_fields(self):
|
||||
resp = MagicMock()
|
||||
resp.status_code = 200
|
||||
with patch("requests.post", return_value=resp) as mock_post:
|
||||
_send_data(API_TOKEN, '{"data":[]}', ENCRYPTION_KEY,
|
||||
"/tmp/file.log", "node1", "SYNC", HUB_URL)
|
||||
payload = mock_post.call_args[1]["data"]
|
||||
assert "data" in payload # encrypted blob
|
||||
assert payload["file_path"] == "/tmp/file.log"
|
||||
assert payload["plugin"] == "SYNC"
|
||||
assert payload["node_name"] == "node1"
|
||||
|
||||
def test_payload_data_is_encrypted_not_plaintext(self):
|
||||
"""The 'data' field in the POST must be encrypted, not the raw content."""
|
||||
plaintext = '{"secret": "do_not_expose"}'
|
||||
resp = MagicMock()
|
||||
resp.status_code = 200
|
||||
with patch("requests.post", return_value=resp) as mock_post:
|
||||
_send_data(API_TOKEN, plaintext, ENCRYPTION_KEY,
|
||||
"/tmp/file.log", "node1", "SYNC", HUB_URL)
|
||||
transmitted = mock_post.call_args[1]["data"]["data"]
|
||||
assert transmitted != plaintext
|
||||
# Verify it round-trips correctly
|
||||
assert decrypt_data(transmitted, ENCRYPTION_KEY) == plaintext
|
||||
|
||||
|
||||
# ===========================================================================
|
||||
# get_data (Mode 2 – PULL)
|
||||
# ===========================================================================
|
||||
|
||||
class TestGetData:
|
||||
|
||||
def _mock_get(self, status_code=200, json_body=None, side_effect=None):
|
||||
resp = MagicMock()
|
||||
resp.status_code = status_code
|
||||
if json_body is not None:
|
||||
resp.json.return_value = json_body
|
||||
if side_effect is not None:
|
||||
return patch("requests.get", side_effect=side_effect)
|
||||
return patch("requests.get", return_value=resp)
|
||||
|
||||
def test_returns_parsed_json_on_200(self):
|
||||
body = {"node_name": "node1", "data_base64": base64.b64encode(b"hello").decode()}
|
||||
with self._mock_get(200, json_body=body):
|
||||
result = _get_data(API_TOKEN, NODE_URL)
|
||||
assert result == body
|
||||
|
||||
def test_gets_from_correct_endpoint(self):
|
||||
resp = MagicMock()
|
||||
resp.status_code = 200
|
||||
resp.json.return_value = {}
|
||||
with patch("requests.get", return_value=resp) as mock_get:
|
||||
_get_data(API_TOKEN, NODE_URL)
|
||||
url_called = mock_get.call_args[0][0]
|
||||
assert url_called == NODE_URL + "/sync"
|
||||
|
||||
def test_bearer_auth_header_sent(self):
|
||||
resp = MagicMock()
|
||||
resp.status_code = 200
|
||||
resp.json.return_value = {}
|
||||
with patch("requests.get", return_value=resp) as mock_get:
|
||||
_get_data(API_TOKEN, NODE_URL)
|
||||
headers = mock_get.call_args[1]["headers"]
|
||||
assert headers["Authorization"] == f"Bearer {API_TOKEN}"
|
||||
|
||||
def test_returns_empty_string_on_json_decode_error(self):
|
||||
resp = MagicMock()
|
||||
resp.status_code = 200
|
||||
resp.json.side_effect = json.JSONDecodeError("bad json", "", 0)
|
||||
with patch("requests.get", return_value=resp):
|
||||
result = _get_data(API_TOKEN, NODE_URL)
|
||||
assert result == ""
|
||||
|
||||
def test_returns_empty_string_on_connection_error(self):
|
||||
with patch("requests.get", side_effect=requests.ConnectionError("refused")):
|
||||
result = _get_data(API_TOKEN, NODE_URL)
|
||||
assert result == ""
|
||||
|
||||
def test_returns_empty_string_on_timeout(self):
|
||||
with patch("requests.get", side_effect=requests.Timeout("timed out")):
|
||||
result = _get_data(API_TOKEN, NODE_URL)
|
||||
assert result == ""
|
||||
|
||||
def test_returns_empty_string_on_non_200(self):
|
||||
resp = MagicMock()
|
||||
resp.status_code = 401
|
||||
with patch("requests.get", return_value=resp):
|
||||
result = _get_data(API_TOKEN, NODE_URL)
|
||||
assert result == ""
|
||||
|
||||
|
||||
# ===========================================================================
|
||||
# Node name extraction from filename (Mode 3 – RECEIVE)
|
||||
# ===========================================================================
|
||||
|
||||
class TestNodeNameExtraction:
|
||||
|
||||
def test_simple_filename(self):
|
||||
# last_result.MyNode.log → "MyNode"
|
||||
assert _node_name_from_filename("last_result.MyNode.log") == "MyNode"
|
||||
|
||||
def test_decoded_filename(self):
|
||||
# last_result.decoded.MyNode.1.log → "MyNode"
|
||||
assert _node_name_from_filename("last_result.decoded.MyNode.1.log") == "MyNode"
|
||||
|
||||
def test_encoded_filename(self):
|
||||
# last_result.encoded.MyNode.1.log → "MyNode"
|
||||
assert _node_name_from_filename("last_result.encoded.MyNode.1.log") == "MyNode"
|
||||
|
||||
def test_node_name_with_underscores(self):
|
||||
assert _node_name_from_filename("last_result.Wladek_Site.log") == "Wladek_Site"
|
||||
|
||||
def test_decoded_node_name_with_underscores(self):
|
||||
assert _node_name_from_filename("last_result.decoded.Wladek_Site.1.log") == "Wladek_Site"
|
||||
|
||||
|
||||
# ===========================================================================
|
||||
# CurrentScan candidates filter (Mode 3 – RECEIVE)
|
||||
# ===========================================================================
|
||||
|
||||
class TestCurrentScanCandidates:
|
||||
|
||||
def test_online_device_is_included(self):
|
||||
d = make_device_dict(devPresentLastScan=1)
|
||||
assert len(_currentscan_candidates([d])) == 1
|
||||
|
||||
def test_offline_device_is_excluded(self):
|
||||
d = make_device_dict(devPresentLastScan=0)
|
||||
assert len(_currentscan_candidates([d])) == 0
|
||||
|
||||
def test_internet_mac_is_excluded(self):
|
||||
d = make_device_dict(mac="internet", devPresentLastScan=1)
|
||||
assert len(_currentscan_candidates([d])) == 0
|
||||
|
||||
def test_internet_mac_case_insensitive(self):
|
||||
for mac in ("INTERNET", "Internet", "iNtErNeT"):
|
||||
d = make_device_dict(mac=mac, devPresentLastScan=1)
|
||||
assert len(_currentscan_candidates([d])) == 0, f"mac={mac!r} should be excluded"
|
||||
|
||||
def test_mixed_batch(self):
|
||||
devices = [
|
||||
make_device_dict(mac="aa:bb:cc:dd:ee:01", devPresentLastScan=1), # included
|
||||
make_device_dict(mac="aa:bb:cc:dd:ee:02", devPresentLastScan=0), # offline
|
||||
make_device_dict(mac="internet", devPresentLastScan=1), # root node
|
||||
make_device_dict(mac="aa:bb:cc:dd:ee:03", devPresentLastScan=1), # included
|
||||
]
|
||||
result = _currentscan_candidates(devices)
|
||||
macs = [d["devMac"] for d in result]
|
||||
assert "aa:bb:cc:dd:ee:01" in macs
|
||||
assert "aa:bb:cc:dd:ee:03" in macs
|
||||
assert "aa:bb:cc:dd:ee:02" not in macs
|
||||
assert "internet" not in macs
|
||||
|
||||
|
||||
# ===========================================================================
|
||||
# DB insert filtering – new vs existing devices (Mode 3 – RECEIVE)
|
||||
# ===========================================================================
|
||||
|
||||
class TestReceiveInsert:
|
||||
|
||||
def test_new_device_is_inserted(self, conn):
|
||||
device = make_device_dict(mac="aa:bb:cc:dd:ee:01")
|
||||
inserted = sync_insert_devices(conn, [device], existing_macs=set())
|
||||
assert inserted == 1
|
||||
cur = conn.cursor()
|
||||
cur.execute("SELECT devMac FROM Devices WHERE devMac = ?", ("aa:bb:cc:dd:ee:01",))
|
||||
assert cur.fetchone() is not None
|
||||
|
||||
def test_existing_device_is_not_reinserted(self, conn):
|
||||
# Pre-populate Devices
|
||||
cur = conn.cursor()
|
||||
cur.execute(
|
||||
"INSERT INTO Devices (devMac, devName) VALUES (?, ?)",
|
||||
("aa:bb:cc:dd:ee:01", "Existing"),
|
||||
)
|
||||
conn.commit()
|
||||
|
||||
device = make_device_dict(mac="aa:bb:cc:dd:ee:01")
|
||||
inserted = sync_insert_devices(conn, [device], existing_macs={"aa:bb:cc:dd:ee:01"})
|
||||
assert inserted == 0
|
||||
|
||||
def test_only_new_devices_inserted_in_mixed_batch(self, conn):
|
||||
cur = conn.cursor()
|
||||
cur.execute(
|
||||
"INSERT INTO Devices (devMac, devName) VALUES (?, ?)",
|
||||
("aa:bb:cc:dd:ee:existing", "Existing"),
|
||||
)
|
||||
conn.commit()
|
||||
|
||||
devices = [
|
||||
make_device_dict(mac="aa:bb:cc:dd:ee:existing"),
|
||||
make_device_dict(mac="aa:bb:cc:dd:ee:new1"),
|
||||
make_device_dict(mac="aa:bb:cc:dd:ee:new2"),
|
||||
]
|
||||
inserted = sync_insert_devices(
|
||||
conn, devices, existing_macs={"aa:bb:cc:dd:ee:existing"}
|
||||
)
|
||||
assert inserted == 2
|
||||
|
||||
def test_computed_fields_in_payload_do_not_abort_insert(self, conn):
|
||||
"""Regression: devIsSleeping / devStatus / devFlapping must be silently dropped."""
|
||||
device = make_device_dict(mac="aa:bb:cc:dd:ee:01")
|
||||
device["devIsSleeping"] = 0
|
||||
device["devStatus"] = "Online"
|
||||
device["devFlapping"] = 0
|
||||
device["rowid"] = 99
|
||||
# Must not raise OperationalError
|
||||
inserted = sync_insert_devices(conn, [device], existing_macs=set())
|
||||
assert inserted == 1
|
||||
|
||||
def test_empty_device_list_returns_zero(self, conn):
|
||||
assert sync_insert_devices(conn, [], existing_macs=set()) == 0
|
||||
Reference in New Issue
Block a user