Compare commits

...

215 Commits

Author SHA1 Message Date
Jokob @NetAlertX
4f2fa86a49 feat(docs): Update coding standards to clarify database storage guidelines
Some checks are pending
🐳 ⚠ docker-unsafe from next_release branch / docker_dev_unsafe (push) Waiting to run
2026-04-03 01:56:42 +00:00
Jokob @NetAlertX
3f80d2e57f feat(plugins): Implement /plugins/stats endpoint for per-plugin row counts with optional foreignKey filtering
Some checks failed
🐳 ⚠ docker-unsafe from next_release branch / docker_dev_unsafe (push) Has been cancelled
2026-03-27 21:35:41 +00:00
Jokob @NetAlertX
b18cf98266 feat(plugins): Enhance plugin counts handling with fail-open support and improved comments 2026-03-27 10:59:22 +00:00
Jokob @NetAlertX
77369c3ce8 feat(plugins): Optimize plugin badge fetching and rendering to prevent flicker and enhance visibility 2026-03-27 10:41:18 +00:00
Jokob @NetAlertX
cd0a3f6de0 feat(plugins): Refactor auto-hide functionality to leverage Bootstrap's tab management for improved visibility handling 2026-03-27 10:12:12 +00:00
Jokob @NetAlertX
13e91731be feat(plugins): Improve auto-hide functionality for empty plugin tabs by ensuring proper visibility handling and Bootstrap integration 2026-03-27 09:49:21 +00:00
Jokob @NetAlertX
7ef19b1c12 feat(plugins): Implement auto-hide functionality for empty plugin tabs 2026-03-27 09:26:25 +00:00
Jokob @NetAlertX
4daead1f8f feat(plugins): Enhance badge fetching with conditional JSON and GraphQL support 2026-03-27 08:08:44 +00:00
Jokob @NetAlertX
48454f6f2f feat(plugins): Optimize badge fetching by using lightweight JSON instead of GraphQL 2026-03-27 07:30:13 +00:00
Jokob @NetAlertX
7305fd78e3 fix(pagination): Ensure page number is always at least 1 in apply_common_pagination 2026-03-27 06:51:17 +00:00
Jokob @NetAlertX
ec3e4c8988 feat(api): Enhance session events API with pagination, sorting, and filtering
- Added support for pagination (page and limit) in the session events endpoint.
- Implemented sorting functionality based on specified columns and directions.
- Introduced free-text search capability for session events.
- Updated SQL queries to retrieve all events and added a new SQL constant for events.
- Refactored GraphQL types and helpers to support new plugin and event queries.
- Created new GraphQL resolvers for plugins and events with pagination and filtering.
- Added comprehensive tests for new GraphQL endpoints and session events functionality.
2026-03-26 20:57:10 +00:00
jokob-sk
250e533655 DOCS: pin mkdocs version
Signed-off-by: jokob-sk <jokob.sk@gmail.com>
2026-03-25 06:26:57 +11:00
jokob-sk
37730301f4 BE: lazy SQL execution caused devIsSleeping to be missing and tiles not show #1569 #1250
Signed-off-by: jokob-sk <jokob.sk@gmail.com>
2026-03-23 09:55:45 +11:00
Jokob @NetAlertX
7278ee8cfa Refactor getTotals method to clarify API contract and ensure stable response structure #1569 #1561 2026-03-21 21:28:42 +00:00
Jokob @NetAlertX
fa22523a0b Refactor device tiles SQL logic to use get_sql_devices_tiles function for improved maintainability Feature Request - Flapping and Sleeping nuances
Fixes #1567
2026-03-21 21:10:37 +00:00
Jokob @NetAlertX
7569923481 Refactor column name replacements to include variations for ObjectPrimaryID and ObjectSecondaryID 2026-03-21 20:55:24 +00:00
Jokob @NetAlertX
d7c7bd2cd2 Enhance SQL templates to prevent duplicate notifications for 'Down Reconnected' devices in event section 2026-03-18 09:57:20 +00:00
Jokob @NetAlertX
b311113575 Fix Spanish translations and improve HTML attributes in config files and report 2026-03-17 11:58:53 +00:00
Jokob @NetAlertX
43984132c4 Fix Spanish translations in config.json files for internet_speedtest, nmap_scan, and snmp_discovery plugins 2026-03-17 09:46:27 +00:00
Jokob @NetAlertX
0a7ecb5b7c Update config.json files to add 'ordeable' option and refactor cacheStrings function for consistency 2026-03-17 09:22:25 +00:00
Jokob @NetAlertX
c7399215ec Refactor event and session column names to camelCase
- Updated test cases to reflect new column names (eve_MAC -> eveMac, eve_DateTime -> eveDateTime, etc.) across various test files.
- Modified SQL table definitions in the database cleanup and migration tests to use camelCase naming conventions.
- Implemented migration tests to ensure legacy column names are correctly renamed to camelCase equivalents.
- Ensured that existing data is preserved during the migration process and that views referencing old column names are dropped before renaming.
- Verified that the migration function is idempotent, allowing for safe re-execution without data loss.
2026-03-16 10:11:22 +00:00
Jokob @NetAlertX
0bb6db155b Merge branch 'next_release' of https://github.com/netalertx/NetAlertX into next_release 2026-03-15 01:42:23 +00:00
Jokob @NetAlertX
7221b4ba96 Keep all local changes while resolving conflicts 2026-03-15 01:19:34 +00:00
Jokob @NetAlertX
c4904739b2 Merge pull request #1559 from netalertx/main
sync
2026-03-15 12:15:19 +11:00
Jokob @NetAlertX
67cab9d606 Merge branch 'main' of https://github.com/netalertx/NetAlertX 2026-03-14 23:27:45 +00:00
Jokob @NetAlertX
f75c53fc5d Implement notification text templates and update related settings for customizable notifications 2026-03-14 23:27:29 +00:00
Jokob @NetAlertX
bff87f4d61 Merge pull request #1552 from MrMeatikins/fix-arp-flux-docs-issue-1546
docs: Clarify ARP flux sysctl limitations with host networking
2026-03-14 09:05:44 +11:00
Jokob @NetAlertX
6f7d2c3253 Rename db_count to dbCount in GraphQL response handling for consistency 2026-03-13 13:17:30 +00:00
Hosted Weblate
0766fb2de6 Merge branch 'origin/main' into Weblate. 2026-03-13 14:08:47 +01:00
大王叫我来巡山
d19cb3d679 Translated using Weblate (Chinese (Simplified Han script))
Currently translated at 97.7% (787 of 805 strings)

Translation: NetAlertX/core
Translate-URL: https://hosted.weblate.org/projects/pialert/core/zh_Hans/
2026-03-13 14:08:45 +01:00
Massimo Pissarello
9b71c210b2 Translated using Weblate (Italian)
Currently translated at 100.0% (805 of 805 strings)

Translation: NetAlertX/core
Translate-URL: https://hosted.weblate.org/projects/pialert/core/it/
2026-03-13 14:08:44 +01:00
Jokob @NetAlertX
c9cb1f3fba Add db_count to DeviceResult and update GraphQL response handling; localize Device_NoMatch_Title in multiple languages 2026-03-13 13:08:26 +00:00
Jokob @NetAlertX
78a8030c6a Merge branch 'main' of https://github.com/netalertx/NetAlertX 2026-03-13 12:52:27 +00:00
Jokob @NetAlertX
b5b0bcc766 work on stale cache #1554 2026-03-13 12:52:22 +00:00
mid
13515603e4 Translated using Weblate (Japanese)
Currently translated at 100.0% (805 of 805 strings)

Translation: NetAlertX/core
Translate-URL: https://hosted.weblate.org/projects/pialert/core/ja/
2026-03-11 12:09:48 +01:00
Sylvain Pichon
518608cffc Translated using Weblate (French)
Currently translated at 99.5% (801 of 805 strings)

Translation: NetAlertX/core
Translate-URL: https://hosted.weblate.org/projects/pialert/core/fr/
2026-03-11 12:09:47 +01:00
Meatloaf Bot
df3ca50c5c Address CodeRabbit review: Clarify sysctl behavior in host network mode 2026-03-10 12:04:30 -04:00
Meatloaf-bot
93fc126da2 docs: clarify ARP flux sysctl limitations with host networking 2026-03-09 19:27:40 -04:00
jokob-sk
a60ec9ed3a Merge branch 'main' of github.com:netalertx/NetAlertX 2026-03-09 21:00:04 +11:00
jokob-sk
e1d206ca74 BE: new_online defined incorrectly
Signed-off-by: jokob-sk <jokob.sk@gmail.com>
2026-03-09 20:59:51 +11:00
Jokob @NetAlertX
2771a6e9c2 Merge branch 'main' of https://github.com/netalertx/NetAlertX 2026-03-08 07:58:00 +00:00
Jokob @NetAlertX
aba1ddd3df Handle JSON decoding errors in _get_data function 2026-03-08 07:57:52 +00:00
Sylvain Pichon
165c9d3baa Translated using Weblate (French)
Currently translated at 99.2% (799 of 805 strings)

Translation: NetAlertX/core
Translate-URL: https://hosted.weblate.org/projects/pialert/core/fr/
2026-03-08 06:09:50 +00:00
Safeguard
0b0c88f712 Translated using Weblate (Russian)
Currently translated at 100.0% (805 of 805 strings)

Translation: NetAlertX/core
Translate-URL: https://hosted.weblate.org/projects/pialert/core/ru/
2026-03-08 06:09:47 +00:00
Jokob @NetAlertX
d49abd9d02 Enhance code standards, update contributing guidelines, and add tests for SYNC plugin functionality 2026-03-07 21:34:38 +00:00
Jokob @NetAlertX
abf024d4d3 Merge branch 'main' of https://github.com/netalertx/NetAlertX 2026-03-06 22:34:25 +00:00
Jokob @NetAlertX
4eb5947ceb Update language folder path to include all language definitions 2026-03-06 22:34:19 +00:00
Safeguard
1d1a8045a0 Translated using Weblate (Russian)
Currently translated at 99.7% (803 of 805 strings)

Translation: NetAlertX/core
Translate-URL: https://hosted.weblate.org/projects/pialert/core/ru/
2026-03-06 13:09:47 +00:00
Jokob @NetAlertX
f8c09d35a7 Enhance scan ETA display logic to reload data for newly discovered devices after scanning finishes 2026-03-05 20:24:57 +00:00
jokob-sk
d8d090404e Merge branch 'main' of github.com:netalertx/NetAlertX 2026-03-05 18:51:05 +11:00
jokob-sk
5a6de6d832 LNG: moved languages.json so weblate skips it
Signed-off-by: jokob-sk <jokob.sk@gmail.com>
2026-03-05 18:50:21 +11:00
Hosted Weblate
05b63cb730 Merge branch 'origin/main' into Weblate. 2026-03-05 08:33:52 +01:00
Safeguard
2921614eac Translated using Weblate (Russian)
Currently translated at 99.0% (797 of 805 strings)

Translation: NetAlertX/core
Translate-URL: https://hosted.weblate.org/projects/pialert/core/ru/
2026-03-05 08:33:51 +01:00
Massimo Pissarello
17d95d802f Translated using Weblate (Italian)
Currently translated at 100.0% (805 of 805 strings)

Translation: NetAlertX/core
Translate-URL: https://hosted.weblate.org/projects/pialert/core/it/
2026-03-05 08:33:50 +01:00
jokob-sk
a0048980b8 LNG: Indonesian
Signed-off-by: jokob-sk <jokob.sk@gmail.com>
2026-03-05 18:33:36 +11:00
Jokob @NetAlertX
89811cd133 Merge pull request #1544 from adamoutler/built-in-tests
Improve built-in test used during system startup - thanks @adamoutler 🙏
2026-03-05 06:48:46 +11:00
Adam Outler
b854206599 Address review comments from PR #1544 2026-03-04 14:36:31 +00:00
Adam Outler
a532c98115 Update test/docker_tests/test_entrypoint.py
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
2026-03-03 23:22:11 -05:00
Adam Outler
da23880eb1 Update docs/docker-troubleshooting/arp-flux-sysctls.md
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
2026-03-03 23:20:40 -05:00
Jokob @NetAlertX
c73ce839f2 Refactor ensure_future_datetime to simplify logic and remove max_retries parameter 2026-03-04 03:07:37 +00:00
Adam Outler
5c0f29b97c coderabbit suggestions 2026-03-04 01:33:43 +00:00
Adam Outler
f1496b483b Fix docker compose unit test to remove ARP FLUX warning. 2026-03-04 01:13:39 +00:00
Jokob @NetAlertX
ba26f34191 Enhance device section UI with collapsible filters and default collapse on mobile 2026-03-03 21:40:19 +00:00
Jokob @NetAlertX
37f8a44cb3 Update devIpLong field to String and handle empty string coercion for Int fields in devices data 2026-03-03 21:25:09 +00:00
Jokob @NetAlertX
76a259d9e5 Fix DataTable redraw logic and update empty message handling 2026-03-03 21:20:22 +00:00
Jokob @NetAlertX
1923a063f0 Merge branch 'main' of https://github.com/netalertx/NetAlertX 2026-03-03 12:33:08 +00:00
Jokob @NetAlertX
01b6b9f04a whitespace 2026-03-03 12:32:07 +00:00
Jokob @NetAlertX
ea77112315 next scan dsiplay DRY 2026-03-03 12:31:50 +00:00
Hosted Weblate
b19973130e Merge branch 'origin/main' into Weblate. 2026-03-03 07:37:53 +01:00
mid
ffbcc2ad25 Translated using Weblate (Japanese)
Currently translated at 99.8% (804 of 805 strings)

Translation: NetAlertX/core
Translate-URL: https://hosted.weblate.org/projects/pialert/core/ja/
2026-03-03 07:37:52 +01:00
Jokob @NetAlertX
c533c2267c Merge branch 'main' of https://github.com/netalertx/NetAlertX 2026-03-03 06:37:44 +00:00
Jokob @NetAlertX
ac407bd86e Update next scan message for clarity in device management 2026-03-03 06:37:37 +00:00
Hosted Weblate
1da3c146d2 Merge branch 'origin/main' into Weblate. 2026-03-03 07:36:42 +01:00
mid
9fe8090a1b Translated using Weblate (Japanese)
Currently translated at 100.0% (805 of 805 strings)

Translation: NetAlertX/core
Translate-URL: https://hosted.weblate.org/projects/pialert/core/ja/
2026-03-03 07:36:42 +01:00
Massimo Pissarello
3ba1b69c1e Translated using Weblate (Italian)
Currently translated at 100.0% (805 of 805 strings)

Translation: NetAlertX/core
Translate-URL: https://hosted.weblate.org/projects/pialert/core/it/
2026-03-03 07:36:42 +01:00
Jokob @NetAlertX
da4d8a9675 Merge branch 'main' of https://github.com/netalertx/NetAlertX 2026-03-03 06:36:36 +00:00
Jokob @NetAlertX
0f20fb38f0 Enhance scanning state handling and localization updates across multiple language files 2026-03-03 06:36:31 +00:00
Hosted Weblate
8361f0ac99 Merge branch 'origin/main' into Weblate. 2026-03-03 07:11:04 +01:00
Massimo Pissarello
99de69e30d Translated using Weblate (Italian)
Currently translated at 98.7% (794 of 804 strings)

Translation: NetAlertX/core
Translate-URL: https://hosted.weblate.org/projects/pialert/core/it/
2026-03-03 07:11:02 +01:00
mid
4637ec6350 Translated using Weblate (Japanese)
Currently translated at 99.8% (803 of 804 strings)

Translation: NetAlertX/core
Translate-URL: https://hosted.weblate.org/projects/pialert/core/ja/
2026-03-03 07:11:00 +01:00
Jokob @NetAlertX
2a4e6ba5e1 Merge branch 'main' of https://github.com/netalertx/NetAlertX 2026-03-03 06:10:41 +00:00
Jokob @NetAlertX
5be7bbe07d Enhance scan ETA display with current scanning state and add localization for 'Scanning...' message 2026-03-03 06:09:56 +00:00
Hosted Weblate
e8c43af7b6 Merge branch 'origin/main' into Weblate. 2026-03-03 05:07:58 +01:00
Adam Stańczyk
27f34963be Translated using Weblate (Polish)
Currently translated at 84.7% (677 of 799 strings)

Translation: NetAlertX/core
Translate-URL: https://hosted.weblate.org/projects/pialert/core/pl/
2026-03-03 05:07:56 +01:00
HAMAD ABDULLA
594c2fe015 Translated using Weblate (Arabic)
Currently translated at 84.9% (679 of 799 strings)

Translation: NetAlertX/core
Translate-URL: https://hosted.weblate.org/projects/pialert/core/ar/
2026-03-03 05:07:54 +01:00
Safeguard
14362d20bd Translated using Weblate (Russian)
Currently translated at 98.7% (789 of 799 strings)

Translation: NetAlertX/core
Translate-URL: https://hosted.weblate.org/projects/pialert/core/ru/
2026-03-03 05:07:53 +01:00
ssantos
4f239be8a3 Translated using Weblate (Portuguese (Portugal))
Currently translated at 64.5% (516 of 799 strings)

Translation: NetAlertX/core
Translate-URL: https://hosted.weblate.org/projects/pialert/core/pt_PT/
2026-03-03 05:07:52 +01:00
Marcus Isdahl
5a65d807a8 Translated using Weblate (Norwegian Bokmål)
Currently translated at 69.8% (558 of 799 strings)

Translation: NetAlertX/core
Translate-URL: https://hosted.weblate.org/projects/pialert/core/nb_NO/
2026-03-03 05:07:49 +01:00
ButterflyOfFire
f3bf37bb24 Translated using Weblate (French)
Currently translated at 98.8% (790 of 799 strings)

Translation: NetAlertX/core
Translate-URL: https://hosted.weblate.org/projects/pialert/core/fr/
2026-03-03 05:07:47 +01:00
大王叫我来巡山
b7e1cb1f9d Translated using Weblate (Chinese (Simplified Han script))
Currently translated at 98.4% (787 of 799 strings)

Translation: NetAlertX/core
Translate-URL: https://hosted.weblate.org/projects/pialert/core/zh_Hans/
2026-03-03 05:07:45 +01:00
Ptsa Daniel
b4510663f7 Translated using Weblate (Chinese (Simplified Han script))
Currently translated at 98.4% (787 of 799 strings)

Translation: NetAlertX/core
Translate-URL: https://hosted.weblate.org/projects/pialert/core/zh_Hans/
2026-03-03 05:07:44 +01:00
Anonymous
dd564b235b Translated using Weblate (Spanish)
Currently translated at 98.2% (785 of 799 strings)

Translation: NetAlertX/core
Translate-URL: https://hosted.weblate.org/projects/pialert/core/es/
2026-03-03 05:07:43 +01:00
mid
04db68ea6c Translated using Weblate (Japanese)
Currently translated at 98.8% (790 of 799 strings)

Translation: NetAlertX/core
Translate-URL: https://hosted.weblate.org/projects/pialert/core/ja/
2026-03-03 05:07:42 +01:00
GoldBull3t
550f59b34f Translated using Weblate (Portuguese (Brazil))
Currently translated at 50.8% (406 of 799 strings)

Translation: NetAlertX/core
Translate-URL: https://hosted.weblate.org/projects/pialert/core/pt_BR/
2026-03-03 05:07:41 +01:00
blomusti
6e8a3d8a58 Translated using Weblate (Turkish)
Currently translated at 56.4% (451 of 799 strings)

Translation: NetAlertX/core
Translate-URL: https://hosted.weblate.org/projects/pialert/core/tr/
2026-03-03 05:07:40 +01:00
Deleted User
c89b2ded26 Translated using Weblate (Ukrainian)
Currently translated at 97.6% (780 of 799 strings)

Translation: NetAlertX/core
Translate-URL: https://hosted.weblate.org/projects/pialert/core/uk/
2026-03-03 05:07:39 +01:00
Anonymous
9f964be0c3 Translated using Weblate (German)
Currently translated at 78.8% (630 of 799 strings)

Translation: NetAlertX/core
Translate-URL: https://hosted.weblate.org/projects/pialert/core/de/
2026-03-03 05:07:39 +01:00
anton garcias
d2bc8410a7 Translated using Weblate (Catalan)
Currently translated at 98.8% (790 of 799 strings)

Translation: NetAlertX/core
Translate-URL: https://hosted.weblate.org/projects/pialert/core/ca/
2026-03-03 05:07:38 +01:00
Jokob @NetAlertX
ab74307ed1 Add next scan ETA display and update app state with scan timing information 2026-03-03 04:07:22 +00:00
Adam Outler
8ab9d9f395 Update docs 2026-03-02 19:43:38 +00:00
Adam Outler
c1d53ff93f Update docker compose and unit tests 2026-03-02 19:43:28 +00:00
Adam Outler
a329c5b541 Tidy up plugin logic 2026-03-02 19:42:29 +00:00
Adam Outler
0555105473 Detect sysctls only, don't modify sysctls; allow user to modify. 2026-03-02 19:42:00 +00:00
Adam Outler
b0aa5d0e45 Fix startup script matching for skips 2026-03-02 19:41:06 +00:00
Adam Outler
93df52f70c Fix healthcheck for non-0.0.0.0. will pass as long as reachable. 2026-03-02 19:39:23 +00:00
jokob-sk
95f411d92a DOCS: statuses
Signed-off-by: jokob-sk <jokob.sk@gmail.com>
2026-03-02 21:51:54 +11:00
Jokob @NetAlertX
bc4f419927 Enhance device MAC address handling in tests to ensure lowercase normalization and skip tests when web protection is disabled 2026-03-02 10:42:36 +00:00
Jokob @NetAlertX
3a73817048 Enhance device down event handling for sleeping devices and improve down alerts query 2026-03-02 10:05:37 +00:00
Hosted Weblate
67aa46f1cf Merge branch 'origin/main' into Weblate. 2026-03-02 10:54:18 +01:00
Massimo Pissarello
da63acb675 Translated using Weblate (Italian)
Currently translated at 100.0% (794 of 794 strings)

Translation: NetAlertX/core
Translate-URL: https://hosted.weblate.org/projects/pialert/core/it/
2026-03-02 10:54:18 +01:00
jokob-sk
50125f0700 DOCS: statuses
Signed-off-by: jokob-sk <jokob.sk@gmail.com>
2026-03-02 20:54:09 +11:00
Jokob @NetAlertX
6724d250d4 Refactor network tree data structure and improve device status handling
- Updated the network tree data structure to use consistent naming conventions for device properties (e.g., devName, devMac).
- Enhanced the initTree function to utilize the new property names and improved the rendering of device nodes.
- Refactored the getStatusBadgeParts function to include additional parameters for archived and new device statuses.
- Introduced convenience functions (badgeFromDevice and badgeFromDataAttrs) to streamline badge generation from device objects and data attributes.
- Updated various language files to include new status labels and ensure consistency across translations.
- Modified the renderSmallBox function to allow for custom icon HTML, improving flexibility in UI components.
2026-03-02 09:35:42 +00:00
Jokob @NetAlertX
3e237bb452 Normalize MAC addresses in SQL queries and add devCanSleep column to device schema 2026-03-02 06:03:18 +00:00
Jokob @NetAlertX
15807b7ab9 Add unit and integration tests for device down event handling and sleeping suppression 2026-03-02 05:53:28 +00:00
Jokob @NetAlertX
0497c2891e Fix formatting issues in DATABASE.md for improved readability 2026-03-02 04:35:36 +00:00
Jokob @NetAlertX
8e6efc3008 sleeping devices status #1519 2026-03-02 04:35:07 +00:00
jokob-sk
deb0d16c3d LNG: PRAGMA_JOURNAL_SIZE_LIMIT
Signed-off-by: jokob-sk <jokob.sk@gmail.com>
2026-03-01 18:38:38 +11:00
jokob-sk
a94f3d7222 DOCS: PRAGMA_JOURNAL_SIZE_LIMIT
Signed-off-by: jokob-sk <jokob.sk@gmail.com>
2026-03-01 17:47:15 +11:00
Jokob @NetAlertX
d9608b4760 Add database performance tuning guidelines and user-configurable WAL size limit 2026-03-01 06:43:07 +00:00
Jokob @NetAlertX
584aba2c7b Set journal size limit to 10 MB for SQLite connections to prevent unbounded WAL growth 2026-03-01 06:19:39 +00:00
Jokob @NetAlertX
ea5585a8ef Add database cleanup for Sessions and optimize queries
- Implemented deletion of Sessions older than DAYS_TO_KEEP_EVENTS.
- Added index for Plugins_History to improve query performance.
- Introduced unit tests for Sessions trimming and database analysis.
2026-03-01 06:07:57 +00:00
Anonymous
c1adfd35f3 Translated using Weblate (Arabic)
Currently translated at 85.8% (680 of 792 strings)

Translation: NetAlertX/core
Translate-URL: https://hosted.weblate.org/projects/pialert/core/ar/
2026-03-01 05:09:57 +00:00
Anonymous
66532c54a1 Translated using Weblate (German)
Currently translated at 79.6% (631 of 792 strings)

Translation: NetAlertX/core
Translate-URL: https://hosted.weblate.org/projects/pialert/core/de/
2026-03-01 05:09:55 +00:00
Anonymous
a6ce4174fe Translated using Weblate (Norwegian Bokmål)
Currently translated at 70.5% (559 of 792 strings)

Translation: NetAlertX/core
Translate-URL: https://hosted.weblate.org/projects/pialert/core/nb_NO/
2026-03-01 05:09:54 +00:00
Anonymous
247a967e9b Translated using Weblate (Polish)
Currently translated at 85.6% (678 of 792 strings)

Translation: NetAlertX/core
Translate-URL: https://hosted.weblate.org/projects/pialert/core/pl/
2026-03-01 05:09:52 +00:00
Anonymous
dbe65b2a27 Translated using Weblate (Turkish)
Currently translated at 57.0% (452 of 792 strings)

Translation: NetAlertX/core
Translate-URL: https://hosted.weblate.org/projects/pialert/core/tr/
2026-03-01 05:09:50 +00:00
Anonymous
563cb4ba20 Translated using Weblate (Portuguese (Brazil))
Currently translated at 51.3% (407 of 792 strings)

Translation: NetAlertX/core
Translate-URL: https://hosted.weblate.org/projects/pialert/core/pt_BR/
2026-03-01 05:09:49 +00:00
大王叫我来巡山
3d4aba4b39 Translated using Weblate (Chinese (Simplified Han script))
Currently translated at 99.3% (787 of 792 strings)

Translation: NetAlertX/core
Translate-URL: https://hosted.weblate.org/projects/pialert/core/zh_Hans/
2026-03-01 05:09:47 +00:00
Jokob @NetAlertX
b96ace0447 Merge branch 'main' of https://github.com/netalertx/NetAlertX 2026-03-01 04:55:40 +00:00
Jokob @NetAlertX
e15c68d189 feat: enhance cacheStrings function to re-fetch plugin language strings on initialization 2026-03-01 04:55:35 +00:00
jokob-sk
f5e411d5d5 chore: jokob-sk->netalertx
Signed-off-by: jokob-sk <jokob.sk@gmail.com>
2026-02-28 16:00:37 +11:00
jokob-sk
f727580798 Merge branch 'main' of github.com:netalertx/NetAlertX 2026-02-28 15:58:52 +11:00
jokob-sk
11499a6890 chore: jokob-sk->netalertx
Signed-off-by: jokob-sk <jokob.sk@gmail.com>
2026-02-28 15:58:46 +11:00
Jokob @NetAlertX
85badb0760 Merge branch 'main' of https://github.com/netalertx/NetAlertX 2026-02-28 01:51:22 +00:00
Jokob @NetAlertX
814ba02d1c feat: implement languages endpoint and refactor language handling to use languages.json 2026-02-28 01:51:12 +00:00
Jokob @NetAlertX
e57fd2e81e fix: update body text color in dark theme and remove unnecessary console log 2026-02-28 01:02:21 +00:00
jokob-sk
4dc2a63ebb DOCS: flapping/unstable status addition
Signed-off-by: jokob-sk <jokob.sk@gmail.com>
2026-02-28 11:37:11 +11:00
jokob-sk
6b320877ec Merge branch 'main' of github.com:netalertx/NetAlertX 2026-02-28 11:30:24 +11:00
jokob-sk
43667a3bc4 DOCS: flapping/unstable status addition
Signed-off-by: jokob-sk <jokob.sk@gmail.com>
2026-02-28 11:30:13 +11:00
Jokob @NetAlertX
4d0b7c944f Merge branch 'main' of https://github.com/netalertx/NetAlertX 2026-02-28 00:24:37 +00:00
jokob-sk
9894009455 Merge branch 'main' of github.com:netalertx/NetAlertX 2026-02-28 11:24:08 +11:00
jokob-sk
0e18e34918 DOCS: flapping/unstable status addition
Signed-off-by: jokob-sk <jokob.sk@gmail.com>
2026-02-28 11:23:56 +11:00
Jokob @NetAlertX
d9c263d506 feat: Update cache handling to normalize legacy device data format and improve logging 2026-02-28 00:13:22 +00:00
Jokob @NetAlertX
58e32a5b43 feat: Refactor device column management and integrate new device-columns.js for centralized field definitions 2026-02-28 00:06:04 +00:00
jokob-sk
24e2036bde DOCS: flappin/usntable status addition
Signed-off-by: jokob-sk <jokob.sk@gmail.com>
2026-02-28 10:31:06 +11:00
Jokob @NetAlertX
b74b803d6c feat: Add devFlapping attribute to device management and update related UI components 2026-02-27 23:29:55 +00:00
Jokob @NetAlertX
173ffbe3b2 feat: Add cache clearing logic for imported settings in state update 2026-02-27 21:31:36 +00:00
Jokob @NetAlertX
d2ebe0d452 Merge branch 'main' of https://github.com/netalertx/NetAlertX 2026-02-27 10:08:56 +00:00
Jokob @NetAlertX
4c0d5c7376 refactor: Consolidate tab initialization logic using shared utility function 2026-02-27 10:07:55 +00:00
jokob-sk
686a713aa8 FE: lower case MAC issues #1538
Signed-off-by: jokob-sk <jokob.sk@gmail.com>
2026-02-27 14:35:35 +11:00
Jokob @NetAlertX
9d64665599 fix: Remove trailing whitespace and clean up formatting in version.php 2026-02-26 04:21:57 +00:00
Jokob @NetAlertX
63cef590d6 Refactor network API calls to use centralized authentication context and improve cache handling
- Removed redundant getApiToken function and replaced its usage with getAuthContext in network-api.js, network-events.js, and network-init.js.
- Updated cache handling in network-events.js and network-init.js to use CACHE_KEYS constants for better maintainability.
- Introduced cache.js for centralized cache management functions and constants, including cache initialization and retrieval.
- Added app-init.js for application lifecycle management, including cache orchestration and initialization checks.
- Created app_config.php to securely fetch API token and GraphQL port from configuration.
- Improved error handling and logging throughout the codebase for better debugging and maintenance.
2026-02-26 04:21:29 +00:00
Jokob @NetAlertX
00042ab594 refactor: Clean up whitespace and improve API token verification in network initialization 2026-02-25 21:59:51 +00:00
Jokob @NetAlertX
786cc5ee33 feat: Implement network topology management with API integration
- Added network-api.js for handling API calls related to network devices and nodes.
- Introduced network-events.js to manage event handlers for node interactions and window resizing.
- Created network-init.js for initializing network topology on page load and fetching device data.
- Developed network-tabs.js for rendering network tabs and managing tab content.
- Implemented network-tree.js for constructing and rendering the tree hierarchy of network devices.
- Enhanced error handling and user feedback for API calls and data loading processes.
- Included caching mechanisms for user preferences regarding device visibility.
2026-02-25 21:59:40 +00:00
大王叫我来巡山
0b32a06178 Translated using Weblate (Chinese (Simplified Han script))
Currently translated at 99.4% (787 of 791 strings)

Translation: NetAlertX/core
Translate-URL: https://hosted.weblate.org/projects/pialert/core/zh_Hans/
2026-02-24 04:09:55 +01:00
anton garcias
1fa381429d Translated using Weblate (Catalan)
Currently translated at 100.0% (791 of 791 strings)

Translation: NetAlertX/core
Translate-URL: https://hosted.weblate.org/projects/pialert/core/ca/
2026-02-24 04:09:54 +01:00
Massimo Pissarello
fae61174a7 Translated using Weblate (Italian)
Currently translated at 100.0% (791 of 791 strings)

Translation: NetAlertX/core
Translate-URL: https://hosted.weblate.org/projects/pialert/core/it/
2026-02-24 04:09:52 +01:00
Sylvain Pichon
d06301ac80 Translated using Weblate (French)
Currently translated at 100.0% (791 of 791 strings)

Translation: NetAlertX/core
Translate-URL: https://hosted.weblate.org/projects/pialert/core/fr/
2026-02-24 04:09:51 +01:00
Marco Rios
f4bc9c93c3 Translated using Weblate (Spanish)
Currently translated at 99.3% (786 of 791 strings)

Translation: NetAlertX/core
Translate-URL: https://hosted.weblate.org/projects/pialert/core/es/
2026-02-24 04:09:49 +01:00
mid
0172ab4311 Translated using Weblate (Japanese)
Currently translated at 100.0% (791 of 791 strings)

Translation: NetAlertX/core
Translate-URL: https://hosted.weblate.org/projects/pialert/core/ja/
2026-02-24 04:09:46 +01:00
jokob-sk
f1fc9f24b1 FE: README
Signed-off-by: jokob-sk <jokob.sk@gmail.com>
2026-02-24 07:53:58 +11:00
jokob-sk
c192f2c032 FE: mixed case on MACs work
Signed-off-by: jokob-sk <jokob.sk@gmail.com>
2026-02-24 07:27:30 +11:00
jokob-sk
a309f99c3d Merge branch 'main' of https://github.com/jokob-sk/NetAlertX 2026-02-24 07:08:43 +11:00
jokob-sk
54e9d52126 DOCS: readme
Signed-off-by: jokob-sk <jokob.sk@gmail.com>
2026-02-24 07:08:29 +11:00
jokob-sk
8fc78f02e9 BE: Better arpo-scan accuracy w/ system optimization
Signed-off-by: jokob-sk <jokob.sk@gmail.com>
2026-02-24 07:07:55 +11:00
Jokob @NetAlertX
123f715241 Merge pull request #1533 from adamoutler/llm-docs
Add LLMs.txt
2026-02-23 15:01:37 +11:00
Jokob @NetAlertX
446545e7eb Merge pull request #1534 from MrMeatikins/feat/docker-install-docs
Docs: Add Docker install instructions for MCP Agents
2026-02-23 15:00:26 +11:00
MrMeatikins
14625926f9 Revert env variables per jokob-sk review
Co-authored-by: jokob-sk <jokob.sk@gmail.com>
2026-02-22 22:37:24 -05:00
MrMeatikins
c7e754966e Simplify README to link official docs 2026-02-22 22:22:21 -05:00
MrMeatikins
4316a436eb Apply CodeRabbit suggestions 2026-02-22 22:20:28 -05:00
Adam Outler
fe22659794 coderabbit suggested changes 2026-02-23 03:15:21 +00:00
Adam Outler
cb0b3b607d Update front/llms.txt
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
2026-02-22 22:11:40 -05:00
Adam Outler
53b2596902 Add LLMs.txt 2026-02-23 03:04:43 +00:00
jokob-sk
1a364e2fe2 Merge branch 'main' of https://github.com/jokob-sk/NetAlertX 2026-02-22 23:13:15 +11:00
jokob-sk
2f1e5068e3 BE+FE: Unstable devices list (3 status changes in 1h)
Signed-off-by: jokob-sk <jokob.sk@gmail.com>
2026-02-22 23:12:46 +11:00
Jokob @NetAlertX
57118bc9bd Merge pull request #1500 from netalertx/next_release
Next release - deep link support after log in and refactor of index.php
2026-02-22 16:22:08 +11:00
Jokob @NetAlertX
25a81556e3 Refactor: Simplify login redirection logic and clean up UI test code 2026-02-22 05:20:05 +00:00
Jokob @NetAlertX
39f617be5f Refactor: Remove unused is_https_request function and related comments; clean up test_login function by removing unnecessary password list 2026-02-22 05:11:09 +00:00
Jokob @NetAlertX
c4c966ffa7 Refactor: Remove unused Remember Me schemas and related comments 2026-02-22 05:07:37 +00:00
Jokob @NetAlertX
f88aefe022 Refactor: Remove unused call_api function and related comments 2026-02-22 05:00:00 +00:00
Jokob @NetAlertX
54db347b94 Refactor: Clean up whitespace in deep link handling functions and tests 2026-02-22 04:54:43 +00:00
Jokob @NetAlertX
2ae87fca38 Refactor login functionality: Remove Remember Me feature and update tests for deep link support 2026-02-22 04:54:34 +00:00
Jokob @NetAlertX
8224363c45 Refactor authentication: Remove Remember Me API endpoints and schemas; implement cookie-based Remember Me functionality 2026-02-22 04:44:57 +00:00
Jokob @NetAlertX
eb399ec193 BE: Clean up whitespace in call_api function 2026-02-22 03:49:21 +00:00
Jokob @NetAlertX
70645e7ef3 server-side remember-me 2026-02-22 03:47:29 +00:00
Jokob @NetAlertX
0e94dcb091 Merge pull request #1530 from netalertx/main
sync
2026-02-22 14:21:47 +11:00
jokob-sk
a26137800d BE: # ---------------------------------------------------------------------------------#
Signed-off-by: jokob-sk <jokob.sk@gmail.com>
2026-02-22 11:44:20 +11:00
jokob-sk
63810bc536 BE: Parameters table, app.sql duplicate removal
Signed-off-by: jokob-sk <jokob.sk@gmail.com>
2026-02-22 11:17:34 +11:00
jokob-sk
57d451fcf4 BE: Parameters table, app.sql duplicate removal
Signed-off-by: jokob-sk <jokob.sk@gmail.com>
2026-02-22 10:28:30 +11:00
jokob-sk
bf6218e836 FE: mixed case on MACs broke node expansion/collapse
Signed-off-by: jokob-sk <jokob.sk@gmail.com>
2026-02-22 09:50:45 +11:00
jokob-sk
e9efabd562 FE: mixed case on MACs broke node expansion/collapse
Signed-off-by: jokob-sk <jokob.sk@gmail.com>
2026-02-22 09:47:00 +11:00
jokob-sk
eb0f705587 BE: devices.csv import from file did nt work becasue of too strict validation
Signed-off-by: jokob-sk <jokob.sk@gmail.com>
2026-02-22 08:30:40 +11:00
jokob-sk
2559702a6a BE+FE: better VLAN/SSID handling
Signed-off-by: jokob-sk <jokob.sk@gmail.com>
2026-02-22 08:08:48 +11:00
jokob-sk
6bbfc0637c BE+FE: better VLAN/SSID handling
Signed-off-by: jokob-sk <jokob.sk@gmail.com>
2026-02-21 23:55:53 +11:00
jokob-sk
688d49b5ae Merge branch 'main' of https://github.com/jokob-sk/NetAlertX 2026-02-21 14:19:09 +11:00
jokob-sk
ab7df4384e INS: handle LOADED_PLUGINS env variable
Signed-off-by: jokob-sk <jokob.sk@gmail.com>
2026-02-21 14:18:11 +11:00
Safeguard
2018636bf8 Translated using Weblate (Russian)
Currently translated at 100.0% (790 of 790 strings)

Translation: NetAlertX/core
Translate-URL: https://hosted.weblate.org/projects/pialert/core/ru/
2026-02-20 09:09:51 +01:00
jokob-sk
50f341e84f BE: force upgrade of unifi-sm-api>=0.2.3 #1524
Signed-off-by: jokob-sk <jokob.sk@gmail.com>
2026-02-20 07:44:49 +11:00
PlanBot
32c21b01bb feat(docs): Update Docker install guide and templates
- Add --force-recreate to install commands for easier version switching

- Remove debug flags (ALWAYS_FRESH_INSTALL, NETALERTX_DEBUG) from templates

- Link to official DOCKER_COMPOSE environment variable docs
2026-02-19 12:30:50 -05:00
jokob-sk
05c332867b BE: force upgrade of unifi-sm-api>=0.2.2 #1524
Signed-off-by: jokob-sk <jokob.sk@gmail.com>
2026-02-19 14:22:23 +11:00
jokob-sk
12b0d911ff DOCS: UNIFIAPI
Signed-off-by: jokob-sk <jokob.sk@gmail.com>
2026-02-19 14:10:47 +11:00
jokob-sk
04884a264b DOCS: bug #1524
Signed-off-by: jokob-sk <jokob.sk@gmail.com>
2026-02-19 09:09:31 +11:00
jokob-sk
2742414123 BE: /health endpoint
Signed-off-by: jokob-sk <jokob.sk@gmail.com>
2026-02-18 22:29:12 +11:00
jokob-sk
876cd4bbe1 BE: psutils missing
Signed-off-by: jokob-sk <jokob.sk@gmail.com>
2026-02-18 13:08:34 +11:00
jokob-sk
91775deaa3 BE: psutil missing
Signed-off-by: jokob-sk <jokob.sk@gmail.com>
2026-02-18 12:04:03 +11:00
jokob-sk
7075091569 BE: psutil missing
Signed-off-by: jokob-sk <jokob.sk@gmail.com>
2026-02-18 11:35:18 +11:00
jokob-sk
f63658af7d Merge branch 'main' of https://github.com/jokob-sk/NetAlertX 2026-02-18 11:17:43 +11:00
jokob-sk
774c123804 BE: psutil missing
Signed-off-by: jokob-sk <jokob.sk@gmail.com>
2026-02-18 11:17:25 +11:00
Jokob @NetAlertX
32e2d571a0 Merge pull request #1521 from netalertx/chore_timestamps
feat: add health check endpoint and related schemas with tests
2026-02-18 10:35:53 +11:00
jokob-sk
249d12ded4 suggestions 2026-02-11 09:10:37 +11:00
jokob-sk
3036cd04fc add redirect after log in to support deep links
Signed-off-by: jokob-sk <jokob.sk@gmail.com>
2026-02-09 12:49:50 +11:00
jokob-sk
3d3abe7e53 add redirect after log in to support deep links
Signed-off-by: jokob-sk <jokob.sk@gmail.com>
2026-02-09 10:22:03 +11:00
jokob-sk
a088f4580a add redirect after log in to support deep links
Signed-off-by: jokob-sk <jokob.sk@gmail.com>
2026-02-09 10:11:21 +11:00
jokob-sk
75c7d6c015 add redirect after log in to support deep links
Signed-off-by: jokob-sk <jokob.sk@gmail.com>
2026-02-09 09:41:20 +11:00
Jokob @NetAlertX
d434cc5315 Merge pull request #1499 from netalertx/main
sync
2026-02-09 09:38:55 +11:00
260 changed files with 13005 additions and 5636 deletions

View File

@@ -35,6 +35,7 @@ RUN apk add --no-cache \
shadow \
python3 \
python3-dev \
py3-psutil \
gcc \
musl-dev \
libffi-dev \
@@ -136,7 +137,7 @@ ENV LANG=C.UTF-8
RUN apk add --no-cache bash mtr libbsd zip lsblk tzdata curl arp-scan iproute2 iproute2-ss nmap fping \
nmap-scripts traceroute nbtscan net-tools net-snmp-tools bind-tools awake ca-certificates \
sqlite php83 php83-fpm php83-cgi php83-curl php83-sqlite3 php83-session python3 envsubst \
sqlite php83 php83-fpm php83-cgi php83-curl php83-sqlite3 php83-session python3 py3-psutil envsubst \
nginx supercronic shadow su-exec jq && \
rm -Rf /var/cache/apk/* && \
rm -Rf /etc/nginx && \

1
.env
View File

@@ -6,7 +6,6 @@ LOGS_LOCATION=/path/to/docker_logs
#ENVIRONMENT VARIABLES
TZ=Europe/Paris
PORT=20211
#DEVELOPMENT VARIABLES

View File

@@ -5,13 +5,15 @@ 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
- DB columns must not contain underscores, use camelCase instead (e.g., deviceInstanceId, not device_instance_id)
- treat DB as temporary storage for stats, long term configuration should be stored in the /config folder, the /config folder should allow you to restore most of your functionality (excluding historical data)
## File Length
@@ -64,7 +66,7 @@ Use timeNowUTC(as_string=False) for datetime operations (scheduling, comparisons
## String Sanitization
Use sanitizers from `server/helper.py` before storing user input.
Use sanitizers from `server/helper.py` before storing user input. MAC addresses are always lowercased and normalized. IP addresses should be validated.
## Devcontainer Constraints

View File

@@ -22,8 +22,10 @@ jobs:
- name: Install MkDocs
run: |
pip install mkdocs mkdocs-material
pip install mkdocs-github-admonitions-plugin
pip install \
mkdocs==1.6.0 \
mkdocs-material==9.5.21 \
mkdocs-github-admonitions-plugin==0.0.4
- name: Build MkDocs
run: mkdocs build

View File

@@ -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

View File

@@ -1,23 +1,38 @@
# 🤝 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/jokob-sk/NetAlertX/issues) for:
- Bug reports 🐞
- Feature requests 💡
- Documentation feedback 📖
Please use the [GitHub Issue Tracker](https://github.com/netalertx/NetAlertX/issues) for:
- 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/jokob-sk/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)
## Use of AI
Use of AI-assisted tools is permitted, provided all generated code is reviewed, understood, and verified before submission.
- All AI-generated code must meet the project's **quality, security, and performance standards**.
- Contributors are responsible for **fully understanding** any code they submit, regardless of how it was produced.
- Prefer **clarity and maintainability over cleverness or brevity**. Readable code is always favored over dense or obfuscated implementations.
- Follow the **DRY (Don't Repeat Yourself) principle** where appropriate, without sacrificing readability.
- Do not submit code that you cannot confidently explain or debug.
All changes must pass the **full test suite** before opening a PR.
---
## Submitting Pull Requests (PRs)
We welcome PRs to improve the code, docs, or UI!
@@ -28,10 +43,23 @@ Please:
- Provide a clear title and description for your PR
- If relevant, add or update tests and documentation
- For plugins, refer to the [Plugin Dev Guide](https://docs.netalertx.com/PLUGINS_DEV)
- Switch the PR to DRAFT mode if still being worked on
- Keep PRs **focused and minimal** — avoid unrelated changes in a single PR
- PRs that do not meet these guidelines may be closed without review
## Commit Messages
- Use clear, descriptive commit messages
- Explain *why* a change was made, not just *what* changed
- Reference related issues where applicable
## 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 +67,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! 💙

View File

@@ -32,6 +32,7 @@ RUN apk add --no-cache \
shadow \
python3 \
python3-dev \
py3-psutil \
gcc \
musl-dev \
libffi-dev \
@@ -133,7 +134,7 @@ ENV LANG=C.UTF-8
RUN apk add --no-cache bash mtr libbsd zip lsblk tzdata curl arp-scan iproute2 iproute2-ss nmap fping \
nmap-scripts traceroute nbtscan net-tools net-snmp-tools bind-tools awake ca-certificates \
sqlite php83 php83-fpm php83-cgi php83-curl php83-sqlite3 php83-session python3 envsubst \
sqlite php83 php83-fpm php83-cgi php83-curl php83-sqlite3 php83-session python3 py3-psutil envsubst \
nginx supercronic shadow su-exec jq && \
rm -Rf /var/cache/apk/* && \
rm -Rf /etc/nginx && \

View File

@@ -10,6 +10,7 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
python3 \
python3-dev \
python3-pip \
python3-psutil \
python3-venv \
gcc \
git \
@@ -193,7 +194,7 @@ RUN for vfile in .VERSION .VERSION_PREV; do \
# setcap cap_net_raw,cap_net_admin+eip $(readlink -f ${VIRTUAL_ENV_BIN}/python) && \
/bin/bash /build/init-nginx.sh && \
/bin/bash /build/init-php-fpm.sh && \
# /bin/bash /build/init-cron.sh && \
# /bin/bash /build/init-cron.sh && \
# Debian cron init might differ, skipping for now or need to check init-cron.sh content
# Checking init-backend.sh
/bin/bash /build/init-backend.sh && \

View File

@@ -1,6 +1,6 @@
[![Docker Size](https://img.shields.io/docker/image-size/jokobsk/netalertx?label=Size&logo=Docker&color=0aa8d2&logoColor=fff&style=for-the-badge)](https://hub.docker.com/r/jokobsk/netalertx)
[![Docker Pulls](https://img.shields.io/docker/pulls/jokobsk/netalertx?label=Pulls&logo=docker&color=0aa8d2&logoColor=fff&style=for-the-badge)](https://hub.docker.com/r/jokobsk/netalertx)
[![GitHub Release](https://img.shields.io/github/v/release/jokob-sk/NetAlertX?color=0aa8d2&logoColor=fff&logo=GitHub&style=for-the-badge)](https://github.com/jokob-sk/NetAlertX/releases)
[![GitHub Release](https://img.shields.io/github/v/release/netalertx/NetAlertX?color=0aa8d2&logoColor=fff&logo=GitHub&style=for-the-badge)](https://github.com/netalertx/NetAlertX/releases)
[![Discord](https://img.shields.io/discord/1274490466481602755?color=0aa8d2&logoColor=fff&logo=Discord&style=for-the-badge)](https://discord.gg/NczTUTWyRr)
[![Home Assistant](https://img.shields.io/badge/Repo-blue?logo=home-assistant&style=for-the-badge&color=0aa8d2&logoColor=fff&label=Add)](https://my.home-assistant.io/redirect/supervisor_add_addon_repository/?repository_url=https%3A%2F%2Fgithub.com%2Falexbelgium%2Fhassio-addons)
@@ -168,9 +168,9 @@ Get notified about a new release, what new functionality you can use and about b
### 🔀 Other Alternative Apps
- [Fing](https://www.fing.com/) - Network scanner app for your Internet security (Commercial, Phone App, Proprietary hardware)
- [NetBox](https://netboxlabs.com/) - Network management software (Commercial)
- [NetBox](https://netboxlabs.com/) - The gold standard for Network Source of Truth (NSoT) and IPAM.
- [Zabbix](https://www.zabbix.com/) or [Nagios](https://www.nagios.org/) - Strong focus on infrastructure monitoring.
- [NetAlertX](https://netalertx.com) - The streamlined, discovery-focused alternative for real-time asset intelligence.
- [NetAlertX](https://netalertx.com) - The streamlined, discovery-focused choice for real-time asset intelligence and noise-free alerting.
### 💙 Donations
@@ -207,6 +207,7 @@ Proudly using [Weblate](https://hosted.weblate.org/projects/pialert/). Help out
### License
> GPL 3.0 | [Read more here](LICENSE.txt) | Source of the [animated GIF (Loading Animation)](https://commons.wikimedia.org/wiki/File:Loading_Animation.gif) | Source of the [selfhosted Fonts](https://github.com/adobe-fonts/source-sans)
_All product names, logos, and brands are property of their respective owners. All company, product and service names used in this website are for identification purposes only. Use of these names, logos, and brands does not imply endorsement._
<!--- --------------------------------------------------------------------- --->
[main]: ./docs/img/devices_split.png "Main screen"

View File

@@ -3,7 +3,7 @@
# Generated: 2022-12-30_22-19-40 #
# #
# Config file for the LAN intruder detection app: #
# https://github.com/jokob-sk/NetAlertX #
# https://github.com/netalertx/NetAlertX #
# #
#-----------------AUTOGENERATED FILE-----------------#

View File

@@ -1,434 +0,0 @@
CREATE TABLE sqlite_stat1(tbl,idx,stat);
CREATE TABLE Events (eve_MAC STRING (50) NOT NULL COLLATE NOCASE, eve_IP STRING (50) NOT NULL COLLATE NOCASE, eve_DateTime DATETIME NOT NULL, eve_EventType STRING (30) NOT NULL COLLATE NOCASE, eve_AdditionalInfo STRING (250) DEFAULT (''), eve_PendingAlertEmail BOOLEAN NOT NULL CHECK (eve_PendingAlertEmail IN (0, 1)) DEFAULT (1), eve_PairEventRowid INTEGER);
CREATE TABLE Sessions (ses_MAC STRING (50) COLLATE NOCASE, ses_IP STRING (50) COLLATE NOCASE, ses_EventTypeConnection STRING (30) COLLATE NOCASE, ses_DateTimeConnection DATETIME, ses_EventTypeDisconnection STRING (30) COLLATE NOCASE, ses_DateTimeDisconnection DATETIME, ses_StillConnected BOOLEAN, ses_AdditionalInfo STRING (250));
CREATE TABLE IF NOT EXISTS "Online_History" (
"Index" INTEGER,
"Scan_Date" TEXT,
"Online_Devices" INTEGER,
"Down_Devices" INTEGER,
"All_Devices" INTEGER,
"Archived_Devices" INTEGER,
"Offline_Devices" INTEGER,
PRIMARY KEY("Index" AUTOINCREMENT)
);
CREATE TABLE sqlite_sequence(name,seq);
CREATE TABLE Devices (
devMac STRING (50) PRIMARY KEY NOT NULL COLLATE NOCASE,
devName STRING (50) NOT NULL DEFAULT "(unknown)",
devOwner STRING (30) DEFAULT "(unknown)" NOT NULL,
devType STRING (30),
devVendor STRING (250),
devFavorite BOOLEAN CHECK (devFavorite IN (0, 1)) DEFAULT (0) NOT NULL,
devGroup STRING (10),
devComments TEXT,
devFirstConnection DATETIME NOT NULL,
devLastConnection DATETIME NOT NULL,
devLastIP STRING (50) NOT NULL COLLATE NOCASE,
devPrimaryIPv4 TEXT,
devPrimaryIPv6 TEXT,
devVlan TEXT,
devForceStatus TEXT,
devStaticIP BOOLEAN DEFAULT (0) NOT NULL CHECK (devStaticIP IN (0, 1)),
devScan INTEGER DEFAULT (1) NOT NULL,
devLogEvents BOOLEAN NOT NULL DEFAULT (1) CHECK (devLogEvents IN (0, 1)),
devAlertEvents BOOLEAN NOT NULL DEFAULT (1) CHECK (devAlertEvents IN (0, 1)),
devAlertDown BOOLEAN NOT NULL DEFAULT (0) CHECK (devAlertDown IN (0, 1)),
devSkipRepeated INTEGER DEFAULT 0 NOT NULL,
devLastNotification DATETIME,
devPresentLastScan BOOLEAN NOT NULL DEFAULT (0) CHECK (devPresentLastScan IN (0, 1)),
devIsNew BOOLEAN NOT NULL DEFAULT (1) CHECK (devIsNew IN (0, 1)),
devLocation STRING (250) COLLATE NOCASE,
devIsArchived BOOLEAN NOT NULL DEFAULT (0) CHECK (devIsArchived IN (0, 1)),
devParentMAC TEXT,
devParentPort INTEGER,
devParentRelType TEXT,
devIcon TEXT,
devGUID TEXT,
devSite TEXT,
devSSID TEXT,
devSyncHubNode TEXT,
devSourcePlugin TEXT,
devMacSource TEXT,
devNameSource TEXT,
devFQDNSource TEXT,
devLastIPSource TEXT,
devVendorSource TEXT,
devSSIDSource TEXT,
devParentMACSource TEXT,
devParentPortSource TEXT,
devParentRelTypeSource TEXT,
devVlanSource TEXT,
"devCustomProps" TEXT);
CREATE TABLE IF NOT EXISTS "Settings" (
"setKey" TEXT,
"setName" TEXT,
"setDescription" TEXT,
"setType" TEXT,
"setOptions" TEXT,
"setGroup" TEXT,
"setValue" TEXT,
"setEvents" TEXT,
"setOverriddenByEnv" INTEGER
);
CREATE TABLE IF NOT EXISTS "Parameters" (
"par_ID" TEXT PRIMARY KEY,
"par_Value" TEXT
);
CREATE TABLE Plugins_Objects(
"Index" INTEGER,
Plugin TEXT NOT NULL,
Object_PrimaryID TEXT NOT NULL,
Object_SecondaryID TEXT NOT NULL,
DateTimeCreated TEXT NOT NULL,
DateTimeChanged TEXT NOT NULL,
Watched_Value1 TEXT NOT NULL,
Watched_Value2 TEXT NOT NULL,
Watched_Value3 TEXT NOT NULL,
Watched_Value4 TEXT NOT NULL,
Status TEXT NOT NULL,
Extra TEXT NOT NULL,
UserData TEXT NOT NULL,
ForeignKey TEXT NOT NULL,
SyncHubNodeName TEXT,
"HelpVal1" TEXT,
"HelpVal2" TEXT,
"HelpVal3" TEXT,
"HelpVal4" TEXT,
ObjectGUID TEXT,
PRIMARY KEY("Index" AUTOINCREMENT)
);
CREATE TABLE Plugins_Events(
"Index" INTEGER,
Plugin TEXT NOT NULL,
Object_PrimaryID TEXT NOT NULL,
Object_SecondaryID TEXT NOT NULL,
DateTimeCreated TEXT NOT NULL,
DateTimeChanged TEXT NOT NULL,
Watched_Value1 TEXT NOT NULL,
Watched_Value2 TEXT NOT NULL,
Watched_Value3 TEXT NOT NULL,
Watched_Value4 TEXT NOT NULL,
Status TEXT NOT NULL,
Extra TEXT NOT NULL,
UserData TEXT NOT NULL,
ForeignKey TEXT NOT NULL,
SyncHubNodeName TEXT,
"HelpVal1" TEXT,
"HelpVal2" TEXT,
"HelpVal3" TEXT,
"HelpVal4" TEXT, "ObjectGUID" TEXT,
PRIMARY KEY("Index" AUTOINCREMENT)
);
CREATE TABLE Plugins_History(
"Index" INTEGER,
Plugin TEXT NOT NULL,
Object_PrimaryID TEXT NOT NULL,
Object_SecondaryID TEXT NOT NULL,
DateTimeCreated TEXT NOT NULL,
DateTimeChanged TEXT NOT NULL,
Watched_Value1 TEXT NOT NULL,
Watched_Value2 TEXT NOT NULL,
Watched_Value3 TEXT NOT NULL,
Watched_Value4 TEXT NOT NULL,
Status TEXT NOT NULL,
Extra TEXT NOT NULL,
UserData TEXT NOT NULL,
ForeignKey TEXT NOT NULL,
SyncHubNodeName TEXT,
"HelpVal1" TEXT,
"HelpVal2" TEXT,
"HelpVal3" TEXT,
"HelpVal4" TEXT, "ObjectGUID" TEXT,
PRIMARY KEY("Index" AUTOINCREMENT)
);
CREATE TABLE Plugins_Language_Strings(
"Index" INTEGER,
Language_Code TEXT NOT NULL,
String_Key TEXT NOT NULL,
String_Value TEXT NOT NULL,
Extra TEXT NOT NULL,
PRIMARY KEY("Index" AUTOINCREMENT)
);
CREATE TABLE CurrentScan (
scanMac STRING(50) NOT NULL COLLATE NOCASE,
scanLastIP STRING(50) NOT NULL COLLATE NOCASE,
scanVendor STRING(250),
scanSourcePlugin STRING(10),
scanName STRING(250),
scanLastQuery STRING(250),
scanLastConnection STRING(250),
scanSyncHubNode STRING(50),
scanSite STRING(250),
scanSSID STRING(250),
scanVlan STRING(250),
scanParentMAC STRING(250),
scanParentPort STRING(250),
scanType STRING(250),
UNIQUE(scanMac)
);
CREATE TABLE IF NOT EXISTS "AppEvents" (
"Index" INTEGER PRIMARY KEY AUTOINCREMENT,
"GUID" TEXT UNIQUE,
"AppEventProcessed" BOOLEAN,
"DateTimeCreated" TEXT,
"ObjectType" TEXT,
"ObjectGUID" TEXT,
"ObjectPlugin" TEXT,
"ObjectPrimaryID" TEXT,
"ObjectSecondaryID" TEXT,
"ObjectForeignKey" TEXT,
"ObjectIndex" TEXT,
"ObjectIsNew" BOOLEAN,
"ObjectIsArchived" BOOLEAN,
"ObjectStatusColumn" TEXT,
"ObjectStatus" TEXT,
"AppEventType" TEXT,
"Helper1" TEXT,
"Helper2" TEXT,
"Helper3" TEXT,
"Extra" TEXT
);
CREATE TABLE IF NOT EXISTS "Notifications" (
"Index" INTEGER,
"GUID" TEXT UNIQUE,
"DateTimeCreated" TEXT,
"DateTimePushed" TEXT,
"Status" TEXT,
"JSON" TEXT,
"Text" TEXT,
"HTML" TEXT,
"PublishedVia" TEXT,
"Extra" TEXT,
PRIMARY KEY("Index" AUTOINCREMENT)
);
CREATE INDEX IDX_eve_DateTime ON Events (eve_DateTime);
CREATE INDEX IDX_eve_EventType ON Events (eve_EventType COLLATE NOCASE);
CREATE INDEX IDX_eve_MAC ON Events (eve_MAC COLLATE NOCASE);
CREATE INDEX IDX_eve_PairEventRowid ON Events (eve_PairEventRowid);
CREATE INDEX IDX_ses_EventTypeDisconnection ON Sessions (ses_EventTypeDisconnection COLLATE NOCASE);
CREATE INDEX IDX_ses_EventTypeConnection ON Sessions (ses_EventTypeConnection COLLATE NOCASE);
CREATE INDEX IDX_ses_DateTimeDisconnection ON Sessions (ses_DateTimeDisconnection);
CREATE INDEX IDX_ses_MAC ON Sessions (ses_MAC COLLATE NOCASE);
CREATE INDEX IDX_ses_DateTimeConnection ON Sessions (ses_DateTimeConnection);
CREATE INDEX IDX_dev_PresentLastScan ON Devices (devPresentLastScan);
CREATE INDEX IDX_dev_FirstConnection ON Devices (devFirstConnection);
CREATE INDEX IDX_dev_AlertDeviceDown ON Devices (devAlertDown);
CREATE INDEX IDX_dev_StaticIP ON Devices (devStaticIP);
CREATE INDEX IDX_dev_ScanCycle ON Devices (devScan);
CREATE INDEX IDX_dev_Favorite ON Devices (devFavorite);
CREATE INDEX IDX_dev_LastIP ON Devices (devLastIP);
CREATE INDEX IDX_dev_NewDevice ON Devices (devIsNew);
CREATE INDEX IDX_dev_Archived ON Devices (devIsArchived);
CREATE UNIQUE INDEX IF NOT EXISTS idx_events_unique
ON Events (
eve_MAC,
eve_IP,
eve_EventType,
eve_DateTime
);
CREATE VIEW Events_Devices AS
SELECT *
FROM Events
LEFT JOIN Devices ON eve_MAC = devMac
/* Events_Devices(eve_MAC,eve_IP,eve_DateTime,eve_EventType,eve_AdditionalInfo,eve_PendingAlertEmail,eve_PairEventRowid,devMac,devName,devOwner,devType,devVendor,devFavorite,devGroup,devComments,devFirstConnection,devLastConnection,devLastIP,devStaticIP,devScan,devLogEvents,devAlertEvents,devAlertDown,devSkipRepeated,devLastNotification,devPresentLastScan,devIsNew,devLocation,devIsArchived,devParentMAC,devParentPort,devIcon,devGUID,devSite,devSSID,devSyncHubNode,devSourcePlugin,devCustomProps) */;
CREATE VIEW LatestEventsPerMAC AS
WITH RankedEvents AS (
SELECT
e.*,
ROW_NUMBER() OVER (PARTITION BY e.eve_MAC ORDER BY e.eve_DateTime DESC) AS row_num
FROM Events AS e
)
SELECT
e.*,
d.*,
c.*
FROM RankedEvents AS e
LEFT JOIN Devices AS d ON e.eve_MAC = d.devMac
INNER JOIN CurrentScan AS c ON e.eve_MAC = c.scanMac
WHERE e.row_num = 1
/* LatestEventsPerMAC(eve_MAC,eve_IP,eve_DateTime,eve_EventType,eve_AdditionalInfo,eve_PendingAlertEmail,eve_PairEventRowid,row_num,devMac,devName,devOwner,devType,devVendor,devFavorite,devGroup,devComments,devFirstConnection,devLastConnection,devLastIP,devStaticIP,devScan,devLogEvents,devAlertEvents,devAlertDown,devSkipRepeated,devLastNotification,devPresentLastScan,devIsNew,devLocation,devIsArchived,devParentMAC,devParentPort,devIcon,devGUID,devSite,devSSID,devSyncHubNode,devSourcePlugin,devCustomProps,scanMac,scanLastIP,scanVendor,scanSourcePlugin,scanName,scanLastQuery,scanLastConnection,scanSyncHubNode,scanSite,scanSSID,scanParentMAC,scanParentPort,scanType) */;
CREATE VIEW Sessions_Devices AS SELECT * FROM Sessions LEFT JOIN "Devices" ON ses_MAC = devMac
/* Sessions_Devices(ses_MAC,ses_IP,ses_EventTypeConnection,ses_DateTimeConnection,ses_EventTypeDisconnection,ses_DateTimeDisconnection,ses_StillConnected,ses_AdditionalInfo,devMac,devName,devOwner,devType,devVendor,devFavorite,devGroup,devComments,devFirstConnection,devLastConnection,devLastIP,devStaticIP,devScan,devLogEvents,devAlertEvents,devAlertDown,devSkipRepeated,devLastNotification,devPresentLastScan,devIsNew,devLocation,devIsArchived,devParentMAC,devParentPort,devIcon,devGUID,devSite,devSSID,devSyncHubNode,devSourcePlugin,devCustomProps) */;
CREATE VIEW Convert_Events_to_Sessions AS SELECT EVE1.eve_MAC,
EVE1.eve_IP,
EVE1.eve_EventType AS eve_EventTypeConnection,
EVE1.eve_DateTime AS eve_DateTimeConnection,
CASE WHEN EVE2.eve_EventType IN ('Disconnected', 'Device Down') OR
EVE2.eve_EventType IS NULL THEN EVE2.eve_EventType ELSE '<missing event>' END AS eve_EventTypeDisconnection,
CASE WHEN EVE2.eve_EventType IN ('Disconnected', 'Device Down') THEN EVE2.eve_DateTime ELSE NULL END AS eve_DateTimeDisconnection,
CASE WHEN EVE2.eve_EventType IS NULL THEN 1 ELSE 0 END AS eve_StillConnected,
EVE1.eve_AdditionalInfo
FROM Events AS EVE1
LEFT JOIN
Events AS EVE2 ON EVE1.eve_PairEventRowID = EVE2.RowID
WHERE EVE1.eve_EventType IN ('New Device', 'Connected','Down Reconnected')
UNION
SELECT eve_MAC,
eve_IP,
'<missing event>' AS eve_EventTypeConnection,
NULL AS eve_DateTimeConnection,
eve_EventType AS eve_EventTypeDisconnection,
eve_DateTime AS eve_DateTimeDisconnection,
0 AS eve_StillConnected,
eve_AdditionalInfo
FROM Events AS EVE1
WHERE (eve_EventType = 'Device Down' OR
eve_EventType = 'Disconnected') AND
EVE1.eve_PairEventRowID IS NULL
/* Convert_Events_to_Sessions(eve_MAC,eve_IP,eve_EventTypeConnection,eve_DateTimeConnection,eve_EventTypeDisconnection,eve_DateTimeDisconnection,eve_StillConnected,eve_AdditionalInfo) */;
CREATE TRIGGER "trg_insert_devices"
AFTER INSERT ON "Devices"
WHEN NOT EXISTS (
SELECT 1 FROM AppEvents
WHERE AppEventProcessed = 0
AND ObjectType = 'Devices'
AND ObjectGUID = NEW.devGUID
AND ObjectStatus = CASE WHEN NEW.devPresentLastScan = 1 THEN 'online' ELSE 'offline' END
AND AppEventType = 'insert'
)
BEGIN
INSERT INTO "AppEvents" (
"GUID",
"DateTimeCreated",
"AppEventProcessed",
"ObjectType",
"ObjectGUID",
"ObjectPrimaryID",
"ObjectSecondaryID",
"ObjectStatus",
"ObjectStatusColumn",
"ObjectIsNew",
"ObjectIsArchived",
"ObjectForeignKey",
"ObjectPlugin",
"AppEventType"
)
VALUES (
lower(
hex(randomblob(4)) || '-' || hex(randomblob(2)) || '-' || '4' ||
substr(hex( randomblob(2)), 2) || '-' ||
substr('AB89', 1 + (abs(random()) % 4) , 1) ||
substr(hex(randomblob(2)), 2) || '-' ||
hex(randomblob(6))
)
,
DATETIME('now'),
FALSE,
'Devices',
NEW.devGUID, -- ObjectGUID
NEW.devMac, -- ObjectPrimaryID
NEW.devLastIP, -- ObjectSecondaryID
CASE WHEN NEW.devPresentLastScan = 1 THEN 'online' ELSE 'offline' END, -- ObjectStatus
'devPresentLastScan', -- ObjectStatusColumn
NEW.devIsNew, -- ObjectIsNew
NEW.devIsArchived, -- ObjectIsArchived
NEW.devGUID, -- ObjectForeignKey
'DEVICES', -- ObjectForeignKey
'insert'
);
END;
CREATE TRIGGER "trg_update_devices"
AFTER UPDATE ON "Devices"
WHEN NOT EXISTS (
SELECT 1 FROM AppEvents
WHERE AppEventProcessed = 0
AND ObjectType = 'Devices'
AND ObjectGUID = NEW.devGUID
AND ObjectStatus = CASE WHEN NEW.devPresentLastScan = 1 THEN 'online' ELSE 'offline' END
AND AppEventType = 'update'
)
BEGIN
INSERT INTO "AppEvents" (
"GUID",
"DateTimeCreated",
"AppEventProcessed",
"ObjectType",
"ObjectGUID",
"ObjectPrimaryID",
"ObjectSecondaryID",
"ObjectStatus",
"ObjectStatusColumn",
"ObjectIsNew",
"ObjectIsArchived",
"ObjectForeignKey",
"ObjectPlugin",
"AppEventType"
)
VALUES (
lower(
hex(randomblob(4)) || '-' || hex(randomblob(2)) || '-' || '4' ||
substr(hex( randomblob(2)), 2) || '-' ||
substr('AB89', 1 + (abs(random()) % 4) , 1) ||
substr(hex(randomblob(2)), 2) || '-' ||
hex(randomblob(6))
)
,
DATETIME('now'),
FALSE,
'Devices',
NEW.devGUID, -- ObjectGUID
NEW.devMac, -- ObjectPrimaryID
NEW.devLastIP, -- ObjectSecondaryID
CASE WHEN NEW.devPresentLastScan = 1 THEN 'online' ELSE 'offline' END, -- ObjectStatus
'devPresentLastScan', -- ObjectStatusColumn
NEW.devIsNew, -- ObjectIsNew
NEW.devIsArchived, -- ObjectIsArchived
NEW.devGUID, -- ObjectForeignKey
'DEVICES', -- ObjectForeignKey
'update'
);
END;
CREATE TRIGGER "trg_delete_devices"
AFTER DELETE ON "Devices"
WHEN NOT EXISTS (
SELECT 1 FROM AppEvents
WHERE AppEventProcessed = 0
AND ObjectType = 'Devices'
AND ObjectGUID = OLD.devGUID
AND ObjectStatus = CASE WHEN OLD.devPresentLastScan = 1 THEN 'online' ELSE 'offline' END
AND AppEventType = 'delete'
)
BEGIN
INSERT INTO "AppEvents" (
"GUID",
"DateTimeCreated",
"AppEventProcessed",
"ObjectType",
"ObjectGUID",
"ObjectPrimaryID",
"ObjectSecondaryID",
"ObjectStatus",
"ObjectStatusColumn",
"ObjectIsNew",
"ObjectIsArchived",
"ObjectForeignKey",
"ObjectPlugin",
"AppEventType"
)
VALUES (
lower(
hex(randomblob(4)) || '-' || hex(randomblob(2)) || '-' || '4' ||
substr(hex( randomblob(2)), 2) || '-' ||
substr('AB89', 1 + (abs(random()) % 4) , 1) ||
substr(hex(randomblob(2)), 2) || '-' ||
hex(randomblob(6))
)
,
DATETIME('now'),
FALSE,
'Devices',
OLD.devGUID, -- ObjectGUID
OLD.devMac, -- ObjectPrimaryID
OLD.devLastIP, -- ObjectSecondaryID
CASE WHEN OLD.devPresentLastScan = 1 THEN 'online' ELSE 'offline' END, -- ObjectStatus
'devPresentLastScan', -- ObjectStatusColumn
OLD.devIsNew, -- ObjectIsNew
OLD.devIsArchived, -- ObjectIsArchived
OLD.devGUID, -- ObjectForeignKey
'DEVICES', -- ObjectForeignKey
'delete'
);
END;

View File

@@ -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

View File

@@ -58,12 +58,12 @@ The Events API provides access to **device event logs**, allowing creation, retr
"success": true,
"events": [
{
"eve_MAC": "00:11:22:33:44:55",
"eve_IP": "192.168.1.10",
"eve_DateTime": "2025-08-24T12:00:00Z",
"eve_EventType": "Device Down",
"eve_AdditionalInfo": "",
"eve_PendingAlertEmail": 1
"eveMac": "00:11:22:33:44:55",
"eveIp": "192.168.1.10",
"eveDateTime": "2025-08-24T12:00:00Z",
"eveEventType": "Device Down",
"eveAdditionalInfo": "",
"evePendingAlertEmail": 1
}
]
}
@@ -102,11 +102,11 @@ The Events API provides access to **device event logs**, allowing creation, retr
"count": 5,
"events": [
{
"eve_DateTime": "2025-12-07 12:00:00",
"eve_EventType": "New Device",
"eve_MAC": "AA:BB:CC:DD:EE:FF",
"eve_IP": "192.168.1.100",
"eve_AdditionalInfo": "Device detected"
"eveDateTime": "2025-12-07 12:00:00",
"eveEventType": "New Device",
"eveMac": "AA:BB:CC:DD:EE:FF",
"eveIp": "192.168.1.100",
"eveAdditionalInfo": "Device detected"
}
]
}
@@ -127,9 +127,9 @@ The Events API provides access to **device event logs**, allowing creation, retr
"count": 10,
"events": [
{
"eve_DateTime": "2025-12-07 12:00:00",
"eve_EventType": "Device Down",
"eve_MAC": "AA:BB:CC:DD:EE:FF"
"eveDateTime": "2025-12-07 12:00:00",
"eveEventType": "Device Down",
"eveMac": "AA:BB:CC:DD:EE:FF"
}
]
}
@@ -159,9 +159,9 @@ The Events API provides access to **device event logs**, allowing creation, retr
1. Total events in the period
2. Total sessions
3. Missing sessions
4. Voided events (`eve_EventType LIKE 'VOIDED%'`)
5. New device events (`eve_EventType LIKE 'New Device'`)
6. Device down events (`eve_EventType LIKE 'Device Down'`)
4. Voided events (`eveEventType LIKE 'VOIDED%'`)
5. New device events (`eveEventType LIKE 'New Device'`)
6. Device down events (`eveEventType LIKE 'Device Down'`)
---
@@ -187,7 +187,7 @@ Event endpoints are available as **MCP Tools** for AI assistant integration:
```
* Events are stored in the **Events table** with the following fields:
`eve_MAC`, `eve_IP`, `eve_DateTime`, `eve_EventType`, `eve_AdditionalInfo`, `eve_PendingAlertEmail`.
`eveMac`, `eveIp`, `eveDateTime`, `eveEventType`, `eveAdditionalInfo`, `evePendingAlertEmail`.
* Event creation automatically logs activity for debugging.

View File

@@ -4,6 +4,10 @@ GraphQL queries are **read-optimized for speed**. Data may be slightly out of da
* Devices
* Settings
* Events
* PluginsObjects
* PluginsHistory
* PluginsEvents
* Language Strings (LangStrings)
## Endpoints
@@ -254,11 +258,160 @@ curl 'http://host:GRAPHQL_PORT/graphql' \
---
## Plugin Tables (Objects, Events, History)
Three queries expose the plugin database tables with server-side pagination, filtering, and search:
* `pluginsObjects` — current plugin object state
* `pluginsEvents` — unprocessed plugin events
* `pluginsHistory` — historical plugin event log
All three share the same `PluginQueryOptionsInput` and return the same `PluginEntry` shape.
### Sample Query
```graphql
query GetPluginObjects($options: PluginQueryOptionsInput) {
pluginsObjects(options: $options) {
dbCount
count
entries {
index plugin objectPrimaryId objectSecondaryId
dateTimeCreated dateTimeChanged
watchedValue1 watchedValue2 watchedValue3 watchedValue4
status extra userData foreignKey
syncHubNodeName helpVal1 helpVal2 helpVal3 helpVal4 objectGuid
}
}
}
```
### Query Parameters (`PluginQueryOptionsInput`)
| Parameter | Type | Description |
| ------------ | ----------------- | ------------------------------------------------------ |
| `page` | Int | Page number (1-based). |
| `limit` | Int | Rows per page (max 1000). |
| `sort` | [SortOptionsInput] | Sorting options (`field`, `order`). |
| `search` | String | Free-text search across key columns. |
| `filters` | [FilterOptionsInput] | Column-value exact-match filters. |
| `plugin` | String | Plugin prefix to scope results (e.g. `"ARPSCAN"`). |
| `foreignKey` | String | Foreign key filter (e.g. device MAC). |
| `dateFrom` | String | Start of date range filter on `dateTimeCreated`. |
| `dateTo` | String | End of date range filter on `dateTimeCreated`. |
### Response Fields
| Field | Type | Description |
| --------- | ------------- | ------------------------------------------------------------- |
| `dbCount` | Int | Total rows for the requested plugin (before search/filters). |
| `count` | Int | Total rows after all filters (before pagination). |
| `entries` | [PluginEntry] | Paginated list of plugin entries. |
### `curl` Example
```sh
curl 'http://host:GRAPHQL_PORT/graphql' \
-X POST \
-H 'Authorization: Bearer API_TOKEN' \
-H 'Content-Type: application/json' \
--data '{
"query": "query GetPluginObjects($options: PluginQueryOptionsInput) { pluginsObjects(options: $options) { dbCount count entries { index plugin objectPrimaryId status foreignKey } } }",
"variables": {
"options": {
"plugin": "ARPSCAN",
"page": 1,
"limit": 25
}
}
}'
```
### Badge Prefetch (Batched Counts)
Use GraphQL aliases to fetch counts for all plugins in a single request:
```graphql
query BadgeCounts {
ARPSCAN: pluginsObjects(options: {plugin: "ARPSCAN", page: 1, limit: 1}) { dbCount }
INTRNT: pluginsObjects(options: {plugin: "INTRNT", page: 1, limit: 1}) { dbCount }
}
```
---
## Events Query
Access the Events table with server-side pagination, filtering, and search.
### Sample Query
```graphql
query GetEvents($options: EventQueryOptionsInput) {
events(options: $options) {
dbCount
count
entries {
eveMac
eveIp
eveDateTime
eveEventType
eveAdditionalInfo
evePendingAlertEmail
}
}
}
```
### Query Parameters (`EventQueryOptionsInput`)
| Parameter | Type | Description |
| ----------- | ------------------ | ------------------------------------------------ |
| `page` | Int | Page number (1-based). |
| `limit` | Int | Rows per page (max 1000). |
| `sort` | [SortOptionsInput] | Sorting options (`field`, `order`). |
| `search` | String | Free-text search across key columns. |
| `filters` | [FilterOptionsInput] | Column-value exact-match filters. |
| `eveMac` | String | Filter by device MAC address. |
| `eventType` | String | Filter by event type (e.g. `"New Device"`). |
| `dateFrom` | String | Start of date range filter on `eveDateTime`. |
| `dateTo` | String | End of date range filter on `eveDateTime`. |
### Response Fields
| Field | Type | Description |
| --------- | ------------ | ------------------------------------------------------------ |
| `dbCount` | Int | Total rows in the Events table (before any filters). |
| `count` | Int | Total rows after all filters (before pagination). |
| `entries` | [EventEntry] | Paginated list of event entries. |
### `curl` Example
```sh
curl 'http://host:GRAPHQL_PORT/graphql' \
-X POST \
-H 'Authorization: Bearer API_TOKEN' \
-H 'Content-Type: application/json' \
--data '{
"query": "query GetEvents($options: EventQueryOptionsInput) { events(options: $options) { dbCount count entries { eveMac eveIp eveDateTime eveEventType } } }",
"variables": {
"options": {
"eveMac": "00:11:22:33:44:55",
"page": 1,
"limit": 50
}
}
}'
```
---
## Notes
* Device, settings, and LangStrings queries can be combined in **one request** since GraphQL supports batching.
* Device, settings, LangStrings, plugin, and event queries can be combined in **one request** since GraphQL supports batching.
* The `fallback_to_en` feature ensures UI always has a value even if a translation is missing.
* Data is **cached in memory** per JSON file; changes to language or plugin files will only refresh after the cache detects a file modification.
* The `setOverriddenByEnv` flag helps identify setting values that are locked at container runtime.
* The schema is **read-only** — updates must be performed through other APIs or configuration management. See the other [API](API.md) endpoints for details.
* Plugin queries scope `dbCount` to the requested `plugin`/`foreignKey` so badge counts reflect per-plugin totals.
* The schema is **read-only** — updates must be performed through other APIs or configuration management. See the other [API](API.md) endpoints for details.

View File

@@ -149,7 +149,7 @@ You can access the following files:
| File name | Description |
|----------------------|----------------------|
| `notification_json_final.json` | The json version of the last notification (e.g. used for webhooks - [sample JSON](https://github.com/jokob-sk/NetAlertX/blob/main/front/report_templates/webhook_json_sample.json)). |
| `notification_json_final.json` | The json version of the last notification (e.g. used for webhooks - [sample JSON](https://github.com/netalertx/NetAlertX/blob/main/front/report_templates/webhook_json_sample.json)). |
| `table_devices.json` | All of the available Devices detected by the app. |
| `table_plugins_events.json` | The list of the unprocessed (pending) notification events (plugins_events DB table). |
| `table_plugins_history.json` | The list of notification events history. |

View File

@@ -106,12 +106,12 @@ curl -X DELETE "http://<server_ip>:<GRAPHQL_PORT>/sessions/delete" \
"success": true,
"sessions": [
{
"ses_MAC": "AA:BB:CC:DD:EE:FF",
"ses_Connection": "2025-08-01 10:00",
"ses_Disconnection": "2025-08-01 12:00",
"ses_Duration": "2h 0m",
"ses_IP": "192.168.1.10",
"ses_Info": ""
"sesMac": "AA:BB:CC:DD:EE:FF",
"sesDateTimeConnection": "2025-08-01 10:00",
"sesDateTimeDisconnection": "2025-08-01 12:00",
"sesDuration": "2h 0m",
"sesIp": "192.168.1.10",
"sesAdditionalInfo": ""
}
]
}
@@ -194,12 +194,12 @@ curl -X GET "http://<server_ip>:<GRAPHQL_PORT>/sessions/calendar?start=2025-08-0
"success": true,
"sessions": [
{
"ses_MAC": "AA:BB:CC:DD:EE:FF",
"ses_Connection": "2025-08-01 10:00",
"ses_Disconnection": "2025-08-01 12:00",
"ses_Duration": "2h 0m",
"ses_IP": "192.168.1.10",
"ses_Info": ""
"sesMac": "AA:BB:CC:DD:EE:FF",
"sesDateTimeConnection": "2025-08-01 10:00",
"sesDateTimeDisconnection": "2025-08-01 12:00",
"sesDuration": "2h 0m",
"sesIp": "192.168.1.10",
"sesAdditionalInfo": ""
}
]
}
@@ -224,15 +224,33 @@ curl -X GET "http://<server_ip>:<GRAPHQL_PORT>/sessions/AA:BB:CC:DD:EE:FF?period
* `type` → Event type (`all`, `sessions`, `missing`, `voided`, `new`, `down`)
Default: `all`
* `period` → Period to retrieve events (`7 days`, `1 month`, etc.)
* `page` → Page number, 1-based (default: `1`)
* `limit` → Rows per page, max 1000 (default: `100`)
* `search` → Free-text search filter across all columns
* `sortCol` → Column index to sort by, 0-based (default: `0`)
* `sortDir` → Sort direction: `asc` or `desc` (default: `desc`)
**Example:**
```
/sessions/session-events?type=all&period=7 days
/sessions/session-events?type=all&period=7 days&page=1&limit=25&sortCol=3&sortDir=desc
```
**Response:**
Returns a list of events or sessions with formatted connection, disconnection, duration, and IP information.
```json
{
"data": [...],
"total": 150,
"recordsFiltered": 150
}
```
| Field | Type | Description |
| ----------------- | ---- | ------------------------------------------------- |
| `data` | list | Paginated rows (each row is a list of values). |
| `total` | int | Total rows before search filter. |
| `recordsFiltered` | int | Total rows after search filter (before paging). |
#### `curl` Example

View File

@@ -13,7 +13,7 @@ There are four key artifacts you can use to back up your NetAlertX configuration
| File | Description | Limitations |
| ------------------------ | ----------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------- |
| `/db/app.db` | The application database | Might be in an uncommitted state or corrupted |
| `/config/app.conf` | Configuration file | Can be overridden using the [`APP_CONF_OVERRIDE`](https://github.com/jokob-sk/NetAlertX/tree/main/dockerfiles#docker-environment-variables) variable |
| `/config/app.conf` | Configuration file | Can be overridden using the [`APP_CONF_OVERRIDE`](https://github.com/netalertx/NetAlertX/tree/main/dockerfiles#docker-environment-variables) variable |
| `/config/devices.csv` | CSV file containing device data | Does not include historical data |
| `/config/workflows.json` | JSON file containing your workflows | N/A |
@@ -37,7 +37,7 @@ This includes settings for:
### Device Data
Stored in `/data/config/devices_<timestamp>.csv` or `/data/config/devices.csv`, created by the [CSV Backup `CSVBCKP` Plugin](https://github.com/jokob-sk/NetAlertX/tree/main/front/plugins/csv_backup).
Stored in `/data/config/devices_<timestamp>.csv` or `/data/config/devices.csv`, created by the [CSV Backup `CSVBCKP` Plugin](https://github.com/netalertx/NetAlertX/tree/main/front/plugins/csv_backup).
Contains:
* Device names, icons, and categories

View File

@@ -1,7 +1,7 @@
# A high-level description of the database structure
An overview of the most important database tables as well as an detailed overview of the Devices table. The MAC address is used as a foreign key in most cases.
An overview of the most important database tables as well as an detailed overview of the Devices table. The MAC address is used as a foreign key in most cases.
## Devices database table
@@ -23,6 +23,7 @@
| `devLogEvents` | Whether events related to the device should be logged. | `0` |
| `devAlertEvents` | Whether alerts should be generated for events. | `1` |
| `devAlertDown` | Whether an alert should be sent when the device goes down. | `0` |
| `devCanSleep` | Whether the device can enter a sleep window. When `1`, offline periods within the `NTFPRCS_sleep_time` window are shown as **Sleeping** instead of **Down** and no down alert is fired. | `0` |
| `devSkipRepeated` | Whether to skip repeated alerts for this device. | `1` |
| `devLastNotification` | Timestamp of the last notification sent for this device. | `2025-03-22 12:07:26+11:00` |
| `devPresentLastScan` | Whether the device was present during the last scan. | `1` |
@@ -42,8 +43,14 @@
| `devParentRelType` | The type of relationship between the current device and it's parent node. By default, selecting `nic` will hide it from lists. | `nic` |
| `devReqNicsOnline` | If all NICs are required to be online to mark teh current device online. | `0` |
> [!NOTE]
> `DevicesView` extends the `Devices` table with two computed fields that are never persisted:
> - `devIsSleeping` (`1` when `devCanSleep=1`, device is offline, and `devLastConnection` is within the `NTFPRCS_sleep_time` window).
> - `devFlapping` (`1` when the device has changed state more than the flap threshold times in the trailing window).
> - `devStatus` — derived string: `On-line`, `Sleeping`, `Down`, or `Off-line`.
To understand how values of these fields influuence application behavior, such as Notifications or Network topology, see also:
To understand how values of these fields influuence application behavior, such as Notifications or Network topology, see also:
- [Device Management](./DEVICE_MANAGEMENT.md)
- [Network Tree Topology Setup](./NETWORK_TREE.md)
@@ -51,32 +58,32 @@ To understand how values of these fields influuence application behavior, such a
## Other Tables overview
| Table name | Description | Sample data |
|----------------------|----------------------| ----------------------|
| CurrentScan | Result of the current scan | ![Screen1][screen1] |
| Devices | The main devices database that also contains the Network tree mappings. If `ScanCycle` is set to `0` device is not scanned. | ![Screen2][screen2] |
| Events | Used to collect connection/disconnection events. | ![Screen4][screen4] |
| Online_History | Used to display the `Device presence` chart | ![Screen6][screen6] |
| Parameters | Used to pass values between the frontend and backend. | ![Screen7][screen7] |
| Plugins_Events | For capturing events exposed by a plugin via the `last_result.log` file. If unique then saved into the `Plugins_Objects` table. Entries are deleted once processed and stored in the `Plugins_History` and/or `Plugins_Objects` tables. | ![Screen10][screen10] |
| Plugins_History | History of all entries from the `Plugins_Events` table | ![Screen11][screen11] |
| Plugins_Language_Strings | Language strings collected from the plugin `config.json` files used for string resolution in the frontend. | ![Screen12][screen12] |
| Plugins_Objects | Unique objects detected by individual plugins. | ![Screen13][screen13] |
| Sessions | Used to display sessions in the charts | ![Screen15][screen15] |
| Settings | Database representation of the sum of all settings from `app.conf` and plugins coming from `config.json` files. | ![Screen16][screen16] |
|----------------------|----------------------| ----------------------|
| CurrentScan | Result of the current scan | ![Screen1][screen1] |
| Devices | The main devices database that also contains the Network tree mappings. If `ScanCycle` is set to `0` device is not scanned. | ![Screen2][screen2] |
| Events | Used to collect connection/disconnection events. | ![Screen4][screen4] |
| Online_History | Used to display the `Device presence` chart | ![Screen6][screen6] |
| Parameters | Used to pass values between the frontend and backend. | ![Screen7][screen7] |
| Plugins_Events | For capturing events exposed by a plugin via the `last_result.log` file. If unique then saved into the `Plugins_Objects` table. Entries are deleted once processed and stored in the `Plugins_History` and/or `Plugins_Objects` tables. | ![Screen10][screen10] |
| Plugins_History | History of all entries from the `Plugins_Events` table | ![Screen11][screen11] |
| Plugins_Language_Strings | Language strings collected from the plugin `config.json` files used for string resolution in the frontend. | ![Screen12][screen12] |
| Plugins_Objects | Unique objects detected by individual plugins. | ![Screen13][screen13] |
| Sessions | Used to display sessions in the charts | ![Screen15][screen15] |
| Settings | Database representation of the sum of all settings from `app.conf` and plugins coming from `config.json` files. | ![Screen16][screen16] |
[screen1]: ./img/DATABASE/CurrentScan.png
[screen2]: ./img/DATABASE/Devices.png
[screen4]: ./img/DATABASE/Events.png
[screen4]: ./img/DATABASE/Events.png
[screen6]: ./img/DATABASE/Online_History.png
[screen7]: ./img/DATABASE/Parameters.png
[screen10]: ./img/DATABASE/Plugins_Events.png
[screen11]: ./img/DATABASE/Plugins_History.png
[screen12]: ./img/DATABASE/Plugins_Language_Strings.png
[screen13]: ./img/DATABASE/Plugins_Objects.png
[screen13]: ./img/DATABASE/Plugins_Objects.png
[screen15]: ./img/DATABASE/Sessions.png
[screen16]: ./img/DATABASE/Settings.png

View File

@@ -43,7 +43,7 @@ Input data from the plugin might cause mapping issues in specific edge cases. Lo
17:31:05 [Scheduler] run for PIHOLE: YES
17:31:05 [Plugin utils] ---------------------------------------------
17:31:05 [Plugin utils] display_name: PiHole (Device sync)
17:31:05 [Plugins] CMD: SELECT n.hwaddr AS Object_PrimaryID, {s-quote}null{s-quote} AS Object_SecondaryID, datetime() AS DateTime, na.ip AS Watched_Value1, n.lastQuery AS Watched_Value2, na.name AS Watched_Value3, n.macVendor AS Watched_Value4, {s-quote}null{s-quote} AS Extra, n.hwaddr AS ForeignKey FROM EXTERNAL_PIHOLE.Network AS n LEFT JOIN EXTERNAL_PIHOLE.Network_Addresses AS na ON na.network_id = n.id WHERE n.hwaddr NOT LIKE {s-quote}ip-%{s-quote} AND n.hwaddr is not {s-quote}00:00:00:00:00:00{s-quote} AND na.ip is not null
17:31:05 [Plugins] CMD: SELECT n.hwaddr AS objectPrimaryId, {s-quote}null{s-quote} AS objectSecondaryId, datetime() AS DateTime, na.ip AS watchedValue1, n.lastQuery AS watchedValue2, na.name AS watchedValue3, n.macVendor AS watchedValue4, {s-quote}null{s-quote} AS Extra, n.hwaddr AS ForeignKey FROM EXTERNAL_PIHOLE.Network AS n LEFT JOIN EXTERNAL_PIHOLE.Network_Addresses AS na ON na.network_id = n.id WHERE n.hwaddr NOT LIKE {s-quote}ip-%{s-quote} AND n.hwaddr is not {s-quote}00:00:00:00:00:00{s-quote} AND na.ip is not null
17:31:05 [Plugins] setTyp: subnets
17:31:05 [Plugin utils] Flattening the below array
17:31:05 ['192.168.1.0/24 --interface=eth1']
@@ -52,7 +52,7 @@ Input data from the plugin might cause mapping issues in specific edge cases. Lo
17:31:05 [Plugins] Convert to Base64: True
17:31:05 [Plugins] base64 value: b'MTkyLjE2OC4xLjAvMjQgLS1pbnRlcmZhY2U9ZXRoMQ=='
17:31:05 [Plugins] Timeout: 10
17:31:05 [Plugins] Executing: SELECT n.hwaddr AS Object_PrimaryID, 'null' AS Object_SecondaryID, datetime() AS DateTime, na.ip AS Watched_Value1, n.lastQuery AS Watched_Value2, na.name AS Watched_Value3, n.macVendor AS Watched_Value4, 'null' AS Extra, n.hwaddr AS ForeignKey FROM EXTERNAL_PIHOLE.Network AS n LEFT JOIN EXTERNAL_PIHOLE.Network_Addresses AS na ON na.network_id = n.id WHERE n.hwaddr NOT LIKE 'ip-%' AND n.hwaddr is not '00:00:00:00:00:00' AND na.ip is not null
17:31:05 [Plugins] Executing: SELECT n.hwaddr AS objectPrimaryId, 'null' AS objectSecondaryId, datetime() AS DateTime, na.ip AS watchedValue1, n.lastQuery AS watchedValue2, na.name AS watchedValue3, n.macVendor AS watchedValue4, 'null' AS Extra, n.hwaddr AS ForeignKey FROM EXTERNAL_PIHOLE.Network AS n LEFT JOIN EXTERNAL_PIHOLE.Network_Addresses AS na ON na.network_id = n.id WHERE n.hwaddr NOT LIKE 'ip-%' AND n.hwaddr is not '00:00:00:00:00:00' AND na.ip is not null
🔻
17:31:05 [Plugins] SUCCESS, received 2 entries
17:31:05 [Plugins] sqlParam entries: [(0, 'PIHOLE', '01:01:01:01:01:01', 'null', 'null', '2023-12-25 06:31:05', '172.30.0.1', 0, 'aaaa', 'vvvvvvvvv', 'not-processed', 'null', 'null', '01:01:01:01:01:01'), (0, 'PIHOLE', '02:42:ac:1e:00:02', 'null', 'null', '2023-12-25 06:31:05', '172.30.0.2', 0, 'dddd', 'vvvvv2222', 'not-processed', 'null', 'null', '02:42:ac:1e:00:02')]

View File

@@ -38,7 +38,7 @@ If possible, check if your issue got fixed in the `_dev` image before opening a
> ⚠ Please backup your DB and config beforehand!
Please also search [open issues](https://github.com/jokob-sk/NetAlertX/issues).
Please also search [open issues](https://github.com/netalertx/NetAlertX/issues).
## 4. Disable restart behavior

View File

@@ -1,6 +1,6 @@
# Device Display Settings
This set of settings allows you to group Devices under different views. The Archived toggle allows you to exclude a Device from most listings and notifications.
This set of settings allows you to group Devices under different views. The Archived toggle allows you to exclude a Device from most listings and notifications.
![Display settings](./img/DEVICE_MANAGEMENT/DeviceDetails_DisplaySettings.png)
@@ -8,13 +8,17 @@ This set of settings allows you to group Devices under different views. The Arch
## Status Colors
![Sattus colors](./img/DEVICE_MANAGEMENT/device_management_status_colors.png)
1. 🔌 Online (Green) = A device that is no longer marked as a "New Device".
2. 🔌 New (Green) = A newly discovered device that is online and is still marked as a "New Device".
3. ✖ New (Grey) = Same as No.2 but device is now offline.
4. ✖ Offline (Grey) = A device that was not detected online in the last scan.
5. ⚠ Down (Red) = A device that has "Alert Down" marked and has been offline for the time set in the Setting `NTFPRCS_alert_down_time`.
| Icon | Status | Image | Description |
|-----------|------------------------|-----------------------------------------------------------------------|-----------------------------------------------------------------------------------------------|
| <i class="fa-solid fa-plug"></i> | Online (Green) | ![Status color - online](./img/DEVICE_MANAGEMENT/device_management_status_online.png) | A device that is no longer marked as a "New Device". |
| <i class="fa-solid fa-plug"></i> | New (Green) | ![Status color - new online](./img/DEVICE_MANAGEMENT/device_management_status_new_online.png) | A newly discovered device that is online and is still marked as a "New Device". |
| <i class="fa-solid fa-plug-circle-exclamation"></i> | Online (Orange) | ![Status color - flapping online](./img/DEVICE_MANAGEMENT/device_management_status_flapping_online.png) | The device is online, but unstable and flapping (3 status changes in the last hour). |
| <i class="fa-solid fa-xmark"></i> | New (Grey) | ![Status color - new offline](./img/DEVICE_MANAGEMENT/device_management_status_new_offline.png) | Same as "New (Green)" but the device is now offline. |
| <i class="fa-solid fa-box-archive"></i> | New (Grey) | ![Status color - new archived](./img/DEVICE_MANAGEMENT/device_management_status_archived_new.png) | Same as "New (Green)" but the device is now offline and archived. |
| <i class="fa-solid fa-xmark"></i> | Offline (Grey) | ![Status color - offline](./img/DEVICE_MANAGEMENT/device_management_status_offline.png) | A device that was not detected online in the last scan. |
| <i class="fa-solid fa-box-archive"></i> | Archived (Grey) | ![Status color - archived](./img/DEVICE_MANAGEMENT/device_management_status_archived.png) | A device that was not detected online in the last scan. |
| <i class="fa-solid fa-moon"></i> | Sleeping (Aqua) | ![Status color - sleeping](./img/DEVICE_MANAGEMENT/device_management_status_sleeping.png) | A device with **Can Sleep** enabled that has gone offline within the `NTFPRCS_sleep_time` window. No down alert is fired while the device is in this state. See [Notifications](./NOTIFICATIONS.md#device-settings). |
| <i class="fa-solid fa-triangle-exclamation"></i> | Down (Red) | ![Status color - down](./img/DEVICE_MANAGEMENT/device_management_status_down.png) | A device marked as "Alert Down" and offline for the duration set in `NTFPRCS_alert_down_time`.|
See also [Notification guide](./NOTIFICATIONS.md).

View File

@@ -77,7 +77,7 @@ Create a folder `netalertx` in the `APP_DATA_LOCATION` (in this example in `/vol
You can then modify the python script without restarting/rebuilding the container every time. Additionally, you can trigger a plugin run via the UI:
![image](https://github.com/jokob-sk/NetAlertX/assets/96159884/3cbf2748-03c8-49e7-b801-f38c7755246b)
![image](https://github.com/netalertx/NetAlertX/assets/96159884/3cbf2748-03c8-49e7-b801-f38c7755246b)
## Tips

View File

@@ -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

View File

@@ -1,6 +1,6 @@
[![Docker Size](https://img.shields.io/docker/image-size/jokobsk/netalertx?label=Size&logo=Docker&color=0aa8d2&logoColor=fff&style=for-the-badge)](https://hub.docker.com/r/jokobsk/netalertx)
[![Docker Pulls](https://img.shields.io/docker/pulls/jokobsk/netalertx?label=Pulls&logo=docker&color=0aa8d2&logoColor=fff&style=for-the-badge)](https://hub.docker.com/r/jokobsk/netalertx)
[![GitHub Release](https://img.shields.io/github/v/release/jokob-sk/NetAlertX?color=0aa8d2&logoColor=fff&logo=GitHub&style=for-the-badge)](https://github.com/jokob-sk/NetAlertX/releases)
[![GitHub Release](https://img.shields.io/github/v/release/jokob-sk/NetAlertX?color=0aa8d2&logoColor=fff&logo=GitHub&style=for-the-badge)](https://github.com/netalertx/NetAlertX/releases)
[![Discord](https://img.shields.io/discord/1274490466481602755?color=0aa8d2&logoColor=fff&logo=Discord&style=for-the-badge)](https://discord.gg/NczTUTWyRr)
[![Home Assistant](https://img.shields.io/badge/Repo-blue?logo=home-assistant&style=for-the-badge&color=0aa8d2&logoColor=fff&label=Add)](https://my.home-assistant.io/redirect/supervisor_add_addon_repository/?repository_url=https%3A%2F%2Fgithub.com%2Falexbelgium%2Fhassio-addons)
@@ -96,7 +96,7 @@ sudo chmod -R a+rwx /local_data_dir
### Initial setup
- If unavailable, the app generates a default `app.conf` and `app.db` file on the first run.
- The preferred way is to manage the configuration via the Settings section in the UI, if UI is inaccessible you can modify [app.conf](https://github.com/jokob-sk/NetAlertX/tree/main/back) in the `/data/config/` folder directly
- The preferred way is to manage the configuration via the Settings section in the UI, if UI is inaccessible you can modify [app.conf](https://github.com/netalertx/NetAlertX/tree/main/back) in the `/data/config/` folder directly
#### Setting up scanners
@@ -116,7 +116,7 @@ You can read or watch several [community configuration guides](https://docs.neta
#### Common issues
- Before creating a new issue, please check if a similar issue was [already resolved](https://github.com/jokob-sk/NetAlertX/issues?q=is%3Aissue+is%3Aclosed).
- Before creating a new issue, please check if a similar issue was [already resolved](https://github.com/netalertx/NetAlertX/issues?q=is%3Aissue+is%3Aclosed).
- Check also common issues and [debugging tips](https://docs.netalertx.com/DEBUG_TIPS).
## 💙 Support me

View File

@@ -77,6 +77,6 @@ After increasing the ARP timeout and adding ICMP scanning (on select IP ranges),
**Tip:** Each environment is unique. Consider fine-tuning scan settings based on your network size, device behavior, and desired detection accuracy.
Let us know in the [NetAlertX Discussions](https://github.com/jokob-sk/NetAlertX/discussions) if you have further feedback or edge cases.
Let us know in the [NetAlertX Discussions](https://github.com/netalertx/NetAlertX/discussions) if you have further feedback or edge cases.
See also [Remote Networks](./REMOTE_NETWORKS.md) for more advanced setups.

View File

@@ -1,4 +1,4 @@
# Frontend development
# Frontend development
This page contains tips for frontend development when extending NetAlertX. Guiding principles are:
@@ -7,17 +7,17 @@ This page contains tips for frontend development when extending NetAlertX. Guidi
3. Reusability
4. Placing more functionality into Plugins and enhancing core Plugins functionality
That means that, when writing code, focus on reusing what's available instead of writing quick fixes. Or creating reusable functions, instead of bespoke functionaility.
That means that, when writing code, focus on reusing what's available instead of writing quick fixes. Or creating reusable functions, instead of bespoke functionaility.
## 🔍 Examples
Some examples how to apply the above:
> Example 1
>
>
> I want to implement a scan fucntion. Options would be:
>
> 1. To add a manual scan functionality to the `deviceDetails.php` page.
> 1. To add a manual scan functionality to the `deviceDetails.php` page.
> 2. To create a separate page that handles the execution of the scan.
> 3. To create a configurable Plugin.
>
@@ -31,16 +31,16 @@ Some examples how to apply the above:
> 2. Implement the changes and add settings to influence the behavior in the `initialize.py` file so the user can adjust these.
> 3. Implement the changes and add settings via a setting-only plugin.
> 4. Implement the changes in a way so the behavior can be toggled on each plugin so the core capabilities of Plugins get extended.
>
>
> From the above, number 4 would be the most appropriate solution. Then followed by number 3. Number 1 or 2 would be approved only in special circumstances.
## 💡 Frontend tips
## 💡 Frontend tips
Some useful frontend JavaScript functions:
- `getDevDataByMac(macAddress, devicesColumn)` - method to retrieve any device data (database column) based on MAC address in the frontend
- `getString(string stringKey)` - method to retrieve translated strings in the frontend
- `getSetting(string stringKey)` - method to retrieve settings in the frontend
- `getDevDataByMac(macAddress, devicesColumn)` - method to retrieve any device data (database column) based on MAC address in the frontend
- `getString(string stringKey)` - method to retrieve translated strings in the frontend
- `getSetting(string stringKey)` - method to retrieve settings in the frontend
Check the [common.js](https://github.com/jokob-sk/NetAlertX/blob/main-2023-06-10/front/js/common.js) file for more frontend functions.
Check the [common.js](https://github.com/netalertx/NetAlertX/blob/main-2023-06-10/front/js/common.js) file for more frontend functions.

View File

@@ -4,7 +4,7 @@ This page provides an overview of community-contributed scripts for NetAlertX. T
## Community Scripts
You can find all scripts in this [scripts GitHub folder](https://github.com/jokob-sk/NetAlertX/tree/main/scripts).
You can find all scripts in this [scripts GitHub folder](https://github.com/netalertx/NetAlertX/tree/main/scripts).
| Script Name | Description | Author | Version | Release Date |
|------------|-------------|--------|---------|--------------|
@@ -17,5 +17,5 @@ You can find all scripts in this [scripts GitHub folder](https://github.com/joko
> [!NOTE]
> These scripts are community-supplied and not actively maintained. Use at your own discretion.
For detailed usage instructions, refer to each script's documentation in each [scripts GitHub folder](https://github.com/jokob-sk/NetAlertX/tree/main/scripts).
For detailed usage instructions, refer to each script's documentation in each [scripts GitHub folder](https://github.com/netalertx/NetAlertX/tree/main/scripts).

View File

@@ -5,11 +5,11 @@ NetAlertX comes with MQTT support, allowing you to show all detected devices as
> [!TIP]
> You can install NetAlertX also as a Home Assistant addon [![Home Assistant](https://img.shields.io/badge/Repo-blue?logo=home-assistant&style=for-the-badge&color=0aa8d2&logoColor=fff&label=Add)](https://my.home-assistant.io/redirect/supervisor_add_addon_repository/?repository_url=https%3A%2F%2Fgithub.com%2Falexbelgium%2Fhassio-addons) via the [alexbelgium/hassio-addons](https://github.com/alexbelgium/hassio-addons/) repository. This is only possible if you run a supervised instance of Home Assistant. If not, you can still run NetAlertX in a separate Docker container and follow this guide to configure MQTT.
## ⚠ Note
## ⚠ Note
- Please note that discovery takes about ~10s per device.
- Deleting of devices is not handled automatically. Please use [MQTT Explorer](https://mqtt-explorer.com/) to delete devices in the broker (Home Assistant), if needed.
- For optimization reasons, the devices are not always fully synchronized. You can delete Plugin objects as described in the [MQTT plugin](https://github.com/jokob-sk/NetAlertX/tree/main/front/plugins/_publisher_mqtt#forcing-an-update) docs to force a full synchronization.
- Deleting of devices is not handled automatically. Please use [MQTT Explorer](https://mqtt-explorer.com/) to delete devices in the broker (Home Assistant), if needed.
- For optimization reasons, the devices are not always fully synchronized. You can delete Plugin objects as described in the [MQTT plugin](https://github.com/netalertx/NetAlertX/tree/main/front/plugins/_publisher_mqtt#forcing-an-update) docs to force a full synchronization.
## 🧭 Guide
@@ -34,26 +34,26 @@ NetAlertX comes with MQTT support, allowing you to show all detected devices as
- Fill in remaining settings as per description
- set MQTT_RUN to schedule or on_notification depending on requirements
![Configuration Example][configuration]
![Configuration Example][configuration]
## 📷 Screenshots
| ![Screen 1][sensors] | ![Screen 2][history] |
|----------------------|----------------------|
| ![Screen 3][list] | ![Screen 4][overview] |
| ![Screen 1][sensors] | ![Screen 2][history] |
|----------------------|----------------------|
| ![Screen 3][list] | ![Screen 4][overview] |
[configuration]: ./img/HOME_ASISSTANT/HomeAssistant-Configuration.png "configuration"
[sensors]: ./img/HOME_ASISSTANT/HomeAssistant-Device-as-Sensors.png "sensors"
[history]: ./img/HOME_ASISSTANT/HomeAssistant-Device-Presence-History.png "history"
[list]: ./img/HOME_ASISSTANT/HomeAssistant-Devices-List.png "list"
[list]: ./img/HOME_ASISSTANT/HomeAssistant-Devices-List.png "list"
[overview]: ./img/HOME_ASISSTANT/HomeAssistant-Overview-Card.png "overview"
## Troubleshooting
If you can't see all devices detected, run `sudo arp-scan --interface=eth0 192.168.1.0/24` (change these based on your setup, read [Subnets](./SUBNETS.md) docs for details). This command has to be executed the NetAlertX container, not in the Home Assistant container.
You can access the NetAlertX container via Portainer on your host or via ssh. The container name will be something like `addon_db21ed7f_netalertx` (you can copy the `db21ed7f_netalertx` part from the browser when accessing the UI of NetAlertX).
You can access the NetAlertX container via Portainer on your host or via ssh. The container name will be something like `addon_db21ed7f_netalertx` (you can copy the `db21ed7f_netalertx` part from the browser when accessing the UI of NetAlertX).
## Accessing the NetAlertX container via SSH

View File

@@ -40,7 +40,7 @@ Some facts about what and where something will be changed/installed by the HW in
- **EXPERIMENTAL** and not recommended way to install NetAlertX.
> [!TIP]
> If the below fails try grabbing and installing one of the [previous releases](https://github.com/jokob-sk/NetAlertX/releases) and run the installation from the zip package.
> If the below fails try grabbing and installing one of the [previous releases](https://github.com/netalertx/NetAlertX/releases) and run the installation from the zip package.
These commands will download the `install.debian12.sh` script from the GitHub repository, make it executable with `chmod`, and then run it using `./install.debian12.sh`.

View File

@@ -102,7 +102,7 @@ Before opening a new issue:
* 📘 [Common Issues](./COMMON_ISSUES.md)
* 🧰 [Debugging Tips](./DEBUG_TIPS.md)
* ✅ [Browse resolved GitHub issues](https://github.com/jokob-sk/NetAlertX/issues?q=is%3Aissue+is%3Aclosed)
* ✅ [Browse resolved GitHub issues](https://github.com/netalertx/NetAlertX/issues?q=is%3Aissue+is%3Aclosed)
---

View File

@@ -17,10 +17,10 @@ If facing issues, please spend a few minutes searching.
- Check [common issues](./COMMON_ISSUES.md)
- Have a look at [Community guides](./COMMUNITY_GUIDES.md)
- [Search closed or open issues or discussions](https://github.com/jokob-sk/NetAlertX/issues?q=is%3Aissue)
- [Search closed or open issues or discussions](https://github.com/netalertx/NetAlertX/issues?q=is%3Aissue)
- Check [Discord](https://discord.gg/NczTUTWyRr)
> [!NOTE]
> If you can't find a solution anywhere, ask in Discord if you think it's a quick question, otherwise open a new [issue](https://github.com/jokob-sk/NetAlertX/issues/new?template=setup-help.yml). Please fill in as much as possible to speed up the help process.
> If you can't find a solution anywhere, ask in Discord if you think it's a quick question, otherwise open a new [issue](https://github.com/netalertx/NetAlertX/issues/new?template=setup-help.yml). Please fill in as much as possible to speed up the help process.
>

View File

@@ -1,5 +1,8 @@
# Notifications 📧
> [!TIP]
> Want to customize how devices appear in text notifications? See [Notification Text Templates](NOTIFICATION_TEMPLATES.md).
There are 4 ways how to influence notifications:
1. On the device itself
@@ -19,22 +22,23 @@ The following device properties influence notifications. You can:
1. **Alert Events** - Enables alerts of connections, disconnections, IP changes (down and down reconnected notifications are still sent even if this is disabled).
2. **Alert Down** - Alerts when a device goes down. This setting overrides a disabled **Alert Events** setting, so you will get a notification of a device going down even if you don't have **Alert Events** ticked. Disabling this will disable down and down reconnected notifications on the device.
3. **Skip repeated notifications**, if for example you know there is a temporary issue and want to pause the same notification for this device for a given time.
4. **Require NICs Online** - Indicates whether this device should be considered online only if all associated NICs (devices with the `nic` relationship type) are online. If disabled, the device is considered online if any NIC is online. If a NIC is online it sets the parent (this) device's status to online irrespectivelly of the detected device's status. The Relationship type is set on the childern device.
3. **Can Sleep** - Marks the device as sleep-capable (e.g. a battery-powered sensor that deep-sleeps between readings). When enabled, offline periods within the **Alert down after (sleep)** (`NTFPRCS_sleep_time`) global window are shown as **Sleeping** (aqua badge 🌙) instead of **Down**, and no down alert is fired during that window. Once the window expires the device falls back to normal down-alert logic. ⚠ Requires **Alert Down** to be enabled — sleeping suppresses the alert during the window only.
4. **Skip repeated notifications**, if for example you know there is a temporary issue and want to pause the same notification for this device for a given time.
5. **Require NICs Online** - Indicates whether this device should be considered online only if all associated NICs (devices with the `nic` relationship type) are online. If disabled, the device is considered online if any NIC is online. If a NIC is online it sets the parent (this) device's status to online irrespectivelly of the detected device's status. The Relationship type is set on the childern device.
> [!NOTE]
> Please read through the [NTFPRCS plugin](https://github.com/jokob-sk/NetAlertX/blob/main/front/plugins/notification_processing/README.md) documentation to understand how device and global settings influence the notification processing.
> Please read through the [NTFPRCS plugin](https://github.com/netalertx/NetAlertX/blob/main/front/plugins/notification_processing/README.md) documentation to understand how device and global settings influence the notification processing.
## Plugin settings 🔌
![Plugin notification settings](./img/NOTIFICATIONS/Plugin-notification-settings.png)
On almost all plugins there are 2 core settings, `<plugin>_WATCH` and `<plugin>_REPORT_ON`.
On almost all plugins there are 2 core settings, `<plugin>_WATCH` and `<plugin>_REPORT_ON`.
1. `<plugin>_WATCH` specifies the columns which the app should watch. If watched columns change the device state is considered changed. This changed status is then used to decide to send out notifications based on the `<plugin>_REPORT_ON` setting.
2. `<plugin>_REPORT_ON` let's you specify on which events the app should notify you. This is related to the `<plugin>_WATCH` setting. So if you select `watched-changed` and in `<plugin>_WATCH` you only select `Watched_Value1`, then a notification is triggered if `Watched_Value1` is changed from the previous value, but no notification is send if `Watched_Value2` changes.
1. `<plugin>_WATCH` specifies the columns which the app should watch. If watched columns change the device state is considered changed. This changed status is then used to decide to send out notifications based on the `<plugin>_REPORT_ON` setting.
2. `<plugin>_REPORT_ON` let's you specify on which events the app should notify you. This is related to the `<plugin>_WATCH` setting. So if you select `watched-changed` and in `<plugin>_WATCH` you only select `watchedValue1`, then a notification is triggered if `watchedValue1` is changed from the previous value, but no notification is send if `watchedValue2` changes.
Click the **Read more in the docs.** Link at the top of each plugin to get more details on how the given plugin works.
Click the **Read more in the docs.** Link at the top of each plugin to get more details on how the given plugin works.
## Global settings ⚙
@@ -42,10 +46,11 @@ Click the **Read more in the docs.** Link at the top of each plugin to get more
In Notification Processing settings, you can specify blanket rules. These allow you to specify exceptions to the Plugin and Device settings and will override those.
1. Notify on (`NTFPRCS_INCLUDED_SECTIONS`) allows you to specify which events trigger notifications. Usual setups will have `new_devices`, `down_devices`, and possibly `down_reconnected` set. Including `plugin` (dependenton the Plugin `<plugin>_WATCH` and `<plugin>_REPORT_ON` settings) and `events` (dependent on the on-device **Alert Events** setting) might be too noisy for most setups. More info in the [NTFPRCS plugin](https://github.com/jokob-sk/NetAlertX/blob/main/front/plugins/notification_processing/README.md) on what events these selections include.
1. Notify on (`NTFPRCS_INCLUDED_SECTIONS`) allows you to specify which events trigger notifications. Usual setups will have `new_devices`, `down_devices`, and possibly `down_reconnected` set. Including `plugin` (dependenton the Plugin `<plugin>_WATCH` and `<plugin>_REPORT_ON` settings) and `events` (dependent on the on-device **Alert Events** setting) might be too noisy for most setups. More info in the [NTFPRCS plugin](https://github.com/netalertx/NetAlertX/blob/main/front/plugins/notification_processing/README.md) on what events these selections include.
2. Alert down after (`NTFPRCS_alert_down_time`) is useful if you want to wait for some time before the system sends out a down notification for a device. This is related to the on-device **Alert down** setting and only devices with this checked will trigger a down notification.
3. Alert down after (sleep) (`NTFPRCS_sleep_time`) sets the **sleep window** in minutes. If a device has **Can Sleep** enabled and goes offline, it is shown as **Sleeping** (aqua 🌙 badge) for this many minutes before down-alert logic kicks in. Default is `30` minutes. Changing this setting takes effect after saving — no restart required.
You can filter out unwanted notifications globally. This could be because of a misbehaving device (GoogleNest/GoogleHub (See also [ARPSAN docs and the `--exclude-broadcast` flag](https://github.com/jokob-sk/NetAlertX/tree/main/front/plugins/arp_scan#ip-flipping-on-google-nest-devices))) which flips between IP addresses, or because you want to ignore new device notifications of a certain pattern.
You can filter out unwanted notifications globally. This could be because of a misbehaving device (GoogleNest/GoogleHub (See also [ARPSAN docs and the `--exclude-broadcast` flag](https://github.com/netalertx/NetAlertX/tree/main/front/plugins/arp_scan#ip-flipping-on-google-nest-devices))) which flips between IP addresses, or because you want to ignore new device notifications of a certain pattern.
1. Events Filter (`NTFPRCS_event_condition`) - Filter out Events from notifications.
2. New Devices Filter (`NTFPRCS_new_dev_condition`) - Filter out New Devices from notifications, but log and keep a new device in the system.
@@ -54,9 +59,9 @@ You can filter out unwanted notifications globally. This could be because of a m
![Ignoring new devices](./img/NOTIFICATIONS/NEWDEV_ignores.png)
You can completely ignore detected devices globally. This could be because your instance detects docker containers, you want to ignore devices from a specific manufacturer via MAC rules or you want to ignore devices on a specific IP range.
You can completely ignore detected devices globally. This could be because your instance detects docker containers, you want to ignore devices from a specific manufacturer via MAC rules or you want to ignore devices on a specific IP range.
1. Ignored MACs (`NEWDEV_ignored_MACs`) - List of MACs to ignore.
2. Ignored IPs (`NEWDEV_ignored_IPs`) - List of IPs to ignore.
2. Ignored IPs (`NEWDEV_ignored_IPs`) - List of IPs to ignore.

View File

@@ -0,0 +1,100 @@
# Notification Text Templates
> Customize how devices and events appear in **text** notifications (email previews, push notifications, Apprise messages).
By default, NetAlertX formats each device as a vertical list of `Header: Value` pairs. Text templates let you define a **single-line format per device** using `{FieldName}` placeholders — ideal for mobile notification previews and high-volume alerts.
HTML email tables are **not affected** by these templates.
## Quick Start
1. Go to **Settings → Notification Processing**.
2. Set a template string for the section you want to customize, e.g.:
- **Text Template: New Devices** → `{devName} ({eveMac}) - {eveIp}`
3. Save. The next notification will use your format.
**Before (default):**
```
🆕 New devices
---------
devName: MyPhone
eveMac: aa:bb:cc:dd:ee:ff
devVendor: Apple
eveIp: 192.168.1.42
eveDateTime: 2025-01-15 10:30:00
eveEventType: New Device
devComments:
```
**After (with template `{devName} ({eveMac}) - {eveIp}`):**
```
🆕 New devices
---------
MyPhone (aa:bb:cc:dd:ee:ff) - 192.168.1.42
```
## Settings Reference
| Setting | Type | Default | Description |
|---------|------|---------|-------------|
| `NTFPRCS_TEXT_SECTION_HEADERS` | Boolean | `true` | Show/hide section titles (e.g. `🆕 New devices \n---------`). |
| `NTFPRCS_TEXT_TEMPLATE_new_devices` | String | *(empty)* | Template for new device rows. |
| `NTFPRCS_TEXT_TEMPLATE_down_devices` | String | *(empty)* | Template for down device rows. |
| `NTFPRCS_TEXT_TEMPLATE_down_reconnected` | String | *(empty)* | Template for reconnected device rows. |
| `NTFPRCS_TEXT_TEMPLATE_events` | String | *(empty)* | Template for event rows. |
| `NTFPRCS_TEXT_TEMPLATE_plugins` | String | *(empty)* | Template for plugin event rows. |
When a template is **empty**, the section uses the original vertical `Header: Value` format (full backward compatibility).
## Template Syntax
Use `{FieldName}` to insert a value from the notification data. Field names are **case-sensitive** and must match the column names exactly.
```
{devName} ({eveMac}) connected at {eveDateTime}
```
- No loops, conditionals, or nesting — just simple string replacement.
- If a `{FieldName}` does not exist in the data, it is left as-is in the output (safe failure). For example, `{NonExistent}` renders literally as `{NonExistent}`.
## Variable Availability by Section
All four device sections (`new_devices`, `down_devices`, `down_reconnected`, `events`) share the same unified field names.
### `new_devices`, `down_devices`, `down_reconnected`, and `events`
| Variable | Description |
|----------|-------------|
| `{devName}` | Device display name |
| `{eveMac}` | Device MAC address |
| `{devVendor}` | Device vendor/manufacturer |
| `{eveIp}` | Device IP address |
| `{eveDateTime}` | Event timestamp |
| `{eveEventType}` | Type of event (e.g. `New Device`, `Connected`, `Device Down`) |
| `{devComments}` | Device comments |
**Example (new_devices/events):** `{devName} ({eveMac}) - {eveIp} [{eveEventType}]`
**Example (down_devices):** `{devName} ({eveMac}) {devVendor} - went down at {eveDateTime}`
**Example (down_reconnected):** `{devName} ({eveMac}) reconnected at {eveDateTime}`
### `plugins`
| Variable | Description |
|----------|-------------|
| `{plugin}` | Plugin code name |
| `{objectPrimaryId}` | Primary identifier of the object |
| `{objectSecondaryId}` | Secondary identifier |
| `{dateTimeChanged}` | Timestamp of change |
| `{watchedValue1}` | First watched value |
| `{watchedValue2}` | Second watched value |
| `{watchedValue3}` | Third watched value |
| `{watchedValue4}` | Fourth watched value |
| `{status}` | Plugin event status |
**Example:** `{plugin}: {objectPrimaryId} - {status}`
## Section Headers Toggle
Set **Text Section Headers** (`NTFPRCS_TEXT_SECTION_HEADERS`) to `false` to remove the section title separators from text notifications. This is useful when you want compact output without the `🆕 New devices \n---------` banners.

View File

@@ -48,6 +48,36 @@ Two plugins help maintain the systems performance:
---
## Database Performance Tuning
The application automatically maintains database performance as data accumulates. However, you can adjust settings to balance CPU usage, disk usage, and responsiveness.
### **WAL Size Tuning (Storage vs. CPU Tradeoff)**
The SQLite Write-Ahead Log (WAL) is a temporary file that grows during normal operation. On systems with constrained resources (NAS, Raspberry Pi), controlling WAL size is important.
**Setting:** **`PRAGMA_JOURNAL_SIZE_LIMIT`** (default: **50 MB**)
| Setting | Effect | Use Case |
|---------|--------|----------|
| **1020 MB** | Smaller storage footprint; more frequent disk operations | NAS with SD card (storage priority) |
| **50 MB** (default) | Balanced; recommended for most setups | General use |
| **75100 MB** | Smoother performance; larger WAL on disk | High-speed NAS or servers |
**Recommendation:** For NAS devices with SD cards, leave at default (50 MB) or increase slightly (75 MB). Avoid very low values (< 10 MB) as they cause frequent disk thrashing and CPU spikes.
### **Automatic Cleanup**
The DB cleanup plugin (`DBCLNP`) automatically optimizes query performance and trims old data:
- **Deletes old events** Controlled by `DAYS_TO_KEEP_EVENTS` (default: 90 days)
- **Trims plugin history** Keeps recent entries only (controlled by `PLUGINS_KEEP_HIST`)
- **Optimizes queries** Updates database statistics so queries remain fast
**If cleanup fails**, performance degrades quickly. Check **Maintenance → Logs** for errors. If you see frequent failures, increase the timeout (`DBCLNP_RUN_TIMEOUT`).
---
## Scan Frequency and Coverage
Frequent scans increase resource usage, network traffic, and database read/write cycles.

View File

@@ -17,7 +17,7 @@ To use this approach make sure the Web UI password in **Pi-hole** is set.
| `PIHOLEAPI_API_MAXCLIENTS` | Maximum number of devices to request from Pi-hole. Defaults are usually fine. | `500` |
| `PIHOLEAPI_FAKE_MAC` | Generate FAKE MAC from IP. | `False` |
Check the [PiHole API plugin readme](https://github.com/jokob-sk/NetAlertX/tree/main/front/plugins/pihole_api_scan/) for details and troubleshooting.
Check the [PiHole API plugin readme](https://github.com/netalertx/NetAlertX/tree/main/front/plugins/pihole_api_scan/) for details and troubleshooting.
### docker-compose changes
@@ -35,7 +35,7 @@ No changes needed
| `DHCPLSS_RUN_SCHD` | If you run multiple device scanner plugins, align the schedules of all plugins to the same value. | `*/5 * * * *` |
| `DHCPLSS_paths_to_check` | You need to map the value in this setting in the `docker-compose.yml` file. The in-container path must contain `pihole` so it's parsed correctly. | `['/etc/pihole/dhcp.leases']` |
Check the [DHCPLSS plugin readme](https://github.com/jokob-sk/NetAlertX/tree/main/front/plugins/dhcp_leases#overview) for details
Check the [DHCPLSS plugin readme](https://github.com/netalertx/NetAlertX/tree/main/front/plugins/dhcp_leases#overview) for details
### docker-compose changes
@@ -54,7 +54,7 @@ Check the [DHCPLSS plugin readme](https://github.com/jokob-sk/NetAlertX/tree/mai
| `PIHOLE_RUN_SCHD` | If you run multiple device scanner plugins, align the schedules of all plugins to the same value. | `*/5 * * * *` |
| `PIHOLE_DB_PATH` | You need to map the value in this setting in the `docker-compose.yml` file. | `/etc/pihole/pihole-FTL.db` |
Check the [PiHole plugin readme](https://github.com/jokob-sk/NetAlertX/tree/main/front/plugins/pihole_scan) for details
Check the [PiHole plugin readme](https://github.com/netalertx/NetAlertX/tree/main/front/plugins/pihole_scan) for details
### docker-compose changes

View File

@@ -1,18 +1,18 @@
# 🔌 Plugins
NetAlertX supports additional plugins to extend its functionality, each with its own settings and options. Plugins can be loaded via the General -> `LOADED_PLUGINS` setting. For custom plugin development, refer to the [Plugin development guide](./PLUGINS_DEV.md).
NetAlertX supports additional plugins to extend its functionality, each with its own settings and options. Plugins can be loaded via the General -> `LOADED_PLUGINS` setting. For custom plugin development, refer to the [Plugin development guide](./PLUGINS_DEV.md).
>[!NOTE]
> Please check this [Plugins debugging guide](./DEBUG_PLUGINS.md) and the corresponding Plugin documentation in the below table if you are facing issues.
> Please check this [Plugins debugging guide](./DEBUG_PLUGINS.md) and the corresponding Plugin documentation in the below table if you are facing issues.
## ⚡ Quick start
> [!TIP]
> You can load additional Plugins via the General -> `LOADED_PLUGINS` setting. You need to save the settings for the new plugins to load (cache/page reload may be necessary).
> You can load additional Plugins via the General -> `LOADED_PLUGINS` setting. You need to save the settings for the new plugins to load (cache/page reload may be necessary).
> ![Loaded plugins settings](./img/PLUGINS/enable_plugin.gif)
1. Pick your `🔍 dev scanner` plugin (e.g. `ARPSCAN` or `NMAPDEV`), or import devices into the application with an `📥 importer` plugin. (See **Enabling plugins** below)
2. Pick a `▶️ publisher` plugin, if you want to send notifications. If you don't see a publisher you'd like to use, look at the [📚_publisher_apprise](/front/plugins/_publisher_apprise/) plugin which is a proxy for over 80 notification services.
2. Pick a `▶️ publisher` plugin, if you want to send notifications. If you don't see a publisher you'd like to use, look at the [📚_publisher_apprise](/front/plugins/_publisher_apprise/) plugin which is a proxy for over 80 notification services.
3. Setup your [Network topology diagram](./NETWORK_TREE.md)
4. Fine-tune [Notifications](./NOTIFICATIONS.md)
5. Setup [Workflows](./WORKFLOWS.md)
@@ -40,56 +40,56 @@ NetAlertX supports additional plugins to extend its functionality, each with its
## Available Plugins
Device-detecting plugins insert values into the `CurrentScan` database table. The plugins that are not required are safe to ignore, however, it makes sense to have at least some device-detecting plugins enabled, such as `ARPSCAN` or `NMAPDEV`.
Device-detecting plugins insert values into the `CurrentScan` database table. The plugins that are not required are safe to ignore, however, it makes sense to have at least some device-detecting plugins enabled, such as `ARPSCAN` or `NMAPDEV`.
| ID | Plugin docs | Type | Description | Features | Required |
| --------------- | ------------------------------------------------------------------------------------------------------------------ | -------- | ----------------------------------------- | -------- | -------- |
| `APPRISE` | [_publisher_apprise](https://github.com/jokob-sk/NetAlertX/tree/main/front/plugins/_publisher_apprise/) | ▶️ | Apprise notification proxy | | |
| `ARPSCAN` | [arp_scan](https://github.com/jokob-sk/NetAlertX/tree/main/front/plugins/arp_scan/) | 🔍 | ARP-scan on current network | | |
| `AVAHISCAN` | [avahi_scan](https://github.com/jokob-sk/NetAlertX/tree/main/front/plugins/avahi_scan/) | 🆎 | Avahi (mDNS-based) name resolution | | |
| `ASUSWRT` | [asuswrt_import](https://github.com/jokob-sk/NetAlertX/tree/main/front/plugins/asuswrt_import/) | 🔍 | Import connected devices from AsusWRT | | |
| `CSVBCKP` | [csv_backup](https://github.com/jokob-sk/NetAlertX/tree/main/front/plugins/csv_backup/) | ⚙ | CSV devices backup | | |
| `CUSTPROP` | [custom_props](https://github.com/jokob-sk/NetAlertX/tree/main/front/plugins/custom_props/) | ⚙ | Managing custom device properties values | | Yes |
| `DBCLNP` | [db_cleanup](https://github.com/jokob-sk/NetAlertX/tree/main/front/plugins/db_cleanup/) | ⚙ | Database cleanup | | Yes\* |
| `DDNS` | [ddns_update](https://github.com/jokob-sk/NetAlertX/tree/main/front/plugins/ddns_update/) | ⚙ | DDNS update | | |
| `DHCPLSS` | [dhcp_leases](https://github.com/jokob-sk/NetAlertX/tree/main/front/plugins/dhcp_leases/) | 🔍/📥/🆎 | Import devices from DHCP leases | | |
| `DHCPSRVS` | [dhcp_servers](https://github.com/jokob-sk/NetAlertX/tree/main/front/plugins/dhcp_servers/) | ♻ | DHCP servers | | |
| `DIGSCAN` | [dig_scan](https://github.com/jokob-sk/NetAlertX/tree/main/front/plugins/dig_scan/) | 🆎 | Dig (DNS) Name resolution | | |
| `FREEBOX` | [freebox](https://github.com/jokob-sk/NetAlertX/tree/main/front/plugins/freebox/) | 🔍/♻/🆎 | Pull data and names from Freebox/Iliadbox | | |
| `ICMP` | [icmp_scan](https://github.com/jokob-sk/NetAlertX/tree/main/front/plugins/icmp_scan/) | ♻ | ICMP (ping) status checker | | |
| `INTRNT` | [internet_ip](https://github.com/jokob-sk/NetAlertX/tree/main/front/plugins/internet_ip/) | 🔍 | Internet IP scanner | | |
| `INTRSPD` | [internet_speedtest](https://github.com/jokob-sk/NetAlertX/tree/main/front/plugins/internet_speedtest/) | ♻ | Internet speed test | | |
| `IPNEIGH` | [ipneigh](https://github.com/jokob-sk/NetAlertX/tree/main/front/plugins/ipneigh/) | 🔍 | Scan ARP (IPv4) and NDP (IPv6) tables | | |
| `LUCIRPC` | [luci_import](https://github.com/jokob-sk/NetAlertX/tree/main/front/plugins/luci_import/) | 🔍 | Import connected devices from OpenWRT | | |
| `MAINT` | [maintenance](https://github.com/jokob-sk/NetAlertX/tree/main/front/plugins/maintenance/) | ⚙ | Maintenance of logs, etc. | | |
| `MQTT` | [_publisher_mqtt](https://github.com/jokob-sk/NetAlertX/tree/main/front/plugins/_publisher_mqtt/) | ▶️ | MQTT for synching to Home Assistant | | |
| `MTSCAN` | [mikrotik_scan](https://github.com/jokob-sk/NetAlertX/tree/main/front/plugins/mikrotik_scan/) | 🔍 | Mikrotik device import & sync | | |
| `NBTSCAN` | [nbtscan_scan](https://github.com/jokob-sk/NetAlertX/tree/main/front/plugins/nbtscan_scan/) | 🆎 | Nbtscan (NetBIOS-based) name resolution | | |
| `NEWDEV` | [newdev_template](https://github.com/jokob-sk/NetAlertX/tree/main/front/plugins/newdev_template/) | ⚙ | New device template | | Yes |
| `NMAP` | [nmap_scan](https://github.com/jokob-sk/NetAlertX/tree/main/front/plugins/nmap_scan/) | ♻ | Nmap port scanning & discovery | | |
| `NMAPDEV` | [nmap_dev_scan](https://github.com/jokob-sk/NetAlertX/tree/main/front/plugins/nmap_dev_scan/) | 🔍 | Nmap dev scan on current network | | |
| `NSLOOKUP` | [nslookup_scan](https://github.com/jokob-sk/NetAlertX/tree/main/front/plugins/nslookup_scan/) | 🆎 | NSLookup (DNS-based) name resolution | | |
| `NTFPRCS` | [notification_processing](https://github.com/jokob-sk/NetAlertX/tree/main/front/plugins/notification_processing/) | ⚙ | Notification processing | | Yes |
| `NTFY` | [_publisher_ntfy](https://github.com/jokob-sk/NetAlertX/tree/main/front/plugins/_publisher_ntfy/) | ▶️ | NTFY notifications | | |
| `OMDSDN` | [omada_sdn_imp](https://github.com/jokob-sk/NetAlertX/tree/main/front/plugins/omada_sdn_imp/) | 📥/🆎 ❌ | UNMAINTAINED use `OMDSDNOPENAPI` | 🖧 🔄 | |
| `OMDSDNOPENAPI` | [omada_sdn_openapi](https://github.com/jokob-sk/NetAlertX/tree/main/front/plugins/omada_sdn_openapi/) | 📥/🆎 | OMADA TP-Link import via OpenAPI | 🖧 | |
| `PIHOLE` | [pihole_scan](https://github.com/jokob-sk/NetAlertX/tree/main/front/plugins/pihole_scan/) | 🔍/🆎/📥 | Pi-hole device import & sync | | |
| `PIHOLEAPI` | [pihole_api_scan](https://github.com/jokob-sk/NetAlertX/tree/main/front/plugins/pihole_api_scan/) | 🔍/🆎/📥 | Pi-hole device import & sync via API v6+ | | |
| `PUSHSAFER` | [_publisher_pushsafer](https://github.com/jokob-sk/NetAlertX/tree/main/front/plugins/_publisher_pushsafer/) | ▶️ | Pushsafer notifications | | |
| `PUSHOVER` | [_publisher_pushover](https://github.com/jokob-sk/NetAlertX/tree/main/front/plugins/_publisher_pushover/) | ▶️ | Pushover notifications | | |
| `SETPWD` | [set_password](https://github.com/jokob-sk/NetAlertX/tree/main/front/plugins/set_password/) | ⚙ | Set password | | Yes |
| `SMTP` | [_publisher_email](https://github.com/jokob-sk/NetAlertX/tree/main/front/plugins/_publisher_email/) | ▶️ | Email notifications | | |
| `SNMPDSC` | [snmp_discovery](https://github.com/jokob-sk/NetAlertX/tree/main/front/plugins/snmp_discovery/) | 🔍/📥 | SNMP device import & sync | | |
| `SYNC` | [sync](https://github.com/jokob-sk/NetAlertX/tree/main/front/plugins/sync/) | 🔍/⚙/📥 | Sync & import from NetAlertX instances | 🖧 🔄 | Yes |
| `TELEGRAM` | [_publisher_telegram](https://github.com/jokob-sk/NetAlertX/tree/main/front/plugins/_publisher_telegram/) | ▶️ | Telegram notifications | | |
| `UI` | [ui_settings](https://github.com/jokob-sk/NetAlertX/tree/main/front/plugins/ui_settings/) | ♻ | UI specific settings | | Yes |
| `UNFIMP` | [unifi_import](https://github.com/jokob-sk/NetAlertX/tree/main/front/plugins/unifi_import/) | 🔍/📥/🆎 | UniFi device import & sync | 🖧 | |
| `UNIFIAPI` | [unifi_api_import](https://github.com/jokob-sk/NetAlertX/tree/main/front/plugins/unifi_api_import/) | 🔍/📥/🆎 | UniFi device import (SM API, multi-site) | | |
| `VNDRPDT` | [vendor_update](https://github.com/jokob-sk/NetAlertX/tree/main/front/plugins/vendor_update/) | ⚙ | Vendor database update | | |
| `WEBHOOK` | [_publisher_webhook](https://github.com/jokob-sk/NetAlertX/tree/main/front/plugins/_publisher_webhook/) | ▶️ | Webhook notifications | | |
| `WEBMON` | [website_monitor](https://github.com/jokob-sk/NetAlertX/tree/main/front/plugins/website_monitor/) | ♻ | Website down monitoring | | |
| `WOL` | [wake_on_lan](https://github.com/jokob-sk/NetAlertX/tree/main/front/plugins/wake_on_lan/) | ♻ | Automatic wake-on-lan | | |
| `APPRISE` | [_publisher_apprise](https://github.com/netalertx/NetAlertX/tree/main/front/plugins/_publisher_apprise/) | ▶️ | Apprise notification proxy | | |
| `ARPSCAN` | [arp_scan](https://github.com/netalertx/NetAlertX/tree/main/front/plugins/arp_scan/) | 🔍 | ARP-scan on current network | | |
| `AVAHISCAN` | [avahi_scan](https://github.com/netalertx/NetAlertX/tree/main/front/plugins/avahi_scan/) | 🆎 | Avahi (mDNS-based) name resolution | | |
| `ASUSWRT` | [asuswrt_import](https://github.com/netalertx/NetAlertX/tree/main/front/plugins/asuswrt_import/) | 🔍 | Import connected devices from AsusWRT | | |
| `CSVBCKP` | [csv_backup](https://github.com/netalertx/NetAlertX/tree/main/front/plugins/csv_backup/) | ⚙ | CSV devices backup | | |
| `CUSTPROP` | [custom_props](https://github.com/netalertx/NetAlertX/tree/main/front/plugins/custom_props/) | ⚙ | Managing custom device properties values | | Yes |
| `DBCLNP` | [db_cleanup](https://github.com/netalertx/NetAlertX/tree/main/front/plugins/db_cleanup/) | ⚙ | Database cleanup | | Yes\* |
| `DDNS` | [ddns_update](https://github.com/netalertx/NetAlertX/tree/main/front/plugins/ddns_update/) | ⚙ | DDNS update | | |
| `DHCPLSS` | [dhcp_leases](https://github.com/netalertx/NetAlertX/tree/main/front/plugins/dhcp_leases/) | 🔍/📥/🆎 | Import devices from DHCP leases | | |
| `DHCPSRVS` | [dhcp_servers](https://github.com/netalertx/NetAlertX/tree/main/front/plugins/dhcp_servers/) | ♻ | DHCP servers | | |
| `DIGSCAN` | [dig_scan](https://github.com/netalertx/NetAlertX/tree/main/front/plugins/dig_scan/) | 🆎 | Dig (DNS) Name resolution | | |
| `FREEBOX` | [freebox](https://github.com/netalertx/NetAlertX/tree/main/front/plugins/freebox/) | 🔍/♻/🆎 | Pull data and names from Freebox/Iliadbox | | |
| `ICMP` | [icmp_scan](https://github.com/netalertx/NetAlertX/tree/main/front/plugins/icmp_scan/) | ♻ | ICMP (ping) status checker | | |
| `INTRNT` | [internet_ip](https://github.com/netalertx/NetAlertX/tree/main/front/plugins/internet_ip/) | 🔍 | Internet IP scanner | | |
| `INTRSPD` | [internet_speedtest](https://github.com/netalertx/NetAlertX/tree/main/front/plugins/internet_speedtest/) | ♻ | Internet speed test | | |
| `IPNEIGH` | [ipneigh](https://github.com/netalertx/NetAlertX/tree/main/front/plugins/ipneigh/) | 🔍 | Scan ARP (IPv4) and NDP (IPv6) tables | | |
| `LUCIRPC` | [luci_import](https://github.com/netalertx/NetAlertX/tree/main/front/plugins/luci_import/) | 🔍 | Import connected devices from OpenWRT | | |
| `MAINT` | [maintenance](https://github.com/netalertx/NetAlertX/tree/main/front/plugins/maintenance/) | ⚙ | Maintenance of logs, etc. | | |
| `MQTT` | [_publisher_mqtt](https://github.com/netalertx/NetAlertX/tree/main/front/plugins/_publisher_mqtt/) | ▶️ | MQTT for synching to Home Assistant | | |
| `MTSCAN` | [mikrotik_scan](https://github.com/netalertx/NetAlertX/tree/main/front/plugins/mikrotik_scan/) | 🔍 | Mikrotik device import & sync | | |
| `NBTSCAN` | [nbtscan_scan](https://github.com/netalertx/NetAlertX/tree/main/front/plugins/nbtscan_scan/) | 🆎 | Nbtscan (NetBIOS-based) name resolution | | |
| `NEWDEV` | [newdev_template](https://github.com/netalertx/NetAlertX/tree/main/front/plugins/newdev_template/) | ⚙ | New device template | | Yes |
| `NMAP` | [nmap_scan](https://github.com/netalertx/NetAlertX/tree/main/front/plugins/nmap_scan/) | ♻ | Nmap port scanning & discovery | | |
| `NMAPDEV` | [nmap_dev_scan](https://github.com/netalertx/NetAlertX/tree/main/front/plugins/nmap_dev_scan/) | 🔍 | Nmap dev scan on current network | | |
| `NSLOOKUP` | [nslookup_scan](https://github.com/netalertx/NetAlertX/tree/main/front/plugins/nslookup_scan/) | 🆎 | NSLookup (DNS-based) name resolution | | |
| `NTFPRCS` | [notification_processing](https://github.com/netalertx/NetAlertX/tree/main/front/plugins/notification_processing/) | ⚙ | Notification processing | | Yes |
| `NTFY` | [_publisher_ntfy](https://github.com/netalertx/NetAlertX/tree/main/front/plugins/_publisher_ntfy/) | ▶️ | NTFY notifications | | |
| `OMDSDN` | [omada_sdn_imp](https://github.com/netalertx/NetAlertX/tree/main/front/plugins/omada_sdn_imp/) | 📥/🆎 ❌ | UNMAINTAINED use `OMDSDNOPENAPI` | 🖧 🔄 | |
| `OMDSDNOPENAPI` | [omada_sdn_openapi](https://github.com/netalertx/NetAlertX/tree/main/front/plugins/omada_sdn_openapi/) | 📥/🆎 | OMADA TP-Link import via OpenAPI | 🖧 | |
| `PIHOLE` | [pihole_scan](https://github.com/netalertx/NetAlertX/tree/main/front/plugins/pihole_scan/) | 🔍/🆎/📥 | Pi-hole device import & sync | | |
| `PIHOLEAPI` | [pihole_api_scan](https://github.com/netalertx/NetAlertX/tree/main/front/plugins/pihole_api_scan/) | 🔍/🆎/📥 | Pi-hole device import & sync via API v6+ | | |
| `PUSHSAFER` | [_publisher_pushsafer](https://github.com/netalertx/NetAlertX/tree/main/front/plugins/_publisher_pushsafer/) | ▶️ | Pushsafer notifications | | |
| `PUSHOVER` | [_publisher_pushover](https://github.com/netalertx/NetAlertX/tree/main/front/plugins/_publisher_pushover/) | ▶️ | Pushover notifications | | |
| `SETPWD` | [set_password](https://github.com/netalertx/NetAlertX/tree/main/front/plugins/set_password/) | ⚙ | Set password | | Yes |
| `SMTP` | [_publisher_email](https://github.com/netalertx/NetAlertX/tree/main/front/plugins/_publisher_email/) | ▶️ | Email notifications | | |
| `SNMPDSC` | [snmp_discovery](https://github.com/netalertx/NetAlertX/tree/main/front/plugins/snmp_discovery/) | 🔍/📥 | SNMP device import & sync | | |
| `SYNC` | [sync](https://github.com/netalertx/NetAlertX/tree/main/front/plugins/sync/) | 🔍/⚙/📥 | Sync & import from NetAlertX instances | 🖧 🔄 | Yes |
| `TELEGRAM` | [_publisher_telegram](https://github.com/netalertx/NetAlertX/tree/main/front/plugins/_publisher_telegram/) | ▶️ | Telegram notifications | | |
| `UI` | [ui_settings](https://github.com/netalertx/NetAlertX/tree/main/front/plugins/ui_settings/) | ♻ | UI specific settings | | Yes |
| `UNFIMP` | [unifi_import](https://github.com/netalertx/NetAlertX/tree/main/front/plugins/unifi_import/) | 🔍/📥/🆎 | UniFi device import & sync | 🖧 | |
| `UNIFIAPI` | [unifi_api_import](https://github.com/netalertx/NetAlertX/tree/main/front/plugins/unifi_api_import/) | 🔍/📥/🆎 | UniFi device import (SM API, multi-site) | | |
| `VNDRPDT` | [vendor_update](https://github.com/netalertx/NetAlertX/tree/main/front/plugins/vendor_update/) | ⚙ | Vendor database update | | |
| `WEBHOOK` | [_publisher_webhook](https://github.com/netalertx/NetAlertX/tree/main/front/plugins/_publisher_webhook/) | ▶️ | Webhook notifications | | |
| `WEBMON` | [website_monitor](https://github.com/netalertx/NetAlertX/tree/main/front/plugins/website_monitor/) | ♻ | Website down monitoring | | |
| `WOL` | [wake_on_lan](https://github.com/netalertx/NetAlertX/tree/main/front/plugins/wake_on_lan/) | ♻ | Automatic wake-on-lan | | |
> \* The database cleanup plugin (`DBCLNP`) is not _required_ but the app will become unusable after a while if not executed.
@@ -100,18 +100,18 @@ Device-detecting plugins insert values into the `CurrentScan` database table. T
## Enabling plugins
Plugins can be enabled via Settings, and can be disabled as needed.
Plugins can be enabled via Settings, and can be disabled as needed.
1. Research which plugin you'd like to use, enable `DISCOVER_PLUGINS` and load the required plugins in Settings via the `LOADED_PLUGINS` setting.
1. Save the changes and review the Settings of the newly loaded plugins.
1. Change the `<prefix>_RUN` Setting to the recommended or custom value as per the documentation of the given setting
1. Save the changes and review the Settings of the newly loaded plugins.
1. Change the `<prefix>_RUN` Setting to the recommended or custom value as per the documentation of the given setting
- If using `schedule` on a `🔍 dev scanner` plugin, make sure the schedules are the same across all `🔍 dev scanner` plugins
### Disabling, Unloading and Ignoring plugins
1. Change the `<prefix>_RUN` Setting to `disabled` if you want to disable the plugin, but keep the settings
1. If you want to speed up the application, you can unload the plugin by unselecting it in the `LOADED_PLUGINS` setting.
- Careful, once you save the Settings Unloaded plugin settings will be lost (old `app.conf` files are kept in the `/config` folder)
- Careful, once you save the Settings Unloaded plugin settings will be lost (old `app.conf` files are kept in the `/config` folder)
1. You can completely ignore plugins by placing a `ignore_plugin` file into the plugin directory. Ignored plugins won't show up in the `LOADED_PLUGINS` setting.
## 🆕 Developing new custom plugins

View File

@@ -34,7 +34,7 @@ NetAlertX comes with a plugin system to feed events from third-party scripts int
### 🐛 Troubleshooting
- **[Debugging Plugins](DEBUG_PLUGINS.md)** - Troubleshoot plugin issues
- **[Plugin Examples](../front/plugins)** - Study existing plugins as reference implementations
- **[Plugin Examples](https://github.com/netalertx/NetAlertX/tree/main/front/plugins)** - Study existing plugins as reference implementations
### 🎥 Video Tutorial
@@ -179,13 +179,13 @@ Quick reference:
| Column | Name | Required | Example |
|--------|------|----------|---------|
| 0 | Object_PrimaryID | **YES** | `"device_name"` or `"192.168.1.1"` |
| 1 | Object_SecondaryID | no | `"secondary_id"` or `null` |
| 0 | objectPrimaryId | **YES** | `"device_name"` or `"192.168.1.1"` |
| 1 | objectSecondaryId | no | `"secondary_id"` or `null` |
| 2 | DateTime | **YES** | `"2023-01-02 15:56:30"` |
| 3 | Watched_Value1 | **YES** | `"online"` or `"200"` |
| 4 | Watched_Value2 | no | `"ip_address"` or `null` |
| 5 | Watched_Value3 | no | `null` |
| 6 | Watched_Value4 | no | `null` |
| 3 | watchedValue1 | **YES** | `"online"` or `"200"` |
| 4 | watchedValue2 | no | `"ip_address"` or `null` |
| 5 | watchedValue3 | no | `null` |
| 6 | watchedValue4 | no | `null` |
| 7 | Extra | no | `"additional data"` or `null` |
| 8 | ForeignKey | no | `"aa:bb:cc:dd:ee:ff"` or `null` |
@@ -243,7 +243,7 @@ Control which rows display in the UI:
{
"data_filters": [
{
"compare_column": "Object_PrimaryID",
"compare_column": "objectPrimaryId",
"compare_operator": "==",
"compare_field_id": "txtMacFilter",
"compare_js_template": "'{value}'.toString()",
@@ -267,7 +267,7 @@ To import plugin data into NetAlertX tables for device discovery or notification
"mapped_to_table": "CurrentScan",
"database_column_definitions": [
{
"column": "Object_PrimaryID",
"column": "objectPrimaryId",
"mapped_to_column": "scanMac",
"show": true,
"type": "device_mac",
@@ -345,7 +345,7 @@ See [PLUGINS_DEV_SETTINGS.md](PLUGINS_DEV_SETTINGS.md) for complete settings doc
### Plugin Output Format
```
Object_PrimaryID|Object_SecondaryID|DateTime|Watched_Value1|Watched_Value2|Watched_Value3|Watched_Value4|Extra|ForeignKey
objectPrimaryId|objectSecondaryId|DateTime|watchedValue1|watchedValue2|watchedValue3|watchedValue4|Extra|ForeignKey
```
9 required columns, 4 optional helpers = 13 max

View File

@@ -77,7 +77,7 @@ It also describes plugin output expectations and the main plugin categories.
* `database_column_definitions`
* `mapped_to_table`
**Example:** `Object_PrimaryID → devMAC`
**Example:** `objectPrimaryId → devMAC`
---
@@ -88,9 +88,9 @@ Output values are pipe-delimited in a fixed order.
#### Identifiers
* `Object_PrimaryID` and `Object_SecondaryID` uniquely identify records (for example, `MAC|IP`).
* `objectPrimaryId` and `objectSecondaryId` uniquely identify records (for example, `MAC|IP`).
#### Watched Values (`Watched_Value14`)
#### Watched Values (`watchedValue14`)
* Used by the core to detect changes between runs.
* Changes in these fields can trigger notifications.
@@ -114,7 +114,7 @@ Output values are pipe-delimited in a fixed order.
### 7. Persistence
* Parsed data is **upserted** into the database.
* Conflicts are resolved using the combined key: `Object_PrimaryID + Object_SecondaryID`.
* Conflicts are resolved using the combined key: `objectPrimaryId + objectSecondaryId`.
---

View File

@@ -107,7 +107,7 @@ Query the NetAlertX SQLite database and display results.
{
"function": "CMD",
"type": {"dataType": "string", "elements": [{"elementType": "input", "elementOptions": [], "transformers": []}]},
"default_value": "SELECT dv.devName as Object_PrimaryID, cast(dv.devLastIP as VARCHAR(100)) || ':' || cast(SUBSTR(ns.Port, 0, INSTR(ns.Port, '/')) as VARCHAR(100)) as Object_SecondaryID, datetime() as DateTime, ns.Service as Watched_Value1, ns.State as Watched_Value2, null as Watched_Value3, null as Watched_Value4, ns.Extra as Extra, dv.devMac as ForeignKey FROM (SELECT * FROM Nmap_Scan) ns LEFT JOIN (SELECT devName, devMac, devLastIP FROM Devices) dv ON ns.MAC = dv.devMac",
"default_value": "SELECT dv.devName as objectPrimaryId, cast(dv.devLastIP as VARCHAR(100)) || ':' || cast(SUBSTR(ns.Port, 0, INSTR(ns.Port, '/')) as VARCHAR(100)) as objectSecondaryId, datetime() as DateTime, ns.Service as watchedValue1, ns.State as watchedValue2, null as watchedValue3, null as watchedValue4, ns.Extra as Extra, dv.devMac as ForeignKey FROM (SELECT * FROM Nmap_Scan) ns LEFT JOIN (SELECT devName, devMac, devLastIP FROM Devices) dv ON ns.MAC = dv.devMac",
"localized": ["name"],
"name": [{"language_code": "en_us", "string": "SQL to run"}],
"description": [{"language_code": "en_us", "string": "This SQL query populates the plugin table"}]
@@ -118,13 +118,13 @@ Query the NetAlertX SQLite database and display results.
```sql
SELECT
e.EventValue as Object_PrimaryID,
d.devName as Object_SecondaryID,
e.EventValue as objectPrimaryId,
d.devName as objectSecondaryId,
e.EventDateTime as DateTime,
e.EventType as Watched_Value1,
d.devLastIP as Watched_Value2,
null as Watched_Value3,
null as Watched_Value4,
e.EventType as watchedValue1,
d.devLastIP as watchedValue2,
null as watchedValue3,
null as watchedValue4,
e.EventDetails as Extra,
d.devMac as ForeignKey
FROM
@@ -181,7 +181,7 @@ Then set data source and query:
```json
{
"function": "CMD",
"default_value": "SELECT hwaddr as Object_PrimaryID, cast('http://' || (SELECT ip FROM EXTERNAL_PIHOLE.network_addresses WHERE network_id = id ORDER BY lastseen DESC LIMIT 1) as VARCHAR(100)) || ':' || cast(SUBSTR((SELECT name FROM EXTERNAL_PIHOLE.network_addresses WHERE network_id = id ORDER BY lastseen DESC LIMIT 1), 0, INSTR((SELECT name FROM EXTERNAL_PIHOLE.network_addresses WHERE network_id = id ORDER BY lastseen DESC LIMIT 1), '/')) as VARCHAR(100)) as Object_SecondaryID, datetime() as DateTime, macVendor as Watched_Value1, lastQuery as Watched_Value2, (SELECT name FROM EXTERNAL_PIHOLE.network_addresses WHERE network_id = id ORDER BY lastseen DESC LIMIT 1) as Watched_Value3, null as Watched_Value4, '' as Extra, hwaddr as ForeignKey FROM EXTERNAL_PIHOLE.network WHERE hwaddr NOT LIKE 'ip-%' AND hwaddr <> '00:00:00:00:00:00'",
"default_value": "SELECT hwaddr as objectPrimaryId, cast('http://' || (SELECT ip FROM EXTERNAL_PIHOLE.network_addresses WHERE network_id = id ORDER BY lastseen DESC LIMIT 1) as VARCHAR(100)) || ':' || cast(SUBSTR((SELECT name FROM EXTERNAL_PIHOLE.network_addresses WHERE network_id = id ORDER BY lastseen DESC LIMIT 1), 0, INSTR((SELECT name FROM EXTERNAL_PIHOLE.network_addresses WHERE network_id = id ORDER BY lastseen DESC LIMIT 1), '/')) as VARCHAR(100)) as objectSecondaryId, datetime() as DateTime, macVendor as watchedValue1, lastQuery as watchedValue2, (SELECT name FROM EXTERNAL_PIHOLE.network_addresses WHERE network_id = id ORDER BY lastseen DESC LIMIT 1) as watchedValue3, null as watchedValue4, '' as Extra, hwaddr as ForeignKey FROM EXTERNAL_PIHOLE.network WHERE hwaddr NOT LIKE 'ip-%' AND hwaddr <> '00:00:00:00:00:00'",
"localized": ["name"],
"name": [{"language_code": "en_us", "string": "SQL to run"}]
}

View File

@@ -18,19 +18,19 @@ Plugins communicate with NetAlertX by writing results to a **pipe-delimited log
## Column Specification
> [!NOTE]
> The order of columns is **FIXED** and cannot be changed. All 9 mandatory columns must be provided. If you use any optional column (`HelpVal1`), you must supply all optional columns (`HelpVal1` through `HelpVal4`).
> The order of columns is **FIXED** and cannot be changed. All 9 mandatory columns must be provided. If you use any optional column (`helpVal1`), you must supply all optional columns (`helpVal1` through `helpVal4`).
### Mandatory Columns (08)
| Order | Column Name | Type | Required | Description |
|-------|-------------|------|----------|-------------|
| 0 | `Object_PrimaryID` | string | **YES** | The primary identifier for grouping. Examples: device MAC, hostname, service name, or any unique ID |
| 1 | `Object_SecondaryID` | string | no | Secondary identifier for relationships (e.g., IP address, port, sub-ID). Use `null` if not needed |
| 0 | `objectPrimaryId` | string | **YES** | The primary identifier for grouping. Examples: device MAC, hostname, service name, or any unique ID |
| 1 | `objectSecondaryId` | string | no | Secondary identifier for relationships (e.g., IP address, port, sub-ID). Use `null` if not needed |
| 2 | `DateTime` | string | **YES** | Timestamp when the event/data was collected. Format: `YYYY-MM-DD HH:MM:SS` |
| 3 | `Watched_Value1` | string | **YES** | Primary watched value. Changes trigger notifications. Examples: IP address, status, version |
| 4 | `Watched_Value2` | string | no | Secondary watched value. Use `null` if not needed |
| 5 | `Watched_Value3` | string | no | Tertiary watched value. Use `null` if not needed |
| 6 | `Watched_Value4` | string | no | Quaternary watched value. Use `null` if not needed |
| 3 | `watchedValue1` | string | **YES** | Primary watched value. Changes trigger notifications. Examples: IP address, status, version |
| 4 | `watchedValue2` | string | no | Secondary watched value. Use `null` if not needed |
| 5 | `watchedValue3` | string | no | Tertiary watched value. Use `null` if not needed |
| 6 | `watchedValue4` | string | no | Quaternary watched value. Use `null` if not needed |
| 7 | `Extra` | string | no | Any additional metadata to display in UI and notifications. Use `null` if not needed |
| 8 | `ForeignKey` | string | no | Foreign key linking to parent object (usually MAC address for device relationship). Use `null` if not needed |
@@ -38,10 +38,10 @@ Plugins communicate with NetAlertX by writing results to a **pipe-delimited log
| Order | Column Name | Type | Required | Description |
|-------|-------------|------|----------|-------------|
| 9 | `HelpVal1` | string | *conditional* | Helper value 1. If used, all help values must be supplied |
| 10 | `HelpVal2` | string | *conditional* | Helper value 2. If used, all help values must be supplied |
| 11 | `HelpVal3` | string | *conditional* | Helper value 3. If used, all help values must be supplied |
| 12 | `HelpVal4` | string | *conditional* | Helper value 4. If used, all help values must be supplied |
| 9 | `helpVal1` | string | *conditional* | Helper value 1. If used, all help values must be supplied |
| 10 | `helpVal2` | string | *conditional* | Helper value 2. If used, all help values must be supplied |
| 11 | `helpVal3` | string | *conditional* | Helper value 3. If used, all help values must be supplied |
| 12 | `helpVal4` | string | *conditional* | Helper value 4. If used, all help values must be supplied |
## Usage Guide
@@ -58,15 +58,15 @@ Watched values are fields that the NetAlertX core monitors for **changes between
**How to use them:**
- `Watched_Value1`: Always required; primary indicator of status/state
- `Watched_Value24`: Optional; use for secondary/tertiary state information
- `watchedValue1`: Always required; primary indicator of status/state
- `watchedValue24`: Optional; use for secondary/tertiary state information
- Leave unused ones as `null`
**Example:**
- Device scanner: `Watched_Value1 = "online"` or `"offline"`
- Port scanner: `Watched_Value1 = "80"` (port number), `Watched_Value2 = "open"` (state)
- Service monitor: `Watched_Value1 = "200"` (HTTP status), `Watched_Value2 = "0.45"` (response time)
- Device scanner: `watchedValue1 = "online"` or `"offline"`
- Port scanner: `watchedValue1 = "80"` (port number), `watchedValue2 = "open"` (state)
- Service monitor: `watchedValue1 = "200"` (HTTP status), `watchedValue2 = "0.45"` (response time)
### Foreign Key
@@ -110,14 +110,14 @@ https://google.com|null|2023-01-02 15:56:30|200|0.7898||null|null
Missing pipe
```
**Missing mandatory Watched_Value1** (column 3):
**Missing mandatory watchedValue1** (column 3):
```csv
https://duckduckgo.com|192.168.1.1|2023-01-02 15:56:30|null|0.9898|null|null|Best|null
Must not be null
```
**Incomplete optional columns** (has HelpVal1 but missing HelpVal24):
**Incomplete optional columns** (has helpVal1 but missing helpVal24):
```csv
device|null|2023-01-02 15:56:30|status|null|null|null|null|null|helper1
@@ -146,19 +146,19 @@ plugin_objects = Plugin_Objects("YOURPREFIX")
# Add objects
plugin_objects.add_object(
Object_PrimaryID="device_id",
Object_SecondaryID="192.168.1.1",
objectPrimaryId="device_id",
objectSecondaryId="192.168.1.1",
DateTime="2023-01-02 15:56:30",
Watched_Value1="online",
Watched_Value2=None,
Watched_Value3=None,
Watched_Value4=None,
watchedValue1="online",
watchedValue2=None,
watchedValue3=None,
watchedValue4=None,
Extra="Additional data",
ForeignKey="aa:bb:cc:dd:ee:ff",
HelpVal1=None,
HelpVal2=None,
HelpVal3=None,
HelpVal4=None
helpVal1=None,
helpVal2=None,
helpVal3=None,
helpVal4=None
)
# Write results (handles formatting, sanitization, and file creation)
@@ -177,7 +177,7 @@ The library automatically:
The core runs **de-duplication once per hour** on the `Plugins_Objects` table:
- **Duplicate Detection Key:** Combination of `Object_PrimaryID`, `Object_SecondaryID`, `Plugin` (auto-filled from `unique_prefix`), and `UserData`
- **Duplicate Detection Key:** Combination of `objectPrimaryId`, `objectSecondaryId`, `Plugin` (auto-filled from `unique_prefix`), and `UserData`
- **Resolution:** Oldest duplicate entries are removed, newest are kept
- **Use Case:** Prevents duplicate notifications when the same object is detected multiple times
@@ -213,9 +213,9 @@ Before writing your plugin's `script.py`, ensure:
- [ ] **9 or 13 columns** in each output line (8 or 12 pipe separators)
- [ ] **Mandatory columns filled:**
- Column 0: `Object_PrimaryID` (not null)
- Column 0: `objectPrimaryId` (not null)
- Column 2: `DateTime` in `YYYY-MM-DD HH:MM:SS` format
- Column 3: `Watched_Value1` (not null)
- Column 3: `watchedValue1` (not null)
- [ ] **Null values as literal string** `null` (not empty string or special chars)
- [ ] **No extra pipes or misaligned columns**
- [ ] **If using optional helpers** (columns 912), all 4 must be present

View File

@@ -68,13 +68,13 @@ try:
# Add an object to results
plugin_objects.add_object(
Object_PrimaryID="example_id",
Object_SecondaryID=None,
objectPrimaryId="example_id",
objectSecondaryId=None,
DateTime="2023-01-02 15:56:30",
Watched_Value1="value1",
Watched_Value2=None,
Watched_Value3=None,
Watched_Value4=None,
watchedValue1="value1",
watchedValue2=None,
watchedValue3=None,
watchedValue4=None,
Extra="additional_data",
ForeignKey=None
)

View File

@@ -16,7 +16,7 @@ Each column definition specifies:
```json
{
"column": "Object_PrimaryID",
"column": "objectPrimaryId",
"mapped_to_column": "devMac",
"mapped_to_column_data": null,
"css_classes": "col-sm-2",
@@ -39,7 +39,7 @@ Each column definition specifies:
| Property | Type | Required | Description |
|----------|------|----------|-------------|
| `column` | string | **YES** | Source column name from data contract (e.g., `Object_PrimaryID`, `Watched_Value1`) |
| `column` | string | **YES** | Source column name from data contract (e.g., `objectPrimaryId`, `watchedValue1`) |
| `mapped_to_column` | string | no | Target database column if mapping to a table like `CurrentScan` |
| `mapped_to_column_data` | object | no | Static value to map instead of using column data |
| `css_classes` | string | no | Bootstrap CSS classes for width/spacing (e.g., `"col-sm-2"`, `"col-sm-6"`) |
@@ -64,7 +64,7 @@ Plain text display (read-only).
```json
{
"column": "Watched_Value1",
"column": "watchedValue1",
"show": true,
"type": "label",
"localized": ["name"],
@@ -99,7 +99,7 @@ Resolves an IP address to a MAC address and creates a device link.
```json
{
"column": "Object_SecondaryID",
"column": "objectSecondaryId",
"show": true,
"type": "device_ip",
"localized": ["name"],
@@ -117,7 +117,7 @@ Creates a device link with the target device's name as the link label.
```json
{
"column": "Object_PrimaryID",
"column": "objectPrimaryId",
"show": true,
"type": "device_name_mac",
"localized": ["name"],
@@ -135,7 +135,7 @@ Renders as a clickable HTTP/HTTPS link.
```json
{
"column": "Watched_Value1",
"column": "watchedValue1",
"show": true,
"type": "url",
"localized": ["name"],
@@ -153,7 +153,7 @@ Creates two links (HTTP and HTTPS) as lock icons for the given IP/hostname.
```json
{
"column": "Object_SecondaryID",
"column": "objectSecondaryId",
"show": true,
"type": "url_http_https",
"localized": ["name"],
@@ -207,7 +207,7 @@ Color-codes values based on ranges. Useful for status codes, latency, capacity p
```json
{
"column": "Watched_Value1",
"column": "watchedValue1",
"show": true,
"type": "threshold",
"options": [
@@ -252,7 +252,7 @@ Replaces specific values with display strings or HTML.
```json
{
"column": "Watched_Value2",
"column": "watchedValue2",
"show": true,
"type": "replace",
"options": [
@@ -286,7 +286,7 @@ Applies a regular expression to extract/transform values.
```json
{
"column": "Watched_Value1",
"column": "watchedValue1",
"show": true,
"type": "regex",
"options": [
@@ -310,7 +310,7 @@ Evaluates JavaScript code with access to the column value (use `${value}` or `{v
```json
{
"column": "Watched_Value1",
"column": "watchedValue1",
"show": true,
"type": "eval",
"default_value": "",
@@ -322,7 +322,7 @@ Evaluates JavaScript code with access to the column value (use `${value}` or `{v
**Example with custom formatting:**
```json
{
"column": "Watched_Value1",
"column": "watchedValue1",
"show": true,
"type": "eval",
"options": [
@@ -347,7 +347,7 @@ You can chain multiple transformations with dot notation:
```json
{
"column": "Watched_Value3",
"column": "watchedValue3",
"show": true,
"type": "regex.url_http_https",
"options": [
@@ -376,7 +376,7 @@ Use SQL query results to populate dropdown options:
```json
{
"column": "Watched_Value2",
"column": "watchedValue2",
"show": true,
"type": "select",
"options": ["{value}"],
@@ -405,7 +405,7 @@ Use plugin settings to populate options:
```json
{
"column": "Watched_Value1",
"column": "watchedValue1",
"show": true,
"type": "select",
"options": ["{value}"],
@@ -439,7 +439,7 @@ To import plugin data into the device scan pipeline (for notifications, heuristi
"mapped_to_table": "CurrentScan",
"database_column_definitions": [
{
"column": "Object_PrimaryID",
"column": "objectPrimaryId",
"mapped_to_column": "scanMac",
"show": true,
"type": "device_mac",
@@ -447,7 +447,7 @@ To import plugin data into the device scan pipeline (for notifications, heuristi
"name": [{"language_code": "en_us", "string": "MAC Address"}]
},
{
"column": "Object_SecondaryID",
"column": "objectSecondaryId",
"mapped_to_column": "scanLastIP",
"show": true,
"type": "device_ip",
@@ -501,7 +501,7 @@ Control which rows are displayed based on filter conditions. Filters are applied
{
"data_filters": [
{
"compare_column": "Object_PrimaryID",
"compare_column": "objectPrimaryId",
"compare_operator": "==",
"compare_field_id": "txtMacFilter",
"compare_js_template": "'{value}'.toString()",
@@ -545,7 +545,7 @@ When viewing a device detail page, the `txtMacFilter` field is populated with th
{
"database_column_definitions": [
{
"column": "Object_PrimaryID",
"column": "objectPrimaryId",
"mapped_to_column": "scanMac",
"css_classes": "col-sm-2",
"show": true,
@@ -555,7 +555,7 @@ When viewing a device detail page, the `txtMacFilter` field is populated with th
"name": [{"language_code": "en_us", "string": "MAC Address"}]
},
{
"column": "Object_SecondaryID",
"column": "objectSecondaryId",
"mapped_to_column": "scanLastIP",
"css_classes": "col-sm-2",
"show": true,
@@ -574,7 +574,7 @@ When viewing a device detail page, the `txtMacFilter` field is populated with th
"name": [{"language_code": "en_us", "string": "Last Seen"}]
},
{
"column": "Watched_Value1",
"column": "watchedValue1",
"css_classes": "col-sm-2",
"show": true,
"type": "threshold",
@@ -589,7 +589,7 @@ When viewing a device detail page, the `txtMacFilter` field is populated with th
"name": [{"language_code": "en_us", "string": "HTTP Status"}]
},
{
"column": "Watched_Value2",
"column": "watchedValue2",
"css_classes": "col-sm-1",
"show": true,
"type": "label",

View File

@@ -137,7 +137,7 @@ Some additional context:
Before submitting a new issue please spend a couple of minutes on research:
* Check [🛑 Common issues](./DEBUG_TIPS.md#common-issues)
* Check [💡 Closed issues](https://github.com/jokob-sk/NetAlertX/issues?q=is%3Aissue+is%3Aclosed) if a similar issue was solved in the past.
* Check [💡 Closed issues](https://github.com/netalertx/NetAlertX/issues?q=is%3Aissue+is%3Aclosed) if a similar issue was solved in the past.
* When submitting an issue ❗[enable debug](./DEBUG_TIPS.md)❗
⚠ Please follow the pre-defined issue template to resolve your issue faster.

View File

@@ -43,11 +43,11 @@ You can use supplementary plugins that employ alternate methods. Protocols used
## Multiple NetAlertX Instances
If you have servers in different networks, you can set up separate NetAlertX instances on those subnets and synchronize the results into one instance using the [`SYNC` plugin](https://github.com/jokob-sk/NetAlertX/tree/main/front/plugins/sync).
If you have servers in different networks, you can set up separate NetAlertX instances on those subnets and synchronize the results into one instance using the [`SYNC` plugin](https://github.com/netalertx/NetAlertX/tree/main/front/plugins/sync).
## Manual Entry
If you don't need to discover new devices and only need to report on their status (`online`, `offline`, `down`), you can manually enter devices and check their status using the [`ICMP` plugin](https://github.com/jokob-sk/NetAlertX/blob/main/front/plugins/icmp_scan/), which uses the `ping` command internally.
If you don't need to discover new devices and only need to report on their status (`online`, `offline`, `down`), you can manually enter devices and check their status using the [`ICMP` plugin](https://github.com/netalertx/NetAlertX/blob/main/front/plugins/icmp_scan/), which uses the `ping` command internally.
For more information on how to add devices manually (or dummy devices), refer to the [Device Management](./DEVICE_MANAGEMENT.md) documentation.
@@ -57,4 +57,4 @@ To create truly dummy devices, you can use a loopback IP address (e.g., `0.0.0.0
Scanning remote networks with NMAP is possible (via the `NMAPDEV` plugin), but since it cannot retrieve the MAC address, you need to enable the `NMAPDEV_FAKE_MAC` setting. This will generate a fake MAC address based on the IP address, allowing you to track devices. However, this can lead to inconsistencies, especially if the IP address changes or a previously logged device is rediscovered. If this setting is disabled, only the IP address will be discovered, and devices with missing MAC addresses will be skipped.
Check the [NMAPDEV plugin](https://github.com/jokob-sk/NetAlertX/tree/main/front/plugins/nmap_dev_scan) for details
Check the [NMAPDEV plugin](https://github.com/netalertx/NetAlertX/tree/main/front/plugins/nmap_dev_scan) for details

View File

@@ -2,35 +2,35 @@
You need to specify the network interface and the network mask. You can also configure multiple subnets and specify VLANs (see VLAN exceptions below).
`ARPSCAN` can scan multiple networks if the network allows it. To scan networks directly, the subnets must be accessible from the network where NetAlertX is running. This means NetAlertX needs to have access to the interface attached to that subnet.
`ARPSCAN` can scan multiple networks if the network allows it. To scan networks directly, the subnets must be accessible from the network where NetAlertX is running. This means NetAlertX needs to have access to the interface attached to that subnet.
> [!WARNING]
> [!WARNING]
> If you don't see all expected devices run the following command in the NetAlertX container (replace the interface and ip mask):
> `sudo arp-scan --interface=eth0 192.168.1.0/24`
>
> If this command returns no results, the network is not accessible due to your network or firewall restrictions (Wi-Fi Extenders, VPNs and inaccessible networks). If direct scans are not possible, check the [remote networks documentation](./REMOTE_NETWORKS.md) for workarounds.
>
> If this command returns no results, the network is not accessible due to your network or firewall restrictions (Wi-Fi Extenders, VPNs and inaccessible networks). If direct scans are not possible, check the [remote networks documentation](./REMOTE_NETWORKS.md) for workarounds.
## Example Values
> [!NOTE]
> Please use the UI to configure settings as it ensures the config file is in the correct format. Edit `app.conf` directly only when really necessary.
> [!NOTE]
> Please use the UI to configure settings as it ensures the config file is in the correct format. Edit `app.conf` directly only when really necessary.
> ![Settings location](./img/SUBNETS/subnets-setting-location.png)
* **Examples for one and two subnets:**
* One subnet: `SCAN_SUBNETS = ['192.168.1.0/24 --interface=eth0']`
* Two subnets: `SCAN_SUBNETS = ['192.168.1.0/24 --interface=eth0','192.168.1.0/24 --interface=eth1 --vlan=107']`
> [!TIP]
> When adding more subnets, you may need to increase both the scan interval (`ARPSCAN_RUN_SCHD`) and the timeout (`ARPSCAN_RUN_TIMEOUT`)—as well as similar settings for related plugins.
>
> If the timeout is too short, you may see timeout errors in the log. To prevent the application from hanging due to unresponsive plugins, scans are canceled when they exceed the timeout limit.
>
> To fix this:
> - Reduce the subnet size (e.g., change `/16` to `/24`).
> - Increase the timeout (e.g., set `ARPSCAN_RUN_TIMEOUT` to `300` for a 5-minute timeout).
> - Extend the scan interval (e.g., set `ARPSCAN_RUN_SCHD` to `*/10 * * * *` to scan every 10 minutes).
>
> [!TIP]
> When adding more subnets, you may need to increase both the scan interval (`ARPSCAN_RUN_SCHD`) and the timeout (`ARPSCAN_RUN_TIMEOUT`)—as well as similar settings for related plugins.
>
> If the timeout is too short, you may see timeout errors in the log. To prevent the application from hanging due to unresponsive plugins, scans are canceled when they exceed the timeout limit.
>
> To fix this:
> - Reduce the subnet size (e.g., change `/16` to `/24`).
> - Increase the timeout (e.g., set `ARPSCAN_RUN_TIMEOUT` to `300` for a 5-minute timeout).
> - Extend the scan interval (e.g., set `ARPSCAN_RUN_SCHD` to `*/10 * * * *` to scan every 10 minutes).
>
> For more troubleshooting tips, see [Debugging Plugins](./DEBUG_PLUGINS.md).
---
@@ -43,7 +43,7 @@ You need to specify the network interface and the network mask. You can also con
The `arp-scan` time itself depends on the number of IP addresses to check.
> The number of IPs to check depends on the [network mask](https://www.calculator.net/ip-subnet-calculator.html) you set in the `SCAN_SUBNETS` setting.
> The number of IPs to check depends on the [network mask](https://www.calculator.net/ip-subnet-calculator.html) you set in the `SCAN_SUBNETS` setting.
> For example, a `/24` mask results in 256 IPs to check, whereas a `/16` mask checks around 65,536 IPs. Each IP takes a couple of seconds, so an incorrect configuration could make `arp-scan` take hours instead of seconds.
Specify the network filter, which **significantly** speeds up the scan process. For example, the filter `192.168.1.0/24` covers IP ranges from `192.168.1.0` to `192.168.1.255`.
@@ -56,7 +56,7 @@ The adapter will probably be `eth0` or `eth1`. (Check `System Info` > `Network H
![Network hardware](./img/SUBNETS/system_info-network_hardware.png)
> [!TIP]
> [!TIP]
> As an alternative to `iwconfig`, run `ip -o link show | awk -F': ' '!/lo|vir|docker/ {print $2}'` in your container to find your interface name(s) (e.g.: `eth0`, `eth1`):
> ```bash
> Synology-NAS:/# ip -o link show | awk -F': ' '!/lo|vir|docker/ {print $2}'
@@ -73,11 +73,11 @@ The adapter will probably be `eth0` or `eth1`. (Check `System Info` > `Network H
#### VLANs on a Hyper-V Setup
> Community-sourced content by [mscreations](https://github.com/mscreations) from this [discussion](https://github.com/jokob-sk/NetAlertX/discussions/404).
> Community-sourced content by [mscreations](https://github.com/mscreations) from this [discussion](https://github.com/netalertx/NetAlertX/discussions/404).
**Tested Setup:** Bare Metal → Hyper-V on Win Server 2019 → Ubuntu 22.04 VM → Docker → NetAlertX.
**Approach 1 (may cause issues):**
**Approach 1 (may cause issues):**
Configure multiple network adapters in Hyper-V with distinct VLANs connected to each one using Hyper-V's network setup. However, this action can potentially lead to the Docker host's inability to handle network traffic correctly. This might interfere with other applications such as Authentik.
**Approach 2 (working example):**

View File

@@ -1,6 +1,6 @@
## Am I running the latest released version?
Since version 23.01.14 NetAlertX uses a simple timestamp-based version check to verify if a new version is available. You can check the [current and past releases here](https://github.com/jokob-sk/NetAlertX/releases), or have a look at what I'm [currently working on](https://github.com/jokob-sk/NetAlertX/issues/138).
Since version 23.01.14 NetAlertX uses a simple timestamp-based version check to verify if a new version is available. You can check the [current and past releases here](https://github.com/netalertx/NetAlertX/releases), or have a look at what I'm [currently working on](https://github.com/netalertx/NetAlertX/issues/138).
If you are not on the latest version, the app will notify you, that a new released version is avialable the following way:
@@ -22,4 +22,4 @@ For a comparison, this is how the UI looks like if you are on the latest stable
## Implementation details
During build a [/app/front/buildtimestamp.txt](https://github.com/jokob-sk/NetAlertX/blob/092797e75ccfa8359444ad149e727358ac4da05f/Dockerfile#L44) file is created. The app then periodically checks if a new release is available with a newer timestamp in GitHub's rest-based JSON endpoint (check the `def isNewVersion:` method for details).
During build a [/app/front/buildtimestamp.txt](https://github.com/netalertx/NetAlertX/blob/092797e75ccfa8359444ad149e727358ac4da05f/Dockerfile#L44) file is created. The app then periodically checks if a new release is available with a newer timestamp in GitHub's rest-based JSON endpoint (check the `def isNewVersion:` method for details).

View File

@@ -1,14 +1,14 @@
### Create a simple n8n workflow
> [!NOTE]
> You need to enable the `WEBHOOK` plugin first in order to follow this guide. See the [Plugins guide](./PLUGINS.md) for details.
> You need to enable the `WEBHOOK` plugin first in order to follow this guide. See the [Plugins guide](./PLUGINS.md) for details.
N8N can be used for more advanced conditional notification use cases. For example, you want only to get notified if two out of a specified list of devices is down. Or you can use other plugins to process the notifiations further. The below is a simple example of sending an email on a webhook.
N8N can be used for more advanced conditional notification use cases. For example, you want only to get notified if two out of a specified list of devices is down. Or you can use other plugins to process the notifiations further. The below is a simple example of sending an email on a webhook.
![n8n workflow](./img/WEBHOOK_N8N/n8n_workflow.png)
### Specify your email template
See [sample JSON](https://github.com/jokob-sk/NetAlertX/blob/main/front/report_templates/webhook_json_sample.json) if you want to see the JSON paths used in the email template below
### Specify your email template
See [sample JSON](https://github.com/netalertx/NetAlertX/blob/main/front/report_templates/webhook_json_sample.json) if you want to see the JSON paths used in the email template below
![Email template](./img/WEBHOOK_N8N/n8n_send_email_settings.png)
```

View File

@@ -8,7 +8,7 @@ Below are a few examples that demonstrate how this module can be used to simplif
## Updating Workflows
> [!NOTE]
> [!NOTE]
> In order to apply a workflow change, you must first **Save** the changes and then reload the application by clicking **Restart server**.
## Workflow components
@@ -25,7 +25,7 @@ Triggers define the event that activates a workflow. They monitor changes to obj
#### Example Trigger:
- **Object Type**: `Devices`
- **Event Type**: `update`
This trigger will activate when a `Device` object is updated.
### Conditions
@@ -42,7 +42,7 @@ Conditions determine whether a workflow should proceed based on certain criteria
- **Field**: `devVendor`
- **Operator**: `contains` (case in-sensitive)
- **Value**: `Google`
This condition checks if the device's vendor is `Google`. The workflow will only proceed if the condition is true.
### Actions
@@ -57,7 +57,7 @@ You can include multiple actions that should execute once the conditions are met
- **Action Type**: `update_field`
- **Field**: `devIsNew`
- **Value**: `0`
This action updates the `devIsNew` field to `0`, marking the device as no longer new.
@@ -67,4 +67,4 @@ You can find a couple of configuration examples in [Workflow Examples](WORKFLOW_
> [!TIP]
> Share your workflows in [Discord](https://discord.com/invite/NczTUTWyRr) or [GitHub Discussions](https://github.com/jokob-sk/NetAlertX/discussions).
> Share your workflows in [Discord](https://discord.com/invite/NczTUTWyRr) or [GitHub Discussions](https://github.com/netalertx/NetAlertX/discussions).

View 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)

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 39 KiB

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 170 KiB

After

Width:  |  Height:  |  Size: 201 KiB

View File

@@ -61,16 +61,16 @@ $(document).ready(function () {
appEvents(options: $options) {
count
appEvents {
DateTimeCreated
AppEventProcessed
AppEventType
ObjectType
ObjectPrimaryID
ObjectSecondaryID
ObjectStatus
ObjectPlugin
ObjectGUID
GUID
dateTimeCreated
appEventProcessed
appEventType
objectType
objectPrimaryId
objectSecondaryId
objectStatus
objectPlugin
objectGuid
guid
}
}
}
@@ -128,16 +128,16 @@ $(document).ready(function () {
},
columns: [
{ data: 'DateTimeCreated', title: getString('AppEvents_DateTimeCreated') },
{ data: 'AppEventProcessed', title: getString('AppEvents_AppEventProcessed') },
{ data: 'AppEventType', title: getString('AppEvents_Type') },
{ data: 'ObjectType', title: getString('AppEvents_ObjectType') },
{ data: 'ObjectPrimaryID', title: getString('AppEvents_ObjectPrimaryID') },
{ data: 'ObjectSecondaryID', title: getString('AppEvents_ObjectSecondaryID') },
{ data: 'ObjectStatus', title: getString('AppEvents_ObjectStatus') },
{ data: 'ObjectPlugin', title: getString('AppEvents_Plugin') },
{ data: 'ObjectGUID', title: 'Object GUID' },
{ data: 'GUID', title: 'Event GUID' }
{ data: 'dateTimeCreated', title: getString('AppEvents_DateTimeCreated') },
{ data: 'appEventProcessed', title: getString('AppEvents_AppEventProcessed') },
{ data: 'appEventType', title: getString('AppEvents_Type') },
{ data: 'objectType', title: getString('AppEvents_ObjectType') },
{ data: 'objectPrimaryId', title: getString('AppEvents_ObjectPrimaryID') },
{ data: 'objectSecondaryId', title: getString('AppEvents_ObjectSecondaryID') },
{ data: 'objectStatus', title: getString('AppEvents_ObjectStatus') },
{ data: 'objectPlugin', title: getString('AppEvents_Plugin') },
{ data: 'objectGuid', title: 'Object GUID' },
{ data: 'guid', title: 'Event GUID' }
],
columnDefs: [

View File

@@ -37,7 +37,7 @@
body, .bg-yellow, .callout.callout-warning, .alert-warning, .label-warning, .modal-warning .modal-body {
background-color: #353c42 !important;
color: #bec5cb !important;
color: #ffffff !important;
}
h4 {
color: #44def1;

View File

@@ -299,7 +299,7 @@ function updateChevrons(currentMac) {
showSpinner();
cacheDevices().then(() => {
cacheDevices(true).then(() => {
hideSpinner();
// Retry after re-caching
@@ -384,25 +384,9 @@ $(document).on('input', 'input:text', function() {
// -----------------------------------------------------------------------------
function initializeTabs () {
key ="activeDevicesTab"
// Activate panel
if(!emptyArr.includes(getCache(key)))
{
selectedTab = getCache(key);
}
$('.nav-tabs a[id='+ selectedTab +']').tab('show');
// When changed save new current tab
$('a[data-toggle="tab"]').on('shown.bs.tab', function (e) {
setCache(key, $(e.target).attr('id'))
});
// events on tab change
$('a[data-toggle="tab"]').on('shown.bs.tab', function (e) {
var target = $(e.target).attr("href") // activated tab
initializeTabsShared({
cacheKey: 'activeDevicesTab',
defaultTab: 'tabDetails'
});
}
@@ -419,7 +403,12 @@ async function renderSmallBoxes() {
const apiToken = getSetting("API_TOKEN");
const apiBaseUrl = getApiBase();
const url = `${apiBaseUrl}/device/${getMac()}?period=${encodeURIComponent(period)}`;
// Ensure period is a string, not an element
let periodValue = period;
if (typeof period === 'object' && period !== null && 'value' in period) {
periodValue = period.value;
}
const url = `${apiBaseUrl}/device/${getMac()}?period=${encodeURIComponent(periodValue)}`;
const response = await fetch(url, {
method: "GET",
@@ -436,17 +425,22 @@ async function renderSmallBoxes() {
const deviceData = await response.json();
// Derive status card appearance from shared getStatusBadgeParts —
// ensures icon, color, label and lang key are always in sync with the rest of the UI.
const statusBadge = badgeFromDevice(deviceData);
const statusText = statusBadge.label;
// Prepare custom data
const customData = [
{
"onclickEvent": "$('#tabDetails').trigger('click')",
"color": "bg-aqua",
"color": statusBadge.cssClass,
"headerId": "deviceStatus",
"headerStyle": "margin-left: 0em",
"labelLang": "DevDetail_Shortcut_CurrentStatus",
"iconId": "deviceStatusIcon",
"iconClass": deviceData.devPresentLastScan == 1 ? "fa fa-check text-green" : "fa fa-xmark text-red",
"dataValue": deviceData.devPresentLastScan == 1 ? getString("Gen_Online") : getString("Gen_Offline")
"iconHtml": statusBadge.iconHtml,
"dataValue": statusText
},
{
"onclickEvent": "$('#tabSessions').trigger('click');",
@@ -513,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);
@@ -553,20 +547,24 @@ function updateDevicePageName(mac) {
//-----------------------------------------------------------------------------------
// Call renderSmallBoxes, then main
(async () => {
await renderSmallBoxes();
main();
})();
window.onload = function async()
{
mac = getMac()
// initializeTabs();
updateChevrons(mac);
updateDevicePageName(mac);
window.onload = function() {
// Always trigger app-init bootstrap
if (typeof executeOnce === 'function') {
executeOnce();
}
mac = getMac();
// Wait for app initialization (cache populated) before using cached data
callAfterAppInitialized(async () => {
updateDevicePageName(mac);
updateChevrons(mac);
await renderSmallBoxes();
main();
});
}
</script>

View File

@@ -138,7 +138,7 @@ function getDeviceData() {
},
// Group for event and alert settings
DevDetail_EveandAl_Title: {
data: ["devAlertEvents", "devAlertDown", "devSkipRepeated", "devReqNicsOnline", "devChildrenNicsDynamic", "devForceStatus"],
data: ["devAlertEvents", "devAlertDown", "devCanSleep", "devSkipRepeated", "devReqNicsOnline", "devChildrenNicsDynamic", "devForceStatus"],
docs: "https://docs.netalertx.com/NOTIFICATIONS",
iconClass: "fa fa-bell",
inputGroupClasses: "field-group alert-group col-lg-4 col-sm-6 col-xs-12",
@@ -363,7 +363,7 @@ function getDeviceData() {
generateSimpleForm(relevantSettings);
toggleNetworkConfiguration(mac == 'Internet')
toggleNetworkConfiguration(mac.toLowerCase() == 'internet')
initSelect2();
initHoverNodeInfo();
@@ -447,6 +447,7 @@ function setDeviceData(direction = '', refreshCallback = '') {
devAlertEvents: ($('#NEWDEV_devAlertEvents')[0].checked * 1),
devAlertDown: ($('#NEWDEV_devAlertDown')[0].checked * 1),
devCanSleep: ($('#NEWDEV_devCanSleep')[0].checked * 1),
devSkipRepeated: $('#NEWDEV_devSkipRepeated').val().split(' ')[0],
devForceStatus: $('#NEWDEV_devForceStatus').val().replace(/'/g, ""),

View File

@@ -32,51 +32,64 @@
function loadEventsData() {
const hideConnections = $('#chkHideConnectionEvents')[0].checked;
const hideConnectionsStr = hideConnections ? 'true' : 'false';
let period = $("#period").val();
let { start, end } = getPeriodStartEnd(period);
const rawSql = `
SELECT eve_DateTime, eve_EventType, eve_IP, eve_AdditionalInfo
FROM Events
WHERE eve_MAC = "${mac}"
AND eve_DateTime BETWEEN "${start}" AND "${end}"
AND (
(eve_EventType NOT IN ("Connected", "Disconnected", "VOIDED - Connected", "VOIDED - Disconnected"))
OR "${hideConnectionsStr}" = "false"
)
const apiToken = getSetting("API_TOKEN");
const apiBase = getApiBase();
const graphqlUrl = `${apiBase}/graphql`;
const query = `
query Events($options: EventQueryOptionsInput) {
events(options: $options) {
count
entries {
eveDateTime
eveEventType
eveIp
eveAdditionalInfo
}
}
}
`;
const apiToken = getSetting("API_TOKEN");
const apiBaseUrl = getApiBase();
const url = `${apiBaseUrl}/dbquery/read`;
$.ajax({
url: url,
url: graphqlUrl,
method: "POST",
contentType: "application/json",
headers: {
"Authorization": `Bearer ${apiToken}`
},
data: JSON.stringify({
rawSql: btoa(rawSql)
query,
variables: {
options: {
eveMac: mac,
dateFrom: start,
dateTo: end,
limit: 500,
sort: [{ field: "eveDateTime", order: "desc" }]
}
}
}),
success: function (data) {
// assuming read_query returns rows directly
const rows = data["results"].map(row => {
const rawDate = row.eve_DateTime;
const formattedDate = rawDate ? localizeTimestamp(rawDate) : '-';
const CONNECTION_TYPES = ["Connected", "Disconnected", "VOIDED - Connected", "VOIDED - Disconnected"];
return [
formattedDate,
row.eve_DateTime,
row.eve_EventType,
row.eve_IP,
row.eve_AdditionalInfo
];
});
const rows = data.data.events.entries
.filter(row => !hideConnections || !CONNECTION_TYPES.includes(row.eveEventType))
.map(row => {
const rawDate = row.eveDateTime;
const formattedDate = rawDate ? localizeTimestamp(rawDate) : '-';
return [
formattedDate,
row.eveDateTime,
row.eveEventType,
row.eveIp,
row.eveAdditionalInfo
];
});
const table = $('#tableEvents').DataTable();
table.clear();

View File

@@ -121,12 +121,12 @@ function loadSessionsData() {
if (data.success && data.sessions.length) {
data.sessions.forEach(session => {
table.row.add([
session.ses_DateTimeOrder,
session.ses_Connection,
session.ses_Disconnection,
session.ses_Duration,
session.ses_IP,
session.ses_Info
session.sesDateTimeOrder,
session.sesConnection,
session.sesDisconnection,
session.sesDuration,
session.sesIp,
session.sesInfo
]);
});
}

View File

@@ -5,7 +5,7 @@
?>
<!-- INTERNET INFO -->
<?php if ($_REQUEST["mac"] == "Internet") { ?>
<?php if (strtolower($_REQUEST["mac"]) == "internet") { ?>
<h4 class=""><i class="fa-solid fa-globe"></i>
<?= lang("DevDetail_Tab_Tools_Internet_Info_Title") ?>
@@ -24,7 +24,7 @@
<?php } ?>
<!-- COPY FROM DEVICE -->
<?php if ($_REQUEST["mac"] != "Internet") { ?>
<?php if (strtolower($_REQUEST["mac"]) != "internet") { ?>
<h4 class=""><i class="fa-solid fa-copy"></i>
<?= lang("DevDetail_Copy_Device_Title") ?>
@@ -47,7 +47,7 @@
<?php } ?>
<!-- WAKE ON LAN - WOL -->
<?php if ($_REQUEST["mac"] != "Internet") { ?>
<?php if (strtolower($_REQUEST["mac"]) != "internet") { ?>
<h4 class=""><i class="fa-solid fa-bell"></i>
<?= lang("DevDetail_Tools_WOL_noti") ?>
@@ -108,7 +108,7 @@
<!-- SPEEDTEST -->
<?php if ($_REQUEST["mac"] == "Internet") { ?>
<?php if (strtolower($_REQUEST["mac"]) == "internet") { ?>
<h4 class=""><i class="fa-solid fa-gauge-high"></i>
<?= lang("DevDetail_Tab_Tools_Speedtest_Title") ?>
</h4>
@@ -126,7 +126,7 @@
<?php } ?>
<!-- TRACEROUTE -->
<?php if ($_REQUEST["mac"] != "Internet") { ?>
<?php if (strtolower($_REQUEST["mac"]) != "internet") { ?>
<h4 class=""><i class="fa-solid fa-route"></i>
<?= lang("DevDetail_Tab_Tools_Traceroute_Title") ?>
</h4>
@@ -144,7 +144,7 @@
<?php } ?>
<!-- NSLOOKUP -->
<?php if ($_REQUEST["mac"] != "Internet") { ?>
<?php if (strtolower($_REQUEST["mac"]) != "internet") { ?>
<h4 class=""><i class="fa-solid fa-magnifying-glass"></i>
<?= lang("DevDetail_Tab_Tools_Nslookup_Title") ?>
</h4>
@@ -526,13 +526,7 @@
function getVisibleDevicesList()
{
// Read cache (skip cookie expiry check)
devicesList = getCache('devicesListAll_JSON', true);
if (devicesList != '') {
devicesList = JSON.parse (devicesList);
} else {
devicesList = [];
}
devicesList = parseDeviceCache(getCache('devicesListAll_JSON', true));
// only loop thru the filtered down list
visibleDevices = getCache("ntx_visible_macs")

View File

@@ -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 ------------------------------------------------------------- -->
@@ -87,6 +97,8 @@
<div class="box-header">
<div class=" col-sm-8 ">
<h3 id="tableDevicesTitle" class="box-title text-gray "></h3>
<!-- Next scan ETA — populated by sse_manager.js via nax:scanEtaUpdate -->
<small id="nextScanEta" class="text-muted" style="display:none;margin-left:8px;font-weight:normal;font-size:0.75em;"></small>
</div>
<div class="dummyDevice col-sm-4 ">
<span id="multiEditPlc">
@@ -143,6 +155,23 @@
headersDefaultOrder = [];
missingNumbers = [];
// 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();
@@ -467,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();
});
@@ -512,49 +538,160 @@ function collectFilters() {
// -----------------------------------------------------------------------------
// Map column index to column name for GraphQL query
function mapColumnIndexToFieldName(index, tableColumnVisible) {
// the order is important, don't change it!
const columnNames = [
"devName", // 0
"devOwner", // 1
"devType", // 2
"devIcon", // 3
"devFavorite", // 4
"devGroup", // 5
"devFirstConnection", // 6
"devLastConnection", // 7
"devLastIP", // 8
"devIsRandomMac", // 9 resolved on the fly
"devStatus", // 10 resolved on the fly
"devMac", // 11
"devIpLong", // 12 formatIPlong(device.devLastIP) || "", // IP orderable
"rowid", // 13
"devParentMAC", // 14
"devParentChildrenCount", // 15 resolved on the fly
"devLocation", // 16
"devVendor", // 17
"devParentPort", // 18
"devGUID", // 19
"devSyncHubNode", // 20
"devSite", // 21
"devSSID", // 22
"devSourcePlugin", // 23
"devPresentLastScan", // 24
"devAlertDown", // 25
"devCustomProps", // 26
"devFQDN", // 27
"devParentRelType", // 28
"devReqNicsOnline", // 29
"devVlan", // 30
"devPrimaryIPv4", // 31
"devPrimaryIPv6", // 32
];
// console.log("OrderBy: " + columnNames[tableColumnOrder[index]]);
return columnNames[tableColumnOrder[index]] || null;
// Derives field name from the authoritative DEVICE_COLUMN_FIELDS constant.
return DEVICE_COLUMN_FIELDS[tableColumnOrder[index]] || null;
}
// ---------------------------------------------------------
// Status badge helper for DataTables rowData (positional array).
// Uses mapIndx(COL.*) for reordered display fields and COL_EXTRA.* for extra fields.
function badgeFromRowData(rowData) {
return getStatusBadgeParts(
rowData[mapIndx(COL.devPresentLastScan)],
rowData[mapIndx(COL.devAlertDown)],
rowData[mapIndx(COL.devFlapping)],
rowData[mapIndx(COL.devMac)],
'',
rowData[COL_EXTRA.devIsSleeping] || 0,
rowData[COL_EXTRA.devIsArchived] || 0,
rowData[COL_EXTRA.devIsNew] || 0
);
}
// ---------------------------------------------------------
// Build the rich empty-table onboarding message (HTML).
// Used as the DataTables 'emptyTable' language option.
function buildEmptyDeviceTableMessage(nextScanLabel) {
var etaLine = nextScanLabel
? '<small class="text-muted" style="margin-top:6px;display:block;">' + nextScanLabel + '</small>'
: '';
return '<div class="text-center" style="padding:20px;">' +
'<i class="fa fa-search fa-2x text-muted" style="margin-bottom:10px;"></i><br>' +
'<strong>' + getString('Device_NoData_Title') + '</strong><br>' +
'<span class="text-muted">' + getString('Device_NoData_Scanning') + '</span><br>' +
etaLine +
'<small style="margin-top:6px;display:block;">' + getString('Device_NoData_Help') + '</small>' +
'</div>';
}
// ---------------------------------------------------------
// Compute a live countdown label from an ISO next_scan_time string.
// next_scan_time is the earliest scheduled run time across enabled device_scanner plugins,
// computed by the backend and broadcast via SSE — no guesswork needed on the frontend.
function computeNextScanLabel(nextScanTime) {
if (!nextScanTime) return getString('Device_NextScan_Imminent');
// Append Z if no UTC offset marker present — backend may emit naive UTC ISO strings.
var isoStr = /Z$|[+-]\d{2}:?\d{2}$/.test(nextScanTime.trim()) ? nextScanTime : nextScanTime + 'Z';
var secsLeft = Math.round((new Date(isoStr).getTime() - Date.now()) / 1000);
if (secsLeft <= 0) return getString('Device_NextScan_Imminent');
if (secsLeft >= 60) {
var m = Math.floor(secsLeft / 60);
var s = secsLeft % 60;
return getString('Device_NextScan_In') + m + 'm ' + s + 's';
}
return getString('Device_NextScan_In') + secsLeft + 's';
}
// Anchor for next scheduled scan time, ticker handle, plugins data, and current state — module-level.
var _nextScanTimeAnchor = null;
var _currentStateAnchor = null;
var _scanEtaTickerId = null;
var _pluginsData = null;
var _wasImminent = false; // true once the countdown displayed "imminent"; gates the Scanning... label
var _imminentForTime = null; // the _nextScanTimeAnchor value that last set _wasImminent
// prevents re-arming on the same (already-consumed) timestamp
// Returns true when the backend is actively scanning (not idle).
// Uses an exclusion approach — only "Process: Idle" and an empty/null state are non-scanning.
// This future-proofs against new states added to the scan pipeline (e.g. "Plugin: AVAHISCAN").
function isScanningState(state) {
return !!state && state !== 'Process: Idle';
}
// Fetch plugins.json once on page load so we can guard ETA display to device_scanner plugins only.
$.get('php/server/query_json.php', { file: 'plugins.json', nocache: Date.now() }, function(res) {
_pluginsData = res['data'] || [];
});
// Returns true only when at least one device_scanner plugin is loaded and not disabled.
function hasEnabledDeviceScanners() {
if (!_pluginsData || !_pluginsData.length) return false;
return getPluginsByType(_pluginsData, 'device_scanner', true).length > 0;
}
// ---------------------------------------------------------
// 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;
// Reset the imminent gate when the scan finishes back to idle so the next cycle starts clean.
if (currentState === 'Process: Idle') { _wasImminent = false; }
// Restart the per-second title-bar ticker
if (_scanEtaTickerId !== null) { clearInterval(_scanEtaTickerId); }
function getEtaLabel() {
if (!hasEnabledDeviceScanners()) return '';
if (isScanningState(_currentStateAnchor) && _wasImminent) return getString('Device_Scanning');
var label = computeNextScanLabel(_nextScanTimeAnchor);
// Arm _wasImminent only for a NEW next_scan_time anchor — not the already-consumed one.
// This prevents the ticker from re-arming immediately after "Process: Idle" resets the flag
// while _nextScanTimeAnchor still holds the now-past timestamp.
if (label === getString('Device_NextScan_Imminent') && _nextScanTimeAnchor !== _imminentForTime) {
_wasImminent = true;
_imminentForTime = _nextScanTimeAnchor;
}
return label;
}
function tickTitleBar() {
var eta = document.getElementById('nextScanEta');
if (!eta) return;
var label = getEtaLabel();
if (!label) { eta.style.display = 'none'; return; }
eta.textContent = label;
eta.style.display = '';
}
// 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();
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();
_scanEtaTickerId = setInterval(tickTitleBar, 1000);
}
// Listen for scan ETA updates dispatched by sse_manager.js (SSE push or poll fallback)
document.addEventListener('nax:scanEtaUpdate', function(e) {
updateScanEtaDisplay(e.detail.nextScanTime, e.detail.currentState);
});
// ---------------------------------------------------------
// Initializes the main devices list datatable
function initializeDatatable (status) {
@@ -619,55 +756,18 @@ function initializeDatatable (status) {
"type": "POST",
"contentType": "application/json",
"data": function (d) {
// Construct GraphQL query with pagination and sorting options
// GraphQL fields are derived from DEVICE_COLUMN_FIELDS + GRAPHQL_EXTRA_FIELDS
// (both defined in js/device-columns.js). No manual field list to maintain.
const _gqlFields = [...new Set([...DEVICE_COLUMN_FIELDS, ...GRAPHQL_EXTRA_FIELDS])]
.join('\n ');
let graphqlQuery = `
query devices($options: PageQueryOptionsInput) {
devices(options: $options) {
devices {
rowid
devMac
devName
devOwner
devType
devVendor
devFavorite
devGroup
devComments
devFirstConnection
devLastConnection
devLastIP
devStaticIP
devScan
devLogEvents
devAlertEvents
devAlertDown
devSkipRepeated
devLastNotification
devPresentLastScan
devIsNew
devIsRandomMac
devLocation
devIsArchived
devParentMAC
devParentPort
devIcon
devGUID
devSite
devSSID
devSyncHubNode
devSourcePlugin
devStatus
devParentChildrenCount
devIpLong
devCustomProps
devFQDN
devParentRelType
devReqNicsOnline
devVlan
devPrimaryIPv4
devPrimaryIPv6
${_gqlFields}
}
count
dbCount
}
}
`;
@@ -708,58 +808,34 @@ 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);
// Return only the array of rows for the table
return json.devices.devices.map(device => {
// Convert each device record into the required DataTable row format
// Order has to be the same as in the UI_device_columns setting options
const originalRow = [
device.devName || "",
device.devOwner || "",
device.devType || "",
device.devIcon || "",
device.devFavorite || "",
device.devGroup || "",
device.devFirstConnection || "",
device.devLastConnection || "",
device.devLastIP || "",
device.devIsRandomMac || "",
device.devStatus || "",
device.devMac || "",
device.devIpLong || "",
device.rowid || "",
device.devParentMAC || "",
device.devParentChildrenCount || 0,
device.devLocation || "",
device.devVendor || "",
device.devParentPort || "",
device.devGUID || "",
device.devSyncHubNode || "",
device.devSite || "",
device.devSSID || "",
device.devSourcePlugin || "",
device.devPresentLastScan || "",
device.devAlertDown || "",
device.devCustomProps || "",
device.devFQDN || "",
device.devParentRelType || "",
device.devReqNicsOnline || 0,
device.devVlan || "",
device.devPrimaryIPv4 || "",
device.devPrimaryIPv6 || "",
];
// Build positional row directly from DEVICE_COLUMN_FIELDS.
// NUMERIC_DEFAULTS controls which fields default to 0 vs "".
// Adding a new column: add to DEVICE_COLUMN_FIELDS (and NUMERIC_DEFAULTS
// if needed) in js/device-columns.js — nothing to change here.
const originalRow = DEVICE_COLUMN_FIELDS.map(
field => device[field] ?? (NUMERIC_DEFAULTS.has(field) ? 0 : "")
);
const newRow = [];
// Reorder data based on user-defined columns order
for (let index = 0; index < tableColumnOrder.length; index++) {
newRow.push(originalRow[tableColumnOrder[index]]);
}
// Append extra (non-display) fields after the display columns so
// they are accessible in createdCell via COL_EXTRA.*
GRAPHQL_EXTRA_FIELDS.forEach(field => {
newRow.push(device[field] ?? (NUMERIC_DEFAULTS.has(field) ? 0 : ""));
});
return newRow;
});
}
@@ -787,15 +863,15 @@ function initializeDatatable (status) {
'columnDefs' : [
{visible: false, targets: tableColumnHide },
{className: 'text-center', targets: [mapIndx(4), mapIndx(9), mapIndx(10), mapIndx(15), mapIndx(18)] },
{className: 'iconColumn text-center', targets: [mapIndx(3)]},
{width: '80px', targets: [mapIndx(6), mapIndx(7), mapIndx(15), mapIndx(27)] },
{width: '85px', targets: [mapIndx(9)] },
{width: '30px', targets: [mapIndx(3), mapIndx(10), mapIndx(13), mapIndx(18)] },
{orderData: [mapIndx(12)], targets: mapIndx(8) },
{className: 'text-center', targets: [mapIndx(COL.devFavorite), mapIndx(COL.devIsRandomMac), mapIndx(COL.devStatus), mapIndx(COL.devParentChildrenCount), mapIndx(COL.devParentPort)] },
{className: 'iconColumn text-center', targets: [mapIndx(COL.devIcon)]},
{width: '80px', targets: [mapIndx(COL.devFirstConnection), mapIndx(COL.devLastConnection), mapIndx(COL.devParentChildrenCount), mapIndx(COL.devFQDN)] },
{width: '85px', targets: [mapIndx(COL.devIsRandomMac)] },
{width: '30px', targets: [mapIndx(COL.devIcon), mapIndx(COL.devStatus), mapIndx(COL.rowid), mapIndx(COL.devParentPort)] },
{orderData: [mapIndx(COL.devIpLong)], targets: mapIndx(COL.devLastIP) },
// Device Name and FQDN
{targets: [mapIndx(0), mapIndx(27)],
{targets: [mapIndx(COL.devName), mapIndx(COL.devFQDN)],
'createdCell': function (td, cellData, rowData, row, col) {
// console.log(cellData)
@@ -809,19 +885,23 @@ function initializeDatatable (status) {
$(td).html (
`<b class="anonymizeDev "
>
<a href="deviceDetails.php?mac=${rowData[mapIndx(11)]}" class="hover-node-info"
<a href="deviceDetails.php?mac=${rowData[mapIndx(COL.devMac)]}" class="hover-node-info"
data-name="${displayedValue}"
data-ip="${rowData[mapIndx(8)]}"
data-mac="${rowData[mapIndx(11)]}"
data-vendor="${rowData[mapIndx(17)]}"
data-type="${rowData[mapIndx(2)]}"
data-firstseen="${rowData[mapIndx(6)]}"
data-lastseen="${rowData[mapIndx(7)]}"
data-relationship="${rowData[mapIndx(28)]}"
data-status="${rowData[mapIndx(10)]}"
data-present="${rowData[mapIndx(24)]}"
data-alert="${rowData[mapIndx(25)]}"
data-icon="${rowData[mapIndx(3)]}">
data-ip="${rowData[mapIndx(COL.devLastIP)]}"
data-mac="${rowData[mapIndx(COL.devMac)]}"
data-vendor="${rowData[mapIndx(COL.devVendor)]}"
data-type="${rowData[mapIndx(COL.devType)]}"
data-firstseen="${rowData[mapIndx(COL.devFirstConnection)]}"
data-lastseen="${rowData[mapIndx(COL.devLastConnection)]}"
data-relationship="${rowData[mapIndx(COL.devParentRelType)]}"
data-status="${rowData[mapIndx(COL.devStatus)]}"
data-present="${rowData[mapIndx(COL.devPresentLastScan)]}"
data-alertdown="${rowData[mapIndx(COL.devAlertDown)]}"
data-flapping="${rowData[mapIndx(COL.devFlapping)]}"
data-sleeping="${rowData[COL_EXTRA.devIsSleeping] || 0}"
data-archived="${rowData[COL_EXTRA.devIsArchived] || 0}"
data-isnew="${rowData[COL_EXTRA.devIsNew] || 0}"
data-icon="${rowData[mapIndx(COL.devIcon)]}">
${displayedValue}
</a>
</b>`
@@ -829,12 +909,12 @@ function initializeDatatable (status) {
} },
// Connected Devices
{targets: [mapIndx(15)],
{targets: [mapIndx(COL.devParentChildrenCount)],
'createdCell': function (td, cellData, rowData, row, col) {
// check if this is a network device
if(getSetting("NETWORK_DEVICE_TYPES").includes(`'${rowData[mapIndx(2)]}'`) )
if(getSetting("NETWORK_DEVICE_TYPES").includes(`'${rowData[mapIndx(COL.devType)]}'`) )
{
$(td).html ('<b><a href="./network.php?mac='+ rowData[mapIndx(11)] +'" class="">'+ cellData +'</a></b>');
$(td).html ('<b><a href="./network.php?mac='+ rowData[mapIndx(COL.devMac)] +'" class="">'+ cellData +'</a></b>');
}
else
{
@@ -844,7 +924,7 @@ function initializeDatatable (status) {
} },
// Icon
{targets: [mapIndx(3)],
{targets: [mapIndx(COL.devIcon)],
'createdCell': function (td, cellData, rowData, row, col) {
if (!emptyArr.includes(cellData)){
@@ -855,7 +935,7 @@ function initializeDatatable (status) {
} },
// Full MAC
{targets: [mapIndx(11)],
{targets: [mapIndx(COL.devMac)],
'createdCell': function (td, cellData, rowData, row, col) {
if (!emptyArr.includes(cellData)){
$(td).html ('<span class="anonymizeMac">'+cellData+'</span>');
@@ -865,7 +945,7 @@ function initializeDatatable (status) {
} },
// IP address
{targets: [mapIndx(8)],
{targets: [mapIndx(COL.devLastIP)],
'createdCell': function (td, cellData, rowData, row, col) {
if (!emptyArr.includes(cellData)){
$(td).html (`<span class="anonymizeIp">
@@ -883,8 +963,8 @@ function initializeDatatable (status) {
}
}
},
// IP address (ordeable)
{targets: [mapIndx(12)],
// IP address (orderable)
{targets: [mapIndx(COL.devIpLong)],
'createdCell': function (td, cellData, rowData, row, col) {
if (!emptyArr.includes(cellData)){
$(td).html (`<span class="anonymizeIp">${cellData}<span>`);
@@ -895,10 +975,10 @@ function initializeDatatable (status) {
},
// Custom Properties
{targets: [mapIndx(26)],
{targets: [mapIndx(COL.devCustomProps)],
'createdCell': function (td, cellData, rowData, row, col) {
if (!emptyArr.includes(cellData)){
$(td).html (`<span>${renderCustomProps(cellData, rowData[mapIndx(11)])}</span>`);
$(td).html (`<span>${renderCustomProps(cellData, rowData[mapIndx(COL.devMac)])}</span>`);
} else {
$(td).html ('');
}
@@ -906,7 +986,7 @@ function initializeDatatable (status) {
},
// Favorite
{targets: [mapIndx(4)],
{targets: [mapIndx(COL.devFavorite)],
'createdCell': function (td, cellData, rowData, row, col) {
if (cellData == 1){
$(td).html ('<i class="fa fa-star text-yellow" style="font-size:16px"></i>');
@@ -916,7 +996,7 @@ function initializeDatatable (status) {
} },
// Dates
{targets: [mapIndx(6), mapIndx(7)],
{targets: [mapIndx(COL.devFirstConnection), mapIndx(COL.devLastConnection)],
'createdCell': function (td, cellData, rowData, row, col) {
var result = cellData.toString(); // Convert to string
if (result.includes("+")) { // Check if timezone offset is present
@@ -926,7 +1006,7 @@ function initializeDatatable (status) {
} },
// Random MAC
{targets: [mapIndx(9)],
{targets: [mapIndx(COL.devIsRandomMac)],
'createdCell': function (td, cellData, rowData, row, col) {
// console.log(cellData)
if (cellData == 1){
@@ -937,7 +1017,7 @@ function initializeDatatable (status) {
} },
// Parent Mac
{targets: [mapIndx(14)],
{targets: [mapIndx(COL.devParentMAC)],
'createdCell': function (td, cellData, rowData, row, col) {
if (!isValidMac(cellData)) {
$(td).html('');
@@ -959,27 +1039,20 @@ function initializeDatatable (status) {
}
},
// Status color
{targets: [mapIndx(10)],
{targets: [mapIndx(COL.devStatus)],
'createdCell': function (td, cellData, rowData, row, col) {
tmp_devPresentLastScan = rowData[mapIndx(24)]
tmp_devAlertDown = rowData[mapIndx(25)]
const badge = badgeFromRowData(rowData);
const badge = getStatusBadgeParts(
rowData[mapIndx(24)], // tmp_devPresentLastScan
rowData[mapIndx(25)], // tmp_devAlertDown
rowData[mapIndx(11)], // MAC
cellData // optional text
);
$(td).html (`<a href="${badge.url}" class="badge ${badge.cssClass}">${badge.iconHtml} ${badge.text}</a>`);
$(td).html(`<a href="${badge.url}" class="badge ${badge.cssClass}">${badge.iconHtml} ${badge.label}</a>`);
} },
],
// Processing
'processing' : true,
'language' : {
emptyTable: 'No data',
emptyTable: buildEmptyDeviceTableMessage(getString('Device_NextScan_Imminent')),
zeroRecords: "<?= lang('Device_NoMatch_Title');?>",
"lengthMenu": "<?= lang('Device_Tablelenght');?>",
"search": "<?= lang('Device_Searchbox');?>: ",
"paginate": {
@@ -1037,7 +1110,7 @@ function initializeDatatable (status) {
},
createdRow: function(row, data, dataIndex) {
// add devMac to the table row
$(row).attr('my-devMac', data[mapIndx(11)]);
$(row).attr('my-devMac', data[mapIndx(COL.devMac)]);
}
@@ -1083,7 +1156,7 @@ function multiEditDevices()
macs = ""
for (var j = 0; j < selectedDevicesDataTableData.length; j++) {
macs += selectedDevicesDataTableData[j][mapIndx(11)] + ","; // [11] == MAC
macs += selectedDevicesDataTableData[j][mapIndx(COL.devMac)] + ","; // MAC
}
// redirect to the Maintenance section
@@ -1104,7 +1177,7 @@ function getMacsOfShownDevices() {
allIndexes.each(function(idx) {
var rowData = table.row(idx).data();
if (rowData) {
macs.push(rowData[mapIndx(11)]); // mapIndx(11) == MAC column
macs.push(rowData[mapIndx(COL.devMac)]); // MAC column
}
});

View File

@@ -105,21 +105,64 @@ function main() {
$('#period').val(period);
initializeDatatable();
getEventsTotals();
getEvents(eventsType);
getEvents(eventsType); // triggers first serverSide draw
}
/* ---------------- Initialize DataTable ---------------- */
function initializeDatatable() {
const table = $('#tableEvents').DataTable({
paging: true,
const apiBase = getApiBase();
const apiToken = getSetting("API_TOKEN");
$('#tableEvents').DataTable({
processing: true,
serverSide: true,
paging: true,
lengthChange: true,
lengthMenu: getLengthMenu(getSetting("UI_DEFAULT_PAGE_SIZE")),
searching: true,
ordering: true,
info: true,
autoWidth: false,
order: [[0, "desc"], [3, "desc"], [5, "desc"]],
pageLength: tableRows,
lengthMenu: getLengthMenu(getSetting("UI_DEFAULT_PAGE_SIZE")),
searching: true,
ordering: true,
info: true,
autoWidth: false,
order: [[0, "desc"]],
pageLength: tableRows,
ajax: function (dtRequest, callback) {
const page = Math.floor(dtRequest.start / dtRequest.length) + 1;
const limit = dtRequest.length;
const search = dtRequest.search?.value || '';
const sortCol = dtRequest.order?.length ? dtRequest.order[0].column : 0;
const sortDir = dtRequest.order?.length ? dtRequest.order[0].dir : 'desc';
const url = `${apiBase}/sessions/session-events`
+ `?type=${encodeURIComponent(eventsType)}`
+ `&period=${encodeURIComponent(period)}`
+ `&page=${page}`
+ `&limit=${limit}`
+ `&sortCol=${sortCol}`
+ `&sortDir=${sortDir}`
+ (search ? `&search=${encodeURIComponent(search)}` : '');
$.ajax({
url,
method: "GET",
dataType: "json",
headers: { "Authorization": `Bearer ${apiToken}` },
success: function (response) {
callback({
data: response.data || [],
recordsTotal: response.total || 0,
recordsFiltered: response.recordsFiltered || 0
});
hideSpinner();
},
error: function (xhr, status, error) {
console.error("Error fetching session events:", status, error, xhr.responseText);
callback({ data: [], recordsTotal: 0, recordsFiltered: 0 });
hideSpinner();
}
});
},
columnDefs: [
{ targets: [0,5,6,7,8,10,11,12,13], visible: false },
{ targets: [7], orderData: [8] },
@@ -131,14 +174,14 @@ function initializeDatatable() {
{ targets: [3], createdCell: (td, cellData) => $(td).html(localizeTimestamp(cellData)) },
{ targets: [4,5,6,7], createdCell: (td, cellData) => $(td).html(translateHTMLcodes(cellData)) }
],
processing: true, // Shows "processing" overlay
language: {
processing: '<table><td width="130px" align="middle"><?= lang("Events_Loading"); ?></td><td><i class="fa-solid fa-spinner fa-spin-pulse"></i></td></table>',
emptyTable: 'No data',
lengthMenu: "<?= lang('Events_Tablelenght'); ?>",
search: "<?= lang('Events_Searchbox'); ?>: ",
paginate: { next: "<?= lang('Events_Table_nav_next'); ?>", previous: "<?= lang('Events_Table_nav_prev'); ?>" },
info: "<?= lang('Events_Table_info'); ?>"
search: "<?= lang('Events_Searchbox'); ?>: ",
paginate: { next: "<?= lang('Events_Table_nav_next'); ?>", previous: "<?= lang('Events_Table_nav_prev'); ?>" },
info: "<?= lang('Events_Table_info'); ?>"
}
});
@@ -179,53 +222,33 @@ function getEventsTotals() {
});
}
/* ---------------- Fetch events and reload DataTable ---------------- */
/* ---------------- Switch event type and reload DataTable ---------------- */
function getEvents(type) {
eventsType = type;
const table = $('#tableEvents').DataTable();
// Event type config: title, color, session columns visibility
const config = {
all: {title: 'Events_Shortcut_AllEvents', color: 'aqua', sesionCols: false},
sessions: {title: 'Events_Shortcut_Sessions', color: 'green', sesionCols: true},
missing: {title: 'Events_Shortcut_MissSessions', color: 'yellow', sesionCols: true},
voided: {title: 'Events_Shortcut_VoidSessions', color: 'yellow', sesionCols: false},
new: {title: 'Events_Shortcut_NewDevices', color: 'yellow', sesionCols: false},
down: {title: 'Events_Shortcut_DownAlerts', color: 'red', sesionCols: false}
all: {title: 'Events_Shortcut_AllEvents', color: 'aqua', sesionCols: false},
sessions: {title: 'Events_Shortcut_Sessions', color: 'green', sesionCols: true},
missing: {title: 'Events_Shortcut_MissSessions', color: 'yellow', sesionCols: true},
voided: {title: 'Events_Shortcut_VoidSessions', color: 'yellow', sesionCols: false},
new: {title: 'Events_Shortcut_NewDevices', color: 'yellow', sesionCols: false},
down: {title: 'Events_Shortcut_DownAlerts', color: 'red', sesionCols: false}
}[type] || {title: 'Events_Shortcut_Events', color: '', sesionCols: false};
// Update title and color
$('#tableEventsTitle').attr('class', 'box-title text-' + config.color).html(getString(config.title));
$('#tableEventsBox').attr('class', 'box box-' + config.color);
// Toggle columns visibility
// Toggle column visibility
table.column(3).visible(!config.sesionCols);
table.column(4).visible(!config.sesionCols);
table.column(5).visible(config.sesionCols);
table.column(6).visible(config.sesionCols);
table.column(7).visible(config.sesionCols);
// Build API URL
const apiBase = getApiBase();
const apiToken = getSetting("API_TOKEN");
const url = `${apiBase}/sessions/session-events?type=${encodeURIComponent(type)}&period=${encodeURIComponent(period)}`;
table.clear().draw(); // Clear old rows
showSpinner()
$.ajax({
url,
method: "GET",
dataType: "json",
headers: { "Authorization": `Bearer ${apiToken}` },
beforeSend: showSpinner, // Show spinner during fetch
complete: hideSpinner, // Hide spinner after fetch
success: response => {
const data = Array.isArray(response) ? response : response.data || [];
table.rows.add(data).draw();
},
error: (xhr, status, error) => console.error("Error fetching session events:", status, error, xhr.responseText)
});
showSpinner();
table.ajax.reload(null, true); // reset to page 1
}
</script>

View File

@@ -3,76 +3,155 @@
<?php
//------------------------------------------------------------------------------
// check if authenticated
// Be CAREFUL WHEN INCLUDING NEW PHP FILES
require_once $_SERVER['DOCUMENT_ROOT'] . '/php/server/db.php';
require_once $_SERVER['DOCUMENT_ROOT'] . '/php/templates/language/lang.php';
require_once $_SERVER['DOCUMENT_ROOT'] . '/php/templates/security.php';
require_once $_SERVER['DOCUMENT_ROOT'].'/php/server/db.php';
require_once $_SERVER['DOCUMENT_ROOT'].'/php/templates/language/lang.php';
require_once $_SERVER['DOCUMENT_ROOT'].'/php/templates/security.php';
$CookieSaveLoginName = 'NetAlertX_SaveLogin';
// if (session_status() === PHP_SESSION_NONE) {
// session_start();
// }
if ($nax_WebProtection != 'true')
{
header('Location: devices.php');
$_SESSION["login"] = 1;
// session_start();
const DEFAULT_REDIRECT = '/devices.php';
/* =====================================================
Helper Functions
===================================================== */
function safe_redirect(string $path): void {
header("Location: {$path}", true, 302);
exit;
}
// Logout
if (isset ($_GET["action"]) && $_GET["action"] == 'logout')
{
setcookie($CookieSaveLoginName, '', time()+1); // reset cookie
$_SESSION["login"] = 0;
header('Location: index.php');
exit;
function validate_local_path(?string $encoded): string {
if (!$encoded) return DEFAULT_REDIRECT;
$decoded = base64_decode($encoded, true);
if ($decoded === false) {
return DEFAULT_REDIRECT;
}
// strict local path check (allow safe query strings + fragments)
// Using ~ as the delimiter instead of #
if (!preg_match('~^(?!//)(?!.*://)/[a-zA-Z0-9_\-./?=&:%#]*$~', $decoded)) {
return DEFAULT_REDIRECT;
}
return $decoded;
}
// Password without Cookie check -> pass and set initial cookie
if (isset ($_POST["loginpassword"]) && $nax_Password === hash('sha256',$_POST["loginpassword"]))
{
header('Location: devices.php');
$_SESSION["login"] = 1;
if (isset($_POST['PWRemember'])) {setcookie($CookieSaveLoginName, hash('sha256',$_POST["loginpassword"]), time()+604800);}
function extract_hash_from_path(string $path): array {
/*
Split a path into path and hash components.
For deep links encoded in the 'next' parameter like /devices.php#device-123,
extract the hash fragment so it can be properly included in the redirect.
Args:
path: Full path potentially with hash (e.g., "/devices.php#device-123")
Returns:
Array with keys 'path' (without hash) and 'hash' (with # prefix, or empty string)
*/
$parts = explode('#', $path, 2);
return [
'path' => $parts[0],
'hash' => !empty($parts[1]) ? '#' . $parts[1] : ''
];
}
// active Session or valid cookie (cookie not extends)
if (( isset ($_SESSION["login"]) && ($_SESSION["login"] == 1)) || (isset ($_COOKIE[$CookieSaveLoginName]) && $nax_Password === $_COOKIE[$CookieSaveLoginName]))
{
header('Location: devices.php');
$_SESSION["login"] = 1;
if (isset($_POST['PWRemember'])) {setcookie($CookieSaveLoginName, hash('sha256',$_POST["loginpassword"]), time()+604800);}
function append_hash(string $url): string {
// First check if the URL already has a hash from the deep link
$parts = extract_hash_from_path($url);
if (!empty($parts['hash'])) {
return $parts['path'] . $parts['hash'];
}
// Fall back to POST url_hash (for browser-captured hashes)
if (!empty($_POST['url_hash'])) {
$sanitized = preg_replace('/[^#a-zA-Z0-9_\-]/', '', $_POST['url_hash']);
if (str_starts_with($sanitized, '#')) {
return $url . $sanitized;
}
}
return $url;
}
function is_authenticated(): bool {
return isset($_SESSION['login']) && $_SESSION['login'] === 1;
}
function login_user(): void {
$_SESSION['login'] = 1;
session_regenerate_id(true);
}
function logout_user(): void {
$_SESSION = [];
session_destroy();
}
/* =====================================================
Redirect Handling
===================================================== */
$redirectTo = validate_local_path($_GET['next'] ?? null);
/* =====================================================
Web Protection Disabled
===================================================== */
if ($nax_WebProtection !== 'true') {
if (!is_authenticated()) {
login_user();
}
safe_redirect(append_hash($redirectTo));
}
/* =====================================================
Login Attempt
===================================================== */
if (!empty($_POST['loginpassword'])) {
$incomingHash = hash('sha256', $_POST['loginpassword']);
if (hash_equals($nax_Password, $incomingHash)) {
login_user();
// Redirect to target page, preserving deep link hash if present
safe_redirect(append_hash($redirectTo));
}
}
/* =====================================================
Already Logged In
===================================================== */
if (is_authenticated()) {
safe_redirect(append_hash($redirectTo));
}
/* =====================================================
Login UI Variables
===================================================== */
$login_headline = lang('Login_Toggle_Info_headline');
$login_info = lang('Login_Info');
$login_mode = 'danger';
$login_display_mode = 'display: block;';
$login_icon = 'fa-info';
$login_info = lang('Login_Info');
$login_mode = 'info';
$login_display_mode = 'display:none;';
$login_icon = 'fa-info';
// no active session, cookie not checked
if (isset ($_SESSION["login"]) == FALSE || $_SESSION["login"] != 1)
{
if ($nax_Password === '8d969eef6ecad3c29a3a629280e686cf0c3f5d5a86aff3ca12020c923adc6c92')
{
if ($nax_Password === '8d969eef6ecad3c29a3a629280e686cf0c3f5d5a86aff3ca12020c923adc6c92') {
$login_info = lang('Login_Default_PWD');
$login_mode = 'danger';
$login_display_mode = 'display: block;';
$login_display_mode = 'display:block;';
$login_headline = lang('Login_Toggle_Alert_headline');
$login_icon = 'fa-ban';
}
else
{
$login_mode = 'info';
$login_display_mode = 'display: none;';
$login_headline = lang('Login_Toggle_Info_headline');
$login_icon = 'fa-info';
}
}
// ##################################################
// ## Login Processing end
// ##################################################
?>
<!DOCTYPE html>
@@ -109,27 +188,21 @@ if (isset ($_SESSION["login"]) == FALSE || $_SESSION["login"] != 1)
<!-- /.login-logo -->
<div class="login-box-body">
<p class="login-box-msg"><?= lang('Login_Box');?></p>
<form action="index.php" method="post">
<form action="index.php<?php
echo !empty($_GET['next'])
? '?next=' . htmlspecialchars($_GET['next'], ENT_QUOTES, 'UTF-8')
: '';
?>" method="post">
<div class="form-group has-feedback">
<input type="hidden" name="url_hash" id="url_hash">
<input type="password" class="form-control" placeholder="<?= lang('Login_Psw-box');?>" name="loginpassword">
<span class="glyphicon glyphicon-lock form-control-feedback"></span>
</div>
<div class="row">
<div class="col-xs-8">
<div class="checkbox icheck">
<label>
<input type="checkbox" name="PWRemember">
<div style="margin-left: 10px; display: inline-block; vertical-align: top;">
<?= lang('Login_Remember');?><br><span style="font-size: smaller"><?= lang('Login_Remember_small');?></span>
</div>
</label>
</div>
</div>
<!-- /.col -->
<div class="col-xs-4" style="padding-top: 10px;">
<div class="col-xs-12">
<button type="submit" class="btn btn-primary btn-block btn-flat"><?= lang('Login_Submit');?></button>
</div>
<!-- /.col -->
<!-- /.col -->
</div>
</form>
@@ -159,6 +232,9 @@ if (isset ($_SESSION["login"]) == FALSE || $_SESSION["login"] != 1)
<!-- iCheck -->
<script src="lib/iCheck/icheck.min.js"></script>
<script>
if (window.location.hash) {
document.getElementById('url_hash').value = window.location.hash;
}
$(function () {
$('input').iCheck({
checkboxClass: 'icheckbox_square-blue',
@@ -174,7 +250,7 @@ function Passwordhinfo() {
} else {
x.style.display = "none";
}
}
}
</script>
</body>

281
front/js/app-init.js Normal file
View File

@@ -0,0 +1,281 @@
/* -----------------------------------------------------------------------------
* NetAlertX
* Open Source Network Guard / WIFI & LAN intrusion detector
*
* app-init.js - Front module. Application lifecycle: initialization,
* cache orchestration, and startup sequencing.
* Loaded AFTER common.js — depends on showSpinner(), isEmpty(),
* mergeUniqueArrays(), getSetting(), getString(), getCache(),
* setCache(), and all cache* functions from cache.js.
*-------------------------------------------------------------------------------
# jokob@duck.com GNU GPLv3
----------------------------------------------------------------------------- */
// -----------------------------------------------------------------------------
// initialize
// -----------------------------------------------------------------------------
var completedCalls = []
var completedCalls_final = ['cacheApiConfig', 'cacheSettings', 'cacheStrings_v2', 'cacheDevices'];
var lang_completedCalls = 0;
// -----------------------------------------------------------------------------
// Clearing all the caches
function clearCache() {
showSpinner();
sessionStorage.clear();
localStorage.clear();
// Wait for spinner to show and cache to clear, then reload
setTimeout(() => {
console.warn("clearCache called");
window.location.reload();
}, 100);
}
// ===================================================================
// DEPRECATED: checkSettingChanges() - Replaced by SSE-based manager
// Settings changes are now handled via SSE events
// Kept for backward compatibility, will be removed in future version
// ===================================================================
function checkSettingChanges() {
// SSE manager handles settings_changed events now
if (typeof netAlertXStateManager !== 'undefined' && netAlertXStateManager.initialized) {
return; // SSE handles this now
}
// Fallback for backward compatibility
$.get('php/server/query_json.php', { file: 'app_state.json', nocache: Date.now() }, function(appState) {
const importedMilliseconds = parseInt(appState["settingsImported"] * 1000);
const lastReloaded = parseInt(getCache(CACHE_KEYS.INIT_TIMESTAMP));
if (importedMilliseconds > lastReloaded) {
console.log("Cache needs to be refreshed because of setting changes");
setTimeout(() => {
clearCache();
}, 500);
}
});
}
// ===================================================================
// Display spinner and reload page if not yet initialized
async function handleFirstLoad(callback) {
if (!isAppInitialized()) {
await new Promise(resolve => setTimeout(resolve, 1000));
callback();
}
}
// ===================================================================
// Execute callback once the app is initialized and GraphQL server is running
async function callAfterAppInitialized(callback) {
if (!isAppInitialized() || !(await isGraphQLServerRunning())) {
setTimeout(() => {
callAfterAppInitialized(callback);
}, 500);
} else {
callback();
}
}
// ===================================================================
// Polling function to repeatedly check if the server is running
async function waitForGraphQLServer() {
const pollInterval = 2000; // 2 seconds between each check
let serverRunning = false;
while (!serverRunning) {
serverRunning = await isGraphQLServerRunning();
if (!serverRunning) {
console.log("GraphQL server not running, retrying in 2 seconds...");
await new Promise(resolve => setTimeout(resolve, pollInterval));
}
}
console.log("GraphQL server is now running.");
}
// -----------------------------------------------------------------------------
// Returns 1 if running, 0 otherwise
async function isGraphQLServerRunning() {
try {
const response = await $.get('php/server/query_json.php', { file: 'app_state.json', nocache: Date.now()});
console.log("graphQLServerStarted: " + response["graphQLServerStarted"]);
setCache(CACHE_KEYS.GRAPHQL_STARTED, response["graphQLServerStarted"]);
return response["graphQLServerStarted"];
} catch (error) {
console.error("Failed to check GraphQL server status:", error);
return false;
}
}
// Throttle isAppInitialized logging so the console isn't spammed on every poll.
let _isAppInit_lastLogTime = 0;
function _isAppInitLog(msg) {
const now = Date.now();
if (now - _isAppInit_lastLogTime > 5000) { // log at most once per 5s
console.log(msg);
_isAppInit_lastLogTime = now;
}
}
// -----------------------------------------------------------------------------
// Check if the code has been executed before by checking localStorage
function isAppInitialized() {
lang_shouldBeCompletedCalls = getLangCode() == 'en_us' ? 1 : 2;
// check if each ajax call completed succesfully
for (const call_name of completedCalls_final) {
if (getCache(CACHE_KEYS.initFlag(call_name)) != "true") {
_isAppInitLog(`[isAppInitialized] waiting on ${call_name} (value: ${getCache(CACHE_KEYS.initFlag(call_name))})`);
return false;
}
}
// check if all required languages chached
if(parseInt(getCache(CACHE_KEYS.STRINGS_COUNT)) != lang_shouldBeCompletedCalls)
{
_isAppInitLog(`[isAppInitialized] waiting on cacheStrings: ${getCache(CACHE_KEYS.STRINGS_COUNT)} of ${lang_shouldBeCompletedCalls}`);
return false;
}
return true;
}
// Retry a single async init step up to maxAttempts times with a delay.
async function retryStep(name, fn, maxAttempts = 3, delayMs = 1500) {
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
try {
await fn();
return; // success
} catch (err) {
console.warn(`[executeOnce] ${name} failed (attempt ${attempt}/${maxAttempts}):`, err);
if (attempt < maxAttempts) {
await new Promise(r => setTimeout(r, delayMs));
} else {
console.error(`[executeOnce] ${name} permanently failed after ${maxAttempts} attempts.`);
}
}
}
}
// -----------------------------------------------------------------------------
// Main execution logic
let _executeOnceRunning = false;
async function executeOnce() {
if (_executeOnceRunning) {
console.log('[executeOnce] Already running — skipping duplicate call.');
return;
}
_executeOnceRunning = true;
showSpinner();
// Auto-bust stale cache if code version has changed since last init.
// Clears localStorage in-place so the subsequent init runs fresh without
// requiring a page reload.
if (getCache(CACHE_KEYS.CACHE_VERSION) !== NAX_CACHE_VERSION) {
console.log(`[executeOnce] Cache version mismatch (stored: "${getCache(CACHE_KEYS.CACHE_VERSION)}", expected: "${NAX_CACHE_VERSION}"). Clearing cache.`);
localStorage.clear();
sessionStorage.clear();
}
if (!isAppInitialized()) {
try {
await waitForGraphQLServer(); // Wait for the server to start
await retryStep('cacheApiConfig', cacheApiConfig); // Bootstrap: API_TOKEN + GRAPHQL_PORT from app.conf
await retryStep('cacheDevices', cacheDevices);
await retryStep('cacheSettings', cacheSettings);
await retryStep('cacheStrings', cacheStrings);
console.log("All AJAX callbacks have completed");
onAllCallsComplete();
} finally {
_executeOnceRunning = false;
}
} else {
_executeOnceRunning = false;
}
}
// -----------------------------------------------------------------------------
// Function to handle successful completion of an AJAX call
const handleSuccess = (callName) => {
console.log(`AJAX call successful: ${callName}`);
if(callName.includes("cacheStrings"))
{
completed_tmp = getCache(CACHE_KEYS.STRINGS_COUNT);
completed_tmp == "" ? completed_tmp = 0 : completed_tmp = completed_tmp;
completed_tmp++;
setCache(CACHE_KEYS.STRINGS_COUNT, completed_tmp);
}
setCache(CACHE_KEYS.initFlag(callName), true)
};
// -----------------------------------------------------------------------------
// Function to handle failure of an AJAX call
const handleFailure = (callName, callback) => {
msg = `AJAX call ${callName} failed`
console.error(msg);
if (typeof callback === 'function') {
callback(new Error(msg));
}
};
// -----------------------------------------------------------------------------
// Function to execute when all AJAX calls have completed
const onAllCallsComplete = () => {
completedCalls = mergeUniqueArrays(getCache(CACHE_KEYS.COMPLETED_CALLS).split(','), completedCalls);
setCache(CACHE_KEYS.COMPLETED_CALLS, completedCalls);
// Check if all necessary strings are initialized
if (areAllStringsInitialized()) {
const millisecondsNow = Date.now();
setCache(CACHE_KEYS.INIT_TIMESTAMP, millisecondsNow);
setCache(CACHE_KEYS.CACHE_VERSION, NAX_CACHE_VERSION);
console.log('✔ Cache initialized');
} else {
// If not all strings are initialized, retry initialization
console.log('❌ Not all strings are initialized. Retrying...');
executeOnce();
return;
}
// Call any other initialization functions here if needed
};
// Function to check if all necessary strings are initialized
const areAllStringsInitialized = () => {
// Implement logic to check if all necessary strings are initialized
// Return true if all strings are initialized, false otherwise
return getString('UI_LANG_name') != ""
};
// Call the function to execute the code
executeOnce();
// Set timer for regular UI refresh if enabled
setTimeout(() => {
// page refresh if configured
const refreshTime = getSetting("UI_REFRESH");
if (refreshTime && refreshTime !== "0" && refreshTime !== "") {
console.log("Refreshing page becasue UI_REFRESH setting enabled.");
newTimerRefreshData(clearCache, parseInt(refreshTime)*1000);
}
// Check if page needs to refresh due to setting changes
checkSettingChanges()
}, 10000);
console.log("init app-init.js");

514
front/js/cache.js Normal file
View File

@@ -0,0 +1,514 @@
/* -----------------------------------------------------------------------------
* NetAlertX
* Open Source Network Guard / WIFI & LAN intrusion detector
*
* cache.js - Front module. Cache primitives, settings, strings, and device
* data caching. Loaded FIRST — no dependencies on other NAX files.
* All cross-file calls (handleSuccess, showSpinner, etc.) are
* call-time dependencies resolved after page load.
*-------------------------------------------------------------------------------
# jokob@duck.com GNU GPLv3
----------------------------------------------------------------------------- */
// Cache version stamp — injected by header.php from the app's .VERSION file.
// Changes automatically on every release, busting stale localStorage caches.
// Falls back to a build-time constant so local dev without PHP still works.
const NAX_CACHE_VERSION = (typeof window.NAX_APP_VERSION !== 'undefined')
? window.NAX_APP_VERSION
: 'dev';
// -----------------------------------------------------------------------------
// Central registry of all localStorage cache keys.
// Use these constants (and the helper functions for dynamic keys) everywhere
// instead of bare string literals to prevent silent typo bugs.
// -----------------------------------------------------------------------------
const CACHE_KEYS = {
// --- Init flags (dynamic) ---
// Stores "true" when an AJAX init call completes. Use initFlag(name) below.
initFlag: (name) => `${name}_completed`,
// --- Settings ---
// Stores the value of a setting by its setKey. nax_set_<setKey>
setting: (key) => `nax_set_${key}`,
// Stores the resolved options array for a setting. nax_set_opt_<setKey>
settingOpts: (key) => `nax_set_opt_${key}`,
// --- Language strings ---
// Stores a translated string. pia_lang_<key>_<langCode>
langString: (key, langCode) => `pia_lang_${key}_${langCode}`,
LANG_FALLBACK: 'en_us', // fallback language code
// --- Devices ---
DEVICES_ALL: 'devicesListAll_JSON', // full device list from table_devices.json
DEVICES_TOPOLOGY: 'devicesListNew', // filtered/sorted list for network topology
// --- UI state ---
VISIBLE_MACS: 'ntx_visible_macs', // comma-separated MACs visible in current view
SHOW_ARCHIVED: 'showArchived', // topology show-archived toggle (network page)
SHOW_OFFLINE: 'showOffline', // topology show-offline toggle (network page)
// --- Internal init tracking ---
GRAPHQL_STARTED: 'graphQLServerStarted', // set when GraphQL server responds
STRINGS_COUNT: 'cacheStringsCountCompleted', // count of language packs loaded
COMPLETED_CALLS: 'completedCalls', // comma-joined list of completed init calls
INIT_TIMESTAMP: 'nax_init_timestamp', // ms timestamp of last successful cache init
CACHE_VERSION: 'nax_cache_version', // version stamp for auto-bust on deploy
};
// -----------------------------------------------------------------------------
// localStorage cache helpers
// -----------------------------------------------------------------------------
function getCache(key)
{
// check cache
cachedValue = localStorage.getItem(key)
if(cachedValue)
{
return cachedValue;
}
return "";
}
// -----------------------------------------------------------------------------
function setCache(key, data)
{
localStorage.setItem(key, data);
}
// -----------------------------------------------------------------------------
// Fetch data from a server-generated JSON file via query_json.php.
// Returns a Promise resolving with the "data" array from the response.
// -----------------------------------------------------------------------------
function fetchJson(file) {
return new Promise((resolve, reject) => {
$.get('php/server/query_json.php', { file: file, nocache: Date.now() })
.done((res) => resolve(res['data'] || []))
.fail((err) => reject(err));
});
}
// -----------------------------------------------------------------------------
// Safely parse and normalize device cache data.
// Handles both direct array format and { data: [...] } format.
// Returns an array, or empty array on failure.
function parseDeviceCache(cachedStr) {
if (!cachedStr || cachedStr === "") {
return [];
}
let parsed;
try {
parsed = JSON.parse(cachedStr);
} catch (err) {
console.error('[parseDeviceCache] Failed to parse:', err);
return [];
}
// If result is an object with a .data property, extract it (handles legacy format)
if (parsed && typeof parsed === 'object' && !Array.isArray(parsed) && Array.isArray(parsed.data)) {
console.debug('[parseDeviceCache] Extracting .data property from wrapper object');
parsed = parsed.data;
}
// Ensure result is an array
if (!Array.isArray(parsed)) {
console.error('[parseDeviceCache] Result is not an array:', parsed);
return [];
}
return parsed;
}
// -----------------------------------------------------------------------------
// Returns the API token, base URL, and a ready-to-use Authorization header
// object for all backend API calls. Centralises the repeated
// getSetting("API_TOKEN") + getApiBase() pattern.
// -----------------------------------------------------------------------------
function getAuthContext() {
const token = getSetting('API_TOKEN');
const apiBase = getApiBase();
return {
token,
apiBase,
authHeader: { 'Authorization': 'Bearer ' + token },
};
}
// -----------------------------------------------------------------------------
// Get settings from the .json file generated by the python backend
// and cache them, if available, with options
// -----------------------------------------------------------------------------
// -----------------------------------------------------------------------------
// Bootstrap: fetch API_TOKEN and GRAPHQL_PORT directly from app.conf via the
// PHP helper endpoint. Runs before cacheSettings so that API calls made during
// or after init always have a token available — even if table_settings.json
// hasn't been generated yet. Writes values into the setting() namespace so
// getSetting("API_TOKEN") and getSetting("GRAPHQL_PORT") work immediately.
// -----------------------------------------------------------------------------
function cacheApiConfig() {
return new Promise((resolve, reject) => {
if (getCache(CACHE_KEYS.initFlag('cacheApiConfig')) === 'true') {
resolve();
return;
}
$.get('php/server/app_config.php', { nocache: Date.now() })
.done((res) => {
if (res && res.api_token) {
setCache(CACHE_KEYS.setting('API_TOKEN'), res.api_token);
setCache(CACHE_KEYS.setting('GRAPHQL_PORT'), String(res.graphql_port || 20212));
handleSuccess('cacheApiConfig');
resolve();
} else {
console.warn('[cacheApiConfig] Response missing api_token — will rely on cacheSettings fallback');
resolve(); // non-fatal: cacheSettings will still populate these
}
})
.fail((err) => {
console.warn('[cacheApiConfig] Failed to reach app_config.php:', err);
resolve(); // non-fatal fallback
});
});
}
function cacheSettings()
{
return new Promise((resolve, reject) => {
if(getCache(CACHE_KEYS.initFlag('cacheSettings')) === "true")
{
resolve();
return;
}
// plugins.json may not exist on first boot — treat its absence as non-fatal
Promise.all([fetchJson('table_settings.json'), fetchJson('plugins.json').catch(() => [])])
.then(([settingsArr, pluginsArr]) => {
pluginsData = pluginsArr;
settingsData = settingsArr;
// Defensive: Accept either array or object with .data property
// for both settings and plugins
if (!Array.isArray(settingsData)) {
if (settingsData && Array.isArray(settingsData.data)) {
settingsData = settingsData.data;
} else {
console.error('[cacheSettings] settingsData is not an array:', settingsData);
reject(new Error('settingsData is not an array'));
return;
}
}
// Normalize plugins array too (may have { data: [...] } format)
if (!Array.isArray(pluginsData)) {
if (pluginsData && Array.isArray(pluginsData.data)) {
pluginsData = pluginsData.data;
} else {
console.warn('[cacheSettings] pluginsData is not an array, treating as empty');
pluginsData = [];
}
}
settingsData.forEach((set) => {
resolvedOptions = createArray(set.setOptions)
resolvedOptionsOld = resolvedOptions
setPlugObj = {};
options_params = [];
resolved = ""
// proceed only if first option item contains something to resolve
if( !set.setKey.includes("__metadata") &&
resolvedOptions.length != 0 &&
resolvedOptions[0].includes("{value}"))
{
// get setting definition from the plugin config if available
setPlugObj = getPluginSettingObject(pluginsData, set.setKey)
// check if options contains parameters and resolve
if(setPlugObj != {} && setPlugObj["options_params"])
{
// get option_params for {value} resolution
options_params = setPlugObj["options_params"]
if(options_params != [])
{
// handles only strings of length == 1
resolved = resolveParams(options_params, resolvedOptions[0])
if(resolved.includes('"')) // check if list of strings
{
resolvedOptions = `[${resolved}]`
} else // one value only
{
resolvedOptions = `["${resolved}"]`
}
}
}
}
setCache(CACHE_KEYS.setting(set.setKey), set.setValue)
setCache(CACHE_KEYS.settingOpts(set.setKey), resolvedOptions)
});
handleSuccess('cacheSettings');
resolve();
})
.catch((err) => { handleFailure('cacheSettings'); reject(err); });
});
}
// -----------------------------------------------------------------------------
// Get a setting options value by key
function getSettingOptions (key) {
result = getCache(CACHE_KEYS.settingOpts(key));
if (result == "")
{
result = []
}
return result;
}
// -----------------------------------------------------------------------------
// Get a setting value by key
function getSetting (key) {
result = getCache(CACHE_KEYS.setting(key));
return result;
}
// -----------------------------------------------------------------------------
// Get language string
// -----------------------------------------------------------------------------
function cacheStrings() {
return new Promise((resolve, reject) => {
if(getCache(CACHE_KEYS.initFlag('cacheStrings_v2')) === "true")
{
// Core strings are cached, but plugin strings may have failed silently on
// the first load (non-fatal fetch). Always re-fetch them so that plugin
// keys like "CSVBCKP_overwrite_description" are available without needing
// a full clearCache().
fetchJson('table_plugins_language_strings.json')
.catch((pluginError) => {
console.warn('[cacheStrings early-return] Plugin language strings unavailable (non-fatal):', pluginError);
return [];
})
.then((data) => {
if (!Array.isArray(data)) { data = []; }
data.forEach((langString) => {
setCache(CACHE_KEYS.langString(langString.stringKey, langString.languageCode), langString.stringValue);
});
resolve();
});
return;
}
// Create a promise for each language (include en_us by default as fallback)
languagesToLoad = ['en_us']
additionalLanguage = getLangCode()
if(additionalLanguage != 'en_us')
{
languagesToLoad.push(additionalLanguage)
}
console.log(languagesToLoad);
const languagePromises = languagesToLoad.map((language_code) => {
return new Promise((resolveLang, rejectLang) => {
// Fetch core strings and translations
$.get(`php/templates/language/${language_code}.json?nocache=${Date.now()}`)
.done((res) => {
// Iterate over each key-value pair and store the translations
Object.entries(res).forEach(([key, value]) => {
setCache(CACHE_KEYS.langString(key, language_code), value);
});
// Fetch strings and translations from plugins (non-fatal — file may
// not exist on first boot or immediately after a cache clear)
fetchJson('table_plugins_language_strings.json')
.catch((pluginError) => {
console.warn('[cacheStrings] Plugin language strings unavailable (non-fatal):', pluginError);
return []; // treat as empty list
})
.then((data) => {
// Defensive: ensure data is an array (fetchJson may return
// an object, undefined, or empty string on edge cases)
if (!Array.isArray(data)) { data = []; }
// Store plugin translations
data.forEach((langString) => {
setCache(CACHE_KEYS.langString(langString.stringKey, langString.languageCode), langString.stringValue);
});
// Handle successful completion of language processing
handleSuccess('cacheStrings_v2');
resolveLang();
});
})
.fail((error) => {
// Handle failure in core strings fetching
rejectLang(error);
});
});
});
// Wait for all language promises to complete
Promise.all(languagePromises)
.then(() => {
// All languages processed successfully
resolve();
})
.catch((error) => {
// Handle failure in any of the language processing
handleFailure('cacheStrings_v2');
reject(error);
});
});
}
// -----------------------------------------------------------------------------
// Get translated language string
function getString(key) {
function fetchString(key) {
lang_code = getLangCode();
let result = getCache(CACHE_KEYS.langString(key, lang_code));
if (isEmpty(result)) {
result = getCache(CACHE_KEYS.langString(key, CACHE_KEYS.LANG_FALLBACK));
}
return result;
}
if (isAppInitialized()) {
return fetchString(key);
} else {
callAfterAppInitialized(() => fetchString(key));
}
}
// -----------------------------------------------------------------------------
// Get current language ISO code.
// The UI_LANG setting value is always in the form "Name (code)", e.g. "English (en_us)".
// Extracting the code with a regex means this function never needs updating when a
// new language is added — the single source of truth is languages.json.
function getLangCode() {
UI_LANG = getSetting("UI_LANG");
const match = (UI_LANG || '').match(/\(([a-z]{2}_[a-z]{2})\)\s*$/i);
return match ? match[1].toLowerCase() : 'en_us';
}
// -----------------------------------------------------------------------------
// A function to get a device property using the mac address as key and DB column name as parameter
// for the value to be returned
function getDevDataByMac(macAddress, dbColumn) {
const sessionDataKey = CACHE_KEYS.DEVICES_ALL;
const devicesCache = getCache(sessionDataKey);
if (!devicesCache || devicesCache == "") {
console.warn(`[getDevDataByMac] Cache key "${sessionDataKey}" is empty — cache may not be initialized yet.`);
return null;
}
const devices = parseDeviceCache(devicesCache);
if (devices.length === 0) {
return null;
}
for (const device of devices) {
if (device["devMac"].toLowerCase() === macAddress.toLowerCase()) {
if(dbColumn)
{
return device[dbColumn];
}
else
{
return device
}
}
}
console.error("⚠ Device with MAC not found:" + macAddress)
return null; // Return a default value if MAC address is not found
}
// -----------------------------------------------------------------------------
/**
* 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(!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
// them in the flat format so parseDeviceCache never needs the fallback branch.
const raw = getCache(CACHE_KEYS.DEVICES_ALL);
if (raw) {
try {
const p = JSON.parse(raw);
if (p && typeof p === 'object' && !Array.isArray(p) && Array.isArray(p.data)) {
setCache(CACHE_KEYS.DEVICES_ALL, JSON.stringify(p.data));
}
} catch (e) { /* ignore malformed cache will be refreshed next init */ }
}
resolve();
return;
}
fetchJson('table_devices.json')
.then((arr) => {
devicesListAll_JSON = arr;
devicesListAll_JSON_str = JSON.stringify(devicesListAll_JSON)
if(devicesListAll_JSON_str == "")
{
showSpinner()
setTimeout(() => {
cacheDevices()
}, 1000);
}
setCache(CACHE_KEYS.DEVICES_ALL, devicesListAll_JSON_str)
handleSuccess('cacheDevices');
resolve();
})
.catch((err) => { handleFailure('cacheDevices'); reject(err); });
}
);
}
var devicesListAll_JSON = []; // this will contain a list off all devices

View File

@@ -12,46 +12,33 @@ var timerRefreshData = ''
var emptyArr = ['undefined', "", undefined, null, 'null'];
var UI_LANG = "English (en_us)";
const allLanguages = ["ar_ar","ca_ca","cs_cz","de_de",
"en_us","es_es","fa_fa","fr_fr",
"it_it","ja_jp","nb_no","pl_pl",
"pt_br","pt_pt","ru_ru","sv_sv",
"tr_tr","uk_ua","vi_vn","zh_cn"]; // needs to be same as in lang.php
// allLanguages is populated at init via fetchAllLanguages() from GET /languages.
// Do not hardcode this list — add new languages to languages.json instead.
let allLanguages = [];
var settingsJSON = {}
// NAX_CACHE_VERSION and CACHE_KEYS moved to cache.js
// getCache, setCache, fetchJson, getAuthContext moved to cache.js
// -----------------------------------------------------------------------------
// Simple session cache withe expiration managed via cookies
// Fetch the canonical language list from GET /languages and populate allLanguages.
// Must be called after the API token is available (e.g. alongside cacheStrings).
// -----------------------------------------------------------------------------
function getCache(key, noCookie = false)
{
// check cache
cachedValue = localStorage.getItem(key)
// console.log(cachedValue);
if(cachedValue)
{
// // check if not expired
// if(noCookie || getCookie(key + '_session_expiry') != "")
// {
return cachedValue;
// }
}
return "";
}
// -----------------------------------------------------------------------------
function setCache(key, data, expirationMinutes='')
{
localStorage.setItem(key, data);
// // create cookie if expiration set to handle refresh of data
// if (expirationMinutes != '')
// {
// setCookie ('cache_session_expiry', 'OK', 1)
// }
function fetchAllLanguages(apiToken) {
return fetch('/languages', {
headers: { 'Authorization': 'Bearer ' + apiToken }
})
.then(function(resp) { return resp.json(); })
.then(function(data) {
if (data && data.success && Array.isArray(data.languages)) {
allLanguages = data.languages.map(function(l) { return l.code; });
}
})
.catch(function(err) {
console.warn('[fetchAllLanguages] Failed to load language list:', err);
});
}
@@ -93,288 +80,13 @@ function deleteCookie (cookie) {
document.cookie = cookie + '=;expires=Thu, 01 Jan 1970 00:00:00 UTC';
}
// -----------------------------------------------------------------------------
function deleteAllCookies() {
// Array of cookies
var allCookies = document.cookie.split(";");
// For each cookie
for (var i = 0; i < allCookies.length; i++) {
var cookie = allCookies[i].trim();
var eqPos = cookie.indexOf("=");
var name = eqPos > -1 ? cookie.substr(0, eqPos) : cookie;
document.cookie = name + "=;expires=Thu, 01 Jan 1970 00:00:00 UTC";
}
}
// cacheApiConfig, cacheSettings, getSettingOptions, getSetting moved to cache.js
// -----------------------------------------------------------------------------
// Get settings from the .json file generated by the python backend
// and cache them, if available, with options
// -----------------------------------------------------------------------------
function cacheSettings()
{
return new Promise((resolve, reject) => {
if(!getCache('cacheSettings_completed') === true)
{
$.get('php/server/query_json.php', { file: 'table_settings.json', nocache: Date.now() }, function(resSet) {
$.get('php/server/query_json.php', { file: 'plugins.json', nocache: Date.now() }, function(resPlug) {
pluginsData = resPlug["data"];
settingsData = resSet["data"];
settingsData.forEach((set) => {
resolvedOptions = createArray(set.setOptions)
resolvedOptionsOld = resolvedOptions
setPlugObj = {};
options_params = [];
resolved = ""
// proceed only if first option item contains something to resolve
if( !set.setKey.includes("__metadata") &&
resolvedOptions.length != 0 &&
resolvedOptions[0].includes("{value}"))
{
// get setting definition from the plugin config if available
setPlugObj = getPluginSettingObject(pluginsData, set.setKey)
// check if options contains parameters and resolve
if(setPlugObj != {} && setPlugObj["options_params"])
{
// get option_params for {value} resolution
options_params = setPlugObj["options_params"]
if(options_params != [])
{
// handles only strings of length == 1
resolved = resolveParams(options_params, resolvedOptions[0])
if(resolved.includes('"')) // check if list of strings
{
resolvedOptions = `[${resolved}]`
} else // one value only
{
resolvedOptions = `["${resolved}"]`
}
}
}
}
setCache(`nax_set_${set.setKey}`, set.setValue)
setCache(`nax_set_opt_${set.setKey}`, resolvedOptions)
});
}).then(() => handleSuccess('cacheSettings', resolve())).catch(() => handleFailure('cacheSettings', reject("cacheSettings already completed"))); // handle AJAX synchronization
})
}
});
}
// -----------------------------------------------------------------------------
// Get a setting options value by key
function getSettingOptions (key) {
// handle initial load to make sure everything is set-up and cached
// handleFirstLoad()
result = getCache(`nax_set_opt_${key}`, true);
if (result == "")
{
// console.log(`Setting options with key "${key}" not found`)
result = []
}
return result;
}
// -----------------------------------------------------------------------------
// Get a setting value by key
function getSetting (key) {
// handle initial load to make sure everything is set-up and cached
// handleFirstLoad()
result = getCache(`nax_set_${key}`, true);
// if (result == "")
// {
// console.log(`Setting with key "${key}" not found`)
// }
return result;
}
// -----------------------------------------------------------------------------
// Get language string
// -----------------------------------------------------------------------------
function cacheStrings() {
return new Promise((resolve, reject) => {
// Create a promise for each language (include en_us by default as fallback)
languagesToLoad = ['en_us']
additionalLanguage = getLangCode()
if(additionalLanguage != 'en_us')
{
languagesToLoad.push(additionalLanguage)
}
console.log(languagesToLoad);
const languagePromises = languagesToLoad.map((language_code) => {
return new Promise((resolveLang, rejectLang) => {
// Fetch core strings and translations
$.get(`php/templates/language/${language_code}.json?nocache=${Date.now()}`)
.done((res) => {
// Iterate over each key-value pair and store the translations
Object.entries(res).forEach(([key, value]) => {
setCache(`pia_lang_${key}_${language_code}`, value);
});
// Fetch strings and translations from plugins
$.get('php/server/query_json.php', { file: 'table_plugins_language_strings.json', nocache: Date.now() })
.done((pluginRes) => {
const data = pluginRes["data"];
// Store plugin translations
data.forEach((langString) => {
setCache(`pia_lang_${langString.String_Key}_${langString.Language_Code}`, langString.String_Value);
});
// Handle successful completion of language processing
handleSuccess(`cacheStrings`, resolveLang);
})
.fail((pluginError) => {
// Handle failure in plugin strings fetching
rejectLang(pluginError);
});
})
.fail((error) => {
// Handle failure in core strings fetching
rejectLang(error);
});
});
});
// Wait for all language promises to complete
Promise.all(languagePromises)
.then(() => {
// All languages processed successfully
resolve();
})
.catch((error) => {
// Handle failure in any of the language processing
handleFailure('cacheStrings', reject);
});
});
}
// -----------------------------------------------------------------------------
// Get translated language string
function getString(key) {
function fetchString(key) {
lang_code = getLangCode();
let result = getCache(`pia_lang_${key}_${lang_code}`, true);
if (isEmpty(result)) {
result = getCache(`pia_lang_${key}_en_us`, true);
}
return result;
}
if (isAppInitialized()) {
return fetchString(key);
} else {
callAfterAppInitialized(() => fetchString(key));
}
}
// -----------------------------------------------------------------------------
// Get current language ISO code
// below has to match exactly the values in /front/php/templates/language/lang.php & /front/js/common.js
function getLangCode() {
UI_LANG = getSetting("UI_LANG");
let lang_code = 'en_us';
switch (UI_LANG) {
case 'English (en_us)':
lang_code = 'en_us';
break;
case 'Spanish (es_es)':
lang_code = 'es_es';
break;
case 'German (de_de)':
lang_code = 'de_de';
break;
case 'Farsi (fa_fa)':
lang_code = 'fa_fa';
break;
case 'French (fr_fr)':
lang_code = 'fr_fr';
break;
case 'Norwegian (nb_no)':
lang_code = 'nb_no';
break;
case 'Polish (pl_pl)':
lang_code = 'pl_pl';
break;
case 'Portuguese (pt_br)':
lang_code = 'pt_br';
break;
case 'Portuguese (pt_pt)':
lang_code = 'pt_pt';
break;
case 'Turkish (tr_tr)':
lang_code = 'tr_tr';
break;
case 'Swedish (sv_sv)':
lang_code = 'sv_sv';
break;
case 'Italian (it_it)':
lang_code = 'it_it';
break;
case 'Japanese (ja_jp)':
lang_code = 'ja_jp';
break;
case 'Russian (ru_ru)':
lang_code = 'ru_ru';
break;
case 'Chinese (zh_cn)':
lang_code = 'zh_cn';
break;
case 'Czech (cs_cz)':
lang_code = 'cs_cz';
break;
case 'Arabic (ar_ar)':
lang_code = 'ar_ar';
break;
case 'Catalan (ca_ca)':
lang_code = 'ca_ca';
break;
case 'Ukrainian (uk_uk)':
lang_code = 'uk_ua';
break;
case 'Vietnamese (vi_vn)':
lang_code = 'vi_vn';
break;
}
return lang_code;
}
// cacheStrings, getString, getLangCode moved to cache.js
const tz = getSetting("TIMEZONE") || 'Europe/Berlin';
const LOCALE = getSetting('UI_LOCALE') || 'en-GB';
@@ -718,14 +430,13 @@ function numberArrayFromString(data)
// -----------------------------------------------------------------------------
// Update network parent/child relationship (network tree)
function updateNetworkLeaf(leafMac, parentMac) {
const apiBase = getApiBase();
const apiToken = getSetting("API_TOKEN");
const { apiBase, authHeader } = getAuthContext();
const url = `${apiBase}/device/${leafMac}/update-column`;
$.ajax({
method: "POST",
url: url,
headers: { "Authorization": `Bearer ${apiToken}` },
headers: authHeader,
data: JSON.stringify({ columnName: "devParentMAC", columnValue: parentMac }),
contentType: "application/json",
success: function(response) {
@@ -1157,72 +868,7 @@ function isRandomMAC(mac)
return options;
}
// -----------------------------------------------------------------------------
// A function to get a device property using the mac address as key and DB column nakme as parameter
// for the value to be returned
function getDevDataByMac(macAddress, dbColumn) {
const sessionDataKey = 'devicesListAll_JSON';
const devicesCache = getCache(sessionDataKey);
if (!devicesCache || devicesCache == "") {
console.error(`Session variable "${sessionDataKey}" not found.`);
return null;
}
const devices = JSON.parse(devicesCache);
for (const device of devices) {
if (device["devMac"].toLowerCase() === macAddress.toLowerCase()) {
if(dbColumn)
{
return device[dbColumn];
}
else
{
return device
}
}
}
console.error("⚠ Device with MAC not found:" + macAddress)
return null; // Return a default value if MAC address is not found
}
// -----------------------------------------------------------------------------
// Cache the devices as one JSON
function cacheDevices()
{
return new Promise((resolve, reject) => {
$.get('php/server/query_json.php', { file: 'table_devices.json', nocache: Date.now() }, function(data) {
// console.log(data)
devicesListAll_JSON = data["data"]
devicesListAll_JSON_str = JSON.stringify(devicesListAll_JSON)
if(devicesListAll_JSON_str == "")
{
showSpinner()
setTimeout(() => {
cacheDevices()
}, 1000);
}
// console.log(devicesListAll_JSON_str);
setCache('devicesListAll_JSON', devicesListAll_JSON_str)
// console.log(getCache('devicesListAll_JSON'))
}).then(() => handleSuccess('cacheDevices', resolve())).catch(() => handleFailure('cacheDevices', reject("cacheDevices already completed"))); // handle AJAX synchronization
}
);
}
var devicesListAll_JSON = []; // this will contain a list off all devices
// getDevDataByMac, cacheDevices, devicesListAll_JSON moved to cache.js
// -----------------------------------------------------------------------------
function isEmpty(value)
@@ -1369,18 +1015,13 @@ function updateApi(apiEndpoints)
// value has to be in format event|param. e.g. run|ARPSCAN
action = `${getGuid()}|update_api|${apiEndpoints}`
// Get data from the server
const apiToken = getSetting("API_TOKEN");
const apiBaseUrl = getApiBase();
const { token: apiToken, apiBase: apiBaseUrl, authHeader } = getAuthContext();
const url = `${apiBaseUrl}/logs/add-to-execution-queue`;
$.ajax({
method: "POST",
url: url,
headers: {
"Authorization": "Bearer " + apiToken,
"Content-Type": "application/json"
},
headers: { ...authHeader, "Content-Type": "application/json" },
data: JSON.stringify({ action: action }),
success: function(data, textStatus) {
console.log(data)
@@ -1548,16 +1189,11 @@ function hideUIelements(setKey) {
function getDevicesList()
{
// Read cache (skip cookie expiry check)
devicesList = getCache('devicesListAll_JSON', true);
if (devicesList != '') {
devicesList = JSON.parse (devicesList);
} else {
devicesList = [];
}
const cached = getCache(CACHE_KEYS.DEVICES_ALL);
let devicesList = parseDeviceCache(cached);
// only loop thru the filtered down list
visibleDevices = getCache("ntx_visible_macs")
visibleDevices = getCache(CACHE_KEYS.VISIBLE_MACS)
if(visibleDevices != "") {
visibleDevicesMACs = visibleDevices.split(',');
@@ -1616,18 +1252,14 @@ function restartBackend() {
modalEventStatusId = 'modal-message-front-event'
const apiToken = getSetting("API_TOKEN");
const apiBaseUrl = getApiBase();
const { token: apiToken, apiBase: apiBaseUrl, authHeader } = getAuthContext();
const url = `${apiBaseUrl}/logs/add-to-execution-queue`;
// Execute
$.ajax({
method: "POST",
url: url,
headers: {
"Authorization": "Bearer " + apiToken,
"Content-Type": "application/json"
},
headers: { ...authHeader, "Content-Type": "application/json" },
data: JSON.stringify({ action: `cron_restart_backend` }),
success: function(data, textStatus) {
// showModalOk ('Result', data );
@@ -1642,237 +1274,8 @@ function restartBackend() {
})
}
// -----------------------------------------------------------------------------
// initialize
// -----------------------------------------------------------------------------
// -----------------------------------------------------------------------------
// Define a unique key for storing the flag in sessionStorage
const sessionStorageKey = "myScriptExecuted_common_js";
var completedCalls = []
var completedCalls_final = ['cacheSettings', 'cacheStrings', 'cacheDevices'];
var lang_completedCalls = 0;
// -----------------------------------------------------------------------------
// Clearing all the caches
function clearCache() {
showSpinner();
sessionStorage.clear();
localStorage.clear();
setTimeout(() => {
console.warn("clearChache called");
window.location.reload();
}, 500);
}
// ===================================================================
// DEPRECATED: checkSettingChanges() - Replaced by SSE-based manager
// Settings changes are now handled via SSE events
// Kept for backward compatibility, will be removed in future version
// ===================================================================
function checkSettingChanges() {
// SSE manager handles settings_changed events now
if (typeof netAlertXStateManager !== 'undefined' && netAlertXStateManager.initialized) {
return; // SSE handles this now
}
// Fallback for backward compatibility
$.get('php/server/query_json.php', { file: 'app_state.json', nocache: Date.now() }, function(appState) {
const importedMilliseconds = parseInt(appState["settingsImported"] * 1000);
const lastReloaded = parseInt(sessionStorage.getItem(sessionStorageKey + '_time'));
if (importedMilliseconds > lastReloaded) {
console.log("Cache needs to be refreshed because of setting changes");
setTimeout(() => {
clearCache();
}, 500);
}
});
}
// ===================================================================
// Display spinner and reload page if not yet initialized
async function handleFirstLoad(callback) {
if (!isAppInitialized()) {
await new Promise(resolve => setTimeout(resolve, 1000));
callback();
}
}
// ===================================================================
// Execute callback once the app is initialized and GraphQL server is running
async function callAfterAppInitialized(callback) {
if (!isAppInitialized() || !(await isGraphQLServerRunning())) {
setTimeout(() => {
callAfterAppInitialized(callback);
}, 500);
} else {
callback();
}
}
// ===================================================================
// Polling function to repeatedly check if the server is running
async function waitForGraphQLServer() {
const pollInterval = 2000; // 2 seconds between each check
let serverRunning = false;
while (!serverRunning) {
serverRunning = await isGraphQLServerRunning();
if (!serverRunning) {
console.log("GraphQL server not running, retrying in 2 seconds...");
await new Promise(resolve => setTimeout(resolve, pollInterval));
}
}
console.log("GraphQL server is now running.");
}
// -----------------------------------------------------------------------------
// Returns 1 if running, 0 otherwise
async function isGraphQLServerRunning() {
try {
const response = await $.get('php/server/query_json.php', { file: 'app_state.json', nocache: Date.now()});
console.log("graphQLServerStarted: " + response["graphQLServerStarted"]);
setCache("graphQLServerStarted", response["graphQLServerStarted"]);
return response["graphQLServerStarted"];
} catch (error) {
console.error("Failed to check GraphQL server status:", error);
return false;
}
}
// -----------------------------------------------------------------------------
// Check if the code has been executed before by checking sessionStorage
function isAppInitialized() {
lang_shouldBeCompletedCalls = getLangCode() == 'en_us' ? 1 : 2;
// check if each ajax call completed succesfully
$.each(completedCalls_final, function(index, call_name){
if(getCache(call_name + "_completed") != "true")
{
console.log(`[isAppInitialized] AJAX call ${call_name} unsuccesful: ${getCache(call_name + "_completed")}`)
return false;
}
});
// check if all required languages chached
if(parseInt(getCache("cacheStringsCountCompleted")) != lang_shouldBeCompletedCalls)
{
console.log(`[isAppInitialized] AJAX call cacheStrings unsuccesful: ${getCache("cacheStringsCountCompleted")} out of ${lang_shouldBeCompletedCalls}`)
return false;
}
return true;
}
// -----------------------------------------------------------------------------
// Main execution logic
async function executeOnce() {
showSpinner();
if (!isAppInitialized()) {
try {
await waitForGraphQLServer(); // Wait for the server to start
await cacheDevices();
await cacheSettings();
await cacheStrings();
console.log("All AJAX callbacks have completed");
onAllCallsComplete();
} catch (error) {
console.error("Error:", error);
}
}
}
// -----------------------------------------------------------------------------
// Function to handle successful completion of an AJAX call
const handleSuccess = (callName) => {
console.log(`AJAX call successful: ${callName}`);
if(callName.includes("cacheStrings"))
{
completed_tmp = getCache("cacheStringsCountCompleted");
completed_tmp == "" ? completed_tmp = 0 : completed_tmp = completed_tmp;
completed_tmp++;
setCache("cacheStringsCountCompleted", completed_tmp);
}
setCache(callName + "_completed", true)
};
// -----------------------------------------------------------------------------
// Function to handle failure of an AJAX call
const handleFailure = (callName, callback) => {
msg = `AJAX call ${callName} failed`
console.error(msg);
// Implement retry logic here if needed
// write_notification(msg, 'interrupt')
};
// -----------------------------------------------------------------------------
// Function to execute when all AJAX calls have completed
const onAllCallsComplete = () => {
completedCalls = mergeUniqueArrays(getCache('completedCalls').split(','), completedCalls);
setCache('completedCalls', completedCalls);
// Check if all necessary strings are initialized
if (areAllStringsInitialized()) {
sessionStorage.setItem(sessionStorageKey, "true");
const millisecondsNow = Date.now();
sessionStorage.setItem(sessionStorageKey + '_time', millisecondsNow);
console.log('✔ Cache initialized');
// setTimeout(() => {
// location.reload()
// }, 10);
} else {
// If not all strings are initialized, retry initialization
console.log('❌ Not all strings are initialized. Retrying...');
executeOnce();
return;
}
// Call any other initialization functions here if needed
};
// Function to check if all necessary strings are initialized
const areAllStringsInitialized = () => {
// Implement logic to check if all necessary strings are initialized
// Return true if all strings are initialized, false otherwise
return getString('UI_LANG_name') != ""
};
// Call the function to execute the code
executeOnce();
// Set timer for regular UI refresh if enabled
setTimeout(() => {
// page refresh if configured
const refreshTime = getSetting("UI_REFRESH");
if (refreshTime && refreshTime !== "0" && refreshTime !== "") {
console.log("Refreshing page becasue UI_REFRESH setting enabled.");
newTimerRefreshData(clearCache, parseInt(refreshTime)*1000);
}
// Check if page needs to refresh due to setting changes
checkSettingChanges()
}, 10000);
console.log("init common.js");
// App lifecycle (completedCalls, executeOnce, handleSuccess, clearCache, etc.) moved to app-init.js

133
front/js/device-columns.js Normal file
View File

@@ -0,0 +1,133 @@
// =============================================================================
// device-columns.js — Single source of truth for device field definitions.
//
// To add a new device column, update ONLY these places:
// 1. DEVICE_COLUMN_FIELDS — add the field name in the correct position
// 2. COLUMN_NAME_MAP — add Device_TableHead_X → fieldName mapping
// 3. NUMERIC_DEFAULTS — add fieldName if its default value is 0 not ""
// 4. GRAPHQL_EXTRA_FIELDS — add fieldName ONLY if it is NOT a display column
// (i.e. fetched for logic but not shown in table)
// 5. front/plugins/ui_settings/config.json options[]
// 6. front/php/templates/language/en_us.json Device_TableHead_X
// then run merge_translations.py for other languages
// 7. Backend: DB view + GraphQL type
// =============================================================================
// Ordered list of all device table column field names.
// Position here determines the positional index used throughout devices.php.
const DEVICE_COLUMN_FIELDS = [
"devName", // 0 Device_TableHead_Name
"devOwner", // 1 Device_TableHead_Owner
"devType", // 2 Device_TableHead_Type
"devIcon", // 3 Device_TableHead_Icon
"devFavorite", // 4 Device_TableHead_Favorite
"devGroup", // 5 Device_TableHead_Group
"devFirstConnection", // 6 Device_TableHead_FirstSession
"devLastConnection", // 7 Device_TableHead_LastSession
"devLastIP", // 8 Device_TableHead_LastIP
"devIsRandomMac", // 9 Device_TableHead_MAC (random MAC flag column)
"devStatus", // 10 Device_TableHead_Status
"devMac", // 11 Device_TableHead_MAC_full
"devIpLong", // 12 Device_TableHead_LastIPOrder
"rowid", // 13 Device_TableHead_Rowid
"devParentMAC", // 14 Device_TableHead_Parent_MAC
"devParentChildrenCount",// 15 Device_TableHead_Connected_Devices
"devLocation", // 16 Device_TableHead_Location
"devVendor", // 17 Device_TableHead_Vendor
"devParentPort", // 18 Device_TableHead_Port
"devGUID", // 19 Device_TableHead_GUID
"devSyncHubNode", // 20 Device_TableHead_SyncHubNodeName
"devSite", // 21 Device_TableHead_NetworkSite
"devSSID", // 22 Device_TableHead_SSID
"devSourcePlugin", // 23 Device_TableHead_SourcePlugin
"devPresentLastScan", // 24 Device_TableHead_PresentLastScan
"devAlertDown", // 25 Device_TableHead_AlertDown
"devCustomProps", // 26 Device_TableHead_CustomProps
"devFQDN", // 27 Device_TableHead_FQDN
"devParentRelType", // 28 Device_TableHead_ParentRelType
"devReqNicsOnline", // 29 Device_TableHead_ReqNicsOnline
"devVlan", // 30 Device_TableHead_Vlan
"devPrimaryIPv4", // 31 Device_TableHead_IPv4
"devPrimaryIPv6", // 32 Device_TableHead_IPv6
"devFlapping", // 33 Device_TableHead_Flapping
];
// Named index constants — eliminates all mapIndx(N) magic numbers.
// Access as COL.devFlapping, COL.devMac, etc.
const COL = Object.fromEntries(DEVICE_COLUMN_FIELDS.map((name, i) => [name, i]));
// Fields whose GraphQL response value should default to 0 instead of "".
const NUMERIC_DEFAULTS = new Set([
"devParentChildrenCount",
"devReqNicsOnline",
"devFlapping",
]);
// Fields fetched from GraphQL for internal logic only — not display columns.
// These are merged with DEVICE_COLUMN_FIELDS to build the GraphQL query.
const GRAPHQL_EXTRA_FIELDS = [
"devComments",
"devStaticIP",
"devScan",
"devLogEvents",
"devAlertEvents",
"devSkipRepeated",
"devLastNotification",
"devIsNew",
"devIsArchived",
"devIsSleeping",
];
// Row positions for extra (non-display) fields.
// In dataSrc, extra fields are appended AFTER the display columns in each row,
// so their position = DEVICE_COLUMN_FIELDS.length + their index in GRAPHQL_EXTRA_FIELDS.
// Use COL_EXTRA.fieldName to access them in createdCell rowData.
const COL_EXTRA = Object.fromEntries(
GRAPHQL_EXTRA_FIELDS.map((name, i) => [name, DEVICE_COLUMN_FIELDS.length + i])
);
// Maps Device_TableHead_* language keys to their GraphQL/DB field names.
// Used by getColumnNameFromLangString() in ui_components.js and by
// column filter logic in devices.php.
//
// NOTE: Device_TableHead_MAC maps to devMac (display), while position 9 in
// DEVICE_COLUMN_FIELDS uses devIsRandomMac (the random-MAC flag column).
// These are intentionally different; do not collapse them.
const COLUMN_NAME_MAP = {
"Device_TableHead_Name": "devName",
"Device_TableHead_Owner": "devOwner",
"Device_TableHead_Type": "devType",
"Device_TableHead_Icon": "devIcon",
"Device_TableHead_Favorite": "devFavorite",
"Device_TableHead_Group": "devGroup",
"Device_TableHead_FirstSession": "devFirstConnection",
"Device_TableHead_LastSession": "devLastConnection",
"Device_TableHead_LastIP": "devLastIP",
"Device_TableHead_MAC": "devMac",
"Device_TableHead_Status": "devStatus",
"Device_TableHead_MAC_full": "devMac",
"Device_TableHead_LastIPOrder": "devIpLong",
"Device_TableHead_Rowid": "rowid",
"Device_TableHead_Parent_MAC": "devParentMAC",
"Device_TableHead_Connected_Devices": "devParentChildrenCount",
"Device_TableHead_Location": "devLocation",
"Device_TableHead_Vendor": "devVendor",
"Device_TableHead_Port": "devParentPort",
"Device_TableHead_GUID": "devGUID",
"Device_TableHead_SyncHubNodeName": "devSyncHubNode",
"Device_TableHead_NetworkSite": "devSite",
"Device_TableHead_SSID": "devSSID",
"Device_TableHead_SourcePlugin": "devSourcePlugin",
"Device_TableHead_PresentLastScan": "devPresentLastScan",
"Device_TableHead_AlertDown": "devAlertDown",
"Device_TableHead_CustomProps": "devCustomProps",
"Device_TableHead_FQDN": "devFQDN",
"Device_TableHead_ParentRelType": "devParentRelType",
"Device_TableHead_ReqNicsOnline": "devReqNicsOnline",
"Device_TableHead_Vlan": "devVlan",
"Device_TableHead_IPv4": "devPrimaryIPv4",
"Device_TableHead_IPv6": "devPrimaryIPv6",
"Device_TableHead_Flapping": "devFlapping",
};
console.log("init device-columns.js");

266
front/js/network-api.js Normal file
View File

@@ -0,0 +1,266 @@
// network-api.js
// API calls and data loading functions for network topology
/**
* Load network nodes (network device types)
* Creates top-level tabs for each network device
*/
function loadNetworkNodes() {
// Create Top level tabs (List of network devices), explanation of the terminology below:
//
// Switch 1 (node)
// /(p1) \ (p2) <----- port numbers
// / \
// Smart TV (leaf) Switch 2 (node (for the PC) and leaf (for Switch 1))
// \
// PC (leaf) <------- leafs are not included in this SQL query
const rawSql = `
SELECT
parent.devName,
LOWER(parent.devMac) AS devMac,
parent.devPresentLastScan,
parent.devType,
LOWER(parent.devParentMAC) AS devParentMAC,
parent.devIcon,
parent.devAlertDown,
parent.devFlapping,
parent.devIsSleeping,
parent.devIsNew,
COUNT(child.devMac) AS node_ports_count
FROM DevicesView AS parent
LEFT JOIN DevicesView AS child
/* CRITICAL FIX: COLLATE NOCASE ensures the join works
even if devParentMAC is uppercase and devMac is lowercase
*/
ON child.devParentMAC = parent.devMac COLLATE NOCASE
WHERE parent.devType IN (${networkDeviceTypes})
AND parent.devIsArchived = 0
GROUP BY parent.devMac, parent.devName, parent.devPresentLastScan,
parent.devType, parent.devParentMAC, parent.devIcon, parent.devAlertDown, parent.devFlapping, parent.devIsSleeping, parent.devIsNew
ORDER BY parent.devName;
`;
const { token: apiToken, apiBase, authHeader } = getAuthContext();
// Verify token is available
if (!apiToken || apiToken.trim() === '') {
console.error("API_TOKEN not available. Settings may not be loaded yet.");
return;
}
const url = `${apiBase}/dbquery/read`;
$.ajax({
url,
method: "POST",
headers: { ...authHeader, "Content-Type": "application/json" },
data: JSON.stringify({ rawSql: btoa(unescape(encodeURIComponent(rawSql))) }),
contentType: "application/json",
success: function(data) {
const nodes = data.results || [];
renderNetworkTabs(nodes);
loadUnassignedDevices();
checkTabsOverflow();
},
error: function(xhr, status, error) {
console.error("Error loading network nodes:", status, error);
// Check if it's an auth error
if (xhr.status === 401) {
console.error("Authorization failed. API_TOKEN may be invalid or not yet loaded.");
}
}
});
}
/**
* Load device table with configurable SQL and rendering
* @param {Object} options - Configuration object
* @param {string} options.sql - SQL query to fetch devices
* @param {string} options.containerSelector - jQuery selector for container
* @param {string} options.tableId - ID for DataTable instance
* @param {string} options.wrapperHtml - HTML wrapper for table
* @param {boolean} options.assignMode - Whether to show assign/unassign buttons
*/
function loadDeviceTable({ sql, containerSelector, tableId, wrapperHtml = null, assignMode = true }) {
const { token: apiToken, apiBase, authHeader } = getAuthContext();
// Verify token is available
if (!apiToken || apiToken.trim() === '') {
console.error("API_TOKEN not available. Settings may not be loaded yet.");
return;
}
const url = `${apiBase}/dbquery/read`;
$.ajax({
url,
method: "POST",
headers: { ...authHeader, "Content-Type": "application/json" },
data: JSON.stringify({ rawSql: btoa(unescape(encodeURIComponent(sql))) }),
contentType: "application/json",
success: function(data) {
const devices = data.results || [];
const $container = $(containerSelector);
// end if nothing to show
if(devices.length == 0)
{
return;
}
$container.html(wrapperHtml);
const $table = $(`#${tableId}`);
const columns = [
{
title: assignMode ? getString('Network_ManageAssign') : getString('Network_ManageUnassign'),
data: 'devMac',
orderable: false,
width: '5%',
render: function (mac) {
// mac = mac.toLowerCase()
const label = assignMode ? 'assign' : 'unassign';
const btnClass = assignMode ? 'btn-primary' : 'btn-primary bg-red';
const btnText = assignMode ? getString('Network_ManageAssign') : getString('Network_ManageUnassign');
return `<button class="btn ${btnClass} btn-sm" data-myleafmac="${mac}" onclick="updateLeaf('${mac}','${label}')">
${btnText}
</button>`;
}
},
{
title: getString('Device_TableHead_Name'),
data: 'devName',
width: '15%',
render: function (name, type, device) {
return `<a href="./deviceDetails.php?mac=${device.devMac}" target="_blank">
<b class="anonymize">${name || '-'}</b>
</a>`;
}
},
{
title: getString('Device_TableHead_Status'),
data: 'devStatus',
width: '15%',
render: function (_, type, device) {
const badge = badgeFromDevice(device);
return `<a href="${badge.url}" class="badge ${badge.cssClass}">${badge.iconHtml} ${badge.label}</a>`;
}
},
{
title: 'MAC',
data: 'devMac',
width: '5%',
render: (data) => `<span class="anonymize">${data}</span>`
},
{
title: getString('Network_Table_IP'),
data: 'devLastIP',
width: '5%'
},
{
title: getString('Device_TableHead_Port'),
data: 'devParentPort',
width: '5%'
},
{
title: getString('Device_TableHead_Vendor'),
data: 'devVendor',
width: '20%'
}
].filter(Boolean);
tableConfig = {
data: devices,
columns: columns,
pageLength: 10,
order: assignMode ? [[2, 'asc']] : [],
responsive: true,
autoWidth: false,
searching: true,
createdRow: function (row, data) {
$(row).attr('data-mac', data.devMac);
}
};
if ($.fn.DataTable.isDataTable($table)) {
$table.DataTable(tableConfig).clear().rows.add(devices).draw();
} else {
$table.DataTable(tableConfig);
}
},
error: function(xhr, status, error) {
console.error("Error loading device table:", status, error);
}
});
}
/**
* Load unassigned devices (devices without parent)
*/
function loadUnassignedDevices() {
const sql = `
SELECT devMac, devPresentLastScan, devName, devLastIP, devVendor, devAlertDown, devParentPort, devFlapping, devIsSleeping, devIsNew, devStatus
FROM DevicesView
WHERE (devParentMAC IS NULL OR devParentMAC IN ("", " ", "undefined", "null"))
AND LOWER(devMac) NOT LIKE "%internet%"
AND devIsArchived = 0
ORDER BY devName ASC`;
const wrapperHtml = `
<div class="content">
<div id="unassignedDevices" class="box box-aqua box-body table-responsive">
<section>
<h5><i class="fa-solid fa-plug-circle-xmark"></i> ${getString('Network_UnassignedDevices')}</h5>
<table id="unassignedDevicesTable" class="table table-striped" width="100%"></table>
</section>
</div>
</div>`;
loadDeviceTable({
sql,
containerSelector: '#unassigned-devices-wrapper',
tableId: 'unassignedDevicesTable',
wrapperHtml,
assignMode: true
});
}
/**
* Load devices connected to a specific node
* @param {string} node_mac - MAC address of the parent node
*/
function loadConnectedDevices(node_mac) {
// Standardize the input just in case
const normalized_mac = node_mac.toLowerCase();
const sql = `
SELECT devName, devMac, devLastIP, devVendor, devPresentLastScan, devAlertDown, devParentPort, devVlan, devFlapping, devIsSleeping, devIsNew, devIsArchived,
CASE
WHEN devIsNew = 1 THEN 'New'
WHEN devPresentLastScan = 1 THEN 'On-line'
WHEN devIsSleeping = 1 THEN 'Sleeping'
WHEN devPresentLastScan = 0 AND devAlertDown != 0 THEN 'Down'
WHEN devIsArchived = 1 THEN 'Archived'
WHEN devPresentLastScan = 0 THEN 'Off-line'
ELSE 'Unknown status'
END AS devStatus
FROM DevicesView
/* Using COLLATE NOCASE here solves the 'TEXT' vs 'NOCASE' mismatch */
WHERE devParentMac = '${normalized_mac}' COLLATE NOCASE`;
// Keep the ID generation consistent
const id = normalized_mac.replace(/:/g, '_');
const wrapperHtml = `
<table class="table table-bordered table-striped node-leafs-table " id="table_leafs_${id}" data-node-mac="${normalized_mac}">
</table>`;
loadDeviceTable({
sql,
containerSelector: `#leafs_${id}`,
tableId: `table_leafs_${id}`,
wrapperHtml,
assignMode: false
});
}

135
front/js/network-events.js Normal file
View File

@@ -0,0 +1,135 @@
// network-events.js
// Event handlers and tree node click interactions
/**
* Handle network node click - select correct tab and scroll to appropriate content
* @param {HTMLElement} el - The clicked element
*/
function handleNodeClick(el)
{
isNetworkDevice = $(el).data("devisnetworknodedynamic") == 1;
targetTabMAC = ""
thisDevMac= $(el).data("mac");
if (isNetworkDevice == false)
{
targetTabMAC = $(el).data("parentmac");
} else
{
targetTabMAC = thisDevMac;
}
var targetTab = $(`a[data-mytabmac="${targetTabMAC}"]`);
if (targetTab.length) {
// Simulate a click event on the target tab
targetTab.click();
}
if (isNetworkDevice) {
// Smooth scroll to the tab content
$('html, body').animate({
scrollTop: targetTab.offset().top - 50
}, 500); // Adjust the duration as needed
} else {
$("tr.selected").removeClass("selected");
$(`tr[data-mac="${thisDevMac}"]`).addClass("selected");
const tableId = "table_leafs_" + targetTabMAC.replace(/:/g, '_');
const $table = $(`#${tableId}`).DataTable();
// Find the row index (in the full data set) that matches
const rowIndex = $table
.rows()
.eq(0)
.filter(function(idx) {
return $table.row(idx).node().getAttribute("data-mac") === thisDevMac;
});
if (rowIndex.length > 0) {
// Change to the page where this row is
$table.page(Math.floor(rowIndex[0] / $table.page.len())).draw(false);
// Delay needed so the row is in the DOM after page draw
setTimeout(() => {
const rowNode = $table.row(rowIndex[0]).node();
$(rowNode).addClass("selected");
// Smooth scroll to the row
$('html, body').animate({
scrollTop: $(rowNode).offset().top - 50
}, 500);
}, 0);
}
}
}
/**
* Handle window resize events to recheck tab overflow
*/
let resizeTimeout;
$(window).on('resize', function () {
clearTimeout(resizeTimeout);
resizeTimeout = setTimeout(() => {
checkTabsOverflow();
}, 100);
});
/**
* Initialize page on document ready
* Sets up toggle filters and event handlers
*/
$(document).ready(function () {
// Restore cached values on load
const cachedOffline = getCache(CACHE_KEYS.SHOW_OFFLINE);
if (cachedOffline !== null) {
$('input[name="showOffline"]').prop('checked', cachedOffline === 'true');
}
const cachedArchived = getCache(CACHE_KEYS.SHOW_ARCHIVED);
if (cachedArchived !== null) {
$('input[name="showArchived"]').prop('checked', cachedArchived === 'true');
}
// Function to enable/disable showArchived based on showOffline
function updateArchivedToggle() {
const isOfflineChecked = $('input[name="showOffline"]').is(':checked');
const archivedToggle = $('input[name="showArchived"]');
if (!isOfflineChecked) {
archivedToggle.prop('checked', false);
archivedToggle.prop('disabled', true);
setCache(CACHE_KEYS.SHOW_ARCHIVED, false);
} else {
archivedToggle.prop('disabled', false);
}
}
// Initial state on load
updateArchivedToggle();
// Bind change event for both toggles
$('input[name="showOffline"], input[name="showArchived"]').on('change', function () {
const name = $(this).attr('name');
const value = $(this).is(':checked');
// setCache(name, value) works because CACHE_KEYS.SHOW_OFFLINE === 'showOffline'
// and CACHE_KEYS.SHOW_ARCHIVED === 'showArchived' — matches the DOM input name attr.
setCache(name, value);
// Update state of showArchived if showOffline changed
if (name === 'showOffline') {
updateArchivedToggle();
}
// Refresh page after a brief delay to ensure cache is written
setTimeout(() => {
location.reload();
}, 100);
});
// init pop up hover boxes for device details
initHoverNodeInfo();
});

152
front/js/network-init.js Normal file
View File

@@ -0,0 +1,152 @@
// network-init.js
// Main initialization and data loading logic for network topology
// Global variables needed by other modules
var networkDeviceTypes = "";
var showArchived = false;
var showOffline = false;
/**
* Initialize network topology on page load
* Fetches all devices and sets up the tree visualization
*/
function initNetworkTopology() {
networkDeviceTypes = getSetting("NETWORK_DEVICE_TYPES").replace("[", "").replace("]", "");
showArchived = getCache(CACHE_KEYS.SHOW_ARCHIVED) === "true";
showOffline = getCache(CACHE_KEYS.SHOW_OFFLINE) === "true";
console.log('showArchived:', showArchived);
console.log('showOffline:', showOffline);
// Always get all devices
const rawSql = `
SELECT *,
LOWER(devMac) AS devMac,
LOWER(devParentMAC) AS devParentMAC,
CASE
WHEN devPresentLastScan = 1 THEN 'On-line'
WHEN devIsSleeping = 1 THEN 'Sleeping'
WHEN devAlertDown != 0 AND devPresentLastScan = 0 THEN 'Down'
ELSE 'Off-line'
END AS devStatus,
CASE
WHEN devType IN (${networkDeviceTypes}) THEN 1
ELSE 0
END AS devIsNetworkNodeDynamic
FROM DevicesView a
`;
const { token: apiToken, apiBase, authHeader } = getAuthContext();
// Verify token is available before making API call
if (!apiToken || apiToken.trim() === '') {
console.error("API_TOKEN not available. Settings may not be loaded yet. Retrying in 500ms...");
// Retry after a short delay to allow settings to load
setTimeout(() => {
initNetworkTopology();
}, 500);
return;
}
const url = `${apiBase}/dbquery/read`;
$.ajax({
url,
method: "POST",
headers: { ...authHeader, "Content-Type": "application/json" },
data: JSON.stringify({ rawSql: btoa(unescape(encodeURIComponent(rawSql))) }),
contentType: "application/json",
success: function(data) {
console.log(data);
const allDevices = data.results || [];
console.log(allDevices);
if (!allDevices || allDevices.length === 0) {
showModalOK(getString('Gen_Warning'), getString('Network_NoDevices'));
return;
}
// Count totals for UI
let archivedCount = 0;
let offlineCount = 0;
allDevices.forEach(device => {
if (parseInt(device.devIsArchived) === 1) archivedCount++;
if (parseInt(device.devPresentLastScan) === 0 && parseInt(device.devIsArchived) === 0) offlineCount++;
});
if(archivedCount > 0)
{
$('#showArchivedNumber').text(`(${archivedCount})`);
}
if(offlineCount > 0)
{
$('#showOfflineNumber').text(`(${offlineCount})`);
}
// Now apply UI filter based on toggles (always keep root)
const filteredDevices = allDevices.filter(device => {
const isRoot = (device.devMac || '').toLowerCase() === 'internet';
if (isRoot) return true;
if (!showArchived && parseInt(device.devIsArchived) === 1) return false;
if (!showOffline && parseInt(device.devPresentLastScan) === 0) return false;
return true;
});
// Sort filtered devices
const orderTopologyBy = createArray(getSetting("UI_TOPOLOGY_ORDER"));
const devicesSorted = filteredDevices.sort((a, b) => {
const parsePort = (port) => {
const parsed = parseInt(port, 10);
return isNaN(parsed) ? Infinity : parsed;
};
switch (orderTopologyBy[0]) {
case "Name":
// ensuring string
const nameA = (a.devName ?? "").toString();
const nameB = (b.devName ?? "").toString();
const nameCompare = nameA.localeCompare(nameB);
return nameCompare !== 0
? nameCompare
: parsePort(a.devParentPort) - parsePort(b.devParentPort);
case "Port":
return parsePort(a.devParentPort) - parsePort(b.devParentPort);
default:
return a.rowid - b.rowid;
}
});
setCache(CACHE_KEYS.DEVICES_TOPOLOGY, JSON.stringify(devicesSorted));
deviceListGlobal = devicesSorted;
// Render filtered result
initTree(getHierarchy());
loadNetworkNodes();
attachTreeEvents();
},
error: function(xhr, status, error) {
console.error("Error loading topology data:", status, error);
if (xhr.status === 401) {
console.error("Authorization failed! API_TOKEN may be invalid. Check that API_TOKEN setting is correct and not empty.");
showMessage("Authorization Failed: API_TOKEN setting may be invalid or not loaded. Please refresh the page.");
}
}
});
}
// Initialize on page load
$(document).ready(function () {
// show spinning icon
showSpinner();
// Start loading the network topology
initNetworkTopology();
});

256
front/js/network-tabs.js Normal file
View File

@@ -0,0 +1,256 @@
// network-tabs.js
// Tab management and tab content rendering functions
/**
* Render network tabs from nodes
* @param {Array} nodes - Array of network node objects
*/
function renderNetworkTabs(nodes) {
let html = '';
nodes.forEach((node, i) => {
const iconClass = node.devPresentLastScan == 1 ? "text-green" :
(node.devIsSleeping == 1 ? "text-aqua" :
(node.devAlertDown == 1 ? "text-red" : "text-gray50"));
const portLabel = node.node_ports_count ? ` (${node.node_ports_count})` : '';
const icon = atob(node.devIcon);
const id = node.devMac.replace(/:/g, '_');
html += `
<li class="networkNodeTabHeaders ${i === 0 ? 'active' : ''}">
<a href="#${id}" data-mytabmac="${node.devMac}" id="${id}_id" data-toggle="tab" title="${node.devName}">
<div class="icon ${iconClass}">${icon}</div>
<span class="node-name">${node.devName}</span>${portLabel}
</a>
</li>`;
});
$('.nav-tabs').html(html);
// populate tabs
renderNetworkTabContent(nodes);
// init selected (first) tab
initTab();
// init selected node highlighting
initSelectedNodeHighlighting()
// Register events on tab change
$('a[data-toggle="tab"]').on('shown.bs.tab', function (e) {
initSelectedNodeHighlighting()
});
}
/**
* Render content for each network tab
* @param {Array} nodes - Array of network node objects
*/
function renderNetworkTabContent(nodes) {
$('.tab-content').empty();
nodes.forEach((node, i) => {
const id = node.devMac.replace(/:/g, '_').toLowerCase();
const badge = badgeFromDevice(node);
const badgeHtml = `<a href="${badge.url}" class="badge ${badge.cssClass}">${badge.iconHtml} ${badge.label}</a>`;
const parentId = node.devParentMAC.replace(/:/g, '_');
isRootNode = node.devParentMAC == "";
const paneHtml = `
<div class="tab-pane box box-aqua box-body ${i === 0 ? 'active' : ''}" id="${id}">
<h5><i class="fa fa-server"></i> ${getString('Network_Node')}</h5>
<div class="mb-3 row">
<label class="col-sm-3 col-form-label fw-bold">${getString('DevDetail_Tab_Details')}</label>
<div class="col-sm-9">
<a href="./deviceDetails.php?mac=${node.devMac}" target="_blank" class="anonymize">${node.devName}</a>
</div>
</div>
<div class="mb-3 row">
<label class="col-sm-3 col-form-label fw-bold">MAC</label>
<div class="col-sm-9 anonymize">${node.devMac}</div>
</div>
<div class="mb-3 row">
<label class="col-sm-3 col-form-label fw-bold">${getString('Device_TableHead_Type')}</label>
<div class="col-sm-9">${node.devType}</div>
</div>
<div class="mb-3 row">
<label class="col-sm-3 col-form-label fw-bold">${getString('Device_TableHead_Status')}</label>
<div class="col-sm-9">${badgeHtml}</div>
</div>
<div class="mb-3 row">
<label class="col-sm-3 col-form-label fw-bold">${getString('Network_Parent')}</label>
<div class="col-sm-9">
${isRootNode ? '' : `<a class="anonymize" href="#">`}
<span my-data-mac="${node.devParentMAC}" data-mac="${node.devParentMAC}" data-devIsNetworkNodeDynamic="1" onclick="handleNodeClick(this)">
${isRootNode ? getString('Network_Root') : getDevDataByMac(node.devParentMAC, "devName")}
</span>
${isRootNode ? '' : `</a>`}
</div>
</div>
<hr/>
<div class="box box-aqua box-body" id="connected">
<h5>
<i class="fa fa-sitemap fa-rotate-270"></i>
${getString('Network_Connected')}
</h5>
<div id="leafs_${id}" class="table-responsive"></div>
</div>
</div>
`;
$('.tab-content').append(paneHtml);
loadConnectedDevices(node.devMac);
});
}
/**
* Initialize the active tab based on cache or query parameter
*/
function initTab()
{
key = "activeNetworkTab"
// default selection
selectedTab = "Internet_id"
// the #target from the url
target = getQueryString('mac')
// update cookie if target specified
if(target != "")
{
setCache(key, target.replaceAll(":","_")+'_id') // _id is added so it doesn't conflict with AdminLTE tab behavior
}
// get the tab id from the cookie (already overridden by the target)
if(!emptyArr.includes(getCache(key)))
{
selectedTab = getCache(key);
}
// Activate panel
$('.nav-tabs a[id='+ selectedTab +']').tab('show');
// When changed save new current tab
$('a[data-toggle="tab"]').on('shown.bs.tab', function (e) {
setCache(key, $(e.target).attr('id'))
});
}
/**
* Highlight the currently selected node in the tree
*/
function initSelectedNodeHighlighting()
{
var currentNodeMac = $(".networkNodeTabHeaders.active a").data("mytabmac");
// change highlighted node in the tree
selNode = $("#networkTree .highlightedNode")[0]
console.log(selNode)
if(selNode)
{
$(selNode).attr('class', $(selNode).attr('class').replace('highlightedNode'))
}
newSelNode = $("#networkTree div[data-mac='"+currentNodeMac+"']")[0]
console.log(newSelNode)
$(newSelNode).attr('class', $(newSelNode).attr('class') + ' highlightedNode')
}
/**
* Update a device's network assignment
* @param {string} leafMac - MAC address of device to update
* @param {string} action - 'assign' or 'unassign'
*/
function updateLeaf(leafMac, action) {
console.log(leafMac); // child
console.log(action); // action
const nodeMac = $(".networkNodeTabHeaders.active a").data("mytabmac") || "";
if (action === "assign") {
if (!nodeMac) {
showMessage(getString("Network_Cant_Assign_No_Node_Selected"));
} else if (leafMac.toLowerCase().includes("internet")) {
showMessage(getString("Network_Cant_Assign"));
} else {
saveData("updateNetworkLeaf", leafMac, nodeMac);
setTimeout(() => location.reload(), 500);
}
} else if (action === "unassign") {
saveData("updateNetworkLeaf", leafMac, "");
setTimeout(() => location.reload(), 500);
} else {
console.warn("Unknown action:", action);
}
}
/**
* Dynamically show/hide tab names based on available space
* Hides tab names when tabs overflow, shows them again when space is available
*/
function checkTabsOverflow() {
const $ul = $('.nav-tabs');
const $lis = $ul.find('li');
// First measure widths with current state
let totalWidth = 0;
$lis.each(function () {
totalWidth += $(this).outerWidth(true);
});
const ulWidth = $ul.width();
const isOverflowing = totalWidth > ulWidth;
if (isOverflowing) {
if (!$ul.hasClass('hide-node-names')) {
$ul.addClass('hide-node-names');
// Re-check: did hiding fix it?
requestAnimationFrame(() => {
let newTotal = 0;
$lis.each(function () {
newTotal += $(this).outerWidth(true);
});
if (newTotal > $ul.width()) {
// Still overflowing — do nothing, keep class
}
});
}
} else {
if ($ul.hasClass('hide-node-names')) {
$ul.removeClass('hide-node-names');
// Re-check: did un-hiding break it?
requestAnimationFrame(() => {
let newTotal = 0;
$lis.each(function () {
newTotal += $(this).outerWidth(true);
});
if (newTotal > $ul.width()) {
// Oops, that broke it — re-hide
$ul.addClass('hide-node-names');
}
});
}
}
}

354
front/js/network-tree.js Normal file
View File

@@ -0,0 +1,354 @@
// network-tree.js
// Tree hierarchy construction and rendering functions
// Global state variables
var leafNodesCount = 0;
var visibleNodesCount = 0;
var parentNodesCount = 0;
var hiddenMacs = []; // hidden children
var hiddenChildren = [];
var deviceListGlobal = null;
var myTree;
/**
* Recursively get children nodes and build a tree
* @param {Object} node - Current node
* @param {Array} list - Full device list
* @param {string} path - Path to current node
* @param {Array} visited - Visited nodes (for cycle detection)
* @returns {Object} Tree node with children
*/
function getChildren(node, list, path, visited = [])
{
var children = [];
// Check for infinite recursion by seeing if the node has been visited before
if (visited.includes(node.devMac.toLowerCase())) {
console.error("Infinite recursion detected at node:", node.devMac);
write_notification("[ERROR] ⚠ Infinite recursion detected. You probably have assigned the Internet node to another children node or to itself. Please open a new issue on GitHub and describe how you did it.", 'interrupt')
return { error: "Infinite recursion detected", node: node.devMac };
}
// Add current node to visited list
visited.push(node.devMac.toLowerCase());
// Loop through all items to find children of the current node
for (var i in list) {
const item = list[i];
const parentMac = item.devParentMAC?.toLowerCase() || ""; // null-safe
const nodeMac = node.devMac?.toLowerCase() || ""; // null-safe
if (parentMac != "" && parentMac == nodeMac && !hiddenMacs.includes(parentMac)) {
visibleNodesCount++;
// Process children recursively, passing a copy of the visited list
children.push(getChildren(list[i], list, path + ((path == "") ? "" : '|') + parentMac, visited));
}
}
// Track leaf and parent node counts
if (children.length == 0) {
leafNodesCount++;
} else {
parentNodesCount++;
}
// console.log(node);
return {
devName: node.devName,
path: path,
devMac: node.devMac,
devParentPort: node.devParentPort,
id: node.devMac,
devParentMAC: node.devParentMAC,
devIcon: node.devIcon,
devType: node.devType,
devIsNetworkNodeDynamic: node.devIsNetworkNodeDynamic,
devVendor: node.devVendor,
devLastConnection: node.devLastConnection,
devFirstConnection: node.devFirstConnection,
devLastIP: node.devLastIP,
devStatus: node.devStatus,
devPresentLastScan: node.devPresentLastScan,
devFlapping: node.devFlapping,
devAlertDown: node.devAlertDown,
devIsSleeping: node.devIsSleeping || 0,
devIsArchived: node.devIsArchived || 0,
devIsNew: node.devIsNew || 0,
hasChildren: children.length > 0 || hiddenMacs.includes(node.devMac),
devParentRelType: node.devParentRelType,
devVlan: node.devVlan,
devSSID: node.devSSID,
hiddenChildren: hiddenMacs.includes(node.devMac),
qty: children.length,
children: children
};
}
/**
* Build complete hierarchy starting from the Internet node
* @returns {Object} Root hierarchy object
*/
function getHierarchy()
{
// reset counters before rebuilding the hierarchy
leafNodesCount = 0;
visibleNodesCount = 0;
parentNodesCount = 0;
let internetNode = null;
for(i in deviceListGlobal)
{
if(deviceListGlobal[i].devMac.toLowerCase() == 'internet')
{
internetNode = deviceListGlobal[i];
return (getChildren(internetNode, deviceListGlobal, ''))
break;
}
}
if (!internetNode) {
showModalOk(
getString('Network_Configuration_Error'),
getString('Network_Root_Not_Configured')
);
console.error("getHierarchy(): Internet node not found");
return null;
}
}
/**
* Toggle collapse/expand state of a subtree
* @param {string} parentMac - MAC address of parent node to toggle
* @param {string} treePath - Path in tree (colon-separated)
*/
function toggleSubTree(parentMac, treePath)
{
treePath = treePath.split('|')
parentMac = parentMac.toLowerCase()
if(!hiddenMacs.includes(parentMac))
{
hiddenMacs.push(parentMac)
}
else
{
removeItemFromArray(hiddenMacs, parentMac)
}
updatedTree = getHierarchy()
myTree.refresh(updatedTree);
// re-attach any onclick events
attachTreeEvents();
}
/**
* Attach click events to tree collapse/expand controls
*/
function attachTreeEvents()
{
// toggle subtree functionality
$("div[data-mytreemac]").each(function(){
$(this).attr('onclick', 'toggleSubTree("'+$(this).attr('data-mytreemac')+'","'+ $(this).attr('data-mytreepath')+'")')
});
}
/**
* Convert pixels to em units
* @param {number} px - Pixel value
* @param {HTMLElement} element - Reference element for font-size
* @returns {number} Value in em units
*/
function pxToEm(px, element) {
var baseFontSize = parseFloat($(element || "body").css("font-size"));
return px / baseFontSize;
}
/**
* Convert em units to pixels
* @param {number} em - Value in em units
* @param {HTMLElement} element - Reference element for font-size
* @returns {number} Value in pixels (rounded)
*/
function emToPx(em, element) {
var baseFontSize = parseFloat($(element || "body").css("font-size"));
return Math.round(em * baseFontSize);
}
/**
* Initialize tree visualization
* @param {Object} myHierarchy - Hierarchy object to render
*/
function initTree(myHierarchy)
{
if(myHierarchy && myHierarchy.type !== "")
{
// calculate the drawing area based on the tree width and available screen size
let baseFontSize = parseFloat($('html').css('font-size'));
let treeAreaHeight = ($(window).height() - 155); ;
let minNodeWidth = 60 // min safe node width not breaking the tree
// calculate the font size of the leaf nodes to fit everything into the tree area
leafNodesCount == 0 ? 1 : leafNodesCount;
emSize = pxToEm((treeAreaHeight/(leafNodesCount)).toFixed(2));
// let screenWidthEm = pxToEm($('.networkTable').width()-15);
let minTreeWidthPx = parentNodesCount * minNodeWidth;
let actualWidthPx = $('.networkTable').width() - 15;
let finalWidthPx = Math.max(actualWidthPx, minTreeWidthPx);
// override original value
let screenWidthEm = pxToEm(finalWidthPx);
// handle canvas and node size if only a few nodes
emSize > 1 ? emSize = 1 : emSize = emSize;
let nodeHeightPx = emToPx(emSize*1);
let nodeWidthPx = emToPx(screenWidthEm / (parentNodesCount));
// handle if only a few nodes
nodeWidthPx > 160 ? nodeWidthPx = 160 : nodeWidthPx = nodeWidthPx;
if (nodeWidthPx < minNodeWidth) nodeWidthPx = minNodeWidth; // minimum safe width
console.log("Calculated nodeWidthPx =", nodeWidthPx, "emSize =", emSize , " screenWidthEm:", screenWidthEm, " emToPx(screenWidthEm):" , emToPx(screenWidthEm));
// init the drawing area size
$("#networkTree").attr('style', `height:${treeAreaHeight}px; width:${emToPx(screenWidthEm)}px`)
console.log(Treeviz);
myTree = Treeviz.create({
htmlId: "networkTree",
renderNode: nodeData => {
(!emptyArr.includes(nodeData.data.devParentPort)) ? port = nodeData.data.devParentPort : port = "";
(port == "" || port == 0 || port == 'None' ) ? portBckgIcon = `<i class="fa fa-wifi"></i>` : portBckgIcon = `<i class="fa fa-ethernet"></i>`;
portHtml = (port == "" || port == 0 || port == 'None' ) ? " &nbsp " : port;
// Build HTML for individual nodes in the network diagram
deviceIcon = (!emptyArr.includes(nodeData.data.devIcon)) ?
`<div class="netIcon">
${atob(nodeData.data.devIcon)}
</div>` : "";
devicePort = `<div class="netPort"
style="width:${emSize}em;height:${emSize}em">
${portHtml}</div>
<div class="portBckgIcon"
style="margin-left:-${emSize*0.7}em;">
${portBckgIcon}
</div>`;
collapseExpandIcon = nodeData.data.hiddenChildren ?
"square-plus" : "square-minus";
// generate +/- icon if node has children nodes
collapseExpandHtml = nodeData.data.hasChildren ?
`<div class="netCollapse"
style="font-size:${nodeHeightPx/2}px;top:${Math.floor(nodeHeightPx / 4)}px"
data-mytreepath="${nodeData.data.path}"
data-mytreemac="${nodeData.data.devMac}">
<i class="fa fa-${collapseExpandIcon} pointer"></i>
</div>` : "";
selectedNodeMac = $(".nav-tabs-custom .active a").attr('data-mytabmac')
highlightedCss = nodeData.data.devMac == selectedNodeMac ?
" highlightedNode " : "";
cssNodeType = nodeData.data.devIsNetworkNodeDynamic ?
" node-network-device " : " node-standard-device ";
networkHardwareIcon = nodeData.data.devIsNetworkNodeDynamic ? `<span class="network-hw-icon">
<i class="fa-solid fa-hard-drive"></i>
</span>` : "";
const badgeConf = badgeFromDevice(nodeData.data);
return result = `<div
class="node-inner hover-node-info box pointer ${highlightedCss} ${cssNodeType}"
style="height:${nodeHeightPx}px;font-size:${nodeHeightPx-5}px;"
onclick="handleNodeClick(this)"
data-mac="${nodeData.data.devMac}"
data-parentMac="${nodeData.data.devParentMAC}"
data-name="${nodeData.data.devName}"
data-ip="${nodeData.data.devLastIP}"
data-mac="${nodeData.data.devMac}"
data-vendor="${nodeData.data.devVendor}"
data-type="${nodeData.data.devType}"
data-devIsNetworkNodeDynamic="${nodeData.data.devIsNetworkNodeDynamic}"
data-lastseen="${nodeData.data.devLastConnection}"
data-firstseen="${nodeData.data.devFirstConnection}"
data-relationship="${nodeData.data.devParentRelType}"
data-flapping="${nodeData.data.devFlapping}"
data-sleeping="${nodeData.data.devIsSleeping || 0}"
data-archived="${nodeData.data.devIsArchived || 0}"
data-isnew="${nodeData.data.devIsNew || 0}"
data-status="${nodeData.data.devStatus}"
data-present="${nodeData.data.devPresentLastScan}"
data-alertdown="${nodeData.data.devAlertDown}"
data-icon="${nodeData.data.devIcon}"
>
<div class="netNodeText">
<strong><span>${devicePort} <span class="${badgeConf.cssText}">${deviceIcon}</span></span>
<span class="spanNetworkTree anonymizeDev" style="width:${nodeWidthPx-50}px">${nodeData.data.devName}</span>
${networkHardwareIcon}
</strong>
</div>
</div>
${collapseExpandHtml}`;
},
mainAxisNodeSpacing: 'auto',
// secondaryAxisNodeSpacing: 0.3,
nodeHeight: nodeHeightPx,
nodeWidth: nodeWidthPx,
marginTop: '5',
isHorizontal : true,
hasZoom: true,
hasPan: true,
marginLeft: '10',
marginRight: '10',
idKey: "devMac",
hasFlatData: false,
relationnalField: "children",
linkLabel: {
render: (parent, child) => {
// Return text or HTML to display on the connection line
connectionLabel = (child?.data.devVlan ?? "") + "/" + (child?.data.devSSID ?? "");
if(connectionLabel == "/")
{
connectionLabel = "";
}
return connectionLabel;
// or with HTML:
// return "<tspan><strong>reports to</strong></tspan>";
},
color: "#336c87ff", // Label text color (optional)
fontSize: nodeHeightPx - 5 // Label font size in px (optional)
},
linkWidth: (nodeData) => 2,
linkColor: (nodeData) => {
relConf = getRelationshipConf(nodeData.data.devParentRelType)
return relConf.color;
}
// onNodeClick: (nodeData) => handleNodeClick(nodeData),
});
console.log(deviceListGlobal);
myTree.refresh(myHierarchy);
// hide spinning icon
hideSpinner()
} else
{
console.error("getHierarchy() not returning expected result");
}
}

View File

@@ -165,6 +165,27 @@ class NetAlertXStateManager {
.html(displayTime)
.attr('data-build-time', buildTime);
// 4. Trigger cache clear if settings were imported after last init
if (appState["settingsImported"]) {
const importedMs = parseInt(appState["settingsImported"] * 1000);
const lastReloaded = parseInt(getCache(CACHE_KEYS.INIT_TIMESTAMP));
if (importedMs > lastReloaded) {
console.log("[NetAlertX State] Settings changed — clearing cache and reloading");
setTimeout(() => clearCache(), 500);
}
}
// 5. Dispatch scan ETA update for pages that display next-scan timing
if (appState["last_scan_run"] !== undefined || appState["next_scan_time"] !== undefined) {
document.dispatchEvent(new CustomEvent('nax:scanEtaUpdate', {
detail: {
lastScanRun: appState["last_scan_run"],
nextScanTime: appState["next_scan_time"],
currentState: appState["currentState"]
}
}));
}
// console.log("[NetAlertX State] UI updated via jQuery");
} catch (e) {
console.error("[NetAlertX State] Failed to update state display:", e);

View File

@@ -8,6 +8,100 @@
----------------------------------------------------------------------------- */
// -------------------------------------------------------------------
// Shared tab initialization utility.
// Resolves the active tab from URL hash, query param, or cache, then activates it.
//
// Options:
// cacheKey (string) - localStorage key for persisting the active tab (required)
// defaultTab (string) - fallback tab ID if nothing is found in URL or cache. Optional, defaults to ''.
// urlParamName (string) - query-string parameter name to read (e.g. 'tab'). Optional.
// useHash (boolean) - if true, reads window.location.hash as a tab target. Optional.
// idSuffix (string) - suffix appended to URL-derived targets to form the tab <a> id (e.g. '_id'). Optional.
// onTabChange (function) - callback(targetHref) invoked when a tab is shown. Optional.
// delay (number) - ms to delay initialization (wraps in setTimeout). Optional. 0 = immediate.
// tabContainer (string) - CSS selector to scope tab lookups and event binding. Optional. null = whole document.
//
// Returns nothing. Activates the resolved tab and binds cache persistence.
// -------------------------------------------------------------------
function initializeTabsShared(options) {
const {
cacheKey,
defaultTab = '',
urlParamName = null,
useHash = false,
idSuffix = '',
onTabChange = null,
delay = 0,
tabContainer = null // CSS selector to scope tab lookups (e.g. '#tabs-location')
} = options;
function run() {
let selectedTab = defaultTab;
// 1. URL hash (e.g. maintenance.php#tab_Logging)
if (useHash) {
let hashTarget = window.location.hash.substring(1);
if (hashTarget.includes('?')) {
hashTarget = hashTarget.split('?')[0];
}
if (hashTarget) {
selectedTab = hashTarget.endsWith(idSuffix) ? hashTarget : hashTarget + idSuffix;
setCache(cacheKey, selectedTab);
}
}
// 2. URL query parameter (e.g. ?tab=WEBMON)
if (urlParamName) {
const urlParams = new URLSearchParams(window.location.search);
const paramVal = urlParams.get(urlParamName);
if (paramVal) {
selectedTab = paramVal.endsWith(idSuffix) ? paramVal : paramVal + idSuffix;
setCache(cacheKey, selectedTab);
}
}
// 3. Cached value (may already have been overridden above)
const cached = getCache(cacheKey);
if (cached && !emptyArr.includes(cached)) {
selectedTab = cached;
}
// Resolve scoped vs global selectors
const $scope = tabContainer ? $(tabContainer) : $(document);
// Activate the resolved tab (no-op if selectedTab is empty or not found)
if (selectedTab) {
$scope.find('a[id="' + selectedTab + '"]').tab('show');
}
// Fire callback for initial tab
if (onTabChange && selectedTab) {
const initialHref = $scope.find('a[id="' + selectedTab + '"]').attr('href');
if (initialHref) {
onTabChange(initialHref);
}
}
// Persist future tab changes to cache and invoke callback
$scope.find('a[data-toggle="tab"]').on('shown.bs.tab', function (e) {
const newTabId = $(e.target).attr('id');
setCache(cacheKey, newTabId);
if (onTabChange) {
const newHref = $(e.target).attr('href');
onTabChange(newHref);
}
});
}
if (delay > 0) {
setTimeout(run, delay);
} else {
run();
}
}
// -------------------------------------------------------------------
// Utility function to generate a random API token in the format t_<random string of specified length>
@@ -636,70 +730,62 @@ function showIconSelection(setKey) {
// -----------------------------------------------------------------------------
// Get the correct db column code name based on table header title string
// Get the correct db column code name based on table header title string.
// COLUMN_NAME_MAP is defined in device-columns.js, loaded before this file.
function getColumnNameFromLangString(headStringKey) {
columnNameMap = {
"Device_TableHead_Name": "devName",
"Device_TableHead_Owner": "devOwner",
"Device_TableHead_Type": "devType",
"Device_TableHead_Icon": "devIcon",
"Device_TableHead_Favorite": "devFavorite",
"Device_TableHead_Group": "devGroup",
"Device_TableHead_FirstSession": "devFirstConnection",
"Device_TableHead_LastSession": "devLastConnection",
"Device_TableHead_LastIP": "devLastIP",
"Device_TableHead_MAC": "devMac",
"Device_TableHead_Status": "devStatus",
"Device_TableHead_MAC_full": "devMac",
"Device_TableHead_LastIPOrder": "devIpLong",
"Device_TableHead_Rowid": "rowid",
"Device_TableHead_Parent_MAC": "devParentMAC",
"Device_TableHead_Connected_Devices": "devParentChildrenCount",
"Device_TableHead_Location": "devLocation",
"Device_TableHead_Vendor": "devVendor",
"Device_TableHead_Port": "devParentPort",
"Device_TableHead_GUID": "devGUID",
"Device_TableHead_SyncHubNodeName": "devSyncHubNode",
"Device_TableHead_NetworkSite": "devSite",
"Device_TableHead_SSID": "devSSID",
"Device_TableHead_SourcePlugin": "devSourcePlugin",
"Device_TableHead_PresentLastScan": "devPresentLastScan",
"Device_TableHead_AlertDown": "devAlertDown",
"Device_TableHead_CustomProps": "devCustomProps",
"Device_TableHead_FQDN": "devFQDN",
"Device_TableHead_ParentRelType": "devParentRelType",
"Device_TableHead_ReqNicsOnline": "devReqNicsOnline",
"Device_TableHead_Vlan": "devVlan",
"Device_TableHead_IPv4": "devPrimaryIPv4",
"Device_TableHead_IPv6": "devPrimaryIPv6"
};
return columnNameMap[headStringKey] || "";
return COLUMN_NAME_MAP[headStringKey] || "";
}
//--------------------------------------------------------------
// Generating the device status chip
function getStatusBadgeParts(devPresentLastScan, devAlertDown, devMac, statusText = '') {
let css = 'bg-gray text-white statusUnknown';
let icon = '<i class="fa-solid fa-question"></i>';
let status = 'unknown';
function getStatusBadgeParts(devPresentLastScan, devAlertDown, devFlapping, devMac, statusText = '', devIsSleeping = 0, devIsArchived = 0, devIsNew = 0) {
let css = 'bg-gray text-white statusUnknown';
let icon = '<i class="fa-solid fa-question"></i>';
let status = 'unknown';
let cssText = '';
let label = getString('Gen_Offline');
if (devPresentLastScan == 1) {
css = 'bg-green text-white statusOnline';
if (devPresentLastScan == 1 && devFlapping == 0) {
css = 'bg-green text-white statusOnline';
cssText = 'text-green';
icon = '<i class="fa-solid fa-plug"></i>';
status = 'online';
} else if (devAlertDown == 1) {
css = 'bg-red text-white statusDown';
cssText = 'text-red';
icon = '<i class="fa-solid fa-triangle-exclamation"></i>';
status = 'down';
} else if (devPresentLastScan != 1) {
css = 'bg-gray text-white statusOffline';
icon = '<i class="fa-solid fa-plug"></i>';
status = 'online';
label = getString('Gen_Online');
} else if (devPresentLastScan == 1 && devFlapping == 1) {
css = 'bg-yellow text-white statusFlapping';
cssText = 'text-yellow';
icon = '<i class="fa-solid fa-plug-circle-exclamation"></i>';
status = 'flapping';
label = getString('Gen_Flapping');
} else if (devIsSleeping == 1) {
css = 'bg-aqua text-white statusSleeping';
cssText = 'text-aqua';
icon = '<i class="fa-solid fa-moon"></i>';
status = 'sleeping';
label = getString('Gen_Sleeping');
} else if (devIsArchived == 1) {
css = 'bg-gray text-white statusArchived';
cssText = 'text-gray50';
icon = '<i class="fa-solid fa-xmark"></i>';
status = 'offline';
icon = '<i class="fa-solid fa-box-archive"></i>';
status = 'archived';
label = getString('Gen_Archived');
} else if (devAlertDown == 1) {
css = 'bg-red text-white statusDown';
cssText = 'text-red';
icon = '<i class="fa-solid fa-triangle-exclamation"></i>';
status = 'down';
label = getString('Gen_Down');
} else if (devPresentLastScan != 1) {
css = 'bg-gray text-white statusOffline';
cssText = 'text-gray50';
icon = '<i class="fa-solid fa-xmark"></i>';
status = 'offline';
label = getString('Gen_Offline');
}
// New devices keep the online/offline color & icon but show "New" as label
if (devIsNew == 1) {
label = getString('Gen_New');
}
const cleanedText = statusText.replace(/-/g, '');
@@ -707,15 +793,36 @@ function getStatusBadgeParts(devPresentLastScan, devAlertDown, devMac, statusTex
return {
cssClass: css,
cssText: cssText,
cssText: cssText,
iconHtml: icon,
mac: devMac,
text: cleanedText,
status: status,
url: url
mac: devMac,
text: cleanedText,
status: status,
label: label,
url: url
};
}
// Convenience wrappers — call getStatusBadgeParts with the right fields
// for each object shape used across the codebase.
// Any object with devXxx field names (API response, cache, SQL DevicesView row,
// network-api nodes, network-tree nodeData.data objects)
function badgeFromDevice(d) {
return getStatusBadgeParts(
d.devPresentLastScan, d.devAlertDown, d.devFlapping, d.devMac,
'', d.devIsSleeping || 0, d.devIsArchived || 0, d.devIsNew || 0
);
}
// hover-box: reads status fields from jQuery data-* attributes on an element
function badgeFromDataAttrs($el) {
return getStatusBadgeParts(
$el.data('present'), $el.data('alertdown'), $el.data('flapping') || 0, $el.data('mac'),
'', $el.data('sleeping') || 0, $el.data('archived') || 0, $el.data('isnew') || 0
);
}
//--------------------------------------------------------------
// Getting the color and css class for device relationships
function getRelationshipConf(relType) {
@@ -861,11 +968,7 @@ function renderDeviceLink(data, container, useName = false) {
}
// Build and return badge parts
const badge = getStatusBadgeParts(
device.devPresentLastScan,
device.devAlertDown,
device.devMac
);
const badge = badgeFromDevice(device);
// badge class and hover-info class to container
$(container)
@@ -880,8 +983,12 @@ function renderDeviceLink(data, container, useName = false) {
'data-firstseen': device.devFirstConnection,
'data-relationship': device.devParentRelType,
'data-status': device.devStatus,
'data-flapping': device.devFlapping,
'data-present': device.devPresentLastScan,
'data-alert': device.devAlertDown,
'data-alertdown': device.devAlertDown,
'data-sleeping': device.devIsSleeping || 0,
'data-archived': device.devIsArchived || 0,
'data-isnew': device.devIsNew || 0,
'data-icon': device.devIcon
});
@@ -950,8 +1057,8 @@ function initHoverNodeInfo() {
const lastseen = $el.data('lastseen') || 'Unknown';
const firstseen = $el.data('firstseen') || 'Unknown';
const relationship = $el.data('relationship') || 'Unknown';
const badge = getStatusBadgeParts( $el.data('present'), $el.data('alert'), $el.data('mac'))
const status =`<span class="badge ${badge.cssClass}">${badge.iconHtml} ${badge.status}</span>`
const badge = badgeFromDataAttrs($el);
const status =`<span class="badge ${badge.cssClass}">${badge.iconHtml} ${badge.label}</span>`
const html = `
<div>

File diff suppressed because one or more lines are too long

View File

48
front/llms.txt Normal file
View File

@@ -0,0 +1,48 @@
# For Humans
To set up MCP access for your AI Agent:
1. Tell it to "Please go here and set up MCP using HOST:BACKEND_PORT <link to this page>"
-or-
2. Copy and paste the contents of this page and let it know your HOST and API token.
---
# For AI Agents: NetAlertX MCP Setup
NetAlertX is a network monitoring system. Connect to its **MCP (Model Context Protocol) server** for live access to devices, scans, and events.
## 1. Gather Connection Details
Ask the user for the following if not provided:
1. **API Token:** Found in **`/settings.php`** under **System / API**. If you were directed to this server, try to browse to /settings.php on your own.
2. **MCP URL:** Typically `http://<HOST>:20212/mcp/sse`. (The UI is usually on port `20211`, and the MCP/GraphQL backend is on `20212`).
*Note: Test HTTP connectivity. If port `20212` fails, ask the user if the `GRAPHQL_PORT` was changed in settings or if a reverse proxy is used.*
## 2. Configure MCP Client
Use the gathered details to configure your MCP connection:
```json
{
"mcpServers": {
"netalertx": {
"url": "http://<HOST>:<GRAPHQL_PORT>/mcp/sse",
"headers": {
"Authorization": "Bearer <API_TOKEN>"
}
}
}
}
```
## 3. Capabilities
Once connected, you can:
- Query and manage devices (search, filter, MAC details, Wake-on-LAN).
- Trigger network scans (ARP, NMAP, Avahi).
- Manage events, sessions, and notifications.
## 4. Important Notes
- **UI Refresh:** When you modify data, tell the user to click the in-app refresh button (🔄) to see changes.
- **REST API Fallback:** If MCP is unavailable, retrieve the OpenAPI spec from `http://<HOST>:<GRAPHQL_PORT>/openapi.json` to discover available endpoints, then call those endpoints with `Authorization: Bearer <API_TOKEN>`. This approach is context-heavy and manual, so use it as a last resort.
- **Authentication:** The API token is distinct from the UI login password and must be obtained/changed in the frontend /settings.php

View File

@@ -778,50 +778,14 @@ function scrollDown() {
// General initialization
// --------------------------------------------------------
function initializeTabs() {
setTimeout(() => {
const key = "activeMaintenanceTab";
// default selection
let selectedTab = "tab_DBTools_id";
// the #target from the URL
let target = window.location.hash.substr(1);
console.log(selectedTab);
// get only the part between #...?
if (target.includes('?')) {
target = target.split('?')[0];
}
// update cookie if target specified
if (target) {
selectedTab = target.endsWith("_id") ? target : `${target}_id`;
setCache(key, selectedTab); // _id is added so it doesn't conflict with AdminLTE tab behavior
}
// get the tab id from the cookie (already overridden by the target)
const cachedTab = getCache(key);
if (cachedTab && !emptyArr.includes(cachedTab)) {
selectedTab = cachedTab;
}
// Activate panel
$('.nav-tabs a[id='+ selectedTab +']').tab('show');
// When changed save new current tab
$('a[data-toggle="tab"]').on('shown.bs.tab', (e) => {
const newTabId = $(e.target).attr('id');
setCache(key, newTabId);
});
// events on tab change
$('a[data-toggle="tab"]').on('shown.bs.tab', (e) => {
const newTarget = $(e.target).attr("href"); // activated tab
});
hideSpinner();
}, 50);
initializeTabsShared({
cacheKey: 'activeMaintenanceTab',
defaultTab: 'tab_DBTools_id',
useHash: true,
idSuffix: '_id',
delay: 50
});
setTimeout(() => hideSpinner(), 50);
}
//------------------------------------------------------------------------------
@@ -895,10 +859,10 @@ window.onload = function asyncFooter() {
initializeTabs();
try {
$("#lastCommit").append('<a href="https://github.com/jokob-sk/NetAlertX/commits" target="_blank"><img alt="GitHub last commit" src="https://img.shields.io/github/last-commit/jokob-sk/netalertx/main?logo=github"></a>');
$("#lastCommit").append('<a href="https://github.com/netalertx/NetAlertX/commits" target="_blank"><img alt="GitHub last commit" src="https://img.shields.io/github/last-commit/jokob-sk/netalertx/main?logo=github"></a>');
$("#lastDockerUpdate").append(
'<a href="https://github.com/jokob-sk/NetAlertX/releases" target="_blank"><img alt="Docker last pushed" src="https://img.shields.io/github/v/release/jokob-sk/NetAlertX?color=0aa8d2&logoColor=fff&logo=GitHub&label=Latest"></a>');
'<a href="https://github.com/netalertx/NetAlertX/releases" target="_blank"><img alt="Docker last pushed" src="https://img.shields.io/github/v/release/jokob-sk/NetAlertX?color=0aa8d2&logoColor=fff&logo=GitHub&label=Latest"></a>');
} catch (error) {
console.error('Failed to load GitHub badges:', error);
}

View File

@@ -2,6 +2,7 @@
//------------------------------------------------------------------------------
// check if authenticated
require_once $_SERVER['DOCUMENT_ROOT'] . '/php/templates/security.php';
require_once $_SERVER['DOCUMENT_ROOT'] . '/php/server/db.php';
require_once $_SERVER['DOCUMENT_ROOT'] . '/php/templates/language/lang.php';
?>
@@ -240,8 +241,8 @@
// Initialize device selectors / pickers fields
function initDeviceSelectors() {
// Parse device list
devicesList = JSON.parse(getCache('devicesListAll_JSON'));
// Parse device list using the shared helper
devicesList = parseDeviceCache(getCache('devicesListAll_JSON'));
// Check if the device list exists and is an array
if (Array.isArray(devicesList)) {

File diff suppressed because it is too large Load Diff

View File

@@ -14,12 +14,13 @@ function renderSmallBox($params) {
$labelLang = isset($params['labelLang']) ? $params['labelLang'] : '';
$iconId = isset($params['iconId']) ? $params['iconId'] : '';
$iconClass = isset($params['iconClass']) ? $params['iconClass'] : '';
$iconHtml = isset($params['iconHtml']) ? $params['iconHtml'] : '';
$dataValue = isset($params['dataValue']) ? $params['dataValue'] : '';
return '
<div class="col-lg-3 col-sm-6 col-xs-6">
<a href="#" onclick="javascript: ' . htmlspecialchars($onclickEvent) . '">
<div class="small-box ' . htmlspecialchars($color) . '">
<div style="cursor:pointer" onclick="javascript: ' . htmlspecialchars($onclickEvent) . '">
<div class="small-box ' . htmlspecialchars($color) . '" style="pointer-events:none">
<div class="inner">
<div class="col-lg-6 col-sm-6 col-xs-6">
<div class="small-box-text col-lg-12 col-sm-12 col-xs-12" id="' . htmlspecialchars($headerId) . '" style="' . htmlspecialchars($headerStyle) . '"> <b>' . htmlspecialchars($dataValue) . '</b> </div>
@@ -27,10 +28,10 @@ function renderSmallBox($params) {
<div class="infobox_label col-lg-6 col-sm-6 col-xs-6">' . lang(htmlspecialchars($labelLang)) . '</div>
</div>
<div class="icon">
<i id="' . htmlspecialchars($iconId) . '" class="' . htmlspecialchars($iconClass) . '"></i>
' . ($iconHtml ? $iconHtml : '<i id="' . htmlspecialchars($iconId) . '" class="' . htmlspecialchars($iconClass) . '"></i>') . '
</div>
</div>
</a>
</div>
</div>';
}

View File

@@ -28,13 +28,13 @@ function initOnlineHistoryGraph() {
res.data.forEach(function(entry) {
var formattedTime = localizeTimestamp(entry.Scan_Date).slice(11, 17);
var formattedTime = localizeTimestamp(entry.scanDate).slice(11, 17);
timeStamps.push(formattedTime);
onlineCounts.push(entry.Online_Devices);
downCounts.push(entry.Down_Devices);
offlineCounts.push(entry.Offline_Devices);
archivedCounts.push(entry.Archived_Devices);
onlineCounts.push(entry.onlineDevices);
downCounts.push(entry.downDevices);
offlineCounts.push(entry.offlineDevices);
archivedCounts.push(entry.archivedDevices);
});
// Call your presenceOverTime function after data is ready

View File

@@ -0,0 +1,38 @@
<?php
// ---- IMPORTS ----
// Check if authenticated - also populates $api_token and $configLines from app.conf
require_once $_SERVER['DOCUMENT_ROOT'] . '/php/templates/security.php';
// getSettingValue() reads from Python-generated table_settings.json (reliable runtime source)
require_once dirname(__FILE__) . '/init.php';
// ---- IMPORTS ----
// Only respond to GET requests
if ($_SERVER['REQUEST_METHOD'] !== 'GET') {
http_response_code(405);
header('Content-Type: application/json');
echo json_encode(['error' => 'Method not allowed']);
exit;
}
// API_TOKEN: security.php extracts it from app.conf but the value is empty until Python
// initialise.py runs. Fall back to table_settings.json (runtime source of truth).
$resolved_token = !empty($api_token) ? $api_token : getSettingValue('API_TOKEN');
// GRAPHQL_PORT: format in app.conf is bare integer — GRAPHQL_PORT=20212 (no quotes)
$graphql_port_raw = getConfigLine('/^GRAPHQL_PORT\s*=/', $configLines);
$graphql_port = isset($graphql_port_raw[1]) ? (int) trim($graphql_port_raw[1]) : 20212;
// Validate we have something useful before returning
if (empty($resolved_token) || str_starts_with($resolved_token, 'Could not')) {
http_response_code(500);
header('Content-Type: application/json');
echo json_encode(['error' => 'Could not read API_TOKEN from configuration']);
exit;
}
header('Content-Type: application/json');
echo json_encode([
'api_token' => $resolved_token,
'graphql_port' => $graphql_port,
]);

View File

@@ -77,7 +77,7 @@ function saveSettings()
$txt = $txt."# Generated: ".$timestamp." #\n";
$txt = $txt."# #\n";
$txt = $txt."# Config file for the LAN intruder detection app: #\n";
$txt = $txt."# https://github.com/jokob-sk/NetAlertX #\n";
$txt = $txt."# https://github.com/netalertx/NetAlertX #\n";
$txt = $txt."# #\n";
$txt = $txt."#-----------------AUTOGENERATED FILE-----------------#\n";

View File

@@ -24,11 +24,10 @@
<!-- NetAlertX footer with url -->
<a href="https://github.com/jokob-sk/NetAlertX" target="_blank">Net<b>Alert</b><sup>x</sup></a>
<!-- To the right -->
<div class="pull-right no-hidden-xs">
| <a href="https://gurubase.io/g/netalertx" class="pointer" target="_blank" title="Ask AI"><i class="fa fa-comment-dots fa-flip-horizontal"></i></a>
| <a href="/llms.txt" class="pointer" target="_blank" data-agent-role="mcp-setup" aria-label="Agentic MCP instructions" title="Agent MCP Instructions"><i class="fa fa-robot"></i></a>
| <a href="https://docs.netalertx.com/" class="pointer" target="_blank" title="Documentation"><i class="fa fa-book"></i></a>
| <a href="https://github.com/jokob-sk/NetAlertX/issues" class="pointer" target="_blank"><i class="fa fa-bug" title="Report a bug"></i></a>
| <a href="https://discord.com/invite/NczTUTWyRr" class="pointer" target="_blank"><i class="fa-brands fa-discord" title="Join Discord"></i></a>
@@ -56,6 +55,7 @@
<!-- NetAlertX -->
<script defer src="js/handle_version.js"></script>
<script src="js/device-columns.js?v=<?php include 'php/templates/version.php'; ?>"></script>
<script src="js/ui_components.js?v=<?php include 'php/templates/version.php'; ?>"></script>

View File

@@ -43,7 +43,10 @@
<script src="lib/datatables.net-bs/js/dataTables.bootstrap.min.js"></script>
<script src="lib/datatables.net/js/dataTables.select.min.js"></script>
<script>window.NAX_APP_VERSION = "<?php echo trim(file_exists('/app/.VERSION') ? file_get_contents('/app/.VERSION') : 'dev'); ?>";</script>
<script src="js/cache.js?v=<?php include 'php/templates/version.php'; ?>"></script>
<script src="js/common.js?v=<?php include 'php/templates/version.php'; ?>"></script>
<script src="js/app-init.js?v=<?php include 'php/templates/version.php'; ?>"></script>
<script src="js/sse_manager.js?v=<?php include 'php/templates/version.php'; ?>"></script>
<script src="js/api.js?v=<?php include 'php/templates/version.php'; ?>"></script>
<script src="js/modal.js?v=<?php include 'php/templates/version.php'; ?>"></script>
@@ -314,6 +317,9 @@
<li>
<a href="devices.php#archived" onclick="forceLoadUrl('devices.php#archived')" > <?= lang("Device_Shortcut_Archived");?> </a>
</li>
<li>
<a href="devices.php#unstable_devices" onclick="forceLoadUrl('devices.php#unstable_devices')" > <?= lang("Device_Shortcut_Unstable");?> </a>
</li>
<li>
<a href="devices.php#all_devices" onclick="forceLoadUrl('devices.php#all_devices')" > <?= lang("Gen_All_Devices");?> </a>
</li>

View File

@@ -203,10 +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_NoMatch_Title": "",
"Device_Save_Failed": "",
"Device_Save_Unauthorized": "",
"Device_Saved_Success": "",
"Device_Saved_Unexpected": "",
"Device_Scanning": "",
"Device_Searchbox": "بحث",
"Device_Shortcut_AllDevices": "جميع الأجهزة",
"Device_Shortcut_AllNodes": "جميع العقد",
@@ -218,12 +225,14 @@
"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": "",
@@ -314,6 +323,7 @@
"Gen_AddDevice": "إضافة جهاز",
"Gen_Add_All": "إضافة الكل",
"Gen_All_Devices": "جميع الأجهزة",
"Gen_Archived": "",
"Gen_AreYouSure": "هل أنت متأكد؟",
"Gen_Backup": "نسخة احتياطية",
"Gen_Cancel": "إلغاء",
@@ -324,13 +334,16 @@
"Gen_Delete": "حذف",
"Gen_DeleteAll": "حذف الكل",
"Gen_Description": "الوصف",
"Gen_Down": "",
"Gen_Error": "خطأ",
"Gen_Filter": "تصفية",
"Gen_Flapping": "",
"Gen_Generate": "إنشاء",
"Gen_InvalidMac": "عنوان MAC غير صالح.",
"Gen_Invalid_Value": "تم إدخال قيمة غير صالحة",
"Gen_LockedDB": "قاعدة البيانات مقفلة",
"Gen_NetworkMask": "قناع الشبكة",
"Gen_New": "",
"Gen_Offline": "غير متصل",
"Gen_Okay": "موافق",
"Gen_Online": "متصل",
@@ -348,6 +361,7 @@
"Gen_SelectIcon": "اختيار أيقونة",
"Gen_SelectToPreview": "اختر للمعاينة",
"Gen_Selected_Devices": "الأجهزة المحددة",
"Gen_Sleeping": "",
"Gen_Subnet": "الشبكة الفرعية",
"Gen_Switch": "تبديل",
"Gen_Upd": "تحديث",
@@ -577,6 +591,8 @@
"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": "تصفية عنوان MAC",
"Plugins_History": "السجل",
@@ -789,4 +805,4 @@
"settings_system_label": "نظام",
"settings_update_item_warning": "قم بتحديث القيمة أدناه. احرص على اتباع التنسيق السابق. <b>لم يتم إجراء التحقق.</b>",
"test_event_tooltip": "احفظ التغييرات أولاً قبل اختبار الإعدادات."
}
}

View File

@@ -27,8 +27,8 @@
"AppEvents_ObjectType": "Tipus d'objecte",
"AppEvents_Plugin": "Plugin",
"AppEvents_Type": "Tipus",
"BACKEND_API_URL_description": "",
"BACKEND_API_URL_name": "",
"BACKEND_API_URL_description": "S'utilitza per permetre la comunicació del frontend al backend. Per defecte establert en <code>/server</code> i no cal canviar-ho.",
"BACKEND_API_URL_name": "URL del API Backend",
"BackDevDetail_Actions_Ask_Run": "Vol executar aquesta comanda?",
"BackDevDetail_Actions_Not_Registered": "Comanda no registrada: ",
"BackDevDetail_Actions_Title_Run": "Executar la comanda",
@@ -98,10 +98,10 @@
"DevDetail_MainInfo_Network": "<i class=\"fa fa-server\"></i> Node (MAC)",
"DevDetail_MainInfo_Network_Port": "<i class=\"fa fa-ethernet\"></i> Port",
"DevDetail_MainInfo_Network_Site": "Lloc web",
"DevDetail_MainInfo_Network_Title": "Xarxa",
"DevDetail_MainInfo_Network_Title": "Detalls de Xarxa",
"DevDetail_MainInfo_Owner": "Propietari",
"DevDetail_MainInfo_SSID": "SSID",
"DevDetail_MainInfo_Title": "Informació principal",
"DevDetail_MainInfo_Title": "Informació del dispositiu",
"DevDetail_MainInfo_Type": "Tipus",
"DevDetail_MainInfo_Vendor": "Venedor",
"DevDetail_MainInfo_mac": "MAC",
@@ -109,7 +109,7 @@
"DevDetail_Network_Node_hover": "Seleccioneu el dispositiu de xarxa al qual aquest dispositiu està connectat, per poder omplir l'arbre de xarxa.",
"DevDetail_Network_Port_hover": "El port on el dispositiu està connectat al dispositiu de xarxa del pare. Si es deixa buit, sortirà una icona wifi a la representació de la Xarxa.",
"DevDetail_Nmap_Scans": "Escaneig manual Nmap",
"DevDetail_Nmap_Scans_desc": "Aquí podeu executar les exploracions NMAP manuals. També podeu programar les exploracions NMAP automàtiques a través del connector Serveis i Ports (NMAP). Ves a <a href=\"https://github.com/jokob-sk/NetAlertX/tree/main/front/plugins/nmap_scan\" target='_blank'>Docs</a> per saber-ne més",
"DevDetail_Nmap_Scans_desc": "Aquí podeu executar les exploracions NMAP manuals. També podeu programar les exploracions NMAP automàtiques a través del connector Serveis i Ports (NMAP). Ves a <a href=\"https://github.com/netalertx/NetAlertX/tree/main/front/plugins/nmap_scan\" target='_blank'>Docs</a> per saber-ne més",
"DevDetail_Nmap_buttonDefault": "Escaneig predeterminat",
"DevDetail_Nmap_buttonDefault_text": "Escaneig predeterminat: Nmap escaneja els 1000 ports superiors per a cada protocol d'exploració sol·licitat. El 93% dels ports TCP i el 49% dels ports UDP. (uns 5 segons)",
"DevDetail_Nmap_buttonDetail": "Escaneig Detallat",
@@ -203,10 +203,17 @@
"Device_MultiEdit_MassActions": "Accions massives:",
"Device_MultiEdit_No_Devices": "Cap dispositiu seleccionat.",
"Device_MultiEdit_Tooltip": "Atenció. Si feu clic a això s'aplicarà el valor de l'esquerra a tots els dispositius seleccionats a dalt.",
"Device_Save_Failed": "",
"Device_Save_Unauthorized": "",
"Device_Saved_Success": "",
"Device_Saved_Unexpected": "",
"Device_NextScan_Imminent": "",
"Device_NextScan_In": "",
"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",
"Device_Saved_Unexpected": "Actualització de dispositiu ha retornat una resposta no esperada",
"Device_Scanning": "",
"Device_Searchbox": "Cerca",
"Device_Shortcut_AllDevices": "Els meus dispositius",
"Device_Shortcut_AllNodes": "Tots els nodes",
@@ -218,16 +225,18 @@
"Device_Shortcut_Favorites": "Favorits",
"Device_Shortcut_NewDevices": "Nous dispositius",
"Device_Shortcut_OnlineChart": "Dispositius detectats",
"Device_Shortcut_Unstable": "Inestable",
"Device_TableHead_AlertDown": "Cancel·lar alerta",
"Device_TableHead_Connected_Devices": "Connexions",
"Device_TableHead_CustomProps": "Props / Accions",
"Device_TableHead_FQDN": "FQDN",
"Device_TableHead_Favorite": "Favorit",
"Device_TableHead_FirstSession": "Primera Sessió",
"Device_TableHead_Flapping": "",
"Device_TableHead_GUID": "GUID",
"Device_TableHead_Group": "Grup",
"Device_TableHead_IPv4": "",
"Device_TableHead_IPv6": "",
"Device_TableHead_IPv4": "IPv4",
"Device_TableHead_IPv6": "IPv6",
"Device_TableHead_Icon": "Icona",
"Device_TableHead_LastIP": "Darrera IP",
"Device_TableHead_LastIPOrder": "Últim Ordre d'IP",
@@ -251,7 +260,7 @@
"Device_TableHead_SyncHubNodeName": "Node Sync",
"Device_TableHead_Type": "Tipus",
"Device_TableHead_Vendor": "Venedor",
"Device_TableHead_Vlan": "",
"Device_TableHead_Vlan": "VLAN",
"Device_Table_Not_Network_Device": "No configurat com a dispositiu de xarxa",
"Device_Table_info": "Mostrant _INICI_ a_FINAL_ d'entrades_ TOTALS",
"Device_Table_nav_next": "Següent",
@@ -299,14 +308,14 @@
"Events_Tablelenght": "Veure_entrades_MENU",
"Events_Tablelenght_all": "Tot",
"Events_Title": "Esdeveniments",
"FakeMAC_hover": "Autodetecció - indica si el dispositiu fa servir una adreça MAC falsa (comença amb FA:CE o 00:1A), típicament generada per un plugin que no pot detectar la MAC real o quan es crea un dispositiu amagat (dummy).",
"FieldLock_Error": "",
"FieldLock_Lock_Tooltip": "",
"FieldLock_Locked": "",
"FieldLock_SaveBeforeLocking": "",
"FieldLock_Source_Label": "",
"FieldLock_Unlock_Tooltip": "",
"FieldLock_Unlocked": "",
"FakeMAC_hover": "Aquest dispositiu fa servir una adreça MAC falsa",
"FieldLock_Error": "Error actualitzant l'estat de bloqueig de camp",
"FieldLock_Lock_Tooltip": "Bloquejar el camp (per previndre modificacions)",
"FieldLock_Locked": "Camp bloquejat",
"FieldLock_SaveBeforeLocking": "Guardar canvis abans de bloquejar",
"FieldLock_Source_Label": "Font: ",
"FieldLock_Unlock_Tooltip": "Desbloquejar camp (permet modificacions)",
"FieldLock_Unlocked": "Camp desbloquejat",
"GRAPHQL_PORT_description": "El número de port del servidor GraphQL. Comprova que el port és únic en totes les aplicacions d'aquest servidor i en totes les instàncies de NetAlertX.",
"GRAPHQL_PORT_name": "Port GraphQL",
"Gen_Action": "Acció",
@@ -314,6 +323,7 @@
"Gen_AddDevice": "Afegir dispositiu",
"Gen_Add_All": "Afegeix tot",
"Gen_All_Devices": "Tots els dispositius",
"Gen_Archived": "",
"Gen_AreYouSure": "Estàs segur?",
"Gen_Backup": "Executar Backup",
"Gen_Cancel": "Cancel·lar",
@@ -324,13 +334,16 @@
"Gen_Delete": "Esborrar",
"Gen_DeleteAll": "Esborrar tot",
"Gen_Description": "Descripció",
"Gen_Down": "",
"Gen_Error": "Error",
"Gen_Filter": "Filtrar",
"Gen_Flapping": "",
"Gen_Generate": "Generar",
"Gen_InvalidMac": "Mac address invàlida.",
"Gen_Invalid_Value": "S'ha introduït un valor incorrecte",
"Gen_LockedDB": "ERROR - DB podria estar bloquejada - Fes servir F12 Eines desenvolupament -> Consola o provar-ho més tard.",
"Gen_NetworkMask": "Màscara de xarxa",
"Gen_New": "",
"Gen_Offline": "Fora de línia",
"Gen_Okay": "Ok",
"Gen_Online": "En línia",
@@ -348,6 +361,7 @@
"Gen_SelectIcon": "<i class=\"fa-solid fa-chevron-down fa-fade\"></i>",
"Gen_SelectToPreview": "Seleccioneu la vista prèvia",
"Gen_Selected_Devices": "Dispositius seleccionats:",
"Gen_Sleeping": "",
"Gen_Subnet": "Subxarxa",
"Gen_Switch": "Switch",
"Gen_Upd": "Actualitzat correctament",
@@ -356,7 +370,7 @@
"Gen_Update_Value": "Actualitzar Valor",
"Gen_ValidIcon": "<i class=\"fa-solid fa-chevron-right \"></i>",
"Gen_Warning": "Advertència",
"Gen_Work_In_Progress": "Work in progress, un bon moment per retroalimentació a https://github.com/jokob-sk/NetAlertX/issues",
"Gen_Work_In_Progress": "Work in progress, un bon moment per retroalimentació a https://github.com/netalertx/NetAlertX/issues",
"Gen_create_new_device": "Nou dispositiu",
"Gen_create_new_device_info": "Els dispositius són típicament descobert utilitzant <a target=\"_blank\" href=\"https://docs.netalertx.com/PLUGINS\">plugins</a>. Tanmateix, en certs casos, pots necessitar afegir dispositius a mà. Per explorar els temes concrets comproveu la <a target=\"_blank\" href=\"https://docs.netalertx.com/REMOTE_NETWORKS\">documentació de Xarxes Remotes</a>.",
"General_display_name": "General",
@@ -372,7 +386,7 @@
"Loading": "Carregant…",
"Login_Box": "Introduïu la vostra contrasenya",
"Login_Default_PWD": "Contrasenya per defecte \"123456\" encara és activa.",
"Login_Info": "Les contrasenyes es canvien al connector(plugin) Configurar Contrasenya. Comprova el <a target=\"_blank\" href=\"https://github.com/jokob-sk/NetAlertX/tree/main/front/plugins/set_password\">SETPWD docs</a> si tens dubtes fent logging.",
"Login_Info": "Les contrasenyes es canvien al connector(plugin) Configurar Contrasenya. Comprova el <a target=\"_blank\" href=\"https://github.com/netalertx/NetAlertX/tree/main/front/plugins/set_password\">SETPWD docs</a> si tens dubtes fent logging.",
"Login_Psw-box": "Contrasenya",
"Login_Psw_alert": "Alerta de contrasenya!",
"Login_Psw_folder": "a la carpeta config.",
@@ -399,25 +413,25 @@
"Maintenance_Tool_DownloadConfig_text": "Descarregueu una còpia de seguretat completa de la vostra configuració de configuració emmagatzemada al fitxer <code>app.conf</code>.",
"Maintenance_Tool_DownloadWorkflows": "Exportació de fluxos de treball",
"Maintenance_Tool_DownloadWorkflows_text": "Descarregueu una còpia de seguretat completa dels vostres fluxos de treball emmagatzemats en el fitxer <code>workflows.json</code>.",
"Maintenance_Tool_ExportCSV": "CSV Exportació de dispositius",
"Maintenance_Tool_ExportCSV_noti": "CSV Exportació",
"Maintenance_Tool_ExportCSV": "Exportació de dispositius (csv)",
"Maintenance_Tool_ExportCSV_noti": "Exportació de dispositius (csv)",
"Maintenance_Tool_ExportCSV_noti_text": "Estàs segur que vols generar un fitxer CSV?",
"Maintenance_Tool_ExportCSV_text": "Genera un fitxer CSV (comma separated value) que conté la llista dels dispositius incloent les relacions de Xarxa entre Nodes i dispositius connectats. També pots disparar-ho accedint a la URL <code>el_vostre_NetAlertX_ url/php/server/devices.php?acció=ExportCSV</code> o activant el connector <a href=\"settings.php#CSVBCKP_header\">CSV Còpia de seguretat</a>.",
"Maintenance_Tool_ImportCSV": "CSV Importació de dispositius",
"Maintenance_Tool_ImportCSV_noti": "CSV Importació",
"Maintenance_Tool_ExportCSV_text": "Genera un fitxer CSV (comma separated value) que conté la llista dels dispositius incloent les relacions de Xarxa entre Nodes i dispositius connectats. També pots disparar-ho activant la <a ref=\"settings.php#CSVBCKP_header\">CSV Còpia de seguretat</a>.",
"Maintenance_Tool_ImportCSV": "Importació de dispositius (csv)",
"Maintenance_Tool_ImportCSV_noti": "Importar dispositius (csv)",
"Maintenance_Tool_ImportCSV_noti_text": "Estàs segur que vols importar el fitxer CSV? Això <b> sobreescriurà</b> completament els dispositius de la seva base de dades.",
"Maintenance_Tool_ImportCSV_text": "Abans d'utilitzar aquesta funció, fes una còpia de seguretat, si us plau. Importa un CSV (comma separated value) el fitxer que conté la llista dels dispositius que inclouen les relacions de Xarxa entre Nodes i dispositius connectats. Per fer-ho col·loca el CSV el fitxer anomenat <b>devices.csv</b> a la vostra <b>/config</b> carpeta.",
"Maintenance_Tool_ImportConfig_noti": "Importació de la configuració (app.conf)",
"Maintenance_Tool_ImportPastedCSV": "CSV Importació de dispositius (Paste)",
"Maintenance_Tool_ImportPastedCSV": "Importar de dispositius (csv) (Paste)",
"Maintenance_Tool_ImportPastedCSV_noti_text": "Estàs segur que vols importar el CSV copiat? Això <b> sobreescriurà</b> completament els dispositius de la base de dades.",
"Maintenance_Tool_ImportPastedCSV_text": "Abans d'utilitzar aquesta funció, feu una còpia de seguretat. Importar un fitxer CSV (comma separated value) que contingui la llista de dispositius, incloent les relacions de xarxa entre els nodes i els dispositius connectats.",
"Maintenance_Tool_ImportPastedConfig": "Importació de la configuració (paste)",
"Maintenance_Tool_ImportPastedConfig_noti_text": "Estàs segur que vols importar la configuració config enganxada? Això <b> sobreescriurà</b> completament el fitxer <code>app.conf</code>.",
"Maintenance_Tool_ImportPastedConfig_text": "Importa el fitxer <code>app.conf</code> que conté tota l'aplicació Configuració. És possible que vulgueu descarregar el fitxer actual <code>app.conf</code> primer amb el <b>Settings Export</b>.",
"Maintenance_Tool_UnlockFields": "",
"Maintenance_Tool_UnlockFields_noti": "",
"Maintenance_Tool_UnlockFields_noti_text": "",
"Maintenance_Tool_UnlockFields_text": "",
"Maintenance_Tool_UnlockFields": "Desbloquejar camps del dispositiu",
"Maintenance_Tool_UnlockFields_noti": "Desbloquejar camps del dispositiu",
"Maintenance_Tool_UnlockFields_noti_text": "Esta segur que vol netejar tots els valors de les fonts (BLOQUEIG/USUARI) de tots els camps de tots els dispositius? L'acció no es pot desfer.",
"Maintenance_Tool_UnlockFields_text": "Aquesta eina eliminarà tots els valors d'origen de tots els camps seguits per a tots els dispositius, desbloquejant tots els camps per als connectors i usuaris. Utilitzeu-ho amb precaució, ja que afectarà tot l'inventari del vostre dispositiu.",
"Maintenance_Tool_arpscansw": "Conmuta arp-Scan (on/off)",
"Maintenance_Tool_arpscansw_noti": "Conmuta arp-Scan on or off",
"Maintenance_Tool_arpscansw_noti_text": "Quan l'escàner ha estat canviat a off es queda off fins que és activat de bell nou.",
@@ -427,9 +441,9 @@
"Maintenance_Tool_backup_noti_text": "Estàs segur que vols executar el Backup DB? Assegura't que no hi ha exploració en funcionament.",
"Maintenance_Tool_backup_text": "Les còpies de seguretat de la base de dades es troben al directori de bases de dades com a arxiu zip, anomenat amb la data de creació. No hi ha un nombre màxim de còpies de seguretat.",
"Maintenance_Tool_check_visible": "Desmarqueu-ho per amagar la columna.",
"Maintenance_Tool_clearSourceFields_selected": "",
"Maintenance_Tool_clearSourceFields_selected_noti": "",
"Maintenance_Tool_clearSourceFields_selected_text": "",
"Maintenance_Tool_clearSourceFields_selected": "Neteja els camps font",
"Maintenance_Tool_clearSourceFields_selected_noti": "Neteja les fonts",
"Maintenance_Tool_clearSourceFields_selected_text": "Això netejarà tots els camps d'origen dels dispositius seleccionats. No es pot desfer.",
"Maintenance_Tool_darkmode": "Canvia Modes (Fosc/Clar)",
"Maintenance_Tool_darkmode_noti": "Canvia Modes",
"Maintenance_Tool_darkmode_noti_text": "Després del canvi de tema, la pàgina intenta recarregar-se per activar el canvi. Si és necessari, s'ha de netejar la memòria cau.",
@@ -460,7 +474,7 @@
"Maintenance_Tool_del_unknowndev_noti": "Elimina dispositius desconeguts",
"Maintenance_Tool_del_unknowndev_noti_text": "Estàs segur que vols eliminar tots els dispositius (no coneguts) o amb nom (no trobat)?",
"Maintenance_Tool_del_unknowndev_text": "Abans d'utilitzar aquesta funció, feu una còpia de seguretat. La supressió no es pot desfer. Tots els dispositius anomenats (no coneguts) s'eliminaran de la base de dades.",
"Maintenance_Tool_del_unlockFields_selecteddev_text": "",
"Maintenance_Tool_del_unlockFields_selecteddev_text": "Això desbloquejarà els camps BLOQUEJAT/USUARI dels dispositius seleccionats. No es pot desfer.e desfer.",
"Maintenance_Tool_displayed_columns_text": "Canvieu la visibilitat i l'ordre de les columnes a la pàgina <a href=\"devices.php\"><b> <i class=\"fa fa-laptop\"></i> Dispositius</b></a>.",
"Maintenance_Tool_drag_me": "Arrossega'm a reorder columnes.",
"Maintenance_Tool_order_columns_text": "Manteniment_Eina_ordre_columnes_text",
@@ -472,8 +486,8 @@
"Maintenance_Tool_restore_noti": "Restaura base de dades",
"Maintenance_Tool_restore_noti_text": "Estàs segur que vols executar la Restauració de Base de Dades? Comprova que no hi ha exploració en funcionament.",
"Maintenance_Tool_restore_text": "L'última còpia de seguretat es pot restaurar mitjançant el botó, però les còpies de seguretat antigues només es poden restaurar manualment. Després de la restauració, feu una comprovació d'integritat a la base de dades per seguretat, per si de cas la Base de dades estigués en mode escriptura quan es va crear la còpia de seguretat.",
"Maintenance_Tool_unlockFields_selecteddev": "",
"Maintenance_Tool_unlockFields_selecteddev_noti": "",
"Maintenance_Tool_unlockFields_selecteddev": "Desbloquejar els camps del dispositiu",
"Maintenance_Tool_unlockFields_selecteddev_noti": "Desbloqueja camps",
"Maintenance_Tool_upgrade_database_noti": "Actualitza base de dades",
"Maintenance_Tool_upgrade_database_noti_text": "T'és segur vols actualitzar la base de dades?<br>(potser prefereixes arxivar-la)",
"Maintenance_Tool_upgrade_database_text": "Aquest botó actualitzarà la base de dades per activar l'activitat de Xarxa dins les darreres 12 hores. Si us plau, feu còpia de la vostra base de dades per si de cas.",
@@ -486,7 +500,7 @@
"Maintenance_arp_status_off": "actualment està desactivat",
"Maintenance_arp_status_on": "s'està fent un scan",
"Maintenance_built_on": "Construït",
"Maintenance_current_version": "Ets actual. Dona un cop d'ull al que <a href=\"https://github.com/jokob-sk/NetAlertX/issues/138\" target=\"_blank\">estic treballant</a>.",
"Maintenance_current_version": "Ets actual. Dona un cop d'ull al que <a href=\"https://github.com/netalertx/NetAlertX/issues/138\" target=\"_blank\">estic treballant</a>.",
"Maintenance_database_backup": "Còpies de seguretat de BBDD",
"Maintenance_database_backup_found": "s'han trobat còpies de seguretat",
"Maintenance_database_backup_total": "ús total del disc",
@@ -498,7 +512,7 @@
"Maintenance_lang_selector_empty": "Tria idioma",
"Maintenance_lang_selector_lable": "Selecció d'idioma",
"Maintenance_lang_selector_text": "El canvi té lloc en el cantó del client, així que afecta només el navegador actual.",
"Maintenance_new_version": "Hi ha una nova versió. Comprova <a href=\"https://github.com/jokob-sk/NetAlertX/releases\" target=\"_blank\">release notes</a>.",
"Maintenance_new_version": "Hi ha una nova versió. Comprova <a href=\"https://github.com/netalertx/NetAlertX/releases\" target=\"_blank\">release notes</a>.",
"Maintenance_themeselector_apply": "Aplica",
"Maintenance_themeselector_empty": "Tria una Skin",
"Maintenance_themeselector_lable": "Selecciona una Skin",
@@ -577,6 +591,8 @@
"PIALERT_WEB_PROTECTION_name": "Activa l'accés",
"PLUGINS_KEEP_HIST_description": "Quantes entrades de Plugins s'han de mantenir a la història (per Plugin, no per dispositiu).",
"PLUGINS_KEEP_HIST_name": "Història dels Plugins",
"PRAGMA_JOURNAL_SIZE_LIMIT_description": "",
"PRAGMA_JOURNAL_SIZE_LIMIT_name": "",
"Plugins_DeleteAll": "Elimina tot (s'ignoraran els filtres)",
"Plugins_Filters_Mac": "Filtre de MAC",
"Plugins_History": "Historial d'Esdeveniments",
@@ -615,7 +631,7 @@
"REPORT_MAIL_description": "Si està activat s'envia un correu electrònic amb una llista de canvis als quals s'ha subscrit. Si us plau, ompli tots els paràmetres restants relacionats amb la configuració SMTP. Si té problemes, configuri <code>LOG_LEVEL</code> i <code>debug</code> comprovi <a href=\"/maintenance.php#tab_Logging\"> el registre d'errors</a>.",
"REPORT_MAIL_name": "Activa el correu electrònic",
"REPORT_TITLE": "Informe",
"RandomMAC_hover": "Auto detectat - indica que el dispositiu aleatoritza l'adreça MAC. Pots excloure MACs específiques amb la configuració UI_NOT_RANDOM_MAC. Fes click per saber més.",
"RandomMAC_hover": "Aquest dispositiu té una l'adreça MAC aleatòria",
"Reports_Sent_Log": "Registre d'informes enviats",
"SCAN_SUBNETS_description": "La majoria dels escàners en xarxa (ARP-SCAN, NMAP, NSLOOKUP, DIG) es basen en l'exploració d'interfícies de xarxa específiques i subxarxes. Comproveu la <a href=\"https://docs.netalertx.com/SUBNETS\" target=\"_blank\">documentació de subxarxes</a> per ajudar en aquesta configuració, especialment VLANs, i quines VLANs són compatibles, o com esbrinar la màscara de xarxa i la seva interfície. <br/> <br/> Una alternativa als escàners en xarxa és activar alguns altres escàners / importadors de dispositius que no requereixin NetAlert<sup>X</sup> per tenir accés a la xarxa (UNIFI, dhcp. leases, PiHole, etc.). <br/> <br/> Nota: El temps d'exploració en si mateix depèn del nombre d'adreces IP per verificar, així que s'ha establir amb cura amb la màscara i la interfície de xarxa adequats.",
"SCAN_SUBNETS_name": "Xarxes per escanejar",
@@ -623,7 +639,7 @@
"Setting_Override": "Valor de sobreescriptura",
"Setting_Override_Description": "Activant aquesta opció anul·larà un valor predeterminat de l'aplicació amb el valor especificat.",
"Settings_Metadata_Toggle": "Mostrar/amagar metadades per a la configuració donada.",
"Settings_Show_Description": "Mostra la descripció de la configuració.",
"Settings_Show_Description": "Mostra la descripció",
"Settings_device_Scanners_desync": "⚠ Els horaris d'escàner de dispositius no estan en sincronia.",
"Settings_device_Scanners_desync_popup": "Els horaris dels escàners de dispositius (<code>*_RUN_SCHD</code>) no són iguals. Això donarà lloc a notificacions inconsistents del dispositiu en línia / fora de línia. Si no és intencionat, utilitzeu el mateix horari per a tots els <b>🔍 escàners de dispositius</b>.",
"Speedtest_Results": "Speedtest Resultats",

View File

@@ -203,10 +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_NoMatch_Title": "",
"Device_Save_Failed": "",
"Device_Save_Unauthorized": "",
"Device_Saved_Success": "",
"Device_Saved_Unexpected": "",
"Device_Scanning": "",
"Device_Searchbox": "",
"Device_Shortcut_AllDevices": "",
"Device_Shortcut_AllNodes": "",
@@ -218,12 +225,14 @@
"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": "",
@@ -314,6 +323,7 @@
"Gen_AddDevice": "",
"Gen_Add_All": "Přidat vše",
"Gen_All_Devices": "Všechna zařízení",
"Gen_Archived": "",
"Gen_AreYouSure": "Jste si jistý?",
"Gen_Backup": "Spustit zálohování",
"Gen_Cancel": "Zrušit",
@@ -324,13 +334,16 @@
"Gen_Delete": "Smazat",
"Gen_DeleteAll": "Smazat vše",
"Gen_Description": "Popis",
"Gen_Down": "",
"Gen_Error": "Chyba",
"Gen_Filter": "Filtr",
"Gen_Flapping": "",
"Gen_Generate": "Vygenerovat",
"Gen_InvalidMac": "",
"Gen_Invalid_Value": "",
"Gen_LockedDB": "CHYBA - Databáze je možná zamčená - Zkontrolujte F12 -> Nástroje pro vývojáře -> Konzole. nebo to zkuste později.",
"Gen_NetworkMask": "",
"Gen_New": "",
"Gen_Offline": "Offline",
"Gen_Okay": "Ok",
"Gen_Online": "Online",
@@ -348,6 +361,7 @@
"Gen_SelectIcon": "<i class=\"fa-solid fa-chevron-down fa-fade\"></i>",
"Gen_SelectToPreview": "Vybrat na náhled",
"Gen_Selected_Devices": "Vybraná zařízení:",
"Gen_Sleeping": "",
"Gen_Subnet": "",
"Gen_Switch": "Přepnout",
"Gen_Upd": "Úspěšně aktualizováno",
@@ -577,6 +591,8 @@
"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": "",

View File

@@ -207,10 +207,17 @@
"Device_MultiEdit_MassActions": "Massen aktionen:",
"Device_MultiEdit_No_Devices": "Keine Geräte ausgewählt.",
"Device_MultiEdit_Tooltip": "Achtung! Beim Drücken werden alle Werte auf die oben ausgewählten Geräte übertragen.",
"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": "Gerät erfolgreich gespeichert",
"Device_Saved_Unexpected": "",
"Device_Scanning": "",
"Device_Searchbox": "Suche",
"Device_Shortcut_AllDevices": "Meine Geräte",
"Device_Shortcut_AllNodes": "Alle Knoten",
@@ -222,12 +229,14 @@
"Device_Shortcut_Favorites": "Favoriten",
"Device_Shortcut_NewDevices": "Neue Geräte",
"Device_Shortcut_OnlineChart": "Gerätepräsenz im Laufe der Zeit",
"Device_Shortcut_Unstable": "",
"Device_TableHead_AlertDown": "Alarm aus",
"Device_TableHead_Connected_Devices": "Verbindungen",
"Device_TableHead_CustomProps": "Eigenschaften / Aktionen",
"Device_TableHead_FQDN": "",
"Device_TableHead_Favorite": "Favorit",
"Device_TableHead_FirstSession": "Erste Sitzung",
"Device_TableHead_Flapping": "",
"Device_TableHead_GUID": "GUID",
"Device_TableHead_Group": "Gruppe",
"Device_TableHead_IPv4": "",
@@ -318,6 +327,7 @@
"Gen_AddDevice": "Gerät hinzufügen",
"Gen_Add_All": "Alle hinzufügen",
"Gen_All_Devices": "Alle Geräte",
"Gen_Archived": "",
"Gen_AreYouSure": "Sind Sie sich sicher?",
"Gen_Backup": "Sichern",
"Gen_Cancel": "Abbrechen",
@@ -328,13 +338,16 @@
"Gen_Delete": "Löschen",
"Gen_DeleteAll": "Alles löschen",
"Gen_Description": "Beschreibung",
"Gen_Down": "",
"Gen_Error": "Fehler",
"Gen_Filter": "Filter",
"Gen_Flapping": "",
"Gen_Generate": "Generieren",
"Gen_InvalidMac": "Ungültige MAC-Adresse.",
"Gen_Invalid_Value": "",
"Gen_LockedDB": "ERROR - DB eventuell gesperrt - Nutze die Konsole in den Entwickler Werkzeugen (F12) zur Überprüfung oder probiere es später erneut.",
"Gen_NetworkMask": "",
"Gen_New": "",
"Gen_Offline": "Offline",
"Gen_Okay": "Ok",
"Gen_Online": "Online",
@@ -352,6 +365,7 @@
"Gen_SelectIcon": "<i class=\"fa-solid fa-chevron-down fa-fade\"></i>",
"Gen_SelectToPreview": "Zur Vorschau auswählen",
"Gen_Selected_Devices": "Ausgewählte Geräte:",
"Gen_Sleeping": "",
"Gen_Subnet": "",
"Gen_Switch": "Umschalten",
"Gen_Upd": "Aktualisierung erfolgreich",
@@ -360,7 +374,7 @@
"Gen_Update_Value": "Wert aktualisieren",
"Gen_ValidIcon": "<i class=\"fa-solid fa-chevron-right \"></i>",
"Gen_Warning": "Warnung",
"Gen_Work_In_Progress": "Keine Finalversion, feedback bitte unter: https://github.com/jokob-sk/NetAlertX/issues",
"Gen_Work_In_Progress": "Keine Finalversion, feedback bitte unter: https://github.com/netalertx/NetAlertX/issues",
"Gen_create_new_device": "Neues Gerät",
"Gen_create_new_device_info": "Geräte werden normalerweise über <a target=\"_blank\" href=\"https://docs.netalertx.com/PLUGINS\">Plugins</a> gefunden. In Ausnahmefällen kann es nötig sein, sie manuell hinzuzufügen. Konkrete Szenarien sind in der <a target=\"_blank\" href=\"https://docs.netalertx.com/REMOTE_NETWORKS\">Dokumentation über entfernte Netzwerke</a> zu finden.",
"General_display_name": "Allgemein",
@@ -504,7 +518,7 @@
"Maintenance_arp_status_off": "ist im Moment deaktiviert",
"Maintenance_arp_status_on": "Scan(s) sind gerade aktiv",
"Maintenance_built_on": "Erstellt am",
"Maintenance_current_version": "Du bist up-to-date. Sieh dir an, <a href=\"https://github.com/jokob-sk/NetAlertX/issues/138\" target=\"_blank\">woran ich gerade arbeite</a>.",
"Maintenance_current_version": "Du bist up-to-date. Sieh dir an, <a href=\"https://github.com/netalertx/NetAlertX/issues/138\" target=\"_blank\">woran ich gerade arbeite</a>.",
"Maintenance_database_backup": "DB Sicherungen",
"Maintenance_database_backup_found": "Sicherungen verfügbar",
"Maintenance_database_backup_total": "Speicherplatz insgesamt",
@@ -516,7 +530,7 @@
"Maintenance_lang_selector_empty": "Sprache wählen",
"Maintenance_lang_selector_lable": "Sprachauswahl",
"Maintenance_lang_selector_text": "Die Änderung findet clientseitig statt, betrifft also nur den aktuellen Browser.",
"Maintenance_new_version": "Eine neue Version ist vefügbar. Sieh dir die <a href=\"https://github.com/jokob-sk/NetAlertX/releases\" target=\"_blank\">Versionshinweise</a> an.",
"Maintenance_new_version": "Eine neue Version ist vefügbar. Sieh dir die <a href=\"https://github.com/netalertx/NetAlertX/releases\" target=\"_blank\">Versionshinweise</a> an.",
"Maintenance_themeselector_apply": "Übernehmen",
"Maintenance_themeselector_empty": "Skin wählen",
"Maintenance_themeselector_lable": "Skin Auswahl",
@@ -606,6 +620,8 @@
"PIALERT_WEB_PROTECTION_name": "Anmeldung aktivieren",
"PLUGINS_KEEP_HIST_description": "Wie viele Plugin Scanresultate behalten werden (pro Plugin, nicht gerätespezifisch).",
"PLUGINS_KEEP_HIST_name": "Plugins Verlauf",
"PRAGMA_JOURNAL_SIZE_LIMIT_description": "",
"PRAGMA_JOURNAL_SIZE_LIMIT_name": "",
"PUSHSAFER_TOKEN_description": "Your secret Pushsafer API key (token).",
"PUSHSAFER_TOKEN_name": "Pushsafer token",
"PUSHSAFER_display_name": "Pushsafer",
@@ -782,7 +798,7 @@
"UI_REFRESH_name": "Benutzeroberfläche automatisch auffrischen",
"VERSION_description": "",
"VERSION_name": "Version oder Zeitstempel",
"WEBHOOK_PAYLOAD_description": "The Webhook payload data format for the <code>body</code> > <code>attachments</code> > <code>text</code> attribute in the payload json. See an example of the payload <a target=\"_blank\" href=\"https://github.com/jokob-sk/NetAlertX/blob/main/front/report_templates/webhook_json_sample.json\">here</a>. (e.g.: for discord use <code>text</code>)",
"WEBHOOK_PAYLOAD_description": "The Webhook payload data format for the <code>body</code> > <code>attachments</code> > <code>text</code> attribute in the payload json. See an example of the payload <a target=\"_blank\" href=\"https://github.com/netalertx/NetAlertX/blob/main/front/report_templates/webhook_json_sample.json\">here</a>. (e.g.: for discord use <code>text</code>)",
"WEBHOOK_PAYLOAD_name": "Payload type",
"WEBHOOK_REQUEST_METHOD_description": "The HTTP request method to be used for the webhook call.",
"WEBHOOK_REQUEST_METHOD_name": "Request method",

View File

@@ -109,7 +109,7 @@
"DevDetail_Network_Node_hover": "Select the parent network device the current device is connected to, to populate the Network tree.",
"DevDetail_Network_Port_hover": "The port this device is connected to on the parent network device. If left empty a wifi icon is displayed in the Network tree.",
"DevDetail_Nmap_Scans": "Manual Nmap Scans",
"DevDetail_Nmap_Scans_desc": "Here you can execute manual NMAP scans. You can also schedule regular automatic NMAP scans via the Services & Ports (NMAP) plugin. Head to <a href=\"https://github.com/jokob-sk/NetAlertX/tree/main/front/plugins/nmap_scan\" target=\"_blank\">Docs</a> to find out more",
"DevDetail_Nmap_Scans_desc": "Here you can execute manual NMAP scans. You can also schedule regular automatic NMAP scans via the Services & Ports (NMAP) plugin. Head to <a href=\"https://github.com/netalertx/NetAlertX/tree/main/front/plugins/nmap_scan\" target=\"_blank\">Docs</a> to find out more",
"DevDetail_Nmap_buttonDefault": "Default Scan",
"DevDetail_Nmap_buttonDefault_text": "Default Scan: Nmap scans the top 1,000 ports for each scan protocol requested. This catches roughly 93% of the TCP ports and 49% of the UDP ports. (about 5 seconds)",
"DevDetail_Nmap_buttonDetail": "Detailed Scan",
@@ -139,7 +139,7 @@
"DevDetail_SessionTable_Duration": "Duration",
"DevDetail_SessionTable_IP": "IP",
"DevDetail_SessionTable_Order": "Order",
"DevDetail_Shortcut_CurrentStatus": "Current Status",
"DevDetail_Shortcut_CurrentStatus": "Status",
"DevDetail_Shortcut_DownAlerts": "Down Alerts",
"DevDetail_Shortcut_Presence": "Presence",
"DevDetail_Shortcut_Sessions": "Sessions",
@@ -203,10 +203,17 @@
"Device_MultiEdit_MassActions": "Mass actions:",
"Device_MultiEdit_No_Devices": "No devices selected.",
"Device_MultiEdit_Tooltip": "Careful. Clicking this will apply the value on the left to all devices selected above.",
"Device_NextScan_Imminent": "Imminent...",
"Device_NextScan_In": "Next scan in about ",
"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",
"Device_Saved_Unexpected": "Device update returned an unexpected response",
"Device_Scanning": "Scanning...",
"Device_Searchbox": "Search",
"Device_Shortcut_AllDevices": "My devices",
"Device_Shortcut_AllNodes": "All Nodes",
@@ -218,12 +225,14 @@
"Device_Shortcut_Favorites": "Favorites",
"Device_Shortcut_NewDevices": "New devices",
"Device_Shortcut_OnlineChart": "Device presence",
"Device_Shortcut_Unstable": "Unstable",
"Device_TableHead_AlertDown": "Alert Down",
"Device_TableHead_Connected_Devices": "Connections",
"Device_TableHead_CustomProps": "Props / Actions",
"Device_TableHead_FQDN": "FQDN",
"Device_TableHead_Favorite": "Favorite",
"Device_TableHead_FirstSession": "First Session",
"Device_TableHead_Flapping": "Flapping",
"Device_TableHead_GUID": "GUID",
"Device_TableHead_Group": "Group",
"Device_TableHead_IPv4": "IPv4",
@@ -314,6 +323,7 @@
"Gen_AddDevice": "Add device",
"Gen_Add_All": "Add all",
"Gen_All_Devices": "All devices",
"Gen_Archived": "Archived",
"Gen_AreYouSure": "Are you sure?",
"Gen_Backup": "Run Backup",
"Gen_Cancel": "Cancel",
@@ -324,13 +334,16 @@
"Gen_Delete": "Delete",
"Gen_DeleteAll": "Delete all",
"Gen_Description": "Description",
"Gen_Down": "Down",
"Gen_Error": "Error",
"Gen_Filter": "Filter",
"Gen_Flapping": "Flapping",
"Gen_Generate": "Generate",
"Gen_InvalidMac": "Invalid Mac address.",
"Gen_Invalid_Value": "An invalid value was entered",
"Gen_LockedDB": "ERROR - DB might be locked - Check F12 Dev tools -> Console or try later.",
"Gen_NetworkMask": "Network mask",
"Gen_New": "New",
"Gen_Offline": "Offline",
"Gen_Okay": "Ok",
"Gen_Online": "Online",
@@ -348,6 +361,7 @@
"Gen_SelectIcon": "<i class=\"fa-solid fa-chevron-down fa-fade\"></i>",
"Gen_SelectToPreview": "Select to preview",
"Gen_Selected_Devices": "Selected devices:",
"Gen_Sleeping": "Sleeping",
"Gen_Subnet": "Subnet",
"Gen_Switch": "Switch",
"Gen_Upd": "Updated successfully",
@@ -356,7 +370,7 @@
"Gen_Update_Value": "Update Value",
"Gen_ValidIcon": "<i class=\"fa-solid fa-chevron-right \"></i>",
"Gen_Warning": "Warning",
"Gen_Work_In_Progress": "Work in progress, good time to feedback on https://github.com/jokob-sk/NetAlertX/issues",
"Gen_Work_In_Progress": "Work in progress, good time to feedback on https://github.com/netalertx/NetAlertX/issues",
"Gen_create_new_device": "New device",
"Gen_create_new_device_info": "Devices are typically discovered using <a target=\"_blank\" href=\"https://docs.netalertx.com/PLUGINS\">plugins</a>. However, in certain cases, you may need to add devices manually. To explore specific scenarios check the <a target=\"_blank\" href=\"https://docs.netalertx.com/REMOTE_NETWORKS\">Remote Networks documentation</a>.",
"General_display_name": "General",
@@ -372,7 +386,7 @@
"Loading": "Loading…",
"Login_Box": "Enter your password",
"Login_Default_PWD": "Default password \"123456\" is still active.",
"Login_Info": "Passwords are set via the Set Password plugin. Check the <a target=\"_blank\" href=\"https://github.com/jokob-sk/NetAlertX/tree/main/front/plugins/set_password\">SETPWD docs</a> if you have issues logging in.",
"Login_Info": "Passwords are set via the Set Password plugin. Check the <a target=\"_blank\" href=\"https://github.com/netalertx/NetAlertX/tree/main/front/plugins/set_password\">SETPWD docs</a> if you have issues logging in.",
"Login_Psw-box": "Password",
"Login_Psw_alert": "Password Alert!",
"Login_Psw_folder": "in the config folder.",
@@ -486,7 +500,7 @@
"Maintenance_arp_status_off": "is currently disabled",
"Maintenance_arp_status_on": "scanning in progress",
"Maintenance_built_on": "Built on",
"Maintenance_current_version": "You are up-to-date. Check out what <a href=\"https://github.com/jokob-sk/NetAlertX/issues/138\" target=\"_blank\">I am working on</a>.",
"Maintenance_current_version": "You are up-to-date. Check out what <a href=\"https://github.com/netalertx/NetAlertX/issues/138\" target=\"_blank\">I am working on</a>.",
"Maintenance_database_backup": "DB Backups",
"Maintenance_database_backup_found": "backups were found",
"Maintenance_database_backup_total": "total disk usage",
@@ -498,7 +512,7 @@
"Maintenance_lang_selector_empty": "Choose Language",
"Maintenance_lang_selector_lable": "Select Language",
"Maintenance_lang_selector_text": "The change takes place on the client side, so it affects only the current browser.",
"Maintenance_new_version": "A new version is available. Check out the <a href=\"https://github.com/jokob-sk/NetAlertX/releases\" target=\"_blank\">release notes</a>.",
"Maintenance_new_version": "A new version is available. Check out the <a href=\"https://github.com/netalertx/NetAlertX/releases\" target=\"_blank\">release notes</a>.",
"Maintenance_themeselector_apply": "Apply",
"Maintenance_themeselector_empty": "Choose a Skin",
"Maintenance_themeselector_lable": "Select Skin",
@@ -577,6 +591,8 @@
"PIALERT_WEB_PROTECTION_name": "Enable login",
"PLUGINS_KEEP_HIST_description": "How many entries of Plugins History scan results should be kept (per Plugin, and not device specific).",
"PLUGINS_KEEP_HIST_name": "Plugins History",
"PRAGMA_JOURNAL_SIZE_LIMIT_description": "SQLite WAL (Write-Ahead Log) maximum size in MB before triggering automatic checkpoints. Lower values (10-20 MB) reduce disk/storage usage but increase CPU usage during scans. Higher values (50-100 MB) reduce CPU spikes during operations but may use more RAM and disk space. Default <code>50 MB</code> balances both. Useful for resource-constrained systems like NAS devices with SD cards. Restart server for changes to take effect after saving the settings.",
"PRAGMA_JOURNAL_SIZE_LIMIT_name": "WAL size limit (MB)",
"Plugins_DeleteAll": "Delete all (filters are ignored)",
"Plugins_Filters_Mac": "Mac Filter",
"Plugins_History": "Events History",

View File

@@ -29,8 +29,8 @@
"AppEvents_Type": "Tipo",
"Apprise_display_name": "Apprise",
"Apprise_icon": "<i class=\"fa fa-bullhorn\"></i>",
"BACKEND_API_URL_description": "",
"BACKEND_API_URL_name": "",
"BACKEND_API_URL_description": "Usado para permitir al front comunicarse con el backend. Por defecto está definido en <code>/server</code> y generalmente no debiera cambiarse.",
"BACKEND_API_URL_name": "URL de la API de backend",
"BackDevDetail_Actions_Ask_Run": "¿Desea ejecutar la acción?",
"BackDevDetail_Actions_Not_Registered": "Acción no registrada: ",
"BackDevDetail_Actions_Title_Run": "Ejecutar acción",
@@ -111,7 +111,7 @@
"DevDetail_Network_Node_hover": "Seleccione el dispositivo de red principal al que está conectado el dispositivo actual para completar el árbol de Red.",
"DevDetail_Network_Port_hover": "El puerto al que está conectado este dispositivo en el dispositivo de red principal. Si se deja vacío, se muestra un icono de wifi en el árbol de Red.",
"DevDetail_Nmap_Scans": "Escaneos de Nmap",
"DevDetail_Nmap_Scans_desc": "Aquí puede ejecutar escaneos NMAP manuales. También puede programar escaneos NMAP automáticos regulares a través del complemento Servicios y puertos (NMAP). Dirígete a <a href=\"https://github.com/jokob-sk/NetAlertX/tree/main/front/plugins/nmap_scan\" target=\"_blank\">Documentación</a> para obtener más información",
"DevDetail_Nmap_Scans_desc": "Aquí puede ejecutar escaneos NMAP manuales. También puede programar escaneos NMAP automáticos regulares a través del complemento Servicios y puertos (NMAP). Dirígete a <a href=\"https://github.com/netalertx/NetAlertX/tree/main/front/plugins/nmap_scan\" target=\"_blank\">Documentación</a> para obtener más información",
"DevDetail_Nmap_buttonDefault": "Escaneado predeterminado",
"DevDetail_Nmap_buttonDefault_text": "Escaneo predeterminado: NMAP escanea los 1,000 puertos principales para cada protocolo de escaneo solicitado. Esto atrapa aproximadamente el 93% de los puertos TCP y el 49% de los puertos UDP. (aproximadamente 5 segundos)",
"DevDetail_Nmap_buttonDetail": "Escaneo detallado",
@@ -203,12 +203,19 @@
"Device_MultiEdit_Backup": "Tenga cuidado, ingresar valores incorrectos o romperá su configuración. Por favor, haga una copia de seguridad de su base de datos o de la configuración de los dispositivos primero (<a href=\"#\" onclick=\"ExportCSV()\">haga clic para descargar <i class=\"fa-solid fa-download fa-bounce\"></i></a>). Lea cómo recuperar dispositivos de este archivo en la documentación de <a href=\"https://docs.netalertx.com/BACKUPS#scenario-2-corrupted-database\" target=\"_blank\">Copia de seguridad</a>. Para aplicar sus cambios haga click en el ícono de <b>Guardar<i class=\"fa-solid fa-save\"></i></b> en cada campo que quiera actualizar.",
"Device_MultiEdit_Fields": "Editar campos:",
"Device_MultiEdit_MassActions": "Acciones masivas:",
"Device_MultiEdit_No_Devices": "",
"Device_MultiEdit_No_Devices": "Sin dispositivo seleccionado.",
"Device_MultiEdit_Tooltip": "Cuidado. Al hacer clic se aplicará el valor de la izquierda a todos los dispositivos seleccionados anteriormente.",
"Device_Save_Failed": "",
"Device_Save_Unauthorized": "",
"Device_Saved_Success": "",
"Device_Saved_Unexpected": "",
"Device_NextScan_Imminent": "",
"Device_NextScan_In": "",
"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",
"Device_Saved_Unexpected": "La actualización del dispositivo retornó una respuesta inesperada",
"Device_Scanning": "",
"Device_Searchbox": "Búsqueda",
"Device_Shortcut_AllDevices": "Mis dispositivos",
"Device_Shortcut_AllNodes": "Todos los nodos",
@@ -220,17 +227,19 @@
"Device_Shortcut_Favorites": "Favorito(s)",
"Device_Shortcut_NewDevices": "Nuevos dispositivos",
"Device_Shortcut_OnlineChart": "Presencia del dispositivo a lo largo del tiempo",
"Device_Shortcut_Unstable": "Inestable",
"Device_TableHead_AlertDown": "Alerta desactivada",
"Device_TableHead_Connected_Devices": "Conexiones",
"Device_TableHead_CustomProps": "Propiedades / Acciones",
"Device_TableHead_FQDN": "FQDN",
"Device_TableHead_Favorite": "Favorito",
"Device_TableHead_FirstSession": "1ra. sesión",
"Device_TableHead_Flapping": "",
"Device_TableHead_GUID": "GUID",
"Device_TableHead_Group": "Grupo",
"Device_TableHead_IPv4": "",
"Device_TableHead_IPv6": "",
"Device_TableHead_Icon": "Icon",
"Device_TableHead_IPv4": "IPv4",
"Device_TableHead_IPv6": "IPv6",
"Device_TableHead_Icon": "Ícono",
"Device_TableHead_LastIP": "Última IP",
"Device_TableHead_LastIPOrder": "Última orden de IP",
"Device_TableHead_LastSession": "Última conexión",
@@ -253,7 +262,7 @@
"Device_TableHead_SyncHubNodeName": "Nodo de sincronización",
"Device_TableHead_Type": "Tipo",
"Device_TableHead_Vendor": "Fabricante",
"Device_TableHead_Vlan": "",
"Device_TableHead_Vlan": "VLAN",
"Device_Table_Not_Network_Device": "No está configurado como dispositivo de red",
"Device_Table_info": "Mostrando el INICIO y el FINAL de TODAS las entradas",
"Device_Table_nav_next": "Siguiente",
@@ -301,14 +310,14 @@
"Events_Tablelenght": "Mostrando entradas del MENÚ",
"Events_Tablelenght_all": "Todos",
"Events_Title": "Eventos",
"FakeMAC_hover": "",
"FieldLock_Error": "",
"FieldLock_Lock_Tooltip": "",
"FieldLock_Locked": "",
"FieldLock_SaveBeforeLocking": "",
"FieldLock_Source_Label": "",
"FieldLock_Unlock_Tooltip": "",
"FieldLock_Unlocked": "",
"FakeMAC_hover": "Este dispositivo tiene una dirección MAC falsa/alterada",
"FieldLock_Error": "Error actualizando el estado de bloqueo del campo",
"FieldLock_Lock_Tooltip": "Campo bloqueado (impide sobreescrituras del plugin)",
"FieldLock_Locked": "Campo bloqueado",
"FieldLock_SaveBeforeLocking": "Guarda tus cambios antes de bloquear",
"FieldLock_Source_Label": "Fuente: ",
"FieldLock_Unlock_Tooltip": "Campo desbloqueado (permite sobreescritura del plugin)",
"FieldLock_Unlocked": "Campo desbloqueado",
"GRAPHQL_PORT_description": "El número de puerto del servidor GraphQL. Asegúrese de que el puerto sea único en todas sus aplicaciones en este host y en las instancias de NetAlertX.",
"GRAPHQL_PORT_name": "Puerto GraphQL",
"Gen_Action": "Acción",
@@ -316,23 +325,27 @@
"Gen_AddDevice": "Añadir dispositivo",
"Gen_Add_All": "Añadir todo",
"Gen_All_Devices": "Todos los dispositivos",
"Gen_Archived": "",
"Gen_AreYouSure": "¿Estás seguro?",
"Gen_Backup": "Ejecutar copia de seguridad",
"Gen_Cancel": "Cancelar",
"Gen_Change": "Cambiar",
"Gen_Copy": "Ejecutar",
"Gen_CopyToClipboard": "",
"Gen_CopyToClipboard": "Copiar al portapapeles",
"Gen_DataUpdatedUITakesTime": "Correcto - La interfaz puede tardar en actualizarse si se está ejecutando un escaneo.",
"Gen_Delete": "Eliminar",
"Gen_DeleteAll": "Eliminar todo",
"Gen_Description": "Descripción",
"Gen_Down": "",
"Gen_Error": "Error",
"Gen_Filter": "Filtro",
"Gen_Flapping": "",
"Gen_Generate": "Generar",
"Gen_InvalidMac": "",
"Gen_Invalid_Value": "",
"Gen_InvalidMac": "Dirección MAC inválida.",
"Gen_Invalid_Value": "Un valor inválido fue ingresado",
"Gen_LockedDB": "Fallo - La base de datos puede estar bloqueada - Pulsa F1 -> Ajustes de desarrolladores -> Consola o prueba más tarde.",
"Gen_NetworkMask": "",
"Gen_NetworkMask": "Máscara de red",
"Gen_New": "",
"Gen_Offline": "Desconectado",
"Gen_Okay": "Aceptar",
"Gen_Online": "En linea",
@@ -350,7 +363,8 @@
"Gen_SelectIcon": "<i class=\"fa-solid fa-chevron-down fa-fade\"></i>",
"Gen_SelectToPreview": "Seleccionar para previsualizar",
"Gen_Selected_Devices": "Dispositivos seleccionados:",
"Gen_Subnet": "",
"Gen_Sleeping": "",
"Gen_Subnet": "Subred",
"Gen_Switch": "Cambiar",
"Gen_Upd": "Actualizado correctamente",
"Gen_Upd_Fail": "Fallo al actualizar",
@@ -358,7 +372,7 @@
"Gen_Update_Value": "Actualizar valor",
"Gen_ValidIcon": "<i class=\"fa-solid fa-chevron-right \"></i>",
"Gen_Warning": "Advertencia",
"Gen_Work_In_Progress": "Trabajo en curso, un buen momento para hacer comentarios en https://github.com/jokob-sk/NetAlertX/issues",
"Gen_Work_In_Progress": "Trabajo en curso, un buen momento para hacer comentarios en https://github.com/netalertx/NetAlertX/issues",
"Gen_create_new_device": "Nuevo dispositivo",
"Gen_create_new_device_info": "Los dispositivos se suelen descubrir utilizando <a target=\"_blank\" href=\"https://docs.netalertx.com/PLUGINS\">plugins</a>. Sin embargo, en algunos casos, es posible que necesite agregar dispositivos manualmente. Para explorar escenarios específicos, consulte la documentación <a target=\"_blank\" href=\"https://docs.netalertx.com/REMOTE_NETWORKS\">Redes remotas</a>.",
"General_display_name": "General",
@@ -374,7 +388,7 @@
"Loading": "Cargando . . .",
"Login_Box": "Ingrese su contraseña",
"Login_Default_PWD": "La contraseña por defecto \"123456\" sigue activa.",
"Login_Info": "Las contraseñas se establecen a través del plugin Establecer contraseña. Compruebe la <a target=\"_blank\" href=\"https://github.com/jokob-sk/NetAlertX/tree/main/front/plugins/set_password\">documentación SETPWD</a> si tiene problemas para iniciar sesión.",
"Login_Info": "Las contraseñas se establecen a través del plugin Establecer contraseña. Compruebe la <a target=\"_blank\" href=\"https://github.com/netalertx/NetAlertX/tree/main/front/plugins/set_password\">documentación SETPWD</a> si tiene problemas para iniciar sesión.",
"Login_Psw-box": "Contraseña",
"Login_Psw_alert": "¡Alerta de Contraseña!",
"Login_Psw_folder": "en la carpeta config.",
@@ -430,10 +444,10 @@
"Maintenance_Tool_ImportPastedConfig": "Importar ajustes (pegar)",
"Maintenance_Tool_ImportPastedConfig_noti_text": "¿Seguro que quieres importar la configuración pegada? Esto <b>sobrescribirá</b> por completo el archivo <code>app.conf</code>.",
"Maintenance_Tool_ImportPastedConfig_text": "Importa el archivo <code>app.conf</code> que contiene toda la configuración de la aplicación. Es recomendable descargar primero el archivo <code>app.conf</code> actual con la <b>Exportación de configuración</b>.",
"Maintenance_Tool_UnlockFields": "",
"Maintenance_Tool_UnlockFields_noti": "",
"Maintenance_Tool_UnlockFields_noti_text": "",
"Maintenance_Tool_UnlockFields_text": "",
"Maintenance_Tool_UnlockFields": "Campos Desbloqueo del Dispositivo",
"Maintenance_Tool_UnlockFields_noti": "Campos Desbloqueo del Dispositivo",
"Maintenance_Tool_UnlockFields_noti_text": "¿Está seguro que desea limpiar los valores fuente (BLOQUEADO/USUARIO) para todos los campos dispositivo en todos los dispositivos? Esta acción no puede deshacerse.",
"Maintenance_Tool_UnlockFields_text": "Esta herramienta removerá todos los valores fuente de cada campo de traza para todos los dispositivos, desbloqueando efectivamente todos los campos para plugins y usuarios. Use esto con precaución, dado que afectará todo su inventario de dispositivos.",
"Maintenance_Tool_arpscansw": "Activar arp-scan (on/off)",
"Maintenance_Tool_arpscansw_noti": "Activar arp-scan on or off",
"Maintenance_Tool_arpscansw_noti_text": "Cuando el escaneo se ha apagado, permanece apagado hasta que se active nuevamente.",
@@ -443,9 +457,9 @@
"Maintenance_Tool_backup_noti_text": "¿Estás seguro de que quieres exactos la copia de seguridad de DB? Asegúrese de que ningún escaneo se esté ejecutando actualmente.",
"Maintenance_Tool_backup_text": "Las copias de seguridad de la base de datos se encuentran en el directorio de la base de datos como una Zip-Archive, nombrada con la fecha de creación. No hay un número máximo de copias de seguridad.",
"Maintenance_Tool_check_visible": "Desactivar para ocultar columna.",
"Maintenance_Tool_clearSourceFields_selected": "",
"Maintenance_Tool_clearSourceFields_selected_noti": "",
"Maintenance_Tool_clearSourceFields_selected_text": "",
"Maintenance_Tool_clearSourceFields_selected": "Limpiar campos fuente",
"Maintenance_Tool_clearSourceFields_selected_noti": "Limpiar fuentes",
"Maintenance_Tool_clearSourceFields_selected_text": "Esto limpiará todos los campos fuente de los dispositivos seleccionados. Esta acción no puede deshacerse.",
"Maintenance_Tool_darkmode": "Cambiar Modo (Dark/Light)",
"Maintenance_Tool_darkmode_noti": "Cambiar Modo",
"Maintenance_Tool_darkmode_noti_text": "Después del cambio de tema, la página intenta volver a cargar para activar el cambio. Si es necesario, el caché debe ser eliminado.",
@@ -476,7 +490,7 @@
"Maintenance_Tool_del_unknowndev_noti": "Eliminar dispositivos (desconocidos)",
"Maintenance_Tool_del_unknowndev_noti_text": "¿Estás seguro de que quieres eliminar todos los dispositivos (desconocidos)?",
"Maintenance_Tool_del_unknowndev_text": "Antes de usar esta función, haga una copia de seguridad. La eliminación no se puede deshacer. Todos los dispositivos nombrados (desconocidos) se eliminarán de la base de datos.",
"Maintenance_Tool_del_unlockFields_selecteddev_text": "",
"Maintenance_Tool_del_unlockFields_selecteddev_text": "Esto desbloqueará los campos BLOQUEADO/USUARIO de los dispositivos seleccionados. Esta acción no puede deshacerse.",
"Maintenance_Tool_displayed_columns_text": "Cambia la visibilidad y el orden de las columnas en la página <a href=\"devices.php\"><b> <i class=\"fa fa-laptop\"></i> Dispositivos</b></a>.",
"Maintenance_Tool_drag_me": "Coger para rearrastrar columnas.",
"Maintenance_Tool_order_columns_text": "Maintenance_Tool_order_columns_text",
@@ -488,8 +502,8 @@
"Maintenance_Tool_restore_noti": "Restaurar DB",
"Maintenance_Tool_restore_noti_text": "¿Estás seguro de que quieres hacer exactos la restauración de DB? Asegúrese de que ningún escaneo se esté ejecutando actualmente.",
"Maintenance_Tool_restore_text": "La última copia de seguridad se puede restaurar a través del botón, pero las copias de seguridad anteriores solo se pueden restaurar manualmente. Después de la restauración, realice una verificación de integridad en la base de datos por seguridad, en caso de que el DB estuviera actualmente en acceso de escritura cuando se creó la copia de seguridad.",
"Maintenance_Tool_unlockFields_selecteddev": "",
"Maintenance_Tool_unlockFields_selecteddev_noti": "",
"Maintenance_Tool_unlockFields_selecteddev": "Campos dispositivo desbloqueado",
"Maintenance_Tool_unlockFields_selecteddev_noti": "Campos desbloqueo",
"Maintenance_Tool_upgrade_database_noti": "Actualizar la base de datos",
"Maintenance_Tool_upgrade_database_noti_text": "¿Estás seguro de que quieres actualizar la base de datos? <br> (tal vez prefieras archivarla)",
"Maintenance_Tool_upgrade_database_text": "Este botón actualizará la base de datos para habilitar la actividad de la red en las últimas 12 horas. Haga una copia de seguridad de su base de datos en caso de problemas.",
@@ -502,7 +516,7 @@
"Maintenance_arp_status_off": "está actualmente deshabilitado",
"Maintenance_arp_status_on": "escaneo en ejecución",
"Maintenance_built_on": "Creada",
"Maintenance_current_version": "No hay actualizaciones disponibles. Comprueba en que <a href=\"https://github.com/jokob-sk/NetAlertX/issues/138\" target=\"_blank\">se está trabajando</a>.",
"Maintenance_current_version": "No hay actualizaciones disponibles. Comprueba en que <a href=\"https://github.com/netalertx/NetAlertX/issues/138\" target=\"_blank\">se está trabajando</a>.",
"Maintenance_database_backup": "Copias de seguridad de BD",
"Maintenance_database_backup_found": "copia(s) de seguridad encontrada(s)",
"Maintenance_database_backup_total": "Uso total de disco",
@@ -514,7 +528,7 @@
"Maintenance_lang_selector_empty": "Elija un idioma",
"Maintenance_lang_selector_lable": "Seleccione su idioma",
"Maintenance_lang_selector_text": "El cambio se produce en el lado del cliente, por lo que sólo afecta al navegador actual.",
"Maintenance_new_version": "Una nueva versión está disponible. Comprueba las <a href=\"https://github.com/jokob-sk/NetAlertX/releases\" target=\"_blank\">notas de lanzamiento</a>.",
"Maintenance_new_version": "Una nueva versión está disponible. Comprueba las <a href=\"https://github.com/netalertx/NetAlertX/releases\" target=\"_blank\">notas de lanzamiento</a>.",
"Maintenance_themeselector_apply": "Aplicar",
"Maintenance_themeselector_empty": "Elige un tema",
"Maintenance_themeselector_lable": "Seleccionar tema",
@@ -604,6 +618,8 @@
"PIALERT_WEB_PROTECTION_name": "Habilitar inicio de sesión",
"PLUGINS_KEEP_HIST_description": "¿Cuántas entradas de los resultados del análisis del historial de complementos deben conservarse (globalmente, no específico del dispositivo!).",
"PLUGINS_KEEP_HIST_name": "Historial de complementos",
"PRAGMA_JOURNAL_SIZE_LIMIT_description": "",
"PRAGMA_JOURNAL_SIZE_LIMIT_name": "",
"PUSHSAFER_TOKEN_description": "Su clave secreta de la API de Pushsafer (token).",
"PUSHSAFER_TOKEN_name": "Token de Pushsafer",
"PUSHSAFER_display_name": "Pushsafer",
@@ -687,7 +703,7 @@
"Settings_device_Scanners_desync": "⚠ Los horarios del escáner de los dispositivos no están sincronizados.",
"Settings_device_Scanners_desync_popup": "Los horarios de escáneres de dispositivos (<code> *_RUN_SCHD</code> ) no son lo mismo. Esto resultará en notificaciones inconsistentes del dispositivo en línea/fuera de línea. A menos que sea así, utilice el mismo horario para todos los habilitados.<b> 🔍Escáneres de dispositivos</b> .",
"Speedtest_Results": "Resultados de la prueba de velocidad",
"Systeminfo_AvailableIps": "",
"Systeminfo_AvailableIps": "IPs disponibles",
"Systeminfo_CPU": "CPU",
"Systeminfo_CPU_Cores": "Núcleos de CPU:",
"Systeminfo_CPU_Name": "Nombre de la CPU:",
@@ -781,7 +797,7 @@
"UI_REFRESH_name": "Actualización automática de la interfaz de usuario",
"VERSION_description": "Valor de ayuda de versión o marca de tiempo para comprobar si la aplicación se ha actualizado.",
"VERSION_name": "Versión o marca de tiempo",
"WEBHOOK_PAYLOAD_description": "El formato de datos de carga de Webhook para el atributo <code>body</code> > <code>attachments</code> > <code>text</code> en el json de carga. Vea un ejemplo de la carga <a target=\"_blank\" href=\"https://github.com/jokob-sk/NetAlertX/blob/main/front/report_templates/webhook_json_sample.json\">aquí</a>. (por ejemplo: para discord use <code>text</code>)",
"WEBHOOK_PAYLOAD_description": "El formato de datos de carga de Webhook para el atributo <code>body</code> > <code>attachments</code> > <code>text</code> en el json de carga. Vea un ejemplo de la carga <a target=\"_blank\" href=\"https://github.com/netalertx/NetAlertX/blob/main/front/report_templates/webhook_json_sample.json\">aquí</a>. (por ejemplo: para discord use <code>text</code>)",
"WEBHOOK_PAYLOAD_name": "Tipo de carga",
"WEBHOOK_REQUEST_METHOD_description": "El método de solicitud HTTP que se utilizará para la llamada de webhook.",
"WEBHOOK_REQUEST_METHOD_name": "Método de solicitud",

View File

@@ -203,10 +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_NoMatch_Title": "",
"Device_Save_Failed": "",
"Device_Save_Unauthorized": "",
"Device_Saved_Success": "",
"Device_Saved_Unexpected": "",
"Device_Scanning": "",
"Device_Searchbox": "",
"Device_Shortcut_AllDevices": "",
"Device_Shortcut_AllNodes": "",
@@ -218,12 +225,14 @@
"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": "",
@@ -314,6 +323,7 @@
"Gen_AddDevice": "",
"Gen_Add_All": "",
"Gen_All_Devices": "",
"Gen_Archived": "",
"Gen_AreYouSure": "",
"Gen_Backup": "",
"Gen_Cancel": "",
@@ -324,13 +334,16 @@
"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": "",
@@ -348,6 +361,7 @@
"Gen_SelectIcon": "",
"Gen_SelectToPreview": "",
"Gen_Selected_Devices": "",
"Gen_Sleeping": "",
"Gen_Subnet": "",
"Gen_Switch": "",
"Gen_Upd": "",
@@ -577,6 +591,8 @@
"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": "",

Some files were not shown because too many files have changed in this diff Show More