Compare commits
201 Commits
v26.2.6
...
feat-proxm
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1923a063f0 | ||
|
|
01b6b9f04a | ||
|
|
ea77112315 | ||
|
|
b19973130e | ||
|
|
ffbcc2ad25 | ||
|
|
c533c2267c | ||
|
|
ac407bd86e | ||
|
|
1da3c146d2 | ||
|
|
9fe8090a1b | ||
|
|
3ba1b69c1e | ||
|
|
da4d8a9675 | ||
|
|
0f20fb38f0 | ||
|
|
8361f0ac99 | ||
|
|
99de69e30d | ||
|
|
4637ec6350 | ||
|
|
2a4e6ba5e1 | ||
|
|
5be7bbe07d | ||
|
|
e8c43af7b6 | ||
|
|
27f34963be | ||
|
|
594c2fe015 | ||
|
|
14362d20bd | ||
|
|
4f239be8a3 | ||
|
|
5a65d807a8 | ||
|
|
f3bf37bb24 | ||
|
|
b7e1cb1f9d | ||
|
|
b4510663f7 | ||
|
|
dd564b235b | ||
|
|
04db68ea6c | ||
|
|
550f59b34f | ||
|
|
6e8a3d8a58 | ||
|
|
c89b2ded26 | ||
|
|
9f964be0c3 | ||
|
|
d2bc8410a7 | ||
|
|
ab74307ed1 | ||
|
|
95f411d92a | ||
|
|
bc4f419927 | ||
|
|
3a73817048 | ||
|
|
67aa46f1cf | ||
|
|
da63acb675 | ||
|
|
50125f0700 | ||
|
|
6724d250d4 | ||
|
|
3e237bb452 | ||
|
|
15807b7ab9 | ||
|
|
0497c2891e | ||
|
|
8e6efc3008 | ||
|
|
deb0d16c3d | ||
|
|
a94f3d7222 | ||
|
|
d9608b4760 | ||
|
|
584aba2c7b | ||
|
|
ea5585a8ef | ||
|
|
c1adfd35f3 | ||
|
|
66532c54a1 | ||
|
|
a6ce4174fe | ||
|
|
247a967e9b | ||
|
|
dbe65b2a27 | ||
|
|
563cb4ba20 | ||
|
|
3d4aba4b39 | ||
|
|
b96ace0447 | ||
|
|
e15c68d189 | ||
|
|
f5e411d5d5 | ||
|
|
f727580798 | ||
|
|
11499a6890 | ||
|
|
85badb0760 | ||
|
|
814ba02d1c | ||
|
|
e57fd2e81e | ||
|
|
4dc2a63ebb | ||
|
|
6b320877ec | ||
|
|
43667a3bc4 | ||
|
|
4d0b7c944f | ||
|
|
9894009455 | ||
|
|
0e18e34918 | ||
|
|
d9c263d506 | ||
|
|
58e32a5b43 | ||
|
|
24e2036bde | ||
|
|
b74b803d6c | ||
|
|
173ffbe3b2 | ||
|
|
d2ebe0d452 | ||
|
|
4c0d5c7376 | ||
|
|
686a713aa8 | ||
|
|
9d64665599 | ||
|
|
63cef590d6 | ||
|
|
00042ab594 | ||
|
|
786cc5ee33 | ||
|
|
0b32a06178 | ||
|
|
1fa381429d | ||
|
|
fae61174a7 | ||
|
|
d06301ac80 | ||
|
|
f4bc9c93c3 | ||
|
|
0172ab4311 | ||
|
|
f1fc9f24b1 | ||
|
|
c192f2c032 | ||
|
|
a309f99c3d | ||
|
|
54e9d52126 | ||
|
|
8fc78f02e9 | ||
|
|
123f715241 | ||
|
|
446545e7eb | ||
|
|
14625926f9 | ||
|
|
c7e754966e | ||
|
|
4316a436eb | ||
|
|
fe22659794 | ||
|
|
cb0b3b607d | ||
|
|
53b2596902 | ||
|
|
1a364e2fe2 | ||
|
|
2f1e5068e3 | ||
|
|
57118bc9bd | ||
|
|
25a81556e3 | ||
|
|
39f617be5f | ||
|
|
c4c966ffa7 | ||
|
|
f88aefe022 | ||
|
|
54db347b94 | ||
|
|
2ae87fca38 | ||
|
|
8224363c45 | ||
|
|
eb399ec193 | ||
|
|
70645e7ef3 | ||
|
|
0e94dcb091 | ||
|
|
a26137800d | ||
|
|
63810bc536 | ||
|
|
57d451fcf4 | ||
|
|
bf6218e836 | ||
|
|
e9efabd562 | ||
|
|
eb0f705587 | ||
|
|
2559702a6a | ||
|
|
6bbfc0637c | ||
|
|
688d49b5ae | ||
|
|
ab7df4384e | ||
|
|
2018636bf8 | ||
|
|
50f341e84f | ||
|
|
32c21b01bb | ||
|
|
05c332867b | ||
|
|
12b0d911ff | ||
|
|
04884a264b | ||
|
|
2742414123 | ||
|
|
876cd4bbe1 | ||
|
|
91775deaa3 | ||
|
|
7075091569 | ||
|
|
f63658af7d | ||
|
|
774c123804 | ||
|
|
32e2d571a0 | ||
|
|
f2af4ffdb8 | ||
|
|
bc97a80375 | ||
|
|
fa36adb015 | ||
|
|
264cae3338 | ||
|
|
b594472f30 | ||
|
|
6d98ee9c2a | ||
|
|
1181b56b16 | ||
|
|
4b58f3b23f | ||
|
|
e61bf097ac | ||
|
|
64dbf8a3ba | ||
|
|
5685a67483 | ||
|
|
c1e6a69e05 | ||
|
|
3587169791 | ||
|
|
fd71527b09 | ||
|
|
9676111ceb | ||
|
|
60036a49c2 | ||
|
|
60ccfc734d | ||
|
|
c91532f3de | ||
|
|
aeaab6d408 | ||
|
|
5e492bc81e | ||
|
|
db689ac269 | ||
|
|
bb39bde9dd | ||
|
|
46781ed71a | ||
|
|
a313b0ccc5 | ||
|
|
2765e441a5 | ||
|
|
eb35e80916 | ||
|
|
4e7df766eb | ||
|
|
e741ff51b5 | ||
|
|
a81255fb18 | ||
|
|
5caa240fcd | ||
|
|
888d39d2fb | ||
|
|
b57d36607a | ||
|
|
70c3530a5c | ||
|
|
7af850cb56 | ||
|
|
9ac8f6fe34 | ||
|
|
933004e792 | ||
|
|
45157b6156 | ||
|
|
a560009611 | ||
|
|
e0d4e9ea9c | ||
|
|
249d12ded4 | ||
|
|
e899f657c5 | ||
|
|
3036cd04fc | ||
|
|
3d3abe7e53 | ||
|
|
a088f4580a | ||
|
|
75c7d6c015 | ||
|
|
d434cc5315 | ||
|
|
cedbd59897 | ||
|
|
b703397543 | ||
|
|
9c4e02f565 | ||
|
|
3510afec7a | ||
|
|
ed44c68d54 | ||
|
|
30c832b14e | ||
|
|
d7f17c8e78 | ||
|
|
8538c87fef | ||
|
|
1bacb59044 | ||
|
|
827b5d2ad3 | ||
|
|
e70bbdb78e | ||
|
|
946ad00253 | ||
|
|
3734c43284 | ||
|
|
0ce4e5f70c | ||
|
|
c074ce1b11 | ||
|
|
5e40ea83d9 | ||
|
|
2124c2e1e2 |
@@ -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
@@ -6,7 +6,6 @@ LOGS_LOCATION=/path/to/docker_logs
|
||||
|
||||
#ENVIRONMENT VARIABLES
|
||||
|
||||
TZ=Europe/Paris
|
||||
PORT=20211
|
||||
|
||||
#DEVELOPMENT VARIABLES
|
||||
|
||||
19
.github/skills/code-standards/SKILL.md
vendored
@@ -5,6 +5,14 @@ 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 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
|
||||
- code files should be less than 500 LOC for better maintainability
|
||||
|
||||
## File Length
|
||||
|
||||
Keep code files under 500 lines. Split larger files into modules.
|
||||
@@ -42,11 +50,18 @@ Nested subprocess calls need their own timeout—outer timeout won't save you.
|
||||
## Time Utilities
|
||||
|
||||
```python
|
||||
from utils.datetime_utils import timeNowDB
|
||||
from utils.datetime_utils import timeNowUTC
|
||||
|
||||
timestamp = timeNowDB()
|
||||
timestamp = timeNowUTC()
|
||||
```
|
||||
|
||||
This is the ONLY function that calls datetime.datetime.now() in the entire codebase.
|
||||
|
||||
⚠️ CRITICAL: ALL database timestamps MUST be stored in UTC
|
||||
This is the SINGLE SOURCE OF TRUTH for current time in NetAlertX
|
||||
Use timeNowUTC() for DB writes (returns UTC string by default)
|
||||
Use timeNowUTC(as_string=False) for datetime operations (scheduling, comparisons, logging)
|
||||
|
||||
## String Sanitization
|
||||
|
||||
Use sanitizers from `server/helper.py` before storing user input.
|
||||
|
||||
8
.github/skills/settings-management/SKILL.md
vendored
@@ -37,11 +37,3 @@ Define in plugin's `config.json` manifest under the settings section.
|
||||
## Environment Override
|
||||
|
||||
Use `APP_CONF_OVERRIDE` environment variable for settings that must be set before startup.
|
||||
|
||||
## Backend API URL
|
||||
|
||||
For Codespaces, set `BACKEND_API_URL` to your Codespace URL:
|
||||
|
||||
```
|
||||
BACKEND_API_URL=https://something-20212.app.github.dev/
|
||||
```
|
||||
|
||||
6
.github/workflows/run-all-tests.yml
vendored
@@ -12,7 +12,7 @@ on:
|
||||
type: boolean
|
||||
default: false
|
||||
run_backend:
|
||||
description: '📂 backend/ (SQL Builder & Security)'
|
||||
description: '📂 backend/ & db/ (SQL Builder, Security & Migration)'
|
||||
type: boolean
|
||||
default: false
|
||||
run_docker_env:
|
||||
@@ -43,9 +43,9 @@ jobs:
|
||||
run: |
|
||||
PATHS=""
|
||||
# Folder Mapping with 'test/' prefix
|
||||
if [ "${{ github.event.inputs.scan }}" == "true" ]; then PATHS="$PATHS test/scan/"; fi
|
||||
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/"; 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
|
||||
|
||||
|
||||
2
.gitignore
vendored
@@ -24,6 +24,8 @@ front/api/*
|
||||
/api/*
|
||||
**/plugins/**/*.log
|
||||
**/plugins/cloud_services/*
|
||||
**/plugins/cloud_connector/*
|
||||
**/plugins/heartbeat/*
|
||||
**/%40eaDir/
|
||||
**/@eaDir/
|
||||
|
||||
|
||||
@@ -6,14 +6,14 @@ First off, **thank you** for taking the time to contribute! NetAlertX is built a
|
||||
|
||||
## 📂 Issues, Bugs, and Feature Requests
|
||||
|
||||
Please use the [GitHub Issue Tracker](https://github.com/jokob-sk/NetAlertX/issues) for:
|
||||
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)
|
||||
- 🔍 [Search Closed Issues](https://github.com/netalertx/NetAlertX/issues?q=is%3Aissue+is%3Aclosed)
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -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 && \
|
||||
|
||||
@@ -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 && \
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[](https://hub.docker.com/r/jokobsk/netalertx)
|
||||
[](https://hub.docker.com/r/jokobsk/netalertx)
|
||||
[](https://github.com/jokob-sk/NetAlertX/releases)
|
||||
[](https://github.com/netalertx/NetAlertX/releases)
|
||||
[](https://discord.gg/NczTUTWyRr)
|
||||
[](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"
|
||||
|
||||
@@ -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-----------------#
|
||||
|
||||
|
||||
56
docs/ADVISORY_EYES_ON_GLASS.md
Normal file
@@ -0,0 +1,56 @@
|
||||
### Build an MSP Wallboard for Network Monitoring
|
||||
|
||||
For Managed Service Providers (MSPs) and Network Operations Centers (NOC), "Eyes on Glass" monitoring requires a UI that is both self-healing (auto-refreshing) and focused only on critical data. By leveraging the **UI Settings Plugin**, you can transform NetAlertX from a management tool into a dedicated live monitor.
|
||||
|
||||

|
||||
|
||||
---
|
||||
|
||||
### 1. Configure Auto-Refresh for Live Monitoring
|
||||
|
||||
Static dashboards are the enemy of real-time response. NetAlertX allows you to force the UI to pull fresh data without manual page reloads.
|
||||
|
||||
* **Setting:** Locate the `UI_REFRESH` (or similar "Auto-refresh UI") setting within the **UI Settings plugin**.
|
||||
* **Optimal Interval:** Set this between **60 to 120 seconds**.
|
||||
* *Note:* Refreshing too frequently (e.g., <30s) on large networks can lead to high browser and server CPU usage.
|
||||
|
||||

|
||||
|
||||
### 2. Streamlining the Dashboard (MSP Mode)
|
||||
|
||||
An MSP's focus is on what is *broken*, not what is working. Hide the noise to increase reaction speed.
|
||||
|
||||
* **Hide Unnecessary Blocks:** Under UI Settings, disable dashboard blocks that don't provide immediate utility, such as **Online presence** or **Tiles**.
|
||||
* **Hide virtual connections:** You can specify which relationships shoudl be hidden from the main view to remove any virtual devices that are not essential from your views.
|
||||
* **Browser Full-Screen:** Use the built-in "Full Screen" toggle in the top bar to remove browser chrome (URL bars/tabs) for a cleaner "Wallboard" look.
|
||||
|
||||
### 3. Creating Custom NOC Views
|
||||
|
||||
Use the UI Filters in tandem with UI Settings to create custom views.
|
||||
|
||||

|
||||
|
||||
| Feature | NOC/MSP Application |
|
||||
| --- | --- |
|
||||
| **Site-Specific Nodes** | Filter the view by a specific "Sync Node" or "Location" filter to monitor a single client site. |
|
||||
| **Filter by Criticality** | Filter devices where `Group == "Infrastructure"` or `"Server"`. (depending on your predefined values) |
|
||||
| **Predefined "Down" View** | Bookmark the URL with the `/devices.php#down` path to ensure the dashboard always loads into an "Alert Only" mode. |
|
||||
|
||||
### 4. Browser & Cache Stability
|
||||
|
||||
Because the UI is a web application, long-running sessions can occasionally experience cache drift.
|
||||
|
||||
* **Cache Refresh:** If you notice the "Show # Entries" resetting or icons failing to load after days of uptime, use the **Reload** icon in the application header (not the browser refresh) to clear the internal app cache.
|
||||
* **Dedicated Hardware:** For 24/7 monitoring, use a dedicated thin client or Raspberry Pi running in "Kiosk Mode" to prevent OS-level popups from obscuring the dashboard.
|
||||
|
||||
> [!TIP]
|
||||
> [NetAlertX - Detailed Dashboard Guide](https://www.youtube.com/watch?v=umh1c_40HW8)
|
||||
> This video provides a visual walkthrough of the NetAlertX dashboard features, including how to map and visualize devices which is crucial for setting up a clear "Eyes on Glass" monitoring environment.
|
||||
|
||||
### Summary Checklist
|
||||
|
||||
* [ ] **Automate Refresh:** Set `UI_REFRESH` to **60-120s** in UI Settings to ensure the dashboard stays current without manual intervention.
|
||||
* [ ] **Filter for Criticality:** Bookmark the **`/devices.php#down`** view to instantly focus on offline assets rather than the entire inventory.
|
||||
* [ ] **Remove UI Noise:** Use UI Settings to hide non-essential dashboard blocks (e.g., **Tiles** or remove **Virtual Connections** devices) to maximize screen real estate for alerts.
|
||||
* [ ] **Segment by Site:** Use **Location** or **Sync Node** filters to create dedicated views for specific client networks or physical branches.
|
||||
* [ ] **Ensure Stability:** Run on a dedicated "Kiosk" browser and use the internal **Reload icon** occasionally to maintain a clean application cache.
|
||||
121
docs/ADVISORY_MULTI_NETWORK.md
Normal file
@@ -0,0 +1,121 @@
|
||||
## ADVISORY: Best Practices for Monitoring Multiple Networks with NetAlertX
|
||||
|
||||
### 1. Define Monitoring Scope & Architecture
|
||||
|
||||
Effective multi-network monitoring starts with understanding how NetAlertX "sees" your traffic.
|
||||
|
||||
* **A. Understand Network Accessibility:** Local ARP-based scanning (**ARPSCAN**) only discovers devices on directly accessible subnets due to Layer 2 limitations. It cannot traverse VPNs or routed borders without specific configuration.
|
||||
* **B. Plan Subnet & Scan Interfaces:** Explicitly configure each accessible segment in `SCAN_SUBNETS` with the corresponding interfaces.
|
||||
* **C. Remote & Inaccessible Networks:** For networks unreachable via ARP, use these strategies:
|
||||
* **Alternate Plugins:** Supplement discovery with [SNMPDSC](SNMPDSC) or [DHCP lease imports](https://docs.netalertx.com/PLUGINS/?h=DHCPLSS#available-plugins).
|
||||
* **Centralized Multi-Tenant Management using Sync Nodes:** Run secondary NetAlertX instances on isolated networks and aggregate data using the **SYNC plugin**.
|
||||
* **Manual Entry:** For static assets where only ICMP (ping) status is needed.
|
||||
|
||||
> [!TIP]
|
||||
> Explore the [remote networks](./REMOTE_NETWORKS.md) documentation for more details on how to set up the approaches menationed above.
|
||||
|
||||
---
|
||||
|
||||
### 2. Automating IT Asset Inventory with Workflows
|
||||
|
||||
[Workflows](./WORKFLOWS.md) are the "engine" of NetAlertX, reducing manual overhead as your device list grows.
|
||||
|
||||
* **A. Logical Ownership & VLAN Tagging:** Create a workflow triggered on **Device Creation** to:
|
||||
1. Inspect the IP/Subnet.
|
||||
2. Set `devVlan` or `devOwner` custom fields automatically.
|
||||
|
||||
|
||||
* **B. Auto-Grouping:** Use conditional logic to categorize devices.
|
||||
* *Example:* If `devLastIP == 10.10.20.*`, then `Set devLocation = "BranchOffice"`.
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "Assign Location - BranchOffice",
|
||||
"trigger": {
|
||||
"object_type": "Devices",
|
||||
"event_type": "update"
|
||||
},
|
||||
"conditions": [
|
||||
{
|
||||
"logic": "AND",
|
||||
"conditions": [
|
||||
{
|
||||
"field": "devLastIP",
|
||||
"operator": "contains",
|
||||
"value": "10.10.20."
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"actions": [
|
||||
{
|
||||
"type": "update_field",
|
||||
"field": "devLocation",
|
||||
"value": "BranchOffice"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
* **C. Sync Node Tracking:** When using multiple instances, ensure all synchub nodes have a descriptive `SYNC_node_name` name to distinguish between sites.
|
||||
|
||||
> [!TIP]
|
||||
> Always test new workflows in a "Staging" instance. A misconfigured workflow can trigger thousands of unintended updates across your database.
|
||||
|
||||
---
|
||||
|
||||
### 3. Notification Strategy: Low Noise, High Signal
|
||||
|
||||
A multi-network environment can generate significant "alert fatigue." Use a layered filtering approach.
|
||||
|
||||
| Level | Strategy | Recommended Action |
|
||||
| --- | --- | --- |
|
||||
| **Device** | Silence Flapping | Use "Skip repeated notifications" for unstable IoT devices. |
|
||||
| **Plugin** | Tune Watchers | Only enable `_WATCH` on reliable plugins (e.g., ICMP/SNMP). |
|
||||
| **Global** | Filter Sections | Limit `NTFPRCS_INCLUDED_SECTIONS` to `new_devices` and `down_devices`. |
|
||||
|
||||
|
||||
> [!TIP]
|
||||
> **Ignore Rules:** Maintain strict **Ignored MAC** (`NEWDEV_ignored_MACs`) and **Ignored IP** (`NEWDEV_ignored_IPs`) lists for guest networks or broadcast scanners to keep your logs clean.
|
||||
|
||||
---
|
||||
|
||||
### 4. UI Filters for Multi-Network Clarity
|
||||
|
||||
Don't let a massive device list overwhelm you. Use the [Multi-edit features](./DEVICES_BULK_EDITING.md) to categorize devices and create focused views:
|
||||
|
||||
* **By Zone:** Filter by "Location", "Site" or "Sync Node" you et up in Section 2.
|
||||
* **By Criticality:** Use custom the device Type field to separate "Core Infrastructure" from "Ephemeral Clients."
|
||||
* **By Status:** Use predefined views specifically for "Devices currently Down" to act as a Network Operations Center (NOC) dashboard.
|
||||
|
||||
> [!TIP]
|
||||
> If you are providing services as a Managed Service Provider (MSP) customize your default UI to be exactly how you need it, by hiding parts of the UI that you are not interested in, or by configuring a auto-refreshed screen monitoring your most important clients. See the [Eyes on glass](./ADVISORY_EYES_ON_GLASS.md) advisory for more details.
|
||||
|
||||
---
|
||||
|
||||
### 5. Operational Stability & Sync Health
|
||||
|
||||
* **Health Checks:** Regularly monitor the [Logs](https://docs.netalertx.com/LOGGING/?h=logs) to ensure remote nodes are reporting in.
|
||||
* **Backups:** Use the **CSV Devices Backup** plugin. Standardize your workflow templates and [back up](./BACKUPS.md) you `/config` folders so that if a node fails, you can redeploy it with the same logic instantly.
|
||||
|
||||
|
||||
### 6. Optimize Performance
|
||||
|
||||
As your environment grows, tuning the underlying engine is vital to maintain a snappy UI and reliable discovery cycles.
|
||||
|
||||
* **Plugin Scheduling:** Avoid "Scan Storms" by staggering plugin execution. Running intensive tasks like `NMAP` or `MASS_DNS` simultaneously can spike CPU and cause database locks.
|
||||
* **Database Health:** Large-scale monitoring generates massive event logs. Use the **[DBCLNP (Database Cleanup)](https://www.google.com/search?q=https://docs.netalertx.com/PLUGINS/%23dbclnp)** plugin to prune old records and keep the SQLite database performant.
|
||||
* **Resource Management:** For high-device counts, consider increasing the memory limit for the container and utilizing `tmpfs` for temporary files to reduce SD card/disk I/O bottlenecks.
|
||||
|
||||
> [!IMPORTANT]
|
||||
> For a deep dive into hardware requirements, database vacuuming, and specific environment variables for high-load instances, refer to the full **[Performance Optimization Guide](https://docs.netalertx.com/PERFORMANCE/)**.
|
||||
|
||||
---
|
||||
|
||||
### Summary Checklist
|
||||
|
||||
* [ ] **Discovery:** Are all subnets explicitly defined?
|
||||
* [ ] **Automation:** Do new devices get auto-assigned to a VLAN/Owner?
|
||||
* [ ] **Noise Control:** Are transient "Down" alerts delayed via `NTFPRCS_alert_down_time`?
|
||||
* [ ] **Remote Sites:** Is the SYNC plugin authenticated and heartbeat-active?
|
||||
@@ -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. |
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -120,3 +120,23 @@ With `ARPSCAN` scans some devices might flip IP addresses after each scan trigge
|
||||
See how to prevent IP flipping in the [ARPSCAN plugin guide](/front/plugins/arp_scan/README.md).
|
||||
|
||||
Alternatively adjust your [notification settings](./NOTIFICATIONS.md) to prevent false positives by filtering out events or devices.
|
||||
|
||||
#### Multiple NICs on Same Host Reporting Same IP
|
||||
|
||||
On systems with multiple NICs (like a Proxmox server), each NIC has its own MAC address. Sometimes NetAlertX can incorrectly assign the same IP to all NICs, causing false device mappings. This is due to the way ARP responses are handled by the OS and cannot be overridden directly in NetAlertX.
|
||||
|
||||
**Resolution (Linux-based systems, e.g., Proxmox):**
|
||||
|
||||
Run the following commands on the host to fix ARP behavior:
|
||||
|
||||
```bash
|
||||
sudo sysctl -w net.ipv4.conf.all.arp_ignore=1
|
||||
sudo sysctl -w net.ipv4.conf.all.arp_announce=2
|
||||
```
|
||||
|
||||
This ensures each NIC responds correctly to ARP requests and prevents NetAlertX from misassigning IPs.
|
||||
|
||||
> For setups with multiple interfaces on the same switch, consider [workflows](./WORKFLOWS.md), [device exclusions](./NOTIFICATIONS.md), or [dummy devices](./DEVICE_MANAGEMENT.md) as additional workarounds.
|
||||
> See [Feature Requests](https://github.com/netalertx/netalertx/issues) for reporting edge cases.
|
||||
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||

|
||||
@@ -8,13 +8,17 @@ This set of settings allows you to group Devices under different views. The Arch
|
||||
|
||||
## Status Colors
|
||||
|
||||

|
||||
|
||||
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) |  | A device that is no longer marked as a "New Device". |
|
||||
| <i class="fa-solid fa-plug"></i> | New (Green) |  | 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) |  | The device is online, but unstable and flapping (3 status changes in the last hour). |
|
||||
| <i class="fa-solid fa-xmark"></i> | New (Grey) |  | Same as "New (Green)" but the device is now offline. |
|
||||
| <i class="fa-solid fa-box-archive"></i> | New (Grey) |  | Same as "New (Green)" but the device is now offline and archived. |
|
||||
| <i class="fa-solid fa-xmark"></i> | Offline (Grey) |  | A device that was not detected online in the last scan. |
|
||||
| <i class="fa-solid fa-box-archive"></i> | Archived (Grey) |  | A device that was not detected online in the last scan. |
|
||||
| <i class="fa-solid fa-moon"></i> | Sleeping (Aqua) |  | 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) |  | A device marked as "Alert Down" and offline for the duration set in `NTFPRCS_alert_down_time`.|
|
||||
|
||||
|
||||
See also [Notification guide](./NOTIFICATIONS.md).
|
||||
@@ -39,9 +39,24 @@ The **MAC** field and the **Last IP** field will then become editable.
|
||||

|
||||
|
||||
|
||||
> [!NOTE]
|
||||
>
|
||||
> You can couple this with the `ICMP` plugin which can be used to monitor the status of these devices, if they are actual devices reachable with the `ping` command. If not, you can use a loopback IP address so they appear online, such as `0.0.0.0` or `127.0.0.1`.
|
||||
## Dummy or Manually Created Device Status
|
||||
|
||||
You can control a dummy device’s status either via `ICMP` (automatic) or the `Force Status` field (manual). Choose based on whether the device is real and how important **data hygiene** is.
|
||||
|
||||
### `ICMP` (Real Devices)
|
||||
|
||||
Use a real IP that responds to ping so status is updated automatically.
|
||||
|
||||
### `Force Status` (Best for Data Hygiene)
|
||||
|
||||
Manually set the status when the device is not reachable or is purely logical.
|
||||
This keeps your data clean and avoids fake IPs.
|
||||
|
||||
### Loopback IP (`127.0.0.1`, `0.0.0.0`)
|
||||
|
||||
Use when you want the device to always appear online via `ICMP`.
|
||||
Note this simulates reachability and introduces artificial data. This approach might be preferred, if you want to filter and distinguish dummy devices based on IP when filtering your asset lists.
|
||||
|
||||
|
||||
## Copying data from an existing device.
|
||||
|
||||
|
||||
@@ -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:
|
||||
|
||||

|
||||

|
||||
|
||||
|
||||
## Tips
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[](https://hub.docker.com/r/jokobsk/netalertx)
|
||||
[](https://hub.docker.com/r/jokobsk/netalertx)
|
||||
[](https://github.com/jokob-sk/NetAlertX/releases)
|
||||
[](https://github.com/netalertx/NetAlertX/releases)
|
||||
[](https://discord.gg/NczTUTWyRr)
|
||||
[](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
|
||||
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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).
|
||||
|
||||
|
||||
@@ -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 [](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
|
||||
|
||||
|
||||
@@ -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`.
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -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.
|
||||
>
|
||||
|
||||
|
||||
@@ -215,7 +215,7 @@ services:
|
||||
|
||||
### 1.3 Migration from NetAlertX `v25.10.1`
|
||||
|
||||
Starting from v25.10.1, the container uses a [more secure, read-only runtime environment](./SECURITY_FEATURES.md), which requires all writable paths (e.g., logs, API cache, temporary data) to be mounted as `tmpfs` or permanent writable volumes, with sufficient access [permissions](./FILE_PERMISSIONS.md). The data location has also hanged from `/app/db` and `/app/config` to `/data/db` and `/data/config`. See detailed steps below.
|
||||
Starting from `v25.10.1`, the container uses a [more secure, read-only runtime environment](./SECURITY_FEATURES.md), which requires all writable paths (e.g., logs, API cache, temporary data) to be mounted as `tmpfs` or permanent writable volumes, with sufficient access [permissions](./FILE_PERMISSIONS.md). The data location has also hanged from `/app/db` and `/app/config` to `/data/db` and `/data/config`. See detailed steps below.
|
||||
|
||||
#### STEPS:
|
||||
|
||||
@@ -248,7 +248,7 @@ services:
|
||||
services:
|
||||
netalertx:
|
||||
container_name: netalertx
|
||||
image: "ghcr.io/jokob-sk/netalertx" # 🆕 This has changed
|
||||
image: "ghcr.io/jokob-sk/netalertx:25.11.29" # 🆕 This has changed
|
||||
network_mode: "host"
|
||||
cap_drop: # 🆕 New line
|
||||
- ALL # 🆕 New line
|
||||
|
||||
@@ -19,22 +19,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 🔌
|
||||
|
||||

|
||||
|
||||
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 `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.
|
||||
|
||||
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 +43,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 +56,9 @@ You can filter out unwanted notifications globally. This could be because of a m
|
||||
|
||||

|
||||
|
||||
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.
|
||||
|
||||
|
||||
|
||||
@@ -48,6 +48,36 @@ Two plugins help maintain the system’s 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 |
|
||||
|---------|--------|----------|
|
||||
| **10–20 MB** | Smaller storage footprint; more frequent disk operations | NAS with SD card (storage priority) |
|
||||
| **50 MB** (default) | Balanced; recommended for most setups | General use |
|
||||
| **75–100 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.
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
110
docs/PLUGINS.md
@@ -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).
|
||||
> 
|
||||
|
||||
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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -63,7 +63,7 @@ There is also an in-app Help / FAQ section that should be answering frequently a
|
||||
|
||||
#### ♻ Misc
|
||||
|
||||
- [Reverse proxy (Nginx, Apache, SWAG)](./REVERSE_PROXY.md)
|
||||
- [Reverse Proxy](./REVERSE_PROXY.md)
|
||||
- [Installing Updates](./UPDATES.md)
|
||||
- [Setting up Authelia](./AUTHELIA.md) (DRAFT)
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -43,18 +43,18 @@ 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.
|
||||
|
||||
To create truly dummy devices, you can use a loopback IP address (e.g., `0.0.0.0` or `127.0.0.1`) so they appear online.
|
||||
To create truly dummy devices, you can use a loopback IP address (e.g., `0.0.0.0` or `127.0.0.1`) or the `Force Status` field so they appear online.
|
||||
|
||||
## NMAP and Fake MAC Addresses
|
||||
|
||||
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
|
||||
|
||||
577
docs/REVERSE_PROXY.md
Executable file → Normal file
@@ -1,526 +1,135 @@
|
||||
# Reverse Proxy Configuration
|
||||
|
||||
> [!NOTE]
|
||||
> This is community-contributed. Due to environment, setup, or networking differences, results may vary. Please open a PR to improve it instead of creating an issue, as the maintainer is not actively maintaining it.
|
||||
A reverse proxy is a server that sits between users and your NetAlertX instance. It allows you to:
|
||||
- Access NetAlertX via a domain name (e.g., `https://netalertx.example.com`).
|
||||
- Add HTTPS/SSL encryption.
|
||||
- Enforce authentication (like SSO).
|
||||
|
||||
> [!NOTE]
|
||||
> NetAlertX requires access to both the **web UI** (default `20211`) and the **GraphQL backend `GRAPHQL_PORT`** (default `20212`) ports.
|
||||
> Ensure your reverse proxy allows traffic to both for proper functionality.
|
||||
|
||||
> [!IMPORTANT]
|
||||
> You will need to specify 2 entries in your reverse proxy, one for the front end, one for the backend URL. The custom backend URL, including the `GRAPHQL_PORT`, needs to be aslo specified in the `BACKEND_API_URL` setting.This is the URL that points to the backend API server.
|
||||
>
|
||||
> 
|
||||
>
|
||||
> 
|
||||
|
||||
See also:
|
||||
|
||||
- [CADDY + AUTHENTIK](./REVERSE_PROXY_CADDY.md)
|
||||
- [TRAEFIK](./REVERSE_PROXY_TRAEFIK.md)
|
||||
|
||||
|
||||
## NGINX HTTP Configuration (Direct Path)
|
||||
|
||||
> Submitted by amazing [cvc90](https://github.com/cvc90) 🙏
|
||||
|
||||
> [!NOTE]
|
||||
> There are various NGINX config files for NetAlertX, some for the bare-metal install, currently Debian 12 and Ubuntu 24 (`netalertx.conf`), and one for the docker container (`netalertx.template.conf`).
|
||||
>
|
||||
> The first one you can find in the respective bare metal installer folder `/app/install/\<system\>/netalertx.conf`.
|
||||
> The docker one can be found in the [install](https://github.com/jokob-sk/NetAlertX/tree/main/install) folder. Map, or use, the one appropriate for your setup.
|
||||
|
||||
<br/>
|
||||
|
||||
1. On your NGINX server, create a new file called /etc/nginx/sites-available/netalertx
|
||||
|
||||
2. In this file, paste the following code:
|
||||
|
||||
```
|
||||
server {
|
||||
listen 80;
|
||||
server_name netalertx;
|
||||
proxy_preserve_host on;
|
||||
proxy_pass http://localhost:20211/;
|
||||
proxy_pass_reverse http://localhost:20211/;
|
||||
}
|
||||
```mermaid
|
||||
flowchart LR
|
||||
Browser --HTTPS--> Proxy[Reverse Proxy] --HTTP--> Container[NetAlertX Container]
|
||||
```
|
||||
|
||||
3. Activate the new website by running the following command:
|
||||
## NetAlertX Ports
|
||||
|
||||
`nginx -s reload` or `systemctl restart nginx`
|
||||
NetAlertX exposes two ports that serve different purposes. Your reverse proxy can target one or both, depending on your needs.
|
||||
|
||||
4. Check your config with `nginx -t`. If there are any issues, it will tell you.
|
||||
| Port | Service | Purpose |
|
||||
|------|---------|---------|
|
||||
| **20211** | Nginx (Web UI) | The main interface. |
|
||||
| **20212** | Backend API | Direct access to the API and GraphQL. Includes API docs you can view with a browser. |
|
||||
|
||||
5. Once NGINX restarts, you should be able to access the proxy website at http://netalertx/
|
||||
> [!WARNING]
|
||||
> **Do not document or use `/server` as an external API endpoint.** It is an internal route used by the Nginx frontend to communicate with the backend.
|
||||
|
||||
<br/>
|
||||
## Connection Patterns
|
||||
|
||||
## NGINX HTTP Configuration (Sub Path)
|
||||
### 1. Default (No Proxy)
|
||||
For local testing or LAN access. The browser accesses the UI on port 20211. Code and API docs are accessible on 20212.
|
||||
|
||||
1. On your NGINX server, create a new file called /etc/nginx/sites-available/netalertx
|
||||
|
||||
2. In this file, paste the following code:
|
||||
|
||||
```
|
||||
server {
|
||||
listen 80;
|
||||
server_name netalertx;
|
||||
proxy_preserve_host on;
|
||||
location ^~ /netalertx/ {
|
||||
proxy_pass http://localhost:20211/;
|
||||
proxy_pass_reverse http://localhost:20211/;
|
||||
proxy_redirect ~^/(.*)$ /netalertx/$1;
|
||||
rewrite ^/netalertx/?(.*)$ /$1 break;
|
||||
}
|
||||
}
|
||||
```mermaid
|
||||
flowchart LR
|
||||
B[Browser]
|
||||
subgraph NAC[NetAlertX Container]
|
||||
N[Nginx listening on port 20211]
|
||||
A[Service on port 20212]
|
||||
N -->|Proxy /server to localhost:20212| A
|
||||
end
|
||||
B -->|port 20211| NAC
|
||||
B -->|port 20212| NAC
|
||||
```
|
||||
|
||||
3. Check your config with `nginx -t`. If there are any issues, it will tell you.
|
||||
### 2. Direct API Consumer (Not Recommended)
|
||||
Connecting directly to the backend API port (20212).
|
||||
|
||||
4. Activate the new website by running the following command:
|
||||
> [!CAUTION]
|
||||
> This exposes the API directly to the network without additional protection. Avoid this on untrusted networks.
|
||||
|
||||
`nginx -s reload` or `systemctl restart nginx`
|
||||
|
||||
5. Once NGINX restarts, you should be able to access the proxy website at http://netalertx/netalertx/
|
||||
|
||||
<br/>
|
||||
|
||||
## NGINX HTTP Configuration (Sub Path) with module ngx_http_sub_module
|
||||
|
||||
1. On your NGINX server, create a new file called /etc/nginx/sites-available/netalertx
|
||||
|
||||
2. In this file, paste the following code:
|
||||
|
||||
```
|
||||
server {
|
||||
listen 80;
|
||||
server_name netalertx;
|
||||
proxy_preserve_host on;
|
||||
location ^~ /netalertx/ {
|
||||
proxy_pass http://localhost:20211/;
|
||||
proxy_pass_reverse http://localhost:20211/;
|
||||
proxy_redirect ~^/(.*)$ /netalertx/$1;
|
||||
rewrite ^/netalertx/?(.*)$ /$1 break;
|
||||
sub_filter_once off;
|
||||
sub_filter_types *;
|
||||
sub_filter 'href="/' 'href="/netalertx/';
|
||||
sub_filter '(?>$host)/css' '/netalertx/css';
|
||||
sub_filter '(?>$host)/js' '/netalertx/js';
|
||||
sub_filter '/img' '/netalertx/img';
|
||||
sub_filter '/lib' '/netalertx/lib';
|
||||
sub_filter '/php' '/netalertx/php';
|
||||
}
|
||||
}
|
||||
```mermaid
|
||||
flowchart LR
|
||||
B[Browser] -->|HTTPS| S[Any API Consumer app]
|
||||
subgraph NAC[NetAlertX Container]
|
||||
N[Nginx listening on port 20211]
|
||||
N -->|Proxy /server to localhost:20212| A[Service on port 20212]
|
||||
end
|
||||
S -->|Port 20212| NAC
|
||||
```
|
||||
|
||||
3. Check your config with `nginx -t`. If there are any issues, it will tell you.
|
||||
### 3. Recommended: Reverse Proxy to Web UI
|
||||
Using a reverse proxy (Nginx, Traefik, Caddy, etc.) to handle HTTPS and Auth in front of the main UI.
|
||||
|
||||
4. Activate the new website by running the following command:
|
||||
|
||||
`nginx -s reload` or `systemctl restart nginx`
|
||||
|
||||
5. Once NGINX restarts, you should be able to access the proxy website at http://netalertx/netalertx/
|
||||
|
||||
<br/>
|
||||
|
||||
**NGINX HTTPS Configuration (Direct Path)**
|
||||
|
||||
1. On your NGINX server, create a new file called /etc/nginx/sites-available/netalertx
|
||||
|
||||
2. In this file, paste the following code:
|
||||
|
||||
```
|
||||
server {
|
||||
listen 443;
|
||||
server_name netalertx;
|
||||
SSLEngine On;
|
||||
SSLCertificateFile /etc/ssl/certs/netalertx.pem;
|
||||
SSLCertificateKeyFile /etc/ssl/private/netalertx.key;
|
||||
proxy_preserve_host on;
|
||||
proxy_pass http://localhost:20211/;
|
||||
proxy_pass_reverse http://localhost:20211/;
|
||||
}
|
||||
```mermaid
|
||||
flowchart LR
|
||||
B[Browser] -->|HTTPS| S[Any Auth/SSL proxy]
|
||||
subgraph NAC[NetAlertX Container]
|
||||
N[Nginx listening on port 20211]
|
||||
N -->|Proxy /server to localhost:20212| A[Service on port 20212]
|
||||
end
|
||||
S -->|port 20211| NAC
|
||||
```
|
||||
|
||||
3. Check your config with `nginx -t`. If there are any issues, it will tell you.
|
||||
### 4. Recommended: Proxied API Consumer
|
||||
Using a proxy to secure API access with TLS or IP limiting.
|
||||
|
||||
4. Activate the new website by running the following command:
|
||||
**Why is this important?**
|
||||
The backend API (`:20212`) is powerful—more so than the Web UI, which is a safer, password-protectable interface. By using a reverse proxy to **limit sources** (e.g., allowing only your Home Assistant server's IP), you ensure that only trusted devices can talk to your backend.
|
||||
|
||||
`nginx -s reload` or `systemctl restart nginx`
|
||||
|
||||
5. Once NGINX restarts, you should be able to access the proxy website at https://netalertx/
|
||||
|
||||
<br/>
|
||||
|
||||
**NGINX HTTPS Configuration (Sub Path)**
|
||||
|
||||
1. On your NGINX server, create a new file called /etc/nginx/sites-available/netalertx
|
||||
|
||||
2. In this file, paste the following code:
|
||||
|
||||
```
|
||||
server {
|
||||
listen 443;
|
||||
server_name netalertx;
|
||||
SSLEngine On;
|
||||
SSLCertificateFile /etc/ssl/certs/netalertx.pem;
|
||||
SSLCertificateKeyFile /etc/ssl/private/netalertx.key;
|
||||
location ^~ /netalertx/ {
|
||||
proxy_pass http://localhost:20211/;
|
||||
proxy_pass_reverse http://localhost:20211/;
|
||||
proxy_redirect ~^/(.*)$ /netalertx/$1;
|
||||
rewrite ^/netalertx/?(.*)$ /$1 break;
|
||||
}
|
||||
}
|
||||
```mermaid
|
||||
flowchart LR
|
||||
B[Browser] -->|HTTPS| S[Any API Consumer app]
|
||||
C[HTTPS/source-limiting Proxy]
|
||||
subgraph NAC[NetAlertX Container]
|
||||
N[Nginx listening on port 20211]
|
||||
N -->|Proxy /server to localhost:20212| A[Service on port 20212]
|
||||
end
|
||||
S -->|HTTPS| C
|
||||
C -->|Port 20212| NAC
|
||||
```
|
||||
|
||||
3. Check your config with `nginx -t`. If there are any issues, it will tell you.
|
||||
## Getting Started: Nginx Proxy Manager
|
||||
|
||||
4. Activate the new website by running the following command:
|
||||
For beginners, we recommend **[Nginx Proxy Manager](https://nginxproxymanager.com/)**. It provides a user-friendly interface to manage proxy hosts and free SSL certificates via Let's Encrypt.
|
||||
|
||||
`nginx -s reload` or `systemctl restart nginx`
|
||||
1. Install Nginx Proxy Manager alongside NetAlertX.
|
||||
2. Create a **Proxy Host** pointing to your NetAlertX IP and Port `20211` for the Web UI.
|
||||
3. (Optional) Create a second host for the API on Port `20212`.
|
||||
|
||||
5. Once NGINX restarts, you should be able to access the proxy website at https://netalertx/netalertx/
|
||||

|
||||
|
||||
<br/>
|
||||
### Configuration Settings
|
||||
|
||||
## NGINX HTTPS Configuration (Sub Path) with module ngx_http_sub_module
|
||||
When using a reverse proxy, you should verify two settings in **Settings > Core > General**:
|
||||
|
||||
1. On your NGINX server, create a new file called /etc/nginx/sites-available/netalertx
|
||||
1. **BACKEND_API_URL**: This should be set to `/server`.
|
||||
* *Reason:* The frontend should communicate with the backend via the internal Nginx proxy rather than routing out to the internet and back.
|
||||
|
||||
2. In this file, paste the following code:
|
||||
2. **REPORT_DASHBOARD_URL**: Set this to your external proxy URL (e.g., `https://netalertx.example.com`).
|
||||
* *Reason:* This URL is used to generate proper clickable links in emails and HTML reports.
|
||||
|
||||
```
|
||||
server {
|
||||
listen 443;
|
||||
server_name netalertx;
|
||||
SSLEngine On;
|
||||
SSLCertificateFile /etc/ssl/certs/netalertx.pem;
|
||||
SSLCertificateKeyFile /etc/ssl/private/netalertx.key;
|
||||
location ^~ /netalertx/ {
|
||||
proxy_pass http://localhost:20211/;
|
||||
proxy_pass_reverse http://localhost:20211/;
|
||||
proxy_redirect ~^/(.*)$ /netalertx/$1;
|
||||
rewrite ^/netalertx/?(.*)$ /$1 break;
|
||||
sub_filter_once off;
|
||||
sub_filter_types *;
|
||||
sub_filter 'href="/' 'href="/netalertx/';
|
||||
sub_filter '(?>$host)/css' '/netalertx/css';
|
||||
sub_filter '(?>$host)/js' '/netalertx/js';
|
||||
sub_filter '/img' '/netalertx/img';
|
||||
sub_filter '/lib' '/netalertx/lib';
|
||||
sub_filter '/php' '/netalertx/php';
|
||||
}
|
||||
}
|
||||
```
|
||||

|
||||
|
||||
3. Check your config with `nginx -t`. If there are any issues, it will tell you.
|
||||
## Other Reverse Proxies
|
||||
|
||||
4. Activate the new website by running the following command:
|
||||
NetAlertX uses standard HTTP. Any reverse proxy will work. Simply forward traffic to the appropriate port (`20211` or `20212`).
|
||||
|
||||
`nginx -s reload` or `systemctl restart nginx`
|
||||
For configuration details, consult the documentation for your preferred proxy:
|
||||
|
||||
5. Once NGINX restarts, you should be able to access the proxy website at https://netalertx/netalertx/
|
||||
* **[NGINX](https://nginx.org/en/docs/http/ngx_http_proxy_module.html)**
|
||||
* **[Apache (mod_proxy)](https://httpd.apache.org/docs/current/mod/mod_proxy.html)**
|
||||
* **[Caddy](https://caddyserver.com/docs/caddyfile/directives/reverse_proxy)**
|
||||
* **[Traefik](https://doc.traefik.io/traefik/routing/services/)**
|
||||
|
||||
<br/>
|
||||
## Authentication
|
||||
|
||||
## Apache HTTP Configuration (Direct Path)
|
||||
If you wish to add Single Sign-On (SSO) or other authentication in front of NetAlertX, refer to the documentation for your identity provider:
|
||||
|
||||
1. On your Apache server, create a new file called /etc/apache2/sites-available/netalertx.conf.
|
||||
* **[Authentik](https://docs.goauthentik.io/)**
|
||||
* **[Authelia](https://www.authelia.com/docs/)**
|
||||
|
||||
2. In this file, paste the following code:
|
||||
|
||||
```
|
||||
<VirtualHost *:80>
|
||||
ServerName netalertx
|
||||
ProxyPreserveHost On
|
||||
ProxyPass / http://localhost:20211/
|
||||
ProxyPassReverse / http://localhost:20211/
|
||||
</VirtualHost>
|
||||
```
|
||||
|
||||
3. Check your config with `httpd -t` (or `apache2ctl -t` on Debian/Ubuntu). If there are any issues, it will tell you.
|
||||
|
||||
4. Activate the new website by running the following command:
|
||||
|
||||
`a2ensite netalertx` or `service apache2 reload`
|
||||
|
||||
5. Once Apache restarts, you should be able to access the proxy website at http://netalertx/
|
||||
|
||||
<br/>
|
||||
|
||||
## Apache HTTP Configuration (Sub Path)
|
||||
|
||||
1. On your Apache server, create a new file called /etc/apache2/sites-available/netalertx.conf.
|
||||
|
||||
2. In this file, paste the following code:
|
||||
|
||||
```
|
||||
<VirtualHost *:80>
|
||||
ServerName netalertx
|
||||
location ^~ /netalertx/ {
|
||||
ProxyPreserveHost On
|
||||
ProxyPass / http://localhost:20211/
|
||||
ProxyPassReverse / http://localhost:20211/
|
||||
}
|
||||
</VirtualHost>
|
||||
```
|
||||
|
||||
3. Check your config with `httpd -t` (or `apache2ctl -t` on Debian/Ubuntu). If there are any issues, it will tell you.
|
||||
|
||||
4. Activate the new website by running the following command:
|
||||
|
||||
`a2ensite netalertx` or `service apache2 reload`
|
||||
|
||||
5. Once Apache restarts, you should be able to access the proxy website at http://netalertx/
|
||||
|
||||
<br/>
|
||||
|
||||
## Apache HTTPS Configuration (Direct Path)
|
||||
|
||||
1. On your Apache server, create a new file called /etc/apache2/sites-available/netalertx.conf.
|
||||
|
||||
2. In this file, paste the following code:
|
||||
|
||||
```
|
||||
<VirtualHost *:443>
|
||||
ServerName netalertx
|
||||
SSLEngine On
|
||||
SSLCertificateFile /etc/ssl/certs/netalertx.pem
|
||||
SSLCertificateKeyFile /etc/ssl/private/netalertx.key
|
||||
ProxyPreserveHost On
|
||||
ProxyPass / http://localhost:20211/
|
||||
ProxyPassReverse / http://localhost:20211/
|
||||
</VirtualHost>
|
||||
```
|
||||
|
||||
3. Check your config with `httpd -t` (or `apache2ctl -t` on Debian/Ubuntu). If there are any issues, it will tell you.
|
||||
|
||||
4. Activate the new website by running the following command:
|
||||
|
||||
`a2ensite netalertx` or `service apache2 reload`
|
||||
|
||||
5. Once Apache restarts, you should be able to access the proxy website at https://netalertx/
|
||||
|
||||
<br/>
|
||||
|
||||
## Apache HTTPS Configuration (Sub Path)
|
||||
|
||||
1. On your Apache server, create a new file called /etc/apache2/sites-available/netalertx.conf.
|
||||
|
||||
2. In this file, paste the following code:
|
||||
|
||||
```
|
||||
<VirtualHost *:443>
|
||||
ServerName netalertx
|
||||
SSLEngine On
|
||||
SSLCertificateFile /etc/ssl/certs/netalertx.pem
|
||||
SSLCertificateKeyFile /etc/ssl/private/netalertx.key
|
||||
location ^~ /netalertx/ {
|
||||
ProxyPreserveHost On
|
||||
ProxyPass / http://localhost:20211/
|
||||
ProxyPassReverse / http://localhost:20211/
|
||||
}
|
||||
</VirtualHost>
|
||||
```
|
||||
|
||||
3. Check your config with `httpd -t` (or `apache2ctl -t` on Debian/Ubuntu). If there are any issues, it will tell you.
|
||||
|
||||
4. Activate the new website by running the following command:
|
||||
|
||||
`a2ensite netalertx` or `service apache2 reload`
|
||||
|
||||
5. Once Apache restarts, you should be able to access the proxy website at https://netalertx/netalertx/
|
||||
|
||||
<br/>
|
||||
|
||||
## Reverse proxy example by using LinuxServer's SWAG container.
|
||||
|
||||
> Submitted by [s33d1ing](https://github.com/s33d1ing). 🙏
|
||||
|
||||
## [linuxserver/swag](https://github.com/linuxserver/docker-swag)
|
||||
|
||||
In the SWAG container create `/config/nginx/proxy-confs/netalertx.subfolder.conf` with the following contents:
|
||||
|
||||
``` nginx
|
||||
## Version 2023/02/05
|
||||
# make sure that your netalertx container is named netalertx
|
||||
# netalertx does not require a base url setting
|
||||
|
||||
# Since NetAlertX uses a Host network, you may need to use the IP address of the system running NetAlertX for $upstream_app.
|
||||
|
||||
location /netalertx {
|
||||
return 301 $scheme://$host/netalertx/;
|
||||
}
|
||||
|
||||
location ^~ /netalertx/ {
|
||||
# enable the next two lines for http auth
|
||||
#auth_basic "Restricted";
|
||||
#auth_basic_user_file /config/nginx/.htpasswd;
|
||||
|
||||
# enable for ldap auth (requires ldap-server.conf in the server block)
|
||||
#include /config/nginx/ldap-location.conf;
|
||||
|
||||
# enable for Authelia (requires authelia-server.conf in the server block)
|
||||
#include /config/nginx/authelia-location.conf;
|
||||
|
||||
# enable for Authentik (requires authentik-server.conf in the server block)
|
||||
#include /config/nginx/authentik-location.conf;
|
||||
|
||||
include /config/nginx/proxy.conf;
|
||||
include /config/nginx/resolver.conf;
|
||||
|
||||
set $upstream_app netalertx;
|
||||
set $upstream_port 20211;
|
||||
set $upstream_proto http;
|
||||
|
||||
proxy_pass $upstream_proto://$upstream_app:$upstream_port;
|
||||
proxy_set_header Accept-Encoding "";
|
||||
|
||||
proxy_redirect ~^/(.*)$ /netalertx/$1;
|
||||
rewrite ^/netalertx/?(.*)$ /$1 break;
|
||||
|
||||
sub_filter_once off;
|
||||
sub_filter_types *;
|
||||
|
||||
sub_filter 'href="/' 'href="/netalertx/';
|
||||
|
||||
sub_filter '(?>$host)/css' '/netalertx/css';
|
||||
sub_filter '(?>$host)/js' '/netalertx/js';
|
||||
|
||||
sub_filter '/img' '/netalertx/img';
|
||||
sub_filter '/lib' '/netalertx/lib';
|
||||
sub_filter '/php' '/netalertx/php';
|
||||
}
|
||||
```
|
||||
|
||||
<br/>
|
||||
|
||||
## Traefik
|
||||
|
||||
> Submitted by [Isegrimm](https://github.com/Isegrimm) 🙏 (based on this [discussion](https://github.com/jokob-sk/NetAlertX/discussions/449#discussioncomment-7281442))
|
||||
|
||||
Assuming the user already has a working Traefik setup, this is what's needed to make NetAlertX work at a URL like www.domain.com/netalertx/.
|
||||
|
||||
Note: Everything in these configs assumes '**www.domain.com**' as your domainname and '**section31**' as an arbitrary name for your certificate setup. You will have to substitute these with your own.
|
||||
|
||||
Also, I use the prefix '**netalertx**'. If you want to use another prefix, change it in these files: dynamic.toml and default.
|
||||
|
||||
Content of my yaml-file (this is the generic Traefik config, which defines which ports to listen on, redirect http to https and sets up the certificate process).
|
||||
It also contains Authelia, which I use for authentication.
|
||||
This part contains nothing specific to NetAlertX.
|
||||
|
||||
```yaml
|
||||
version: '3.8'
|
||||
|
||||
services:
|
||||
traefik:
|
||||
image: traefik
|
||||
container_name: traefik
|
||||
command:
|
||||
- "--api=true"
|
||||
- "--api.insecure=true"
|
||||
- "--api.dashboard=true"
|
||||
- "--entrypoints.web.address=:80"
|
||||
- "--entrypoints.web.http.redirections.entryPoint.to=websecure"
|
||||
- "--entrypoints.web.http.redirections.entryPoint.scheme=https"
|
||||
- "--entrypoints.websecure.address=:443"
|
||||
- "--providers.file.filename=/traefik-config/dynamic.toml"
|
||||
- "--providers.file.watch=true"
|
||||
- "--log.level=ERROR"
|
||||
- "--certificatesresolvers.section31.acme.email=postmaster@domain.com"
|
||||
- "--certificatesresolvers.section31.acme.storage=/traefik-config/acme.json"
|
||||
- "--certificatesresolvers.section31.acme.httpchallenge=true"
|
||||
- "--certificatesresolvers.section31.acme.httpchallenge.entrypoint=web"
|
||||
ports:
|
||||
- "80:80"
|
||||
- "443:443"
|
||||
- "8080:8080"
|
||||
volumes:
|
||||
- "/var/run/docker.sock:/var/run/docker.sock:ro"
|
||||
- /appl/docker/traefik/config:/traefik-config
|
||||
depends_on:
|
||||
- authelia
|
||||
restart: unless-stopped
|
||||
authelia:
|
||||
container_name: authelia
|
||||
image: authelia/authelia:latest
|
||||
ports:
|
||||
- "9091:9091"
|
||||
volumes:
|
||||
- /appl/docker/authelia:/config
|
||||
restart: u
|
||||
nless-stopped
|
||||
```
|
||||
Snippet of the dynamic.toml file (referenced in the yml-file above) that defines the config for NetAlertX:
|
||||
The following are self-defined keywords, everything else is traefik keywords:
|
||||
- netalertx-router
|
||||
- netalertx-service
|
||||
- auth
|
||||
- netalertx-stripprefix
|
||||
|
||||
|
||||
```toml
|
||||
[http.routers]
|
||||
[http.routers.netalertx-router]
|
||||
entryPoints = ["websecure"]
|
||||
rule = "Host(`www.domain.com`) && PathPrefix(`/netalertx`)"
|
||||
service = "netalertx-service"
|
||||
middlewares = "auth,netalertx-stripprefix"
|
||||
[http.routers.netalertx-router.tls]
|
||||
certResolver = "section31"
|
||||
[[http.routers.netalertx-router.tls.domains]]
|
||||
main = "www.domain.com"
|
||||
|
||||
[http.services]
|
||||
[http.services.netalertx-service]
|
||||
[[http.services.netalertx-service.loadBalancer.servers]]
|
||||
url = "http://internal-ip-address:20211/"
|
||||
|
||||
[http.middlewares]
|
||||
[http.middlewares.auth.forwardAuth]
|
||||
address = "http://authelia:9091/api/verify?rd=https://www.domain.com/authelia/"
|
||||
trustForwardHeader = true
|
||||
authResponseHeaders = ["Remote-User", "Remote-Groups", "Remote-Name", "Remote-Email"]
|
||||
[http.middlewares.netalertx-stripprefix.stripprefix]
|
||||
prefixes = "/netalertx"
|
||||
forceSlash = false
|
||||
|
||||
```
|
||||
To make NetAlertX work with this setup I modified the default file at `/etc/nginx/sites-available/default` in the docker container by copying it to my local filesystem, adding the changes as specified by [cvc90](https://github.com/cvc90) and mounting the new file into the docker container, overwriting the original one. By mapping the file instead of changing the file in-place, the changes persist if an updated dockerimage is pulled. This is also a downside when the default file is updated, so I only use this as a temporary solution, until the dockerimage is updated with this change.
|
||||
|
||||
Default-file:
|
||||
|
||||
```
|
||||
server {
|
||||
listen 80 default_server;
|
||||
root /var/www/html;
|
||||
index index.php;
|
||||
#rewrite /netalertx/(.*) / permanent;
|
||||
add_header X-Forwarded-Prefix "/netalertx" always;
|
||||
proxy_set_header X-Forwarded-Prefix "/netalertx";
|
||||
|
||||
location ~* \.php$ {
|
||||
fastcgi_pass unix:/run/php/php8.2-fpm.sock;
|
||||
include fastcgi_params;
|
||||
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
|
||||
fastcgi_param SCRIPT_NAME $fastcgi_script_name;
|
||||
fastcgi_connect_timeout 75;
|
||||
fastcgi_send_timeout 600;
|
||||
fastcgi_read_timeout 600;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Mapping the updated file (on the local filesystem at `/appl/docker/netalertx/default`) into the docker container:
|
||||
|
||||
|
||||
```yaml
|
||||
...
|
||||
volumes:
|
||||
- /appl/docker/netalertx/default:/etc/nginx/sites-available/default
|
||||
...
|
||||
```
|
||||
## Further Reading
|
||||
|
||||
If you want to understand more about reverse proxies and networking concepts:
|
||||
|
||||
* [What is a Reverse Proxy? (Cloudflare)](https://www.cloudflare.com/learning/cdn/glossary/reverse-proxy/)
|
||||
* [Proxy vs Reverse Proxy (StrongDM)](https://www.strongdm.com/blog/difference-between-proxy-and-reverse-proxy)
|
||||
* [Nginx Reverse Proxy Glossary](https://www.nginx.com/resources/glossary/reverse-proxy-server/)
|
||||
|
||||
@@ -1,892 +0,0 @@
|
||||
## Caddy + Authentik Outpost Proxy SSO
|
||||
> Submitted by [luckylinux](https://github.com/luckylinux) 🙏.
|
||||
|
||||
> [!NOTE]
|
||||
> This is community-contributed. Due to environment, setup, or networking differences, results may vary. Please open a PR to improve it instead of creating an issue, as the maintainer is not actively maintaining it.
|
||||
|
||||
> [!NOTE]
|
||||
> NetAlertX requires access to both the **web UI** (default `20211`) and the **GraphQL backend `GRAPHQL_PORT`** (default `20212`) ports.
|
||||
> Ensure your reverse proxy allows traffic to both for proper functionality.
|
||||
|
||||
### Introduction
|
||||
|
||||
This Setup assumes:
|
||||
|
||||
1. Authentik Installation running on a separate Host at `https://authentik.MYDOMAIN.TLD`
|
||||
2. Container Management is done on Baremetal OR in a Virtual Machine (KVM/Xen/ESXi/..., no LXC Containers !):
|
||||
i. Docker and Docker Compose configured locally running as Root (needed for `network_mode: host`) OR
|
||||
ii. Podman (optionally `podman-compose`) configured locally running as Root (needed for `network_mode: host`)
|
||||
3. TLS Certificates are already pre-obtained and located at `/var/lib/containers/certificates/letsencrypt/MYDOMAIN.TLD`.
|
||||
I use the `certbot/dns-cloudflare` Podman Container on a separate Host to obtain the Certificates which I then distribute internally.
|
||||
This Container uses the Wildcard Top-Level Domain Certificate which is valid for `MYDOMAIN.TLD` and `*.MYDOMAIN.TLD`.
|
||||
4. Proxied Access
|
||||
i. NetAlertX Web Interface is accessible via Caddy Reverse Proxy at `https://netalertx.MYDOMAIN.TLD` (default HTTPS Port 443: `https://netalertx.MYDOMAIN.TLD:443`) with `REPORT_DASHBOARD_URL=https://netalertx.MYDOMAIN.TLD`
|
||||
ii. NetAlertX GraphQL Interface is accessible via Caddy Reverse Proxy at `https://netalertx.MYDOMAIN.TLD:20212` with `BACKEND_API_URL=https://netalertx.MYDOMAIN.TLD:20212`
|
||||
iii. Authentik Proxy Outpost is accessible via Caddy Reverse Proxy at `https://netalertx.MYDOMAIN.TLD:9443`
|
||||
5. Internal Ports
|
||||
i. NGINX Web Server is set to listen on internal Port 20211 set via `PORT=20211`
|
||||
ii. Python Web Server is set to listen on internal Port `GRAPHQL_PORT=20219`
|
||||
iii. Authentik Proxy Outpost is listening on internal Port `AUTHENTIK_LISTEN__HTTP=[::1]:6000` (unencrypted) and Port `AUTHENTIK_LISTEN__HTTPS=[::1]:6443` (encrypted)
|
||||
|
||||
8. Some further Configuration for Caddy is performed in Terms of Logging, SSL Certificates, etc
|
||||
|
||||
It's also possible to [let Caddy automatically request & keep TLS Certificates up-to-date](https://caddyserver.com/docs/automatic-https), although please keep in mind that:
|
||||
|
||||
1. You risk enumerating your LAN. Every Domain/Subdomain for which Caddy requests a TLS Certificate for you will result in that Host to be listed on [List of Letsencrypt Certificates issued](https://crt.sh/).
|
||||
2. You need to either:
|
||||
i. Open Port 80 for external Access ([HTTP challenge](https://caddyserver.com/docs/automatic-https#http-challenge)) in order for Letsencrypt to verify the Ownership of the Domain/Subdomain
|
||||
ii. Open Port 443 for external Access ([TLS-ALPN challenge](https://caddyserver.com/docs/automatic-https#tls-alpn-challenge)) in order for Letsencrypt to verify the Ownership of the Domain/Subdomain
|
||||
iii. Give Caddy the Credentials to update the DNS Records at your DNS Provider ([DNS challenge](https://caddyserver.com/docs/automatic-https#dns-challenge))
|
||||
|
||||
You can also decide to deploy your own Certificates & Certification Authority, either manually with OpenSSL, or by using something like [mkcert](https://github.com/FiloSottile/mkcert).
|
||||
|
||||
In Terms of IP Stack Used:
|
||||
- External: Caddy listens on both IPv4 and IPv6.
|
||||
- Internal:
|
||||
- Authentik Outpost Proxy listens on IPv6 `[::1]`
|
||||
- NetAlertX listens on IPv4 `0.0.0.0`
|
||||
|
||||
### Flow
|
||||
The Traffic Flow will therefore be as follows:
|
||||
|
||||
- Web GUI:
|
||||
i. Client accesses `http://authentik.MYDOMAIN.TLD:80`: default (built-in Caddy) Redirect to `https://authentik.MYDOMAIN.TLD:443`
|
||||
ii. Client accesses `https://authentik.MYDOMAIN.TLD:443` -> reverse Proxy to internal Port 20211 (NetAlertX Web GUI / NGINX - unencrypted)
|
||||
- GraphQL: Client accesses `https://authentik.MYDOMAIN.TLD:20212` -> reverse Proxy to internal Port 20219 (NetAlertX GraphQL - unencrypted)
|
||||
- Authentik Outpost: Client accesses `https://authentik.MYDOMAIN.TLD:9443` -> reverse Proxy to internal Port 6000 (Authentik Outpost Proxy - unencrypted)
|
||||
|
||||
An Overview of the Flow is provided in the Picture below:
|
||||
|
||||

|
||||
|
||||
### Security Considerations
|
||||
|
||||
#### Caddy should be run rootless
|
||||
|
||||
> [!WARNING]
|
||||
> By default Caddy runs as `root` which is a Security Risk.
|
||||
> In order to solve this, it's recommended to create an unprivileged User `caddy` and Group `caddy` on the Host:
|
||||
> ```
|
||||
> groupadd --gid 980 caddy
|
||||
> useradd --shell /usr/sbin/nologin --gid 980 --uid 980 -c "Caddy web server" --base-dir /var/lib/caddy
|
||||
> ```
|
||||
|
||||
At least using Quadlets with Usernames (NOT required with UID/GID), but possibly using Compose in certain Cases as well, a custom `/etc/passwd` and `/etc/group` might need to be bind-mounted inside the Container.
|
||||
`passwd`:
|
||||
```
|
||||
root:x:0:0:root:/root:/bin/sh
|
||||
bin:x:1:1:bin:/bin:/sbin/nologin
|
||||
daemon:x:2:2:daemon:/sbin:/sbin/nologin
|
||||
lp:x:4:7:lp:/var/spool/lpd:/sbin/nologin
|
||||
sync:x:5:0:sync:/sbin:/bin/sync
|
||||
shutdown:x:6:0:shutdown:/sbin:/sbin/shutdown
|
||||
halt:x:7:0:halt:/sbin:/sbin/halt
|
||||
mail:x:8:12:mail:/var/mail:/sbin/nologin
|
||||
news:x:9:13:news:/usr/lib/news:/sbin/nologin
|
||||
uucp:x:10:14:uucp:/var/spool/uucppublic:/sbin/nologin
|
||||
cron:x:16:16:cron:/var/spool/cron:/sbin/nologin
|
||||
ftp:x:21:21::/var/lib/ftp:/sbin/nologin
|
||||
sshd:x:22:22:sshd:/dev/null:/sbin/nologin
|
||||
games:x:35:35:games:/usr/games:/sbin/nologin
|
||||
ntp:x:123:123:NTP:/var/empty:/sbin/nologin
|
||||
guest:x:405:100:guest:/dev/null:/sbin/nologin
|
||||
nobody:x:65534:65534:nobody:/:/sbin/nologin
|
||||
caddy:x:980:980:caddy:/var/lib/caddy:/bin/sh
|
||||
```
|
||||
|
||||
`group`:
|
||||
```
|
||||
root:x:0:root
|
||||
bin:x:1:root,bin,daemon
|
||||
daemon:x:2:root,bin,daemon
|
||||
sys:x:3:root,bin
|
||||
adm:x:4:root,daemon
|
||||
tty:x:5:
|
||||
disk:x:6:root
|
||||
lp:x:7:lp
|
||||
kmem:x:9:
|
||||
wheel:x:10:root
|
||||
floppy:x:11:root
|
||||
mail:x:12:mail
|
||||
news:x:13:news
|
||||
uucp:x:14:uucp
|
||||
cron:x:16:cron
|
||||
audio:x:18:
|
||||
cdrom:x:19:
|
||||
dialout:x:20:root
|
||||
ftp:x:21:
|
||||
sshd:x:22:
|
||||
input:x:23:
|
||||
tape:x:26:root
|
||||
video:x:27:root
|
||||
netdev:x:28:
|
||||
kvm:x:34:kvm
|
||||
games:x:35:
|
||||
shadow:x:42:
|
||||
www-data:x:82:
|
||||
users:x:100:games
|
||||
ntp:x:123:
|
||||
abuild:x:300:
|
||||
utmp:x:406:
|
||||
ping:x:999:
|
||||
nogroup:x:65533:
|
||||
nobody:x:65534:
|
||||
caddy:x:980:
|
||||
```
|
||||
|
||||
#### Authentication of GraphQL Endpoint
|
||||
|
||||
> [!WARNING]
|
||||
> Currently the GraphQL Endpoint is NOT authenticated !
|
||||
|
||||
### Environment Files
|
||||
Depending on the Preference of the User (Environment Variables defined in Compose/Quadlet or in external `.env` File[s]), it might be prefereable to place at least some Environment Variables in external `.env` and `.env.<application>` Files.
|
||||
|
||||
The following is proposed:
|
||||
|
||||
- `.env`: common Settings (empty by Default)
|
||||
- `.env.caddy`: Caddy Settings
|
||||
- `.env.server`: NetAlertX Server/Application Settings
|
||||
- `.env.outpost.proxy`: Authentik Proxy Outpost Settings
|
||||
|
||||
The following Contents is assumed.
|
||||
|
||||
`.env.caddy`:
|
||||
```
|
||||
# Define Application Hostname
|
||||
APPLICATION_HOSTNAME=netalertx.MYDOMAIN.TLD
|
||||
|
||||
# Define Certificate Domain
|
||||
# In this case: use Wildcard Certificate
|
||||
APPLICATION_CERTIFICATE_DOMAIN=MYDOMAIN.TLD
|
||||
APPLICATION_CERTIFICATE_CERT_FILE=fullchain.pem
|
||||
APPLICATION_CERTIFICATE_KEY_FILE=privkey.pem
|
||||
|
||||
# Define Outpost Hostname
|
||||
OUTPOST_HOSTNAME=netalertx.MYDOMAIN.TLD
|
||||
|
||||
# Define Outpost External Port (TLS)
|
||||
OUTPOST_EXTERNAL_PORT=9443
|
||||
```
|
||||
|
||||
`.env.server`:
|
||||
```
|
||||
PORT=20211
|
||||
PORT_SSL=443
|
||||
NETALERTX_NETWORK_MODE=host
|
||||
LISTEN_ADDR=0.0.0.0
|
||||
GRAPHQL_PORT=20219
|
||||
NETALERTX_DEBUG=1
|
||||
BACKEND_API_URL=https://netalertx.MYDOMAIN.TLD:20212
|
||||
```
|
||||
|
||||
`.env.outpost.proxy`:
|
||||
```
|
||||
AUTHENTIK_TOKEN=XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
|
||||
AUTHENTIK_LISTEN__HTTP=[::1]:6000
|
||||
AUTHENTIK_LISTEN__HTTPS=[::1]:6443
|
||||
```
|
||||
|
||||
### Compose Setup
|
||||
```
|
||||
version: "3.8"
|
||||
services:
|
||||
netalertx-caddy:
|
||||
container_name: netalertx-caddy
|
||||
|
||||
network_mode: host
|
||||
image: docker.io/library/caddy:latest
|
||||
pull: missing
|
||||
|
||||
env_file:
|
||||
- .env
|
||||
- .env.caddy
|
||||
|
||||
environment:
|
||||
CADDY_DOCKER_CADDYFILE_PATH: "/etc/caddy/Caddyfile"
|
||||
|
||||
volumes:
|
||||
- ./Caddyfile:/etc/caddy/Caddyfile:ro,z
|
||||
- /var/lib/containers/data/netalertx/caddy:/data/caddy:rw,z
|
||||
- /var/lib/containers/log/netalertx/caddy:/var/log:rw,z
|
||||
- /var/lib/containers/config/netalertx/caddy:/config/caddy:rw,z
|
||||
- /var/lib/containers/certificates/letsencrypt:/certificates:ro,z
|
||||
|
||||
# Set User
|
||||
user: "caddy:caddy"
|
||||
|
||||
# Automatically restart Container
|
||||
restart: unless-stopped
|
||||
|
||||
netalertx-server:
|
||||
container_name: netalertx-server # The name when you docker contiainer ls
|
||||
|
||||
network_mode: host # Use host networking for ARP scanning and other services
|
||||
|
||||
depends_on:
|
||||
netalertx-caddy:
|
||||
condition: service_started
|
||||
restart: true
|
||||
netalertx-outpost-proxy:
|
||||
condition: service_started
|
||||
restart: true
|
||||
|
||||
# Local built Image including latest Changes
|
||||
image: localhost/netalertx-dev:dev-20260109-232454
|
||||
|
||||
read_only: true # Make the container filesystem read-only
|
||||
|
||||
# It is most secure to start with user 20211, but then we lose provisioning capabilities.
|
||||
# user: "${NETALERTX_UID:-20211}:${NETALERTX_GID:-20211}"
|
||||
cap_drop: # Drop all capabilities for enhanced security
|
||||
- ALL
|
||||
cap_add: # Add only the necessary capabilities
|
||||
- NET_ADMIN # Required for scanning with arp-scan, nmap, nbtscan, traceroute, and zero-conf
|
||||
- NET_RAW # Required for raw socket operations with arp-scan, nmap, nbtscan, traceroute and zero-conf
|
||||
- NET_BIND_SERVICE # Required to bind to privileged ports with nbtscan
|
||||
- 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
|
||||
volumes:
|
||||
|
||||
# Override NGINX Configuration Template
|
||||
- type: bind
|
||||
source: /var/lib/containers/config/netalertx/server/nginx/netalertx.conf.template
|
||||
target: /services/config/nginx/netalertx.conf.template
|
||||
read_only: true
|
||||
bind:
|
||||
selinux: Z
|
||||
|
||||
# Letsencrypt Certificates
|
||||
- type: bind
|
||||
source: /var/lib/containers/certificates/letsencrypt/MYDOMAIN.TLD
|
||||
target: /certificates
|
||||
read_only: true
|
||||
bind:
|
||||
selinux: Z
|
||||
|
||||
# Data Storage for NetAlertX
|
||||
- type: bind # Persistent Docker-managed Named Volume for storage
|
||||
source: /var/lib/containers/data/netalertx/server
|
||||
target: /data # consolidated configuration and database storage
|
||||
read_only: false # writable volume
|
||||
bind:
|
||||
selinux: Z
|
||||
|
||||
# Set the Timezone
|
||||
- type: bind # Bind mount for timezone consistency
|
||||
source: /etc/localtime
|
||||
target: /etc/localtime
|
||||
read_only: true
|
||||
bind:
|
||||
selinux: Z
|
||||
|
||||
# tmpfs mounts for writable directories in a read-only container and improve system performance
|
||||
# All writes now live under /tmp/* subdirectories which are created dynamically by entrypoint.d scripts
|
||||
# mode=1700 gives rwx------ permissions; ownership is set by /root-entrypoint.sh
|
||||
- type: tmpfs
|
||||
target: /tmp
|
||||
tmpfs-mode: 1700
|
||||
uid: 0
|
||||
gid: 0
|
||||
rw: true
|
||||
noexec: true
|
||||
nosuid: true
|
||||
nodev: true
|
||||
async: true
|
||||
noatime: true
|
||||
nodiratime: true
|
||||
bind:
|
||||
selinux: Z
|
||||
|
||||
env_file:
|
||||
- .env
|
||||
- .env.server
|
||||
|
||||
environment:
|
||||
PUID: ${NETALERTX_UID:-20211} # Runtime UID after priming (Synology/no-copy-up safe)
|
||||
PGID: ${NETALERTX_GID:-20211} # Runtime GID after priming (Synology/no-copy-up safe)
|
||||
LISTEN_ADDR: ${LISTEN_ADDR:-0.0.0.0} # Listen for connections on all interfaces
|
||||
PORT: ${PORT:-20211} # Application port
|
||||
PORT_SSL: ${PORT_SSL:-443}
|
||||
GRAPHQL_PORT: ${GRAPHQL_PORT:-20212} # GraphQL API port
|
||||
ALWAYS_FRESH_INSTALL: ${ALWAYS_FRESH_INSTALL:-false} # Set to true to reset your config and database on each container start
|
||||
NETALERTX_DEBUG: ${NETALERTX_DEBUG:-0} # 0=kill all services and restart if any dies. 1 keeps running dead services.
|
||||
BACKEND_API_URL: ${BACKEND_API_URL-"https://netalertx.MYDOMAIN.TLD:20212"}
|
||||
|
||||
# Resource limits to prevent resource exhaustion
|
||||
mem_limit: 4096m # Maximum memory usage
|
||||
mem_reservation: 2048m # Soft memory limit
|
||||
cpu_shares: 512 # Relative CPU weight for CPU contention scenarios
|
||||
pids_limit: 512 # Limit the number of processes/threads to prevent fork bombs
|
||||
logging:
|
||||
driver: "json-file" # Use JSON file logging driver
|
||||
options:
|
||||
max-size: "10m" # Rotate log files after they reach 10MB
|
||||
max-file: "3" # Keep a maximum of 3 log files
|
||||
|
||||
# Always restart the container unless explicitly stopped
|
||||
restart: unless-stopped
|
||||
|
||||
# To sign Out, you need to visit
|
||||
# {$OUTPOST_HOSTNAME}:{$OUTPOST_EXTERNAL_PORT}/outpost.goauthentik.io/sign_out
|
||||
netalertx-outpost-proxy:
|
||||
container_name: netalertx-outpost-proxy
|
||||
|
||||
network_mode: host
|
||||
|
||||
depends_on:
|
||||
netalertx-caddy:
|
||||
condition: service_started
|
||||
restart: true
|
||||
|
||||
restart: unless-stopped
|
||||
|
||||
image: ghcr.io/goauthentik/proxy:2025.10
|
||||
pull: missing
|
||||
|
||||
env_file:
|
||||
- .env
|
||||
- .env.outpost.proxy
|
||||
|
||||
environment:
|
||||
AUTHENTIK_HOST: "https://authentik.MYDOMAIN.TLD"
|
||||
AUTHENTIK_INSECURE: false
|
||||
AUTHENTIK_LISTEN__HTTP: "[::1]:6000"
|
||||
AUTHENTIK_LISTEN__HTTPS: "[::1]:6443"
|
||||
```
|
||||
|
||||
### Quadlet Setup
|
||||
`netalertx.pod`:
|
||||
```
|
||||
[Pod]
|
||||
# Name of the Pod
|
||||
PodName=netalertx
|
||||
|
||||
# Network Mode Host is required for ARP to work
|
||||
Network=host
|
||||
|
||||
# Automatically start Pod at Boot Time
|
||||
[Install]
|
||||
WantedBy=default.target
|
||||
```
|
||||
|
||||
`netalertx-caddy.container`:
|
||||
```
|
||||
[Unit]
|
||||
Description=NetAlertX Caddy Container
|
||||
|
||||
[Service]
|
||||
Restart=always
|
||||
|
||||
[Container]
|
||||
ContainerName=netalertx-caddy
|
||||
|
||||
Pod=netalertx.pod
|
||||
StartWithPod=true
|
||||
|
||||
# Generic Environment Configuration
|
||||
EnvironmentFile=.env
|
||||
|
||||
# Caddy Specific Environment Configuration
|
||||
EnvironmentFile=.env.caddy
|
||||
|
||||
Environment=CADDY_DOCKER_CADDYFILE_PATH=/etc/caddy/Caddyfile
|
||||
|
||||
Image=docker.io/library/caddy:latest
|
||||
Pull=missing
|
||||
|
||||
# Run as rootless
|
||||
# Specifying User & Group by Name requires to mount a custom passwd & group File inside the Container
|
||||
# Otherwise an Error like the following will result: netalertx-caddy[593191]: Error: unable to find user caddy: no matching entries in passwd file
|
||||
# User=caddy
|
||||
# Group=caddy
|
||||
# Volume=/var/lib/containers/config/netalertx/caddy-rootless/passwd:/etc/passwd:ro,z
|
||||
# Volume=/var/lib/containers/config/netalertx/caddy-rootless/group:/etc/group:ro,z
|
||||
|
||||
# Run as rootless
|
||||
# Specifying User & Group by UID/GID will NOT require a custom passwd / group File to be bind-mounted inside the Container
|
||||
User=980
|
||||
Group=980
|
||||
|
||||
Volume=./Caddyfile:/etc/caddy/Caddyfile:ro,z
|
||||
Volume=/var/lib/containers/data/netalertx/caddy:/data/caddy:z
|
||||
Volume=/var/lib/containers/log/netalertx/caddy:/var/log:z
|
||||
Volume=/var/lib/containers/config/netalertx/caddy:/config/caddy:z
|
||||
Volume=/var/lib/containers/certificates/letsencrypt:/certificates:ro,z
|
||||
```
|
||||
|
||||
`netalertx-server.container`:
|
||||
```
|
||||
[Unit]
|
||||
Description=NetAlertX Server Container
|
||||
Requires=netalertx-caddy.service netalertx-outpost-proxy.service
|
||||
After=netalertx-caddy.service netalertx-outpost-proxy.service
|
||||
|
||||
[Service]
|
||||
Restart=always
|
||||
|
||||
[Container]
|
||||
ContainerName=netalertx-server
|
||||
|
||||
Pod=netalertx.pod
|
||||
StartWithPod=true
|
||||
|
||||
# Local built Image including latest Changes
|
||||
Image=localhost/netalertx-dev:dev-20260109-232454
|
||||
Pull=missing
|
||||
|
||||
# Make the container filesystem read-only
|
||||
ReadOnly=true
|
||||
|
||||
# Drop all capabilities for enhanced security
|
||||
DropCapability=ALL
|
||||
|
||||
# It is most secure to start with user 20211, but then we lose provisioning capabilities.
|
||||
# User=20211:20211
|
||||
|
||||
# Required for scanning with arp-scan, nmap, nbtscan, traceroute, and zero-conf
|
||||
AddCapability=NET_ADMIN
|
||||
|
||||
# Required for raw socket operations with arp-scan, nmap, nbtscan, traceroute and zero-conf
|
||||
AddCapability=NET_RAW
|
||||
|
||||
# Required to bind to privileged ports with nbtscan
|
||||
AddCapability=NET_BIND_SERVICE
|
||||
|
||||
# Required for root-entrypoint to chown /data + /tmp before dropping privileges
|
||||
AddCapability=CHOWN
|
||||
|
||||
# Required for root-entrypoint to switch to non-root user
|
||||
AddCapability=SETUID
|
||||
|
||||
# Required for root-entrypoint to switch to non-root group
|
||||
AddCapability=SETGID
|
||||
|
||||
# Override the Configuration Template
|
||||
Volume=/var/lib/containers/config/netalertx/server/nginx/netalertx.conf.template:/services/config/nginx/netalertx.conf.template:ro,Z
|
||||
|
||||
# Letsencrypt Certificates
|
||||
Volume=/var/lib/containers/certificates/letsencrypt/MYDOMAIN.TLD:/certificates:ro,Z
|
||||
|
||||
# Data Storage for NetAlertX
|
||||
Volume=/var/lib/containers/data/netalertx/server:/data:rw,Z
|
||||
|
||||
# Set the Timezone
|
||||
Volume=/etc/localtime:/etc/localtime:ro,Z
|
||||
|
||||
# tmpfs mounts for writable directories in a read-only container and improve system performance
|
||||
# All writes now live under /tmp/* subdirectories which are created dynamically by entrypoint.d scripts
|
||||
# mode=1700 gives rwx------ permissions; ownership is set by /root-entrypoint.sh
|
||||
# Mount=type=tmpfs,destination=/tmp,tmpfs-mode=1700,uid=0,gid=0,rw=true,noexec=true,nosuid=true,nodev=true,async=true,noatime=true,nodiratime=true,relabel=private
|
||||
Mount=type=tmpfs,destination=/tmp,tmpfs-mode=1700,rw=true,noexec=true,nosuid=true,nodev=true
|
||||
|
||||
# Environment Configuration
|
||||
EnvironmentFile=.env
|
||||
EnvironmentFile=.env.server
|
||||
|
||||
# Runtime UID after priming (Synology/no-copy-up safe)
|
||||
Environment=PUID=20211
|
||||
|
||||
# Runtime GID after priming (Synology/no-copy-up safe)
|
||||
Environment=PGID=20211
|
||||
|
||||
# Listen for connections on all interfaces (IPv4)
|
||||
Environment=LISTEN_ADDR=0.0.0.0
|
||||
|
||||
# Application port
|
||||
Environment=PORT=20211
|
||||
|
||||
# SSL Port
|
||||
Environment=PORT_SSL=443
|
||||
|
||||
# GraphQL API port
|
||||
Environment=GRAPHQL_PORT=20212
|
||||
|
||||
# Set to true to reset your config and database on each container start
|
||||
Environment=ALWAYS_FRESH_INSTALL=false
|
||||
|
||||
# 0=kill all services and restart if any dies. 1 keeps running dead services.
|
||||
Environment=NETALERTX_DEBUG=0
|
||||
|
||||
# Set the GraphQL URL for external Access (via Caddy Reverse Proxy)
|
||||
Environment=BACKEND_API_URL=https://netalertx-fedora.MYDOMAIN.TLD:20212
|
||||
|
||||
# Resource limits to prevent resource exhaustion
|
||||
# Maximum memory usage
|
||||
Memory=4g
|
||||
|
||||
# Limit the number of processes/threads to prevent fork bombs
|
||||
PidsLimit=512
|
||||
|
||||
# Relative CPU weight for CPU contention scenarios
|
||||
PodmanArgs=--cpus=2
|
||||
PodmanArgs=--cpu-shares=512
|
||||
|
||||
# Soft memory limit
|
||||
PodmanArgs=--memory-reservation=2g
|
||||
|
||||
# !! The following Keys are unfortunately not [yet] supported !!
|
||||
|
||||
# Relative CPU weight for CPU contention scenarios
|
||||
#CpuShares=512
|
||||
|
||||
# Soft memory limit
|
||||
#MemoryReservation=2g
|
||||
```
|
||||
|
||||
`netalertx-outpost-proxy.container`:
|
||||
```
|
||||
[Unit]
|
||||
Description=NetAlertX Authentik Proxy Outpost Container
|
||||
Requires=netalertx-caddy.service
|
||||
After=netalertx-caddy.service
|
||||
|
||||
[Service]
|
||||
Restart=always
|
||||
|
||||
[Container]
|
||||
ContainerName=netalertx-outpost-proxy
|
||||
|
||||
Pod=netalertx.pod
|
||||
StartWithPod=true
|
||||
|
||||
# General Configuration
|
||||
EnvironmentFile=.env
|
||||
|
||||
# Authentik Outpost Proxy Specific Configuration
|
||||
EnvironmentFile=.env.outpost.proxy
|
||||
|
||||
Environment=AUTHENTIK_HOST=https://authentik.MYDOMAIN.TLD
|
||||
Environment=AUTHENTIK_INSECURE=false
|
||||
|
||||
# Overrides Value from .env.outpost.rac
|
||||
# Environment=AUTHENTIK_TOKEN=XXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
|
||||
|
||||
# Optional setting to be used when `authentik_host` for internal communication doesn't match the public URL
|
||||
# Environment=AUTHENTIK_HOST_BROWSER=https://authentik.MYDOMAIN.TLD
|
||||
|
||||
# Container Image
|
||||
Image=ghcr.io/goauthentik/proxy:2025.10
|
||||
Pull=missing
|
||||
|
||||
# Network Configuration
|
||||
Network=container:supermicro-ikvm-pve031-caddy
|
||||
|
||||
# Security Configuration
|
||||
NoNewPrivileges=true
|
||||
```
|
||||
|
||||
### Firewall Setup
|
||||
|
||||
Depending on which GNU/Linux Distribution you are running, it might be required to open up some Firewall Ports in order to be able to access the Endpoints from outside the Host itself.
|
||||
|
||||
This is for instance the Case for Fedora Linux, where I had to open:
|
||||
|
||||
- Port 20212 for external GraphQL Access (both TCP & UDP are open, unsure if UDP is required)
|
||||
- Port 9443 for external Authentik Outpost Proxy Access (both TCP & UDP are open, unsure if UDP is required)
|
||||
|
||||

|
||||
|
||||
### Authentik Setup
|
||||
|
||||
In order to enable Single Sign On (SSO) with Authentik, you will need to create a Provider, an Application and an Outpost.
|
||||
|
||||

|
||||
|
||||
First of all, using the Left Sidebar, navigate to `Applications` → `Providers`, click on `Create` (Blue Button at the Top of the Screen), select `Proxy Provider`, then click `Next`:
|
||||

|
||||
|
||||
Fill in the required Fields:
|
||||
|
||||
- Name: choose a Name for the Provider (e.g. `netalertx`)
|
||||
- Authorization Flow: choose the Authorization Flow. I typically use `default-provider-authorization-implicit-consent (Authorize Application)`. If you select the `default-provider-authorization-explicit-consent (Authorize Application)` you will need to authorize Authentik every Time you want to log in NetAlertX, which can make the Experience less User-friendly
|
||||
- Type: Click on `Forward Auth (single application)`
|
||||
- External Host: set to `https://netalertx.MYDOMAIN.TLD`
|
||||
|
||||
Click `Finish`.
|
||||
|
||||

|
||||
|
||||
Now, using the Left Sidebar, navigate to `Applications` → `Applications`, click on `Create` (Blue Button at the Top of the Screen) and fill in the required Fields:
|
||||
|
||||
- Name: choose a Name for the Application (e.g. `netalertx`)
|
||||
- Slug: choose a Slug for the Application (e.g. `netalertx`)
|
||||
- Group: optionally you can assign this Application to a Group of Applications of your Choosing (for grouping Purposes within Authentik User Interface)
|
||||
- Provider: select the Provider you created the the `Providers` Section previosly (e.g. `netalertx`)
|
||||
|
||||
Then click `Create`.
|
||||
|
||||

|
||||
|
||||
Now, using the Left Sidebar, navigate to `Applications` → `Outposts`, click on `Create` (Blue Button at the Top of the Screen) and fill in the required Fields:
|
||||
|
||||
- Name: choose a Name for the Outpost (e.g. `netalertx`)
|
||||
- Type: `Proxy`
|
||||
- Integration: open the Dropdown and click on `---------`. Make sure it is NOT set to `Local Docker connection` !
|
||||
|
||||
In the `Available Applications` Section, select the Application you created in the Previous Step, then click the right Arrow (approx. located in the Center of the Screen), so that it gets copied in the `Selected Applications` Section.
|
||||
|
||||
Then click `Create`.
|
||||
|
||||

|
||||
|
||||
Wait a few Seconds for the Outpost to be created. Once it appears in the List, click on `Deployment Info` on the Right Side of the relevant Line.
|
||||
|
||||

|
||||
|
||||
Take note of that Token. You will need it for the Authentik Outpost Proxy Container, which will read it as the `AUTHENTIK_TOKEN` Environment Variable.
|
||||
|
||||
### NGINX Configuration inside NetAlertX Container
|
||||
> [!NOTE]
|
||||
> This is something that was implemented based on the previous Content of this Reverse Proxy Document.
|
||||
> Due to some Buffer Warnings/Errors in the Logs as well as some other Issues I was experiencing, I increased a lot the client_body_buffer_size and large_client_header_buffers Parameters, although these might not be required anymore.
|
||||
> Further Testing might be required.
|
||||
|
||||
```
|
||||
# Set number of worker processes automatically based on number of CPU cores.
|
||||
worker_processes auto;
|
||||
|
||||
# Enables the use of JIT for regular expressions to speed-up their processing.
|
||||
pcre_jit on;
|
||||
|
||||
# Configures default error logger.
|
||||
error_log /tmp/log/nginx-error.log warn;
|
||||
|
||||
pid /tmp/run/nginx.pid;
|
||||
|
||||
events {
|
||||
# The maximum number of simultaneous connections that can be opened by
|
||||
# a worker process.
|
||||
worker_connections 1024;
|
||||
}
|
||||
|
||||
http {
|
||||
|
||||
# Mapping of temp paths for various nginx modules.
|
||||
client_body_temp_path /tmp/nginx/client_body;
|
||||
proxy_temp_path /tmp/nginx/proxy;
|
||||
fastcgi_temp_path /tmp/nginx/fastcgi;
|
||||
uwsgi_temp_path /tmp/nginx/uwsgi;
|
||||
scgi_temp_path /tmp/nginx/scgi;
|
||||
|
||||
# Includes mapping of file name extensions to MIME types of responses
|
||||
# and defines the default type.
|
||||
include /services/config/nginx/mime.types;
|
||||
default_type application/octet-stream;
|
||||
|
||||
# Name servers used to resolve names of upstream servers into addresses.
|
||||
# It's also needed when using tcpsocket and udpsocket in Lua modules.
|
||||
#resolver 1.1.1.1 1.0.0.1 [2606:4700:4700::1111] [2606:4700:4700::1001];
|
||||
|
||||
# Don't tell nginx version to the clients. Default is 'on'.
|
||||
server_tokens off;
|
||||
|
||||
# Specifies the maximum accepted body size of a client request, as
|
||||
# indicated by the request header Content-Length. If the stated content
|
||||
# length is greater than this size, then the client receives the HTTP
|
||||
# error code 413. Set to 0 to disable. Default is '1m'.
|
||||
client_max_body_size 1m;
|
||||
|
||||
# Sendfile copies data between one FD and other from within the kernel,
|
||||
# which is more efficient than read() + write(). Default is off.
|
||||
sendfile on;
|
||||
|
||||
# Causes nginx to attempt to send its HTTP response head in one packet,
|
||||
# instead of using partial frames. Default is 'off'.
|
||||
tcp_nopush on;
|
||||
|
||||
|
||||
# Enables the specified protocols. Default is TLSv1 TLSv1.1 TLSv1.2.
|
||||
# TIP: If you're not obligated to support ancient clients, remove TLSv1.1.
|
||||
ssl_protocols TLSv1.2 TLSv1.3;
|
||||
|
||||
# Path of the file with Diffie-Hellman parameters for EDH ciphers.
|
||||
# TIP: Generate with: `openssl dhparam -out /etc/ssl/nginx/dh2048.pem 2048`
|
||||
#ssl_dhparam /etc/ssl/nginx/dh2048.pem;
|
||||
|
||||
# Specifies that our cipher suits should be preferred over client ciphers.
|
||||
# Default is 'off'.
|
||||
ssl_prefer_server_ciphers on;
|
||||
|
||||
# Enables a shared SSL cache with size that can hold around 8000 sessions.
|
||||
# Default is 'none'.
|
||||
ssl_session_cache shared:SSL:2m;
|
||||
|
||||
# Specifies a time during which a client may reuse the session parameters.
|
||||
# Default is '5m'.
|
||||
ssl_session_timeout 1h;
|
||||
|
||||
# Disable TLS session tickets (they are insecure). Default is 'on'.
|
||||
ssl_session_tickets off;
|
||||
|
||||
|
||||
# Enable gzipping of responses.
|
||||
gzip on;
|
||||
|
||||
# Set the Vary HTTP header as defined in the RFC 2616. Default is 'off'.
|
||||
gzip_vary on;
|
||||
|
||||
|
||||
# Specifies the main log format.
|
||||
log_format main '$remote_addr - $remote_user [$time_local] "$request" '
|
||||
'$status $body_bytes_sent "$http_referer" '
|
||||
'"$http_user_agent" "$http_x_forwarded_for"';
|
||||
|
||||
# Sets the path, format, and configuration for a buffered log write.
|
||||
access_log /tmp/log/nginx-access.log main;
|
||||
|
||||
|
||||
# Virtual host config (unencrypted)
|
||||
server {
|
||||
listen ${LISTEN_ADDR}:${PORT} default_server;
|
||||
root /app/front;
|
||||
index index.php;
|
||||
add_header X-Forwarded-Prefix "/app" always;
|
||||
|
||||
server_name netalertx-server;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
|
||||
client_body_buffer_size 512k;
|
||||
large_client_header_buffers 64 128k;
|
||||
|
||||
location ~* \.php$ {
|
||||
# Set Cache-Control header to prevent caching on the first load
|
||||
add_header Cache-Control "no-store";
|
||||
fastcgi_pass unix:/tmp/run/php.sock;
|
||||
include /services/config/nginx/fastcgi_params;
|
||||
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
|
||||
fastcgi_param SCRIPT_NAME $fastcgi_script_name;
|
||||
fastcgi_connect_timeout 75;
|
||||
fastcgi_send_timeout 600;
|
||||
fastcgi_read_timeout 600;
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Caddyfile
|
||||
```
|
||||
# Example and Guide
|
||||
# https://caddyserver.com/docs/caddyfile/options
|
||||
|
||||
# General Options
|
||||
{
|
||||
# (Optional) Debug Mode
|
||||
# debug
|
||||
|
||||
# (Optional ) Enable / Disable Admin API
|
||||
admin off
|
||||
|
||||
# TLS Options
|
||||
# (Optional) Disable Certificates Management (only if SSL/TLS Certificates are managed by certbot or other external Tools)
|
||||
auto_https disable_certs
|
||||
}
|
||||
|
||||
# (Optional Enable Admin API)
|
||||
# localhost {
|
||||
# reverse_proxy /api/* localhost:9001
|
||||
# }
|
||||
|
||||
# NetAlertX Web GUI (HTTPS Port 443)
|
||||
# (Optional) Only if SSL/TLS Certificates are managed by certbot or other external Tools and Custom Logging is required
|
||||
{$APPLICATION_HOSTNAME}:443 {
|
||||
tls /certificates/{$APPLICATION_CERTIFICATE_DOMAIN}/{$APPLICATION_CERTIFICATE_CERT_FILE:fullchain.pem} /certificates/{$APPLICATION_CERTIFICATE_DOMAIN}/{$APPLICATION_CERTIFICATE_KEY_FILE:privkey.pem}
|
||||
|
||||
log {
|
||||
output file /var/log/{$APPLICATION_HOSTNAME}/access_web.json {
|
||||
roll_size 100MiB
|
||||
roll_keep 5000
|
||||
roll_keep_for 720h
|
||||
roll_uncompressed
|
||||
}
|
||||
|
||||
format json
|
||||
}
|
||||
|
||||
route {
|
||||
# Always forward outpost path to actual outpost
|
||||
reverse_proxy /outpost.goauthentik.io/* https://{$OUTPOST_HOSTNAME}:{$OUTPOST_EXTERNAL_PORT} {
|
||||
header_up Host {http.reverse_proxy.upstream.hostport}
|
||||
}
|
||||
|
||||
# Forward authentication to outpost
|
||||
forward_auth https://{$OUTPOST_HOSTNAME}:{$OUTPOST_EXTERNAL_PORT} {
|
||||
uri /outpost.goauthentik.io/auth/caddy
|
||||
|
||||
# Capitalization of the headers is important, otherwise they will be empty
|
||||
copy_headers X-Authentik-Username X-Authentik-Groups X-Authentik-Email X-Authentik-Name X-Authentik-Uid X-Authentik-Jwt X-Authentik-Meta-Jwks X-Authentik-Meta-Outpost X-Authentik-Meta-Provider X-Authentik-Meta-App X-Authentik-Meta-Version
|
||||
|
||||
# (Optional)
|
||||
# If not set, trust all private ranges, but for Security Reasons, this should be set to the outposts IP
|
||||
trusted_proxies private_ranges
|
||||
}
|
||||
}
|
||||
|
||||
# IPv4 Reverse Proxy to NetAlertX Web GUI (internal unencrypted Host)
|
||||
reverse_proxy http://0.0.0.0:20211
|
||||
|
||||
# IPv6 Reverse Proxy to NetAlertX Web GUI (internal unencrypted Host)
|
||||
# reverse_proxy http://[::1]:20211
|
||||
}
|
||||
|
||||
# NetAlertX GraphQL Endpoint (HTTPS Port 20212)
|
||||
# (Optional) Only if SSL/TLS Certificates are managed by certbot or other external Tools and Custom Logging is required
|
||||
{$APPLICATION_HOSTNAME}:20212 {
|
||||
tls /certificates/{$APPLICATION_CERTIFICATE_DOMAIN}/{$APPLICATION_CERTIFICATE_CERT_FILE:fullchain.pem} /certificates/{$APPLICATION_CERTIFICATE_DOMAIN}/{$APPLICATION_CERTIFICATE_KEY_FILE:privkey.pem}
|
||||
|
||||
log {
|
||||
output file /var/log/{$APPLICATION_HOSTNAME}/access_graphql.json {
|
||||
roll_size 100MiB
|
||||
roll_keep 5000
|
||||
roll_keep_for 720h
|
||||
roll_uncompressed
|
||||
}
|
||||
|
||||
format json
|
||||
}
|
||||
|
||||
# IPv4 Reverse Proxy to NetAlertX GraphQL Endpoint (internal unencrypted Host)
|
||||
reverse_proxy http://0.0.0.0:20219
|
||||
|
||||
# IPv6 Reverse Proxy to NetAlertX GraphQL Endpoint (internal unencrypted Host)
|
||||
# reverse_proxy http://[::1]:6000
|
||||
}
|
||||
|
||||
# Authentik Outpost
|
||||
# (Optional) Only if SSL/TLS Certificates are managed by certbot or other external Tools and Custom Logging is required
|
||||
{$OUTPOST_HOSTNAME}:{$OUTPOST_EXTERNAL_PORT} {
|
||||
tls /certificates/{$APPLICATION_CERTIFICATE_DOMAIN}/{$APPLICATION_CERTIFICATE_CERT_FILE:fullchain.pem} /certificates/{$APPLICATION_CERTIFICATE_DOMAIN}/{$APPLICATION_CERTIFICATE_KEY_FILE:privkey.pem}
|
||||
|
||||
log {
|
||||
output file /var/log/outpost/{$OUTPOST_HOSTNAME}/access.json {
|
||||
roll_size 100MiB
|
||||
roll_keep 5000
|
||||
roll_keep_for 720h
|
||||
roll_uncompressed
|
||||
}
|
||||
|
||||
format json
|
||||
}
|
||||
|
||||
# IPv4 Reverse Proxy to internal unencrypted Host
|
||||
# reverse_proxy http://0.0.0.0:6000
|
||||
|
||||
# IPv6 Reverse Proxy to internal unencrypted Host
|
||||
reverse_proxy http://[::1]:6000
|
||||
}
|
||||
```
|
||||
|
||||
### Login
|
||||
Now try to login by visiting `https://netalertx.MYDOMAIN.TLD`.
|
||||
|
||||
You should be greeted with a Login Screen by Authentik.
|
||||
|
||||
If you are already logged in Authentik, log out first. You can do that by visiting `https://netalertx.MYDOMAIN.TLD/outpost.goauthentik.io/sign_out`, then click on `Log out of authentik` (2nd Button). Or you can just sign out from your Authentik Admin Panel at `https://authentik.MYDOMAIN.TLD`.
|
||||
|
||||
If everything works as expected, then you can now set `SETPWD_enable_password=false` to disable double Authentication.
|
||||
|
||||

|
||||
@@ -1,86 +0,0 @@
|
||||
# Guide: Routing NetAlertX API via Traefik v3
|
||||
|
||||
> [!NOTE]
|
||||
> NetAlertX requires access to both the **web UI** (default `20211`) and the **GraphQL backend `GRAPHQL_PORT`** (default `20212`) ports.
|
||||
> Ensure your reverse proxy allows traffic to both for proper functionality.
|
||||
|
||||
|
||||
> [!NOTE]
|
||||
> This is community-contributed. Due to environment, setup, or networking differences, results may vary. Please open a PR to improve it instead of creating an issue, as the maintainer is not actively maintaining it.
|
||||
|
||||
|
||||
Traefik v3 requires the following setup to route traffic properly. This guide shows a working configuration using a dedicated `PathPrefix`.
|
||||
|
||||
---
|
||||
|
||||
## 1. Configure NetAlertX Backend URL
|
||||
|
||||
1. Open the NetAlertX UI: **Settings → Core → General**.
|
||||
2. Set the `BACKEND_API_URL` to include a custom path prefix, for example:
|
||||
|
||||
```
|
||||
https://netalertx.yourdomain.com/netalertx-api
|
||||
```
|
||||
|
||||
This tells the frontend where to reach the backend API.
|
||||
|
||||
---
|
||||
|
||||
## 2. Create a Traefik Router for the API
|
||||
|
||||
Define a router specifically for the API with a higher priority and a `PathPrefix` rule:
|
||||
|
||||
```yaml
|
||||
netalertx-api:
|
||||
rule: "Host(`netalertx.yourdomain.com`) && PathPrefix(`/netalertx-api`)"
|
||||
service: netalertx-api-service
|
||||
middlewares:
|
||||
- netalertx-stripprefix
|
||||
priority: 100
|
||||
```
|
||||
|
||||
**Notes:**
|
||||
|
||||
* `Host(...)` ensures requests are only routed for your domain.
|
||||
* `PathPrefix(...)` routes anything under `/netalertx-api` to the backend.
|
||||
* Priority `100` ensures this router takes precedence over other routes.
|
||||
|
||||
---
|
||||
|
||||
## 3. Add a Middleware to Strip the Prefix
|
||||
|
||||
NetAlertX expects requests at the root (`/`). Use Traefik’s `StripPrefix` middleware:
|
||||
|
||||
```yaml
|
||||
middlewares:
|
||||
netalertx-stripprefix:
|
||||
stripPrefix:
|
||||
prefixes:
|
||||
- "/netalertx-api"
|
||||
```
|
||||
|
||||
This removes `/netalertx-api` before forwarding the request to the backend container.
|
||||
|
||||
---
|
||||
|
||||
## 4. Map the API Service to the Backend Container
|
||||
|
||||
Point the service to the internal GraphQL/Backend port (20212):
|
||||
|
||||
```yaml
|
||||
netalertx-api-service:
|
||||
loadBalancer:
|
||||
servers:
|
||||
- url: "http://<INTERNAL_IP>:20212"
|
||||
```
|
||||
|
||||
Replace `<INTERNAL_IP>` with your NetAlertX container’s internal address.
|
||||
|
||||
---
|
||||
|
||||
✅ With this setup:
|
||||
|
||||
* `https://netalertx.yourdomain.com` → Web interface (port 20211)
|
||||
* `https://netalertx.yourdomain.com/netalertx-api` → API/GraphQL backend (port 20212)
|
||||
|
||||
This cleanly separates API requests from frontend requests while keeping everything under the same domain.
|
||||
@@ -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.
|
||||
> 
|
||||
|
||||
* **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
|
||||
|
||||

|
||||
|
||||
> [!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):**
|
||||
|
||||
@@ -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).
|
||||
@@ -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.
|
||||
|
||||

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

|
||||
|
||||
```
|
||||
|
||||
@@ -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).
|
||||
|
||||
BIN
docs/img/ADVISORIES/down_devices.png
Normal file
|
After Width: | Height: | Size: 63 KiB |
BIN
docs/img/ADVISORIES/filters.png
Normal file
|
After Width: | Height: | Size: 83 KiB |
BIN
docs/img/ADVISORIES/ui_customization_settings.png
Normal file
|
After Width: | Height: | Size: 137 KiB |
BIN
docs/img/DEVICE_MANAGEMENT/device_management_status_archived.png
Normal file
|
After Width: | Height: | Size: 2.2 KiB |
|
After Width: | Height: | Size: 1.6 KiB |
|
Before Width: | Height: | Size: 39 KiB After Width: | Height: | Size: 32 KiB |
BIN
docs/img/DEVICE_MANAGEMENT/device_management_status_down.png
Normal file
|
After Width: | Height: | Size: 1.8 KiB |
|
After Width: | Height: | Size: 2.5 KiB |
|
After Width: | Height: | Size: 1.6 KiB |
|
After Width: | Height: | Size: 1.6 KiB |
BIN
docs/img/DEVICE_MANAGEMENT/device_management_status_offline.png
Normal file
|
After Width: | Height: | Size: 1.8 KiB |
BIN
docs/img/DEVICE_MANAGEMENT/device_management_status_online.png
Normal file
|
After Width: | Height: | Size: 1.7 KiB |
BIN
docs/img/DEVICE_MANAGEMENT/device_management_status_sleeping.png
Normal file
|
After Width: | Height: | Size: 2.5 KiB |
|
Before Width: | Height: | Size: 78 KiB |
|
Before Width: | Height: | Size: 1.5 MiB |
|
Before Width: | Height: | Size: 61 KiB |
|
Before Width: | Height: | Size: 52 KiB |
|
Before Width: | Height: | Size: 128 KiB |
|
Before Width: | Height: | Size: 89 KiB |
|
Before Width: | Height: | Size: 27 KiB |
|
Before Width: | Height: | Size: 67 KiB |
@@ -1,202 +0,0 @@
|
||||
<mxfile host="Electron" modified="2026-01-15T05:36:26.645Z" agent="Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) draw.io/24.1.0 Chrome/120.0.6099.109 Electron/28.1.0 Safari/537.36" etag="OpSjRPjeNeyudFLZJ2fD" version="24.1.0" type="device">
|
||||
<diagram name="Page-1" id="mulIpG3YQAhf4Klf7Njm">
|
||||
<mxGraphModel dx="6733" dy="1168" grid="1" gridSize="10" guides="1" tooltips="1" connect="1" arrows="1" fold="1" page="1" pageScale="1" pageWidth="4681" pageHeight="3300" math="0" shadow="0">
|
||||
<root>
|
||||
<mxCell id="0" />
|
||||
<mxCell id="1" parent="0" />
|
||||
<mxCell id="wwqsnaxs0Bt7SYwqQu8i-1" value="" style="rounded=0;whiteSpace=wrap;html=1;" vertex="1" parent="1">
|
||||
<mxGeometry x="850" y="160" width="920" height="810" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="wwqsnaxs0Bt7SYwqQu8i-2" value="NetAlertX Pod" style="text;html=1;align=center;verticalAlign=middle;whiteSpace=wrap;rounded=0;fontSize=32;" vertex="1" parent="1">
|
||||
<mxGeometry x="850" y="130" width="670" height="30" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="wwqsnaxs0Bt7SYwqQu8i-3" value="" style="image;html=1;image=img/lib/clip_art/computers/Laptop_128x128.png" vertex="1" parent="1">
|
||||
<mxGeometry x="-50" y="395" width="140" height="140" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="wwqsnaxs0Bt7SYwqQu8i-4" value="" style="image;html=1;image=img/lib/clip_art/networking/Firewall_02_128x128.png" vertex="1" parent="1">
|
||||
<mxGeometry x="488" y="344" width="80" height="80" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="wwqsnaxs0Bt7SYwqQu8i-5" value="" style="image;html=1;image=img/lib/clip_art/networking/Firewall_02_128x128.png" vertex="1" parent="1">
|
||||
<mxGeometry x="488" y="555" width="80" height="80" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="wwqsnaxs0Bt7SYwqQu8i-8" value="Web UI<br>(NGINX + PHP)" style="text;html=1;align=center;verticalAlign=middle;whiteSpace=wrap;rounded=0;fontSize=24;" vertex="1" parent="1">
|
||||
<mxGeometry x="230" y="320" width="200" height="60" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="wwqsnaxs0Bt7SYwqQu8i-9" value="API GraphQL<br>(Python)" style="text;html=1;align=center;verticalAlign=middle;whiteSpace=wrap;rounded=0;fontSize=24;" vertex="1" parent="1">
|
||||
<mxGeometry x="230" y="555" width="200" height="30" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="wwqsnaxs0Bt7SYwqQu8i-10" value="" style="endArrow=classic;html=1;rounded=0;dashed=1;dashPattern=8 8;" edge="1" parent="1">
|
||||
<mxGeometry width="50" height="50" relative="1" as="geometry">
|
||||
<mxPoint x="240" y="390" as="sourcePoint" />
|
||||
<mxPoint x="240" y="600" as="targetPoint" />
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="wwqsnaxs0Bt7SYwqQu8i-12" value="<div>443</div>" style="text;html=1;align=center;verticalAlign=middle;whiteSpace=wrap;rounded=0;fontSize=24;fontColor=#FF0000;" vertex="1" parent="1">
|
||||
<mxGeometry x="581" y="335" width="100" height="60" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="wwqsnaxs0Bt7SYwqQu8i-13" value="20212" style="text;html=1;align=center;verticalAlign=middle;whiteSpace=wrap;rounded=0;fontSize=24;fontColor=#FF0000;" vertex="1" parent="1">
|
||||
<mxGeometry x="581" y="554" width="100" height="60" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="wwqsnaxs0Bt7SYwqQu8i-14" value="" style="image;html=1;image=img/lib/clip_art/networking/Firewall_02_128x128.png" vertex="1" parent="1">
|
||||
<mxGeometry x="488" y="813" width="80" height="80" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="wwqsnaxs0Bt7SYwqQu8i-16" value="Authentik SSO for Web UI" style="text;html=1;align=center;verticalAlign=middle;whiteSpace=wrap;rounded=0;fontSize=24;" vertex="1" parent="1">
|
||||
<mxGeometry x="230" y="793" width="200" height="60" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="wwqsnaxs0Bt7SYwqQu8i-17" value="9443" style="text;html=1;align=center;verticalAlign=middle;whiteSpace=wrap;rounded=0;fontSize=24;fontColor=#FF0000;" vertex="1" parent="1">
|
||||
<mxGeometry x="580" y="803" width="100" height="60" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="wwqsnaxs0Bt7SYwqQu8i-18" value="" style="rounded=0;whiteSpace=wrap;html=1;" vertex="1" parent="1">
|
||||
<mxGeometry x="1470" y="250" width="288" height="440" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="wwqsnaxs0Bt7SYwqQu8i-19" value="NetAlertX" style="text;html=1;align=center;verticalAlign=middle;whiteSpace=wrap;rounded=0;fontSize=24;" vertex="1" parent="1">
|
||||
<mxGeometry x="1470" y="210" width="288" height="40" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="wwqsnaxs0Bt7SYwqQu8i-21" value="" style="rounded=0;whiteSpace=wrap;html=1;" vertex="1" parent="1">
|
||||
<mxGeometry x="1260" y="751" width="500" height="199" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="wwqsnaxs0Bt7SYwqQu8i-22" value="Authentik Outpost Proxy" style="text;html=1;align=center;verticalAlign=middle;whiteSpace=wrap;rounded=0;fontSize=24;" vertex="1" parent="1">
|
||||
<mxGeometry x="1280" y="711" width="480" height="40" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="wwqsnaxs0Bt7SYwqQu8i-23" value="" style="rounded=0;whiteSpace=wrap;html=1;" vertex="1" parent="1">
|
||||
<mxGeometry x="860" y="250" width="380" height="700" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="wwqsnaxs0Bt7SYwqQu8i-24" value="Caddy" style="text;html=1;align=center;verticalAlign=middle;whiteSpace=wrap;rounded=0;fontSize=24;" vertex="1" parent="1">
|
||||
<mxGeometry x="860" y="210" width="390" height="40" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="wwqsnaxs0Bt7SYwqQu8i-25" value="" style="rounded=0;whiteSpace=wrap;html=1;" vertex="1" parent="1">
|
||||
<mxGeometry x="1498" y="319" width="220" height="130" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="wwqsnaxs0Bt7SYwqQu8i-26" value="" style="rounded=0;whiteSpace=wrap;html=1;" vertex="1" parent="1">
|
||||
<mxGeometry x="1498" y="530" width="220" height="150" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="wwqsnaxs0Bt7SYwqQu8i-27" value="Web UI<div>(NGINX + PHP)</div>" style="text;html=1;align=center;verticalAlign=middle;whiteSpace=wrap;rounded=0;fontSize=24;" vertex="1" parent="1">
|
||||
<mxGeometry x="1498" y="264" width="220" height="50" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="wwqsnaxs0Bt7SYwqQu8i-28" value="API GraphQL<div>(Python)</div>" style="text;html=1;align=center;verticalAlign=middle;whiteSpace=wrap;rounded=0;fontSize=24;" vertex="1" parent="1">
|
||||
<mxGeometry x="1498" y="475" width="220" height="50" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="wwqsnaxs0Bt7SYwqQu8i-6" value="" style="endArrow=classic;html=1;rounded=0;entryX=0;entryY=0.5;entryDx=0;entryDy=0;" edge="1" parent="1" source="wwqsnaxs0Bt7SYwqQu8i-53" target="wwqsnaxs0Bt7SYwqQu8i-58">
|
||||
<mxGeometry width="50" height="50" relative="1" as="geometry">
|
||||
<mxPoint x="130" y="390" as="sourcePoint" />
|
||||
<mxPoint x="1129" y="389.9999999999998" as="targetPoint" />
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="wwqsnaxs0Bt7SYwqQu8i-30" value="" style="endArrow=classic;html=1;rounded=0;entryX=0;entryY=0.5;entryDx=0;entryDy=0;exitX=1;exitY=0.5;exitDx=0;exitDy=0;" edge="1" parent="1" source="wwqsnaxs0Bt7SYwqQu8i-59" target="wwqsnaxs0Bt7SYwqQu8i-31">
|
||||
<mxGeometry width="50" height="50" relative="1" as="geometry">
|
||||
<mxPoint x="1214" y="483" as="sourcePoint" />
|
||||
<mxPoint x="1209" y="823" as="targetPoint" />
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="wwqsnaxs0Bt7SYwqQu8i-31" value="Authenticated &amp; Authorized ?" style="rhombus;whiteSpace=wrap;html=1;fontSize=18;" vertex="1" parent="1">
|
||||
<mxGeometry x="1294" y="773.5" width="170" height="160" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="wwqsnaxs0Bt7SYwqQu8i-35" value="20211" style="text;html=1;align=center;verticalAlign=middle;whiteSpace=wrap;rounded=0;fontSize=24;fontColor=#FF0000;" vertex="1" parent="1">
|
||||
<mxGeometry x="1488" y="335" width="100" height="60" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="wwqsnaxs0Bt7SYwqQu8i-36" value="" style="endArrow=classic;html=1;rounded=0;" edge="1" parent="1">
|
||||
<mxGeometry width="50" height="50" relative="1" as="geometry">
|
||||
<mxPoint x="1688" y="369" as="sourcePoint" />
|
||||
<mxPoint x="1688" y="649" as="targetPoint" />
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="wwqsnaxs0Bt7SYwqQu8i-37" value="20219" style="text;html=1;align=center;verticalAlign=middle;whiteSpace=wrap;rounded=0;fontSize=24;fontColor=#FF0000;" vertex="1" parent="1">
|
||||
<mxGeometry x="1498" y="535" width="100" height="60" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="wwqsnaxs0Bt7SYwqQu8i-38" value="HTTPS" style="text;html=1;align=center;verticalAlign=middle;whiteSpace=wrap;rounded=0;fontSize=24;fontColor=#66CC00;" vertex="1" parent="1">
|
||||
<mxGeometry x="730" y="340" width="100" height="60" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="wwqsnaxs0Bt7SYwqQu8i-39" value="HTTPS" style="text;html=1;align=center;verticalAlign=middle;whiteSpace=wrap;rounded=0;fontSize=24;fontColor=#66CC00;" vertex="1" parent="1">
|
||||
<mxGeometry x="730" y="803" width="100" height="60" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="wwqsnaxs0Bt7SYwqQu8i-40" value="HTTPS" style="text;html=1;align=center;verticalAlign=middle;whiteSpace=wrap;rounded=0;fontSize=24;fontColor=#66CC00;" vertex="1" parent="1">
|
||||
<mxGeometry x="730" y="554" width="100" height="60" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="wwqsnaxs0Bt7SYwqQu8i-42" value="" style="endArrow=none;html=1;rounded=0;endFill=0;" edge="1" parent="1">
|
||||
<mxGeometry width="50" height="50" relative="1" as="geometry">
|
||||
<mxPoint x="1381" y="1071" as="sourcePoint" />
|
||||
<mxPoint x="130" y="1071" as="targetPoint" />
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="wwqsnaxs0Bt7SYwqQu8i-43" value="" style="endArrow=classic;html=1;rounded=0;exitX=0.5;exitY=1;exitDx=0;exitDy=0;" edge="1" parent="1">
|
||||
<mxGeometry width="50" height="50" relative="1" as="geometry">
|
||||
<mxPoint x="130.5" y="1070" as="sourcePoint" />
|
||||
<mxPoint x="130" y="860" as="targetPoint" />
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="wwqsnaxs0Bt7SYwqQu8i-44" value="NO" style="text;html=1;align=center;verticalAlign=middle;whiteSpace=wrap;rounded=0;fontSize=24;" vertex="1" parent="1">
|
||||
<mxGeometry x="1364" y="1000" width="100" height="60" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="wwqsnaxs0Bt7SYwqQu8i-45" value="YES" style="text;html=1;align=center;verticalAlign=middle;whiteSpace=wrap;rounded=0;fontSize=24;" vertex="1" parent="1">
|
||||
<mxGeometry x="1294" y="680" width="100" height="60" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="wwqsnaxs0Bt7SYwqQu8i-47" value="" style="endArrow=classic;html=1;rounded=0;exitX=1;exitY=0.5;exitDx=0;exitDy=0;" edge="1" parent="1">
|
||||
<mxGeometry width="50" height="50" relative="1" as="geometry">
|
||||
<mxPoint x="1156.5" y="450" as="sourcePoint" />
|
||||
<mxPoint x="1157" y="1070" as="targetPoint" />
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="wwqsnaxs0Bt7SYwqQu8i-48" value="" style="endArrow=classic;html=1;rounded=0;exitX=1;exitY=0.5;exitDx=0;exitDy=0;entryX=0;entryY=0.5;entryDx=0;entryDy=0;" edge="1" parent="1" source="wwqsnaxs0Bt7SYwqQu8i-56" target="wwqsnaxs0Bt7SYwqQu8i-26">
|
||||
<mxGeometry width="50" height="50" relative="1" as="geometry">
|
||||
<mxPoint x="1299" y="600" as="sourcePoint" />
|
||||
<mxPoint x="1499" y="600" as="targetPoint" />
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="wwqsnaxs0Bt7SYwqQu8i-49" value="HTTP" style="text;html=1;align=center;verticalAlign=middle;whiteSpace=wrap;rounded=0;fontSize=24;fontColor=#FF0000;" vertex="1" parent="1">
|
||||
<mxGeometry x="1379" y="340" width="100" height="60" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="wwqsnaxs0Bt7SYwqQu8i-50" value="HTTP" style="text;html=1;align=center;verticalAlign=middle;whiteSpace=wrap;rounded=0;fontSize=24;fontColor=#FF0000;" vertex="1" parent="1">
|
||||
<mxGeometry x="1379" y="554" width="100" height="60" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="wwqsnaxs0Bt7SYwqQu8i-54" value="" style="endArrow=classic;html=1;rounded=0;" edge="1" parent="1" target="wwqsnaxs0Bt7SYwqQu8i-53">
|
||||
<mxGeometry width="50" height="50" relative="1" as="geometry">
|
||||
<mxPoint x="130" y="390" as="sourcePoint" />
|
||||
<mxPoint x="1129" y="390" as="targetPoint" />
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="wwqsnaxs0Bt7SYwqQu8i-53" value="TLS Termination" style="whiteSpace=wrap;html=1;aspect=fixed;fontSize=18;" vertex="1" parent="1">
|
||||
<mxGeometry x="905" y="340" width="100" height="100" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="wwqsnaxs0Bt7SYwqQu8i-56" value="TLS Termination" style="whiteSpace=wrap;html=1;aspect=fixed;fontSize=18;" vertex="1" parent="1">
|
||||
<mxGeometry x="902" y="554" width="100" height="100" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="wwqsnaxs0Bt7SYwqQu8i-7" value="" style="endArrow=classic;html=1;rounded=0;entryX=0;entryY=0.5;entryDx=0;entryDy=0;" edge="1" parent="1" target="wwqsnaxs0Bt7SYwqQu8i-56">
|
||||
<mxGeometry width="50" height="50" relative="1" as="geometry">
|
||||
<mxPoint x="130" y="601" as="sourcePoint" />
|
||||
<mxPoint x="850" y="601" as="targetPoint" />
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="wwqsnaxs0Bt7SYwqQu8i-58" value="Check Authentication" style="whiteSpace=wrap;html=1;aspect=fixed;fontSize=18;" vertex="1" parent="1">
|
||||
<mxGeometry x="1097" y="330" width="120" height="120" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="wwqsnaxs0Bt7SYwqQu8i-59" value="TLS Termination" style="whiteSpace=wrap;html=1;aspect=fixed;fontSize=18;" vertex="1" parent="1">
|
||||
<mxGeometry x="899" y="803" width="100" height="100" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="wwqsnaxs0Bt7SYwqQu8i-15" value="" style="endArrow=classic;html=1;rounded=0;entryX=0;entryY=0.5;entryDx=0;entryDy=0;" edge="1" parent="1" target="wwqsnaxs0Bt7SYwqQu8i-59">
|
||||
<mxGeometry width="50" height="50" relative="1" as="geometry">
|
||||
<mxPoint x="30" y="853" as="sourcePoint" />
|
||||
<mxPoint x="850" y="853" as="targetPoint" />
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="wwqsnaxs0Bt7SYwqQu8i-60" value="" style="endArrow=classic;html=1;rounded=0;entryX=0;entryY=0.5;entryDx=0;entryDy=0;" edge="1" parent="1">
|
||||
<mxGeometry width="50" height="50" relative="1" as="geometry">
|
||||
<mxPoint x="1379" y="390" as="sourcePoint" />
|
||||
<mxPoint x="1500" y="389.58" as="targetPoint" />
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="wwqsnaxs0Bt7SYwqQu8i-61" value="" style="endArrow=none;html=1;rounded=0;entryX=0;entryY=0.5;entryDx=0;entryDy=0;endFill=0;" edge="1" parent="1">
|
||||
<mxGeometry width="50" height="50" relative="1" as="geometry">
|
||||
<mxPoint x="1379" y="773" as="sourcePoint" />
|
||||
<mxPoint x="1379" y="390" as="targetPoint" />
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="wwqsnaxs0Bt7SYwqQu8i-62" value="" style="endArrow=classic;html=1;rounded=0;exitX=1;exitY=0.5;exitDx=0;exitDy=0;" edge="1" parent="1">
|
||||
<mxGeometry width="50" height="50" relative="1" as="geometry">
|
||||
<mxPoint x="1380" y="933.5" as="sourcePoint" />
|
||||
<mxPoint x="1379" y="1069" as="targetPoint" />
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
</root>
|
||||
</mxGraphModel>
|
||||
</diagram>
|
||||
</mxfile>
|
||||
|
Before Width: | Height: | Size: 176 KiB |
|
Before Width: | Height: | Size: 31 KiB |
|
Before Width: | Height: | Size: 170 KiB After Width: | Height: | Size: 201 KiB |
65
front/403_internal.html
Normal file
@@ -0,0 +1,65 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Access Restricted - NetAlertX</title>
|
||||
<style>
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
|
||||
background-color: #f8f9fa;
|
||||
color: #212529;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 100vh;
|
||||
margin: 0;
|
||||
}
|
||||
.container {
|
||||
text-align: center;
|
||||
padding: 2rem;
|
||||
background: white;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 4px 6px rgba(0,0,0,0.1);
|
||||
max-width: 600px;
|
||||
}
|
||||
h1 {
|
||||
color: #dc3545;
|
||||
font-size: 2rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
p {
|
||||
margin-bottom: 1rem;
|
||||
line-height: 1.5;
|
||||
}
|
||||
.code-snippet {
|
||||
background-color: #e9ecef;
|
||||
padding: 0.2rem 0.4rem;
|
||||
border-radius: 4px;
|
||||
font-family: monospace;
|
||||
font-weight: bold;
|
||||
}
|
||||
.footer {
|
||||
margin-top: 2rem;
|
||||
font-size: 0.9rem;
|
||||
color: #6c757d;
|
||||
border-top: 1px solid #dee2e6;
|
||||
padding-top: 1rem;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<h1>403 Forbidden</h1>
|
||||
<p>
|
||||
The <span class="code-snippet">/server</span> endpoint is for <strong>internal use only</strong> and cannot be accessed from external browsers or applications.
|
||||
</p>
|
||||
<p>
|
||||
This security measure protects the backend API. You will need to contact your system administrator in order to gain access to the API port (default: 20212), or use the application through the standard web interface.
|
||||
</p>
|
||||
<div class="footer">
|
||||
NetAlertX Security • Trust Level: Untrusted Origin
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -42,14 +42,7 @@ h4 {
|
||||
.content-header > .breadcrumb > li > a {
|
||||
color: #bec5cb;
|
||||
}
|
||||
.table > thead > tr > th,
|
||||
.table > tbody > tr > th,
|
||||
.table > tfoot > tr > th,
|
||||
.table > thead > tr > td,
|
||||
.table > tbody > tr > td,
|
||||
.table > tfoot > tr > td {
|
||||
border-top: 0;
|
||||
}
|
||||
|
||||
.table > thead > tr.odd,
|
||||
.table > tbody > tr.odd,
|
||||
.table > tfoot > tr.odd {
|
||||
@@ -73,7 +66,6 @@ h4 {
|
||||
border: 1px solid #353c42;
|
||||
}
|
||||
.dataTables_wrapper input[type="search"] {
|
||||
border-radius: 4px;
|
||||
background-color: #353c42;
|
||||
border: 0;
|
||||
color: #bec5cb;
|
||||
@@ -126,7 +118,6 @@ h4 {
|
||||
border-color: #3c8dbc;
|
||||
}
|
||||
.sidebar-menu > li > .treeview-menu {
|
||||
margin: 0 1px;
|
||||
background-color: #32393e;
|
||||
}
|
||||
.sidebar a {
|
||||
@@ -144,16 +135,13 @@ h4 {
|
||||
color: #fff;
|
||||
}
|
||||
.sidebar-form {
|
||||
border-radius: 3px;
|
||||
border: 1px solid #3e464c;
|
||||
margin: 10px;
|
||||
}
|
||||
.sidebar-form input[type="text"],
|
||||
.sidebar-form .btn {
|
||||
box-shadow: none;
|
||||
background-color: #3e464c;
|
||||
border: 1px solid transparent;
|
||||
height: 35px;
|
||||
}
|
||||
.sidebar-form input[type="text"] {
|
||||
color: #666;
|
||||
@@ -207,20 +195,13 @@ h4 {
|
||||
.box > .box-header .btn {
|
||||
color: #bec5cb;
|
||||
}
|
||||
.box.box-info,
|
||||
.box.box-primary,
|
||||
.box.box-success,
|
||||
.box.box-warning,
|
||||
.box.box-danger {
|
||||
border-top-width: 3px;
|
||||
}
|
||||
|
||||
.main-header .navbar {
|
||||
background-color: #272c30;
|
||||
}
|
||||
.main-header .navbar .nav > li > a,
|
||||
.main-header .navbar .nav > li > .navbar-text {
|
||||
color: #bec5cb;
|
||||
max-height: 50px;
|
||||
}
|
||||
.main-header .navbar .nav > li > a:hover,
|
||||
.main-header .navbar .nav > li > a:active,
|
||||
@@ -277,7 +258,6 @@ h4 {
|
||||
background: rgba(64, 72, 80, 0.666);
|
||||
}
|
||||
.nav-tabs-custom > .nav-tabs > li {
|
||||
margin-right: 1px;
|
||||
color: #bec5cb;
|
||||
}
|
||||
.nav-tabs-custom > .nav-tabs > li.active > a,
|
||||
@@ -386,11 +366,8 @@ h4 {
|
||||
|
||||
code,
|
||||
pre {
|
||||
padding: 2px 4px;
|
||||
font-size: 90%;
|
||||
color: #bec5cb;
|
||||
background-color: #353c42;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
/* Used in the Query Log table */
|
||||
@@ -456,7 +433,7 @@ td.highlight {
|
||||
/* Used by the long-term pages */
|
||||
.daterangepicker {
|
||||
background-color: #3e464c;
|
||||
border-radius: 4px;
|
||||
|
||||
border: 1px solid #353c42;
|
||||
}
|
||||
.daterangepicker .ranges li:hover {
|
||||
@@ -467,7 +444,7 @@ td.highlight {
|
||||
}
|
||||
.daterangepicker .calendar-table {
|
||||
background-color: #3e464c;
|
||||
border-radius: 4px;
|
||||
|
||||
border: 1px solid #353c42;
|
||||
}
|
||||
.daterangepicker td.off,
|
||||
@@ -535,7 +512,7 @@ textarea[readonly],
|
||||
.panel-body,
|
||||
.panel-default > .panel-heading {
|
||||
background-color: #3e464c;
|
||||
border-radius: 4px;
|
||||
|
||||
border: 1px solid #353c42;
|
||||
color: #bec5cb;
|
||||
}
|
||||
@@ -568,23 +545,10 @@ input[type="password"]::-webkit-caps-lock-indicator {
|
||||
background-image: linear-gradient(to right, #114100 0%, #525200 100%);
|
||||
}
|
||||
|
||||
.icheckbox_polaris,
|
||||
.icheckbox_futurico,
|
||||
.icheckbox_minimal-blue {
|
||||
margin-right: 10px;
|
||||
}
|
||||
.iradio_polaris,
|
||||
.iradio_futurico,
|
||||
.iradio_minimal-blue {
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
/* Overlay box with spinners as shown during data collection for graphs */
|
||||
.box .overlay,
|
||||
.overlay-wrapper .overlay {
|
||||
z-index: 50;
|
||||
background-color: rgba(53, 60, 66, 0.733);
|
||||
border-radius: 3px;
|
||||
}
|
||||
.box .overlay > .fa,
|
||||
.overlay-wrapper .overlay > .fa,
|
||||
@@ -594,7 +558,6 @@ input[type="password"]::-webkit-caps-lock-indicator {
|
||||
|
||||
.navbar-nav > .user-menu > .dropdown-menu > .user-footer {
|
||||
background-color: #353c42bb;
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.modal-content {
|
||||
@@ -626,29 +589,29 @@ input[type="password"]::-webkit-caps-lock-indicator {
|
||||
/*** Additional fixes For UI ***/
|
||||
.pa-small-box-aqua .inner {
|
||||
background-color: rgb(45,108,133);
|
||||
border-top-left-radius: 10px;
|
||||
border-top-right-radius: 10px;
|
||||
|
||||
|
||||
}
|
||||
.pa-small-box-green .inner {
|
||||
background-color: rgb(31,76,46);
|
||||
border-top-left-radius: 10px;
|
||||
border-top-right-radius: 10px;
|
||||
|
||||
|
||||
}
|
||||
.pa-small-box-yellow .inner {
|
||||
background-color: rgb(151,104,37);
|
||||
border-top-left-radius: 10px;
|
||||
border-top-right-radius: 10px;
|
||||
|
||||
|
||||
}
|
||||
.pa-small-box-red .inner {
|
||||
background-color: rgb(120,50,38);
|
||||
border-top-left-radius: 10px;
|
||||
border-top-right-radius: 10px;
|
||||
|
||||
|
||||
}
|
||||
.pa-small-box-gray .inner {
|
||||
background-color: #777;
|
||||
/* color: rgba(20,20,20,30%); */
|
||||
border-top-left-radius: 10px;
|
||||
border-top-right-radius: 10px;
|
||||
|
||||
|
||||
}
|
||||
.pa-small-box-gray .inner h3 {
|
||||
color: #bbb;
|
||||
@@ -687,15 +650,6 @@ table.dataTable tbody tr.selected, table.dataTable tbody tr .selected
|
||||
.db_tools_table_cell_b:nth-child(1) {background: #272c30}
|
||||
.db_tools_table_cell_b:nth-child(2) {background: #272c30}
|
||||
|
||||
.db_info_table {
|
||||
display: table;
|
||||
border-spacing: 0em;
|
||||
font-weight: 400;
|
||||
font-size: 15px;
|
||||
width: 100%;
|
||||
margin: auto;
|
||||
}
|
||||
|
||||
.nav-tabs-custom > .nav-tabs > li:hover > a, .nav-tabs-custom > .nav-tabs > li.active:hover > a {
|
||||
background-color: #272c30;
|
||||
color: #bec5cb;
|
||||
@@ -738,23 +692,7 @@ table.dataTable tbody tr.selected, table.dataTable tbody tr .selected
|
||||
color: #bec5cb;
|
||||
background-color: #272c30;
|
||||
}
|
||||
/* Add border radius to bottom of the status boxes*/
|
||||
.pa-small-box-footer {
|
||||
border-bottom-left-radius: 10px;
|
||||
border-bottom-right-radius: 10px;
|
||||
}
|
||||
|
||||
.small-box > .inner h3, .small-box > .inner p {
|
||||
margin-bottom: 0px;
|
||||
margin-left: 0px;
|
||||
}
|
||||
.small-box:hover .icon {
|
||||
font-size: 3em;
|
||||
}
|
||||
.small-box .icon {
|
||||
top: 0.01em;
|
||||
font-size: 3.25em;
|
||||
}
|
||||
.nax_semitransparent-panel{
|
||||
background-color: #000 !important;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
@@ -76,9 +76,7 @@
|
||||
border: 1px solid #353c42;
|
||||
}
|
||||
.dataTables_wrapper input[type="search"] {
|
||||
border-radius: 4px;
|
||||
background-color: #353c42;
|
||||
border: 0;
|
||||
color: #bec5cb;
|
||||
}
|
||||
.dataTables_paginate .pagination li > a {
|
||||
@@ -129,7 +127,6 @@
|
||||
border-color: #3c8dbc;
|
||||
}
|
||||
.sidebar-menu > li > .treeview-menu {
|
||||
margin: 0 1px;
|
||||
background-color: #32393e;
|
||||
}
|
||||
.sidebar a {
|
||||
@@ -147,23 +144,16 @@
|
||||
color: #fff;
|
||||
}
|
||||
.sidebar-form {
|
||||
border-radius: 3px;
|
||||
border: 1px solid #3e464c;
|
||||
margin: 10px;
|
||||
}
|
||||
.sidebar-form input[type="text"],
|
||||
.sidebar-form .btn {
|
||||
box-shadow: none;
|
||||
background-color: #3e464c;
|
||||
border: 1px solid transparent;
|
||||
height: 35px;
|
||||
}
|
||||
.sidebar-form input[type="text"] {
|
||||
color: #666;
|
||||
border-top-left-radius: 2px;
|
||||
border-top-right-radius: 0;
|
||||
border-bottom-right-radius: 0;
|
||||
border-bottom-left-radius: 2px;
|
||||
}
|
||||
.sidebar-form input[type="text"]:focus,
|
||||
.sidebar-form input[type="text"]:focus + .input-group-btn .btn {
|
||||
@@ -175,10 +165,6 @@
|
||||
}
|
||||
.sidebar-form .btn {
|
||||
color: #999;
|
||||
border-top-left-radius: 0;
|
||||
border-top-right-radius: 2px;
|
||||
border-bottom-right-radius: 2px;
|
||||
border-bottom-left-radius: 0;
|
||||
}
|
||||
.box,
|
||||
.box-footer,
|
||||
@@ -210,20 +196,12 @@
|
||||
.box > .box-header .btn {
|
||||
color: #bec5cb;
|
||||
}
|
||||
.box.box-info,
|
||||
.box.box-primary,
|
||||
.box.box-success,
|
||||
.box.box-warning,
|
||||
.box.box-danger {
|
||||
border-top-width: 3px;
|
||||
}
|
||||
.main-header .navbar {
|
||||
background-color: #272c30;
|
||||
}
|
||||
.main-header .navbar .nav > li > a,
|
||||
.main-header .navbar .nav > li > .navbar-text {
|
||||
color: #bec5cb;
|
||||
max-height: 50px;
|
||||
}
|
||||
.main-header .navbar .nav > li > a:hover,
|
||||
.main-header .navbar .nav > li > a:active,
|
||||
@@ -280,7 +258,6 @@
|
||||
background: rgba(64, 72, 80, 0.666);
|
||||
}
|
||||
.nav-tabs-custom > .nav-tabs > li {
|
||||
margin-right: 1px;
|
||||
color: #bec5cb;
|
||||
}
|
||||
.nav-tabs-custom > .nav-tabs > li.active > a,
|
||||
@@ -389,11 +366,8 @@
|
||||
|
||||
code,
|
||||
pre {
|
||||
padding: 2px 4px;
|
||||
font-size: 90%;
|
||||
color: #bec5cb;
|
||||
background-color: #353c42;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
/* Used in the Query Log table */
|
||||
@@ -459,7 +433,6 @@
|
||||
/* Used by the long-term pages */
|
||||
.daterangepicker {
|
||||
background-color: #3e464c;
|
||||
border-radius: 4px;
|
||||
border: 1px solid #353c42;
|
||||
}
|
||||
.daterangepicker .ranges li:hover {
|
||||
@@ -470,7 +443,6 @@
|
||||
}
|
||||
.daterangepicker .calendar-table {
|
||||
background-color: #3e464c;
|
||||
border-radius: 4px;
|
||||
border: 1px solid #353c42;
|
||||
}
|
||||
.daterangepicker td.off,
|
||||
@@ -537,7 +509,6 @@
|
||||
.panel-body,
|
||||
.panel-default > .panel-heading {
|
||||
background-color: #3e464c;
|
||||
border-radius: 4px;
|
||||
border: 1px solid #353c42;
|
||||
color: #bec5cb;
|
||||
}
|
||||
@@ -570,23 +541,10 @@
|
||||
background-image: linear-gradient(to right, #114100 0%, #525200 100%);
|
||||
}
|
||||
|
||||
.icheckbox_polaris,
|
||||
.icheckbox_futurico,
|
||||
.icheckbox_minimal-blue {
|
||||
margin-right: 10px;
|
||||
}
|
||||
.iradio_polaris,
|
||||
.iradio_futurico,
|
||||
.iradio_minimal-blue {
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
/* Overlay box with spinners as shown during data collection for graphs */
|
||||
.box .overlay,
|
||||
.overlay-wrapper .overlay {
|
||||
z-index: 50;
|
||||
background-color: rgba(53, 60, 66, 0.733);
|
||||
border-radius: 3px;
|
||||
}
|
||||
.box .overlay > .fa,
|
||||
.overlay-wrapper .overlay > .fa,
|
||||
@@ -596,7 +554,6 @@
|
||||
|
||||
.navbar-nav > .user-menu > .dropdown-menu > .user-footer {
|
||||
background-color: #353c42bb;
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.modal-content {
|
||||
@@ -625,36 +582,21 @@
|
||||
border-color: rgb(120, 127, 133);
|
||||
}
|
||||
|
||||
/*** Additional fixes For Pi.Alert UI ***/
|
||||
.small-box {
|
||||
border-radius: 10px;
|
||||
border-top: 0px;
|
||||
}
|
||||
.pa-small-box-aqua .inner {
|
||||
background-color: rgb(45,108,133);
|
||||
border-top-left-radius: 10px;
|
||||
border-top-right-radius: 10px;
|
||||
}
|
||||
.pa-small-box-green .inner {
|
||||
background-color: rgb(31,76,46);
|
||||
border-top-left-radius: 10px;
|
||||
border-top-right-radius: 10px;
|
||||
}
|
||||
.pa-small-box-yellow .inner {
|
||||
background-color: rgb(151,104,37);
|
||||
border-top-left-radius: 10px;
|
||||
border-top-right-radius: 10px;
|
||||
}
|
||||
.pa-small-box-red .inner {
|
||||
background-color: rgb(120,50,38);
|
||||
border-top-left-radius: 10px;
|
||||
border-top-right-radius: 10px;
|
||||
}
|
||||
.pa-small-box-gray .inner {
|
||||
background-color: #777;
|
||||
/* color: rgba(20,20,20,30%); */
|
||||
border-top-left-radius: 10px;
|
||||
border-top-right-radius: 10px;
|
||||
}
|
||||
.pa-small-box-gray .inner h3 {
|
||||
color: #bbb;
|
||||
@@ -693,15 +635,6 @@
|
||||
.db_tools_table_cell_b:nth-child(1) {background: #272c30}
|
||||
.db_tools_table_cell_b:nth-child(2) {background: #272c30}
|
||||
|
||||
.db_info_table {
|
||||
display: table;
|
||||
border-spacing: 0em;
|
||||
font-weight: 400;
|
||||
font-size: 15px;
|
||||
width: 100%;
|
||||
margin: auto;
|
||||
}
|
||||
|
||||
.nav-tabs-custom > .nav-tabs > li:hover > a, .nav-tabs-custom > .nav-tabs > li.active:hover > a {
|
||||
background-color: #272c30;
|
||||
color: #bec5cb;
|
||||
@@ -723,15 +656,6 @@
|
||||
color: white !important;
|
||||
}
|
||||
|
||||
/* remove white border that appears on mobile screen sizes */
|
||||
.box-body {
|
||||
border: 0px;
|
||||
}
|
||||
/* remove white border that appears on mobile screen sizes */
|
||||
.table-responsive {
|
||||
border: 0px;
|
||||
}
|
||||
|
||||
.login-page {
|
||||
background-color: transparent;
|
||||
}
|
||||
@@ -744,23 +668,6 @@
|
||||
color: #bec5cb;
|
||||
background-color: #272c30;
|
||||
}
|
||||
/* Add border radius to bottom of the status boxes*/
|
||||
.pa-small-box-footer {
|
||||
border-bottom-left-radius: 10px;
|
||||
border-bottom-right-radius: 10px;
|
||||
}
|
||||
|
||||
.small-box > .inner h3, .small-box > .inner p {
|
||||
margin-bottom: 0px;
|
||||
margin-left: 0px;
|
||||
}
|
||||
.small-box:hover .icon {
|
||||
font-size: 3em;
|
||||
}
|
||||
.small-box .icon {
|
||||
top: 0.01em;
|
||||
font-size: 3.25em;
|
||||
}
|
||||
.nax_semitransparent-panel{
|
||||
background-color: #000 !important;
|
||||
}
|
||||
|
||||
@@ -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');",
|
||||
@@ -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>
|
||||
|
||||
@@ -17,7 +17,7 @@ require_once $_SERVER["DOCUMENT_ROOT"] . "/php/templates/security.php"; ?>
|
||||
class="btn btn-default pa-btn pa-btn-delete"
|
||||
style="margin-left:0px;"
|
||||
id="btnDelete"
|
||||
onclick="askDeleteDevice()">
|
||||
onclick="askDeleteDeviceByMac()">
|
||||
<i class="fas fa-trash-alt"></i>
|
||||
<?= lang("DevDetail_button_Delete") ?>
|
||||
</button>
|
||||
@@ -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();
|
||||
@@ -423,7 +423,7 @@ function setDeviceData(direction = '', refreshCallback = '') {
|
||||
|
||||
// Build payload
|
||||
const payload = {
|
||||
devName: $('#NEWDEV_devName').val().replace(/'/g, "’"),
|
||||
devName: $('#NEWDEV_devName').val(),
|
||||
devOwner: $('#NEWDEV_devOwner').val().replace(/'/g, "’"),
|
||||
devType: $('#NEWDEV_devType').val().replace(/'/g, ""),
|
||||
devVendor: $('#NEWDEV_devVendor').val().replace(/'/g, "’"),
|
||||
@@ -432,7 +432,7 @@ function setDeviceData(direction = '', refreshCallback = '') {
|
||||
devFavorite: ($('#NEWDEV_devFavorite')[0].checked * 1),
|
||||
devGroup: $('#NEWDEV_devGroup').val().replace(/'/g, "’"),
|
||||
devLocation: $('#NEWDEV_devLocation').val().replace(/'/g, "’"),
|
||||
devComments: encodeSpecialChars($('#NEWDEV_devComments').val()),
|
||||
devComments: ($('#NEWDEV_devComments').val()),
|
||||
|
||||
devParentMAC: $('#NEWDEV_devParentMAC').val(),
|
||||
devParentPort: $('#NEWDEV_devParentPort').val(),
|
||||
@@ -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, ""),
|
||||
|
||||
@@ -479,7 +480,12 @@ function setDeviceData(direction = '', refreshCallback = '') {
|
||||
if (resp && resp.success) {
|
||||
showMessage(getString("Device_Saved_Success"));
|
||||
} else {
|
||||
showMessage(getString("Device_Saved_Unexpected"));
|
||||
|
||||
console.log(resp);
|
||||
|
||||
errorMessage = resp?.error;
|
||||
|
||||
showMessage(`${getString("Device_Saved_Unexpected")}: ${errorMessage}`, 5000, "modal_red");
|
||||
}
|
||||
|
||||
// Remove navigation prompt
|
||||
|
||||
@@ -116,7 +116,7 @@ function initializeEventsDatatable (eventsRows) {
|
||||
{
|
||||
targets: [0],
|
||||
'createdCell': function (td, cellData, rowData, row, col) {
|
||||
$(td).html(translateHTMLcodes(localizeTimestamp(cellData)));
|
||||
$(td).html(translateHTMLcodes((cellData)));
|
||||
}
|
||||
}
|
||||
],
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -87,6 +87,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 +145,9 @@
|
||||
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.
|
||||
|
||||
// Read parameters & Initialize components
|
||||
callAfterAppInitialized(main)
|
||||
showSpinner();
|
||||
@@ -512,49 +517,142 @@ 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) {
|
||||
// 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 — avoids AJAX spam on server-side tables.
|
||||
var label = getEtaLabel();
|
||||
if ($.fn.DataTable.isDataTable('#tableDevices')) {
|
||||
var dt = $('#tableDevices').DataTable();
|
||||
dt.settings()[0].oLanguage.sEmptyTable = buildEmptyDeviceTableMessage(label);
|
||||
if (dt.page.info().recordsTotal === 0) { dt.draw(false); }
|
||||
}
|
||||
|
||||
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,53 +717,15 @@ 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
|
||||
}
|
||||
@@ -717,49 +777,24 @@ function initializeDatatable (status) {
|
||||
|
||||
// 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 +822,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 +844,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 +868,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 +883,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 +894,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 +904,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 +922,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 +934,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 +945,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 +955,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 +965,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 +976,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 +998,19 @@ 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')),
|
||||
"lengthMenu": "<?= lang('Device_Tablelenght');?>",
|
||||
"search": "<?= lang('Device_Searchbox');?>: ",
|
||||
"paginate": {
|
||||
@@ -1037,7 +1068,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 +1114,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 +1135,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
|
||||
}
|
||||
});
|
||||
|
||||
@@ -1148,7 +1179,7 @@ function renderCustomProps(custProps, mac) {
|
||||
onClickEvent = `alert('Not implemented')`;
|
||||
break;
|
||||
case "delete_dev":
|
||||
onClickEvent = `askDelDevDTInline('${mac}')`;
|
||||
onClickEvent = `askDeleteDeviceByMac('${mac}')`;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
|
||||
210
front/index.php
@@ -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
@@ -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', '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");
|
||||
502
front/js/cache.js
Normal file
@@ -0,0 +1,502 @@
|
||||
/* -----------------------------------------------------------------------------
|
||||
* 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')) === "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.String_Key, langString.Language_Code), langString.String_Value);
|
||||
});
|
||||
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.String_Key, langString.Language_Code), langString.String_Value);
|
||||
});
|
||||
|
||||
// Handle successful completion of language processing
|
||||
handleSuccess('cacheStrings');
|
||||
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');
|
||||
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
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// Cache the devices as one JSON
|
||||
function cacheDevices()
|
||||
{
|
||||
return new Promise((resolve, reject) => {
|
||||
if(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
|
||||
@@ -12,42 +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","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);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -89,285 +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;
|
||||
}
|
||||
|
||||
return lang_code;
|
||||
}
|
||||
// cacheStrings, getString, getLangCode moved to cache.js
|
||||
|
||||
const tz = getSetting("TIMEZONE") || 'Europe/Berlin';
|
||||
const LOCALE = getSetting('UI_LOCALE') || 'en-GB';
|
||||
@@ -447,21 +166,36 @@ function localizeTimestamp(input) {
|
||||
return formatSafe(input, tz);
|
||||
|
||||
function formatSafe(str, tz) {
|
||||
const date = new Date(str);
|
||||
|
||||
// CHECK: Does the input string have timezone information?
|
||||
// - Ends with Z: "2026-02-11T11:37:02Z"
|
||||
// - Has GMT±offset: "Wed Feb 11 2026 12:34:12 GMT+1100 (...)"
|
||||
// - Has offset at end: "2026-02-11 11:37:02+11:00"
|
||||
// - Has timezone name in parentheses: "(Australian Eastern Daylight Time)"
|
||||
const hasOffset = /Z$/i.test(str.trim()) ||
|
||||
/GMT[+-]\d{2,4}/.test(str) ||
|
||||
/[+-]\d{2}:?\d{2}$/.test(str.trim()) ||
|
||||
/\([^)]+\)$/.test(str.trim());
|
||||
|
||||
// ⚠️ CRITICAL: All DB timestamps are stored in UTC without timezone markers.
|
||||
// If no offset is present, we must explicitly mark it as UTC by appending 'Z'
|
||||
// so JavaScript doesn't interpret it as local browser time.
|
||||
let isoStr = str.trim();
|
||||
if (!hasOffset) {
|
||||
// Ensure proper ISO format before appending Z
|
||||
// Replace space with 'T' if needed: "2026-02-11 11:37:02" → "2026-02-11T11:37:02Z"
|
||||
isoStr = isoStr.trim().replace(/^(\d{4}-\d{2}-\d{2}) (\d{2}:\d{2}:\d{2})$/, '$1T$2') + 'Z';
|
||||
}
|
||||
|
||||
const date = new Date(isoStr);
|
||||
if (!isFinite(date)) {
|
||||
console.error(`ERROR: Couldn't parse date: '${str}' with TIMEZONE ${tz}`);
|
||||
return 'Failed conversion';
|
||||
}
|
||||
|
||||
// CHECK: Does the input string have an offset (e.g., +11:00 or Z)?
|
||||
// If it does, and we apply a 'tz' again, we double-shift.
|
||||
const hasOffset = /[Z|[+-]\d{2}:?\d{2}]$/.test(str.trim());
|
||||
|
||||
return new Intl.DateTimeFormat(LOCALE, {
|
||||
// If it has an offset, we display it as-is (UTC mode in Intl
|
||||
// effectively means "don't add more hours").
|
||||
// If no offset, apply your variable 'tz'.
|
||||
timeZone: hasOffset ? 'UTC' : tz,
|
||||
// Convert from UTC to user's configured timezone
|
||||
timeZone: tz,
|
||||
year: 'numeric', month: '2-digit', day: '2-digit',
|
||||
hour: '2-digit', minute: '2-digit', second: '2-digit',
|
||||
hour12: false
|
||||
@@ -696,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) {
|
||||
@@ -1135,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)
|
||||
@@ -1347,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)
|
||||
@@ -1526,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(',');
|
||||
@@ -1594,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 );
|
||||
@@ -1620,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
@@ -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");
|
||||
@@ -1,21 +1,5 @@
|
||||
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
function askDeleteDevice() {
|
||||
|
||||
mac = getMac()
|
||||
|
||||
// Ask delete device
|
||||
showModalWarning(
|
||||
getString("DevDetail_button_Delete"),
|
||||
getString("DevDetail_button_Delete_ask"),
|
||||
getString('Gen_Cancel'),
|
||||
getString('Gen_Delete'),
|
||||
'deleteDevice');
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
function askDelDevDTInline(mac) {
|
||||
function askDeleteDeviceByMac(mac) {
|
||||
|
||||
// only try getting mac from URL if not supplied - used in inline buttons on in the my devices listing pages
|
||||
if(isEmpty(mac))
|
||||
@@ -31,34 +15,9 @@ function askDelDevDTInline(mac) {
|
||||
() => deleteDeviceByMac(mac))
|
||||
}
|
||||
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
function deleteDevice() {
|
||||
// Check MAC
|
||||
mac = getMac()
|
||||
|
||||
const apiBase = getApiBase();
|
||||
const apiToken = getSetting("API_TOKEN");
|
||||
const url = `${apiBase}/device/${mac}/delete`;
|
||||
|
||||
$.ajax({
|
||||
url,
|
||||
method: "DELETE",
|
||||
headers: { "Authorization": `Bearer ${apiToken}` },
|
||||
success: function(response) {
|
||||
showMessage(response.success ? "Device deleted successfully" : (response.error || "Unknown error"));
|
||||
updateApi("devices,appevents");
|
||||
},
|
||||
error: function(xhr, status, error) {
|
||||
console.error("Error deleting device:", status, error);
|
||||
showMessage("Error: " + (xhr.responseJSON?.error || error));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
function deleteDeviceByMac(mac) {
|
||||
// only try getting mac from URL if not supplied - used in inline buttons on in teh my devices listing pages
|
||||
// only try getting mac from URL if not supplied - used in inline buttons on in the my devices listing pages
|
||||
if(isEmpty(mac))
|
||||
{
|
||||
mac = getMac()
|
||||
|
||||
266
front/js/network-api.js
Normal 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
@@ -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
@@ -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
@@ -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
@@ -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' ) ? "   " : 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");
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
@@ -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>
|
||||
|
||||
23
front/lib/treeviz/treeviz.iife.js
Normal file
0
front/lib/treeviz/bundle.js → front/lib/treeviz/treeviz.iife.old.js
Executable file → Normal file
48
front/llms.txt
Normal 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
|
||||
@@ -373,7 +373,10 @@ function deleteAllDevices()
|
||||
url,
|
||||
method: "DELETE",
|
||||
headers: { "Authorization": `Bearer ${apiToken}` },
|
||||
data: JSON.stringify({ macs: null }),
|
||||
data: JSON.stringify({
|
||||
macs: [],
|
||||
confirm_delete_all: true
|
||||
}),
|
||||
contentType: "application/json",
|
||||
success: function(response) {
|
||||
showMessage(response.success ? "All devices deleted successfully" : (response.error || "Unknown error"));
|
||||
@@ -775,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);
|
||||
}
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
@@ -892,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);
|
||||
}
|
||||
|
||||
@@ -240,8 +240,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)) {
|
||||
|
||||
1036
front/network.php
@@ -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>';
|
||||
}
|
||||
|
||||
|
||||
@@ -27,8 +27,8 @@ function initOnlineHistoryGraph() {
|
||||
var archivedCounts = [];
|
||||
|
||||
res.data.forEach(function(entry) {
|
||||
var dateObj = new Date(entry.Scan_Date);
|
||||
var formattedTime = dateObj.toLocaleTimeString([], {hour: '2-digit', minute: '2-digit', hour12: false});
|
||||
|
||||
var formattedTime = localizeTimestamp(entry.Scan_Date).slice(11, 17);
|
||||
|
||||
timeStamps.push(formattedTime);
|
||||
onlineCounts.push(entry.Online_Devices);
|
||||
|
||||
38
front/php/server/app_config.php
Normal 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,
|
||||
]);
|
||||
@@ -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";
|
||||
|
||||
|
||||