Compare commits

..

94 Commits

Author SHA1 Message Date
jokob-sk
2b2ae516da weblate
Some checks failed
Code checks / check-url-paths (push) Has been cancelled
docker / docker_dev (push) Has been cancelled
Deploy MkDocs / deploy (push) Has been cancelled
Signed-off-by: jokob-sk <jokob.sk@gmail.com>
2025-09-09 07:47:11 +10:00
jokob-sk
2df7d143d3 Merge branch 'main' of https://github.com/jokob-sk/NetAlertX 2025-09-09 07:46:50 +10:00
jokob-sk
1688d029b9 docs
Signed-off-by: jokob-sk <jokob.sk@gmail.com>
2025-09-09 07:38:15 +10:00
anton garcias
6d8f451be1 Translated using Weblate (Catalan)
Some checks failed
Code checks / check-url-paths (push) Has been cancelled
docker / docker_dev (push) Has been cancelled
Deploy MkDocs / deploy (push) Has been cancelled
Currently translated at 100.0% (761 of 761 strings)

Translation: NetAlertX/core
Translate-URL: https://hosted.weblate.org/projects/pialert/core/ca/
2025-09-08 19:01:55 +02:00
jokob-sk
840e1e50a9 docs
Signed-off-by: jokob-sk <jokob.sk@gmail.com>
2025-09-08 21:17:55 +10:00
jokob-sk
164fe504a4 weblate
Signed-off-by: jokob-sk <jokob.sk@gmail.com>
2025-09-08 21:17:45 +10:00
jokob-sk
9040e49e16 sync plugin
Some checks failed
Code checks / check-url-paths (push) Has been cancelled
docker / docker_dev (push) Has been cancelled
Deploy MkDocs / deploy (push) Has been cancelled
Signed-off-by: jokob-sk <jokob.sk@gmail.com>
2025-09-08 08:14:42 +10:00
jokob-sk
629736ad39 Merge branch 'main' of https://github.com/jokob-sk/NetAlertX 2025-09-08 08:12:04 +10:00
jokob-sk
ebc41ada45 logger
Signed-off-by: jokob-sk <jokob.sk@gmail.com>
2025-09-08 08:11:33 +10:00
jokob-sk
4fea786e16 sync plugin
Signed-off-by: jokob-sk <jokob.sk@gmail.com>
2025-09-08 08:11:23 +10:00
Jokob @NetAlertX
0edd20c82c Merge pull request #1155 from FlyingToto/main
adding example 5 of docker compose (3rd try!)
2025-09-08 07:05:10 +10:00
Jokob @NetAlertX
296dd0d0df Merge pull request #1165 from adamoutler/patch-1
Enhance in-app tooltips for clarity
2025-09-08 07:04:33 +10:00
Adam Outler
f2151cd9e8 Enhance in-app tooltips for clarity 2025-09-07 14:47:04 -04:00
Hosted Weblate
60876b14ce Merge branch 'origin/main' into Weblate.
Some checks failed
Deploy MkDocs / deploy (push) Has been cancelled
Code checks / check-url-paths (push) Has been cancelled
docker / docker_dev (push) Has been cancelled
2025-09-04 23:58:20 +02:00
ssantos
9231ba742c Translated using Weblate (Portuguese (Portugal))
Currently translated at 54.5% (415 of 761 strings)

Translation: NetAlertX/core
Translate-URL: https://hosted.weblate.org/projects/pialert/core/pt_PT/
2025-09-04 23:58:18 +02:00
jokob-sk
8a538102da weblate
Signed-off-by: jokob-sk <jokob.sk@gmail.com>
2025-09-05 07:57:57 +10:00
jokob-sk
31f901da35 Merge branch 'main' of https://github.com/jokob-sk/NetAlertX 2025-09-05 07:57:25 +10:00
jokob-sk
c5b731fcb2 weblate
Signed-off-by: jokob-sk <jokob.sk@gmail.com>
2025-09-05 07:56:57 +10:00
jokob-sk
b2c7945513 docs
Signed-off-by: jokob-sk <jokob.sk@gmail.com>
2025-09-05 07:56:42 +10:00
suibian
6bf5c1f535 Translated using Weblate (Chinese (Simplified Han script))
Some checks failed
Code checks / check-url-paths (push) Has been cancelled
docker / docker_dev (push) Has been cancelled
Deploy MkDocs / deploy (push) Has been cancelled
Currently translated at 100.0% (761 of 761 strings)

Translation: NetAlertX/core
Translate-URL: https://hosted.weblate.org/projects/pialert/core/zh_Hans/
2025-09-03 12:38:56 +02:00
martinkuck
3da50fe83d Translated using Weblate (German)
Currently translated at 81.2% (618 of 761 strings)

Translation: NetAlertX/core
Translate-URL: https://hosted.weblate.org/projects/pialert/core/de/
2025-09-03 12:38:56 +02:00
Jokob @NetAlertX
b46bdb9b60 Merge pull request #1156 from ingoratsdorf/contrib
Some checks failed
Code checks / check-url-paths (push) Has been cancelled
docker / docker_dev (push) Has been cancelled
Deploy MkDocs / deploy (push) Has been cancelled
Adding secondary cache to settings
2025-09-03 07:02:11 +10:00
Ingo Ratsdorf
00c7bb65e1 Update server/helper.py
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
2025-09-03 07:10:26 +12:00
Ingo Ratsdorf
9946f9affd Merge branch 'jokob-sk:main' into contrib 2025-09-02 20:43:25 +12:00
anton garcias
46a11b1cca Translated using Weblate (Catalan)
Some checks failed
Code checks / check-url-paths (push) Has been cancelled
docker / docker_dev (push) Has been cancelled
Deploy MkDocs / deploy (push) Has been cancelled
Currently translated at 93.0% (708 of 761 strings)

Translation: NetAlertX/core
Translate-URL: https://hosted.weblate.org/projects/pialert/core/ca/
2025-09-02 10:42:15 +02:00
Ingo Ratsdorf
8a003ad805 Merge branch 'jokob-sk:main' into contrib 2025-09-02 20:41:59 +12:00
Jokob @NetAlertX
7dd860b2ab Merge branch 'main' into main 2025-09-02 15:22:04 +10:00
Jokob @NetAlertX
a9d7ca8809 Merge pull request #1154 from FlyingToto/patch-2
Some checks failed
Code checks / check-url-paths (push) Has been cancelled
docker / docker_dev (push) Has been cancelled
Deploy MkDocs / deploy (push) Has been cancelled
added a variant of example 2 as 5...
2025-09-02 15:16:02 +10:00
Ingo Ratsdorf
5695f4f3e7 Adding secondary cache to settings
Caching get_setting_value independent from what backend is used.
2025-09-02 14:48:12 +12:00
FlyingToto
1d74398337 adding address and uid/gid
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
2025-09-01 22:13:02 -04:00
FlyingToto
e8f17346ff 3rd attempt to add example 5 of docker compose! 2025-09-01 22:02:25 -04:00
FlyingToto
bb1e00301c fixing typo
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
2025-09-01 18:29:39 -04:00
FlyingToto
883786ec91 added a variant of example 2 as 5... 2025-09-01 17:39:58 -04:00
jokob-sk
3a023a675f CPU optimization work 5 #1144
Signed-off-by: jokob-sk <jokob.sk@gmail.com>
2025-09-01 09:13:13 +10:00
jokob-sk
8c895864da CPU optimizartion work 4 #1144
Some checks failed
Code checks / check-url-paths (push) Has been cancelled
docker / docker_dev (push) Has been cancelled
Deploy MkDocs / deploy (push) Has been cancelled
Signed-off-by: jokob-sk <jokob.sk@gmail.com>
2025-09-01 08:45:41 +10:00
jokob-sk
90474a6b92 Merge branch 'main' of https://github.com/jokob-sk/NetAlertX 2025-09-01 08:33:38 +10:00
Jokob @NetAlertX
f7cf8a0b1d Merge pull request #1151 from ingoratsdorf/contrib
Added cache to get_settings
2025-09-01 08:33:29 +10:00
jokob-sk
98fdccb58f CPU optimizartion work 2 #1144
Signed-off-by: jokob-sk <jokob.sk@gmail.com>
2025-09-01 08:33:14 +10:00
jokob-sk
6f606f34d1 docs
Some checks failed
Code checks / check-url-paths (push) Has been cancelled
docker / docker_dev (push) Has been cancelled
Deploy MkDocs / deploy (push) Has been cancelled
Signed-off-by: jokob-sk <jokob.sk@gmail.com>
2025-08-31 10:23:31 +10:00
jokob-sk
fd3f1fc929 api layer v0.3.2 - /settings
Signed-off-by: jokob-sk <jokob.sk@gmail.com>
2025-08-31 09:54:56 +10:00
Ingo Ratsdorf
36ea3e62fd Added cache to get_settings
The settings file  is read about 30 times per second and parsed from json. Cache function added for now.
2025-08-30 21:35:15 +12:00
jokob-sk
7c9b37d827 lang
Some checks failed
Code checks / check-url-paths (push) Has been cancelled
docker / docker_dev (push) Has been cancelled
Deploy MkDocs / deploy (push) Has been cancelled
Signed-off-by: jokob-sk <jokob.sk@gmail.com>
2025-08-30 08:23:35 +10:00
jokob-sk
3fc0787b84 docs
Signed-off-by: jokob-sk <jokob.sk@gmail.com>
2025-08-30 08:23:25 +10:00
jokob-sk
5ba50f6d80 Merge branch 'main' of https://github.com/jokob-sk/NetAlertX
Some checks failed
Code checks / check-url-paths (push) Has been cancelled
docker / docker_dev (push) Has been cancelled
Deploy MkDocs / deploy (push) Has been cancelled
2025-08-29 08:09:39 +10:00
jokob-sk
c0c685c561 FE code disclaimers
Signed-off-by: jokob-sk <jokob.sk@gmail.com>
2025-08-29 08:09:29 +10:00
Artyom Rybakov
64a0fd0446 Translated using Weblate (Russian)
Currently translated at 100.0% (761 of 761 strings)

Translation: NetAlertX/core
Translate-URL: https://hosted.weblate.org/projects/pialert/core/ru/
2025-08-28 08:03:31 +02:00
jokob-sk
b1b67c268f api layer v0.3.1 - /dbquery
Some checks failed
Code checks / check-url-paths (push) Has been cancelled
docker / docker_dev (push) Has been cancelled
Deploy MkDocs / deploy (push) Has been cancelled
Signed-off-by: jokob-sk <jokob-sk@gmail.com>
2025-08-28 08:12:23 +10:00
jokob-sk
ae12195439 localizeTimestamp 2 #1147
Some checks failed
Code checks / check-url-paths (push) Has been cancelled
docker / docker_dev (push) Has been cancelled
Deploy MkDocs / deploy (push) Has been cancelled
Signed-off-by: jokob-sk <jokob-sk@gmail.com>
2025-08-27 07:34:43 +10:00
jokob-sk
3106b39566 CPU optimizartion work 2 #1144
Some checks failed
Deploy MkDocs / deploy (push) Has been cancelled
Code checks / check-url-paths (push) Has been cancelled
docker / docker_dev (push) Has been cancelled
2025-08-26 08:26:55 +10:00
jokob-sk
d88aa9d6eb FE more defensive localizeTimestamp #1147 2025-08-26 07:33:11 +10:00
jokob-sk
9f9f2ff58c docs 2025-08-25 18:24:28 +10:00
jokob-sk
ce887968b7 docs 2025-08-25 18:19:02 +10:00
jokob-sk
40e9fbdb3f docs
Some checks failed
Deploy MkDocs / deploy (push) Has been cancelled
Code checks / check-url-paths (push) Has been cancelled
docker / docker_dev (push) Has been cancelled
2025-08-24 13:12:33 +10:00
jokob-sk
3227cbbfa4 docs 2025-08-24 12:59:58 +10:00
jokob-sk
df9a17ed85 docs
Some checks failed
Code checks / check-url-paths (push) Has been cancelled
docker / docker_dev (push) Has been cancelled
Deploy MkDocs / deploy (push) Has been cancelled
2025-08-24 12:57:36 +10:00
jokob-sk
3ad7b59c84 docs 2025-08-24 10:05:41 +10:00
jokob-sk
b94da568a9 CPU optimizartion work #1144 2025-08-24 09:35:25 +10:00
jokob-sk
0146ae7c30 FE 2025-08-24 09:21:13 +10:00
jokob-sk
afbcf5985f SMTP_SUBJECT #1146 2025-08-24 08:45:32 +10:00
jokob-sk
af879ec84d graphql fix 2025-08-23 08:25:09 +10:00
jokob-sk
f78c84d9a8 api layer v0.3 - /events /sessions work
Some checks failed
Code checks / check-url-paths (push) Has been cancelled
docker / docker_dev (push) Has been cancelled
Deploy MkDocs / deploy (push) Has been cancelled
2025-08-21 22:36:22 +10:00
jokob-sk
2d11d3dd3e api layer v0.2.6 - /events work 2025-08-21 21:16:34 +10:00
jokob-sk
39c556576c FE - graphql response wrap into data 2025-08-21 15:51:58 +10:00
jokob-sk
73fd094cfc api layer v0.2.5 - graphql standardization 2025-08-21 15:33:32 +10:00
jokob-sk
cbf2cd0ee8 FE 2025-08-21 15:15:45 +10:00
jokob-sk
915bb523d6 api layer v0.2.5 - /sessions + graphql tests 2025-08-21 15:10:47 +10:00
jokob-sk
3dc87d2adb FE 2025-08-20 08:59:42 +10:00
jokob-sk
9155303674 api layer v0.2.4 - /nettools/speedtest endpoint 2025-08-20 08:58:34 +10:00
jokob-sk
0777824d96 FE 2025-08-20 08:50:35 +10:00
jokob-sk
b170ca3e18 api layer v0.2.4 - /nettools/traceroute endpoint 2025-08-20 08:49:34 +10:00
jokob-sk
5fd30fe3c8 FE
Some checks failed
Deploy MkDocs / deploy (push) Has been cancelled
Code checks / check-url-paths (push) Has been cancelled
docker / docker_dev (push) Has been cancelled
2025-08-20 08:41:38 +10:00
jokob-sk
2fa181ffbc api layer v0.2.4 - /nettools endpoint 2025-08-20 08:40:14 +10:00
jokob-sk
a2bccdfb8e FE 2025-08-20 08:11:56 +10:00
jokob-sk
f3b159116f Merge branch 'main' of https://github.com/jokob-sk/NetAlertX 2025-08-20 08:11:05 +10:00
jokob-sk
03b9a9cf0d api layer v0.2.3 - /device(s) endpoints work 2025-08-20 08:10:55 +10:00
Jokob @NetAlertX
bf2fae6e1a Merge pull request #1140 from cvc90/Fix-Relative-URL-in-report.php
Some checks failed
Code checks / check-url-paths (push) Has been cancelled
docker / docker_dev (push) Has been cancelled
Deploy MkDocs / deploy (push) Has been cancelled
Changing absolute path URL to relative path URL in report.php
2025-08-19 22:19:53 +10:00
Carlos V.
086fa54035 Update report.php
Change static route to relative route in URL for proper proxy operation
2025-08-19 13:07:58 +02:00
jokob-sk
962bbaa5a1 api layer v0.2.2 - CSV import/export, refactor
Some checks failed
Code checks / check-url-paths (push) Has been cancelled
docker / docker_dev (push) Has been cancelled
Deploy MkDocs / deploy (push) Has been cancelled
2025-08-19 07:56:54 +10:00
jokob-sk
9c71a8ecab api layer v0.2.1 - /events /history 2025-08-16 17:19:14 +10:00
jokob-sk
deff5a4ed0 api layer v0.2 - /devices
Some checks failed
Code checks / check-url-paths (push) Has been cancelled
docker / docker_dev (push) Has been cancelled
Deploy MkDocs / deploy (push) Has been cancelled
2025-08-16 16:43:15 +10:00
jokob-sk
e10c1c9c8d Added pt_pt language file
Some checks failed
Code checks / check-url-paths (push) Has been cancelled
docker / docker_dev (push) Has been cancelled
Deploy MkDocs / deploy (push) Has been cancelled
2025-08-16 08:18:23 +10:00
jokob-sk
b155fe2b06 api layer v0.1
Some checks failed
Code checks / check-url-paths (push) Has been cancelled
docker / docker_dev (push) Has been cancelled
Deploy MkDocs / deploy (push) Has been cancelled
2025-08-15 08:04:02 +10:00
jokob-sk
840bfe32d2 sync plugin endpoint refactor 2025-08-14 14:28:10 +10:00
jokob-sk
f33ef9861b css fixes, CurrentScan removed mac uniqueness check
Some checks failed
Deploy MkDocs / deploy (push) Has been cancelled
Code checks / check-url-paths (push) Has been cancelled
docker / docker_dev (push) Has been cancelled
2025-08-13 08:22:30 +10:00
jokob-sk
cbe71cc203 UNIFIAPI v0.5, css fixes
Some checks failed
Code checks / check-url-paths (push) Has been cancelled
docker / docker_dev (push) Has been cancelled
Deploy MkDocs / deploy (push) Has been cancelled
2025-08-13 06:48:36 +10:00
jokob-sk
beaf8131ae UNIFIAPI v0.4
Some checks failed
Code checks / check-url-paths (push) Has been cancelled
docker / docker_dev (push) Has been cancelled
Deploy MkDocs / deploy (push) Has been cancelled
2025-08-11 21:39:51 +10:00
jokob-sk
99bfbb56de better check for available device #1132 2025-08-11 19:58:24 +10:00
jokob-sk
e73c8e830a better check for available device #1132 2025-08-11 19:52:16 +10:00
jokob-sk
1c4e6c7e38 UNIFIAPI v0.3 FE setings done 2025-08-11 15:00:22 +10:00
jokob-sk
1319c3380d UNIFIAPI v0.3
Some checks failed
Code checks / check-url-paths (push) Has been cancelled
docker / docker_dev (push) Has been cancelled
Deploy MkDocs / deploy (push) Has been cancelled
2025-08-10 21:24:17 +10:00
jokob-sk
dce8c34064 docs, rewrite docker image 2025-08-10 20:22:43 +10:00
jokob-sk
9502ee0cd0 UNIFIAPI v0.2, not ofund mac handling #1132 2025-08-10 20:08:09 +10:00
jokob-sk
8eb4ffe3ed logging
Some checks failed
Code checks / check-url-paths (push) Has been cancelled
docker / docker_dev (push) Has been cancelled
Deploy MkDocs / deploy (push) Has been cancelled
2025-08-08 07:34:23 +10:00
jokob-sk
4be59807e5 docs, UNIFIAPI v0.1 2025-08-07 16:41:40 +10:00
112 changed files with 9184 additions and 1398 deletions

2
.github/FUNDING.yml vendored
View File

@@ -1,3 +1,3 @@
github: jokob-sk
patreon: 84385063
patreon: netalertx
buy_me_a_coffee: jokobsk

81
.github/workflows/docker_rewrite.yml vendored Executable file
View File

@@ -0,0 +1,81 @@
name: docker
on:
push:
branches:
- rewrite
tags:
- '*.*.*'
pull_request:
branches:
- rewrite
jobs:
docker_rewrite:
runs-on: ubuntu-latest
timeout-minutes: 30
permissions:
contents: read
packages: write
if: >
contains(github.event.head_commit.message, 'PUSHPROD') != 'True' &&
github.repository == 'jokob-sk/NetAlertX'
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Set up dynamic build ARGs
id: getargs
run: echo "version=$(cat ./stable/VERSION)" >> $GITHUB_OUTPUT
- name: Get release version
id: get_version
run: echo "version=Dev" >> $GITHUB_OUTPUT
- name: Create .VERSION file
run: echo "${{ steps.get_version.outputs.version }}" >> .VERSION
- name: Docker meta
id: meta
uses: docker/metadata-action@v4
with:
images: |
ghcr.io/jokob-sk/netalertx-dev-rewrite
jokobsk/netalertx-dev-rewrite
tags: |
type=raw,value=latest
type=ref,event=branch
type=ref,event=pr
type=semver,pattern={{version}}
type=semver,pattern={{major}}.{{minor}}
type=semver,pattern={{major}}
type=sha
- name: Log in to Github Container Registry (GHCR)
uses: docker/login-action@v3
with:
registry: ghcr.io
username: jokob-sk
password: ${{ secrets.GITHUB_TOKEN }}
- name: Log in to DockerHub
if: github.event_name != 'pull_request'
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Build and push
uses: docker/build-push-action@v3
with:
context: .
platforms: linux/amd64,linux/arm64,linux/arm/v7,linux/arm/v6
push: ${{ github.event_name != 'pull_request' }}
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}

View File

@@ -13,7 +13,7 @@ ENV PATH="/opt/venv/bin:$PATH"
COPY . ${INSTALL_DIR}/
RUN pip install openwrt-luci-rpc asusrouter asyncio aiohttp graphene flask flask-cors tplink-omada-client wakeonlan pycryptodome requests paho-mqtt scapy cron-converter pytz json2table dhcp-leases pyunifi speedtest-cli chardet python-nmap dnspython librouteros yattag git+https://github.com/foreign-sub/aiofreepybox.git \
RUN pip install openwrt-luci-rpc asusrouter asyncio aiohttp graphene flask flask-cors unifi-sm-api tplink-omada-client wakeonlan pycryptodome requests paho-mqtt scapy cron-converter pytz json2table dhcp-leases pyunifi speedtest-cli chardet python-nmap dnspython librouteros yattag git+https://github.com/foreign-sub/aiofreepybox.git \
&& bash -c "find ${INSTALL_DIR} -type d -exec chmod 750 {} \;" \
&& bash -c "find ${INSTALL_DIR} -type f -exec chmod 640 {} \;" \
&& bash -c "find ${INSTALL_DIR} -type f \( -name '*.sh' -o -name '*.py' -o -name 'speedtest-cli' \) -exec chmod 750 {} \;"

View File

@@ -43,7 +43,7 @@ RUN phpenmod -v 8.2 sqlite3
RUN apt-get install -y python3-venv
RUN python3 -m venv myenv
RUN /bin/bash -c "source myenv/bin/activate && update-alternatives --install /usr/bin/python python /usr/bin/python3 10 && pip3 install openwrt-luci-rpc asusrouter asyncio aiohttp graphene flask flask-cors tplink-omada-client wakeonlan pycryptodome requests paho-mqtt scapy cron-converter pytz json2table dhcp-leases pyunifi speedtest-cli chardet python-nmap dnspython librouteros yattag "
RUN /bin/bash -c "source myenv/bin/activate && update-alternatives --install /usr/bin/python python /usr/bin/python3 10 && pip3 install openwrt-luci-rpc asusrouter asyncio aiohttp graphene flask flask-cors unifi-sm-api tplink-omada-client wakeonlan pycryptodome requests paho-mqtt scapy cron-converter pytz json2table dhcp-leases pyunifi speedtest-cli chardet python-nmap dnspython librouteros yattag "
# Create a buildtimestamp.txt to later check if a new version was released
RUN date +%s > ${INSTALL_DIR}/front/buildtimestamp.txt

View File

@@ -1,6 +1,6 @@
# API endpoints
# NetAlertX API Documentation
NetAlertX comes with a couple of API endpoints. All requests need to be authorized (executed in a logged in browser session) or you have to pass the value of the `API_TOKEN` settings as authorization bearer, for example:
This API provides programmatic access to **devices, events, sessions, metrics, network tools, and sync** in NetAlertX. It is implemented as a **REST and GraphQL server**. All requests require authentication via **API Token** (`API_TOKEN` setting) unless explicitly noted. For example, to authorize a GraphQL request, you need to use a `Authorization: Bearer API_TOKEN` header as per example below:
```graphql
curl 'http://host:GRAPHQL_PORT/graphql' \
@@ -21,347 +21,59 @@ curl 'http://host:GRAPHQL_PORT/graphql' \
}'
```
## API Endpoint: GraphQL
The API server runs on `0.0.0.0:<graphql_port>` with **CORS enabled** for all main endpoints.
- Endpoint URL: `php/server/query_graphql.php`
- Host: `same as front end (web ui)`
- Port: `20212` or as defined by the `GRAPHQL_PORT` setting
---
### Example Query to Fetch Devices
## Authentication
First, let's define the GraphQL query to fetch devices with pagination and sorting options.
All endpoints require an API token provided in the HTTP headers:
```graphql
query GetDevices($options: PageQueryOptionsInput) {
devices(options: $options) {
devices {
rowid
devMac
devName
devOwner
devType
devVendor
devLastConnection
devStatus
}
count
}
}
```http
Authorization: Bearer <API_TOKEN>
```
See also: [Debugging GraphQL issues](./DEBUG_GRAPHQL.md)
### `curl` Command
You can use the following `curl` command to execute the query.
```sh
curl 'http://host:GRAPHQL_PORT/graphql' -X POST -H 'Authorization: Bearer API_TOKEN' -H 'Content-Type: application/json' --data '{
"query": "query GetDevices($options: PageQueryOptionsInput) { devices(options: $options) { devices { rowid devMac devName devOwner devType devVendor devLastConnection devStatus } count } }",
"variables": {
"options": {
"page": 1,
"limit": 10,
"sort": [{ "field": "devName", "order": "asc" }],
"search": "",
"status": "connected"
}
}
}'
```
### Explanation:
1. **GraphQL Query**:
- The `query` parameter contains the GraphQL query as a string.
- The `variables` parameter contains the input variables for the query.
2. **Query Variables**:
- `page`: Specifies the page number of results to fetch.
- `limit`: Specifies the number of results per page.
- `sort`: Specifies the sorting options, with `field` being the field to sort by and `order` being the sort order (`asc` for ascending or `desc` for descending).
- `search`: A search term to filter the devices.
- `status`: The status filter to apply (valid values are `my_devices` (determined by the `UI_MY_DEVICES` setting), `connected`, `favorites`, `new`, `down`, `archived`, `offline`).
3. **`curl` Command**:
- The `-X POST` option specifies that we are making a POST request.
- The `-H "Content-Type: application/json"` option sets the content type of the request to JSON.
- The `-d` option provides the request payload, which includes the GraphQL query and variables.
### Sample Response
The response will be in JSON format, similar to the following:
If the token is missing or invalid, the server will return:
```json
{
"data": {
"devices": {
"devices": [
{
"rowid": 1,
"devMac": "00:11:22:33:44:55",
"devName": "Device 1",
"devOwner": "Owner 1",
"devType": "Type 1",
"devVendor": "Vendor 1",
"devLastConnection": "2025-01-01T00:00:00Z",
"devStatus": "connected"
},
{
"rowid": 2,
"devMac": "66:77:88:99:AA:BB",
"devName": "Device 2",
"devOwner": "Owner 2",
"devType": "Type 2",
"devVendor": "Vendor 2",
"devLastConnection": "2025-01-02T00:00:00Z",
"devStatus": "connected"
}
],
"count": 2
}
}
}
```
## API Endpoint: JSON files
This API endpoint retrieves static files, that are periodically updated.
- Endpoint URL: `php/server/query_json.php?file=<file name>`
- Host: `same as front end (web ui)`
- Port: `20211` or as defined by the $PORT docker environment variable (same as the port for the web ui)
### When are the endpoints updated
The endpoints are updated when objects in the API endpoints are changed.
### Location of the endpoints
In the container, these files are located under the `/app/api/` folder. You can access them via the `/php/server/query_json.php?file=user_notifications.json` endpoint.
### Available endpoints
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)). |
| `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. |
| `table_plugins_objects.json` | The content of the plugins_objects table. Find more info on the [Plugin system here](https://github.com/jokob-sk/NetAlertX/tree/main/docs/PLUGINS.md)|
| `language_strings.json` | The content of the language_strings table, which in turn is loaded from the plugins `config.json` definitions. |
| `table_custom_endpoint.json` | A custom endpoint generated by the SQL query specified by the `API_CUSTOM_SQL` setting. |
| `table_settings.json` | The content of the settings table. |
| `app_state.json` | Contains the current application state. |
### JSON Data format
The endpoints starting with the `table_` prefix contain most, if not all, data contained in the corresponding database table. The common format for those is:
```JSON
{
"data": [
{
"db_column_name": "data",
"db_column_name2": "data2"
},
{
"db_column_name": "data3",
"db_column_name2": "data4"
}
]
}
```
Example JSON of the `table_devices.json` endpoint with two Devices (database rows):
```JSON
{
"data": [
{
"devMac": "Internet",
"devName": "Net - Huawei",
"devType": "Router",
"devVendor": null,
"devGroup": "Always on",
"devFirstConnection": "2021-01-01 00:00:00",
"devLastConnection": "2021-01-28 22:22:11",
"devLastIP": "192.168.1.24",
"devStaticIP": 0,
"devPresentLastScan": 1,
"devLastNotification": "2023-01-28 22:22:28.998715",
"devIsNew": 0,
"devParentMAC": "",
"devParentPort": "",
"devIcon": "globe"
},
{
"devMac": "a4:8f:ff:aa:ba:1f",
"devName": "Net - USG",
"devType": "Firewall",
"devVendor": "Ubiquiti Inc",
"devGroup": "",
"devFirstConnection": "2021-02-12 22:05:00",
"devLastConnection": "2021-07-17 15:40:00",
"devLastIP": "192.168.1.1",
"devStaticIP": 1,
"devPresentLastScan": 1,
"devLastNotification": "2021-07-17 15:40:10.667717",
"devIsNew": 0,
"devParentMAC": "Internet",
"devParentPort": 1,
"devIcon": "shield-halved"
}
]
}
```
## API Endpoint: Prometheus Exporter
* **Endpoint URL**: `/metrics`
* **Host**: (where NetAlertX exporter is running)
* **Port**: as configured in the `GRAPHQL_PORT` setting (`20212` by default)
---
### Example Output of the `/metrics` Endpoint
Below is a representative snippet of the metrics you may find when querying the `/metrics` endpoint for `netalertx`. It includes both aggregate counters and `device_status` labels per device.
```
netalertx_connected_devices 31
netalertx_offline_devices 54
netalertx_down_devices 0
netalertx_new_devices 0
netalertx_archived_devices 31
netalertx_favorite_devices 2
netalertx_my_devices 54
netalertx_device_status{device="Net - Huawei", mac="Internet", ip="1111.111.111.111", vendor="None", first_connection="2021-01-01 00:00:00", last_connection="2025-08-04 17:57:00", dev_type="Router", device_status="Online"} 1
netalertx_device_status{device="Net - USG", mac="74:ac:74:ac:74:ac", ip="192.168.1.1", vendor="Ubiquiti Networks Inc.", first_connection="2022-02-12 22:05:00", last_connection="2025-06-07 08:16:49", dev_type="Firewall", device_status="Archived"} 1
netalertx_device_status{device="Raspberry Pi 4 LAN", mac="74:ac:74:ac:74:74", ip="192.168.1.9", vendor="Raspberry Pi Trading Ltd", first_connection="2022-02-12 22:05:00", last_connection="2025-08-04 17:57:00", dev_type="Singleboard Computer (SBC)", device_status="Online"} 1
...
{ "error": "Forbidden" }
```
---
### Metrics Explanation
## Base URL
#### 1. Aggregate Device Counts
Metric names prefixed with `netalertx_` provide aggregated counts by device status:
* `netalertx_connected_devices`: number of devices currently connected
* `netalertx_offline_devices`: devices currently offline
* `netalertx_down_devices`: down/unreachable devices
* `netalertx_new_devices`: devices recently detected
* `netalertx_archived_devices`: archived devices
* `netalertx_favorite_devices`: user-marked favorite devices
* `netalertx_my_devices`: devices associated with the current user context
These numeric values give a high-level overview of device distribution.
#### 2. PerDevice Status with Labels
Each individual device is represented by a `netalertx_device_status` metric, with descriptive labels:
* `device`: friendly name of the device
* `mac`: MAC address (or placeholder)
* `ip`: last recorded IP address
* `vendor`: manufacturer or "None" if unknown
* `first_connection`: timestamp when the device was first observed
* `last_connection`: most recent contact timestamp
* `dev_type`: device category or type
* `device_status`: current status (Online / Offline / Archived / Down / ...)
The metric value is always `1` (indicating presence or active state) and the combination of labels identifies the device.
```
http://<server>:<GRAPHQL_PORT>/
```
---
### How to Query with `curl`
## Endpoints
To fetch the metrics from the NetAlertX exporter:
> [!TIP]
> When retrieving devices or settings try using the GraphQL API endpoint first as it is read-optimized.
```sh
curl 'http://<server_ip>:<GRAPHQL_PORT>/metrics' \
-H 'Authorization: Bearer <API_TOKEN>' \
-H 'Accept: text/plain'
```
* [Device API Endpoints](API_DEVICE.md) Manage individual devices
* [Devices Collection](API_DEVICES.md) Bulk operations on multiple devices
* [Events](API_EVENTS.md) Device event logging and management
* [Sessions](API_SESSIONS.md) Connection sessions and history
* [Settings](API_SETTINGS.md) Settings
* [Metrics](API_METRICS.md) Prometheus metrics and per-device status
* [Network Tools](API_NETTOOLS.md) Utilities like Wake-on-LAN, traceroute, nslookup, nmap, and internet info
* [Online History](API_ONLINEHISTORY.md) Online/offline device records
* [GraphQL](API_GRAPHQL.md) Advanced queries and filtering
* [Sync](API_SYNC.md) Synchronization between multiple NetAlertX instances
* [DB query](API_DBQUERY.md) (⚠ Internal) - Low level database access - use other endpoints if possible
Replace:
* `<server_ip>`: IP or hostname of the NetAlertX server
* `<GRAPHQL_PORT>`: port specified in your `GRAPHQL_PORT` setting (default: `20212`)
* `<API_TOKEN>` your Bearer token from the `API_TOKEN` setting
See [Testing](API_TESTS.md) for example requests and usage.
---
### Summary
* **Endpoint**: `/metrics` provides both summary counters and per-device status entries.
* **Aggregate metrics** help monitor overall device states.
* **Detailed metrics** expose each devices metadata via labels.
* **Use case**: feed into Prometheus for scraping, monitoring, alerting, or charting dashboard views.
### Prometheus Scraping Configuration
```yaml
scrape_configs:
- job_name: 'netalertx'
metrics_path: /metrics
scheme: http
scrape_interval: 60s
static_configs:
- targets: ['<server_ip>:<GRAPHQL_PORT>']
authorization:
type: Bearer
credentials: <API_TOKEN>
```
### Grafana template
Grafana template sample: [Download json](./samples/API/Grafana_Dashboard.json)
## API Endpoint: /log files
This API endpoint retrieves files from the `/app/log` folder.
- Endpoint URL: `php/server/query_logs.php?file=<file name>`
- Host: `same as front end (web ui)`
- Port: `20211` or as defined by the $PORT docker environment variable (same as the port for the web ui)
| File | Description |
|--------------------------|---------------------------------------------------------------|
| `IP_changes.log` | Logs of IP address changes |
| `app.log` | Main application log |
| `app.php_errors.log` | PHP error log |
| `app_front.log` | Frontend application log |
| `app_nmap.log` | Logs of Nmap scan results |
| `db_is_locked.log` | Logs when the database is locked |
| `execution_queue.log` | Logs of execution queue activities |
| `plugins/` | Directory for temporary plugin-related files (not accessible) |
| `report_output.html` | HTML report output |
| `report_output.json` | JSON format report output |
| `report_output.txt` | Text format report output |
| `stderr.log` | Logs of standard error output |
| `stdout.log` | Logs of standard output |
## API Endpoint: /config files
To retrieve files from the `/app/config` folder.
- Endpoint URL: `php/server/query_config.php?file=<file name>`
- Host: `same as front end (web ui)`
- Port: `20211` or as defined by the $PORT docker environment variable (same as the port for the web ui)
| File | Description |
|--------------------------|--------------------------------------------------|
| `devices.csv` | Devices csv file |
| `app.conf` | Application config file |
## Notes
* All endpoints enforce **Bearer token authentication**.
* Errors return JSON with `success: False` and an error message.
* GraphQL is available for advanced queries, while REST endpoints cover structured use cases.
* Endpoints run on `0.0.0.0:<GRAPHQL_PORT>` with **CORS enabled**.
* Use consistent API tokens and node/plugin names when interacting with `/sync` to ensure data integrity.

183
docs/API_DBQUERY.md Executable file
View File

@@ -0,0 +1,183 @@
# Database Query API
The **Database Query API** provides direct, low-level access to the NetAlertX database. It allows **read, write, update, and delete** operations against tables, using **base64-encoded** SQL or structured parameters.
> [!Warning]
> This API is primarily used internally to generate and render the application UI. These endpoints are low-level and powerful, and should be used with caution. Wherever possible, prefer the [standard API endpoints](API.md). Invalid or unsafe queries can corrupt data.
> If you need data in a specific format that is not already provided, please open an issue or pull request with a clear, broadly useful use case. This helps ensure new endpoints benefit the wider community rather than relying on raw database queries.
---
## Authentication
All `/dbquery/*` endpoints require an API token in the HTTP headers:
```http
Authorization: Bearer <API_TOKEN>
```
If the token is missing or invalid:
```json
{ "error": "Forbidden" }
```
---
## Endpoints
### 1. `POST /dbquery/read`
Execute a **read-only** SQL query (e.g., `SELECT`).
#### Request Body
```json
{
"rawSql": "U0VMRUNUICogRlJPTSBERVZJQ0VT" // base64 encoded SQL
}
```
Decoded SQL:
```sql
SELECT * FROM Devices;
```
#### Response
```json
{
"success": true,
"results": [
{ "devMac": "AA:BB:CC:DD:EE:FF", "devName": "Phone" }
]
}
```
#### `curl` Example
```bash
curl -X POST "http://<server_ip>:<GRAPHQL_PORT>/dbquery/read" \
-H "Authorization: Bearer <API_TOKEN>" \
-H "Accept: application/json" \
-H "Content-Type: application/json" \
-d '{
"rawSql": "U0VMRUNUICogRlJPTSBERVZJQ0VT"
}'
```
---
### 2. `POST /dbquery/update` (safer than `/dbquery/write`)
Update rows in a table by `columnName` + `id`. `/dbquery/update` is parameterized to reduce the risk of SQL injection, while `/dbquery/write` executes raw SQL directly.
#### Request Body
```json
{
"columnName": "devMac",
"id": ["AA:BB:CC:DD:EE:FF"],
"dbtable": "Devices",
"columns": ["devName", "devOwner"],
"values": ["Laptop", "Alice"]
}
```
#### Response
```json
{ "success": true, "updated_count": 1 }
```
#### `curl` Example
```bash
curl -X POST "http://<server_ip>:<GRAPHQL_PORT>/dbquery/update" \
-H "Authorization: Bearer <API_TOKEN>" \
-H "Accept: application/json" \
-H "Content-Type: application/json" \
-d '{
"columnName": "devMac",
"id": ["AA:BB:CC:DD:EE:FF"],
"dbtable": "Devices",
"columns": ["devName", "devOwner"],
"values": ["Laptop", "Alice"]
}'
```
---
### 3. `POST /dbquery/write`
Execute a **write query** (`INSERT`, `UPDATE`, `DELETE`).
#### Request Body
```json
{
"rawSql": "SU5TRVJUIElOVE8gRGV2aWNlcyAoZGV2TWFjLCBkZXYgTmFtZSwgZGV2Rmlyc3RDb25uZWN0aW9uLCBkZXZMYXN0Q29ubmVjdGlvbiwgZGV2TGFzdElQKSBWQUxVRVMgKCc2QTpCQjo0Qzo1RDo2RTonLCAnVGVzdERldmljZScsICcyMDI1LTA4LTMwIDEyOjAwOjAwJywgJzIwMjUtMDgtMzAgMTI6MDA6MDAnLCAnMTAuMC4wLjEwJyk="
}
```
Decoded SQL:
```sql
INSERT INTO Devices (devMac, devName, devFirstConnection, devLastConnection, devLastIP)
VALUES ('6A:BB:4C:5D:6E', 'TestDevice', '2025-08-30 12:00:00', '2025-08-30 12:00:00', '10.0.0.10');
```
#### Response
```json
{ "success": true, "affected_rows": 1 }
```
#### `curl` Example
```bash
curl -X POST "http://<server_ip>:<GRAPHQL_PORT>/dbquery/write" \
-H "Authorization: Bearer <API_TOKEN>" \
-H "Accept: application/json" \
-H "Content-Type: application/json" \
-d '{
"rawSql": "SU5TRVJUIElOVE8gRGV2aWNlcyAoZGV2TWFjLCBkZXYgTmFtZSwgZGV2Rmlyc3RDb25uZWN0aW9uLCBkZXZMYXN0Q29ubmVjdGlvbiwgZGV2TGFzdElQKSBWQUxVRVMgKCc2QTpCQjo0Qzo1RDo2RTonLCAnVGVzdERldmljZScsICcyMDI1LTA4LTMwIDEyOjAwOjAwJywgJzIwMjUtMDgtMzAgMTI6MDA6MDAnLCAnMTAuMC4wLjEwJyk="
}'
```
---
### 4. `POST /dbquery/delete`
Delete rows in a table by `columnName` + `id`.
#### Request Body
```json
{
"columnName": "devMac",
"id": ["AA:BB:CC:DD:EE:FF"],
"dbtable": "Devices"
}
```
#### Response
```json
{ "success": true, "deleted_count": 1 }
```
#### `curl` Example
```bash
curl -X POST "http://<server_ip>:<GRAPHQL_PORT>/dbquery/delete" \
-H "Authorization: Bearer <API_TOKEN>" \
-H "Accept: application/json" \
-H "Content-Type: application/json" \
-d '{
"columnName": "devMac",
"id": ["AA:BB:CC:DD:EE:FF"],
"dbtable": "Devices"
}'
```

233
docs/API_DEVICE.md Executable file
View File

@@ -0,0 +1,233 @@
# Device API Endpoints
Manage a **single device** by its MAC address. Operations include retrieval, updates, deletion, resetting properties, and copying data between devices. All endpoints require **authorization** via Bearer token.
---
## 1. Retrieve Device Details
* **GET** `/device/<mac>`
Fetch all details for a single device, including:
* Computed status (`devStatus`) → `On-line`, `Off-line`, or `Down`
* Session and event counts (`devSessions`, `devEvents`, `devDownAlerts`)
* Presence hours (`devPresenceHours`)
* Children devices (`devChildrenDynamic`) and NIC children (`devChildrenNicsDynamic`)
**Special case**: `mac=new` returns a template for a new device with default values.
**Response** (success):
```json
{
"devMac": "AA:BB:CC:DD:EE:FF",
"devName": "Net - Huawei",
"devOwner": "Admin",
"devType": "Router",
"devVendor": "Huawei",
"devStatus": "On-line",
"devSessions": 12,
"devEvents": 5,
"devDownAlerts": 1,
"devPresenceHours": 32,
"devChildrenDynamic": [...],
"devChildrenNicsDynamic": [...],
...
}
```
**Error Responses**:
* Device not found → HTTP 404
* Unauthorized → HTTP 403
---
## 2. Update Device Fields
* **POST** `/device/<mac>`
Create or update a device record.
**Request Body**:
```json
{
"devName": "New Device",
"devOwner": "Admin",
"createNew": true
}
```
**Behavior**:
* If `createNew=true` → creates a new device
* Otherwise → updates existing device fields
**Response**:
```json
{
"success": true
}
```
**Error Responses**:
* Unauthorized → HTTP 403
---
## 3. Delete a Device
* **DELETE** `/device/<mac>/delete`
Deletes the device with the given MAC.
**Response**:
```json
{
"success": true
}
```
**Error Responses**:
* Unauthorized → HTTP 403
---
## 4. Delete All Events for a Device
* **DELETE** `/device/<mac>/events/delete`
Removes all events associated with a device.
**Response**:
```json
{
"success": true
}
```
---
## 5. Reset Device Properties
* **POST** `/device/<mac>/reset-props`
Resets the device's custom properties to default values.
**Request Body**: Optional JSON for additional parameters.
**Response**:
```json
{
"success": true
}
```
---
## 6. Copy Device Data
* **POST** `/device/copy`
Copy all data from one device to another. If a device exists with `macTo`, it is replaced.
**Request Body**:
```json
{
"macFrom": "AA:BB:CC:DD:EE:FF",
"macTo": "11:22:33:44:55:66"
}
```
**Response**:
```json
{
"success": true,
"message": "Device copied from AA:BB:CC:DD:EE:FF to 11:22:33:44:55:66"
}
```
**Error Responses**:
* Missing `macFrom` or `macTo` → HTTP 400
* Unauthorized → HTTP 403
---
## 7. Update a Single Column
* **POST** `/device/<mac>/update-column`
Update one specific column for a device.
**Request Body**:
```json
{
"columnName": "devName",
"columnValue": "Updated Device Name"
}
```
**Response** (success):
```json
{
"success": true
}
```
**Error Responses**:
* Device not found → HTTP 404
* Missing `columnName` or `columnValue` → HTTP 400
* Unauthorized → HTTP 403
---
## Example `curl` Requests
**Get Device Details**:
```bash
curl -X GET "http://<server_ip>:<GRAPHQL_PORT>/device/AA:BB:CC:DD:EE:FF" \
-H "Authorization: Bearer <API_TOKEN>"
```
**Update Device Fields**:
```bash
curl -X POST "http://<server_ip>:<GRAPHQL_PORT>/device/AA:BB:CC:DD:EE:FF" \
-H "Authorization: Bearer <API_TOKEN>" \
-H "Content-Type: application/json" \
--data '{"devName": "New Device Name"}'
```
**Delete Device**:
```bash
curl -X DELETE "http://<server_ip>:<GRAPHQL_PORT>/device/AA:BB:CC:DD:EE:FF/delete" \
-H "Authorization: Bearer <API_TOKEN>"
```
**Copy Device Data**:
```bash
curl -X POST "http://<server_ip>:<GRAPHQL_PORT>/device/copy" \
-H "Authorization: Bearer <API_TOKEN>" \
-H "Content-Type: application/json" \
--data '{"macFrom":"AA:BB:CC:DD:EE:FF","macTo":"11:22:33:44:55:66"}'
```
**Update Single Column**:
```bash
curl -X POST "http://<server_ip>:<GRAPHQL_PORT>/device/AA:BB:CC:DD:EE:FF/update-column" \
-H "Authorization: Bearer <API_TOKEN>" \
-H "Content-Type: application/json" \
--data '{"columnName":"devName","columnValue":"Updated Device"}'
```

249
docs/API_DEVICES.md Executable file
View File

@@ -0,0 +1,249 @@
# Devices Collection API Endpoints
The Devices Collection API provides operations to **retrieve, manage, import/export, and filter devices** in bulk. All endpoints require **authorization** via Bearer token.
---
## Endpoints
### 1. Get All Devices
* **GET** `/devices`
Retrieves all devices from the database.
**Response** (success):
```json
{
"success": true,
"devices": [
{
"devName": "Net - Huawei",
"devMAC": "AA:BB:CC:DD:EE:FF",
"devIP": "192.168.1.1",
"devType": "Router",
"devFavorite": 0,
"devStatus": "online"
},
...
]
}
```
**Error Responses**:
* Unauthorized → HTTP 403
---
### 2. Delete Devices by MAC
* **DELETE** `/devices`
Deletes devices by MAC address. Supports exact matches or wildcard `*`.
**Request Body**:
```json
{
"macs": ["AA:BB:CC:DD:EE:FF", "11:22:33:*"]
}
```
**Behavior**:
* If `macs` is omitted or `null` → deletes **all devices**.
* Wildcards `*` match multiple devices.
**Response**:
```json
{
"success": true,
"deleted_count": 5
}
```
**Error Responses**:
* Unauthorized → HTTP 403
---
### 3. Delete Devices with Empty MACs
* **DELETE** `/devices/empty-macs`
Removes all devices where MAC address is null or empty.
**Response**:
```json
{
"success": true,
"deleted": 3
}
```
---
### 4. Delete Unknown Devices
* **DELETE** `/devices/unknown`
Deletes devices with names marked as `(unknown)` or `(name not found)`.
**Response**:
```json
{
"success": true,
"deleted": 2
}
```
---
### 5. Export Devices
* **GET** `/devices/export` or `/devices/export/<format>`
Exports all devices in **CSV** (default) or **JSON** format.
**Query Parameter / URL Parameter**:
* `format` (optional) → `csv` (default) or `json`
**CSV Response**:
* Returns as a downloadable CSV file: `Content-Disposition: attachment; filename=devices.csv`
**JSON Response**:
```json
{
"data": [
{ "devName": "Net - Huawei", "devMAC": "AA:BB:CC:DD:EE:FF", ... },
...
],
"columns": ["devName", "devMAC", "devIP", "devType", "devFavorite", "devStatus"]
}
```
**Error Responses**:
* Unsupported format → HTTP 400
---
### 6. Import Devices from CSV
* **POST** `/devices/import`
Imports devices from an uploaded CSV or base64-encoded CSV content.
**Request Body** (multipart file or JSON with `content` field):
```json
{
"content": "<base64-encoded CSV content>"
}
```
**Response**:
```json
{
"success": true,
"inserted": 25,
"skipped_lines": [3, 7]
}
```
**Error Responses**:
* Missing file or content → HTTP 400 / 404
* CSV malformed → HTTP 400
---
### 7. Get Device Totals
* **GET** `/devices/totals`
Returns counts of devices by various categories.
**Response**:
```json
[
120, // Total devices
85, // Connected
5, // Favorites
10, // New
8, // Down
12 // Archived
]
```
*Order: `[all, connected, favorites, new, down, archived]`*
---
### 8. Get Devices by Status
* **GET** `/devices/by-status?status=<status>`
Returns devices filtered by status.
**Query Parameter**:
* `status` → Supported values: `online`, `offline`, `down`, `archived`, `favorites`, `new`, `my`
* If omitted, returns **all devices**.
**Response** (success):
```json
[
{ "id": "AA:BB:CC:DD:EE:FF", "title": "Net - Huawei", "favorite": 0 },
{ "id": "11:22:33:44:55:66", "title": "★ USG Firewall", "favorite": 1 }
]
```
*If `devFavorite=1`, the title is prepended with a star `★`.*
---
## Example `curl` Requests
**Get All Devices**:
```sh
curl -X GET "http://<server_ip>:<GRAPHQL_PORT>/devices" \
-H "Authorization: Bearer <API_TOKEN>"
```
**Delete Devices by MAC**:
```sh
curl -X DELETE "http://<server_ip>:<GRAPHQL_PORT>/devices" \
-H "Authorization: Bearer <API_TOKEN>" \
-H "Content-Type: application/json" \
--data '{"macs":["AA:BB:CC:DD:EE:FF","11:22:33:*"]}'
```
**Export Devices CSV**:
```sh
curl -X GET "http://<server_ip>:<GRAPHQL_PORT>/devices/export?format=csv" \
-H "Authorization: Bearer <API_TOKEN>"
```
**Import Devices from CSV**:
```sh
curl -X POST "http://<server_ip>:<GRAPHQL_PORT>/devices/import" \
-H "Authorization: Bearer <API_TOKEN>" \
-F "file=@devices.csv"
```
**Get Devices by Status**:
```sh
curl -X GET "http://<server_ip>:<GRAPHQL_PORT>/devices/by-status?status=online" \
-H "Authorization: Bearer <API_TOKEN>"
```

169
docs/API_EVENTS.md Executable file
View File

@@ -0,0 +1,169 @@
# Events API Endpoints
The Events API provides access to **device event logs**, allowing creation, retrieval, deletion, and summary of events over time.
---
## Endpoints
### 1. Create Event
* **POST** `/events/create/<mac>`
Create an event for a device identified by its MAC address.
**Request Body** (JSON):
```json
{
"ip": "192.168.1.10",
"event_type": "Device Down",
"additional_info": "Optional info about the event",
"pending_alert": 1,
"event_time": "2025-08-24T12:00:00Z"
}
```
* **Parameters**:
* `ip` (string, optional): IP address of the device
* `event_type` (string, optional): Type of event (default `"Device Down"`)
* `additional_info` (string, optional): Extra information
* `pending_alert` (int, optional): 1 if alert email is pending (default 1)
* `event_time` (ISO datetime, optional): Event timestamp; defaults to current time
**Response** (JSON):
```json
{
"success": true,
"message": "Event created for 00:11:22:33:44:55"
}
```
---
### 2. Get Events
* **GET** `/events`
Retrieve all events, optionally filtered by MAC address:
```
/events?mac=<mac>
```
**Response**:
```json
{
"success": true,
"events": [
{
"eve_MAC": "00:11:22:33:44:55",
"eve_IP": "192.168.1.10",
"eve_DateTime": "2025-08-24T12:00:00Z",
"eve_EventType": "Device Down",
"eve_AdditionalInfo": "",
"eve_PendingAlertEmail": 1
}
]
}
```
---
### 3. Delete Events
* **DELETE** `/events/<mac>` → Delete events for a specific MAC
* **DELETE** `/events` → Delete **all** events
* **DELETE** `/events/<days>` → Delete events older than N days
**Response**:
```json
{
"success": true,
"message": "Deleted events older than <days> days"
}
```
---
### 4. Event Totals Over a Period
* **GET** `/sessions/totals?period=<period>`
Return event and session totals over a given period.
**Query Parameters**:
| Parameter | Description |
| --------- | -------------------------------------------------------------------------------- |
| `period` | Time period for totals, e.g., `"7 days"`, `"1 month"`, `"1 year"`, `"100 years"` |
**Sample Response** (JSON Array):
```json
[120, 85, 5, 10, 3, 7]
```
**Meaning of Values**:
1. Total events in the period
2. Total sessions
3. Missing sessions
4. Voided events (`eve_EventType LIKE 'VOIDED%'`)
5. New device events (`eve_EventType LIKE 'New Device'`)
6. Device down events (`eve_EventType LIKE 'Device Down'`)
---
## Notes
* All endpoints require **authorization** (Bearer token). Unauthorized requests return:
```json
{ "error": "Forbidden" }
```
* Events are stored in the **Events table** with the following fields:
`eve_MAC`, `eve_IP`, `eve_DateTime`, `eve_EventType`, `eve_AdditionalInfo`, `eve_PendingAlertEmail`.
* Event creation automatically logs activity for debugging.
---
## Example `curl` Requests
**Create Event**:
```sh
curl -X POST "http://<server_ip>:<GRAPHQL_PORT>/events/create/00:11:22:33:44:55" \
-H "Authorization: Bearer <API_TOKEN>" \
-H "Content-Type: application/json" \
--data '{
"ip": "192.168.1.10",
"event_type": "Device Down",
"additional_info": "Power outage",
"pending_alert": 1
}'
```
**Get Events for a Device**:
```sh
curl "http://<server_ip>:<GRAPHQL_PORT>/events?mac=00:11:22:33:44:55" \
-H "Authorization: Bearer <API_TOKEN>"
```
**Delete Events Older Than 30 Days**:
```sh
curl -X DELETE "http://<server_ip>:<GRAPHQL_PORT>/events/30" \
-H "Authorization: Bearer <API_TOKEN>"
```
**Get Event Totals for 7 Days**:
```sh
curl "http://<server_ip>:<GRAPHQL_PORT>/sessions/totals?period=7 days" \
-H "Authorization: Bearer <API_TOKEN>"
```

200
docs/API_GRAPHQL.md Executable file
View File

@@ -0,0 +1,200 @@
# GraphQL API Endpoint
GraphQL queries are **read-optimized for speed**. Data may be slightly out of date until the file system cache refreshes. The GraphQL endpoints allows you to access the following objects:
- Devices
- Settings
## Endpoints
* **GET** `/graphql`
Returns a simple status message (useful for browser or debugging).
* **POST** `/graphql`
Execute GraphQL queries against the `devicesSchema`.
---
## Devices Query
### Sample Query
```graphql
query GetDevices($options: PageQueryOptionsInput) {
devices(options: $options) {
devices {
rowid
devMac
devName
devOwner
devType
devVendor
devLastConnection
devStatus
}
count
}
}
```
### Query Parameters
| Parameter | Description |
| --------- | ------------------------------------------------------------------------------------------------------- |
| `page` | Page number of results to fetch. |
| `limit` | Number of results per page. |
| `sort` | Sorting options (`field` = field name, `order` = `asc` or `desc`). |
| `search` | Term to filter devices. |
| `status` | Filter devices by status: `my_devices`, `connected`, `favorites`, `new`, `down`, `archived`, `offline`. |
| `filters` | Additional filters (array of `{ filterColumn, filterValue }`). |
---
### `curl` Example
```sh
curl 'http://host:GRAPHQL_PORT/graphql' \
-X POST \
-H 'Authorization: Bearer API_TOKEN' \
-H 'Content-Type: application/json' \
--data '{
"query": "query GetDevices($options: PageQueryOptionsInput) { devices(options: $options) { devices { rowid devMac devName devOwner devType devVendor devLastConnection devStatus } count } }",
"variables": {
"options": {
"page": 1,
"limit": 10,
"sort": [{ "field": "devName", "order": "asc" }],
"search": "",
"status": "connected"
}
}
}'
```
---
### Sample Response
```json
{
"data": {
"devices": {
"devices": [
{
"rowid": 1,
"devMac": "00:11:22:33:44:55",
"devName": "Device 1",
"devOwner": "Owner 1",
"devType": "Type 1",
"devVendor": "Vendor 1",
"devLastConnection": "2025-01-01T00:00:00Z",
"devStatus": "connected"
}
],
"count": 1
}
}
}
```
---
## Settings Query
The **settings query** provides access to NetAlertX configuration stored in the settings table.
### Sample Query
```graphql
query GetSettings {
settings {
settings {
setKey
setName
setDescription
setType
setOptions
setGroup
setValue
setEvents
setOverriddenByEnv
}
count
}
}
```
### Schema Fields
| Field | Type | Description |
| -------------------- | ------- | ------------------------------------------------------------------------ |
| `setKey` | String | Unique key identifier for the setting. |
| `setName` | String | Human-readable name. |
| `setDescription` | String | Description or documentation of the setting. |
| `setType` | String | Data type (`string`, `int`, `bool`, `json`, etc.). |
| `setOptions` | String | Available options (for dropdown/select-type settings). |
| `setGroup` | String | Group/category the setting belongs to. |
| `setValue` | String | Current value of the setting. |
| `setEvents` | String | Events or triggers related to this setting. |
| `setOverriddenByEnv` | Boolean | Whether the setting is overridden by an environment variable at runtime. |
---
### `curl` Example
```sh
curl 'http://host:GRAPHQL_PORT/graphql' \
-X POST \
-H 'Authorization: Bearer API_TOKEN' \
-H 'Content-Type: application/json' \
--data '{
"query": "query GetSettings { settings { settings { setKey setName setDescription setType setOptions setGroup setValue setEvents setOverriddenByEnv } count } }"
}'
```
---
### Sample Response
```json
{
"data": {
"settings": {
"settings": [
{
"setKey": "UI_MY_DEVICES",
"setName": "My Devices Filter",
"setDescription": "Defines which statuses to include in the 'My Devices' view.",
"setType": "list",
"setOptions": "[\"online\",\"new\",\"down\",\"offline\",\"archived\"]",
"setGroup": "UI",
"setValue": "[\"online\",\"new\"]",
"setEvents": null,
"setOverriddenByEnv": false
},
{
"setKey": "NETWORK_DEVICE_TYPES",
"setName": "Network Device Types",
"setDescription": "Types of devices considered as network infrastructure.",
"setType": "list",
"setOptions": "[\"Router\",\"Switch\",\"AP\"]",
"setGroup": "Network",
"setValue": "[\"Router\",\"Switch\"]",
"setEvents": null,
"setOverriddenByEnv": true
}
],
"count": 2
}
}
}
```
---
## Notes
* Device and settings queries can be combined in one request since GraphQL supports batching.
* The `setOverriddenByEnv` flag helps identify setting values that are locked at container runtime.
* The schema is **read-only** — updates must be performed through other APIs or configuration management. See the other [API](API.md) endpoints for details.

103
docs/API_METRICS.md Executable file
View File

@@ -0,0 +1,103 @@
# Metrics API Endpoint
The `/metrics` endpoint exposes **Prometheus-compatible metrics** for NetAlertX, including aggregate device counts and per-device status.
---
## Endpoint Details
* **GET** `/metrics` → Returns metrics in plain text.
* **Host**: NetAlertX server
* **Port**: As configured in `GRAPHQL_PORT` (default: `20212`)
---
## Example Output
```text
netalertx_connected_devices 31
netalertx_offline_devices 54
netalertx_down_devices 0
netalertx_new_devices 0
netalertx_archived_devices 31
netalertx_favorite_devices 2
netalertx_my_devices 54
netalertx_device_status{device="Net - Huawei", mac="Internet", ip="1111.111.111.111", vendor="None", first_connection="2021-01-01 00:00:00", last_connection="2025-08-04 17:57:00", dev_type="Router", device_status="Online"} 1
netalertx_device_status{device="Net - USG", mac="74:ac:74:ac:74:ac", ip="192.168.1.1", vendor="Ubiquiti Networks Inc.", first_connection="2022-02-12 22:05:00", last_connection="2025-06-07 08:16:49", dev_type="Firewall", device_status="Archived"} 1
netalertx_device_status{device="Raspberry Pi 4 LAN", mac="74:ac:74:ac:74:74", ip="192.168.1.9", vendor="Raspberry Pi Trading Ltd", first_connection="2022-02-12 22:05:00", last_connection="2025-08-04 17:57:00", dev_type="Singleboard Computer (SBC)", device_status="Online"} 1
...
```
---
## Metrics Overview
### 1. Aggregate Device Counts
| Metric | Description |
| ----------------------------- | ---------------------------------------- |
| `netalertx_connected_devices` | Devices currently connected |
| `netalertx_offline_devices` | Devices currently offline |
| `netalertx_down_devices` | Down/unreachable devices |
| `netalertx_new_devices` | Recently detected devices |
| `netalertx_archived_devices` | Archived devices |
| `netalertx_favorite_devices` | User-marked favorites |
| `netalertx_my_devices` | Devices associated with the current user |
---
### 2. Per-Device Status
Metric: `netalertx_device_status`
Each device has labels:
* `device`: friendly name
* `mac`: MAC address (or placeholder)
* `ip`: last recorded IP
* `vendor`: manufacturer or "None"
* `first_connection`: timestamp of first detection
* `last_connection`: most recent contact
* `dev_type`: device type/category
* `device_status`: current status (`Online`, `Offline`, `Archived`, `Down`, …)
Metric value is always `1` (presence indicator).
---
## Querying with `curl`
```sh
curl 'http://<server_ip>:<GRAPHQL_PORT>/metrics' \
-H 'Authorization: Bearer <API_TOKEN>' \
-H 'Accept: text/plain'
```
Replace placeholders:
* `<server_ip>` NetAlertX host IP/hostname
* `<GRAPHQL_PORT>` configured port (default `20212`)
* `<API_TOKEN>` your API token
---
## Prometheus Scraping Configuration
```yaml
scrape_configs:
- job_name: 'netalertx'
metrics_path: /metrics
scheme: http
scrape_interval: 60s
static_configs:
- targets: ['<server_ip>:<GRAPHQL_PORT>']
authorization:
type: Bearer
credentials: <API_TOKEN>
```
---
## Grafana Dashboard Template
Sample template JSON: [Download](./samples/API/Grafana_Dashboard.json)

243
docs/API_NETTOOLS.md Executable file
View File

@@ -0,0 +1,243 @@
# Net Tools API Endpoints
The Net Tools API provides **network diagnostic utilities**, including Wake-on-LAN, traceroute, speed testing, DNS resolution, nmap scanning, and internet connection information.
All endpoints require **authorization** via Bearer token.
---
## Endpoints
### 1. Wake-on-LAN
* **POST** `/nettools/wakeonlan`
Sends a Wake-on-LAN packet to wake a device.
**Request Body** (JSON):
```json
{
"devMac": "AA:BB:CC:DD:EE:FF"
}
```
**Response** (success):
```json
{
"success": true,
"message": "WOL packet sent",
"output": "Sent magic packet to AA:BB:CC:DD:EE:FF"
}
```
**Error Responses**:
* Invalid MAC address → HTTP 400
* Command failure → HTTP 500
---
### 2. Traceroute
* **POST** `/nettools/traceroute`
Performs a traceroute to a specified IP address.
**Request Body**:
```json
{
"devLastIP": "192.168.1.1"
}
```
**Response** (success):
```json
{
"success": true,
"output": "traceroute output as string"
}
```
**Error Responses**:
* Invalid IP → HTTP 400
* Traceroute command failure → HTTP 500
---
### 3. Speedtest
* **GET** `/nettools/speedtest`
Runs an internet speed test using `speedtest-cli`.
**Response** (success):
```json
{
"success": true,
"output": [
"Ping: 15 ms",
"Download: 120.5 Mbit/s",
"Upload: 22.4 Mbit/s"
]
}
```
**Error Responses**:
* Command failure → HTTP 500
---
### 4. DNS Lookup (nslookup)
* **POST** `/nettools/nslookup`
Resolves an IP address or hostname using `nslookup`.
**Request Body**:
```json
{
"devLastIP": "8.8.8.8"
}
```
**Response** (success):
```json
{
"success": true,
"output": [
"Server: 8.8.8.8",
"Address: 8.8.8.8#53",
"Name: google-public-dns-a.google.com"
]
}
```
**Error Responses**:
* Missing or invalid `devLastIP` → HTTP 400
* Command failure → HTTP 500
---
### 5. Nmap Scan
* **POST** `/nettools/nmap`
Runs an nmap scan on a target IP address or range.
**Request Body**:
```json
{
"scan": "192.168.1.0/24",
"mode": "fast"
}
```
**Supported Modes**:
| Mode | nmap Arguments |
| --------------- | -------------- |
| `fast` | `-F` |
| `normal` | default |
| `detail` | `-A` |
| `skipdiscovery` | `-Pn` |
**Response** (success):
```json
{
"success": true,
"mode": "fast",
"ip": "192.168.1.0/24",
"output": [
"Starting Nmap 7.91",
"Host 192.168.1.1 is up",
"... scan results ..."
]
}
```
**Error Responses**:
* Invalid IP → HTTP 400
* Invalid mode → HTTP 400
* Command failure → HTTP 500
---
### 6. Internet Connection Info
* **GET** `/nettools/internetinfo`
Fetches public internet connection information using `ipinfo.io`.
**Response** (success):
```json
{
"success": true,
"output": "IP: 203.0.113.5 City: Sydney Country: AU Org: Example ISP"
}
```
**Error Responses**:
* Failed request or empty response → HTTP 500
---
## Example `curl` Requests
**Wake-on-LAN**:
```sh
curl -X POST "http://<server_ip>:<GRAPHQL_PORT>/nettools/wakeonlan" \
-H "Authorization: Bearer <API_TOKEN>" \
-H "Content-Type: application/json" \
--data '{"devMac":"AA:BB:CC:DD:EE:FF"}'
```
**Traceroute**:
```sh
curl -X POST "http://<server_ip>:<GRAPHQL_PORT>/nettools/traceroute" \
-H "Authorization: Bearer <API_TOKEN>" \
-H "Content-Type: application/json" \
--data '{"devLastIP":"192.168.1.1"}'
```
**Speedtest**:
```sh
curl "http://<server_ip>:<GRAPHQL_PORT>/nettools/speedtest" \
-H "Authorization: Bearer <API_TOKEN>"
```
**Nslookup**:
```sh
curl -X POST "http://<server_ip>:<GRAPHQL_PORT>/nettools/nslookup" \
-H "Authorization: Bearer <API_TOKEN>" \
-H "Content-Type: application/json" \
--data '{"devLastIP":"8.8.8.8"}'
```
**Nmap Scan**:
```sh
curl -X POST "http://<server_ip>:<GRAPHQL_PORT>/nettools/nmap" \
-H "Authorization: Bearer <API_TOKEN>" \
-H "Content-Type: application/json" \
--data '{"scan":"192.168.1.0/24","mode":"fast"}'
```
**Internet Info**:
```sh
curl "http://<server_ip>:<GRAPHQL_PORT>/nettools/internetinfo" \
-H "Authorization: Bearer <API_TOKEN>"
```

370
docs/API_OLD.md Executable file
View File

@@ -0,0 +1,370 @@
# [Deprecated] API endpoints
> [!WARNING]
> Some of these endpoints will be deprecated soon. Please refere to the new [API](API.md) endpoints docs for details on the new API layer.
NetAlertX comes with a couple of API endpoints. All requests need to be authorized (executed in a logged in browser session) or you have to pass the value of the `API_TOKEN` settings as authorization bearer, for example:
```graphql
curl 'http://host:GRAPHQL_PORT/graphql' \
-X POST \
-H 'Authorization: Bearer API_TOKEN' \
-H 'Content-Type: application/json' \
--data '{
"query": "query GetDevices($options: PageQueryOptionsInput) { devices(options: $options) { devices { rowid devMac devName devOwner devType devVendor devLastConnection devStatus } count } }",
"variables": {
"options": {
"page": 1,
"limit": 10,
"sort": [{ "field": "devName", "order": "asc" }],
"search": "",
"status": "connected"
}
}
}'
```
## API Endpoint: GraphQL
- Endpoint URL: `php/server/query_graphql.php`
- Host: `same as front end (web ui)`
- Port: `20212` or as defined by the `GRAPHQL_PORT` setting
### Example Query to Fetch Devices
First, let's define the GraphQL query to fetch devices with pagination and sorting options.
```graphql
query GetDevices($options: PageQueryOptionsInput) {
devices(options: $options) {
devices {
rowid
devMac
devName
devOwner
devType
devVendor
devLastConnection
devStatus
}
count
}
}
```
See also: [Debugging GraphQL issues](./DEBUG_GRAPHQL.md)
### `curl` Command
You can use the following `curl` command to execute the query.
```sh
curl 'http://host:GRAPHQL_PORT/graphql' -X POST -H 'Authorization: Bearer API_TOKEN' -H 'Content-Type: application/json' --data '{
"query": "query GetDevices($options: PageQueryOptionsInput) { devices(options: $options) { devices { rowid devMac devName devOwner devType devVendor devLastConnection devStatus } count } }",
"variables": {
"options": {
"page": 1,
"limit": 10,
"sort": [{ "field": "devName", "order": "asc" }],
"search": "",
"status": "connected"
}
}
}'
```
### Explanation:
1. **GraphQL Query**:
- The `query` parameter contains the GraphQL query as a string.
- The `variables` parameter contains the input variables for the query.
2. **Query Variables**:
- `page`: Specifies the page number of results to fetch.
- `limit`: Specifies the number of results per page.
- `sort`: Specifies the sorting options, with `field` being the field to sort by and `order` being the sort order (`asc` for ascending or `desc` for descending).
- `search`: A search term to filter the devices.
- `status`: The status filter to apply (valid values are `my_devices` (determined by the `UI_MY_DEVICES` setting), `connected`, `favorites`, `new`, `down`, `archived`, `offline`).
3. **`curl` Command**:
- The `-X POST` option specifies that we are making a POST request.
- The `-H "Content-Type: application/json"` option sets the content type of the request to JSON.
- The `-d` option provides the request payload, which includes the GraphQL query and variables.
### Sample Response
The response will be in JSON format, similar to the following:
```json
{
"data": {
"devices": {
"devices": [
{
"rowid": 1,
"devMac": "00:11:22:33:44:55",
"devName": "Device 1",
"devOwner": "Owner 1",
"devType": "Type 1",
"devVendor": "Vendor 1",
"devLastConnection": "2025-01-01T00:00:00Z",
"devStatus": "connected"
},
{
"rowid": 2,
"devMac": "66:77:88:99:AA:BB",
"devName": "Device 2",
"devOwner": "Owner 2",
"devType": "Type 2",
"devVendor": "Vendor 2",
"devLastConnection": "2025-01-02T00:00:00Z",
"devStatus": "connected"
}
],
"count": 2
}
}
}
```
## API Endpoint: JSON files
This API endpoint retrieves static files, that are periodically updated.
- Endpoint URL: `php/server/query_json.php?file=<file name>`
- Host: `same as front end (web ui)`
- Port: `20211` or as defined by the $PORT docker environment variable (same as the port for the web ui)
### When are the endpoints updated
The endpoints are updated when objects in the API endpoints are changed.
### Location of the endpoints
In the container, these files are located under the `/app/api/` folder. You can access them via the `/php/server/query_json.php?file=user_notifications.json` endpoint.
### Available endpoints
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)). |
| `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. |
| `table_plugins_objects.json` | The content of the plugins_objects table. Find more info on the [Plugin system here](https://github.com/jokob-sk/NetAlertX/tree/main/docs/PLUGINS.md)|
| `language_strings.json` | The content of the language_strings table, which in turn is loaded from the plugins `config.json` definitions. |
| `table_custom_endpoint.json` | A custom endpoint generated by the SQL query specified by the `API_CUSTOM_SQL` setting. |
| `table_settings.json` | The content of the settings table. |
| `app_state.json` | Contains the current application state. |
### JSON Data format
The endpoints starting with the `table_` prefix contain most, if not all, data contained in the corresponding database table. The common format for those is:
```JSON
{
"data": [
{
"db_column_name": "data",
"db_column_name2": "data2"
},
{
"db_column_name": "data3",
"db_column_name2": "data4"
}
]
}
```
Example JSON of the `table_devices.json` endpoint with two Devices (database rows):
```JSON
{
"data": [
{
"devMac": "Internet",
"devName": "Net - Huawei",
"devType": "Router",
"devVendor": null,
"devGroup": "Always on",
"devFirstConnection": "2021-01-01 00:00:00",
"devLastConnection": "2021-01-28 22:22:11",
"devLastIP": "192.168.1.24",
"devStaticIP": 0,
"devPresentLastScan": 1,
"devLastNotification": "2023-01-28 22:22:28.998715",
"devIsNew": 0,
"devParentMAC": "",
"devParentPort": "",
"devIcon": "globe"
},
{
"devMac": "a4:8f:ff:aa:ba:1f",
"devName": "Net - USG",
"devType": "Firewall",
"devVendor": "Ubiquiti Inc",
"devGroup": "",
"devFirstConnection": "2021-02-12 22:05:00",
"devLastConnection": "2021-07-17 15:40:00",
"devLastIP": "192.168.1.1",
"devStaticIP": 1,
"devPresentLastScan": 1,
"devLastNotification": "2021-07-17 15:40:10.667717",
"devIsNew": 0,
"devParentMAC": "Internet",
"devParentPort": 1,
"devIcon": "shield-halved"
}
]
}
```
## API Endpoint: Prometheus Exporter
* **Endpoint URL**: `/metrics`
* **Host**: (where NetAlertX exporter is running)
* **Port**: as configured in the `GRAPHQL_PORT` setting (`20212` by default)
---
### Example Output of the `/metrics` Endpoint
Below is a representative snippet of the metrics you may find when querying the `/metrics` endpoint for `netalertx`. It includes both aggregate counters and `device_status` labels per device.
```
netalertx_connected_devices 31
netalertx_offline_devices 54
netalertx_down_devices 0
netalertx_new_devices 0
netalertx_archived_devices 31
netalertx_favorite_devices 2
netalertx_my_devices 54
netalertx_device_status{device="Net - Huawei", mac="Internet", ip="1111.111.111.111", vendor="None", first_connection="2021-01-01 00:00:00", last_connection="2025-08-04 17:57:00", dev_type="Router", device_status="Online"} 1
netalertx_device_status{device="Net - USG", mac="74:ac:74:ac:74:ac", ip="192.168.1.1", vendor="Ubiquiti Networks Inc.", first_connection="2022-02-12 22:05:00", last_connection="2025-06-07 08:16:49", dev_type="Firewall", device_status="Archived"} 1
netalertx_device_status{device="Raspberry Pi 4 LAN", mac="74:ac:74:ac:74:74", ip="192.168.1.9", vendor="Raspberry Pi Trading Ltd", first_connection="2022-02-12 22:05:00", last_connection="2025-08-04 17:57:00", dev_type="Singleboard Computer (SBC)", device_status="Online"} 1
...
```
---
### Metrics Explanation
#### 1. Aggregate Device Counts
Metric names prefixed with `netalertx_` provide aggregated counts by device status:
* `netalertx_connected_devices`: number of devices currently connected
* `netalertx_offline_devices`: devices currently offline
* `netalertx_down_devices`: down/unreachable devices
* `netalertx_new_devices`: devices recently detected
* `netalertx_archived_devices`: archived devices
* `netalertx_favorite_devices`: user-marked favorite devices
* `netalertx_my_devices`: devices associated with the current user context
These numeric values give a high-level overview of device distribution.
#### 2. PerDevice Status with Labels
Each individual device is represented by a `netalertx_device_status` metric, with descriptive labels:
* `device`: friendly name of the device
* `mac`: MAC address (or placeholder)
* `ip`: last recorded IP address
* `vendor`: manufacturer or "None" if unknown
* `first_connection`: timestamp when the device was first observed
* `last_connection`: most recent contact timestamp
* `dev_type`: device category or type
* `device_status`: current status (Online / Offline / Archived / Down / ...)
The metric value is always `1` (indicating presence or active state) and the combination of labels identifies the device.
---
### How to Query with `curl`
To fetch the metrics from the NetAlertX exporter:
```sh
curl 'http://<server_ip>:<GRAPHQL_PORT>/metrics' \
-H 'Authorization: Bearer <API_TOKEN>' \
-H 'Accept: text/plain'
```
Replace:
* `<server_ip>`: IP or hostname of the NetAlertX server
* `<GRAPHQL_PORT>`: port specified in your `GRAPHQL_PORT` setting (default: `20212`)
* `<API_TOKEN>` your Bearer token from the `API_TOKEN` setting
---
### Summary
* **Endpoint**: `/metrics` provides both summary counters and per-device status entries.
* **Aggregate metrics** help monitor overall device states.
* **Detailed metrics** expose each devices metadata via labels.
* **Use case**: feed into Prometheus for scraping, monitoring, alerting, or charting dashboard views.
### Prometheus Scraping Configuration
```yaml
scrape_configs:
- job_name: 'netalertx'
metrics_path: /metrics
scheme: http
scrape_interval: 60s
static_configs:
- targets: ['<server_ip>:<GRAPHQL_PORT>']
authorization:
type: Bearer
credentials: <API_TOKEN>
```
### Grafana template
Grafana template sample: [Download json](./samples/API/Grafana_Dashboard.json)
## API Endpoint: /log files
This API endpoint retrieves files from the `/app/log` folder.
- Endpoint URL: `php/server/query_logs.php?file=<file name>`
- Host: `same as front end (web ui)`
- Port: `20211` or as defined by the $PORT docker environment variable (same as the port for the web ui)
| File | Description |
|--------------------------|---------------------------------------------------------------|
| `IP_changes.log` | Logs of IP address changes |
| `app.log` | Main application log |
| `app.php_errors.log` | PHP error log |
| `app_front.log` | Frontend application log |
| `app_nmap.log` | Logs of Nmap scan results |
| `db_is_locked.log` | Logs when the database is locked |
| `execution_queue.log` | Logs of execution queue activities |
| `plugins/` | Directory for temporary plugin-related files (not accessible) |
| `report_output.html` | HTML report output |
| `report_output.json` | JSON format report output |
| `report_output.txt` | Text format report output |
| `stderr.log` | Logs of standard error output |
| `stdout.log` | Logs of standard output |
## API Endpoint: /config files
To retrieve files from the `/app/config` folder.
- Endpoint URL: `php/server/query_config.php?file=<file name>`
- Host: `same as front end (web ui)`
- Port: `20211` or as defined by the $PORT docker environment variable (same as the port for the web ui)
| File | Description |
|--------------------------|--------------------------------------------------|
| `devices.csv` | Devices csv file |
| `app.conf` | Application config file |

32
docs/API_ONLINEHISTORY.md Executable file
View File

@@ -0,0 +1,32 @@
# Online History API Endpoints
Manage the **online history records** of devices. Currently, the API supports deletion of all history entries. All endpoints require **authorization**.
---
## 1. Delete Online History
* **DELETE** `/history`
Remove **all records** from the online history table (`Online_History`). This operation **cannot be undone**.
**Response** (success):
```json
{
"success": true,
"message": "Deleted online history"
}
```
**Error Responses**:
* Unauthorized → HTTP 403
---
### Example `curl` Request
```bash
curl -X DELETE "http://<server_ip>:<GRAPHQL_PORT>/history" \
-H "Authorization: Bearer <API_TOKEN>"
```

240
docs/API_SESSIONS.md Executable file
View File

@@ -0,0 +1,240 @@
# Sessions API Endpoints
Track and manage device connection sessions. Sessions record when a device connects or disconnects on the network.
### Create a Session
* **POST** `/sessions/create` → Create a new session for a device
**Request Body:**
```json
{
"mac": "AA:BB:CC:DD:EE:FF",
"ip": "192.168.1.10",
"start_time": "2025-08-01T10:00:00",
"end_time": "2025-08-01T12:00:00", // optional
"event_type_conn": "Connected", // optional, default "Connected"
"event_type_disc": "Disconnected" // optional, default "Disconnected"
}
```
**Response:**
```json
{
"success": true,
"message": "Session created for MAC AA:BB:CC:DD:EE:FF"
}
```
#### `curl` Example
```bash
curl -X POST "http://<server_ip>:<GRAPHQL_PORT>/sessions/create" \
-H "Authorization: Bearer <API_TOKEN>" \
-H "Accept: application/json" \
-H "Content-Type: application/json" \
-d '{
"mac": "AA:BB:CC:DD:EE:FF",
"ip": "192.168.1.10",
"start_time": "2025-08-01T10:00:00",
"end_time": "2025-08-01T12:00:00",
"event_type_conn": "Connected",
"event_type_disc": "Disconnected"
}'
```
---
### Delete Sessions
* **DELETE** `/sessions/delete` → Delete all sessions for a given MAC
**Request Body:**
```json
{
"mac": "AA:BB:CC:DD:EE:FF"
}
```
**Response:**
```json
{
"success": true,
"message": "Deleted sessions for MAC AA:BB:CC:DD:EE:FF"
}
```
#### `curl` Example
```bash
curl -X DELETE "http://<server_ip>:<GRAPHQL_PORT>/sessions/delete" \
-H "Authorization: Bearer <API_TOKEN>" \
-H "Accept: application/json" \
-H "Content-Type: application/json" \
-d '{
"mac": "AA:BB:CC:DD:EE:FF"
}'
```
---
### List Sessions
* **GET** `/sessions/list` → Retrieve sessions optionally filtered by device and date range
**Query Parameters:**
* `mac` (optional) → Filter by device MAC address
* `start_date` (optional) → Filter sessions starting from this date (`YYYY-MM-DD`)
* `end_date` (optional) → Filter sessions ending by this date (`YYYY-MM-DD`)
**Example:**
```
/sessions/list?mac=AA:BB:CC:DD:EE:FF&start_date=2025-08-01&end_date=2025-08-21
```
**Response:**
```json
{
"success": true,
"sessions": [
{
"ses_MAC": "AA:BB:CC:DD:EE:FF",
"ses_Connection": "2025-08-01 10:00",
"ses_Disconnection": "2025-08-01 12:00",
"ses_Duration": "2h 0m",
"ses_IP": "192.168.1.10",
"ses_Info": ""
}
]
}
```
#### `curl` Example
```bash
curl -X GET "http://<server_ip>:<GRAPHQL_PORT>/sessions/list?mac=AA:BB:CC:DD:EE:FF&start_date=2025-08-01&end_date=2025-08-21" \
-H "Authorization: Bearer <API_TOKEN>" \
-H "Accept: application/json"
```
---
### Calendar View of Sessions
* **GET** `/sessions/calendar` → View sessions in calendar format
**Query Parameters:**
* `start` → Start date (`YYYY-MM-DD`)
* `end` → End date (`YYYY-MM-DD`)
**Example:**
```
/sessions/calendar?start=2025-08-01&end=2025-08-21
```
**Response:**
```json
{
"success": true,
"sessions": [
{
"resourceId": "AA:BB:CC:DD:EE:FF",
"title": "",
"start": "2025-08-01T10:00:00",
"end": "2025-08-01T12:00:00",
"color": "#00a659",
"tooltip": "Connection: 2025-08-01 10:00\nDisconnection: 2025-08-01 12:00\nIP: 192.168.1.10",
"className": "no-border"
}
]
}
```
#### `curl` Example
```bash
curl -X GET "http://<server_ip>:<GRAPHQL_PORT>/sessions/calendar?start=2025-08-01&end=2025-08-21" \
-H "Authorization: Bearer <API_TOKEN>" \
-H "Accept: application/json"
```
---
### Device Sessions
* **GET** `/sessions/<mac>` → Retrieve sessions for a specific device
**Query Parameters:**
* `period` → Period to retrieve sessions (`1 day`, `7 days`, `1 month`, etc.)
Default: `1 day`
**Example:**
```
/sessions/AA:BB:CC:DD:EE:FF?period=7 days
```
**Response:**
```json
{
"success": true,
"sessions": [
{
"ses_MAC": "AA:BB:CC:DD:EE:FF",
"ses_Connection": "2025-08-01 10:00",
"ses_Disconnection": "2025-08-01 12:00",
"ses_Duration": "2h 0m",
"ses_IP": "192.168.1.10",
"ses_Info": ""
}
]
}
```
#### `curl` Example
```bash
curl -X GET "http://<server_ip>:<GRAPHQL_PORT>/sessions/AA:BB:CC:DD:EE:FF?period=7%20days" \
-H "Authorization: Bearer <API_TOKEN>" \
-H "Accept: application/json"
```
---
### Session Events Summary
* **GET** `/sessions/session-events` → Retrieve a summary of session events
**Query Parameters:**
* `type` → Event type (`all`, `sessions`, `missing`, `voided`, `new`, `down`)
Default: `all`
* `period` → Period to retrieve events (`7 days`, `1 month`, etc.)
**Example:**
```
/sessions/session-events?type=all&period=7 days
```
**Response:**
Returns a list of events or sessions with formatted connection, disconnection, duration, and IP information.
#### `curl` Example
```bash
curl -X GET "http://<server_ip>:<GRAPHQL_PORT>/sessions/session-events?type=all&period=7%20days" \
-H "Authorization: Bearer <API_TOKEN>" \
-H "Accept: application/json"
```

92
docs/API_SETTINGS.md Executable file
View File

@@ -0,0 +1,92 @@
# Settings API Endpoints
Retrieve application settings stored in the configuration system. This endpoint is useful for quickly fetching individual settings such as `API_TOKEN` or `TIMEZONE`.
For bulk or structured access (all settings, schema details, or filtering), use the [GraphQL API Endpoint](API_GRAPHQL.md).
---
### Get a Setting
* **GET** `/settings/<key>` → Retrieve the value of a specific setting
**Path Parameter:**
* `key` → The setting key to retrieve (e.g., `API_TOKEN`, `TIMEZONE`)
**Authorization:**
Requires a valid API token in the `Authorization` header.
---
#### `curl` Example (Success)
```sh
curl 'http://<server_ip>:<GRAPHQL_PORT>/settings/API_TOKEN' \
-H 'Authorization: Bearer <API_TOKEN>' \
-H 'Accept: application/json'
```
**Response:**
```json
{
"success": true,
"value": "my-secret-token"
}
```
---
#### `curl` Example (Invalid Key)
```sh
curl 'http://<server_ip>:<GRAPHQL_PORT>/settings/DOES_NOT_EXIST' \
-H 'Authorization: Bearer <API_TOKEN>' \
-H 'Accept: application/json'
```
**Response:**
```json
{
"success": true,
"value": null
}
```
---
#### `curl` Example (Unauthorized)
```sh
curl 'http://<server_ip>:<GRAPHQL_PORT>/settings/API_TOKEN' \
-H 'Accept: application/json'
```
**Response:**
```json
{
"error": "Forbidden"
}
```
---
### Notes
* This endpoint is optimized for **direct retrieval of a single setting**.
* For **complex retrieval scenarios** (listing all settings, retrieving schema metadata like `setName`, `setDescription`, `setType`, or checking if a setting is overridden by environment variables), use the **GraphQL Settings Query**:
```sh
curl 'http://<server_ip>:<GRAPHQL_PORT>/graphql' \
-X POST \
-H 'Authorization: Bearer <API_TOKEN>' \
-H 'Content-Type: application/json' \
--data '{
"query": "query GetSettings { settings { settings { setKey setName setDescription setType setOptions setGroup setValue setEvents setOverriddenByEnv } count } }"
}'
```
See the [GraphQL API Endpoint](API_GRAPHQL.md) for more details.

125
docs/API_SYNC.md Executable file
View File

@@ -0,0 +1,125 @@
# Sync API Endpoint
---
The `/sync` endpoint is used by the **SYNC plugin** to synchronize data between multiple NetAlertX instances (e.g., from a node to a hub). It supports both **GET** and **POST** requests.
#### 9.1 GET `/sync`
Fetches data from a node to the hub. The data is returned as a **base64-encoded JSON file**.
**Example Request:**
```sh
curl 'http://<server>:<GRAPHQL_PORT>/sync' \
-H 'Authorization: Bearer <API_TOKEN>'
```
**Response Example:**
```json
{
"node_name": "NODE-01",
"status": 200,
"message": "OK",
"data_base64": "eyJkZXZpY2VzIjogW3siZGV2TWFjIjogIjAwOjExOjIyOjMzOjQ0OjU1IiwiZGV2TmFtZSI6ICJEZXZpY2UgMSJ9XSwgImNvdW50Ijog1fQ==",
"timestamp": "2025-08-24T10:15:00+10:00"
}
```
**Notes:**
* `data_base64` contains the full JSON data encoded in Base64.
* `node_name` corresponds to the `SYNC_node_name` setting on the node.
* Errors (e.g., missing file) return HTTP 500 with an error message.
---
#### 9.2 POST `/sync`
The **POST** endpoint is used by nodes to **send data to the hub**. The hub expects the data as **form-encoded fields** (application/x-www-form-urlencoded or multipart/form-data). The hub then stores the data in the plugin log folder for processing.
#### Required Fields
| Field | Type | Description |
| ----------- | ----------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `data` | string | The payload from the plugin or devices. Typically **plain text**, **JSON**, or **encrypted Base64** data. In your Python script, `encrypt_data()` is applied before sending. |
| `node_name` | string | The name of the node sending the data. Matches the nodes `SYNC_node_name` setting. Used to generate the filename on the hub. |
| `plugin` | string | The name of the plugin sending the data. Determines the filename prefix (`last_result.<plugin>...`). |
| `file_path` | string (optional) | Path of the local file being sent. Used only for logging/debugging purposes on the hub; **not required for processing**. |
---
### How the Hub Processes the POST Data
1. **Receives the data** and validates the API token.
2. **Stores the raw payload** in:
```
INSTALL_PATH/log/plugins/last_result.<plugin>.encoded.<node_name>.<sequence>.log
```
* `<plugin>` → plugin name from the POST request.
* `<node_name>` → node name from the POST request.
* `<sequence>` → incremented number for each submission.
3. **Decodes / decrypts the data** if necessary (Base64 or encrypted) before processing.
4. **Processes JSON payloads** (e.g., device info) to:
* Avoid duplicates by tracking `devMac`.
* Add metadata like `devSyncHubNode`.
* Insert new devices into the database.
5. **Renames files** to indicate they have been processed:
```
processed_last_result.<plugin>.<node_name>.<sequence>.log
```
---
### Example POST Payload
If a node is sending device data:
```bash
curl -X POST 'http://<hub>:<PORT>/sync' \
-H 'Authorization: Bearer <API_TOKEN>' \
-F 'data={"data":[{"devMac":"00:11:22:33:44:55","devName":"Device 1","devVendor":"Vendor A","devLastIP":"192.168.1.10"}]}' \
-F 'node_name=NODE-01' \
-F 'plugin=SYNC'
```
* The `data` field contains JSON with a **`data` array**, where each element is a **device object** or **plugin data object**.
* The `plugin` and `node_name` fields allow the hub to **organize and store the file correctly**.
* The data is only processed if the relevant plugins are enabled and run on the target server.
---
### Key Notes
* **Always use the same `plugin` and `node_name` values** for consistent storage.
* **Encrypted data**: The Python script uses `encrypt_data()` before sending, and the hub decodes it before processing.
* **Sequence numbers**: Every submission generates a new sequence, preventing overwriting previous data.
* **Form-encoded**: The hub expects `multipart/form-data` (cURL `-F`) or `application/x-www-form-urlencoded`.
**Storage Details:**
* Data is stored under `INSTALL_PATH/log/plugins` with filenames following the pattern:
```
last_result.<plugin>.encoded.<node_name>.<sequence>.log
```
* Both encoded and decoded files are tracked, and new submissions increment the sequence number.
* If storing fails, the API returns HTTP 500 with an error message.
* The data is only processed if the relevant plugins are enabled and run on the target server.
---
#### 9.3 Notes and Best Practices
* **Authorization Required** Both GET and POST require a valid API token.
* **Data Integrity** Ensure that `node_name` and `plugin` are consistent to avoid overwriting files.
* **Monitoring** Notifications are generated whenever data is sent or received (`write_notification`), which can be used for alerting or auditing.
* **Use Case** Typically used in multi-node deployments to consolidate device and event data on a central hub.

12
docs/API_TESTS.md Executable file
View File

@@ -0,0 +1,12 @@
### Unit Tests
>[!WARNING]
> Please note these test modify data in the database.
1. See the `/test` directory for available test cases. These are not exhaustive but cover the main API endpoints.
2. To run a test case, SSH into the container:
`sudo docker exec -it netalertx /bin/bash`
3. Inside the container, install pytest (if not already installed):
`pip install pytest`
4. Run a specific test case:
`pytest /app/test/TESTFILE.py`

34
docs/DEBUG_PHP.md Executable file
View File

@@ -0,0 +1,34 @@
# Debugging backend PHP issues
## Logs in UI
![Logs UI](./img/DEBUG/maintenance_debug_php.png)
You can view recent backend PHP errors directly in the **Maintenance > Logs** section of the UI. This provides quick access to logs without needing terminal access.
## Accessing logs directly
Sometimes, the UI might not be accessible. In that case, you can access the logs directly inside the container.
### Step-by-step:
1. **Open a shell into the container:**
```bash
docker exec -it netalertx /bin/sh
```
2. **Check the NGINX error log:**
```bash
cat /var/log/nginx/error.log
```
3. **Check the PHP application error log:**
```bash
cat /app/log/app.php_errors.log
```
These logs will help identify syntax issues, fatal errors, or startup problems when the UI fails to load properly.

View File

@@ -53,6 +53,9 @@ The function `guess_device_attributes(...)` runs a series of matching functions
4. IP pattern → `match_ip()`
5. Final fallback → defaults defined in the `NEWDEV_devIcon` and `NEWDEV_devType` settings.
> [!NOTE]
> The app will try guessing the device type or icon if `devType` or `devIcon` are `""` or `"null"`.
### Use of default values
The guessing process runs for every device **as long as the current type or icon still matches the default values**. Even if earlier heuristics return a match, the system continues evaluating additional clues — like name or IP — to try and replace placeholders.

View File

@@ -1,32 +1,42 @@
# Development environment set up
# Development Environment Setup
>[!NOTE]
> Replace `/development` with the path where your code files will be stored. The default container name is `netalertx` so there might be a conflict with your running containers.
I truly appreciate all contributions! To help keep this project maintainable, this guide provides an overview of project priorities, key design considerations, and overall philosophy. It also includes instructions for setting up your environment so you can start contributing right away.
## Development Guidelines
Before starting development, please scan the below development guidelines.
Before starting development, please review the following guidelines.
### Priority Order (Highest to Lowest)
1. 🔼 Fixing core bugs that lack workarounds.
2. 🔵 Adding core functionality that unlocks other features (e.g., plugins).
3. 🔵 Refactoring to enable faster development.
4. 🔽 UI improvements (PRs welcome).
1. 🔼 Fixing core bugs that lack workarounds
2. 🔵 Adding core functionality that unlocks other features (e.g., plugins)
3. 🔵 Refactoring to enable faster development
4. 🔽 UI improvements (PRs welcome, but low priority)
### Design Philosophy
Focus on core functionality and integrate with existing tools rather than reinventing the wheel.
Examples:
The application architecture is designed for extensibility and maintainability. It relies heavily on configuration manifests via plugins and settings to dynamically build the UI and populate the application with data from various sources.
- Using **Apprise** for notifications instead of implementing multiple separate gateways.
- Implementing **regex-based validation** instead of one-off validation for each setting.
For details, see:
- [Plugins Development](PLUGINS_DEV.md) (includes video)
- [Settings System](SETTINGS_SYSTEM.md)
> [!NOTE]
> UI changes have lower priority, however, PRs are welcome, but **keep them small & focused**.
Focus on **core functionality** and integrate with existing tools rather than reinventing the wheel.
Examples:
- Using **Apprise** for notifications instead of implementing multiple separate gateways
- Implementing **regex-based validation** instead of one-off validation for each setting
> [!NOTE]
> UI changes have lower priority. PRs are welcome, but please keep them **small and focused**.
## Development Environment Set Up
The following steps will guide you to set up your environment for local development and to run a custom docker build on your system. For most changes the container doesn't need to be rebuild which speeds up the development significantly.
>[!NOTE]
> Replace `/development` with the path where your code files will be stored. The default container name is `netalertx` so there might be a conflict with your running containers.
### 1. Download the code:
- `mkdir /development`
@@ -91,7 +101,7 @@ Most code changes can be tested without rebuilding the container. When working o
- `sudo docker exec -it netalertx /bin/bash`
- `pkill -f "python /app/server" && python /app/server & `
3. If none of the above work, restart the docker caontainer.
3. If none of the above work, restart the docker container.
- This is usually the last resort as sometimes the Docker engine becomes unresponsive and the whole engine needs to be restarted.
@@ -119,3 +129,6 @@ Most code changes can be tested without rebuilding the container. When working o
- Updating a Device
- Plugin functionality.
- Error log inspection.
> [!NOTE]
> Always run all available tests as per the [Testing documentation](API_TESTS.md).

View File

@@ -137,3 +137,67 @@ networks:
```
### Example 5: same as 3 but with a top-level root directory; also works in Portainer as-is
`docker-compose.yml`
```yaml
services:
netalertx:
container_name: netalertx
# use the below line if you want to test the latest dev image instead of the stable release
# image: "ghcr.io/jokob-sk/netalertx-dev:latest"
image: "ghcr.io/jokob-sk/netalertx:latest"
network_mode: "host"
restart: unless-stopped
volumes:
- ${APP_FOLDER}/netalertx/config:/app/config
- ${APP_FOLDER}/netalertx/db:/app/db
# (optional) useful for debugging if you have issues setting up the container
- ${APP_FOLDER}/netalertx/log:/app/log
# (API: OPTION 1) default -> use for performance
- type: tmpfs
target: /app/api
# (API: OPTION 2) use when debugging issues
# - ${APP_FOLDER}/netalertx/api:/app/api
environment:
- TZ=${TZ}
- PORT=${PORT}
- PUID=${PUID}
- PGID=${PGID}
- LISTEN_ADDR=${LISTEN_ADDR}
```
`.env` file
```yaml
APP_FOLDER=/path/to/local/NetAlertX/location
#ENVIRONMENT VARIABLES
PUID=200
PGID=300
TZ=America/New_York
LISTEN_ADDR=0.0.0.0
PORT=20211
#GLOBAL PATH VARIABLE
# you may want to create a dedicated user and group to run the container with
# sudo groupadd -g 300 nax-g
# sudo useradd -u 200 -g 300 nax-u
# mkdir -p $APP_FOLDER/{db,config,log}
# chown -R 200:300 $APP_FOLDER
# chmod -R 775 $APP_FOLDER
# DEVELOPMENT VARIABLES
# you can create multiple env files called .env.dev1, .env.dev2 etc and use them by running:
# docker compose --env-file .env.dev1 up -d
# you can then clone multiple dev copies of NetAlertX just make sure to change the APP_FOLDER and PORT variables in each .env.devX file
```
To run the container execute: `sudo docker-compose --env-file /path/to/.env up`

97
docs/DOCKER_PORTAINER.md Executable file
View File

@@ -0,0 +1,97 @@
# Deploying NetAlertX in Portainer (via Stacks)
This guide shows you how to set up **NetAlertX** using Portainers **Stacks** feature.
![Portainer > Stacks](./img/DOCKER/DOCKER_PORTAINER.png)
---
## 1. Prepare Your Host
Before deploying, make sure you have a folder on your Docker host for NetAlertX data. Replace `APP_FOLDER` with your preferred location, for example `/opt` here:
```bash
mkdir -p /opt/netalertx/config
mkdir -p /opt/netalertx/db
mkdir -p /opt/netalertx/log
```
---
## 2. Open Portainer Stacks
1. Log in to your **Portainer UI**.
2. Navigate to **Stacks****Add stack**.
3. Give your stack a name (e.g., `netalertx`).
---
## 3. Paste the Stack Configuration
Copy and paste the following YAML into the **Web editor**:
```yaml
services:
netalertx:
container_name: netalertx
# Use this line for stable release
image: "ghcr.io/jokob-sk/netalertx:latest"
# Or, use this for the latest development build
# image: "ghcr.io/jokob-sk/netalertx-dev:latest"
network_mode: "host"
restart: unless-stopped
volumes:
- ${APP_FOLDER}/netalertx/config:/app/config
- ${APP_FOLDER}/netalertx/db:/app/db
# Optional: logs (useful for debugging setup issues, comment out for performance)
- ${APP_FOLDER}/netalertx/log:/app/log
# API storage options:
# (Option 1) tmpfs (default, best performance)
- type: tmpfs
target: /app/api
# (Option 2) bind mount (useful for debugging)
# - ${APP_FOLDER}/netalertx/api:/app/api
environment:
- TZ=${TZ}
- PORT=${PORT}
- APP_CONF_OVERRIDE=${APP_CONF_OVERRIDE}
```
---
## 4. Configure Environment Variables
In the **Environment variables** section of Portainer, add the following:
* `APP_FOLDER=/opt` (or wherever you created the directories in step 1)
* `TZ=Europe/Berlin` (replace with your timezone)
* `PORT=22022` (or another port if needed)
* `APP_CONF_OVERRIDE={"GRAPHQL_PORT":"22023"}` (optional advanced settings)
---
## 5. Deploy the Stack
1. Scroll down and click **Deploy the stack**.
2. Portainer will pull the image and start NetAlertX.
3. Once running, access the app at:
```
http://<your-docker-host-ip>:22022
```
---
## 6. Verify and Troubleshoot
* Check logs via Portainer → **Containers**`netalertx`**Logs**.
* Logs are stored under `${APP_FOLDER}/netalertx/log` if you enabled that volume.
Once the application is running, configure it by reading the [initial setup](INITIAL_SETUP.md) guide, or [troubleshoot common issues](COMMON_ISSUES.md).

View File

@@ -91,6 +91,12 @@ You can confirm that `raspberrypi` now acts as a network device in two places:
> Hovering over devices in the tree reveals connection details and tooltips for quick inspection.
> [!NOTE]
> Selecting certain relationship types hides the device in the default device views.
> You can change this behavior by adjusting the `UI_hide_rel_types` setting, which by default is set to `["nic","virtual"]`.
> This means devices with `devParentRelType` set to `nic` or `virtual` will not be shown.
> All devices, regardless of relationship type, are always accessible in the **All devices** view.
---
## ✅ Summary

View File

@@ -79,10 +79,11 @@ Device-detecting plugins insert values into the `CurrentScan` database table. T
| `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 |
| `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 | | |

Binary file not shown.

After

Width:  |  Height:  |  Size: 160 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 36 KiB

After

Width:  |  Height:  |  Size: 7.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 180 KiB

View File

@@ -1,7 +1,7 @@
<?php
require 'php/templates/header.php';
require 'php/templates/notification.php';
require 'php/templates/modals.php';
?>
<!-- ----------------------------------------------------------------------- -->

View File

@@ -18,6 +18,7 @@
--color-yellow: #f39c12;
--color-red: #dd4b39;
--color-gray: #8c8c8c;
--color-black: #000;
}
.input-group .checkbox
@@ -604,6 +605,20 @@ body
cursor: default;
}
.btn-outline:hover
{
border: 1px solid var(--color-black);
background: transparent;
color: var(--color-black);
}
.btn-outline
{
border: 1px solid var(--color-gray);
background: transparent;
color: var(--color-gray);
}
/* -----------------------------------------------------------------------------
Customized Full Calendar
----------------------------------------------------------------------------- */
@@ -1726,6 +1741,16 @@ input[readonly] {
width: 92%;
}
#modal-ok
{
z-index: 1051; /*highest priority*/
}
#modal-form-plc
{
display: grid;
}
/* ----------------------------------------------------------------- */
/* NETWORK page */
/* ----------------------------------------------------------------- */

View File

@@ -419,6 +419,12 @@ td.highlight {
border: 1px solid #353c42;
}
.btn-outline {
border: 1px solid var(--color-black);
background: transparent;
color: var(--color-white);
}
/* Used in debug log page */
.log-red {
color: #ff4038;

View File

@@ -422,6 +422,12 @@
border: 1px solid #353c42;
}
.btn-outline {
border: 1px solid var(--color-black);
background: transparent;
color: var(--color-white);
}
/* Used in debug log page */
.log-red {
color: #ff4038;

View File

@@ -25,7 +25,7 @@
<!-- Content header--------------------------------------------------------- -->
<section class="content-header">
<?php require 'php/templates/notification.php'; ?>
<?php require 'php/templates/modals.php'; ?>
<h1 id="pageTitle">
&nbsp<small>Quering device info...</small>
@@ -497,7 +497,7 @@ function updateDevicePageName(mac) {
let owner = getDevDataByMac(mac, "devOwner");
// If data is missing, re-cache and retry once
if (mac != 'new' && (name === "Unknown" || owner === "Unknown")) {
if (mac != 'new' && (name === null|| owner === null)) {
console.warn("Device not found in cache, retrying after re-cache:", mac);
showSpinner();
cacheDevices().then(() => {

View File

@@ -53,14 +53,6 @@
var deviceData = JSON.parse(data);
// // Deactivate next previous buttons
// if (readAllData) {
// $('#btnPrevious').attr ('disabled','');
// $('#btnPrevious').addClass ('text-gray50');
// $('#btnNext').attr ('disabled','');
// $('#btnNext').addClass ('text-gray50');
// }
// some race condition, need to implement delay
setTimeout(() => {
$.get('php/server/query_json.php', {
@@ -256,25 +248,27 @@
// update readonly fields
handleReadOnly(settingsData, disabledFields);
};
};
// console.log(relevantSettings)
// console.log(relevantSettings)
generateSimpleForm(relevantSettings);
generateSimpleForm(relevantSettings);
toggleNetworkConfiguration(mac == 'Internet')
toggleNetworkConfiguration(mac == 'Internet')
initSelect2();
initHoverNodeInfo();
initSelect2();
initHoverNodeInfo();
hideSpinner();
hideSpinner();
})
})
}, 100);
});
}, 100);
});
}
}
// ----------------------------------------
@@ -290,18 +284,6 @@
});
}
// ----------------------------------------
// Show the description of a setting
function showDescriptionPopup(e) {
console.log($(e).attr("my-set-key"));
showModalOK("Info", getString($(e).attr("my-set-key") + '_description'))
}
// -----------------------------------------------------------------------------
// Save device data to DB
function setDeviceData(direction = '', refreshCallback = '') {

View File

@@ -683,11 +683,19 @@ function initializeDatatable (status) {
return JSON.stringify(query); // Send the JSON request
},
"dataSrc": function (json) {
// Set the total number of records for pagination
json.recordsTotal = json.devices.count || 0;
json.recordsFiltered = json.devices.count || 0;
"dataSrc": function (res) {
console.log("Raw response:", res);
const json = res["data"];
// Set the total number of records for pagination at the *root level* so DataTables sees them
res.recordsTotal = json.devices.count || 0;
res.recordsFiltered = json.devices.count || 0;
// console.log("recordsTotal:", res.recordsTotal, "recordsFiltered:", res.recordsFiltered);
// console.log("tableRows:", tableRows);
// Return only the array of rows for the table
return json.devices.devices.map(device => {
// Convert each device record into the required DataTable row format
// Order has to be the same as in the UI_device_columns setting options
@@ -701,10 +709,10 @@ function initializeDatatable (status) {
device.devFirstConnection || "",
device.devLastConnection || "",
device.devLastIP || "",
device.devIsRandomMac || "", // Custom logic for randomized MAC
device.devIsRandomMac || "",
device.devStatus || "",
device.devMac || "", // hidden
device.devIpLong || "", // IP orderable
device.devMac || "",
device.devIpLong || "",
device.rowid || "",
device.devParentMAC || "",
device.devParentChildrenCount || 0,
@@ -729,7 +737,6 @@ function initializeDatatable (status) {
for (let index = 0; index < tableColumnOrder.length; index++) {
newRow.push(originalRow[tableColumnOrder[index]]);
}
return newRow;
});
}
@@ -1004,9 +1011,7 @@ function initializeDatatable (status) {
}
});
});
}

View File

@@ -4,7 +4,7 @@
"display": "standalone",
"icons": [
{
"src": "",
"src": "/img/NetAlertX_logo.png",
"sizes": "180x180",
"type": "image/png"
}

View File

@@ -12,7 +12,7 @@ var timerRefreshData = ''
var emptyArr = ['undefined', "", undefined, null, 'null'];
var UI_LANG = "English";
const allLanguages = ["en_us", "es_es", "de_de", "fr_fr", "it_it", "ru_ru", "nb_no", "pl_pl", "pt_br", "tr_tr", "zh_cn", "cs_cz", "ar_ar", "ca_ca", "uk_ua"]; // needs to be same as in lang.php
const allLanguages = ["en_us", "es_es", "de_de", "fr_fr", "it_it", "ru_ru", "nb_no", "pl_pl", "pt_br", "pt_pt", "tr_tr", "zh_cn", "cs_cz", "ar_ar", "ca_ca", "uk_ua"]; // needs to be same as in lang.php
var settingsJSON = {}
@@ -328,6 +328,9 @@ function getLangCode() {
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;
@@ -364,75 +367,70 @@ function getLangCode() {
// -----------------------------------------------------------------------------
function localizeTimestamp(input) {
let tz = getSetting("TIMEZONE") || 'Europe/Berlin';
// Convert to string and trim
input = String(input || '').trim();
// Normalize multiple spaces and remove commas
const cleaned = input.replace(',', ' ').replace(/\s+/g, ' ');
// DD/MM/YYYY format check
const dateTimeParts = cleaned.split(' ');
if (dateTimeParts.length >= 2 && dateTimeParts[0].includes('/')) {
const [day, month, year] = dateTimeParts[0].split('/');
const timePart = dateTimeParts[1];
if (day && month && year && timePart) {
const isoString = `${year}-${month}-${day}T${timePart.length === 5 ? timePart + ':00' : timePart}`;
const date = new Date(isoString);
if (!isFinite(date)) return 'b-';
return new Intl.DateTimeFormat('default', {
timeZone: tz,
year: 'numeric',
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit',
second: '2-digit',
hour12: false
}).format(date);
}
}
// ISO style YYYY-MM-DD HH:mm(:ss)?
const match = cleaned.match(/^(\d{4}-\d{2}-\d{2}) (\d{2}:\d{2})(:\d{2})?$/);
if (match) {
let iso = `${match[1]}T${match[2]}${match[3] || ':00'}`;
const date = new Date(iso);
if (!isFinite(date)) return 'c-';
// ✅ 1. Unix timestamps (10 or 13 digits)
if (/^\d+$/.test(input)) {
const ms = input.length === 10 ? parseInt(input, 10) * 1000 : parseInt(input, 10);
return new Intl.DateTimeFormat('default', {
timeZone: tz,
year: 'numeric',
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit',
second: '2-digit',
year: 'numeric', month: '2-digit', day: '2-digit',
hour: '2-digit', minute: '2-digit', second: '2-digit',
hour12: false
}).format(new Date(ms));
}
// ✅ 2. European DD/MM/YYYY
let match = input.match(/^(\d{1,2})\/(\d{1,2})\/(\d{4})(?:[ ,]+(\d{1,2}:\d{2}(?::\d{2})?))?(.*)$/);
if (match) {
let [ , d, m, y, t = "00:00:00", tzPart = "" ] = match;
const iso = `${y}-${m.padStart(2,'0')}-${d.padStart(2,'0')}T${t.length===5?t+":00":t}${tzPart}`;
return formatSafe(iso, tz);
}
// ✅ 3. US MM/DD/YYYY
match = input.match(/^(\d{1,2})\/(\d{1,2})\/(\d{4})(?:[ ,]+(\d{1,2}:\d{2}(?::\d{2})?))?(.*)$/);
if (match) {
let [ , m, d, y, t = "00:00:00", tzPart = "" ] = match;
const iso = `${y}-${m.padStart(2,'0')}-${d.padStart(2,'0')}T${t.length===5?t+":00":t}${tzPart}`;
return formatSafe(iso, tz);
}
// ✅ 4. ISO-style (with T, Z, offsets)
match = input.match(/^(\d{4}-\d{1,2}-\d{1,2})[ T](\d{1,2}:\d{2}(?::\d{2})?)(Z|[+-]\d{2}:?\d{2})?$/);
if (match) {
let [ , ymd, time, offset = "" ] = match;
// normalize to YYYY-MM-DD
let [y, m, d] = ymd.split('-').map(x => x.padStart(2,'0'));
const iso = `${y}-${m}-${d}T${time.length===5?time+":00":time}${offset}`;
return formatSafe(iso, tz);
}
// ✅ 5. RFC2822 / "25 Aug 2025 13:45:22 +0200"
match = input.match(/^\d{1,2} [A-Za-z]{3,} \d{4}/);
if (match) {
return formatSafe(input, tz);
}
// ✅ 6. Fallback (whatever Date() can parse)
return formatSafe(input, tz);
function formatSafe(str, tz) {
const date = new Date(str);
if (!isFinite(date)) {
console.error(`ERROR: Couldn't parse date: '${str}' with TIMEZONE ${tz}`);
return 'Failed conversion - Check browser console';
}
return new Intl.DateTimeFormat('default', {
timeZone: tz,
year: 'numeric', month: '2-digit', day: '2-digit',
hour: '2-digit', minute: '2-digit', second: '2-digit',
hour12: false
}).format(date);
}
// Fallback: try to parse any other string input
const date = new Date(input);
if (!isFinite(date)) return 'Failed conversion: ' + input;
return new Intl.DateTimeFormat('default', {
timeZone: tz,
year: 'numeric',
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit',
second: '2-digit',
hour12: false
}).format(date);
}
// ----------------------------------------------------
/**
* Replaces double quotes within single-quoted strings, then converts all single quotes to double quotes,
@@ -609,7 +607,7 @@ function createDeviceLink(input)
{
if(checkMacOrInternet(input))
{
return `<span class="anonymizeMac"><a href="/deviceDetails.php?mac=${input}" target="_blank">${getNameByMacAddress(input)}</a><span>`
return `<span class="anonymizeMac"><a href="/deviceDetails.php?mac=${input}" target="_blank">${getDevDataByMac(input, "devName")}</a><span>`
}
return input;
@@ -813,7 +811,6 @@ function forceLoadUrl(relativeUrl) {
}
// -----------------------------------------------------------------------------
function navigateToDeviceWithIp (ip) {
@@ -836,11 +833,6 @@ function navigateToDeviceWithIp (ip) {
});
}
// -----------------------------------------------------------------------------
function getNameByMacAddress(macAddress) {
return getDevDataByMac(macAddress, "devName")
}
// -----------------------------------------------------------------------------
// Check if MAC or Internet
function checkMacOrInternet(inputStr) {
@@ -1013,7 +1005,7 @@ function getDevDataByMac(macAddress, dbColumn) {
if (!devicesCache || devicesCache == "") {
console.error(`Session variable "${sessionDataKey}" not found.`);
return "Unknown";
return null;
}
const devices = JSON.parse(devicesCache);
@@ -1032,7 +1024,8 @@ function getDevDataByMac(macAddress, dbColumn) {
}
}
return "Unknown"; // Return a default value if MAC address is not found
console.error("⚠ Device with MAC not found:" + macAddress)
return null; // Return a default value if MAC address is not found
}
// -----------------------------------------------------------------------------

View File

@@ -161,6 +161,139 @@ function showModalFieldInput(
$(`#${prefix}`).modal("show");
}
// -----------------------------------------------------------------------------
function showModalPopupForm(
title,
message,
btnCancel = getString("Gen_Cancel"),
btnOK = getString("Gen_Okay"),
curValue = null,
popupFormJson = null,
parentSettingKey = null,
triggeredBy = null
) {
// set captions
prefix = "modal-form";
console.log(popupFormJson);
$(`#${prefix}-title`).html(title);
$(`#${prefix}-message`).html(message);
$(`#${prefix}-cancel`).html(btnCancel);
$(`#${prefix}-OK`).html(btnOK);
// if curValue not null
if (curValue)
{
initialValues = JSON.parse(atob(curValue));
}
outputHtml = "";
if (Array.isArray(popupFormJson)) {
popupFormJson.forEach((field, index) => {
// You'll need to define these or map them from `field`
const setKey = field.function || `field_${index}`;
const setName = getString(`${parentSettingKey}_popupform_${setKey}_name`);
const labelClasses = "col-sm-2"; // example, or from your obj.labelClasses
const inputClasses = "col-sm-10"; // example, or from your obj.inputClasses
let initialValue = '';
if (curValue && Array.isArray(initialValues)) {
const match = initialValues.find(
v => v[1] == setKey
);
if (match) {
initialValue = match[3];
}
}
const fieldOptionsOverride = field.type?.elements[0]?.elementOptions || [];
const setValue = initialValue;
const setType = JSON.stringify(field.type);
const setEvents = field.events || []; // default to empty array if missing
const setObj = { setKey, setValue, setType, setEvents };
// Generate the input field HTML
const inputFormHtml = `
<div class="form-group col-xs-12">
<label id="${setKey}_label" class="${labelClasses}"> ${setName}
<i my-set-key="${parentSettingKey}_popupform_${setKey}"
title="${getString("Settings_Show_Description")}"
class="fa fa-circle-info pointer helpIconSmallTopRight"
onclick="showDescriptionPopup(this)">
</i>
</label>
<div class="${inputClasses}">
${generateFormHtml(
null, // settingsData only required for datatables
setObj,
null,
fieldOptionsOverride,
null
)}
</div>
</div>
`;
// Append to result
outputHtml += inputFormHtml;
});
}
$(`#modal-form-plc`).html(outputHtml);
// Bind OK button click event
$(`#${prefix}-OK`).off("click").on("click", function() {
let settingsArray = [];
if (Array.isArray(popupFormJson)) {
popupFormJson.forEach(field => {
collectSetting(
`${parentSettingKey}_popupform`, // prefix
field.function, // setCodeName
field.type, // setType (object)
settingsArray
);
});
}
// Encode settings
const jsonData = JSON.stringify(settingsArray);
const encodedValue = btoa(jsonData);
// Get label from the FIRST field (value in 4th column)
const label = settingsArray[0][3]
// Add new option to target select
const selectId = parentSettingKey;
// If triggered by an option, update it; otherwise append new
if (triggeredBy && $(triggeredBy).is("option")) {
// Update existing option
$(triggeredBy)
.attr("value", encodedValue)
.text(label);
} else {
const newOption = $("<option class='interactable-option'></option>")
.attr("value", encodedValue)
.text(label);
$("#" + selectId).append(newOption);
initListInteractionOptions(newOption);
}
console.log("Collected popup form settings:", settingsArray);
if (typeof modalCallbackFunction === "function") {
modalCallbackFunction(settingsArray);
}
$(`#${prefix}`).modal("hide");
});
// Show modal
$(`#${prefix}`).modal("show");
}
// -----------------------------------------------------------------------------
function modalDefaultOK() {
// Hide modal

View File

@@ -67,6 +67,15 @@ function getPluginConfig(pluginsData, prefix) {
return result;
}
// ----------------------------------------
// Show the description of a setting
function showDescriptionPopup(e) {
console.log($(e).attr("my-set-key"));
showModalOK("Info", getString($(e).attr("my-set-key") + '_description'))
}
// -------------------------------------------------------------------
// Generate plugin HTML card based on prefixes in an array
function pluginCards(prefixesOfEnabledPlugins, includeSettings) {
@@ -237,13 +246,6 @@ function settingsCollectedCorrectly(settingsArray, settingsJSON_DB) {
// Manipulating Editable List options
// -------------------------------------------------------------------
// ---------------------------------------------------------
// Add row to datatable
function addDataTableRow(el)
{
alert("a")
}
// ---------------------------------------------------------
// Clone datatable row
function cloneDataTableRow(el){
@@ -299,6 +301,33 @@ function removeDataTableRow(el) {
}
}
// ---------------------------------------------------------
// Add item via pop up form dialog
function addViaPopupForm(element) {
console.log(element);
const toId = $(element).attr("my-input-to");
const curValue = $(`#${toId}`).val();
const parsed = JSON.parse(atob($(`#${toId}`).data("elementoptionsbase64")));
const popupFormJson = parsed.find(obj => "popupForm" in obj)?.popupForm ?? null;
console.log(`toId | curValue: ${toId} | ${curValue}`);
showModalPopupForm(
`<i class="fa-solid fa-square-plus"></i> ${getString("Gen_Add")}`, // title
"", // message
getString("Gen_Cancel"), // btnCancel
getString("Gen_Add"), // btnOK
null, // curValue
popupFormJson, // popupform
toId, // parentSettingKey
element // triggeredBy
);
// flag something changes to prevent navigating from page
settingsChanged();
}
// ---------------------------------------------------------
// Add item to list
function addList(element, clearInput = true) {
@@ -427,18 +456,41 @@ function initListInteractionOptions(element) {
// Perform action based on click count
if (clickCounter === 1) {
// Single-click action
showModalFieldInput(
`<i class="fa fa-pen-to-square"></i> ${getString(
"Gen_Update_Value"
)}`,
getString("settings_update_item_warning"),
getString("Gen_Cancel"),
getString("Gen_Update"),
$option.html(),
function () {
updateOptionItem($option, $(`#modal-field-input-field`).val());
}
);
const $parent = $option.parent();
const transformers = $parent.attr("my-transformers");
if (transformers && transformers === "name|base64") {
// Parent has my-transformers="name|base64"
const toId = $parent.attr("id");
const curValue = $option.val();
const parsed = JSON.parse(atob($parent.data("elementoptionsbase64")));
const popupFormJson = parsed.find(obj => "popupForm" in obj)?.popupForm ?? null;
showModalPopupForm(
`<i class="fa fa-pen-to-square"></i> ${getString("Gen_Update_Value")}`, // title
"", // message
getString("Gen_Cancel"), // btnCancel
getString("Gen_Update"), // btnOK
curValue, // curValue
popupFormJson, // popupform
toId, // parentSettingKey
this // triggeredBy
);
} else {
// Fallback to normal field input
showModalFieldInput(
`<i class="fa fa-pen-to-square"></i> ${getString("Gen_Update_Value")}`,
getString("settings_update_item_warning"),
getString("Gen_Cancel"),
getString("Gen_Update"),
$option.html(),
function () {
updateOptionItem($option, $(`#modal-field-input-field`).val());
}
);
}
} else if (clickCounter === 2) {
// Double-click action
removeOptionItem($option);
@@ -622,8 +674,6 @@ function generateOptionsOrSetOptions(
// obj.push({ id: item, name: item })
options = arrayToObject(createArray(overrideOptions ? overrideOptions : getSettingOptions(setKey)))
// Call to render lists
renderList(
options,
@@ -633,8 +683,6 @@ function generateOptionsOrSetOptions(
targetField,
transformers
);
}
@@ -655,6 +703,13 @@ function applyTransformers(val, transformers) {
val = btoa(val);
}
break;
case "name|base64":
// // Implement base64 logic
// if (!isBase64(val)) {
// val = btoa(val);
// }
val = val; // probably TODO ⚠
break;
case "getString":
// no change
val = val;
@@ -667,7 +722,7 @@ function applyTransformers(val, transformers) {
}
// ------------------------------------------------------------
// Function to reverse transformers applied to a value
// Function to reverse transformers applied to a value - returns the LABEL
function reverseTransformers(val, transformers) {
transformers.reverse().forEach((transformer) => {
switch (transformer) {
@@ -681,6 +736,13 @@ function reverseTransformers(val, transformers) {
val = atob(val);
}
break;
case "name|base64":
// Implement base64 decoding logic
if (isBase64(val)) {
val = JSON.parse(atob(val))[0][3];
}
val = val; // probably TODO ⚠
break;
case "getString":
// retrieve string
val = getString(val);
@@ -720,8 +782,8 @@ const handleElementOptions = (setKey, elementOptions, transformers, val) => {
let customParams = "";
let customId = "";
let columns = [];
let base64Regex = "";
let base64Regex = "";
let elementOptionsBase64 = btoa(JSON.stringify(elementOptions));
elementOptions.forEach((option) => {
if (option.prefillValue) {
@@ -804,7 +866,8 @@ const handleElementOptions = (setKey, elementOptions, transformers, val) => {
customParams,
customId,
columns,
base64Regex
base64Regex,
elementOptionsBase64
};
};
@@ -934,13 +997,102 @@ function genListWithInputSet(options, valuesArray, targetField, transformers, pl
$("#" + placeholder).replaceWith(listHtml);
}
// -----------------------------------------------------------------
// Collects a setting based on code name
function collectSetting(prefix, setCodeName, setType, settingsArray) {
// Parse setType if it's a JSON string
const setTypeObject = (typeof setType === "string")
? JSON.parse(processQuotes(setType))
: setType;
const dataType = setTypeObject.dataType;
// Pick element with input value
let elements = setTypeObject.elements.filter(el => el.elementHasInputValue === 1);
let elementWithInputValue = elements.length === 0
? setTypeObject.elements[setTypeObject.elements.length - 1]
: elements[0];
const { elementType, elementOptions = [], transformers = [] } = elementWithInputValue;
const opts = handleElementOptions('none', elementOptions, transformers, val = "");
// Map of handlers
const handlers = {
datatableString: () => {
const value = collectTableData(`#${setCodeName}_table`);
return btoa(JSON.stringify(value));
},
simpleValue: () => {
let value = $(`#${setCodeName}`).val();
return applyTransformers(value, transformers);
},
checkbox: () => {
let value = $(`#${setCodeName}`).is(':checked') ? 1 : 0;
if (dataType === "boolean") {
value = value === 1 ? "True" : "False";
}
return applyTransformers(value, transformers);
},
array: () => {
let temps = [];
if (opts.isOrdeable) {
temps = $(`#${setCodeName}`).val();
} else {
const sel = $(`#${setCodeName}`).attr("my-editable") === "true" ? "" : ":selected";
$(`#${setCodeName} option${sel}`).each(function() {
const vl = $(this).val();
if (vl !== '') {
temps.push(applyTransformers(vl, transformers));
}
});
}
return JSON.stringify(temps);
},
none: () => "",
json: () => {
let value = $(`#${setCodeName}`).val();
value = applyTransformers(value, transformers);
return JSON.stringify(value, null, 2);
},
fallback: () => {
console.error(`[collectSetting] Couldn't determine how to handle (${setCodeName}|${dataType}|${opts.inputType})`);
let value = $(`#${setCodeName}`).val();
return applyTransformers(value, transformers);
}
};
// Select handler key
let handlerKey;
if (dataType === "string" && elementType === "datatable") {
handlerKey = "datatableString";
} else if (dataType === "string" ||
(dataType === "integer" && (opts.inputType === "number" || opts.inputType === "text"))) {
handlerKey = "simpleValue";
} else if (opts.inputType === "checkbox") {
handlerKey = "checkbox";
} else if (dataType === "array") {
handlerKey = "array";
} else if (dataType === "none") {
handlerKey = "none";
} else if (dataType === "json") {
handlerKey = "json";
} else {
handlerKey = "fallback";
}
const value = handlers[handlerKey]();
settingsArray.push([prefix, setCodeName, dataType, value]);
return settingsArray;
}
// ------------------------------------------------------------------------------
// Generate the form control for setting
function generateFormHtml(settingsData, set, overrideValue, overrideOptions, originalSetKey) {
let inputHtml = '';
isEmpty(overrideValue) ? inVal = set['setValue'] : inVal = overrideValue;
const setKey = set['setKey'];
const setType = set['setType'];
@@ -955,6 +1107,8 @@ function generateFormHtml(settingsData, set, overrideValue, overrideOptions, ori
// }
// Parse the setType JSON string
// console.log(processQuotes(setType));
const setTypeObject = JSON.parse(processQuotes(setType))
const dataType = setTypeObject.dataType;
const elements = setTypeObject.elements || [];
@@ -982,7 +1136,8 @@ function generateFormHtml(settingsData, set, overrideValue, overrideOptions, ori
customParams,
customId,
columns,
base64Regex
base64Regex,
elementOptionsBase64
} = handleElementOptions(setKey, elementOptions, transformers, inVal);
// Override value
@@ -1014,6 +1169,7 @@ function generateFormHtml(settingsData, set, overrideValue, overrideOptions, ori
my-customparams="${customParams}"
my-customid="${customId}"
my-originalSetKey="${originalSetKey}"
data-elementoptionsbase64="${elementOptionsBase64}"
${multi}
${readOnly ? "disabled" : ""}>
<option value="" id="${setKey + "_temp_"}"></option>
@@ -1051,6 +1207,7 @@ function generateFormHtml(settingsData, set, overrideValue, overrideOptions, ori
my-originalSetKey="${originalSetKey}"
my-input-from="${sourceIds}"
my-input-to="${setKey}"
data-elementoptionsbase64="${elementOptionsBase64}"
onclick="${onClick}">
${getString(getStringKey)}
</button>`;

View File

@@ -782,17 +782,24 @@ function initSelect2() {
// ------------------------------------------
// Render a device link with hover-over functionality
function renderDeviceLink(data, container, useName = false) {
if (!data.id) return data.text; // default placeholder etc.
// If no valid MAC, return placeholder text
if (!data.id || !isValidMac(data.id)) {
return `<span>${data.text}<span/>`;
}
const device = getDevDataByMac(data.id);
if (!device) {
return data.text;
}
// Build and return badge parts
const badge = getStatusBadgeParts(
device.devPresentLastScan,
device.devAlertDown,
device.devMac
);
// Add badge class and hover-info class to container
// badge class and hover-info class to container
$(container)
.addClass(`${badge.cssClass} hover-node-info`)
.attr({

View File

@@ -1,6 +1,6 @@
<?php
require 'php/templates/header.php';
require 'php/templates/notification.php';
require 'php/templates/modals.php';
?>
<!-- Page ------------------------------------------------------------------ -->

View File

@@ -136,7 +136,8 @@
customParams,
customId,
columns,
base64Regex
base64Regex,
elementOptionsBase64
} = handleElementOptions('none', elementOptions, transformers, val = "");
// render based on element type

View File

@@ -1,6 +1,6 @@
<?php
require 'php/templates/header.php';
require 'php/templates/notification.php';
require 'php/templates/modals.php';
?>
<script>
@@ -197,7 +197,7 @@
<div class="col-sm-9">
${isRootNode ? '' : `<a class="anonymize" href="#">`}
<span my-data-mac="${node.parent_mac}" data-mac="${node.parent_mac}" data-devIsNetworkNodeDynamic="1" onclick="handleNodeClick(this)">
${isRootNode ? getString('Network_Root') : getNameByMacAddress(node.parent_mac)}
${isRootNode ? getString('Network_Root') : getDevDataByMac(node.parent_mac, "devName")}
</span>
${isRootNode ? '' : `</a>`}
</div>

View File

@@ -7,6 +7,10 @@
# Puche 2022+ jokob jokob@duck.com GNU GPLv3
//------------------------------------------------------------------------------
// 🔺----- API ENDPOINTS SUPERSEDED -----🔺
// check server/api_server/api_server_start.py for equivalents
// equivalent: /dbquery
// 🔺----- API ENDPOINTS SUPERSEDED -----🔺
//------------------------------------------------------------------------------
// External files

View File

@@ -8,6 +8,10 @@
# Puche 2021 / 2022+ jokob jokob@duck.com GNU GPLv3
//------------------------------------------------------------------------------
// 🔺----- API ENDPOINTS SUPERSEDED -----🔺
// check server/api_server/api_server_start.py for equivalents
// 🔺----- API ENDPOINTS SUPERSEDED -----🔺
// External files
require dirname(__FILE__).'/init.php';
@@ -26,35 +30,31 @@
if (isset ($_REQUEST['action']) && !empty ($_REQUEST['action'])) {
$action = $_REQUEST['action'];
switch ($action) {
case 'getServerDeviceData': getServerDeviceData(); break;
case 'setDeviceData': setDeviceData(); break;
case 'deleteDevice': deleteDevice(); break;
case 'deleteAllWithEmptyMACs': deleteAllWithEmptyMACs(); break;
// check server/api_server/api_server_start.py for equivalents
case 'getServerDeviceData': getServerDeviceData(); break; // equivalent: get_device_data
case 'setDeviceData': setDeviceData(); break; // equivalent: set_device_data
case 'deleteDevice': deleteDevice(); break; // equivalent: delete_device(mac)
case 'deleteAllWithEmptyMACs': deleteAllWithEmptyMACs(); break; // equivalent: delete_all_with_empty_macs
case 'deleteAllDevices': deleteAllDevices(); break;
case 'deleteUnknownDevices': deleteUnknownDevices(); break;
case 'deleteEvents': deleteEvents(); break;
case 'deleteEvents30': deleteEvents30(); break;
case 'deleteActHistory': deleteActHistory(); break;
case 'deleteDeviceEvents': deleteDeviceEvents(); break;
case 'resetDeviceProps': resetDeviceProps(); break;
case 'PiaBackupDBtoArchive': PiaBackupDBtoArchive(); break;
case 'PiaRestoreDBfromArchive': PiaRestoreDBfromArchive(); break;
case 'PiaPurgeDBBackups': PiaPurgeDBBackups(); break;
case 'ExportCSV': ExportCSV(); break;
case 'ImportCSV': ImportCSV(); break;
case 'deleteAllDevices': deleteAllDevices(); break; // equivalent: delete_devices(macs)
case 'deleteUnknownDevices': deleteUnknownDevices(); break; // equivalent: delete_unknown_devices
case 'deleteEvents': deleteEvents(); break; // equivalent: delete_events
case 'deleteEvents30': deleteEvents30(); break; // equivalent: delete_events_30
case 'deleteActHistory': deleteActHistory(); break; // equivalent: delete_online_history
case 'deleteDeviceEvents': deleteDeviceEvents(); break; // equivalent: delete_device_events(mac)
case 'resetDeviceProps': resetDeviceProps(); break; // equivalent: reset_device_props
case 'ExportCSV': ExportCSV(); break; // equivalent: export_devices
case 'ImportCSV': ImportCSV(); break; // equivalent: import_csv
case 'getDevicesTotals': getDevicesTotals(); break;
case 'getDevicesListCalendar': getDevicesListCalendar(); break; //todo: slowly deprecate this
case 'getDevicesTotals': getDevicesTotals(); break; // equivalent: devices_totals
case 'getDevicesListCalendar': getDevicesListCalendar(); break; // equivalent: devices_by_status
case 'updateNetworkLeaf': updateNetworkLeaf(); break;
case 'getIcons': getIcons(); break;
case 'getActions': getActions(); break;
case 'getDevices': getDevices(); break;
case 'copyFromDevice': copyFromDevice(); break;
case 'wakeonlan': wakeonlan(); break;
case 'updateNetworkLeaf': updateNetworkLeaf(); break; // equivalent: update_device_column(mac, column_name, column_value)
default: logServerConsole ('Action: '. $action); break;
case 'copyFromDevice': copyFromDevice(); break; // equivalent: copy_device(mac_from, mac_to)
case 'wakeonlan': wakeonlan(); break; // equivalent: wakeonlan
default: logServerConsole ('Action: '. $action); break; // equivalent:
}
}
@@ -517,92 +517,6 @@ function deleteActHistory() {
}
}
//------------------------------------------------------------------------------
// Backup DB to Archiv
//------------------------------------------------------------------------------
function PiaBackupDBtoArchive() {
// prepare fast Backup
$dbfilename = 'app.db';
$file = '../../../db/'.$dbfilename;
$newfile = '../../../db/'.$dbfilename.'.latestbackup';
// copy files as a fast Backup
if (!copy($file, $newfile)) {
echo lang('BackDevices_Backup_CopError');
} else {
// Create archive with actual date
$Pia_Archive_Name = 'appdb_'.date("Ymd_His").'.zip';
$Pia_Archive_Path = '../../../db/';
exec('zip -j '.$Pia_Archive_Path.$Pia_Archive_Name.' ../../../db/'.$dbfilename, $output);
// chheck if archive exists
if (file_exists($Pia_Archive_Path.$Pia_Archive_Name) && filesize($Pia_Archive_Path.$Pia_Archive_Name) > 0) {
echo lang('BackDevices_Backup_okay').': ('.$Pia_Archive_Name.')';
unlink($newfile);
echo("<meta http-equiv='refresh' content='1'>");
} else {
echo lang('BackDevices_Backup_Failed').' ('.$dbfilename.'.latestbackup)';
}
}
}
//------------------------------------------------------------------------------
// Restore DB from Archiv
//------------------------------------------------------------------------------
function PiaRestoreDBfromArchive() {
// prepare fast Backup
$file = '../../../db/'.$dbfilename;
$oldfile = '../../../db/'.$dbfilename.'.prerestore';
// copy files as a fast Backup
if (!copy($file, $oldfile)) {
echo lang('BackDevices_Restore_CopError');
} else {
// extract latest archive and overwrite the actual .db
$Pia_Archive_Path = '../../../db/';
exec('/bin/ls -Art '.$Pia_Archive_Path.'*.zip | /bin/tail -n 1 | /usr/bin/xargs -n1 /bin/unzip -o -d ../../../db/', $output);
// check if the .db exists
if (file_exists($file)) {
echo lang('BackDevices_Restore_okay');
unlink($oldfile);
echo("<meta http-equiv='refresh' content='1'>");
} else {
echo lang('BackDevices_Restore_Failed');
}
}
}
//------------------------------------------------------------------------------
// Purge Backups
//------------------------------------------------------------------------------
function PiaPurgeDBBackups() {
$Pia_Archive_Path = '../../../db';
$Pia_Backupfiles = array();
$files = array_diff(scandir($Pia_Archive_Path, SCANDIR_SORT_DESCENDING), array('.', '..', $dbfilename, 'netalertxdb-reset.zip'));
foreach ($files as &$item)
{
$item = $Pia_Archive_Path.'/'.$item;
if (stristr($item, 'setting_') == '') {array_push($Pia_Backupfiles, $item);}
}
if (sizeof($Pia_Backupfiles) > 3)
{
rsort($Pia_Backupfiles);
unset($Pia_Backupfiles[0], $Pia_Backupfiles[1], $Pia_Backupfiles[2]);
$Pia_Backupfiles_Purge = array_values($Pia_Backupfiles);
for ($i = 0; $i < sizeof($Pia_Backupfiles_Purge); $i++)
{
unlink($Pia_Backupfiles_Purge[$i]);
}
}
echo lang('BackDevices_DBTools_Purge');
echo("<meta http-equiv='refresh' content='1'>");
}
//------------------------------------------------------------------------------
// Export CSV of devices
//------------------------------------------------------------------------------
@@ -827,75 +741,6 @@ function getDevicesListCalendar() {
// Query Device Data
//------------------------------------------------------------------------------
//------------------------------------------------------------------------------
function getIcons() {
global $db;
// Device Data
$sql = 'select devIcon from Devices group by devIcon';
$result = $db->query($sql);
// arrays of rows
$tableData = array();
while ($row = $result -> fetchArray (SQLITE3_ASSOC)) {
$icon = handleNull($row['devIcon'], "<i class='fa fa-laptop'></i>");
// Push row data
$tableData[] = array('id' => $icon,
'name' => $icon );
}
// Control no rows
if (empty($tableData)) {
$tableData = [];
}
// Return json
echo (json_encode ($tableData));
}
//------------------------------------------------------------------------------
function getActions() {
$tableData = array(
array('id' => 'wake-on-lan',
'name' => lang('DevDetail_WOL_Title'))
);
// Return json
echo (json_encode ($tableData));
}
//------------------------------------------------------------------------------
function getDevices() {
global $db;
// Device Data
$sql = 'select devMac, devName from Devices';
$result = $db->query($sql);
// arrays of rows
$tableData = array();
while ($row = $result -> fetchArray (SQLITE3_ASSOC)) {
$name = handleNull($row['devName'], "(unknown)");
$mac = handleNull($row['devMac'], "(unknown)");
// Push row data
$tableData[] = array('id' => $mac,
'name' => $name );
}
// Control no rows
if (empty($tableData)) {
$tableData = [];
}
// Return json
echo (json_encode ($tableData));
}
// ----------------------------------------------------------------------------------------
function updateNetworkLeaf()
{

View File

@@ -8,6 +8,12 @@
# Puche 2021 / 2022+ jokob jokob@duck.com GNU GPLv3
//------------------------------------------------------------------------------
//------------------------------------------------------------------------------
// 🔺----- API ENDPOINTS SUPERSEDED -----🔺
// check server/api_server/api_server_start.py for equivalents
// equivalent: /sessions /events
// 🔺----- API ENDPOINTS SUPERSEDED -----🔺
// External files
require dirname(__FILE__).'/init.php';

View File

@@ -12,6 +12,12 @@
# cvc90 2023 https://github.com/cvc90 GNU GPLv3 #
###################################################################################
// 🔺----- API ENDPOINTS SUPERSEDED -----🔺
// check server/api_server/api_server_start.py for equivalents
// equivalent: /nettools
// 🔺----- API ENDPOINTS SUPERSEDED -----🔺
// Get init.php
require dirname(__FILE__).'/../server/init.php';

View File

@@ -1,5 +1,10 @@
<?php
// 🔺----- API ENDPOINTS SUPERSEDED -----🔺
// check server/api_server/api_server_start.py for equivalents
// equivalent: /nettools
// 🔺----- API ENDPOINTS SUPERSEDED -----🔺
require 'util.php';
//------------------------------------------------------------------------------

View File

@@ -12,6 +12,11 @@
# cvc90 2023 https://github.com/cvc90 GNU GPLv3 #
###################################################################################
// 🔺----- API ENDPOINTS SUPERSEDED -----🔺
// check server/api_server/api_server_start.py for equivalents
// equivalent: /nettools
// 🔺----- API ENDPOINTS SUPERSEDED -----🔺
// Get init.php
require dirname(__FILE__).'/../server/init.php';

View File

@@ -1,6 +1,11 @@
<?php
require dirname(__FILE__).'/../server/init.php';
// 🔺----- API ENDPOINTS SUPERSEDED -----🔺
// check server/api_server/api_server_start.py for equivalents
// equivalent: /nettools
// 🔺----- API ENDPOINTS SUPERSEDED -----🔺
//------------------------------------------------------------------------------
// check if authenticated
require_once $_SERVER['DOCUMENT_ROOT'] . '/php/templates/security.php';

View File

@@ -12,6 +12,11 @@
# cvc90 2023 https://github.com/cvc90 GNU GPLv3 #
###################################################################################
// 🔺----- API ENDPOINTS SUPERSEDED -----🔺
// check server/api_server/api_server_start.py for equivalents
// equivalent: /nettools
// 🔺----- API ENDPOINTS SUPERSEDED -----🔺
// Get init.php
require dirname(__FILE__).'/../server/init.php';
@@ -19,6 +24,8 @@ require dirname(__FILE__).'/../server/init.php';
// check if authenticated
require_once $_SERVER['DOCUMENT_ROOT'] . '/php/templates/security.php';
// NEW ENDPOINT EQUIVALENT: /nettools/traceroute
// Get IP
$ip = $_GET['ip'];

View File

@@ -40,7 +40,7 @@
"BackDevices_Backup_Failed": "La còpia de seguretat s'ha executat parcialment. L'arxiu no es pot crear o està buit.",
"BackDevices_Backup_okay": "La còpia de seguretat s'ha executat en un nou arxiu",
"BackDevices_DBTools_DelDevError_a": "Error esborrant el Dispositiu",
"BackDevices_DBTools_DelDevError_b": "Error esborrant els Dispositius",
"BackDevices_DBTools_DelDevError_b": "Error esborrant dispositius",
"BackDevices_DBTools_DelDev_a": "Dispositiu esborrat",
"BackDevices_DBTools_DelDev_b": "Dispositius esborrats",
"BackDevices_DBTools_DelEvents": "Esdeveniments esborrats",
@@ -66,7 +66,7 @@
"DAYS_TO_KEEP_EVENTS_name": "Esborrar esdeveniments més vells de",
"DISCOVER_PLUGINS_description": "Desactiva aquesta opció per accelerar la inicialització i l'estalvi de configuració. Quan està desactivat, els connectors no es descobreixen, i no podeu afegir nous connectors a la configuració <code>LOADED_PLUGINS</code>.",
"DISCOVER_PLUGINS_name": "Descobreix els plugins",
"DevDetail_Children_Title": "",
"DevDetail_Children_Title": "Relacions filles",
"DevDetail_Copy_Device_Title": "<i class=\"fa fa-copy\"></i> Copiar detalls des del dispositiu",
"DevDetail_Copy_Device_Tooltip": "Copiar detalls del dispositius des de la llista desplegable. Tot el d'aquesta pàgina es sobre-escriurà",
"DevDetail_CustomProperties_Title": "Propietats personalitzades",
@@ -75,7 +75,7 @@
"DevDetail_EveandAl_AlertAllEvents": "Alertes",
"DevDetail_EveandAl_AlertDown": "Cancel·lar alerta",
"DevDetail_EveandAl_Archived": "Arxivat",
"DevDetail_EveandAl_NewDevice": "Nou Dispositiu",
"DevDetail_EveandAl_NewDevice": "Nou dispositiu",
"DevDetail_EveandAl_NewDevice_Tooltip": "Es mostrarà el nou estat del dispositiu i s'inclourà a les llistes quan el filtre New Devices estigui actiu. No afecta les notificacions.",
"DevDetail_EveandAl_RandomMAC": "MAC aleatori",
"DevDetail_EveandAl_ScanCycle": "Dispositiu d'escaneig",
@@ -87,7 +87,7 @@
"DevDetail_GoToNetworkNode": "Navegació a la pàgina de la Xarxa del node donat.",
"DevDetail_Icon": "Icona",
"DevDetail_Icon_Descr": "Si us plau, introdueix dins de la caixa de text els caràcters que veu a la imatge de sota. Això és requerit per evitar enviaments automàtics.",
"DevDetail_Loading": "Carregant ...",
"DevDetail_Loading": "Carregant ",
"DevDetail_MainInfo_Comments": "Comentaris",
"DevDetail_MainInfo_Favorite": "Favorit",
"DevDetail_MainInfo_Group": "Grup",
@@ -103,11 +103,11 @@
"DevDetail_MainInfo_Type": "Tipus",
"DevDetail_MainInfo_Vendor": "Venedor",
"DevDetail_MainInfo_mac": "MAC",
"DevDetail_NavToChildNode": "",
"DevDetail_NavToChildNode": "Obrir un node fill",
"DevDetail_Network_Node_hover": "Seleccioneu el dispositiu de xarxa al qual aquest dispositiu està connectat, per poder omplir l'arbre de xarxa.",
"DevDetail_Network_Port_hover": "El port on el dispositiu està connectat al dispositiu de xarxa del pare. Si es deixa buit, sortirà una icona wifi a la representació de la Xarxa.",
"DevDetail_Nmap_Scans": "Escaneig manual Nmap",
"DevDetail_Nmap_Scans_desc": "Aquí podeu executar les exploracions NMAP manuals. També podeu programar les exploracions NMAP automàtiques a través del connector Serveis i Ports (NMAP). Ves a <a href='/settings.php' target='_blank'>Configuració</a> per saber-ne més",
"DevDetail_Nmap_Scans_desc": "Aquí podeu executar les exploracions NMAP manuals. També podeu programar les exploracions NMAP automàtiques a través del connector Serveis i Ports (NMAP). Ves a <a href=\"https://github.com/jokob-sk/NetAlertX/tree/main/front/plugins/nmap_scan\" target='_blank'>Docs</a> per saber-ne més",
"DevDetail_Nmap_buttonDefault": "Escaneig predeterminat",
"DevDetail_Nmap_buttonDefault_text": "Escaneig predeterminat: Nmap escaneja els 1000 ports superiors per a cada protocol d'exploració sol·licitat. El 93% dels ports TCP i el 49% dels ports UDP. (uns 5 segons)",
"DevDetail_Nmap_buttonDetail": "Escaneig Detallat",
@@ -196,13 +196,13 @@
"DevDetail_button_Save": "Guardar",
"DeviceEdit_ValidMacIp": "Entra una adreça <b>IP</b> i <b>Mac</b> vàlides.",
"Device_MultiEdit": "Multi-edició",
"Device_MultiEdit_Backup": "Atenció, entrar valors incorrectes a continuació trencarà la configuració. Si us plau, abans feu còpia de seguretat la vostra base de dades o configuració de Dispositius (<a href=\"php/server/devices.php?action=ExportCSV\">clic per descarregar <i class=\"fa-solid fa-download fa-bounce\"></i></a>). Llegiu com per recuperar Dispositius des d'aquest fitxer al <a href=\"https://github.com/jokob-sk/NetAlertX/blob/main/docs/BACKUPS.md#scenario-2-corrupted-database\" target=\"_blank\">documentació de Còpies de seguretat</a>.",
"Device_MultiEdit_Backup": "Atenció, entrar valors incorrectes a continuació trencarà la configuració. Si us plau, abans feu còpia de seguretat la vostra base de dades o configuració de Dispositius (<a href=\"php/server/devices.php?action=ExportCSV\">clic per descarregar <i class=\"fa-solid fa-download fa-bounce\"></i></a>). Llegiu com per recuperar Dispositius des d'aquest fitxer al <a href=\"https://github.com/jokob-sk/NetAlertX/blob/main/docs/BACKUPS.md#scenario-2-corrupted-database\" target=\"_blank\">documentació de Còpies de seguretat</a>. Per aplicar els canvis, feu click a la <b>Save<i class=\"fa-solid fa-save\"></i></b> icona de cada camp que volgueu actualitzar.",
"Device_MultiEdit_Fields": "Editar camps:",
"Device_MultiEdit_MassActions": "Accions massives:",
"Device_MultiEdit_Tooltip": "Atenció. Si feu clic a això s'aplicarà el valor de l'esquerra a tots els dispositius seleccionats a dalt.",
"Device_Searchbox": "Cerca",
"Device_Shortcut_AllDevices": "Els meus dispositius",
"Device_Shortcut_AllNodes": "",
"Device_Shortcut_AllNodes": "Tots els nodes",
"Device_Shortcut_Archived": "Arxivat",
"Device_Shortcut_Connected": "Connectat",
"Device_Shortcut_Devices": "Dispositius",
@@ -229,11 +229,11 @@
"Device_TableHead_Name": "Nom",
"Device_TableHead_NetworkSite": "Network Site",
"Device_TableHead_Owner": "Propietari",
"Device_TableHead_ParentRelType": "",
"Device_TableHead_ParentRelType": "Tipus de relació",
"Device_TableHead_Parent_MAC": "Node pare de xarxa",
"Device_TableHead_Port": "Port",
"Device_TableHead_PresentLastScan": "Presència",
"Device_TableHead_ReqNicsOnline": "",
"Device_TableHead_ReqNicsOnline": "Requereix NICs En línia",
"Device_TableHead_RowID": "ID de fila",
"Device_TableHead_Rowid": "ID de fila",
"Device_TableHead_SSID": "SSID",
@@ -256,7 +256,7 @@
"ENCRYPTION_KEY_name": "Clau d'encriptació",
"Email_display_name": "Correu electrònic",
"Email_icon": "<i class=\"fa fa-at\"></i>",
"Events_Loading": "Carregant ...",
"Events_Loading": "Carregant ",
"Events_Periodselect_All": "Tota la informació",
"Events_Periodselect_LastMonth": "Darrer Mes",
"Events_Periodselect_LastWeek": "Darrera setmana",
@@ -301,7 +301,7 @@
"Gen_Cancel": "Cancel·lar",
"Gen_Change": "Canviar",
"Gen_Copy": "Executar",
"Gen_CopyToClipboard": "",
"Gen_CopyToClipboard": "Copia a portapapers",
"Gen_DataUpdatedUITakesTime": "D'acord - Pot passar una estona perquè la interfície d'usuari s'actualitzi si s'està executant una exploració.",
"Gen_Delete": "Esborrar",
"Gen_DeleteAll": "Esborrar tot",
@@ -309,9 +309,9 @@
"Gen_Error": "Error",
"Gen_Filter": "Filtrar",
"Gen_Generate": "Generar",
"Gen_InvalidMac": "",
"Gen_InvalidMac": "Mac address invàlida.",
"Gen_LockedDB": "ERROR - DB podria estar bloquejada - Fes servir F12 Eines desenvolupament -> Consola o provar-ho més tard.",
"Gen_NetworkMask": "",
"Gen_NetworkMask": "Màscara de xarxa",
"Gen_Offline": "Fora de línia",
"Gen_Okay": "Ok",
"Gen_Online": "En línia",
@@ -329,7 +329,7 @@
"Gen_SelectIcon": "<i class=\"fa-solid fa-chevron-down fa-fade\"></i>",
"Gen_SelectToPreview": "Seleccioneu la vista prèvia",
"Gen_Selected_Devices": "Dispositius seleccionats:",
"Gen_Subnet": "",
"Gen_Subnet": "Subxarxa",
"Gen_Switch": "Switch",
"Gen_Upd": "Actualitzat correctament",
"Gen_Upd_Fail": "Actualització fallida",
@@ -350,7 +350,7 @@
"LOADED_PLUGINS_name": "Connectors carregats",
"LOG_LEVEL_description": "Aquest paràmetre permetrà un registre més detallat. Útil per a la depuració d'esdeveniments d'escriptura a la base de dades.",
"LOG_LEVEL_name": "Imprimeix el registre addicional",
"Loading": "Carregant ...",
"Loading": "Carregant",
"Login_Box": "Introduïu la vostra contrasenya",
"Login_Default_PWD": "Contrasenya per defecte \"123456\" encara és activa.",
"Login_Info": "Les contrasenyes es canvien al connector(plugin) Configurar Contrasenya. Comprova el <a target=\"_blank\" href=\"https://github.com/jokob-sk/NetAlertX/tree/main/front/plugins/set_password\">SETPWD docs</a> si tens dubtes fent logging.",
@@ -419,7 +419,7 @@
"Maintenance_Tool_del_allevents": "Elimina deteccions (presència)",
"Maintenance_Tool_del_allevents30": "Suprimeix tots els esdeveniments anteriors a 30 dies",
"Maintenance_Tool_del_allevents30_noti": "Eliminar Esdeveniments",
"Maintenance_Tool_del_allevents30_noti_text": "T'és segur vols eliminar tot els successos més vells que 30 dies? Això elimina tots els dispositius presents.",
"Maintenance_Tool_del_allevents30_noti_text": "Estàs segur que vols eliminar tot els successos més vells que 30 dies? Això elimina tots els dispositius presents.",
"Maintenance_Tool_del_allevents30_text": "Abans d'utilitzar aquesta funció, feu una còpia de seguretat. La supressió no es pot desfer. S'eliminaran tots els esdeveniments mes vells de 30 dies a la base de dades. També es restablirà la detecció de presència de tots els dispositius. Això pot portar a sessions no vàlides. Això significa que els dispositius es mostren com a \"presents/detectats\" encara que estiguin fora de línia. Una anàlisi mentre el dispositiu en qüestió està en línia resol el problema.",
"Maintenance_Tool_del_allevents_noti": "Eliminar Esdeveniments",
"Maintenance_Tool_del_allevents_noti_text": "Estàs segur que vols eliminar tots els esdeveniments? Això reinicialitza la detecció de presència de tots els dispositius.",
@@ -455,7 +455,7 @@
"Maintenance_Tools_Tab_UISettings": "Configuració UI",
"Maintenance_arp_status": "Estat de Scan",
"Maintenance_arp_status_off": "actualment està desactivat",
"Maintenance_arp_status_on": "scan(s) actualment en execució",
"Maintenance_arp_status_on": "s'està fent un scan",
"Maintenance_built_on": "Construït",
"Maintenance_current_version": "Ets actual. Dona un cop d'ull al que <a href=\"https://github.com/jokob-sk/NetAlertX/issues/138\" target=\"_blank\">estic treballant</a>.",
"Maintenance_database_backup": "Còpies de seguretat de BBDD",
@@ -495,10 +495,10 @@
"Navigation_Workflows": "Workflows",
"Network_Assign": "Connecta el <i class=\"fa fa-server\"></i> node de Xarxa",
"Network_Cant_Assign": "No es pot assignar el node arrel d'Internet com a node fill.",
"Network_Cant_Assign_No_Node_Selected": "",
"Network_Cant_Assign_No_Node_Selected": "No es pot assignar, no s'ha seleccionat cap node pare.",
"Network_Configuration_Error": "Error de configuració",
"Network_Connected": "Dispositius connectats",
"Network_Devices": "",
"Network_Devices": "Dispositius de xarxa",
"Network_ManageAdd": "Afegir dispositiu",
"Network_ManageAdd_Name": "Nom del dispositiu",
"Network_ManageAdd_Name_text": "Nom sense caràcters especials",
@@ -533,8 +533,8 @@
"Network_Root": "Node arrel",
"Network_Root_Not_Configured": "Seleccioneu un tipus de dispositiu de xarxa, per exemple un tipus <b>Gateway</b>, al camp <b>Tipus</b>del <a href=\"deviceDetails.php?mac=Internet\">dispositiu arrel d'Internet</a> per començar a configurar aquesta pantalla. <br/><br/>. Podeu trobar més documentació a la <a href=\"https://github.com/jokob-sk/NetAlertX/blob/main/docs/NETWORK_TREE.md\" target=\"_blank\">Guia de com configurar la vostra pàgina de xarxa</a>",
"Network_Root_Unconfigurable": "Arrel no configurable",
"Network_ShowArchived": "",
"Network_ShowOffline": "",
"Network_ShowArchived": "Mostra els arxivats",
"Network_ShowOffline": "Mostra fora de línia",
"Network_Table_Hostname": "Hostname",
"Network_Table_IP": "IP",
"Network_Table_State": "Estat",
@@ -569,7 +569,7 @@
"Presence_Key_OnlinePastMiss": "Anteriorment en línia (miss-match)",
"Presence_Key_OnlinePastMiss_desc": "El dispositiu estava en línia anteriorment, i actualment està fora de línia. podria ser que a la sessió d'inici li faltin dades conflictives (o podria ser un bug) - si us plau, envií una petició de canvi dins de la caixa de text si sap com solucionar-ho (millor en anglès)",
"Presence_Key_OnlinePast_desc": "Dispositiu en línia en el passat, però actualment fora de línia.",
"Presence_Loading": "Carregant...",
"Presence_Loading": "Carregant",
"Presence_Shortcut_AllDevices": "Els meus Dispositius",
"Presence_Shortcut_Archived": "Arxivat",
"Presence_Shortcut_Connected": "Connectat",
@@ -577,7 +577,7 @@
"Presence_Shortcut_DownAlerts": "Aturar alertes",
"Presence_Shortcut_Favorites": "Favorits",
"Presence_Shortcut_NewDevices": "Nous dispositius",
"Presence_Title": "Detecció de dispositius",
"Presence_Title": "Presència per dispositiu",
"REFRESH_FQDN_description": "Re-escaneja tots dispositius i refresca el seu (FQDN). Si està desactivat, nomes s'escanegen els noms coneguts per fer-ho més ràpid. En aquest cas, FQDN s'actualitza només durant descoberta inicial de dispositius.",
"REFRESH_FQDN_name": "Refresc FQDN",
"REPORT_DASHBOARD_URL_description": "Aquesta URL s'utilitza com a base per generar enllaços en informes HTML (per exemple: correus electrònics). Introduïu la URL completa començant per <code>http://</code> incloent el número de port (sense barra inicial <code>/</code>).",
@@ -598,7 +598,7 @@
"Settings_device_Scanners_desync": "⚠ Els horaris d'escàner de dispositius no estan en sincronia.",
"Settings_device_Scanners_desync_popup": "Els horaris dels escàners de dispositius (<code>*_RUN_SCHD</code>) no són iguals. Això donarà lloc a notificacions inconsistents del dispositiu en línia / fora de línia. Si no és intencionat, utilitzeu el mateix horari per a tots els <b>🔍 escàners de dispositius</b>.",
"Speedtest_Results": "Speedtest Resultats",
"Systeminfo_AvailableIps": "",
"Systeminfo_AvailableIps": "IPs disponibles",
"Systeminfo_CPU": "CPU",
"Systeminfo_CPU_Cores": "Nuclis de CPU:",
"Systeminfo_CPU_Name": "Nom de CPU:",
@@ -683,7 +683,7 @@
"UI_LANG_description": "Seleccioneu l'idioma d'usuari preferit. Ajudeu a traduir o suggerir idiomes al portal en línia de <a href=\"https://hosted.weblate.org/projects/pialert/core/\" target=\"_blank\">Weblate</a>.",
"UI_LANG_name": "Llenguatge UI",
"UI_MY_DEVICES_description": "Els dispositius dels quals s'han de mostrar en la vista predeterminada <b> Els meus dispositius</b>.",
"UI_MY_DEVICES_name": "Veure a la vista els meus dispositius",
"UI_MY_DEVICES_name": "Veure a la vista Els Meus Dispositius",
"UI_NOT_RANDOM_MAC_description": "Prefixos MAC que no s'han de marcar com a dispositius aleatoris. Introduïu per exemple <code> 52</code> per excloure els dispositius començant per <code> 52:xx:xx:xx:xx:xx</code> de ser marcats com a dispositius amb una adreça MAC aleatòria.",
"UI_NOT_RANDOM_MAC_name": "No marqueu com a aleatori",
"UI_PRESENCE_description": "Seleccioneu quins estats s'han de mostrar al gràfic <b> Presència de dispositius</b> a la pàgina <a href=\"/devices.php\" target=\"_blank\">Dispositius</a>.",
@@ -721,10 +721,10 @@
"add_icon_event_tooltip": "Afegir nova icona",
"add_option_event_tooltip": "Afegir nou valor",
"copy_icons_event_tooltip": "Sobreescriure icones de tots els dispositius amb el mateix tipus de dispositiu",
"devices_old": "Refrescant...",
"devices_old": "Refrescant",
"general_event_description": "L'esdeveniment que has desencadenat pot trigar un temps fins que acabin els processos de fons. L'execució acabarà una cop buida la cua d'execució (Comprova el registre d'errors <a href='/maintenance.php#tab_Logging'></a> si hi ha problemes). <br/> <br/> Cua d'execució:",
"general_event_title": "Execució d'un esdeveniment ad-hoc",
"go_to_device_event_tooltip": "",
"go_to_device_event_tooltip": "Navegar al dispositiu",
"go_to_node_event_tooltip": "Navegació a la pàgina de la Xarxa del node donat",
"new_version_available": "Ja està disponible una nova versió.",
"report_guid": "Notificació guid:",
@@ -732,7 +732,7 @@
"report_select_format": "Seleccioneu Format:",
"report_time": "Data de recepció:",
"run_event_tooltip": "Habiliteu la configuració i deseu els canvis al principi abans d'executar-lo.",
"select_icon_event_tooltip": "",
"select_icon_event_tooltip": "Selecciona la icona",
"settings_core_icon": "fa-solid fa-gem",
"settings_core_label": "Nucli",
"settings_device_scanners": "Escàners de dispositius utilitzats per descobrir dispositius que escriuen a la taula de base de dades CurrentScan.",
@@ -746,7 +746,7 @@
"settings_imported_label": "Configuració importada",
"settings_missing": "No tots els paràmetres carregats! Alta càrrega en la seqüència d'inici de la base de dades o aplicació. Feu clic al botó de recarregar 🔄 a la part superior.",
"settings_missing_block": "Error: els paràmetres no carregats correctament. Fer clic el botó recarregar 🔄 dalt de tot, o també comprova el registre de navegador per a detalls (F12).",
"settings_old": "Importar la configuració i re-inicialitzar ...",
"settings_old": "Importar la configuració i re-iniciar…",
"settings_other_scanners": "Uns altres plugins no relacionats amb dispositius que estan actualment activats.",
"settings_other_scanners_icon": "fa-solid fa-recycle",
"settings_other_scanners_label": "Altres escàners",
@@ -760,4 +760,4 @@
"settings_system_label": "Sistema",
"settings_update_item_warning": "Actualitza el valor sota. Sigues curós de seguir el format anterior. <b>No hi ha validació.</b>",
"test_event_tooltip": "Deseu els canvis primer abans de comprovar la configuració."
}
}

View File

@@ -211,7 +211,7 @@
"Device_Shortcut_Connected": "Verbunden",
"Device_Shortcut_Devices": "Geräte",
"Device_Shortcut_DownAlerts": "Nicht erreichbar & offline",
"Device_Shortcut_DownOnly": "Offline",
"Device_Shortcut_DownOnly": "Nicht erreichbar",
"Device_Shortcut_Favorites": "Favoriten",
"Device_Shortcut_NewDevices": "Neue Geräte",
"Device_Shortcut_OnlineChart": "Gerätepräsenz im Laufe der Zeit",

View File

@@ -760,4 +760,4 @@
"settings_system_label": "Système",
"settings_update_item_warning": "Mettre à jour la valeur ci-dessous. Veillez à bien suivre le même format qu'auparavant. <b>Il n'y a pas de pas de contrôle.</b>",
"test_event_tooltip": "Enregistrer d'abord vos modifications avant de tester vôtre paramétrage."
}
}

View File

@@ -760,4 +760,4 @@
"settings_system_label": "Sistema",
"settings_update_item_warning": "Aggiorna il valore qui sotto. Fai attenzione a seguire il formato precedente. <b>La convalida non viene eseguita.</b>",
"test_event_tooltip": "Salva le modifiche prima di provare le nuove impostazioni."
}
}

View File

@@ -5,7 +5,7 @@
// ###################################
$defaultLang = "en_us";
$allLanguages = ["en_us", "es_es", "de_de", "fr_fr", "it_it", "ru_ru", "nb_no", "pl_pl", "pt_br", "tr_tr", "zh_cn", "cs_cz", "ar_ar", "ca_ca", "uk_ua"];
$allLanguages = ["en_us", "es_es", "de_de", "fr_fr", "it_it", "ru_ru", "nb_no", "pl_pl", "pt_br", "pt_pt", "tr_tr", "zh_cn", "cs_cz", "ar_ar", "ca_ca", "uk_ua"];
global $db;
@@ -19,6 +19,7 @@ switch($result){
case 'Norwegian': $pia_lang_selected = 'nb_no'; break;
case 'Polish (pl_pl)': $pia_lang_selected = 'pl_pl'; break;
case 'Portuguese (pt_br)': $pia_lang_selected = 'pt_br'; break;
case 'Portuguese (pt_pt)': $pia_lang_selected = 'pt_pt'; break;
case 'Italian (it_it)': $pia_lang_selected = 'it_it'; break;
case 'Russian': $pia_lang_selected = 'ru_ru'; break;
case 'Turkish (tr_tr)': $pia_lang_selected = 'tr_tr'; break;

View File

@@ -33,6 +33,6 @@ def merge_translations(main_file, other_files):
if __name__ == "__main__":
current_path = os.path.dirname(os.path.abspath(__file__))
# language codes can be found here: http://www.lingoes.net/en/translator/langcode.htm
json_files = ["en_us.json", "de_de.json", "es_es.json", "fr_fr.json", "nb_no.json", "ru_ru.json", "it_it.json", "pt_br.json", "pl_pl.json", "zh_cn.json", "tr_tr.json", "cs_cz.json", "ar_ar.json", "ca_ca.json", "uk_ua.json"]
json_files = ["en_us.json", "de_de.json", "es_es.json", "fr_fr.json", "nb_no.json", "ru_ru.json", "it_it.json", "pt_br.json", "pt_pt.json", "pl_pl.json", "zh_cn.json", "tr_tr.json", "cs_cz.json", "ar_ar.json", "ca_ca.json", "uk_ua.json"]
file_paths = [os.path.join(current_path, file) for file in json_files]
merge_translations(file_paths[0], file_paths[1:])

View File

@@ -0,0 +1,763 @@
{
"API_CUSTOM_SQL_description": "Pode especificar uma consulta SQL personalizada que irá gerar um ficheiro JSON e, em seguida, expô-lo por meio do <a href=\"/php/server/query_json.php?file=table_custom_endpoint.json\" target=\"_blank\"><code>table_custom_endpoint.json</code> endpoint do ficheiro</a>.",
"API_CUSTOM_SQL_name": "Endpoint customizado",
"API_TOKEN_description": "Token de API para comunicação segura. Gere um ou insira qualquer valor. Ele é enviado no cabeçalho da solicitação e usado no plugin <code>SYNC</code>, no servidor GraphQL e em outros endpoints de API. Pode usar os endpoints de API para criar integrações personalizadas, conforme descrito na <a href=\"https://github.com/jokob-sk/NetAlertX/blob/main/docs/API.md\" target=\"_blank\">documentação da API</a>.",
"API_TOKEN_name": "API token",
"API_display_name": "API",
"API_icon": "<i class=\"fa fa-arrow-down-up-across-line\"></i>",
"About_Design": "Desenvolvido por:",
"About_Exit": "Sair",
"About_Title": "Analisador de segurança de rede & framework de notificação",
"AppEvents_AppEventProcessed": "Processado",
"AppEvents_DateTimeCreated": "Registado em",
"AppEvents_Extra": "Adicional",
"AppEvents_GUID": "Evento de aplicação GUID",
"AppEvents_Helper1": "Auxiliar 1",
"AppEvents_Helper2": "Auxiliar 2",
"AppEvents_Helper3": "Auxiliar 3",
"AppEvents_ObjectForeignKey": "Chave Estrangeira",
"AppEvents_ObjectIndex": "Índice",
"AppEvents_ObjectIsArchived": "Foi arquivado (no horário do log)",
"AppEvents_ObjectIsNew": "É novo (no horário do log)",
"AppEvents_ObjectPlugin": "Plugin Associado",
"AppEvents_ObjectPrimaryID": "ID Primário",
"AppEvents_ObjectSecondaryID": "ID Secundário",
"AppEvents_ObjectStatus": "Estado registado",
"AppEvents_ObjectStatusColumn": "Coluna de Estado",
"AppEvents_ObjectType": "Tipo de Objeto",
"AppEvents_Plugin": "Plugin",
"AppEvents_Type": "Tipo",
"BackDevDetail_Actions_Ask_Run": "Deseja executar esta ação?",
"BackDevDetail_Actions_Not_Registered": "Ação não registada: ",
"BackDevDetail_Actions_Title_Run": "Executar ação",
"BackDevDetail_Copy_Ask": "Copiar pormenores de dispositivos da lista suspenda (Tudo nesta página será substituído)?",
"BackDevDetail_Copy_Title": "Copiar pormenores",
"BackDevDetail_Tools_WOL_error": "O comando NÃO foi executado.",
"BackDevDetail_Tools_WOL_okay": "O comando foi executado.",
"BackDevices_Arpscan_disabled": "Análise Arp Desativada",
"BackDevices_Arpscan_enabled": "Análise ARP Ativada",
"BackDevices_Backup_CopError": "A base da dados original não pode ser gravada.",
"BackDevices_Backup_Failed": "A copia de segurança foi parcialmente executada. O arquivo não pode ser criado ou está vazio.",
"BackDevices_Backup_okay": "A copia de segurança foi feita executado corretamente com o novo arquivo",
"BackDevices_DBTools_DelDevError_a": "Erro ao apagar o dispositivo",
"BackDevices_DBTools_DelDevError_b": "Erro ao apagar dispositivos",
"BackDevices_DBTools_DelDev_a": "Dispositivo apagado",
"BackDevices_DBTools_DelDev_b": "Dispositivos apagados",
"BackDevices_DBTools_DelEvents": "Eventos apagados",
"BackDevices_DBTools_DelEventsError": "Erro ao apagar os eventos",
"BackDevices_DBTools_ImportCSV": "Os dispositivos do ficheiro CSV foram importados com sucesso.",
"BackDevices_DBTools_ImportCSVError": "O ficheiro CSV não pode ser importado. Assegure que o formato está correto.",
"BackDevices_DBTools_ImportCSVMissing": "O ficheiro CSV não foi localizado em <b>/config/devices.csv.</b>",
"BackDevices_DBTools_Purge": "As copias de segurança antigas foram apagadas",
"BackDevices_DBTools_UpdDev": "Dispositivo atualizado com sucesso. A lista de dispositivos principais pode levar algum tempo para recarregar se uma varredura estiver em andamento.",
"BackDevices_DBTools_UpdDevError": "Erro atualizando o dispositivo",
"BackDevices_DBTools_Upgrade": "Base de dados atualizada com sucesso",
"BackDevices_DBTools_UpgradeError": "A atualização da base de dados falhou",
"BackDevices_Device_UpdDevError": "Erro atualizando os dispositivos, tente novamente mais tarde. A base de dados provavelmente está travado com uma tarefa em andamento.",
"BackDevices_Restore_CopError": "A base de dados original não pode ser gradava.",
"BackDevices_Restore_Failed": "A restauração falhou. Por favor restaure a copia de segurança manualmente.",
"BackDevices_Restore_okay": "Restauração executada com sucesso.",
"BackDevices_darkmode_disabled": "Modo Noturno Desativado",
"BackDevices_darkmode_enabled": "Modo Noturno Ativado",
"CLEAR_NEW_FLAG_description": "Se ativado (<code>0</code> está desativado), dispositivos marcados como<b>Novo Dispositivo</b> serão desmarcados se o limite (especificado em horas) exceder o tempo da <b>Primeira Sessão </b>.",
"CLEAR_NEW_FLAG_name": "",
"CustProps_cant_remove": "Não é possível remover, é necessária pelo menos uma propriedade.",
"DAYS_TO_KEEP_EVENTS_description": "Esta é uma definição de manutenção. Especifica o número de dias de entradas de eventos que serão mantidas. Todos os eventos mais antigos serão apagados periodicamente. Também se aplica ao Histórico de eventos do plug-in.",
"DAYS_TO_KEEP_EVENTS_name": "Apagar eventos mais antigos que",
"DISCOVER_PLUGINS_description": "Desative esta opção para acelerar a inicialização e a gravação de definições. Quando desativada, os plug-ins não são descobertos e não é possível adicionar novos plug-ins à definição<code>LOADED_PLUGINS</code>.",
"DISCOVER_PLUGINS_name": "Descobrir plugins",
"DevDetail_Children_Title": "Relacionamentos de crianças",
"DevDetail_Copy_Device_Title": "Copiar pormenores do dispositivo",
"DevDetail_Copy_Device_Tooltip": "Copiar pormenores do dispositivo a partir da lista pendente. Tudo o que se encontra nesta página será substituído",
"DevDetail_CustomProperties_Title": "Propriedades personalizadas",
"DevDetail_CustomProps_reset_info": "Isto irá remover as suas propriedades personalizadas neste dispositivo e repô-las para o valor predefinido.",
"DevDetail_DisplayFields_Title": "Visualização",
"DevDetail_EveandAl_AlertAllEvents": "Eventos de alerta",
"DevDetail_EveandAl_AlertDown": "",
"DevDetail_EveandAl_Archived": "Arquivado",
"DevDetail_EveandAl_NewDevice": "Novo dispositivo",
"DevDetail_EveandAl_NewDevice_Tooltip": "",
"DevDetail_EveandAl_RandomMAC": "MAC Aleatório",
"DevDetail_EveandAl_ScanCycle": "Rastrear dispositivo",
"DevDetail_EveandAl_ScanCycle_a": "Rastear dispositivo",
"DevDetail_EveandAl_ScanCycle_z": "Não rastear dispositivo",
"DevDetail_EveandAl_Skip": "Pular notificações repetidas para",
"DevDetail_EveandAl_Title": "Configuração de Eventos & Alertas",
"DevDetail_Events_CheckBox": "Ocultar eventos de conexão",
"DevDetail_GoToNetworkNode": "Navega para a página Rede do nó indicado.",
"DevDetail_Icon": "Icone",
"DevDetail_Icon_Descr": "Introduza o nome de um ícone fantástico do tipo de letra sem o prefixo fa- ou com a classe completa, por exemplo: fa fa-brands fa-apple.",
"DevDetail_Loading": "A carregar…",
"DevDetail_MainInfo_Comments": "Comentários",
"DevDetail_MainInfo_Favorite": "Favorito",
"DevDetail_MainInfo_Group": "Grupo",
"DevDetail_MainInfo_Location": "Localização",
"DevDetail_MainInfo_Name": "Nome",
"DevDetail_MainInfo_Network": "<i class=\"fa fa-server\"> </i> Node (MAC)",
"DevDetail_MainInfo_Network_Port": "<i class=\"fa fa-ethernet\"></i>Porta",
"DevDetail_MainInfo_Network_Site": "Site",
"DevDetail_MainInfo_Network_Title": "Rede",
"DevDetail_MainInfo_Owner": "Proprietário",
"DevDetail_MainInfo_SSID": "SSID",
"DevDetail_MainInfo_Title": "Informações principais",
"DevDetail_MainInfo_Type": "Tipo",
"DevDetail_MainInfo_Vendor": "Fornecedor",
"DevDetail_MainInfo_mac": "MAC",
"DevDetail_NavToChildNode": "",
"DevDetail_Network_Node_hover": "Selecione o dispositivo de rede principal ao qual o dispositivo atual está conectado, para preencher a árvore Rede.",
"DevDetail_Network_Port_hover": "A porta a que este dispositivo está ligado no dispositivo de rede principal. Se for deixado vazio, é apresentado um ícone wifi na árvore Rede.",
"DevDetail_Nmap_Scans": "Varreduras manuais do Nmap",
"DevDetail_Nmap_Scans_desc": "",
"DevDetail_Nmap_buttonDefault": "Verificação predefinida",
"DevDetail_Nmap_buttonDefault_text": "Scan padrão: Nmap verifica as 1.000 portas superiores para cada protocolo de digitalização solicitado. Isto atinge cerca de 93% das portas TCP e 49% das portas UDP. (cerca de 5 segundos)",
"DevDetail_Nmap_buttonDetail": "Verificação Detalhada",
"DevDetail_Nmap_buttonDetail_text": "Verificação detalhada: Verificação predefinida com deteção de SO ativada, deteção de versão, verificação de scripts e traceroute (até 30 segundos ou mais)",
"DevDetail_Nmap_buttonFast": "Verificação rápida",
"DevDetail_Nmap_buttonFast_text": "Verificação rápida: Verifica menos portas (100) do que a verificação predefinida (alguns segundos)",
"DevDetail_Nmap_buttonSkipDiscovery": "Saltar descoberta do host",
"DevDetail_Nmap_buttonSkipDiscovery_text": "Ignorar a descoberta do host (-Pn opção): Verificação padrão sem descoberta do host",
"DevDetail_Nmap_resultsLink": "Pode deixar esta página depois de iniciar uma varredura. Os resultados também estarão disponíveis no ficheiro <code>app_front.log</code>.",
"DevDetail_Owner_hover": "Quem é o dono deste dispositivo. Campo de texto gratuito.",
"DevDetail_Periodselect_All": "Todas as informações",
"DevDetail_Periodselect_LastMonth": "Último mês",
"DevDetail_Periodselect_LastWeek": "Semana passada",
"DevDetail_Periodselect_LastYear": "Ano passado",
"DevDetail_Periodselect_today": "Hoje",
"DevDetail_Run_Actions_Title": "<i class=\"fa fa-play\"></i> Executar ação no dispositivo",
"DevDetail_Run_Actions_Tooltip": "Execute uma ação no dispositivo atual da lista suspensa.",
"DevDetail_SessionInfo_FirstSession": "Primeira sessão",
"DevDetail_SessionInfo_LastIP": "Último IP",
"DevDetail_SessionInfo_LastSession": "Último offline",
"DevDetail_SessionInfo_StaticIP": "IP estático",
"DevDetail_SessionInfo_Status": "Estado",
"DevDetail_SessionInfo_Title": "Informações de Sessão",
"DevDetail_SessionTable_Additionalinfo": "Informações adicionais",
"DevDetail_SessionTable_Connection": "Conexão",
"DevDetail_SessionTable_Disconnection": "Desconexão",
"DevDetail_SessionTable_Duration": "Duração",
"DevDetail_SessionTable_IP": "IP",
"DevDetail_SessionTable_Order": "Ordem",
"DevDetail_Shortcut_CurrentStatus": "Estado atual",
"DevDetail_Shortcut_DownAlerts": "Alertas para baixo",
"DevDetail_Shortcut_Presence": "Presença",
"DevDetail_Shortcut_Sessions": "Sessões",
"DevDetail_Tab_Details": "Pormenores",
"DevDetail_Tab_Events": "Eventos",
"DevDetail_Tab_EventsTableDate": "Data",
"DevDetail_Tab_EventsTableEvent": "Tipo de evento",
"DevDetail_Tab_EventsTableIP": "IP",
"DevDetail_Tab_EventsTableInfo": "Informações adicionais",
"DevDetail_Tab_Nmap": "<i class=\"fa fa-ethernet\"> </i> Nmap",
"DevDetail_Tab_NmapEmpty": "Nenhuma porta detetada com Nmap neste dispositivo.",
"DevDetail_Tab_NmapTableExtra": "Adicional",
"DevDetail_Tab_NmapTableHeader": "Resultados da verificação programada",
"DevDetail_Tab_NmapTableIndex": "Índice",
"DevDetail_Tab_NmapTablePort": "Porta",
"DevDetail_Tab_NmapTableService": "Serviço",
"DevDetail_Tab_NmapTableState": "Estado",
"DevDetail_Tab_NmapTableText": "",
"DevDetail_Tab_NmapTableTime": "Tempo",
"DevDetail_Tab_Plugins": "Plugins",
"DevDetail_Tab_Presence": "Presença",
"DevDetail_Tab_Sessions": "Sessões",
"DevDetail_Tab_Tools": "Ferramentas",
"DevDetail_Tab_Tools_Internet_Info_Description": "",
"DevDetail_Tab_Tools_Internet_Info_Error": "Ocorreu um erro",
"DevDetail_Tab_Tools_Internet_Info_Start": "",
"DevDetail_Tab_Tools_Internet_Info_Title": "",
"DevDetail_Tab_Tools_Nslookup_Description": "",
"DevDetail_Tab_Tools_Nslookup_Error": "",
"DevDetail_Tab_Tools_Nslookup_Start": "",
"DevDetail_Tab_Tools_Nslookup_Title": "",
"DevDetail_Tab_Tools_Speedtest_Description": "",
"DevDetail_Tab_Tools_Speedtest_Start": "",
"DevDetail_Tab_Tools_Speedtest_Title": "",
"DevDetail_Tab_Tools_Traceroute_Description": "",
"DevDetail_Tab_Tools_Traceroute_Error": "",
"DevDetail_Tab_Tools_Traceroute_Start": "",
"DevDetail_Tab_Tools_Traceroute_Title": "",
"DevDetail_Tools_WOL": "",
"DevDetail_Tools_WOL_noti": "",
"DevDetail_Tools_WOL_noti_text": "",
"DevDetail_Type_hover": "",
"DevDetail_Vendor_hover": "",
"DevDetail_WOL_Title": "",
"DevDetail_button_AddIcon": "",
"DevDetail_button_AddIcon_Help": "Cole uma tag HTML SVG ou um ícone de tag HTML Font Awesome. Leia a <a href=\"https://github.com/jokob-sk/NetAlertX/blob/main/docs/ICONS.md\" target=\"_blank\">documentação sobre ícones</a> para obter pormenores.",
"DevDetail_button_AddIcon_Tooltip": "Adicione um novo ícone a este dispositivo que ainda não esteja disponível no menu suspenso.",
"DevDetail_button_Delete": "Apagar dispositivo",
"DevDetail_button_DeleteEvents": "Apagar eventos",
"DevDetail_button_DeleteEvents_Warning": "Tem certeza de que deseja apagar todos os eventos deste dispositivo?<br><br>(isto limpará o <b>Histórico de eventos</b> e as <b>sessões</b> e poderá ajudar com constantes (persistentes) notificações)",
"DevDetail_button_Delete_ask": "Tem a certeza de que pretende apagar este dispositivo? Em vez disso, também o pode arquivar.",
"DevDetail_button_OverwriteIcons": "Substituir ícones",
"DevDetail_button_OverwriteIcons_Tooltip": "Substituir ícones de todos os dispositivos pelo mesmo tipo de dispositivo",
"DevDetail_button_OverwriteIcons_Warning": "Tem certeza de que deseja substituir todos os ícones de todos os dispositivos pelo mesmo tipo de dispositivo do tipo de dispositivo atual?",
"DevDetail_button_Reset": "Redefinir alterações",
"DevDetail_button_Save": "Gravar",
"DeviceEdit_ValidMacIp": "Insira um endereço <b>Mac</b> e <b>IP</b> válidos.",
"Device_MultiEdit": "Edição múltipla",
"Device_MultiEdit_Backup": "",
"Device_MultiEdit_Fields": "Editar campos:",
"Device_MultiEdit_MassActions": "Ações em massa:",
"Device_MultiEdit_Tooltip": "Cuidadoso. Clicar aqui aplicará o valor à esquerda a todos os dispositivos selecionados acima.",
"Device_Searchbox": "Procurar",
"Device_Shortcut_AllDevices": "",
"Device_Shortcut_AllNodes": "",
"Device_Shortcut_Archived": "Arquivado",
"Device_Shortcut_Connected": "Conectado",
"Device_Shortcut_Devices": "Dispositivos",
"Device_Shortcut_DownAlerts": "Inativo e off-line",
"Device_Shortcut_DownOnly": "Inativo",
"Device_Shortcut_Favorites": "Favoritos",
"Device_Shortcut_NewDevices": "",
"Device_Shortcut_OnlineChart": "Presença do dispositivo",
"Device_TableHead_AlertDown": "Alerta em baixo",
"Device_TableHead_Connected_Devices": "Conexões",
"Device_TableHead_CustomProps": "",
"Device_TableHead_FQDN": "",
"Device_TableHead_Favorite": "Favorito",
"Device_TableHead_FirstSession": "Primeira sessão",
"Device_TableHead_GUID": "GUID",
"Device_TableHead_Group": "Grupo",
"Device_TableHead_Icon": "Ícone",
"Device_TableHead_LastIP": "Último IP",
"Device_TableHead_LastIPOrder": "Último pedido de IP",
"Device_TableHead_LastSession": "Último off-line",
"Device_TableHead_Location": "Localização",
"Device_TableHead_MAC": "MAC aleatório",
"Device_TableHead_MAC_full": "MAC completo",
"Device_TableHead_Name": "Nome",
"Device_TableHead_NetworkSite": "Site da rede",
"Device_TableHead_Owner": "Proprietário",
"Device_TableHead_ParentRelType": "",
"Device_TableHead_Parent_MAC": "",
"Device_TableHead_Port": "Porta",
"Device_TableHead_PresentLastScan": "Presença",
"Device_TableHead_ReqNicsOnline": "",
"Device_TableHead_RowID": "ID da linha",
"Device_TableHead_Rowid": "ID da linha",
"Device_TableHead_SSID": "SSID",
"Device_TableHead_SourcePlugin": "Plugin de fonte",
"Device_TableHead_Status": "Estado",
"Device_TableHead_SyncHubNodeName": "Nó de sincronização",
"Device_TableHead_Type": "Tipo",
"Device_TableHead_Vendor": "Fornecedor",
"Device_Table_Not_Network_Device": "Não configurado como um dispositivo de rede",
"Device_Table_info": "A mostrar _START_ to _END_ of _TOTAL_ entradas",
"Device_Table_nav_next": "Próximo",
"Device_Table_nav_prev": "Anterior",
"Device_Tablelenght": "Mostrar entradas do _MENU_",
"Device_Tablelenght_all": "Todos",
"Device_Title": "Dispositivos",
"Devices_Filters": "Filtros",
"ENABLE_PLUGINS_description": "Ativa a funcionalidade de <a target=\"_blank\" href=\"https://github.com/jokob-sk/NetAlertX/tree/main/docs/PLUGINS.md\">plugins</a>. Carregar plug-ins requer mais recursos de hardware, então podedesativá-los em sistemas de baixa potência.",
"ENABLE_PLUGINS_name": "Ativar plug-ins",
"ENCRYPTION_KEY_description": "Chave de encriptação de dados.",
"ENCRYPTION_KEY_name": "Chave de encriptação",
"Email_display_name": "Email",
"Email_icon": "<i class=\"fa fa-at\"></i>",
"Events_Loading": "",
"Events_Periodselect_All": "Todas as informações",
"Events_Periodselect_LastMonth": "Mês passado",
"Events_Periodselect_LastWeek": "Semana passada",
"Events_Periodselect_LastYear": "Ano passado",
"Events_Periodselect_today": "Hoje",
"Events_Searchbox": "Procurar",
"Events_Shortcut_AllEvents": "Todos os eventos",
"Events_Shortcut_DownAlerts": "Alertas de queda",
"Events_Shortcut_Events": "Eventos",
"Events_Shortcut_MissSessions": "Sessões ausentes",
"Events_Shortcut_NewDevices": "",
"Events_Shortcut_Sessions": "Sessões",
"Events_Shortcut_VoidSessions": "Sessões anuladas",
"Events_TableHead_AdditionalInfo": "Informação adicional",
"Events_TableHead_Connection": "Conexão",
"Events_TableHead_Date": "Data",
"Events_TableHead_Device": "Dispositivo",
"Events_TableHead_Disconnection": "Desconexão",
"Events_TableHead_Duration": "Duração",
"Events_TableHead_DurationOrder": "Duração do pedido",
"Events_TableHead_EventType": "",
"Events_TableHead_IP": "IP",
"Events_TableHead_IPOrder": "Pedido de IP",
"Events_TableHead_Order": "Ordem",
"Events_TableHead_Owner": "Proprietário",
"Events_TableHead_PendingAlert": "Alerta Pendente",
"Events_Table_info": "A mostrar_START_ to _END_ of _TOTAL_ entradas",
"Events_Table_nav_next": "Próxima",
"Events_Table_nav_prev": "Anterior",
"Events_Tablelenght": "Mostrar entradas do _MENU_",
"Events_Tablelenght_all": "Todos",
"Events_Title": "Eventos",
"GRAPHQL_PORT_description": "O número da porta do servidor GraphQL. Certifique-se de que a porta seja exclusiva em todas as suas aplicações neste host e nas instâncias do NetAlertX.",
"GRAPHQL_PORT_name": "Porta GraphQL",
"Gen_Action": "Ação",
"Gen_Add": "Adicionar",
"Gen_AddDevice": "",
"Gen_Add_All": "Adicionar todos",
"Gen_All_Devices": "",
"Gen_AreYouSure": "Tem certeza?",
"Gen_Backup": "Executar backup",
"Gen_Cancel": "Cancelar",
"Gen_Change": "Alterar",
"Gen_Copy": "Executar",
"Gen_CopyToClipboard": "",
"Gen_DataUpdatedUITakesTime": "OK - Pode levar um tempo para a interface do utilizador ser atualizada se uma verificação estiver em execução.",
"Gen_Delete": "Apagar",
"Gen_DeleteAll": "Apagar todos",
"Gen_Description": "Descrição",
"Gen_Error": "Erro",
"Gen_Filter": "Filtro",
"Gen_Generate": "Gerar",
"Gen_InvalidMac": "",
"Gen_LockedDB": "ERRO - A base de dados pode estar bloqueada - Verifique F12 Ferramentas de desenvolvimento -> Console ou tente mais tarde.",
"Gen_NetworkMask": "",
"Gen_Offline": "Offline",
"Gen_Okay": "Ok",
"Gen_Online": "Online",
"Gen_Purge": "Purge",
"Gen_ReadDocs": "Leia mais em documentos.",
"Gen_Remove_All": "Remover tudo",
"Gen_Remove_Last": "Remover o último",
"Gen_Reset": "Repor",
"Gen_Restore": "Executar restauração",
"Gen_Run": "Executar",
"Gen_Save": "Gravar",
"Gen_Saved": "Gravado",
"Gen_Search": "Procurar",
"Gen_Select": "Selecionar",
"Gen_SelectIcon": "<i class=\"fa-solid fa-chevron-down fa-fade\"></i>",
"Gen_SelectToPreview": "Selecionar para pré-visualizar",
"Gen_Selected_Devices": "",
"Gen_Subnet": "",
"Gen_Switch": "Trocar",
"Gen_Upd": "Atualizado com sucesso",
"Gen_Upd_Fail": "A atualização falhou",
"Gen_Update": "Atualizar",
"Gen_Update_Value": "Atualizar valor",
"Gen_ValidIcon": "<i class=\"fa-solid fa-chevron-right \"></i>",
"Gen_Warning": "Aviso",
"Gen_Work_In_Progress": "Trabalho em andamento, um bom momento para enviar feedback em https://github.com/jokob-sk/NetAlertX/issues",
"Gen_create_new_device": "Novo dispositivo",
"Gen_create_new_device_info": "Os dispositivos são normalmente descobertos usando <a target=\"_blank\" href=\"https://github.com/jokob-sk/NetAlertX/tree/main/docs/PLUGINS.md\">plugins</a>. No entanto, em certos casos, pode ser necessário adicionar dispositivos manualmente. Para explorar cenários específicos, verifique a <a target=\"_blank\" href=\"https://github.com/jokob-sk/NetAlertX/blob/main/docs/REMOTE_NETWORKS.md\">documentação de Redes Remotas</a>.",
"General_display_name": "Geral",
"General_icon": "<i class=\"fa fa-gears\"></i>",
"HRS_TO_KEEP_NEWDEV_description": "",
"HRS_TO_KEEP_NEWDEV_name": "",
"HRS_TO_KEEP_OFFDEV_description": "",
"HRS_TO_KEEP_OFFDEV_name": "Apagar dispositivos offline após",
"LOADED_PLUGINS_description": "Quais plugins carregar. Adicionar plugins pode deixar a aplicação lenta. Leia mais sobre quais plugins precisam ser ativados, tipos ou opções de escaneamento na <a target=\"_blank\" href=\"https://github.com/jokob-sk/NetAlertX/tree/main/docs/PLUGINS.md\">documentação de plugins</a>. Plugins descarregados perderão as suas configurações. Somente plugins <code>desativados</code> podem ser descarregados.",
"LOADED_PLUGINS_name": "Plugins carregados",
"LOG_LEVEL_description": "Esta definição permite um registo mais detalhado. Útil para depurar eventos gravados na base de dados.",
"LOG_LEVEL_name": "Imprimir registo adicional",
"Loading": "",
"Login_Box": "Introduza a sua palavra-passe",
"Login_Default_PWD": "A palavra-passe predefinida “123456” ainda está ativa.",
"Login_Info": "As palavra-passes são definidas por meio do plugin Definir palavra-passe. Verifique a <a target=\"_blank\" href=\"https://github.com/jokob-sk/NetAlertX/tree/main/front/plugins/set_password\">documentação do SETPWD</a> se tiver problemas para fazer login.",
"Login_Psw-box": "Palavra-passe",
"Login_Psw_alert": "Alerta de palavra-passe!",
"Login_Psw_folder": "na pasta de configuração.",
"Login_Psw_new": "nova_palavra-passe",
"Login_Psw_run": "Para alterar a palavra-passe, executar:",
"Login_Remember": "Lembrar",
"Login_Remember_small": "(válido por 7 dias)",
"Login_Submit": "Iniciar sessão",
"Login_Toggle_Alert_headline": "Alerta de palavra-passe!",
"Login_Toggle_Info": "Informações sobre a palavra-passe",
"Login_Toggle_Info_headline": "Informações sobre a palavra-passe",
"Maint_PurgeLog": "Limpar o registo",
"Maint_RestartServer": "Reiniciar o servidor",
"Maint_Restart_Server_noti_text": "Tem certeza de que deseja reiniciar o servidor backend? Isto pode causar inconsistência na app. Faça primeiro um backup da sua configuração. <br/> <br/> Nota: Isto pode levar alguns minutos.",
"Maintenance_InitCheck": "",
"Maintenance_InitCheck_Checking": "",
"Maintenance_InitCheck_QuickSetupGuide": "",
"Maintenance_InitCheck_Success": "",
"Maintenance_ReCheck": "",
"Maintenance_Running_Version": "Versão instalada",
"Maintenance_Status": "Situação",
"Maintenance_Title": "Ferramentas de manutenção",
"Maintenance_Tool_DownloadConfig": "",
"Maintenance_Tool_DownloadConfig_text": "Descarregue um backup completo da configuração das Configurações armazenada no ficheiro <code>app.conf</code>.",
"Maintenance_Tool_DownloadWorkflows": "",
"Maintenance_Tool_DownloadWorkflows_text": "",
"Maintenance_Tool_ExportCSV": "",
"Maintenance_Tool_ExportCSV_noti": "",
"Maintenance_Tool_ExportCSV_noti_text": "Tem a certeza de que pretende gerar um ficheiro CSV?",
"Maintenance_Tool_ExportCSV_text": "Gere um ficheiro CSV (valor separado por vírgula) contendo a lista de dispositivos, incluindo os relacionamentos de rede entre os nós de rede e os dispositivos conectados. Também pode acionar isto a aceder esta URL <code>your_NetAlertX_url/php/server/devices.php?action=ExportCSV</code> ou ativando o plugin <a href=\"settings.php#CSVBCKP_header\">CSV Backup</a>.",
"Maintenance_Tool_ImportCSV": "Importação de dispositivos (csv)",
"Maintenance_Tool_ImportCSV_noti": "Importação de dispositivos (csv)",
"Maintenance_Tool_ImportCSV_noti_text": "Tem certeza de que deseja importar o ficheiro CSV? Isto <b>sobrescreverá</b> completamente os dispositivos na sua base de dados.",
"Maintenance_Tool_ImportCSV_text": "Antes de usar esta função, faça um backup. Importe um ficheiro CSV (valores separados por vírgula) contendo a lista de dispositivos, incluindo os relacionamentos de rede entre os nós de rede e os dispositivos conectados. Para fazer isto, ponha o ficheiro CSV chamado <b>devices.csv</b> na sua pasta <b>/config</b>.",
"Maintenance_Tool_ImportConfig_noti": "Importação de configurações (app.conf)",
"Maintenance_Tool_ImportPastedCSV": "Importação de dispositivos (csv) (colar)",
"Maintenance_Tool_ImportPastedCSV_noti_text": "Tem certeza de que deseja importar o CSV colado? Isto <b>sobrescreverá</b> completamente os dispositivos na sua base de dados.",
"Maintenance_Tool_ImportPastedCSV_text": "Antes de usar esta função, faça um backup. Importe um ficheiro CSV (valor separado por vírgula) contendo a lista de Dispositivos, incluindo os relacionamentos de Rede entre os Nós de Rede e os dispositivos conectados.",
"Maintenance_Tool_ImportPastedConfig": "Configurações Importar (colar)",
"Maintenance_Tool_ImportPastedConfig_noti_text": "Tem certeza de que deseja importar as configurações coladas? Isto irá <b>sobrescrever</b> completamente o ficheiro <code>app.conf</code>.",
"Maintenance_Tool_ImportPastedConfig_text": "Importa o ficheiro <code>app.conf</code> contendo todas as configurações da aplicação. Pode descarregar primeiro o ficheiro <code>app.conf</code> com a <b>Exportação de configurações</b>.",
"Maintenance_Tool_arpscansw": "Alternar arp-Scan (ligado/desligado)",
"Maintenance_Tool_arpscansw_noti": "Ativar ou desativar o arp-Scan",
"Maintenance_Tool_arpscansw_noti_text": "Quando a análise é desligada, permanece desligada até ser novamente ativada.",
"Maintenance_Tool_arpscansw_text": "Ligar ou desligar o arp-scan. Quando o scan é desligado, permanece desligado até ser novamente ativado. As varreduras ativas não são canceladas.",
"Maintenance_Tool_backup": "Cópia de segurança da BD",
"Maintenance_Tool_backup_noti": "Cópia de segurança da BD",
"Maintenance_Tool_backup_noti_text": "Tem a certeza de que pretende executar a cópia de segurança da BD? Certifique-se de que não está a ser executada nenhuma verificação.",
"Maintenance_Tool_backup_text": "Os backups da base de dados estão localizadas no diretório da base de dados como um arquivo zip, nomeado com a data de criação. Não há nenhum número máximo de backups.",
"Maintenance_Tool_check_visible": "Desmarque para esconder a coluna.",
"Maintenance_Tool_darkmode": "Modos de alternância (escuro/claro)",
"Maintenance_Tool_darkmode_noti": "Modos de alternância",
"Maintenance_Tool_darkmode_noti_text": "Após a mudança de tema, a página tenta recarregar-se para ativar a alteração. Se necessário, a cache deve ser limpa.",
"Maintenance_Tool_darkmode_text": "Alternar entre o modo escuro e o modo claro. Se a alternância não funcionar corretamente, tente limpar a cache do browser. A alteração ocorre no lado do servidor, pelo que afecta todos os dispositivos em utilização.",
"Maintenance_Tool_del_ActHistory": "A apagar a atividade da rede",
"Maintenance_Tool_del_ActHistory_noti": "Apagar atividade de rede",
"Maintenance_Tool_del_ActHistory_noti_text": "Tem certeza de que deseja redefinir a atividade da rede?",
"Maintenance_Tool_del_ActHistory_text": "O gráfico de atividade da rede é redefinido. Isto não afeta os eventos.",
"Maintenance_Tool_del_alldev": "",
"Maintenance_Tool_del_alldev_noti": "",
"Maintenance_Tool_del_alldev_noti_text": "Tem certeza de que deseja apagar todos os dispositivos?",
"Maintenance_Tool_del_alldev_text": "Antes de usar esta função, faça um backup. Apagar não pode ser desfeito. Todos os dispositivos serão apagados da base de dados.",
"Maintenance_Tool_del_allevents": "Apagar eventos (Repor presença)",
"Maintenance_Tool_del_allevents30": "Apagar todos os eventos com mais que 30 dias",
"Maintenance_Tool_del_allevents30_noti": "Apagar eventos",
"Maintenance_Tool_del_allevents30_noti_text": "",
"Maintenance_Tool_del_allevents30_text": "Antes de utilizar esta função, faça uma cópia de segurança. Apagar não pode ser anulado. Todos os eventos com mais que 30 dias na base de dados serão eliminados. Nesse momento, a presença de todos os dispositivos será reiniciada. Este facto pode dar origem a sessões inválidas. Isto significa que os dispositivos são apresentados como “presentes” apesar de estarem offline. Uma verificação enquanto o dispositivo em questão está online resolve o problema.",
"Maintenance_Tool_del_allevents_noti": "Apagar eventos",
"Maintenance_Tool_del_allevents_noti_text": "",
"Maintenance_Tool_del_allevents_text": "Antes de usar esta função, faça um backup. Apagar não pode ser desfeito. Todos os eventos na base de dados serão apagados. Nesse momento, a presença de todos os dispositivos será redefinida. Isto pode levar a sessões inválidas. Isto significa que os dispositivos são exibidos como \"presente\" embora estejam offline. Uma varredura enquanto o dispositivo em questão é on-line resolve o problema.",
"Maintenance_Tool_del_empty_macs": "",
"Maintenance_Tool_del_empty_macs_noti": "",
"Maintenance_Tool_del_empty_macs_noti_text": "Tem certeza que deseja apagar todos os dispositivos com endereços MAC vazios?<br>(talvez prefira arquivá-los)",
"Maintenance_Tool_del_empty_macs_text": "Antes de usar esta função, faça um backup. Apagar não pode ser desfeito. Todos os dispositivos sem MAC serão apagados da base de dados.",
"Maintenance_Tool_del_selecteddev": "Apagar dispositivos selecionados",
"Maintenance_Tool_del_selecteddev_text": "Antes de usar esta função, faça um backup. Apagar não pode ser desfeito. Dispositivos selecionados serão apagados da base de dados.",
"Maintenance_Tool_del_unknowndev": "",
"Maintenance_Tool_del_unknowndev_noti": "",
"Maintenance_Tool_del_unknowndev_noti_text": "Tem certeza que deseja apagar todos (desconhecidos) e (nome não encontrados) dispositivos?",
"Maintenance_Tool_del_unknowndev_text": "Antes de usar esta função, faça um backup. Apagar não pode ser desfeito. Todos os dispositivos nomeados (não conhecidos) serão apagados da base de dados.",
"Maintenance_Tool_displayed_columns_text": "Altere a visibilidade e a ordem das colunas na página <a href=\"devices.php\"><b> <i class=\"fa fa-portátil\"></i> Dispositivos</b></a>.",
"Maintenance_Tool_drag_me": "Arraste-me para reordenar colunas.",
"Maintenance_Tool_order_columns_text": "",
"Maintenance_Tool_purgebackup": "Limpar cópias de segurança",
"Maintenance_Tool_purgebackup_noti": "Limpar cópias de segurança",
"Maintenance_Tool_purgebackup_noti_text": "Tem certeza que deseja apagar todos os backups exceto os últimos 3?",
"Maintenance_Tool_purgebackup_text": "Todos os outros backups serão apagados exceto para os últimos 3 backups.",
"Maintenance_Tool_restore": "Restauração de DB",
"Maintenance_Tool_restore_noti": "Restauração de DB",
"Maintenance_Tool_restore_noti_text": "Tem a certeza de que quer executar a Restauração DB? Certifique-se de que nenhuma varredura funciona atualmente.",
"Maintenance_Tool_restore_text": "O backup mais recente pode ser restaurado através do botão, mas os backups mais antigos só podem ser restaurados manualmente. Após a restauração, faça uma verificação de integridade na base de dados para segurança, caso o db estivesse atualmente em acesso de gravação quando o backup foi criado.",
"Maintenance_Tool_upgrade_database_noti": "Atualizar a base de dados",
"Maintenance_Tool_upgrade_database_noti_text": "Tem certeza de que deseja atualizar a base de dados?<br>(talvez prefira arquivá-la)",
"Maintenance_Tool_upgrade_database_text": "Este botão atualizará a base de dados para ativar o gráfico Atividade de rede nas últimas 12 horas. Faça uma cópia de segurança da sua base de dados em caso de problemas.",
"Maintenance_Tools_Tab_BackupRestore": "Backup / Restauração",
"Maintenance_Tools_Tab_Logging": "",
"Maintenance_Tools_Tab_Settings": "Configurações",
"Maintenance_Tools_Tab_Tools": "Ferramentas",
"Maintenance_Tools_Tab_UISettings": "Configurações de interface",
"Maintenance_arp_status": "Estado de digitalização",
"Maintenance_arp_status_off": "está atualmente desativado",
"Maintenance_arp_status_on": "",
"Maintenance_built_on": "Construído em",
"Maintenance_current_version": "Você está atualizado. Confira o que <a href=\"https://github.com/jokob-sk/NetAlertX/issues/138\" target=\"_blank\"> estou a trabalhar em</a>.",
"Maintenance_database_backup": "Backups DB",
"Maintenance_database_backup_found": "foram encontrados backups",
"Maintenance_database_backup_total": "uso total do disco",
"Maintenance_database_lastmod": "Última modificação",
"Maintenance_database_path": "Caminho da base de dados",
"Maintenance_database_rows": "Tabela (linhas)",
"Maintenance_database_size": "Tamanho da base de dados",
"Maintenance_lang_selector_apply": "Aplicar",
"Maintenance_lang_selector_empty": "",
"Maintenance_lang_selector_lable": "",
"Maintenance_lang_selector_text": "A mudança ocorre no lado do cliente, por isso afeta apenas o navegador atual.",
"Maintenance_new_version": "Uma nova versão está disponível. Confira as <a href=\"https://github.com/jokob-sk/NetAlertX/releases\" target=\"_blank\">notas de lançamento</a>.",
"Maintenance_themeselector_apply": "Aplicar",
"Maintenance_themeselector_empty": "Escolha uma Skin",
"Maintenance_themeselector_lable": "Selecionar Skin",
"Maintenance_themeselector_text": "A mudança ocorre no lado do servidor, por isso afeta todos os dispositivos em uso.",
"Maintenance_version": "Atualizações de apps",
"NETWORK_DEVICE_TYPES_description": "",
"NETWORK_DEVICE_TYPES_name": "Tipos de dispositivo de rede",
"Navigation_About": "Sobre a",
"Navigation_AppEvents": "",
"Navigation_Devices": "Dispositivos",
"Navigation_Donations": "Doações",
"Navigation_Events": "Eventos",
"Navigation_Integrations": "Integrações",
"Navigation_Maintenance": "Manutenção",
"Navigation_Monitoring": "Acompanhamento",
"Navigation_Network": "Rede",
"Navigation_Notifications": "Notificações",
"Navigation_Plugins": "Plugins",
"Navigation_Presence": "",
"Navigation_Report": "",
"Navigation_Settings": "",
"Navigation_SystemInfo": "",
"Navigation_Workflows": "",
"Network_Assign": "",
"Network_Cant_Assign": "",
"Network_Cant_Assign_No_Node_Selected": "",
"Network_Configuration_Error": "",
"Network_Connected": "",
"Network_Devices": "",
"Network_ManageAdd": "",
"Network_ManageAdd_Name": "",
"Network_ManageAdd_Name_text": "",
"Network_ManageAdd_Port": "",
"Network_ManageAdd_Port_text": "",
"Network_ManageAdd_Submit": "",
"Network_ManageAdd_Type": "",
"Network_ManageAdd_Type_text": "",
"Network_ManageAssign": "",
"Network_ManageDel": "",
"Network_ManageDel_Name": "",
"Network_ManageDel_Name_text": "",
"Network_ManageDel_Submit": "",
"Network_ManageDevices": "",
"Network_ManageEdit": "",
"Network_ManageEdit_ID": "",
"Network_ManageEdit_ID_text": "",
"Network_ManageEdit_Name": "",
"Network_ManageEdit_Name_text": "",
"Network_ManageEdit_Port": "",
"Network_ManageEdit_Port_text": "",
"Network_ManageEdit_Submit": "",
"Network_ManageEdit_Type": "",
"Network_ManageEdit_Type_text": "",
"Network_ManageLeaf": "",
"Network_ManageUnassign": "",
"Network_NoAssignedDevices": "",
"Network_NoDevices": "",
"Network_Node": "",
"Network_Node_Name": "",
"Network_Parent": "",
"Network_Root": "",
"Network_Root_Not_Configured": "",
"Network_Root_Unconfigurable": "",
"Network_ShowArchived": "",
"Network_ShowOffline": "",
"Network_Table_Hostname": "",
"Network_Table_IP": "",
"Network_Table_State": "",
"Network_Title": "",
"Network_UnassignedDevices": "",
"Notifications_All": "",
"Notifications_Mark_All_Read": "",
"PIALERT_WEB_PASSWORD_description": "",
"PIALERT_WEB_PASSWORD_name": "",
"PIALERT_WEB_PROTECTION_description": "",
"PIALERT_WEB_PROTECTION_name": "",
"PLUGINS_KEEP_HIST_description": "",
"PLUGINS_KEEP_HIST_name": "",
"Plugins_DeleteAll": "",
"Plugins_Filters_Mac": "",
"Plugins_History": "",
"Plugins_Obj_DeleteListed": "",
"Plugins_Objects": "",
"Plugins_Out_of": "",
"Plugins_Unprocessed_Events": "",
"Plugins_no_control": "",
"Presence_CalHead_day": "",
"Presence_CalHead_lang": "",
"Presence_CalHead_month": "",
"Presence_CalHead_quarter": "",
"Presence_CalHead_week": "",
"Presence_CalHead_year": "",
"Presence_CallHead_Devices": "",
"Presence_Key_OnlineNow": "",
"Presence_Key_OnlineNow_desc": "",
"Presence_Key_OnlinePast": "",
"Presence_Key_OnlinePastMiss": "",
"Presence_Key_OnlinePastMiss_desc": "",
"Presence_Key_OnlinePast_desc": "",
"Presence_Loading": "",
"Presence_Shortcut_AllDevices": "",
"Presence_Shortcut_Archived": "",
"Presence_Shortcut_Connected": "",
"Presence_Shortcut_Devices": "",
"Presence_Shortcut_DownAlerts": "",
"Presence_Shortcut_Favorites": "",
"Presence_Shortcut_NewDevices": "",
"Presence_Title": "",
"REFRESH_FQDN_description": "",
"REFRESH_FQDN_name": "",
"REPORT_DASHBOARD_URL_description": "",
"REPORT_DASHBOARD_URL_name": "",
"REPORT_ERROR": "",
"REPORT_MAIL_description": "",
"REPORT_MAIL_name": "",
"REPORT_TITLE": "",
"RandomMAC_hover": "",
"Reports_Sent_Log": "",
"SCAN_SUBNETS_description": "",
"SCAN_SUBNETS_name": "",
"SYSTEM_TITLE": "",
"Setting_Override": "",
"Setting_Override_Description": "",
"Settings_Metadata_Toggle": "",
"Settings_Show_Description": "",
"Settings_device_Scanners_desync": "",
"Settings_device_Scanners_desync_popup": "",
"Speedtest_Results": "",
"Systeminfo_AvailableIps": "",
"Systeminfo_CPU": "",
"Systeminfo_CPU_Cores": "",
"Systeminfo_CPU_Name": "",
"Systeminfo_CPU_Speed": "",
"Systeminfo_CPU_Temp": "",
"Systeminfo_CPU_Vendor": "",
"Systeminfo_Client_Resolution": "",
"Systeminfo_Client_User_Agent": "",
"Systeminfo_General": "",
"Systeminfo_General_Date": "",
"Systeminfo_General_Date2": "",
"Systeminfo_General_Full_Date": "",
"Systeminfo_General_TimeZone": "",
"Systeminfo_Memory": "",
"Systeminfo_Memory_Total_Memory": "",
"Systeminfo_Memory_Usage": "",
"Systeminfo_Memory_Usage_Percent": "",
"Systeminfo_Motherboard": "",
"Systeminfo_Motherboard_BIOS": "",
"Systeminfo_Motherboard_BIOS_Date": "",
"Systeminfo_Motherboard_BIOS_Vendor": "",
"Systeminfo_Motherboard_Manufactured": "",
"Systeminfo_Motherboard_Name": "",
"Systeminfo_Motherboard_Revision": "",
"Systeminfo_Network": "",
"Systeminfo_Network_Accept_Encoding": "",
"Systeminfo_Network_Accept_Language": "",
"Systeminfo_Network_Connection_Port": "",
"Systeminfo_Network_HTTP_Host": "",
"Systeminfo_Network_HTTP_Referer": "",
"Systeminfo_Network_HTTP_Referer_String": "",
"Systeminfo_Network_Hardware": "",
"Systeminfo_Network_Hardware_Interface_Mask": "",
"Systeminfo_Network_Hardware_Interface_Name": "",
"Systeminfo_Network_Hardware_Interface_RX": "",
"Systeminfo_Network_Hardware_Interface_TX": "",
"Systeminfo_Network_IP": "",
"Systeminfo_Network_IP_Connection": "",
"Systeminfo_Network_IP_Server": "",
"Systeminfo_Network_MIME": "",
"Systeminfo_Network_Request_Method": "",
"Systeminfo_Network_Request_Time": "",
"Systeminfo_Network_Request_URI": "",
"Systeminfo_Network_Secure_Connection": "",
"Systeminfo_Network_Secure_Connection_String": "",
"Systeminfo_Network_Server_Name": "",
"Systeminfo_Network_Server_Name_String": "",
"Systeminfo_Network_Server_Query": "",
"Systeminfo_Network_Server_Query_String": "",
"Systeminfo_Network_Server_Version": "",
"Systeminfo_Services": "",
"Systeminfo_Services_Description": "",
"Systeminfo_Services_Name": "",
"Systeminfo_Storage": "",
"Systeminfo_Storage_Device": "",
"Systeminfo_Storage_Mount": "",
"Systeminfo_Storage_Size": "",
"Systeminfo_Storage_Type": "",
"Systeminfo_Storage_Usage": "",
"Systeminfo_Storage_Usage_Free": "",
"Systeminfo_Storage_Usage_Mount": "",
"Systeminfo_Storage_Usage_Total": "",
"Systeminfo_Storage_Usage_Used": "",
"Systeminfo_System": "",
"Systeminfo_System_AVG": "",
"Systeminfo_System_Architecture": "",
"Systeminfo_System_Kernel": "",
"Systeminfo_System_OSVersion": "",
"Systeminfo_System_Running_Processes": "",
"Systeminfo_System_System": "",
"Systeminfo_System_Uname": "",
"Systeminfo_System_Uptime": "",
"Systeminfo_This_Client": "",
"Systeminfo_USB_Devices": "",
"TICKER_MIGRATE_TO_NETALERTX": "",
"TIMEZONE_description": "",
"TIMEZONE_name": "",
"UI_DEV_SECTIONS_description": "",
"UI_DEV_SECTIONS_name": "",
"UI_ICONS_description": "",
"UI_ICONS_name": "",
"UI_LANG_description": "",
"UI_LANG_name": "",
"UI_MY_DEVICES_description": "",
"UI_MY_DEVICES_name": "",
"UI_NOT_RANDOM_MAC_description": "",
"UI_NOT_RANDOM_MAC_name": "",
"UI_PRESENCE_description": "",
"UI_PRESENCE_name": "",
"UI_REFRESH_description": "",
"UI_REFRESH_name": "",
"VERSION_description": "",
"VERSION_name": "",
"WF_Action_Add": "",
"WF_Action_field": "",
"WF_Action_type": "",
"WF_Action_value": "",
"WF_Actions": "",
"WF_Add": "",
"WF_Add_Condition": "",
"WF_Add_Group": "",
"WF_Condition_field": "",
"WF_Condition_operator": "",
"WF_Condition_value": "",
"WF_Conditions": "",
"WF_Conditions_logic_rules": "",
"WF_Duplicate": "",
"WF_Enabled": "",
"WF_Export": "",
"WF_Export_Copy": "",
"WF_Import": "",
"WF_Import_Copy": "",
"WF_Name": "",
"WF_Remove": "",
"WF_Remove_Copy": "",
"WF_Save": "",
"WF_Trigger": "",
"WF_Trigger_event_type": "",
"WF_Trigger_type": "",
"add_icon_event_tooltip": "",
"add_option_event_tooltip": "",
"copy_icons_event_tooltip": "",
"devices_old": "",
"general_event_description": "",
"general_event_title": "",
"go_to_device_event_tooltip": "",
"go_to_node_event_tooltip": "",
"new_version_available": "",
"report_guid": "",
"report_guid_missing": "",
"report_select_format": "",
"report_time": "",
"run_event_tooltip": "",
"select_icon_event_tooltip": "",
"settings_core_icon": "",
"settings_core_label": "",
"settings_device_scanners": "",
"settings_device_scanners_icon": "",
"settings_device_scanners_info": "",
"settings_device_scanners_label": "",
"settings_enabled": "",
"settings_enabled_icon": "",
"settings_expand_all": "",
"settings_imported": "",
"settings_imported_label": "",
"settings_missing": "",
"settings_missing_block": "",
"settings_old": "",
"settings_other_scanners": "",
"settings_other_scanners_icon": "",
"settings_other_scanners_label": "",
"settings_publishers": "",
"settings_publishers_icon": "",
"settings_publishers_info": "",
"settings_publishers_label": "",
"settings_readonly": "",
"settings_saved": "",
"settings_system_icon": "",
"settings_system_label": "",
"settings_update_item_warning": "",
"test_event_tooltip": "Guarde as alterações antes de testar as definições."
}

View File

@@ -107,7 +107,7 @@
"DevDetail_Network_Node_hover": "Выберите родительское сетевое устройство, к которому подключено текущее устройство, чтобы заполнить дерево сети.",
"DevDetail_Network_Port_hover": "Порт, к которому подключено это устройство на родительском сетевом устройстве. Если оставить пустым, в дереве сети отобразится значок Wi-Fi.",
"DevDetail_Nmap_Scans": "Ручные сканеры Nmap",
"DevDetail_Nmap_Scans_desc": "Здесь вы можете выполнить сканирование NMAP вручную. Вы также можете запланировать регулярное автоматическое сканирование NMAP с помощью плагина «Службы и порты» (NMAP). Чтобы узнать больше, перейдите в <a href='/settings.php' target='_blank'>Настройки</a>",
"DevDetail_Nmap_Scans_desc": "Здесь вы можете выполнить сканирование NMAP вручную. Вы также можете запланировать регулярное автоматическое сканирование NMAP с помощью плагина «Службы и порты» (NMAP). Чтобы узнать больше, перейдите в <a href=\"https://github.com/jokob-sk/NetAlertX/tree/main/front/plugins/nmap_scan\" target=\"_blank\">Документацию</a>",
"DevDetail_Nmap_buttonDefault": "Сканирование по умолчанию",
"DevDetail_Nmap_buttonDefault_text": "Сканирование по умолчанию: Nmap сканирует 1000 верхних портов для каждого запрошенного протокола сканирования. Это перехватывает примерно 93% портов TCP и 49% портов UDP. (около 5 секунд)",
"DevDetail_Nmap_buttonDetail": "Детальное сканирование",
@@ -301,7 +301,7 @@
"Gen_Cancel": "Отмена",
"Gen_Change": "Изменить",
"Gen_Copy": "Запустить",
"Gen_CopyToClipboard": "",
"Gen_CopyToClipboard": "Копировать в буфер обмена",
"Gen_DataUpdatedUITakesTime": "ОК - Обновление UI может занять некоторое время, если сканирование выполняется.",
"Gen_Delete": "Удалить",
"Gen_DeleteAll": "Удалить все",
@@ -309,9 +309,9 @@
"Gen_Error": "Ошибка",
"Gen_Filter": "Фильтр",
"Gen_Generate": "Генерировать",
"Gen_InvalidMac": "",
"Gen_InvalidMac": "Неверный Mac-адрес.",
"Gen_LockedDB": "ОШИБКА - Возможно, база данных заблокирована. Проверьте инструменты разработчика F12 -> Консоль или повторите попытку позже.",
"Gen_NetworkMask": "",
"Gen_NetworkMask": "Маска сети",
"Gen_Offline": "Оффлайн",
"Gen_Okay": "OK",
"Gen_Online": "Онлайн",
@@ -329,7 +329,7 @@
"Gen_SelectIcon": "<i class=\"fa-solid fa-chevron-down fa-fade\"></i>",
"Gen_SelectToPreview": "Выберите для предварительного просмотра",
"Gen_Selected_Devices": "Выбранные устройства:",
"Gen_Subnet": "",
"Gen_Subnet": "Подсеть",
"Gen_Switch": "Переключить",
"Gen_Upd": "Успешное обновление",
"Gen_Upd_Fail": "Не удалось обновить",
@@ -567,7 +567,7 @@
"Presence_Key_OnlineNow_desc": "Устройство, обнаруженное при последнем сканировании как подключенное к сети.",
"Presence_Key_OnlinePast": "В прошлом в сети",
"Presence_Key_OnlinePastMiss": "В прошлом в сети (несовпадение)",
"Presence_Key_OnlinePastMiss_desc": "Устройство в прошлом было подключено к сети, но сейчас находится в автономном режиме, однако стартовый сеанс может отсутствовать или иметь противоречивые данные. (Возможно, это ошибка — отправьте PR, если знаете, как это исправить — здесь я немного запутался в коде)",
"Presence_Key_OnlinePastMiss_desc": "Устройство в прошлом было подключено к сети, но сейчас находится в автономном режиме, однако стартовый сеанс может отсутствовать или иметь противоречивые данные.",
"Presence_Key_OnlinePast_desc": "Устройство раньше было в сети, но в настоящее время не в сети.",
"Presence_Loading": "Загрузка…",
"Presence_Shortcut_AllDevices": "Мои устройства",
@@ -598,7 +598,7 @@
"Settings_device_Scanners_desync": "⚠ Расписания сканера устройств не синхронизированы.",
"Settings_device_Scanners_desync_popup": "Расписания сканеров устройств (<code>*_RUN_SCHD</code>) не совпадают. Это приведет к несогласованным онлайн/оффлайн уведомлениям устройства. Если это не предусмотрено, используйте одно и то же расписание для всех включенных <b>🔍Сканеров устройств</b>.",
"Speedtest_Results": "Результаты теста скорости",
"Systeminfo_AvailableIps": "",
"Systeminfo_AvailableIps": "Доступные IP-адреса",
"Systeminfo_CPU": "CPU",
"Systeminfo_CPU_Cores": "Ядра CPU:",
"Systeminfo_CPU_Name": "Имя CPU:",
@@ -677,7 +677,7 @@
"TIMEZONE_description": "Часовой пояс для корректного отображения статистики. Найдите свой часовой пояс <a target=\"_blank\" href=\"https://en.wikipedia.org/wiki/List_of_tz_database_time_zones\" rel=\"nofollow\">здесь</a>.",
"TIMEZONE_name": "Часовой пояс",
"UI_DEV_SECTIONS_description": "Выберите, какие элементы интерфейса нужно скрыть на страницах «Устройства».",
"UI_DEV_SECTIONS_name": "Скрыть разделы Устройств",
"UI_DEV_SECTIONS_name": "Скрыть разделы устройств",
"UI_ICONS_description": "Список предопределенных значков. Действуйте осторожно: предпочтительный способ добавления значков описан в разделе <a href=\"https://github.com/jokob-sk/NetAlertX/blob/main/docs/ICONS.md\" target=\"_blank\"> документации по значкам</a>. Вы можете добавить HTML-тег SVG в кодировке Base64 или HTML-тег Font-awesome.",
"UI_ICONS_name": "Предопределенные значки",
"UI_LANG_description": "Выберите предпочтительный язык пользовательского интерфейса. Помогите перевести или предложите языки на онлайн-портале <a href=\"https://hosted.weblate.org/projects/pialert/core/\" target=\"_blank\">Weblate</a>.",
@@ -737,7 +737,7 @@
"settings_core_label": "Основные",
"settings_device_scanners": "Сканеры устройств, используемые для обнаружения устройств, записывающих данные в таблицу базы данных CurrentScan.",
"settings_device_scanners_icon": "fa-solid fa-magnifying-glass-plus",
"settings_device_scanners_info": "Загрузите еще больше сканеров устройств с помощью параметра <a href=\"/settings.php#LOADED_PLUGINS\">LOADED_PLUGINS</a>",
"settings_device_scanners_info": "Загрузите больше сканеров устройств с помощью параметра <a href=\"/settings.php#LOADED_PLUGINS\">LOADED_PLUGINS</a>",
"settings_device_scanners_label": "Сканеры устройств",
"settings_enabled": "Вкл. настройки",
"settings_enabled_icon": "fa-solid fa-toggle-on",
@@ -760,4 +760,4 @@
"settings_system_label": "Система",
"settings_update_item_warning": "Обновить значение ниже. Будьте осторожны, следуя предыдущему формату. <b>Проверка не выполняется.</b>",
"test_event_tooltip": "Сначала сохраните изменения, прежде чем проверять настройки."
}
}

View File

@@ -760,4 +760,4 @@
"settings_system_label": "Система",
"settings_update_item_warning": "Оновіть значення нижче. Слідкуйте за попереднім форматом. <b>Перевірка не виконана.</b>",
"test_event_tooltip": "Перш ніж перевіряти налаштування, збережіть зміни."
}
}

View File

@@ -301,7 +301,7 @@
"Gen_Cancel": "取消",
"Gen_Change": "修改",
"Gen_Copy": "运行",
"Gen_CopyToClipboard": "",
"Gen_CopyToClipboard": "复制到剪贴板",
"Gen_DataUpdatedUITakesTime": "好的 - 如果扫描正在运行UI 可能需要一段时间才能更新。",
"Gen_Delete": "删除",
"Gen_DeleteAll": "全部删除",
@@ -309,9 +309,9 @@
"Gen_Error": "错误",
"Gen_Filter": "筛选",
"Gen_Generate": "生成",
"Gen_InvalidMac": "",
"Gen_InvalidMac": "无效的 Mac 地址。",
"Gen_LockedDB": "错误 - DB 可能被锁定 - 检查 F12 开发工具 -> 控制台或稍后重试。",
"Gen_NetworkMask": "",
"Gen_NetworkMask": "网络掩码",
"Gen_Offline": "离线",
"Gen_Okay": "Ok",
"Gen_Online": "在线",
@@ -329,7 +329,7 @@
"Gen_SelectIcon": "<i class=\"fa-solid fa-chevron-down fa-fade\"></i>",
"Gen_SelectToPreview": "选择预览",
"Gen_Selected_Devices": "选定的设备:",
"Gen_Subnet": "",
"Gen_Subnet": "子网",
"Gen_Switch": "交换",
"Gen_Upd": "已成功更新",
"Gen_Upd_Fail": "更新失败",
@@ -498,7 +498,7 @@
"Network_Cant_Assign_No_Node_Selected": "无法分配,未选择父节点。",
"Network_Configuration_Error": "配置错误",
"Network_Connected": "联网设备",
"Network_Devices": "",
"Network_Devices": "网络设备",
"Network_ManageAdd": "添加设备",
"Network_ManageAdd_Name": "设备名称",
"Network_ManageAdd_Name_text": "名称不包含特殊字符",
@@ -567,7 +567,7 @@
"Presence_Key_OnlineNow_desc": "该设备在上次扫描中被检测为在线。",
"Presence_Key_OnlinePast": "之前在线",
"Presence_Key_OnlinePastMiss": "之前在线(不匹配)",
"Presence_Key_OnlinePastMiss_desc": "设备曾在线,但目前离线初始会话可能缺失或存在数据冲突",
"Presence_Key_OnlinePastMiss_desc": "设备曾在线,但目前离线初始会话可能缺失或存在数据冲突",
"Presence_Key_OnlinePast_desc": "设备曾在线,但目前离线。",
"Presence_Loading": "加载中…",
"Presence_Shortcut_AllDevices": "我的设备",
@@ -598,7 +598,7 @@
"Settings_device_Scanners_desync": "⚠ 设备扫描计划不同步。",
"Settings_device_Scanners_desync_popup": "设备扫描 (<code>*_RUN_SCHD</code>) 的时间表并不相同。这将导致设备在线/离线通知不一致。除非有意为之,否则请对所有启用的 <b>🔍设备扫描</b> 使用相同的时间表。",
"Speedtest_Results": "Speedtest 结果",
"Systeminfo_AvailableIps": "",
"Systeminfo_AvailableIps": "可用 IP",
"Systeminfo_CPU": "CPU",
"Systeminfo_CPU_Cores": "CPU 核心:",
"Systeminfo_CPU_Name": "CPU 名称:",
@@ -760,4 +760,4 @@
"settings_system_label": "系统",
"settings_update_item_warning": "更新下面的值。请注意遵循先前的格式。<b>未执行验证。</b>",
"test_event_tooltip": "在测试设置之前,请先保存更改。"
}
}

View File

@@ -1,6 +1,6 @@
<?php
require 'php/templates/notification.php';
require 'php/templates/modals.php';
//------------------------------------------------------------------------------
// check if authenticated
require_once $_SERVER['DOCUMENT_ROOT'] . '/php/templates/security.php';

View File

@@ -117,6 +117,29 @@
</div>
</div>
<!-- Modal form input -->
<div class="modal fade" id="modal-form" data-myparam-triggered-by="" style="display: none;">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal" aria-label="Close"><span aria-hidden="true">&times;</span></button>
<h4 id="modal-form-title" class="modal-title"> Modal Title </h4>
</div>
<div id="modal-form-message" class="modal-body"> Modal message </div>
<div id="modal-form-plc"></div>
<div class="modal-footer">
<button id="modal-form-cancel" type="button" class="btn btn-outline pull-left" style="min-width: 80px;" data-dismiss="modal"> Cancel </button>
<button id="modal-form-OK" type="button" class="btn btn-outline btn-modal-submit" style="min-width: 80px;" > OK </button>
</div>
</div>
</div>
</div>
<!-- Modal field input -->
<div class="modal modal-warning fade" id="modal-field-input" data-myparam-triggered-by="" style="display: none;">

View File

@@ -1,7 +1,7 @@
<?php
require 'php/templates/header.php';
require 'php/templates/notification.php';
require 'php/templates/modals.php';
?>
<!-- Page ------------------------------------------------------------------ -->

View File

@@ -642,7 +642,7 @@
{ "elementType": "input", "elementOptions": [], "transformers": [] }
]
},
"default_value": "user@gmail.com",
"default_value": "to@email.com",
"options": [],
"localized": ["name", "description"],
"name": [
@@ -674,7 +674,31 @@
{ "elementType": "input", "elementOptions": [], "transformers": [] }
]
},
"default_value": "NetAlertX <user@gmail.com>",
"default_value": "NetAlertX <from@email.com>",
"options": [],
"localized": ["name", "description"],
"name": [
{
"language_code": "en_us",
"string": "From email"
}
],
"description": [
{
"language_code": "en_us",
"string": "From which email the notification is sent <code>Name &lt;email&gt;</code>. Some SMTP servers need this to be an email only."
}
]
},
{
"function": "SUBJECT",
"type": {
"dataType": "string",
"elements": [
{ "elementType": "input", "elementOptions": [], "transformers": [] }
]
},
"default_value": "NetAlertX Report",
"options": [],
"localized": ["name", "description"],
"name": [
@@ -690,7 +714,7 @@
"description": [
{
"language_code": "en_us",
"string": "Notification email subject line. Some SMTP servers need this to be an email."
"string": "Notification email subject line."
},
{
"language_code": "es_es",

View File

@@ -115,8 +115,7 @@ def send(pHTML, pText):
mylog('debug', [f'[{pluginName}] SMTP_REPORT_TO: {hide_email(str(get_setting_value("SMTP_REPORT_TO")))} SMTP_USER: {hide_email(str(get_setting_value("SMTP_USER")))}'])
subject, from_email, to_email, message_html, message_text = sanitize_email_content('NetAlertX Report', get_setting_value("SMTP_REPORT_FROM"), get_setting_value("SMTP_REPORT_TO"), pHTML, pText)
subject, from_email, to_email, message_html, message_text = sanitize_email_content(str(get_setting_value("SMTP_SUBJECT")), get_setting_value("SMTP_REPORT_FROM"), get_setting_value("SMTP_REPORT_TO"), pHTML, pText)
emails = []

View File

@@ -4,6 +4,7 @@ from pytz import timezone, all_timezones, UnknownTimeZoneError
import sys
import re
import base64
import json
from datetime import datetime
INSTALL_PATH = "/app"
@@ -116,6 +117,51 @@ def decodeBase64(inputParamBase64):
return result
# -------------------------------------------------------------------
def decode_settings_base64(encoded_str, convert_types=True):
"""
Decodes a base64-encoded JSON list of settings into a dict.
Each setting entry format:
[group, key, type, value]
Example:
[
["group", "name", "string", "Home - local"],
["group", "base_url", "string", "https://..."],
["group", "api_version", "integer", "2"],
["group", "verify_ssl", "boolean", "False"]
]
Returns:
{
"name": "Home - local",
"base_url": "https://...",
"api_version": 2,
"verify_ssl": False
}
"""
decoded_json = base64.b64decode(encoded_str).decode("utf-8")
settings_list = json.loads(decoded_json)
settings_dict = {}
for _, key, _type, value in settings_list:
if convert_types:
_type_lower = _type.lower()
if _type_lower == "boolean":
settings_dict[key] = value.lower() == "true"
elif _type_lower == "integer":
settings_dict[key] = int(value)
elif _type_lower == "float":
settings_dict[key] = float(value)
else:
settings_dict[key] = value
else:
settings_dict[key] = value
return settings_dict
# -------------------------------------------------------------------
def normalize_mac(mac):
# Split the MAC address by colon (:) or hyphen (-) and convert each part to uppercase

View File

@@ -41,7 +41,7 @@ The plugin operates in three different modes based on the configuration settings
- **Schedule** `[n,h]`: `SYNC_RUN_SCHD`
- **Encryption Key** `[n,h]`: `SYNC_encryption_key`
- **Node Name** `[n]`: `SYNC_node_name`
- **Hub URL** `[n]`: `SYNC_hub_url`
- **Hub URL** `[n]`: `SYNC_hub_url` + `GRAPHQL_PORT` of the target
- **Sync Devices** `[n]`: `SYNC_devices`
- **Sync Plugins** `[n]`: `SYNC_plugins`
@@ -52,7 +52,7 @@ The plugin operates in three different modes based on the configuration settings
- **When to Run** `[n,h]`: `SYNC_RUN`
- **Schedule** `[n,h]`: `SYNC_RUN_SCHD`
- **Encryption Key** `[n,h]`: `SYNC_encryption_key`
- **Nodes to Pull From** `[h]`: `SYNC_nodes`
- **Nodes to Pull From** `[h]`: `SYNC_nodes` + `GRAPHQL_PORT` of the source nodes
### Usage
@@ -115,7 +115,7 @@ Initially, I had one virtual machine (VM) with 6 network cards, one for each VLA
2. Set the schedule (5 minutes works for me).
3. **API Token**: Use any string, but it must match the clients (e.g., `abc123`).
4. **Encryption Key**: Use any string, but it must match the clients (e.g., `abc123`).
5. Under **Nodes**, add the full URL for each client, e.g., `http://192.168.1.20.20211/`.
5. Under **Nodes**, add the full URL for each client, e.g., `http://192.168.1.20.20212/`, where the port `20212` is the value of the `GRAPHQL_PORT` setting of the given node (client)
6. **Node Name**: Leave blank.
7. Check **Sync Devices**.

View File

@@ -125,11 +125,11 @@
},
{
"language_code": "es_es",
"string": "Solo está habilitado si selecciona <code>schedule</code> en la configuración <a href=\"#SYNC_RUN\"><code>SYNC_RUN</code></a>. Asegúrese de ingresar la programación en el formato similar a cron correcto (por ejemplo, valide en <a href=\"https://crontab.guru/\" target=\"_blank\">crontab.guru</a>). Por ejemplo, ingresar <code>0 4 * * *</code> ejecutará el escaneo después de las 4 a.m. en el <a onclick=\"toggleAllSettings()\" href=\"#TIMEZONE\"><code>TIMEZONE</ código> que configuró arriba</a>. Se ejecutará la PRÓXIMA vez que pase el tiempo."
"string": "Solo está habilitado si selecciona <code>schedule</code> en la configuración <a href=\"#SYNC_RUN\"><code>SYNC_RUN</code></a>. Asegúrese de ingresar la programación en el formato similar a cron correcto (por ejemplo, valide en <a href=\"https://crontab.guru/\" target=\"_blank\">crontab.guru</a>). Por ejemplo, ingresar <code>0 4 * * *</code> ejecutará el escaneo después de las 4 a.m. en el <a onclick=\"toggleAllSettings()\" href=\"#TIMEZONE\"><code>TIMEZONE</código> que configuró arriba</a>. Se ejecutará la PRÓXIMA vez que pase el tiempo."
},
{
"language_code": "de_de",
"string": "Nur aktiviert, wenn Sie <code>schedule</code> in der <a href=\"#SYNC_RUN\"><code>SYNC_RUN</code>-Einstellung</a> auswählen. Stellen Sie sicher, dass Sie den Zeitplan im richtigen Cron-ähnlichen Format eingeben (z. B. validieren unter <a href=\"https://crontab.guru/\" target=\"_blank\">crontab.guru</a>). Wenn Sie beispielsweise <code>0 4 * * *</code> eingeben, wird der Scan nach 4 Uhr morgens in der <a onclick=\"toggleAllSettings()\" href=\"#TIMEZONE\"><code>TIMEZONE</ ausgeführt. Code> den Sie oben festgelegt haben</a>. Wird das NÄCHSTE Mal ausgeführt, wenn die Zeit vergeht."
"string": "Nur aktiviert, wenn Sie <code>schedule</code> in der <a href=\"#SYNC_RUN\"><code>SYNC_RUN</code>-Einstellung</a> auswählen. Stellen Sie sicher, dass Sie den Zeitplan im richtigen Cron-ähnlichen Format eingeben (z. B. validieren unter <a href=\"https://crontab.guru/\" target=\"_blank\">crontab.guru</a>). Wenn Sie beispielsweise <code>0 4 * * *</code> eingeben, wird der Scan nach 4 Uhr morgens in der <a onclick=\"toggleAllSettings()\" href=\"#TIMEZONE\"><code>TIMEZONE</code> ausgeführt, den Sie oben festgelegt haben</a>. Wird das NÄCHSTE Mal ausgeführt, wenn die Zeit vergeht."
}
]
},
@@ -245,7 +245,7 @@
"description": [
{
"language_code": "en_us",
"string": "If specified, the hub will pull Devices data from the listed nodes. The <code>API_TOKEN</code> and <code>SYNC_encryption_key</code> must be set to the same value across the hub and all the nodes to ensure proper authentication and communication."
"string": "If specified, the hub will pull Devices data from the listed nodes. The <code>API_TOKEN</code> and <code>SYNC_encryption_key</code> must be set to the same value across the hub and all the nodes to ensure proper authentication and communication. Add full host URL and use the value of the <code>GRAPHQL_PORT</code> setting of the target, as the port."
}
]
},
@@ -271,7 +271,7 @@
"description": [
{
"language_code": "en_us",
"string": "The URL of the hub (target instance). Set on the Node. Without a trailig slash, for example <code>http://192.168.1.82:20211</code>"
"string": "If specified, this node will push data to the <code>GRAPHQL_PORT</code> API of the target. Use the target device <code>GRAPHQL_PORT</code> URL Without a trailing slash, for example <code>http://192.168.1.82:20212</code>"
}
]
},
@@ -296,7 +296,7 @@
"description": [
{
"language_code": "en_us",
"string": "Use a unique node name, without spaces or special characters, such as <code>Node_Vlan01</code>"
"string": "The unique name of this node, without spaces or special characters, such as <code>Node_Vlan01</code>"
}
]
},

View File

@@ -28,7 +28,7 @@ from pytz import timezone
conf.tz = timezone(get_setting_value('TIMEZONE'))
# Make sure log level is initialized correctly
Logger(get_setting_value('LOG_LEVEL'))
lggr = Logger(get_setting_value('LOG_LEVEL'))
pluginName = 'SYNC'
@@ -148,7 +148,8 @@ def main():
message = f'[{pluginName}] Device data from node "{node_name}" written to {log_file_name}'
mylog('verbose', [message])
write_notification(message, 'info', timeNowTZ())
if lggr.isAbove('verbose'):
write_notification(message, 'info', timeNowTZ())
# Process any received data for the Device DB table (ONLY JSON)
@@ -265,66 +266,81 @@ def main():
return 0
# ------------------------------------------------------------------
# Data retrieval methods
api_endpoints = [
f"/sync", # New Python-based endpoint
f"/plugins/sync/hub.php" # Legacy PHP endpoint
]
# send data to the HUB
def send_data(api_token, file_content, encryption_key, file_path, node_name, pref, hub_url):
# Encrypt the log data using the encryption_key
"""Send encrypted data to HUB, preferring /sync endpoint and falling back to PHP version."""
encrypted_data = encrypt_data(file_content, encryption_key)
mylog('verbose', [f'[{pluginName}] Sending encrypted_data: "{encrypted_data}"'])
# Prepare the data payload for the POST request
data = {
'data': encrypted_data,
'file_path': file_path,
'plugin': pref,
'node_name': node_name
}
# Set the authorization header with the API token
headers = {'Authorization': f'Bearer {api_token}'}
api_endpoint = f"{hub_url}/plugins/sync/hub.php"
response = requests.post(api_endpoint, data=data, headers=headers)
mylog('verbose', [f'[{pluginName}] response: "{response}"'])
for endpoint in api_endpoints:
final_endpoint = hub_url + endpoint
try:
response = requests.post(final_endpoint, data=data, headers=headers, timeout=5)
mylog('verbose', [f'[{pluginName}] Tried endpoint: {final_endpoint}, status: {response.status_code}'])
if response.status_code == 200:
message = f'[{pluginName}] Data for "{file_path}" sent successfully via {final_endpoint}'
mylog('verbose', [message])
write_notification(message, 'info', timeNowTZ())
return True
except requests.RequestException as e:
mylog('verbose', [f'[{pluginName}] Error calling {final_endpoint}: {e}'])
# If all endpoints fail
message = f'[{pluginName}] Failed to send data for "{file_path}" via all endpoints'
mylog('verbose', [message])
write_notification(message, 'alert', timeNowTZ())
return False
if response.status_code == 200:
message = f'[{pluginName}] Data for "{file_path}" sent successfully'
mylog('verbose', [message])
write_notification(message, 'info', timeNowTZ())
else:
message = f'[{pluginName}] Failed to send data for "{file_path}" (Status code: {response.status_code})'
mylog('verbose', [message])
write_notification(message, 'alert', timeNowTZ())
# get data from the nodes to the HUB
def get_data(api_token, node_url):
"""Get data from NODE, preferring /sync endpoint and falling back to PHP version."""
mylog('verbose', [f'[{pluginName}] Getting data from node: "{node_url}"'])
# Set the authorization header with the API token
headers = {'Authorization': f'Bearer {api_token}'}
api_endpoint = f"{node_url}/plugins/sync/hub.php"
response = requests.get(api_endpoint, headers=headers)
# mylog('verbose', [f'[{pluginName}] response: "{response.text}"'])
for endpoint in api_endpoints:
final_endpoint = node_url + endpoint
if response.status_code == 200:
try:
# Parse JSON response
response_json = response.json()
return response_json
response = requests.get(final_endpoint, headers=headers, timeout=5)
mylog('verbose', [f'[{pluginName}] Tried endpoint: {final_endpoint}, status: {response.status_code}'])
except json.JSONDecodeError:
message = f'[{pluginName}] Failed to parse JSON response from "{node_url}"'
mylog('verbose', [message])
write_notification(message, 'alert', timeNowTZ())
return ""
if response.status_code == 200:
try:
return response.json()
except json.JSONDecodeError:
message = f'[{pluginName}] Failed to parse JSON from {final_endpoint}'
mylog('verbose', [message])
write_notification(message, 'alert', timeNowTZ())
return ""
except requests.RequestException as e:
mylog('verbose', [f'[{pluginName}] Error calling {final_endpoint}: {e}'])
else:
message = f'[{pluginName}] Failed to send data for "{node_url}" (Status code: {response.status_code})'
mylog('verbose', [message])
write_notification(message, 'alert', timeNowTZ())
return ""
# If all endpoints fail
message = f'[{pluginName}] Failed to get data from "{node_url}" via all endpoints'
mylog('verbose', [message])
write_notification(message, 'alert', timeNowTZ())
return ""

View File

@@ -0,0 +1,25 @@
## Overview
Unifi import plugin using the Site Manager API.
> [!TIP]
> The Site Manager API doesn't seems to have feature parity with the old API yet, so certain limitations apply.
### Quick setup guide
Navigate to your UniFi Site Manager _Settings -> Control Plane -> Integrations_.
- `UNIFIAPI_api_key` : You can generate your API key under the _Your API Keys_ section.
- `UNIFIAPI_base_url` : You can find your base url in the _API Request Format_ section, e.g. `https://192.168.100.1/proxy/network/integration/`
- `UNIFIAPI_api_version` : You can find your version as part of the url in the _API Request Format_ section, e.g. `v1`
- `UNIFIAPI_verify_ssl` : To skip SSL with you don't have an SSL certificate
### Usage
- Head to **Settings** > **Plugin name** to adjust the default values.
### Notes
- Version: 1.0.0
- Author: `jokob-sk`
- Release Date: `Aug 2025`

View File

@@ -0,0 +1,717 @@
{
"code_name": "unifi_api_import",
"unique_prefix": "UNIFIAPI",
"plugin_type": "device_scanner",
"execution_order": "Layer_0",
"enabled": true,
"data_source": "script",
"mapped_to_table": "CurrentScan",
"data_filters": [
{
"compare_column": "Object_PrimaryID",
"compare_operator": "==",
"compare_field_id": "txtMacFilter",
"compare_js_template": "'{value}'.toString()",
"compare_use_quotes": true
}
],
"show_ui": true,
"localized": [
"display_name",
"description",
"icon"
],
"display_name": [
{
"language_code": "en_us",
"string": "UniFi import (API)"
}
],
"description": [
{
"language_code": "en_us",
"string": "This plugin is used to import devices from an UNIFI controller via the Site Manager API."
}
],
"icon": [
{
"language_code": "en_us",
"string": "<i class=\"fa-solid fa-shield-halved\"></i>"
}
],
"params": [],
"settings": [
{
"function": "RUN",
"events": [
"run"
],
"type": {
"dataType": "string",
"elements": [
{
"elementType": "select",
"elementOptions": [],
"transformers": []
}
]
},
"default_value": "disabled",
"options": [
"disabled",
"once",
"schedule"
],
"localized": [
"name",
"description"
],
"name": [
{
"language_code": "en_us",
"string": "When to run"
}
],
"description": [
{
"language_code": "en_us",
"string": "When the plugin should run. Good options are <code>schedule</code>, <code>once</code>."
}
]
},
{
"function": "RUN_SCHD",
"type": {
"dataType": "string",
"elements": [
{
"elementType": "span",
"elementOptions": [
{
"cssClasses": "input-group-addon validityCheck"
},
{
"getStringKey": "Gen_ValidIcon"
}
],
"transformers": []
},
{
"elementType": "input",
"elementOptions": [
{
"onChange": "validateRegex(this)"
},
{
"base64Regex": "Xig/OlwqfCg/OlswLTldfFsxLTVdWzAtOV18WzAtOV0rLVswLTldK3xcKi9bMC05XSspKVxzKyg/OlwqfCg/OlswLTldfDFbMC05XXwyWzAtM118WzAtOV0rLVswLTldK3xcKi9bMC05XSspKVxzKyg/OlwqfCg/OlsxLTldfFsxMl1bMC05XXwzWzAxXXxbMC05XSstWzAtOV0rfFwqL1swLTldKykpXHMrKD86XCp8KD86WzEtOV18MVswLTJdfFswLTldKy1bMC05XSt8XCovWzAtOV0rKSlccysoPzpcKnwoPzpbMC02XXxbMC02XS1bMC02XXxcKi9bMC05XSspKSQ="
}
],
"transformers": []
}
]
},
"default_value": "*/5 * * * *",
"options": [],
"localized": [
"name",
"description"
],
"name": [
{
"language_code": "en_us",
"string": "Schedule"
}
],
"description": [
{
"language_code": "en_us",
"string": "Only enabled if you select <code>schedule</code> in the <a href=\"#UNIFIAPI_RUN\"><code>UNIFIAPI_RUN</code> setting</a>. Make sure you enter the schedule in the correct cron-like format (e.g. validate at <a href=\"https://crontab.guru/\" target=\"_blank\">crontab.guru</a>). For example entering <code>0 4 * * *</code> will run the scan after 4 am in the <a onclick=\"toggleAllSettings()\" href=\"#TIMEZONE\"><code>TIMEZONE</code> you set above</a>. Will be run NEXT time the time passes."
}
]
},
{
"function": "CMD",
"type": {
"dataType": "string",
"elements": [
{
"elementType": "input",
"elementOptions": [
{
"readonly": "true"
}
],
"transformers": []
}
]
},
"default_value": "python3 /app/front/plugins/unifi_api_import/unifi_api_import.py",
"options": [],
"localized": [
"name",
"description"
],
"name": [
{
"language_code": "en_us",
"string": "Command"
}
],
"description": [
{
"language_code": "en_us",
"string": "Command to run. This can not be changed"
}
]
},
{
"function": "RUN_TIMEOUT",
"type": {
"dataType": "integer",
"elements": [
{
"elementType": "input",
"elementOptions": [
{
"type": "number"
}
],
"transformers": []
}
]
},
"default_value": 10,
"options": [],
"localized": [
"name",
"description"
],
"name": [
{
"language_code": "en_us",
"string": "Run timeout"
}
],
"description": [
{
"language_code": "en_us",
"string": "Maximum time in seconds to wait for the script to finish. If this time is exceeded the script is aborted."
}
]
},
{
"function": "sites",
"type": {
"dataType": "array",
"elements": [
{
"elementType": "button",
"elementOptions": [
{
"sourceSuffixes": []
},
{
"separator": ""
},
{
"cssClasses": "col-xs-12"
},
{
"onClick": "addViaPopupForm(this)"
},
{
"getStringKey": "Gen_Add"
}
],
"transformers": []
},
{
"elementType": "select",
"elementHasInputValue": 1,
"elementOptions": [
{
"multiple": "true"
},
{
"readonly": "true"
},
{
"editable": "true"
},
{
"popupForm": [
{
"function": "UNIFIAPI_site_name",
"type": {
"dataType": "string",
"elements": [
{
"elementType": "input",
"elementOptions": [
{
"placeholder": "Enter value"
},
{
"cssClasses": "col-sm-10"
}
],
"transformers": []
}
]
},
"default_value": "default",
"options": [],
"localized": [
"name",
"description"
],
"name": [
{
"language_code": "en_us",
"string": "Site name"
}
],
"description": [
{
"language_code": "en_us",
"string": "The name of your site. Use a descriptive name."
}
]
},
{
"function": "UNIFIAPI_base_url",
"type": {
"dataType": "string",
"elements": [
{
"elementType": "input",
"elementOptions": [
{
"placeholder": "https://host_ip/proxy/network/integration/"
},
{
"cssClasses": "col-sm-10"
}
],
"transformers": []
}
]
},
"default_value": "default",
"options": [],
"localized": [
"name",
"description"
],
"name": [
{
"language_code": "en_us",
"string": "Base URL"
}
],
"description": [
{
"language_code": "en_us",
"string": "You can find your base url in the UniFi Site Manager in <i>Settings -> Control Plane -> Integrations</i> in the <i>API Request Format</i> section, (e.g. <code>https://host_ip/proxy/network/integration/</code>)."
}
]
},
{
"function": "UNIFIAPI_api_version",
"type": {
"dataType": "string",
"elements": [
{
"elementType": "input",
"elementOptions": [
{
"placeholder": "v1"
},
{
"cssClasses": "col-sm-10"
}
],
"transformers": []
}
]
},
"default_value": "default",
"options": [],
"localized": [
"name",
"description"
],
"name": [
{
"language_code": "en_us",
"string": "API version"
}
],
"description": [
{
"language_code": "en_us",
"string": "The version of the API (e.g.: <code>v1</code>)."
}
]
},
{
"function": "UNIFIAPI_api_key",
"type": {
"dataType": "string",
"elements": [
{
"elementType": "input",
"elementOptions": [
{
"placeholder": "Enter value"
},
{
"cssClasses": "col-sm-10"
}
],
"transformers": []
}
]
},
"default_value": "default",
"options": [],
"localized": [
"name",
"description"
],
"name": [
{
"language_code": "en_us",
"string": "API key"
}
],
"description": [
{
"language_code": "en_us",
"string": "You can get an API key in your UniFi Site Manager in <i>Settings -> Control Plane -> Integrations</i>."
}
]
},
{
"function": "UNIFIAPI_verify_ssl",
"type": {
"dataType": "boolean",
"elements": [
{
"elementType": "input",
"elementOptions": [
{
"type": "checkbox"
}
],
"transformers": []
}
]
},
"default_value": 1,
"options": [],
"localized": [
"name",
"description"
],
"name": [
{
"language_code": "en_us",
"string": "Verify SSL"
}
],
"description": [
{
"language_code": "en_us",
"string": "Disable if you do not have an SSL certificate set up."
}
]
}
],
"transformers": []
}
],
"transformers": [
"name|base64"
]
},
{
"elementType": "button",
"elementOptions": [
{
"sourceSuffixes": []
},
{
"separator": ""
},
{
"cssClasses": "col-xs-6"
},
{
"onClick": "removeFromList(this)"
},
{
"getStringKey": "Gen_Remove_Last"
}
],
"transformers": []
},
{
"elementType": "button",
"elementOptions": [
{
"sourceSuffixes": []
},
{
"separator": ""
},
{
"cssClasses": "col-xs-6"
},
{
"onClick": "removeAllOptions(this)"
},
{
"getStringKey": "Gen_Remove_All"
}
],
"transformers": []
}
]
},
"default_value": [],
"options": [],
"localized": [
"name",
"description"
],
"name": [
{
"language_code": "en_us",
"string": "Sites"
}
],
"description": [
{
"language_code": "en_us",
"string": "UniFi site configurations. Use a unique name for each site. You can find necessary details to configure this in your controller under <i>Settings -> Control Plane -> Integrations</i>."
}
]
}
],
"database_column_definitions": [
{
"column": "Index",
"css_classes": "col-sm-2",
"show": true,
"type": "none",
"default_value": "",
"options": [],
"localized": [
"name"
],
"name": [
{
"language_code": "en_us",
"string": "Index"
}
]
},
{
"column": "Object_PrimaryID",
"mapped_to_column": "cur_MAC",
"css_classes": "col-sm-3",
"show": true,
"type": "device_name_mac",
"default_value": "",
"options": [],
"localized": [
"name"
],
"name": [
{
"language_code": "en_us",
"string": "MAC (name)"
}
]
},
{
"column": "Object_SecondaryID",
"mapped_to_column": "cur_IP",
"css_classes": "col-sm-2",
"show": true,
"type": "device_ip",
"default_value": "",
"options": [],
"localized": [
"name"
],
"name": [
{
"language_code": "en_us",
"string": "IP"
}
]
},
{
"column": "Watched_Value1",
"mapped_to_column": "cur_Name",
"css_classes": "col-sm-2",
"show": true,
"type": "label",
"default_value": "",
"options": [],
"localized": [
"name"
],
"name": [
{
"language_code": "en_us",
"string": "Name"
}
]
},
{
"column": "Watched_Value2",
"mapped_to_column": "cur_Type",
"css_classes": "col-sm-2",
"show": true,
"type": "label",
"default_value": "",
"options": [],
"localized": [
"name"
],
"name": [
{
"language_code": "en_us",
"string": "Device Type"
}
]
},
{
"column": "Watched_Value3",
"css_classes": "col-sm-2",
"show": true,
"type": "label",
"default_value": "",
"options": [],
"localized": [
"name"
],
"name": [
{
"language_code": "en_us",
"string": "Connected"
}
]
},
{
"column": "Watched_Value4",
"mapped_to_column": "cur_NetworkNodeMAC",
"css_classes": "col-sm-2",
"show": true,
"type": "device_mac",
"default_value": "",
"options": [],
"localized": [
"name"
],
"name": [
{
"language_code": "en_us",
"string": "Parent"
}
]
},
{
"column": "Dummy",
"mapped_to_column": "cur_ScanMethod",
"mapped_to_column_data": {
"value": "UNIFIAPI"
},
"css_classes": "col-sm-2",
"show": false,
"type": "label",
"default_value": "",
"options": [],
"localized": [
"name"
],
"name": [
{
"language_code": "en_us",
"string": "Scan method"
}
]
},
{
"column": "DateTimeCreated",
"css_classes": "col-sm-2",
"show": true,
"type": "label",
"default_value": "",
"options": [],
"localized": [
"name"
],
"name": [
{
"language_code": "en_us",
"string": "Created"
}
]
},
{
"column": "DateTimeChanged",
"css_classes": "col-sm-2",
"show": true,
"type": "label",
"default_value": "",
"options": [],
"localized": [
"name"
],
"name": [
{
"language_code": "en_us",
"string": "Changed"
}
]
},
{
"column": "Status",
"css_classes": "col-sm-1",
"show": true,
"type": "replace",
"default_value": "",
"options": [
{
"equals": "watched-not-changed",
"replacement": "<div style='text-align:center'><i class='fa-solid fa-square-check'></i><div></div>"
},
{
"equals": "watched-changed",
"replacement": "<div style='text-align:center'><i class='fa-solid fa-triangle-exclamation'></i></div>"
},
{
"equals": "new",
"replacement": "<div style='text-align:center'><i class='fa-solid fa-circle-plus'></i></div>"
},
{
"equals": "missing-in-last-scan",
"replacement": "<div style='text-align:center'><i class='fa-solid fa-question'></i></div>"
}
],
"localized": [
"name"
],
"name": [
{
"language_code": "en_us",
"string": "Status"
}
]
}
]
}

View File

@@ -0,0 +1,182 @@
#!/usr/bin/env python
import os
import pathlib
import sys
import json
import sqlite3
from pytz import timezone
from unifi_sm_api.api import SiteManagerAPI
# Define the installation path and extend the system path for plugin imports
INSTALL_PATH = "/app"
sys.path.extend([f"{INSTALL_PATH}/front/plugins", f"{INSTALL_PATH}/server"])
from plugin_helper import Plugin_Object, Plugin_Objects, decodeBase64, decode_settings_base64
from plugin_utils import get_plugins_configs
from logger import mylog, Logger
from const import pluginsPath, fullDbPath, logPath
from helper import timeNowTZ, get_setting_value
from messaging.in_app import write_notification
import conf
# Make sure the TIMEZONE for logging is correct
conf.tz = timezone(get_setting_value('TIMEZONE'))
# Make sure log level is initialized correctly
Logger(get_setting_value('LOG_LEVEL'))
pluginName = 'UNIFIAPI'
# Define the current path and log file paths
LOG_PATH = logPath + '/plugins'
LOG_FILE = os.path.join(LOG_PATH, f'script.{pluginName}.log')
RESULT_FILE = os.path.join(LOG_PATH, f'last_result.{pluginName}.log')
# Initialize the Plugin obj output file
plugin_objects = Plugin_Objects(RESULT_FILE)
def main():
mylog('verbose', [f'[{pluginName}] In script'])
# Retrieve configuration settings
unifi_sites_configs = get_setting_value('UNIFIAPI_sites')
mylog('verbose', [f'[{pluginName}] number of unifi_sites_configs: {len(unifi_sites_configs)}'])
for site_config in unifi_sites_configs:
siteDict = decode_settings_base64(site_config)
mylog('verbose', [f'[{pluginName}] siteDict: {json.dumps(siteDict)}'])
mylog('none', [f'[{pluginName}] Connecting to: {siteDict["UNIFIAPI_site_name"]}'])
api = SiteManagerAPI(
api_key=siteDict["UNIFIAPI_api_key"],
version=siteDict["UNIFIAPI_api_version"],
base_url=siteDict["UNIFIAPI_base_url"],
verify_ssl=siteDict["UNIFIAPI_verify_ssl"]
)
sites_resp = api.get_sites()
sites = sites_resp.get("data", [])
for site in sites:
# retrieve data
device_data = get_device_data(site, api)
# Process the data into native application tables
if len(device_data) > 0:
# insert devices into the lats_result.log
for device in device_data:
plugin_objects.add_object(
primaryId = device['dev_mac'], # mac
secondaryId = device['dev_ip'], # IP
watched1 = device['dev_name'], # name
watched2 = device['dev_type'], # device_type (AP/Switch etc)
watched3 = device['dev_connected'], # connectedAt or empty
watched4 = device['dev_parent_mac'],# parent_mac or "Internet"
extra = '',
foreignKey = device['dev_mac']
)
mylog('verbose', [f'[{pluginName}] New entries: "{len(device_data)}"'])
# log result
plugin_objects.write_result_file()
return 0
# retrieve data
def get_device_data(site, api):
device_data = []
mylog('verbose', [f'[{pluginName}] Site: {site} '])
site_id = site["id"]
site_name = site.get("name", "Unnamed Site")
mylog('verbose', [f'[{pluginName}] Site: {site_name} ({site_id})'])
# --- Devices ---
unifi_devices_resp = api.get_unifi_devices(site_id)
unifi_devices = unifi_devices_resp.get("data", [])
mylog('verbose', [f'[{pluginName}] Site: {site_name} unifi devices: {json.dumps(unifi_devices_resp, indent=2)}'])
# --- Clients ---
clients_resp = api.get_clients(site_id)
clients = clients_resp.get("data", [])
mylog('verbose', [f'[{pluginName}] Site: {site_name} clients: {json.dumps(clients_resp, indent=2)}'])
# Build a lookup for devices by their 'id' to find parent MAC easily
device_id_to_mac = {dev['id']: dev.get('macAddress', '') for dev in unifi_devices}
# Helper to resolve uplinkDeviceId to parent MAC, or "Internet" if no uplink
def resolve_parent_mac(uplink_id):
if not uplink_id:
return "Internet"
return device_id_to_mac.get(uplink_id, "Unknown")
# Process Unifi devices
for device in unifi_devices:
dev_mac = device.get('macAddress', '')
dev_ip = device.get('ipAddress', '')
dev_name = device.get('name', '')
# Determine device_type based on features and type
# If device has "accessPoint" feature => type "AP"
# Else if "switching" feature => type "Switch"
# fallback to "Unknown"
features = device.get('features', [])
if 'accessPoint' in features:
device_type = 'AP'
elif 'switching' in features:
device_type = 'Switch'
else:
device_type = 'Unknown'
dev_type = device_type
# No connectedAt for devices, so empty
dev_connected = ''
uplinkDeviceId = device.get('uplinkDeviceId', '')
dev_parent_mac = resolve_parent_mac(uplinkDeviceId)
device_data.append({
"dev_mac": dev_mac,
"dev_ip": dev_ip,
"dev_name": dev_name,
"dev_type": dev_type,
"dev_connected": dev_connected,
"dev_parent_mac": dev_parent_mac
})
# Process Clients (child devices connected to APs or switches)
for client in clients:
dev_mac = client.get('macAddress', '')
dev_ip = client.get('ipAddress', '')
dev_name = client.get('name', '')
device_type = ""
dev_type = device_type
dev_connected = client.get('connectedAt', '')
uplinkDeviceId = client.get('uplinkDeviceId', '')
dev_parent_mac = resolve_parent_mac(uplinkDeviceId)
device_data.append({
"dev_mac": dev_mac,
"dev_ip": dev_ip,
"dev_name": dev_name,
"dev_type": dev_type,
"dev_connected": dev_connected,
"dev_parent_mac": dev_parent_mac
})
return device_data
if __name__ == '__main__':
main()

View File

@@ -1,7 +1,7 @@
<?php
require 'php/templates/header.php';
require 'php/templates/notification.php';
require 'php/templates/modals.php';
?>
@@ -86,7 +86,7 @@
// Function to update the displayed data and timestamp based on the selected format and index
function updateData(format, index) {
// Fetch data from the API endpoint
fetch(`/php/server/query_json.php?file=table_notifications.json&nocache=${Date.now()}`)
fetch(`php/server/query_json.php?file=table_notifications.json&nocache=${Date.now()}`)
.then(response => response.json())
.then(data => {
if (index < 0) {

View File

@@ -58,6 +58,12 @@ $settingsJSON_DB = json_encode($settings, JSON_HEX_TAG | JSON_HEX_AMP | JSON_HEX
<div id="settingsPage" class="content-wrapper">
<a style="cursor:pointer">
<span>
<i id='toggleSettings' onclick="toggleAllSettings()" class="settings-expand-icon fa fa-angle-double-down"></i>
</span>
</a>
<!-- Content header--------------------------------------------------------- -->
<section class="content-header">
@@ -193,8 +199,8 @@ $settingsJSON_DB = json_encode($settings, JSON_HEX_TAG | JSON_HEX_AMP | JSON_HEX
console.log("Response:", response);
// Handle the successful response
if (response && response.settings) {
const settingsData = response.settings.settings;
if (response && response.data && response.data.settings && response.data.settings.settings) {
const settingsData = response.data.settings.settings;
console.log("Settings:", settingsData);
// Wrong number of settings processing
@@ -547,7 +553,7 @@ $settingsJSON_DB = json_encode($settings, JSON_HEX_TAG | JSON_HEX_AMP | JSON_HEX
}, 1500);
} else {
var settingsArray = [];
let settingsArray = [];
// collect values for each of the different input form controls
// get settings to determine setting type to store values appropriately
@@ -559,120 +565,123 @@ $settingsJSON_DB = json_encode($settings, JSON_HEX_TAG | JSON_HEX_AMP | JSON_HEX
setType = set["setType"]
setCodeName = set["setKey"]
// console.log(prefix);
settingsArray = collectSetting(prefix, setCodeName, setType, settingsArray)
const setTypeObject = JSON.parse(processQuotes(setType))
// console.log(setTypeObject);
// // console.log(prefix);
const dataType = setTypeObject.dataType;
// const setTypeObject = JSON.parse(processQuotes(setType))
// // console.log(setTypeObject);
// get the element with the input value(s)
let elements = setTypeObject.elements.filter(element => element.elementHasInputValue === 1);
// const dataType = setTypeObject.dataType;
// if none found, take last
if(elements.length == 0)
{
elementWithInputValue = setTypeObject.elements[setTypeObject.elements.length - 1]
} else
{
elementWithInputValue = elements[0]
}
// // get the element with the input value(s)
// let elements = setTypeObject.elements.filter(element => element.elementHasInputValue === 1);
const { elementType, elementOptions = [], transformers = [] } = elementWithInputValue;
const {
inputType,
readOnly,
isMultiSelect,
isOrdeable,
cssClasses,
placeholder,
suffix,
sourceIds,
separator,
editable,
valRes,
getStringKey,
onClick,
onChange,
customParams,
customId,
columns,
base64Regex
} = handleElementOptions('none', elementOptions, transformers, val = "");
// // if none found, take last
// if(elements.length == 0)
// {
// elementWithInputValue = setTypeObject.elements[setTypeObject.elements.length - 1]
// } else
// {
// elementWithInputValue = elements[0]
// }
let value;
// const { elementType, elementOptions = [], transformers = [] } = elementWithInputValue;
// const {
// inputType,
// readOnly,
// isMultiSelect,
// isOrdeable,
// cssClasses,
// placeholder,
// suffix,
// sourceIds,
// separator,
// editable,
// valRes,
// getStringKey,
// onClick,
// onChange,
// customParams,
// customId,
// columns,
// base64Regex,
// elementOptionsBase64
// } = handleElementOptions('none', elementOptions, transformers, val = "");
if (dataType === "string" && elementWithInputValue.elementType === "datatable" ) {
// let value;
value = collectTableData(`#${setCodeName}_table`)
settingsArray.push([prefix, setCodeName, dataType, btoa(JSON.stringify(value))]);
// if (dataType === "string" && elementWithInputValue.elementType === "datatable" ) {
} else if (dataType === "string" ||
(dataType === "integer" && (inputType === "number" || inputType === "text"))) {
// value = collectTableData(`#${setCodeName}_table`)
// settingsArray.push([prefix, setCodeName, dataType, btoa(JSON.stringify(value))]);
// } else if (dataType === "string" ||
// (dataType === "integer" && (inputType === "number" || inputType === "text"))) {
value = $('#' + setCodeName).val();
value = applyTransformers(value, transformers);
// value = $('#' + setCodeName).val();
// value = applyTransformers(value, transformers);
settingsArray.push([prefix, setCodeName, dataType, value]);
// settingsArray.push([prefix, setCodeName, dataType, value]);
} else if (inputType === 'checkbox') {
// } else if (inputType === 'checkbox') {
value = $(`#${setCodeName}`).is(':checked') ? 1 : 0;
// value = $(`#${setCodeName}`).is(':checked') ? 1 : 0;
if(dataType === "boolean")
{
value = value == 1 ? "True" : "False";
}
// if(dataType === "boolean")
// {
// value = value == 1 ? "True" : "False";
// }
value = applyTransformers(value, transformers);
settingsArray.push([prefix, setCodeName, dataType, value]);
// value = applyTransformers(value, transformers);
// settingsArray.push([prefix, setCodeName, dataType, value]);
} else if (dataType === "array" ) {
// } else if (dataType === "array" ) {
let temps = [];
// let temps = [];
if(isOrdeable)
{
temps = $(`#${setCodeName}`).val()
} else
{
// make sure to collect all if set as "editable" or selected only otherwise
$(`#${setCodeName}`).attr("my-editable") == "true" ? additionalSelector = "" : additionalSelector = ":selected";
// if(isOrdeable)
// {
// temps = $(`#${setCodeName}`).val()
// } else
// {
// // make sure to collect all if set as "editable" or selected only otherwise
// $(`#${setCodeName}`).attr("my-editable") == "true" ? additionalSelector = "" : additionalSelector = ":selected";
$(`#${setCodeName} option${additionalSelector}`).each(function() {
const vl = $(this).val();
if (vl !== '') {
temps.push(applyTransformers(vl, transformers));
}
});
}
// $(`#${setCodeName} option${additionalSelector}`).each(function() {
// const vl = $(this).val();
// if (vl !== '') {
// temps.push(applyTransformers(vl, transformers));
// }
// });
// }
value = JSON.stringify(temps);
// value = JSON.stringify(temps);
settingsArray.push([prefix, setCodeName, dataType, value]);
// settingsArray.push([prefix, setCodeName, dataType, value]);
} else if (dataType === "none") {
// no value to save
value = ""
settingsArray.push([prefix, setCodeName, dataType, value]);
// } else if (dataType === "none") {
// // no value to save
// value = ""
// settingsArray.push([prefix, setCodeName, dataType, value]);
} else if (dataType === "json") {
// } else if (dataType === "json") {
value = $('#' + setCodeName).val();
value = applyTransformers(value, transformers);
value = JSON.stringify(value, null, 2)
settingsArray.push([prefix, setCodeName, dataType, value]);
// value = $('#' + setCodeName).val();
// value = applyTransformers(value, transformers);
// value = JSON.stringify(value, null, 2)
// settingsArray.push([prefix, setCodeName, dataType, value]);
} else {
// } else {
console.error(`[saveSettings] Couldn't determine how to handle (setCodeName|dataType|inputType):(${setCodeName}|${dataType}|${inputType})`);
// console.error(`[saveSettings] Couldn't determine how to handle (setCodeName|dataType|inputType):(${setCodeName}|${dataType}|${inputType})`);
value = $('#' + setCodeName).val();
value = applyTransformers(value, transformers);
console.error(`[saveSettings] Saving value "${value}"`);
settingsArray.push([prefix, setCodeName, dataType, value]);
}
// value = $('#' + setCodeName).val();
// value = applyTransformers(value, transformers);
// console.error(`[saveSettings] Saving value "${value}"`);
// settingsArray.push([prefix, setCodeName, dataType, value]);
// }
});
// sanity check to make sure settings were loaded & collected correctly

View File

@@ -13,7 +13,7 @@
require 'php/templates/header.php';
?>
<?php require 'php/templates/notification.php'; ?>
<?php require 'php/templates/modals.php'; ?>
<!-- ----------------------------------------------------------------------- -->

View File

@@ -249,7 +249,7 @@ function fetchUsedIps(callback) {
console.log(response);
const usedIps = (response?.devices?.devices || [])
const usedIps = (response?.data?.devices?.devices || [])
.map(d => d.devLastIP)
.filter(ip => ip && ip.includes('.'));
callback(usedIps);

View File

@@ -1,7 +1,7 @@
<?php
require 'php/templates/header.php';
require 'php/templates/notification.php';
require 'php/templates/modals.php';
?>
<!-- ----------------------------------------------------------------------- -->

View File

@@ -30,5 +30,5 @@ source myenv/bin/activate
update-alternatives --install /usr/bin/python python /usr/bin/python3 10
# install packages thru pip3
pip3 install openwrt-luci-rpc asusrouter asyncio aiohttp graphene flask flask-cors tplink-omada-client wakeonlan pycryptodome requests paho-mqtt scapy cron-converter pytz json2table dhcp-leases pyunifi speedtest-cli chardet python-nmap dnspython librouteros yattag git+https://github.com/foreign-sub/aiofreepybox.git
pip3 install openwrt-luci-rpc asusrouter asyncio aiohttp graphene flask flask-cors unifi-sm-api tplink-omada-client wakeonlan pycryptodome requests paho-mqtt scapy cron-converter pytz json2table dhcp-leases pyunifi speedtest-cli chardet python-nmap dnspython librouteros yattag git+https://github.com/foreign-sub/aiofreepybox.git

View File

@@ -19,6 +19,7 @@ nav:
- Docker Updates: UPDATES.md
- Other:
- Synology Guide: SYNOLOGY_GUIDE.md
- Portainer Stacks: DOCKER_PORTAINER.md
- Community Guides: COMMUNITY_GUIDES.md
- Bare-metal (Experimental): HW_INSTALL.md
- Migration Guide: MIGRATION.md
@@ -64,6 +65,7 @@ nav:
- Debugging Tips: DEBUG_TIPS.md
- Debugging GraphQL: DEBUG_GRAPHQL.md
- Debugging Invalid JSON: DEBUG_INVALID_JSON.md
- Debugging PHP: DEBUG_PHP.md
- Debugging Plugins: DEBUG_PLUGINS.md
- Debugging Web UI Port: WEB_UI_PORT_DEBUG.md
- Debugging Workflows: WORKFLOWS_DEBUGGING.md
@@ -76,9 +78,23 @@ nav:
- Settings: SETTINGS_SYSTEM.md
- Versions: VERSIONS.md
- Icon and Type guessing: DEVICE_HEURISTICS.md
- API:
- Overview: API.md
- Devices Collection: API_DEVICES.md
- Device: API_DEVICE.md
- Sessions: API_SESSIONS.md
- Settings: API_SETTINGS.md
- Events: API_EVENTS.md
- Metrics: API_METRICS.md
- Net Tools: API_NETTOOLS.md
- Online History: API_ONLINEHISTORY.md
- Sync: API_SYNC.md
- GraphQL: API_GRAPHQL.md
- DB query: API_DBQUERY.md
- Tests: API_TESTS.md
- SUPERSEDED OLD API Overview: API_OLD.md
- Integrations:
- Webhook Secret: WEBHOOK_SECRET.md
- API: API.md
- Webhook Secret: WEBHOOK_SECRET.md
- Helper scripts: HELPER_SCRIPTS.md

View File

@@ -20,6 +20,7 @@ import time
import datetime
import multiprocessing
import subprocess
from pathlib import Path
# Register NetAlertX modules
import conf
@@ -29,7 +30,7 @@ from helper import filePermissions, timeNowTZ, get_setting_value
from app_state import updateState
from api import update_api
from scan.session_events import process_scan
from initialise import importConfigs
from initialise import importConfigs, renameSettings
from database import DB
from messaging.reporting import get_notifications
from models.notification_instance import NotificationInstance
@@ -44,9 +45,10 @@ from workflows.manager import WorkflowManager
#===============================================================================
#===============================================================================
"""
main structure of Pi Alert
main structure of NetAlertX
Initialise All
Rename old settings
start Loop forever
initialise loop
(re)import config
@@ -91,14 +93,17 @@ def main ():
mylog('debug', '[MAIN] Starting loop')
all_plugins = None
pm = None
# -- SETTINGS BACKWARD COMPATIBILITY START --
# rename settings that have changed names due to code cleanup or migration to plugins
renameSettings(Path(fullConfPath))
# -- SETTINGS BACKWARD COMPATIBILITY END --
while True:
# re-load user configuration and plugins
all_plugins, imported = importConfigs(db, all_plugins)
# initiate plugin manager
pm = plugin_manager(db, all_plugins)
pm, all_plugins, imported = importConfigs(pm, db, all_plugins)
# update time started
conf.loop_start_time = timeNowTZ()

View File

@@ -1,9 +1,16 @@
import threading
from flask import Flask, request, jsonify, Response
from flask_cors import CORS
from .graphql_schema import devicesSchema
from .prometheus_metrics import getMetricStats
from graphene import Schema
from .graphql_endpoint import devicesSchema
from .device_endpoint import get_device_data, set_device_data, delete_device, delete_device_events, reset_device_props, copy_device, update_device_column
from .devices_endpoint import get_all_devices, delete_unknown_devices, delete_all_with_empty_macs, delete_devices, export_devices, import_csv, devices_totals, devices_by_status
from .events_endpoint import delete_events, delete_events_older_than, get_events, create_event, get_events_totals
from .history_endpoint import delete_online_history
from .prometheus_endpoint import get_metric_stats
from .sessions_endpoint import get_sessions, delete_session, create_session, get_sessions_calendar, get_device_sessions, get_session_events
from .nettools_endpoint import wakeonlan, traceroute, speedtest, nslookup, nmap_scan, internet_info
from .dbquery_endpoint import read_query, write_query, update_query, delete_query
from .sync_endpoint import handle_sync_post, handle_sync_get
import sys
# Register NetAlertX directories
@@ -12,12 +19,28 @@ sys.path.extend([f"{INSTALL_PATH}/server"])
from logger import mylog
from helper import get_setting_value, timeNowTZ
from db.db_helper import get_date_from_period
from app_state import updateState
from messaging.in_app import write_notification
# Flask application
app = Flask(__name__)
CORS(app, resources={r"/metrics": {"origins": "*"}}, supports_credentials=True, allow_headers=["Authorization"])
CORS(
app,
resources={
r"/metrics": {"origins": "*"},
r"/device/*": {"origins": "*"},
r"/devices/*": {"origins": "*"},
r"/history/*": {"origins": "*"},
r"/nettools/*": {"origins": "*"},
r"/sessions/*": {"origins": "*"},
r"/settings/*": {"origins": "*"},
r"/dbquery/*": {"origins": "*"},
r"/events/*": {"origins": "*"}
},
supports_credentials=True,
allow_headers=["Authorization", "Content-Type"]
)
# --------------------------
# GraphQL Endpoints
@@ -45,33 +68,470 @@ def graphql_endpoint():
# Execute the GraphQL query
result = devicesSchema.execute(data.get("query"), variables=data.get("variables"))
# Return the result as JSON
return jsonify(result.data)
# Initialize response
response = {}
if result.errors:
response["errors"] = [str(e) for e in result.errors]
if result.data:
response["data"] = result.data
return jsonify(response)
# --------------------------
# Prometheus /metrics Endpoint
# Settings Endpoints
# --------------------------
@app.route("/settings/<setKey>", methods=["GET"])
def api_get_setting(setKey):
if not is_authorized():
return jsonify({"error": "Forbidden"}), 403
value = get_setting_value(setKey)
return jsonify({"success": True, "value": value})
# --------------------------
# Device Endpoints
# --------------------------
@app.route("/device/<mac>", methods=["GET"])
def api_get_device(mac):
if not is_authorized():
return jsonify({"error": "Forbidden"}), 403
return get_device_data(mac)
@app.route("/device/<mac>", methods=["POST"])
def api_set_device(mac):
if not is_authorized():
return jsonify({"error": "Forbidden"}), 403
return set_device_data(mac, request.json)
@app.route("/device/<mac>/delete", methods=["DELETE"])
def api_delete_device(mac):
if not is_authorized():
return jsonify({"error": "Forbidden"}), 403
return delete_device(mac)
@app.route("/device/<mac>/events/delete", methods=["DELETE"])
def api_delete_device_events(mac):
if not is_authorized():
return jsonify({"error": "Forbidden"}), 403
return delete_device_events(mac)
@app.route("/device/<mac>/reset-props", methods=["POST"])
def api_reset_device_props(mac):
if not is_authorized():
return jsonify({"error": "Forbidden"}), 403
return reset_device_props(mac, request.json)
@app.route("/device/copy", methods=["POST"])
def api_copy_device():
if not is_authorized():
return jsonify({"error": "Forbidden"}), 403
data = request.get_json() or {}
mac_from = data.get("macFrom")
mac_to = data.get("macTo")
if not mac_from or not mac_to:
return jsonify({"success": False, "error": "macFrom and macTo are required"}), 400
return copy_device(mac_from, mac_to)
@app.route("/device/<mac>/update-column", methods=["POST"])
def api_update_device_column(mac):
if not is_authorized():
return jsonify({"error": "Forbidden"}), 403
data = request.get_json() or {}
column_name = data.get("columnName")
column_value = data.get("columnValue")
if not column_name or not column_value:
return jsonify({"success": False, "error": "columnName and columnValue are required"}), 400
return update_device_column(mac, column_name, column_value)
# --------------------------
# Devices Collections
# --------------------------
@app.route("/devices", methods=["GET"])
def api_get_devices():
if not is_authorized():
return jsonify({"error": "Forbidden"}), 403
return get_all_devices()
@app.route("/devices", methods=["DELETE"])
def api_delete_devices():
if not is_authorized():
return jsonify({"error": "Forbidden"}), 403
macs = request.json.get("macs") if request.is_json else None
return delete_devices(macs)
@app.route("/devices/empty-macs", methods=["DELETE"])
def api_delete_all_empty_macs():
if not is_authorized():
return jsonify({"error": "Forbidden"}), 403
return delete_all_with_empty_macs()
@app.route("/devices/unknown", methods=["DELETE"])
def api_delete_unknown_devices():
if not is_authorized():
return jsonify({"error": "Forbidden"}), 403
return delete_unknown_devices()
@app.route("/devices/export", methods=["GET"])
@app.route("/devices/export/<format>", methods=["GET"])
def api_export_devices(format=None):
if not is_authorized():
return jsonify({"error": "Forbidden"}), 403
export_format = (format or request.args.get("format", "csv")).lower()
return export_devices(export_format)
@app.route("/devices/import", methods=["POST"])
def api_import_csv():
if not is_authorized():
return jsonify({"error": "Forbidden"}), 403
return import_csv(request.files.get("file"))
@app.route("/devices/totals", methods=["GET"])
def api_devices_totals():
if not is_authorized():
return jsonify({"error": "Forbidden"}), 403
return devices_totals()
@app.route("/devices/by-status", methods=["GET"])
def api_devices_by_status():
if not is_authorized():
return jsonify({"error": "Forbidden"}), 403
status = request.args.get("status", "") if request.args else None
return devices_by_status(status)
# --------------------------
# Net tools
# --------------------------
@app.route("/nettools/wakeonlan", methods=["POST"])
def api_wakeonlan():
if not is_authorized():
return jsonify({"error": "Forbidden"}), 403
mac = request.json.get("devMac")
return wakeonlan(mac)
@app.route("/nettools/traceroute", methods=["POST"])
def api_traceroute():
if not is_authorized():
return jsonify({"error": "Forbidden"}), 403
ip = request.json.get("devLastIP")
return traceroute(ip)
@app.route("/nettools/speedtest", methods=["GET"])
def api_speedtest():
if not is_authorized():
return jsonify({"error": "Forbidden"}), 403
return speedtest()
@app.route("/nettools/nslookup", methods=["POST"])
def api_nslookup():
"""
API endpoint to handle nslookup requests.
Expects JSON with 'devLastIP'.
"""
if not is_authorized():
return jsonify({"success": False, "error": "Forbidden"}), 403
data = request.get_json(silent=True)
if not data or "devLastIP" not in data:
return jsonify({"success": False, "error": "Missing 'devLastIP'"}), 400
ip = data["devLastIP"]
return nslookup(ip)
@app.route("/nettools/nmap", methods=["POST"])
def api_nmap():
"""
API endpoint to handle nmap scan requests.
Expects JSON with 'scan' (IP address) and 'mode' (scan mode).
"""
if not is_authorized():
return jsonify({"success": False, "error": "Forbidden"}), 403
data = request.get_json(silent=True)
if not data or "scan" not in data or "mode" not in data:
return jsonify({"success": False, "error": "Missing 'scan' or 'mode'"}), 400
ip = data["scan"]
mode = data["mode"]
return nmap_scan(ip, mode)
@app.route("/nettools/internetinfo", methods=["GET"])
def api_internet_info():
if not is_authorized():
return jsonify({"success": False, "error": "Forbidden"}), 403
return internet_info()
# --------------------------
# DB query
# --------------------------
@app.route("/dbquery/read", methods=["POST"])
def dbquery_read():
if not is_authorized():
return jsonify({"error": "Forbidden"}), 403
data = request.get_json() or {}
raw_sql_b64 = data.get("rawSql")
if not raw_sql_b64:
return jsonify({"error": "rawSql is required"}), 400
return read_query(raw_sql_b64)
@app.route("/dbquery/write", methods=["POST"])
def dbquery_write():
if not is_authorized():
return jsonify({"error": "Forbidden"}), 403
data = request.get_json() or {}
raw_sql_b64 = data.get("rawSql")
if not raw_sql_b64:
return jsonify({"error": "rawSql is required"}), 400
return write_query(raw_sql_b64)
@app.route("/dbquery/update", methods=["POST"])
def dbquery_update():
if not is_authorized():
return jsonify({"error": "Forbidden"}), 403
data = request.get_json() or {}
required = ["columnName", "id", "dbtable", "columns", "values"]
if not all(data.get(k) for k in required):
return jsonify({"error": "Missing required parameters"}), 400
return update_query(
column_name=data["columnName"],
ids=data["id"],
dbtable=data["dbtable"],
columns=data["columns"],
values=data["values"],
)
@app.route("/dbquery/delete", methods=["POST"])
def dbquery_delete():
if not is_authorized():
return jsonify({"error": "Forbidden"}), 403
data = request.get_json() or {}
required = ["columnName", "id", "dbtable"]
if not all(data.get(k) for k in required):
return jsonify({"error": "Missing required parameters"}), 400
return delete_query(
column_name=data["columnName"],
ids=data["id"],
dbtable=data["dbtable"],
)
# --------------------------
# Online history
# --------------------------
@app.route("/history", methods=["DELETE"])
def api_delete_online_history():
if not is_authorized():
return jsonify({"error": "Forbidden"}), 403
return delete_online_history()
# --------------------------
# Device Events
# --------------------------
@app.route("/events/create/<mac>", methods=["POST"])
def api_create_event(mac):
if not is_authorized():
return jsonify({"error": "Forbidden"}), 403
data = request.json or {}
ip = data.get("ip", "0.0.0.0")
event_type = data.get("event_type", "Device Down")
additional_info = data.get("additional_info", "")
pending_alert = data.get("pending_alert", 1)
event_time = data.get("event_time", None)
# Call the helper to insert into DB
create_event(mac, ip, event_type, additional_info, pending_alert, event_time)
# Return consistent JSON response
return jsonify({"success": True, "message": f"Event created for {mac}"})
@app.route("/events/<mac>", methods=["DELETE"])
def api_events_by_mac(mac):
if not is_authorized():
return jsonify({"error": "Forbidden"}), 403
return delete_device_events(mac)
@app.route("/events", methods=["DELETE"])
def api_delete_all_events():
if not is_authorized():
return jsonify({"error": "Forbidden"}), 403
return delete_events()
@app.route("/events", methods=["GET"])
def api_get_events():
if not is_authorized():
return jsonify({"error": "Forbidden"}), 403
mac = request.args.get("mac")
return get_events(mac)
@app.route("/events/<int:days>", methods=["DELETE"])
def api_delete_old_events(days: int):
"""
Delete events older than <days> days.
Example: DELETE /events/30
"""
if not is_authorized():
return jsonify({"error": "Forbidden"}), 403
return delete_events_older_than(days)
@app.route("/sessions/totals", methods=["GET"])
def api_get_events_totals():
if not is_authorized():
return jsonify({"error": "Forbidden"}), 403
period = get_date_from_period(request.args.get("period", "7 days"))
return get_events_totals(period)
# --------------------------
# Sessions
# --------------------------
@app.route("/sessions/create", methods=["POST"])
def api_create_session():
if not is_authorized():
return jsonify({"error": "Forbidden"}), 403
data = request.json
mac = data.get("mac")
ip = data.get("ip")
start_time = data.get("start_time")
end_time = data.get("end_time")
event_type_conn = data.get("event_type_conn", "Connected")
event_type_disc = data.get("event_type_disc", "Disconnected")
if not mac or not ip or not start_time:
return jsonify({"success": False, "error": "Missing required parameters"}), 400
return create_session(mac, ip, start_time, end_time, event_type_conn, event_type_disc)
@app.route("/sessions/delete", methods=["DELETE"])
def api_delete_session():
if not is_authorized():
return jsonify({"error": "Forbidden"}), 403
mac = request.json.get("mac") if request.is_json else None
if not mac:
return jsonify({"success": False, "error": "Missing MAC parameter"}), 400
return delete_session(mac)
@app.route("/sessions/list", methods=["GET"])
def api_get_sessions():
if not is_authorized():
return jsonify({"error": "Forbidden"}), 403
mac = request.args.get("mac")
start_date = request.args.get("start_date")
end_date = request.args.get("end_date")
return get_sessions(mac, start_date, end_date)
@app.route("/sessions/calendar", methods=["GET"])
def api_get_sessions_calendar():
if not is_authorized():
return jsonify({"error": "Forbidden"}), 403
# Query params: /sessions/calendar?start=2025-08-01&end=2025-08-21
start_date = request.args.get("start")
end_date = request.args.get("end")
return get_sessions_calendar(start_date, end_date)
@app.route("/sessions/<mac>", methods=["GET"])
def api_device_sessions(mac):
if not is_authorized():
return jsonify({"error": "Forbidden"}), 403
period = request.args.get("period", "1 day")
return get_device_sessions(mac, period)
@app.route("/sessions/session-events", methods=["GET"])
def api_get_session_events():
if not is_authorized():
return jsonify({"error": "Forbidden"}), 403
session_event_type = request.args.get("type", "all")
period = get_date_from_period(request.args.get("period", "7 days"))
return get_session_events(session_event_type, period)
# --------------------------
# Prometheus metrics endpoint
# --------------------------
@app.route("/metrics")
def metrics():
# Check for API token in headers
if not is_authorized():
msg = '[metrics] Unauthorized access attempt - make sure your GRAPHQL_PORT and API_TOKEN settings are correct.'
mylog('verbose', [msg])
return jsonify({"error": msg}), 401
return jsonify({"error": "Forbidden"}), 403
# Return Prometheus metrics as plain text
return Response(getMetricStats(), mimetype="text/plain")
return Response(get_metric_stats(), mimetype="text/plain")
# --------------------------
# SYNC endpoint
# --------------------------
@app.route("/sync", methods=["GET", "POST"])
def sync_endpoint():
if not is_authorized():
return jsonify({"error": "Forbidden"}), 403
if request.method == "GET":
return handle_sync_get()
elif request.method == "POST":
return handle_sync_post()
else:
msg = "[sync endpoint] Method Not Allowed"
write_notification(msg, "alert")
mylog("verbose", [msg])
return jsonify({"error": "Method Not Allowed"}), 405
# --------------------------
# Background Server Start
# --------------------------
def is_authorized():
token = request.headers.get("Authorization")
return token == f"Bearer {get_setting_value('API_TOKEN')}"
is_authorized = token == f"Bearer {get_setting_value('API_TOKEN')}"
if not is_authorized:
msg = f"[api] Unauthorized access attempt - make sure your GRAPHQL_PORT and API_TOKEN settings are correct."
write_notification(msg, "alert")
mylog("verbose", [msg])
return is_authorized
def start_server(graphql_port, app_state):
@@ -79,7 +539,7 @@ def start_server(graphql_port, app_state):
if app_state.graphQLServerStarted == 0:
mylog('verbose', [f'[graphql_server] Starting on port: {graphql_port}'])
mylog('verbose', [f'[graphql endpoint] Starting on port: {graphql_port}'])
# Start Flask app in a separate thread
thread = threading.Thread(

View File

@@ -0,0 +1,103 @@
#!/usr/bin/env python
import json
import argparse
import os
import pathlib
import base64
import re
import sys
from datetime import datetime
from flask import jsonify, request, Response
import csv
import io
from io import StringIO
# Register NetAlertX directories
INSTALL_PATH="/app"
sys.path.extend([f"{INSTALL_PATH}/front/plugins", f"{INSTALL_PATH}/server"])
from database import get_temp_db_connection
def read_query(raw_sql_b64):
"""Execute a read-only query (SELECT)."""
try:
raw_sql = base64.b64decode(raw_sql_b64).decode("utf-8")
conn = get_temp_db_connection()
cur = conn.cursor()
cur.execute(raw_sql)
rows = cur.fetchall()
# Convert rows → dict list
columns = [col[0] for col in cur.description] if cur.description else []
results = [dict(zip(columns, row)) for row in rows]
conn.close()
return jsonify({"success": True, "results": results})
except Exception as e:
return jsonify({"success": False, "error": str(e)}), 400
def write_query(raw_sql_b64):
"""Execute a write query (INSERT/UPDATE/DELETE)."""
try:
raw_sql = base64.b64decode(raw_sql_b64).decode("utf-8")
conn = get_temp_db_connection()
cur = conn.cursor()
cur.execute(raw_sql)
conn.commit()
affected = cur.rowcount
conn.close()
return jsonify({"success": True, "affected_rows": affected})
except Exception as e:
return jsonify({"success": False, "error": str(e)}), 400
def update_query(column_name, ids, dbtable, columns, values):
"""Update rows in dbtable based on column_name + ids."""
try:
conn = get_temp_db_connection()
cur = conn.cursor()
if not isinstance(ids, list):
ids = [ids]
updated_count = 0
for i in range(len(ids)):
set_clause = ", ".join([f"{col} = ?" for col in columns])
sql = f"UPDATE {dbtable} SET {set_clause} WHERE {column_name} = ?"
params = list(values) + [ids[i]]
cur.execute(sql, params)
updated_count += cur.rowcount
conn.commit()
conn.close()
return jsonify({"success": True, "updated_count": updated_count})
except Exception as e:
return jsonify({"success": False, "error": str(e)}), 400
def delete_query(column_name, ids, dbtable):
"""Delete rows in dbtable based on column_name + ids."""
try:
conn = get_temp_db_connection()
cur = conn.cursor()
if not isinstance(ids, list):
ids = [ids]
deleted_count = 0
for id_val in ids:
sql = f"DELETE FROM {dbtable} WHERE {column_name} = ?"
cur.execute(sql, (id_val,))
deleted_count += cur.rowcount
conn.commit()
conn.close()
return jsonify({"success": True, "deleted_count": deleted_count})
except Exception as e:
return jsonify({"success": False, "error": str(e)}), 400

View File

@@ -0,0 +1,336 @@
#!/usr/bin/env python
import json
import subprocess
import argparse
import os
import pathlib
import sys
from datetime import datetime
from flask import jsonify, request
# Register NetAlertX directories
INSTALL_PATH="/app"
sys.path.extend([f"{INSTALL_PATH}/front/plugins", f"{INSTALL_PATH}/server"])
from database import get_temp_db_connection
from helper import is_random_mac, format_date, get_setting_value
from db.db_helper import row_to_json, get_date_from_period
# --------------------------
# Device Endpoints Functions
# --------------------------
def get_device_data(mac):
"""Fetch device info with children, event stats, and presence calculation."""
# Open temporary connection for this request
conn = get_temp_db_connection()
cur = conn.cursor()
# Special case for new device
if mac.lower() == "new":
now = datetime.now().strftime("%Y-%m-%d %H:%M")
device_data = {
"devMac": "",
"devName": "",
"devOwner": "",
"devType": "",
"devVendor": "",
"devFavorite": 0,
"devGroup": "",
"devComments": "",
"devFirstConnection": now,
"devLastConnection": now,
"devLastIP": "",
"devStaticIP": 0,
"devScan": 0,
"devLogEvents": 0,
"devAlertEvents": 0,
"devAlertDown": 0,
"devParentRelType": "default",
"devReqNicsOnline": 0,
"devSkipRepeated": 0,
"devLastNotification": "",
"devPresentLastScan": 0,
"devIsNew": 1,
"devLocation": "",
"devIsArchived": 0,
"devParentMAC": "",
"devParentPort": "",
"devIcon": "",
"devGUID": "",
"devSite": "",
"devSSID": "",
"devSyncHubNode": "",
"devSourcePlugin": "",
"devCustomProps": "",
"devStatus": "Unknown",
"devIsRandomMAC": False,
"devSessions": 0,
"devEvents": 0,
"devDownAlerts": 0,
"devPresenceHours": 0,
"devFQDN": ""
}
return jsonify(device_data)
# Compute period date for sessions/events
period = request.args.get('period', '') # e.g., '7 days', '1 month', etc.
period_date_sql = get_date_from_period(period)
current_date = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
# Fetch device info + computed fields
sql = f"""
SELECT
d.*,
CASE
WHEN d.devAlertDown != 0 AND d.devPresentLastScan = 0 THEN 'Down'
WHEN d.devPresentLastScan = 1 THEN 'On-line'
ELSE 'Off-line'
END AS devStatus,
(SELECT COUNT(*) FROM Sessions
WHERE ses_MAC = d.devMac AND (
ses_DateTimeConnection >= {period_date_sql} OR
ses_DateTimeDisconnection >= {period_date_sql} OR
ses_StillConnected = 1
)) AS devSessions,
(SELECT COUNT(*) FROM Events
WHERE eve_MAC = d.devMac AND eve_DateTime >= {period_date_sql}
AND eve_EventType NOT IN ('Connected','Disconnected')) AS devEvents,
(SELECT COUNT(*) FROM Events
WHERE eve_MAC = d.devMac AND eve_DateTime >= {period_date_sql}
AND eve_EventType = 'Device Down') AS devDownAlerts,
(SELECT CAST(MAX(0, SUM(
julianday(IFNULL(ses_DateTimeDisconnection,'{current_date}')) -
julianday(CASE WHEN ses_DateTimeConnection < {period_date_sql}
THEN {period_date_sql} ELSE ses_DateTimeConnection END)
) * 24) AS INT)
FROM Sessions
WHERE ses_MAC = d.devMac
AND ses_DateTimeConnection IS NOT NULL
AND (ses_DateTimeDisconnection IS NOT NULL OR ses_StillConnected = 1)
AND (ses_DateTimeConnection >= {period_date_sql}
OR ses_DateTimeDisconnection >= {period_date_sql} OR ses_StillConnected = 1)
) AS devPresenceHours
FROM Devices d
WHERE d.devMac = ? OR CAST(d.rowid AS TEXT) = ?
"""
# Fetch device
cur.execute(sql, (mac, mac))
row = cur.fetchone()
if not row:
return jsonify({"error": "Device not found"}), 404
device_data = row_to_json(list(row.keys()), row)
device_data['devFirstConnection'] = format_date(device_data['devFirstConnection'])
device_data['devLastConnection'] = format_date(device_data['devLastConnection'])
device_data['devIsRandomMAC'] = is_random_mac(device_data['devMac'])
# Fetch children
cur.execute("SELECT * FROM Devices WHERE devParentMAC = ? ORDER BY devPresentLastScan DESC", ( device_data['devMac'],))
children_rows = cur.fetchall()
children = [row_to_json(list(r.keys()), r) for r in children_rows]
children_nics = [c for c in children if c.get("devParentRelType") == "nic"]
device_data['devChildrenDynamic'] = children
device_data['devChildrenNicsDynamic'] = children_nics
conn.close()
return jsonify(device_data)
def set_device_data(mac, data):
"""Update or create a device."""
if data.get("createNew", False):
sql = """
INSERT INTO Devices (
devMac, devName, devOwner, devType, devVendor, devIcon,
devFavorite, devGroup, devLocation, devComments,
devParentMAC, devParentPort, devSSID, devSite,
devStaticIP, devScan, devAlertEvents, devAlertDown,
devParentRelType, devReqNicsOnline, devSkipRepeated,
devIsNew, devIsArchived, devLastConnection,
devFirstConnection, devLastIP, devGUID, devCustomProps,
devSourcePlugin
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
"""
values = (
mac,
data.get("devName", ""),
data.get("devOwner", ""),
data.get("devType", ""),
data.get("devVendor", ""),
data.get("devIcon", ""),
data.get("devFavorite", 0),
data.get("devGroup", ""),
data.get("devLocation", ""),
data.get("devComments", ""),
data.get("devParentMAC", ""),
data.get("devParentPort", ""),
data.get("devSSID", ""),
data.get("devSite", ""),
data.get("devStaticIP", 0),
data.get("devScan", 0),
data.get("devAlertEvents", 0),
data.get("devAlertDown", 0),
data.get("devParentRelType", "default"),
data.get("devReqNicsOnline", 0),
data.get("devSkipRepeated", 0),
data.get("devIsNew", 0),
data.get("devIsArchived", 0),
data.get("devLastConnection", datetime.now().strftime("%Y-%m-%d %H:%M:%S")),
data.get("devFirstConnection", datetime.now().strftime("%Y-%m-%d %H:%M:%S")),
data.get("devLastIP", ""),
data.get("devGUID", ""),
data.get("devCustomProps", ""),
data.get("devSourcePlugin", "DUMMY"),
)
else:
sql = """
UPDATE Devices SET
devName=?, devOwner=?, devType=?, devVendor=?, devIcon=?,
devFavorite=?, devGroup=?, devLocation=?, devComments=?,
devParentMAC=?, devParentPort=?, devSSID=?, devSite=?,
devStaticIP=?, devScan=?, devAlertEvents=?, devAlertDown=?,
devParentRelType=?, devReqNicsOnline=?, devSkipRepeated=?,
devIsNew=?, devIsArchived=?, devCustomProps=?
WHERE devMac=?
"""
values = (
data.get("devName", ""),
data.get("devOwner", ""),
data.get("devType", ""),
data.get("devVendor", ""),
data.get("devIcon", ""),
data.get("devFavorite", 0),
data.get("devGroup", ""),
data.get("devLocation", ""),
data.get("devComments", ""),
data.get("devParentMAC", ""),
data.get("devParentPort", ""),
data.get("devSSID", ""),
data.get("devSite", ""),
data.get("devStaticIP", 0),
data.get("devScan", 0),
data.get("devAlertEvents", 0),
data.get("devAlertDown", 0),
data.get("devParentRelType", "default"),
data.get("devReqNicsOnline", 0),
data.get("devSkipRepeated", 0),
data.get("devIsNew", 0),
data.get("devIsArchived", 0),
data.get("devCustomProps", ""),
mac
)
conn = get_temp_db_connection()
cur = conn.cursor()
cur.execute(sql, values)
conn.commit()
conn.close()
return jsonify({"success": True})
def delete_device(mac):
"""Delete a device by MAC."""
conn = get_temp_db_connection()
cur = conn.cursor()
cur.execute("DELETE FROM Devices WHERE devMac=?", (mac,))
conn.commit()
conn.close()
return jsonify({"success": True})
def delete_device_events(mac):
"""Delete all events for a device."""
conn = get_temp_db_connection()
cur = conn.cursor()
cur.execute("DELETE FROM Events WHERE eve_MAC=?", (mac,))
conn.commit()
conn.close()
return jsonify({"success": True})
def reset_device_props(mac, data=None):
"""Reset device custom properties to default."""
default_props = get_setting_value("NEWDEV_devCustomProps")
conn = get_temp_db_connection()
cur = conn.cursor()
cur.execute(
"UPDATE Devices SET devCustomProps=? WHERE devMac=?",
(default_props, mac),
)
conn.commit()
conn.close()
return jsonify({"success": True})
def update_device_column(mac, column_name, column_value):
"""
Update a specific column for a given device.
Example: update_device_column("AA:BB:CC:DD:EE:FF", "devParentMAC", "Internet")
"""
conn = get_temp_db_connection()
cur = conn.cursor()
# Build safe SQL with column name whitelisted
sql = f"UPDATE Devices SET {column_name}=? WHERE devMac=?"
cur.execute(sql, (column_value, mac))
conn.commit()
if cur.rowcount > 0:
return jsonify({"success": True})
else:
return jsonify({"success": False, "error": "Device not found"}), 404
conn.close()
return jsonify({"success": True})
def copy_device(mac_from, mac_to):
"""
Copy a device entry from one MAC to another.
If a device already exists with mac_to, it will be replaced.
"""
conn = get_temp_db_connection()
cur = conn.cursor()
try:
# Drop temporary table if exists
cur.execute("DROP TABLE IF EXISTS temp_devices")
# Create temporary table with source device
cur.execute("CREATE TABLE temp_devices AS SELECT * FROM Devices WHERE devMac = ?", (mac_from,))
# Update temporary table to target MAC
cur.execute("UPDATE temp_devices SET devMac = ?", (mac_to,))
# Delete previous entry with target MAC
cur.execute("DELETE FROM Devices WHERE devMac = ?", (mac_to,))
# Insert new entry from temporary table
cur.execute("INSERT INTO Devices SELECT * FROM temp_devices WHERE devMac = ?", (mac_to,))
# Drop temporary table
cur.execute("DROP TABLE temp_devices")
conn.commit()
return jsonify({"success": True, "message": f"Device copied from {mac_from} to {mac_to}"})
except Exception as e:
conn.rollback()
return jsonify({"success": False, "error": str(e)})
finally:
conn.close()

View File

@@ -0,0 +1,263 @@
#!/usr/bin/env python
import json
import subprocess
import argparse
import os
import pathlib
import base64
import re
import sys
from datetime import datetime
from flask import jsonify, request, Response
import csv
import io
from io import StringIO
# Register NetAlertX directories
INSTALL_PATH="/app"
sys.path.extend([f"{INSTALL_PATH}/front/plugins", f"{INSTALL_PATH}/server"])
from database import get_temp_db_connection
from helper import is_random_mac, format_date, get_setting_value
from db.db_helper import get_table_json, get_device_condition_by_status
# --------------------------
# Device Endpoints Functions
# --------------------------
def get_all_devices():
"""Retrieve all devices from the database."""
conn = get_temp_db_connection()
cur = conn.cursor()
cur.execute("SELECT * FROM Devices")
rows = cur.fetchall()
# Convert rows to list of dicts using column names
columns = [col[0] for col in cur.description]
devices = [dict(zip(columns, row)) for row in rows]
conn.close()
return jsonify({"success": True, "devices": devices})
def delete_devices(macs):
"""
Delete devices from the Devices table.
- If `macs` is None → delete ALL devices.
- If `macs` is a list → delete only matching MACs (supports wildcard '*').
"""
conn = get_temp_db_connection()
cur = conn.cursor()
if not macs:
# No MACs provided → delete all
cur.execute("DELETE FROM Devices")
conn.commit()
conn.close()
return jsonify({"success": True, "deleted": "all"})
deleted_count = 0
for mac in macs:
if "*" in mac:
# Wildcard matching
sql_pattern = mac.replace("*", "%")
cur.execute("DELETE FROM Devices WHERE devMAC LIKE ?", (sql_pattern,))
else:
# Exact match
cur.execute("DELETE FROM Devices WHERE devMAC = ?", (mac,))
deleted_count += cur.rowcount
conn.commit()
conn.close()
return jsonify({"success": True, "deleted_count": deleted_count})
def delete_all_with_empty_macs():
"""Delete devices with empty MAC addresses."""
conn = get_temp_db_connection()
cur = conn.cursor()
cur.execute("DELETE FROM Devices WHERE devMAC IS NULL OR devMAC = ''")
deleted = cur.rowcount
conn.commit()
conn.close()
return jsonify({"success": True, "deleted": deleted})
def delete_unknown_devices():
"""Delete devices marked as unknown."""
conn = get_temp_db_connection()
cur = conn.cursor()
cur.execute("""DELETE FROM Devices WHERE devName='(unknown)' OR devName='(name not found)'""")
conn.commit()
conn.close()
return jsonify({"success": True, "deleted": cur.rowcount})
def export_devices(export_format):
"""
Export devices from the Devices table in teh desired format.
- If `macs` is None → delete ALL devices.
- If `macs` is a list → delete only matching MACs (supports wildcard '*').
"""
conn = get_temp_db_connection()
cur = conn.cursor()
# Fetch all devices
devices_json = get_table_json(cur, "SELECT * FROM Devices")
conn.close()
# Ensure columns exist
columns = devices_json.columnNames or (
list(devices_json["data"][0].keys()) if devices_json["data"] else []
)
if export_format == "json":
# Convert to standard dict for Flask JSON
return jsonify({
"data": [row for row in devices_json["data"]],
"columns": list(columns)
})
elif export_format == "csv":
si = StringIO()
writer = csv.DictWriter(si, fieldnames=columns, quoting=csv.QUOTE_ALL)
writer.writeheader()
for row in devices_json.json["data"]:
writer.writerow(row)
return Response(
si.getvalue(),
mimetype="text/csv",
headers={"Content-Disposition": "attachment; filename=devices.csv"},
)
else:
return jsonify({"error": f"Unsupported format '{export_format}'"}), 400
def import_csv(file_storage=None):
data = ""
skipped = []
error = None
# 1. Try JSON `content` (base64-encoded CSV)
if request.is_json and request.json.get("content"):
try:
data = base64.b64decode(request.json["content"], validate=True).decode("utf-8")
except Exception as e:
return jsonify({"error": f"Base64 decode failed: {e}"}), 400
# 2. Otherwise, try uploaded file
elif file_storage:
data = file_storage.read().decode("utf-8")
# 3. Fallback: try local file (same as PHP `$file = '../../../config/devices.csv';`)
else:
local_file = "/app/config/devices.csv"
try:
with open(local_file, "r", encoding="utf-8") as f:
data = f.read()
except FileNotFoundError:
return jsonify({"error": "CSV file missing"}), 404
if not data:
return jsonify({"error": "No CSV data found"}), 400
# --- Clean up newlines inside quoted fields ---
data = re.sub(
r'"([^"]*)"',
lambda m: m.group(0).replace("\n", " "),
data
)
# --- Parse CSV ---
lines = data.splitlines()
reader = csv.reader(lines)
try:
header = [h.strip() for h in next(reader)]
except StopIteration:
return jsonify({"error": "CSV missing header"}), 400
# --- Wipe Devices table ---
conn = get_temp_db_connection()
sql = conn.cursor()
sql.execute("DELETE FROM Devices")
# --- Prepare insert ---
placeholders = ",".join(["?"] * len(header))
insert_sql = f"INSERT INTO Devices ({', '.join(header)}) VALUES ({placeholders})"
row_count = 0
for idx, row in enumerate(reader, start=1):
if len(row) != len(header):
skipped.append(idx)
continue
try:
sql.execute(insert_sql, [col.strip() for col in row])
row_count += 1
except sqlite3.Error as e:
mylog("error", [f"[ImportCSV] SQL ERROR row {idx}: {e}"])
skipped.append(idx)
conn.commit()
conn.close()
return jsonify({
"success": True,
"inserted": row_count,
"skipped_lines": skipped
})
def devices_totals():
conn = get_temp_db_connection()
sql = conn.cursor()
# Build a combined query with sub-selects for each status
query = f"""
SELECT
(SELECT COUNT(*) FROM Devices {get_device_condition_by_status('my')}) AS devices,
(SELECT COUNT(*) FROM Devices {get_device_condition_by_status('connected')}) AS connected,
(SELECT COUNT(*) FROM Devices {get_device_condition_by_status('favorites')}) AS favorites,
(SELECT COUNT(*) FROM Devices {get_device_condition_by_status('new')}) AS new,
(SELECT COUNT(*) FROM Devices {get_device_condition_by_status('down')}) AS down,
(SELECT COUNT(*) FROM Devices {get_device_condition_by_status('archived')}) AS archived
"""
sql.execute(query)
row = sql.fetchone() # returns a tuple like (devices, connected, favorites, new, down, archived)
conn.close()
# Return counts as JSON array
return jsonify(list(row))
def devices_by_status(status=None):
"""
Return devices filtered by status.
"""
conn = get_temp_db_connection()
sql = conn.cursor()
# Build condition for SQL
condition = get_device_condition_by_status(status) if status else ""
query = f"SELECT * FROM Devices {condition}"
sql.execute(query)
table_data = []
for row in sql.fetchall():
r = dict(row) # Convert sqlite3.Row to dict for .get()
dev_name = r.get("devName", "")
if r.get("devFavorite") == 1:
dev_name = f'<span class="text-yellow">&#9733</span>&nbsp;{dev_name}'
table_data.append({
"id": r.get("devMac", ""),
"title": dev_name,
"favorite": r.get("devFavorite", 0)
})
conn.close()
return jsonify(table_data)

View File

@@ -0,0 +1,146 @@
#!/usr/bin/env python
import json
import subprocess
import argparse
import os
import pathlib
import sys
from datetime import datetime
from flask import jsonify, request
# Register NetAlertX directories
INSTALL_PATH="/app"
sys.path.extend([f"{INSTALL_PATH}/front/plugins", f"{INSTALL_PATH}/server"])
from database import get_temp_db_connection
from helper import is_random_mac, format_date, get_setting_value, format_date_iso, format_event_date, timeNowTZ, mylog, ensure_datetime
from db.db_helper import row_to_json, get_date_from_period
# --------------------------
# Events Endpoints Functions
# --------------------------
def create_event(
mac: str,
ip: str,
event_type: str = "Device Down",
additional_info: str = "",
pending_alert: int = 1,
event_time: datetime | None = None
):
"""
Insert a single event into the Events table and return a standardized JSON response.
Exceptions will propagate to the caller.
"""
conn = get_temp_db_connection()
cur = conn.cursor()
if isinstance(event_time, str):
start_time = ensure_datetime(event_time)
start_time = ensure_datetime(event_time)
cur.execute("""
INSERT INTO Events (eve_MAC, eve_IP, eve_DateTime, eve_EventType, eve_AdditionalInfo, eve_PendingAlertEmail)
VALUES (?, ?, ?, ?, ?, ?)
""", (mac, ip, start_time, event_type, additional_info, pending_alert))
conn.commit()
conn.close()
mylog("debug", f"[Events] Created event for {mac} ({event_type})")
return jsonify({"success": True, "message": f"Created event for {mac}"})
def get_events(mac=None):
"""
Fetch all events, or events for a specific MAC if provided.
Returns JSON list of events.
"""
conn = get_temp_db_connection()
cur = conn.cursor()
if mac:
sql = "SELECT * FROM Events WHERE eve_MAC=? ORDER BY eve_DateTime DESC"
cur.execute(sql, (mac,))
else:
sql = "SELECT * FROM Events ORDER BY eve_DateTime DESC"
cur.execute(sql)
rows = cur.fetchall()
events = [row_to_json(list(r.keys()), r) for r in rows]
conn.close()
return jsonify({"success": True, "events": events})
def delete_events_older_than(days):
"""Delete all events older than a specified number of days"""
conn = get_temp_db_connection()
cur = conn.cursor()
# Use a parameterized query with sqlite date function
sql = "DELETE FROM Events WHERE eve_DateTime <= date('now', ?)"
cur.execute(sql, [f'-{days} days'])
conn.commit()
conn.close()
return jsonify({
"success": True,
"message": f"Deleted events older than {days} days"
})
def delete_events():
"""Delete all events"""
conn = get_temp_db_connection()
cur = conn.cursor()
sql = "DELETE FROM Events"
cur.execute(sql)
conn.commit()
conn.close()
return jsonify({"success": True, "message": "Deleted all events"})
def get_events_totals(period: str = "7 days"):
"""
Return counts for events and sessions totals over a given period.
period: "7 days", "1 month", "1 year", "100 years"
"""
# Convert period to SQLite date expression
period_date_sql = get_date_from_period(period)
conn = get_temp_db_connection()
cur = conn.cursor()
sql = f"""
SELECT
(SELECT COUNT(*) FROM Events WHERE eve_DateTime >= {period_date_sql}) AS all_events,
(SELECT COUNT(*) FROM Sessions WHERE
ses_DateTimeConnection >= {period_date_sql}
OR ses_DateTimeDisconnection >= {period_date_sql}
OR ses_StillConnected = 1
) AS sessions,
(SELECT COUNT(*) FROM Sessions WHERE
(ses_DateTimeConnection IS NULL AND ses_DateTimeDisconnection >= {period_date_sql})
OR (ses_DateTimeDisconnection IS NULL AND ses_StillConnected = 0 AND ses_DateTimeConnection >= {period_date_sql})
) AS missing,
(SELECT COUNT(*) FROM Events WHERE eve_DateTime >= {period_date_sql} AND eve_EventType LIKE 'VOIDED%') AS voided,
(SELECT COUNT(*) FROM Events WHERE eve_DateTime >= {period_date_sql} AND eve_EventType LIKE 'New Device') AS new,
(SELECT COUNT(*) FROM Events WHERE eve_DateTime >= {period_date_sql} AND eve_EventType LIKE 'Device Down') AS down
"""
cur.execute(sql)
row = cur.fetchone()
conn.close()
# Return as JSON array
result_json = [row[0], row[1], row[2], row[3], row[4], row[5]]
return jsonify(result_json)

View File

@@ -124,8 +124,10 @@ class Query(ObjectType):
device["devParentChildrenCount"] = get_number_of_children(device["devMac"], devices_data)
device["devIpLong"] = format_ip_long(device.get("devLastIP", ""))
mylog('verbose', f'[graphql_schema] devices_data: {devices_data}')
mylog('trace', f'[graphql_schema] devices_data: {devices_data}')
# initialize total_count
total_count = len(devices_data)
# Apply sorting if options are provided
if options:
@@ -133,16 +135,16 @@ class Query(ObjectType):
# Define status-specific filtering
if options.status:
status = options.status
mylog('verbose', f'[graphql_schema] Applying status filter: {status}')
mylog('trace', f'[graphql_schema] Applying status filter: {status}')
# Include devices matching criteria in UI_MY_DEVICES
allowed_statuses = get_setting_value("UI_MY_DEVICES")
hidden_relationships = get_setting_value("UI_hide_rel_types")
network_dev_types = get_setting_value("NETWORK_DEVICE_TYPES")
mylog('verbose', f'[graphql_schema] allowed_statuses: {allowed_statuses}')
mylog('verbose', f'[graphql_schema] hidden_relationships: {hidden_relationships}')
mylog('verbose', f'[graphql_schema] network_dev_types: {network_dev_types}')
mylog('trace', f'[graphql_schema] allowed_statuses: {allowed_statuses}')
mylog('trace', f'[graphql_schema] hidden_relationships: {hidden_relationships}')
mylog('trace', f'[graphql_schema] network_dev_types: {network_dev_types}')
# Filtering based on the "status"
if status == "my_devices":
@@ -221,7 +223,7 @@ class Query(ObjectType):
reverse=(sort_option.order.lower() == "desc")
)
# capture total count after all the filtering and searching
# capture total count after all the filtering and searching, BEFORE pagination
total_count = len(devices_data)
# Then apply pagination
@@ -232,6 +234,7 @@ class Query(ObjectType):
# Convert dict objects to Device instances to enable field resolution
devices = [Device(**device) for device in devices_data]
return DeviceResult(devices=devices, count=total_count)
@@ -248,7 +251,7 @@ class Query(ObjectType):
return SettingResult(settings=[], count=0)
mylog('verbose', f'[graphql_schema] settings_data: {settings_data}')
mylog('trace', f'[graphql_schema] settings_data: {settings_data}')
# Convert to Setting objects
settings = [Setting(**setting) for setting in settings_data]

View File

@@ -0,0 +1,35 @@
#!/usr/bin/env python
import json
import subprocess
import argparse
import os
import pathlib
import sys
from datetime import datetime
from flask import jsonify, request
# Register NetAlertX directories
INSTALL_PATH="/app"
sys.path.extend([f"{INSTALL_PATH}/front/plugins", f"{INSTALL_PATH}/server"])
from database import get_temp_db_connection
from helper import is_random_mac, format_date, get_setting_value
# --------------------------------------------------
# Online History Activity Endpoints Functions
# --------------------------------------------------
def delete_online_history():
"""Delete all online history activity"""
conn = get_temp_db_connection()
cur = conn.cursor()
sql = "DELETE FROM Online_History"
cur.execute(sql)
conn.commit()
conn.close()
return jsonify({"success": True, "message": "Deleted online history"})

View File

@@ -0,0 +1,222 @@
import subprocess
import re
import sys
import ipaddress
from flask import jsonify
# Register NetAlertX directories
INSTALL_PATH = "/app"
sys.path.extend([f"{INSTALL_PATH}/front/plugins", f"{INSTALL_PATH}/server"])
def wakeonlan(mac):
# Validate MAC
if not re.match(r'^([0-9A-Fa-f]{2}[:-]){5}([0-9A-Fa-f]{2})$', mac):
return jsonify({"success": False, "error": f"Invalid MAC: {mac}"}), 400
try:
result = subprocess.run(
["wakeonlan", mac],
capture_output=True,
text=True,
check=True
)
return jsonify({"success": True, "message": "WOL packet sent", "output": result.stdout.strip()})
except subprocess.CalledProcessError as e:
return jsonify({"success": False, "error": "Failed to send WOL packet", "details": e.stderr.strip()}), 500
def traceroute(ip):
"""
Executes a traceroute to the given IP address.
Parameters:
ip (str): The target IP address to trace.
Returns:
JSON response with:
- success (bool)
- output (str) if successful
- error (str) and details (str) if failed
"""
# --------------------------
# Step 1: Validate IP address
# --------------------------
try:
ipaddress.ip_address(ip)
except ValueError:
# Return 400 if IP is invalid
return jsonify({"success": False, "error": f"Invalid IP: {ip}"}), 400
# --------------------------
# Step 2: Execute traceroute
# --------------------------
try:
result = subprocess.run(
["traceroute", ip], # Command and argument
capture_output=True, # Capture stdout/stderr
text=True, # Return output as string
check=True # Raise CalledProcessError on non-zero exit
)
# Return success response with traceroute output
return jsonify({"success": True, "output": result.stdout.strip()})
# --------------------------
# Step 3: Handle command errors
# --------------------------
except subprocess.CalledProcessError as e:
# Return 500 if traceroute fails
return jsonify({
"success": False,
"error": "Traceroute failed",
"details": e.stderr.strip()
}), 500
def speedtest():
"""
API endpoint to run a speedtest using speedtest-cli.
Returns JSON with the test output or error.
"""
try:
# Run speedtest-cli command
result = subprocess.run(
[f"{INSTALL_PATH}/back/speedtest-cli", "--secure", "--simple"],
capture_output=True,
text=True,
check=True
)
# Return each line as a list
output_lines = result.stdout.strip().split("\n")
return jsonify({"success": True, "output": output_lines})
except subprocess.CalledProcessError as e:
return jsonify({
"success": False,
"error": "Speedtest failed",
"details": e.stderr.strip()
}), 500
def nslookup(ip):
"""
Run an nslookup on the given IP address.
Returns JSON with the lookup output or error.
"""
# Validate IP
try:
ipaddress.ip_address(ip)
except ValueError:
return jsonify({
"success": False,
"error": "Invalid IP address"
}), 400
try:
# Run nslookup command
result = subprocess.run(
["nslookup", ip],
capture_output=True,
text=True,
check=True
)
output_lines = result.stdout.strip().split("\n")
return jsonify({"success": True, "output": output_lines})
except subprocess.CalledProcessError as e:
return jsonify({
"success": False,
"error": "nslookup failed",
"details": e.stderr.strip()
}), 500
def nmap_scan(ip, mode):
"""
Run an nmap scan on the given IP address with the requested mode.
Modes supported:
- "fast" → nmap -F <ip>
- "normal" → nmap <ip>
- "detail" → nmap -A <ip>
- "skipdiscovery" → nmap -Pn <ip>
Returns JSON with the scan output or error.
"""
# Validate IP
try:
ipaddress.ip_address(ip)
except ValueError:
return jsonify({
"success": False,
"error": "Invalid IP address"
}), 400
# Map scan modes to nmap arguments
mode_args = {
"fast": ["-F"],
"normal": [],
"detail": ["-A"],
"skipdiscovery": ["-Pn"]
}
if mode not in mode_args:
return jsonify({
"success": False,
"error": f"Invalid scan mode '{mode}'"
}), 400
try:
# Build and run nmap command
cmd = ["nmap"] + mode_args[mode] + [ip]
result = subprocess.run(
cmd,
capture_output=True,
text=True,
check=True
)
output_lines = result.stdout.strip().split("\n")
return jsonify({
"success": True,
"mode": mode,
"ip": ip,
"output": output_lines
})
except subprocess.CalledProcessError as e:
return jsonify({
"success": False,
"error": "nmap scan failed",
"details": e.stderr.strip()
}), 500
def internet_info():
"""
API endpoint to fetch internet info using ipinfo.io.
Returns JSON with the info or error.
"""
try:
# Perform the request via curl
result = subprocess.run(
["curl", "-s", "https://ipinfo.io"],
capture_output=True,
text=True,
check=True
)
output = result.stdout.strip()
if not output:
raise ValueError("Empty response from ipinfo.io")
# Clean up the JSON-like string by removing { } , and "
cleaned_output = output.replace("{", "").replace("}", "").replace(",", "").replace('"', "")
return jsonify({"success": True, "output": cleaned_output})
except (subprocess.CalledProcessError, ValueError) as e:
return jsonify({
"success": False,
"error": "Failed to fetch internet info",
"details": str(e)
}), 500

View File

@@ -18,7 +18,7 @@ def escape_label_value(val):
# Define a base URL with the user's home directory
folder = apiPath
def getMetricStats():
def get_metric_stats():
output = []
# 1. Dashboard totals

View File

@@ -0,0 +1,383 @@
#!/usr/bin/env python
import json
import subprocess
import argparse
import os
import pathlib
import sqlite3
import time
import sys
from datetime import datetime
from flask import jsonify, request
# Register NetAlertX directories
INSTALL_PATH="/app"
sys.path.extend([f"{INSTALL_PATH}/front/plugins", f"{INSTALL_PATH}/server"])
from database import get_temp_db_connection
from helper import is_random_mac, format_date, get_setting_value, format_date_iso, format_event_date, mylog, timeNowTZ, format_date_diff, format_ip_long, parse_datetime
from db.db_helper import row_to_json, get_date_from_period
# --------------------------
# Sessions Endpoints Functions
# --------------------------
# -------------------------------------------------------------------------------------------
def create_session(mac, ip, start_time, end_time=None, event_type_conn="Connected", event_type_disc="Disconnected"):
"""Insert a new session into Sessions table"""
conn = get_temp_db_connection()
cur = conn.cursor()
cur.execute("""
INSERT INTO Sessions (ses_MAC, ses_IP, ses_DateTimeConnection, ses_DateTimeDisconnection,
ses_EventTypeConnection, ses_EventTypeDisconnection)
VALUES (?, ?, ?, ?, ?, ?)
""", (mac, ip, start_time, end_time, event_type_conn, event_type_disc))
conn.commit()
conn.close()
return jsonify({"success": True, "message": f"Session created for MAC {mac}"})
# -------------------------------------------------------------------------------------------
def delete_session(mac):
"""Delete all sessions for a given MAC"""
conn = get_temp_db_connection()
cur = conn.cursor()
cur.execute("DELETE FROM Sessions WHERE ses_MAC = ?", (mac,))
conn.commit()
conn.close()
return jsonify({"success": True, "message": f"Deleted sessions for MAC {mac}"})
# -------------------------------------------------------------------------------------------
def get_sessions(mac=None, start_date=None, end_date=None):
"""Retrieve sessions optionally filtered by MAC and date range"""
conn = get_temp_db_connection()
cur = conn.cursor()
sql = "SELECT * FROM Sessions WHERE 1=1"
params = []
if mac:
sql += " AND ses_MAC = ?"
params.append(mac)
if start_date:
sql += " AND ses_DateTimeConnection >= ?"
params.append(start_date)
if end_date:
sql += " AND ses_DateTimeDisconnection <= ?"
params.append(end_date)
cur.execute(sql, tuple(params))
rows = cur.fetchall()
conn.close()
# Convert rows to list of dicts
table_data = [dict(r) for r in rows]
return jsonify({"success": True, "sessions": table_data})
def get_sessions_calendar(start_date, end_date):
"""
Fetch sessions between a start and end date for calendar display.
Returns JSON list of calendar sessions.
"""
if not start_date or not end_date:
return jsonify({"success": False, "error": "Missing start or end date"}), 400
conn = get_temp_db_connection()
cur = conn.cursor()
sql = """
-- Correct missing connection/disconnection sessions:
-- If ses_EventTypeConnection is missing, backfill from last disconnection
-- If ses_EventTypeDisconnection is missing, forward-fill from next connection
SELECT
SES1.ses_MAC, SES1.ses_EventTypeConnection, SES1.ses_DateTimeConnection,
SES1.ses_EventTypeDisconnection, SES1.ses_DateTimeDisconnection, SES1.ses_IP,
SES1.ses_AdditionalInfo, SES1.ses_StillConnected,
CASE
WHEN SES1.ses_EventTypeConnection = '<missing event>' THEN
IFNULL(
(SELECT MAX(SES2.ses_DateTimeDisconnection)
FROM Sessions AS SES2
WHERE SES2.ses_MAC = SES1.ses_MAC
AND SES2.ses_DateTimeDisconnection < SES1.ses_DateTimeDisconnection
AND SES2.ses_DateTimeDisconnection BETWEEN Date(?) AND Date(?)
),
DATETIME(SES1.ses_DateTimeDisconnection, '-1 hour')
)
ELSE SES1.ses_DateTimeConnection
END AS ses_DateTimeConnectionCorrected,
CASE
WHEN SES1.ses_EventTypeDisconnection = '<missing event>' THEN
(SELECT MIN(SES2.ses_DateTimeConnection)
FROM Sessions AS SES2
WHERE SES2.ses_MAC = SES1.ses_MAC
AND SES2.ses_DateTimeConnection > SES1.ses_DateTimeConnection
AND SES2.ses_DateTimeConnection BETWEEN Date(?) AND Date(?)
)
ELSE SES1.ses_DateTimeDisconnection
END AS ses_DateTimeDisconnectionCorrected
FROM Sessions AS SES1
WHERE (SES1.ses_DateTimeConnection BETWEEN Date(?) AND Date(?))
OR (SES1.ses_DateTimeDisconnection BETWEEN Date(?) AND Date(?))
OR SES1.ses_StillConnected = 1
"""
cur.execute(sql, (start_date, end_date, start_date, end_date, start_date, end_date, start_date, end_date))
rows = cur.fetchall()
table_data = []
for r in rows:
row = dict(r)
# Determine color
if row["ses_EventTypeConnection"] == "<missing event>" or row["ses_EventTypeDisconnection"] == "<missing event>":
color = "#f39c12"
elif row["ses_StillConnected"] == 1:
color = "#00a659"
else:
color = "#0073b7"
# Tooltip
tooltip = (
f"Connection: {format_event_date(row['ses_DateTimeConnection'], row['ses_EventTypeConnection'])}\n"
f"Disconnection: {format_event_date(row['ses_DateTimeDisconnection'], row['ses_EventTypeDisconnection'])}\n"
f"IP: {row['ses_IP']}"
)
# Append calendar entry
table_data.append({
"resourceId": row["ses_MAC"],
"title": "",
"start": format_date_iso(row["ses_DateTimeConnectionCorrected"]),
"end": format_date_iso(row["ses_DateTimeDisconnectionCorrected"]),
"color": color,
"tooltip": tooltip,
"className": "no-border"
})
conn.close()
return jsonify({"success": True, "sessions": table_data})
def get_device_sessions(mac, period):
"""
Fetch device sessions for a given MAC address and period.
"""
period_date = get_date_from_period(period)
conn = get_temp_db_connection()
cur = conn.cursor()
sql = f"""
SELECT
IFNULL(ses_DateTimeConnection, ses_DateTimeDisconnection) AS ses_DateTimeOrder,
ses_EventTypeConnection,
ses_DateTimeConnection,
ses_EventTypeDisconnection,
ses_DateTimeDisconnection,
ses_StillConnected,
ses_IP,
ses_AdditionalInfo
FROM Sessions
WHERE ses_MAC = ?
AND (
ses_DateTimeConnection >= {period_date}
OR ses_DateTimeDisconnection >= {period_date}
OR ses_StillConnected = 1
)
"""
cur.execute(sql, (mac,))
rows = cur.fetchall()
conn.close()
table_data = {"data": []}
for row in rows:
# Connection DateTime
if row["ses_EventTypeConnection"] == "<missing event>":
ini = row["ses_EventTypeConnection"]
else:
ini = format_date(row["ses_DateTimeConnection"])
# Disconnection DateTime
if row["ses_StillConnected"]:
end = "..."
elif row["ses_EventTypeDisconnection"] == "<missing event>":
end = row["ses_EventTypeDisconnection"]
else:
end = format_date(row["ses_DateTimeDisconnection"])
# Duration
if row["ses_EventTypeConnection"] in ("<missing event>", None) or row["ses_EventTypeDisconnection"] in ("<missing event>", None):
dur = "..."
elif row["ses_StillConnected"]:
dur = format_date_diff(row["ses_DateTimeConnection"], None)["text"]
else:
dur = format_date_diff(row["ses_DateTimeConnection"], row["ses_DateTimeDisconnection"])["text"]
# Additional Info
info = row["ses_AdditionalInfo"]
if row["ses_EventTypeConnection"] == "New Device":
info = f"{row['ses_EventTypeConnection']}: {info}"
# Push row data
table_data["data"].append({
"ses_MAC": mac,
"ses_DateTimeOrder": row["ses_DateTimeOrder"],
"ses_Connection": ini,
"ses_Disconnection": end,
"ses_Duration": dur,
"ses_IP": row["ses_IP"],
"ses_Info": info,
})
# Control no rows
if not table_data["data"]:
table_data["data"] = []
sessions = table_data["data"]
return jsonify({
"success": True,
"sessions": sessions
})
def get_session_events(event_type, period_date):
"""
Fetch events or sessions based on type and period.
"""
conn = get_temp_db_connection()
conn.row_factory = sqlite3.Row
cur = conn.cursor()
# Base SQLs
sql_events = f"""
SELECT
eve_DateTime AS eve_DateTimeOrder,
devName,
devOwner,
eve_DateTime,
eve_EventType,
NULL,
NULL,
NULL,
NULL,
eve_IP,
NULL,
eve_AdditionalInfo,
NULL,
devMac,
eve_PendingAlertEmail
FROM Events_Devices
WHERE eve_DateTime >= {period_date}
"""
sql_sessions = f"""
SELECT
IFNULL(ses_DateTimeConnection, ses_DateTimeDisconnection) AS ses_DateTimeOrder,
devName,
devOwner,
NULL,
NULL,
ses_DateTimeConnection,
ses_DateTimeDisconnection,
NULL,
NULL,
ses_IP,
NULL,
ses_AdditionalInfo,
ses_StillConnected,
devMac
FROM Sessions_Devices
"""
# Build SQL based on type
if event_type == "all":
sql = sql_events
elif event_type == "sessions":
sql = sql_sessions + f"""
WHERE (
ses_DateTimeConnection >= {period_date}
OR ses_DateTimeDisconnection >= {period_date}
OR ses_StillConnected = 1
)
"""
elif event_type == "missing":
sql = sql_sessions + f"""
WHERE (
(ses_DateTimeConnection IS NULL AND ses_DateTimeDisconnection >= {period_date})
OR (ses_DateTimeDisconnection IS NULL AND ses_StillConnected = 0 AND ses_DateTimeConnection >= {period_date})
)
"""
elif event_type == "voided":
sql = sql_events + ' AND eve_EventType LIKE "VOIDED%"'
elif event_type == "new":
sql = sql_events + ' AND eve_EventType = "New Device"'
elif event_type == "down":
sql = sql_events + ' AND eve_EventType = "Device Down"'
else:
sql = sql_events + ' AND 1=0'
cur.execute(sql)
rows = cur.fetchall()
conn.close()
table_data = {"data": []}
for row in rows:
row = list(row) # make mutable
if event_type in ("sessions", "missing"):
# Duration
if row[5] and row[6]:
delta = format_date_diff(row[5], row[6])
row[7] = delta["text"]
row[8] = int(delta["total_minutes"] * 60) # seconds
elif row[12] == 1:
delta = format_date_diff(row[5], None)
row[7] = delta["text"]
row[8] = int(delta["total_minutes"] * 60) # seconds
else:
row[7] = "..."
row[8] = 0
# Connection
row[5] = format_date(row[5]) if row[5] else "<missing event>"
# Disconnection
if row[6]:
row[6] = format_date(row[6])
elif row[12] == 0:
row[6] = "<missing event>"
else:
row[6] = "..."
else:
# Event Date
row[3] = format_date(row[3])
# IP Order
row[10] = format_ip_long(row[9])
table_data["data"].append(row)
return jsonify(table_data)

View File

@@ -0,0 +1,71 @@
import os
import base64
from flask import jsonify, request
from logger import mylog
from helper import get_setting_value, timeNowTZ
from messaging.in_app import write_notification
INSTALL_PATH = "/app"
def handle_sync_get():
"""Handle GET requests for SYNC (NODE → HUB)."""
file_path = INSTALL_PATH + "/api/table_devices.json"
try:
with open(file_path, "rb") as f:
raw_data = f.read()
except FileNotFoundError:
msg = f"[Plugin: SYNC] Data file not found: {file_path}"
write_notification(msg, "alert", timeNowTZ())
mylog("verbose", [msg])
return jsonify({"error": msg}), 500
response_data = base64.b64encode(raw_data).decode("utf-8")
write_notification("[Plugin: SYNC] Data sent", "info", timeNowTZ())
return jsonify({
"node_name": get_setting_value("SYNC_node_name"),
"status": 200,
"message": "OK",
"data_base64": response_data,
"timestamp": timeNowTZ()
}), 200
def handle_sync_post():
"""Handle POST requests for SYNC (HUB receiving from NODE)."""
data = request.form.get("data", "")
node_name = request.form.get("node_name", "")
plugin = request.form.get("plugin", "")
storage_path = INSTALL_PATH + "/log/plugins"
os.makedirs(storage_path, exist_ok=True)
encoded_files = [
f for f in os.listdir(storage_path)
if f.startswith(f"last_result.{plugin}.encoded.{node_name}")
]
decoded_files = [
f for f in os.listdir(storage_path)
if f.startswith(f"last_result.{plugin}.decoded.{node_name}")
]
file_count = len(encoded_files + decoded_files) + 1
file_path_new = os.path.join(
storage_path,
f"last_result.{plugin}.encoded.{node_name}.{file_count}.log"
)
try:
with open(file_path_new, "w") as f:
f.write(data)
except Exception as e:
msg = f"[Plugin: SYNC] Failed to store data: {e}"
write_notification(msg, "alert", timeNowTZ())
mylog("verbose", [msg])
return jsonify({"error": msg}), 500
msg = f"[Plugin: SYNC] Data received ({file_path_new})"
write_notification(msg, "info", timeNowTZ())
mylog("verbose", [msg])
return jsonify({"message": "Data received and stored successfully"}), 200

View File

@@ -8,7 +8,8 @@ import json
from const import fullDbPath, sql_devices_stats, sql_devices_all, sql_generateGuid
from logger import mylog
from helper import json_obj, initOrSetParam, row_to_json, timeNowTZ
from helper import timeNowTZ
from db.db_helper import row_to_json, get_table_json, json_obj
from workflows.app_events import AppEvent_obj
from db.db_upgrade import ensure_column, ensure_views, ensure_CurrentScan, ensure_plugins_tables, ensure_Parameters, ensure_Settings, ensure_Indexes
@@ -121,26 +122,41 @@ class DB():
AppEvent_obj(self)
#-------------------------------------------------------------------------------
# #-------------------------------------------------------------------------------
# def get_table_as_json(self, sqlQuery):
# # mylog('debug',[ '[Database] - get_table_as_json - Query: ', sqlQuery])
# try:
# self.sql.execute(sqlQuery)
# columnNames = list(map(lambda x: x[0], self.sql.description))
# rows = self.sql.fetchall()
# except sqlite3.Error as e:
# mylog('verbose',[ '[Database] - SQL ERROR: ', e])
# return json_obj({}, []) # return empty object
# result = {"data":[]}
# for row in rows:
# tmp = row_to_json(columnNames, row)
# result["data"].append(tmp)
# # mylog('debug',[ '[Database] - get_table_as_json - returning ', len(rows), " rows with columns: ", columnNames])
# # mylog('debug',[ '[Database] - get_table_as_json - returning json ', json.dumps(result) ])
# return json_obj(result, columnNames)
def get_table_as_json(self, sqlQuery):
# mylog('debug',[ '[Database] - get_table_as_json - Query: ', sqlQuery])
"""
Wrapper to use the central get_table_as_json helper.
"""
try:
self.sql.execute(sqlQuery)
columnNames = list(map(lambda x: x[0], self.sql.description))
rows = self.sql.fetchall()
except sqlite3.Error as e:
mylog('verbose',[ '[Database] - SQL ERROR: ', e])
return json_obj({}, []) # return empty object
result = {"data":[]}
for row in rows:
tmp = row_to_json(columnNames, row)
result["data"].append(tmp)
result = get_table_json(self.sql, sqlQuery)
except Exception as e:
mylog('verbose', ['[Database] - get_table_as_json ERROR:', e])
return json_obj({}, []) # return empty object on failure
# mylog('debug',[ '[Database] - get_table_as_json - returning ', len(rows), " rows with columns: ", columnNames])
# mylog('debug',[ '[Database] - get_table_as_json - returning json ', json.dumps(result) ])
return json_obj(result, columnNames)
return result
#-------------------------------------------------------------------------------
# referece from here: https://codereview.stackexchange.com/questions/241043/interface-class-for-sqlite-databases
@@ -204,3 +220,13 @@ def get_array_from_sql_rows(rows):
#-------------------------------------------------------------------------------
def get_temp_db_connection():
"""
Returns a new SQLite connection with Row factory.
Should be used per-thread/request to avoid cross-thread issues.
"""
conn = sqlite3.connect(fullDbPath, timeout=5, isolation_level=None)
conn.execute("PRAGMA journal_mode=WAL;")
conn.execute("PRAGMA busy_timeout=5000;") # 5s wait before giving up
conn.row_factory = sqlite3.Row
return conn

269
server/db/db_helper.py Executable file
View File

@@ -0,0 +1,269 @@
import sys
import sqlite3
# Register NetAlertX directories
INSTALL_PATH="/app"
sys.path.extend([f"{INSTALL_PATH}/server"])
from helper import if_byte_then_to_str
from logger import mylog
#-------------------------------------------------------------------------------
# Return the SQL WHERE clause for filtering devices based on their status.
def get_device_condition_by_status(device_status):
"""
Return the SQL WHERE clause for filtering devices based on their status.
Parameters:
device_status (str): The status of the device. Possible values:
- 'all' : All active devices
- 'my' : Same as 'all' (active devices)
- 'connected' : Devices that are active and present in the last scan
- 'favorites' : Devices marked as favorite
- 'new' : Devices marked as new
- 'down' : Devices not present in the last scan but with alerts
- 'archived' : Devices that are archived
Returns:
str: SQL WHERE clause corresponding to the device status.
Defaults to 'WHERE 1=0' for unrecognized statuses.
"""
conditions = {
'all': 'WHERE devIsArchived=0',
'my': 'WHERE devIsArchived=0',
'connected': 'WHERE devIsArchived=0 AND devPresentLastScan=1',
'favorites': 'WHERE devIsArchived=0 AND devFavorite=1',
'new': 'WHERE devIsArchived=0 AND devIsNew=1',
'down': 'WHERE devIsArchived=0 AND devAlertDown != 0 AND devPresentLastScan=0',
'archived': 'WHERE devIsArchived=1'
}
return conditions.get(device_status, 'WHERE 1=0')
#-------------------------------------------------------------------------------
# Creates a JSON-like dictionary from a database row
def row_to_json(names, row):
"""
Convert a database row into a JSON-like dictionary.
Parameters:
names (list of str): List of column names corresponding to the row fields.
row (dict or sequence): A database row, typically a dictionary or list-like object,
where each column can be accessed by index or key.
Returns:
dict: A dictionary where keys are column names and values are the corresponding
row values. Byte values are automatically converted to strings using
`if_byte_then_to_str`.
Example:
names = ['id', 'name', 'data']
row = {0: 1, 1: b'Example', 2: b'\x01\x02'}
row_to_json(names, row)
# Returns: {'id': 1, 'name': 'Example', 'data': '\\x01\\x02'}
"""
rowEntry = {}
for index, name in enumerate(names):
rowEntry[name] = if_byte_then_to_str(row[name])
return rowEntry
#-------------------------------------------------------------------------------
def sanitize_SQL_input(val):
"""
Sanitize a value for use in SQL queries by replacing single quotes in strings.
Parameters:
val (any): The value to sanitize.
Returns:
str or any:
- Returns an empty string if val is None.
- Returns a string with single quotes replaced by underscores if val is a string.
- Returns val unchanged if it is any other type.
"""
if val is None:
return ''
if isinstance(val, str):
return val.replace("'", "_")
return val # Return non-string values as they are
# -------------------------------------------------------------------------------------------
def get_date_from_period(period):
"""
Convert a period string into an SQLite date expression.
Parameters:
period (str): The requested period (e.g., '7 days', '1 month', '1 year', '100 years').
Returns:
str: An SQLite date expression like "date('now', '-7 day')" corresponding to the period.
"""
days_map = {
'7 days': 7,
'1 month': 30,
'1 year': 365,
'100 years': 3650, # actually 10 years in original PHP
}
days = days_map.get(period, 1) # default 1 day
period_sql = f"date('now', '-{days} day')"
return period_sql
#-------------------------------------------------------------------------------
def print_table_schema(db, table):
"""
Print the schema of a database table to the log.
Parameters:
db: A database connection object with a `sql` cursor.
table (str): The name of the table whose schema is to be printed.
Returns:
None: Logs the column information including cid, name, type, notnull, default value, and primary key.
"""
sql = db.sql
sql.execute(f"PRAGMA table_info({table})")
result = sql.fetchall()
if not result:
mylog('none', f'[Schema] Table "{table}" not found or has no columns.')
return
mylog('debug', f'[Schema] Structure for table: {table}')
header = f"{'cid':<4} {'name':<20} {'type':<10} {'notnull':<8} {'default':<10} {'pk':<2}"
mylog('debug', header)
mylog('debug', '-' * len(header))
for row in result:
# row = (cid, name, type, notnull, dflt_value, pk)
line = f"{row[0]:<4} {row[1]:<20} {row[2]:<10} {row[3]:<8} {str(row[4]):<10} {row[5]:<2}"
mylog('debug', line)
#-------------------------------------------------------------------------------
# Generate a WHERE condition for SQLite based on a list of values.
def list_to_where(logical_operator, column_name, condition_operator, values_list):
"""
Generate a WHERE condition for SQLite based on a list of values.
Parameters:
- logical_operator: The logical operator ('AND' or 'OR') to combine conditions.
- column_name: The name of the column to filter on.
- condition_operator: The condition operator ('LIKE', 'NOT LIKE', '=', '!=', etc.).
- values_list: A list of values to be included in the condition.
Returns:
- A string representing the WHERE condition.
"""
# If the list is empty, return an empty string
if not values_list:
return ""
# Replace {s-quote} with single quote in values_list
values_list = [value.replace("{s-quote}", "'") for value in values_list]
# Build the WHERE condition for the first value
condition = f"{column_name} {condition_operator} '{values_list[0]}'"
# Add the rest of the values using the logical operator
for value in values_list[1:]:
condition += f" {logical_operator} {column_name} {condition_operator} '{value}'"
return f'({condition})'
#-------------------------------------------------------------------------------
def get_table_json(sql, sql_query):
"""
Execute a SQL query and return the results as JSON-like dict.
Args:
sql: SQLite cursor or connection wrapper supporting execute(), description, and fetchall().
sql_query (str): The SQL query to execute.
Returns:
dict: JSON-style object with data and column names.
"""
try:
sql.execute(sql_query)
column_names = [col[0] for col in sql.description]
rows = sql.fetchall()
except sqlite3.Error as e:
mylog('verbose', ['[Database] - SQL ERROR: ', e])
return json_obj({}, []) # return empty object
result = {"data": [row_to_json(column_names, row) for row in rows]}
return json_obj(result, column_names)
#-------------------------------------------------------------------------------
class json_obj:
"""
A wrapper class for JSON-style objects returned from database queries.
Provides dict-like access to the JSON data while storing column metadata.
Attributes:
json (dict): The actual JSON-style data returned from the database.
columnNames (list): List of column names corresponding to the data.
"""
def __init__(self, jsn, columnNames):
"""
Initialize the json_obj with JSON data and column names.
Args:
jsn (dict): JSON-style dictionary containing the data.
columnNames (list): List of column names for the data.
"""
self.json = jsn
self.columnNames = columnNames
def get(self, key, default=None):
"""
Dict-like .get() access to the JSON data.
Args:
key (str): Key to retrieve from the JSON data.
default: Value to return if key is not found (default: None).
Returns:
Value corresponding to key in the JSON data, or default if not present.
"""
return self.json.get(key, default)
def keys(self):
"""
Return the keys of the JSON data.
Returns:
Iterable of keys in the JSON dictionary.
"""
return self.json.keys()
def items(self):
"""
Return the items of the JSON data.
Returns:
Iterable of (key, value) pairs in the JSON dictionary.
"""
return self.json.items()
def __getitem__(self, key):
"""
Allow bracket-access (obj[key]) to the JSON data.
Args:
key (str): Key to retrieve from the JSON data.
Returns:
Value corresponding to the key.
"""
return self.json[key]

View File

@@ -230,8 +230,7 @@ def ensure_CurrentScan(sql) -> bool:
cur_SSID STRING(250),
cur_NetworkNodeMAC STRING(250),
cur_PORT STRING(250),
cur_Type STRING(250),
UNIQUE(cur_MAC)
cur_Type STRING(250)
);
""")

View File

@@ -7,6 +7,7 @@ import os
import re
import unicodedata
import subprocess
from typing import Union
import pytz
from pytz import timezone
import json
@@ -16,9 +17,9 @@ import requests
import base64
import hashlib
import random
import email
import string
import ipaddress
import dns.resolver
import conf
from const import *
@@ -54,20 +55,114 @@ def get_timezone_offset():
#-------------------------------------------------------------------------------
def updateSubnets(scan_subnets):
subnets = []
# Date and time methods
#-------------------------------------------------------------------------------
# multiple interfaces
if type(scan_subnets) is list:
for interface in scan_subnets :
subnets.append(interface)
# one interface only
# # -------------------------------------------------------------------------------------------
# def format_date(date_str: str) -> str:
# """Format a date string as 'YYYY-MM-DD HH:MM'"""
# dt = datetime.datetime.fromisoformat(date_str) if isinstance(date_str, str) else date_str
# return dt.strftime('%Y-%m-%d %H:%M')
# # -------------------------------------------------------------------------------------------
# def format_date_diff(date1: str, date2: str) -> str:
# """Return difference between two dates formatted as 'Xd HH:MM'"""
# dt1 = datetime.datetime.fromisoformat(date1) if isinstance(date1, str) else date1
# dt2 = datetime.datetime.fromisoformat(date2) if isinstance(date2, str) else date2
# delta = dt2 - dt1
# days = delta.days
# hours, remainder = divmod(delta.seconds, 3600)
# minutes = remainder // 60
# return f"{days}d {hours:02}:{minutes:02}"
# -------------------------------------------------------------------------------------------
def format_date_iso(date1: str) -> str:
"""Return ISO 8601 string for a date or None if empty"""
if date1 is None:
return None
dt = datetime.datetime.fromisoformat(date1) if isinstance(date1, str) else date1
return dt.isoformat()
# -------------------------------------------------------------------------------------------
def format_event_date(date_str: str, event_type: str) -> str:
"""Format event date with fallback rules."""
if date_str:
return format_date(date_str)
elif event_type == "<missing event>":
return "<missing event>"
else:
subnets.append(scan_subnets)
return "<still connected>"
return subnets
# -------------------------------------------------------------------------------------------
def ensure_datetime(dt: Union[str, datetime, None]) -> datetime:
if dt is None:
return timeNowTZ()
if isinstance(dt, str):
return datetime.datetime.fromisoformat(dt)
return dt
def parse_datetime(dt_str):
if not dt_str:
return None
try:
# Try ISO8601 first
return datetime.datetime.fromisoformat(dt_str)
except ValueError:
# Try RFC1123 / HTTP format
try:
return datetime.datetime.strptime(dt_str, '%a, %d %b %Y %H:%M:%S GMT')
except ValueError:
return None
def format_date(date_str: str) -> str:
dt = parse_datetime(date_str)
return dt.strftime('%Y-%m-%d %H:%M') if dt else "invalid"
def format_date_diff(date1, date2):
"""
Return difference between two datetimes as 'Xd HH:MM'.
Uses app timezone if datetime is naive.
date2 can be None (uses now).
"""
# Get timezone from settings
tz_name = get_setting_value("TIMEZONE") or "UTC"
tz = pytz.timezone(tz_name)
def parse_dt(dt):
if dt is None:
return datetime.datetime.now(tz)
if isinstance(dt, str):
try:
dt_parsed = email.utils.parsedate_to_datetime(dt)
except Exception:
# fallback: parse ISO string
dt_parsed = datetime.datetime.fromisoformat(dt)
# convert naive GMT/UTC to app timezone
if dt_parsed.tzinfo is None:
dt_parsed = tz.localize(dt_parsed)
else:
dt_parsed = dt_parsed.astimezone(tz)
return dt_parsed
return dt if dt.tzinfo else tz.localize(dt)
dt1 = parse_dt(date1)
dt2 = parse_dt(date2)
delta = dt2 - dt1
total_minutes = int(delta.total_seconds() // 60)
days, rem_minutes = divmod(total_minutes, 1440) # 1440 mins in a day
hours, minutes = divmod(rem_minutes, 60)
return {
"text": f"{days}d {hours:02}:{minutes:02}",
"days": days,
"hours": hours,
"minutes": minutes,
"total_minutes": total_minutes
}
#-------------------------------------------------------------------------------
# File system permission handling
@@ -192,64 +287,132 @@ def write_file(pPath, pText):
#-------------------------------------------------------------------------------
# Setting methods
#-------------------------------------------------------------------------------
SETTINGS_CACHE = {}
SETTINGS_LASTCACHEDATE = 0
SETTINGS_SECONDARYCACHE={}
#-------------------------------------------------------------------------------
# Return whole setting touple
def get_setting(key):
"""
Retrieve the full setting tuple (dictionary) for a given key from the JSON settings file.
- Uses a cache to avoid re-reading the file if it hasn't changed.
- Loads settings from `table_settings.json` located at `apiPath`.
- Returns `None` if the key is not found or the file cannot be read.
Args:
key (str): The key of the setting to retrieve.
Returns:
dict | None: The setting dictionary for the key, or None if not found.
"""
global SETTINGS_LASTCACHEDATE, SETTINGS_CACHE, SETTINGS_SECONDARYCACHE
settingsFile = apiPath + 'table_settings.json'
try:
with open(settingsFile, 'r') as json_file:
data = json.load(json_file)
for item in data.get("data",[]):
if item.get("setKey") == key:
return item
mylog('debug', [f'[Settings] ⚠ ERROR - setting_missing - Setting not found for key: {key} in file {settingsFile}'])
return None
except (FileNotFoundError, json.JSONDecodeError, ValueError) as e:
# Handle the case when the file is not found, JSON decoding fails, or data is not in the expected format
mylog('none', [f'[Settings] ⚠ ERROR - JSONDecodeError or FileNotFoundError for file {settingsFile}'])
fileModifiedTime = os.path.getmtime(settingsFile)
except FileNotFoundError:
mylog('none', [f'[Settings] ⚠ File not found: {settingsFile}'])
return None
mylog('trace', [
'[Import table_settings.json] checking table_settings.json file',
f'SETTINGS_LASTCACHEDATE: {SETTINGS_LASTCACHEDATE}',
f'fileModifiedTime: {fileModifiedTime}'
])
# Use cache if file hasn't changed
if fileModifiedTime == SETTINGS_LASTCACHEDATE and SETTINGS_CACHE:
mylog('trace', ['[Import table_settings.json] using cached version'])
return SETTINGS_CACHE.get(key)
#-------------------------------------------------------------------------------
# Settings
#-------------------------------------------------------------------------------
# invalidate CACHE
SETTINGS_CACHE = {}
SETTINGS_SECONDARYCACHE={}
# Load JSON and populate cache
try:
with open(settingsFile, 'r') as json_file:
data = json.load(json_file)
SETTINGS_CACHE = {item["setKey"]: item for item in data.get("data", [])}
except json.JSONDecodeError:
mylog('none', [f'[Settings] ⚠ JSON decode error in file {settingsFile}'])
return None
except ValueError as e:
mylog('none', [f'[Settings] ⚠ Value error: {e} in file {settingsFile}'])
return None
# Only update file date when we successfully parsed the file
SETTINGS_LASTCACHEDATE = fileModifiedTime
if key not in SETTINGS_CACHE:
mylog('none', [f'[Settings] ⚠ ERROR - setting_missing - {key} not in {settingsFile}'])
return None
return SETTINGS_CACHE[key]
#-------------------------------------------------------------------------------
# Return setting value
def get_setting_value(key):
"""
Retrieve a setting value from configuration.
# Returns empty string if not set
value = ''
- First checks if `conf.mySettings` is populated and contains the key.
- Falls back to `get_setting(key)` if not found.
- Converts the raw stored value into the correct Python type
using `setting_value_to_python_type`.
Args:
key (str): The setting key to look up.
Returns:
Any: The Python-typed setting value, or an empty string if not found.
"""
global SETTINGS_SECONDARYCACHE
# Returns empty string if not found
value = ''
# lookup key in secondary cache
if key in SETTINGS_SECONDARYCACHE:
return SETTINGS_SECONDARYCACHE[key]
# Prefer conf.mySettings if available
if hasattr(conf, "mySettings") and conf.mySettings:
# conf.mySettings is a list of tuples, find by key (tuple[0])
for item in conf.mySettings:
if item[0] == key:
set_type = item[3] # type
set_value = item[5] # value
if isinstance(set_value, (list, dict)):
value = setting_value_to_python_type(set_type, set_value)
else:
value = setting_value_to_python_type(set_type, str(set_value))
SETTINGS_SECONDARYCACHE[key] = value
return value
# Otherwise fall back to retrive from json
setting = get_setting(key)
if setting is not None:
# mylog('none', [f'[SETTINGS] setting json:{json.dumps(setting)}'])
set_type = 'Error: Not handled'
set_value = 'Error: Not handled'
set_value = setting["setValue"] # Setting value (Value (upper case) = user overridden default_value)
set_type = setting["setType"] # Setting type # lower case "type" - default json value vs uppper-case "setType" (= from user defined settings)
set_type = setting["setType"] # Setting type # lower case "type" - default json value vs uppper-case "setType" (= from user defined settings)
value = setting_value_to_python_type(set_type, set_value)
SETTINGS_SECONDARYCACHE[key] = value
return value
#-------------------------------------------------------------------------------
# Convert the setting value to the corresponding python type
def setting_value_to_python_type(set_type, set_value):
value = '----not processed----'
@@ -341,6 +504,30 @@ def setting_value_to_python_type(set_type, set_value):
return value
#-------------------------------------------------------------------------------
def updateSubnets(scan_subnets):
"""
Normalize scan subnet input into a list of subnets.
Parameters:
scan_subnets (str or list): A single subnet string or a list of subnet strings.
Returns:
list: A list containing all subnets. If a single subnet is provided, it is returned as a single-element list.
"""
subnets = []
# multiple interfaces
if isinstance(scan_subnets, list):
for interface in scan_subnets:
subnets.append(interface)
# one interface only
else:
subnets.append(scan_subnets)
return subnets
#-------------------------------------------------------------------------------
# Reverse transformed values if needed
def reverseTransformers(val, transformers):
@@ -360,41 +547,6 @@ def reverseTransformers(val, transformers):
else:
return reverse_transformers(val, transformers)
#-------------------------------------------------------------------------------
# Generate a WHERE condition for SQLite based on a list of values.
def list_to_where(logical_operator, column_name, condition_operator, values_list):
"""
Generate a WHERE condition for SQLite based on a list of values.
Parameters:
- logical_operator: The logical operator ('AND' or 'OR') to combine conditions.
- column_name: The name of the column to filter on.
- condition_operator: The condition operator ('LIKE', 'NOT LIKE', '=', '!=', etc.).
- values_list: A list of values to be included in the condition.
Returns:
- A string representing the WHERE condition.
"""
# If the list is empty, return an empty string
if not values_list:
return ""
# Replace {s-quote} with single quote in values_list
values_list = [value.replace("{s-quote}", "'") for value in values_list]
# Build the WHERE condition for the first value
condition = f"{column_name} {condition_operator} '{values_list[0]}'"
# Add the rest of the values using the logical operator
for value in values_list[1:]:
condition += f" {logical_operator} {column_name} {condition_operator} '{value}'"
return f'({condition})'
#-------------------------------------------------------------------------------
# IP validation methods
@@ -432,6 +584,19 @@ def check_IP_format (pIP):
# String manipulation methods
#-------------------------------------------------------------------------------
#-------------------------------------------------------------------------------
def generate_random_string(length):
characters = string.ascii_letters + string.digits
return ''.join(random.choice(characters) for _ in range(length))
#-------------------------------------------------------------------------------
def extract_between_strings(text, start, end):
start_index = text.find(start)
end_index = text.find(end, start_index + len(start))
if start_index != -1 and end_index != -1:
return text[start_index + len(start):end_index]
else:
return ""
#-------------------------------------------------------------------------------
@@ -474,7 +639,6 @@ def removeDuplicateNewLines(text):
return text
#-------------------------------------------------------------------------------
def sanitize_string(input):
if isinstance(input, bytes):
input = input.decode('utf-8')
@@ -482,15 +646,6 @@ def sanitize_string(input):
return input
#-------------------------------------------------------------------------------
def sanitize_SQL_input(val):
if val is None:
return ''
if isinstance(val, str):
return val.replace("'", "_")
return val # Return non-string values as they are
#-------------------------------------------------------------------------------
# Function to normalize the string and remove diacritics
def normalize_string(text):
@@ -501,8 +656,29 @@ def normalize_string(text):
# Filter out diacritics and unwanted characters
return ''.join(c for c in normalized_text if unicodedata.category(c) != 'Mn')
# ------------------------------------------------------------------------------
# MAC and IP helper methods
#-------------------------------------------------------------------------------
# -------------------------------------------------------------------------------------------
def is_random_mac(mac: str) -> bool:
"""Determine if a MAC address is random, respecting user-defined prefixes not to mark as random."""
is_random = mac[1].upper() in ["2", "6", "A", "E"]
# Get prefixes from settings
prefixes = get_setting_value("UI_NOT_RANDOM_MAC")
# If detected as random, make sure it doesn't start with a prefix the user wants to exclude
if is_random:
for prefix in prefixes:
if mac.upper().startswith(prefix.upper()):
is_random = False
break
return is_random
# -------------------------------------------------------------------------------------------
def generate_mac_links (html, deviceUrl):
p = re.compile(r'(?:[0-9a-fA-F]:?){12}')
@@ -514,15 +690,6 @@ def generate_mac_links (html, deviceUrl):
return html
#-------------------------------------------------------------------------------
def extract_between_strings(text, start, end):
start_index = text.find(start)
end_index = text.find(end, start_index + len(start))
if start_index != -1 and end_index != -1:
return text[start_index + len(start):end_index]
else:
return ""
#-------------------------------------------------------------------------------
def extract_mac_addresses(text):
mac_pattern = r"([0-9A-Fa-f]{2}[:-][0-9A-Fa-f]{2}[:-][0-9A-Fa-f]{2}[:-][0-9A-Fa-f]{2}[:-][0-9A-Fa-f]{2}[:-][0-9A-Fa-f]{2})"
@@ -536,11 +703,6 @@ def extract_ip_addresses(text):
return ip_addresses
#-------------------------------------------------------------------------------
def generate_random_string(length):
characters = string.ascii_letters + string.digits
return ''.join(random.choice(characters) for _ in range(length))
# Helper function to determine if a MAC address is random
def is_random_mac(mac):
# Check if second character matches "2", "6", "A", "E" (case insensitive)
@@ -555,13 +717,14 @@ def is_random_mac(mac):
break
return is_random
#-------------------------------------------------------------------------------
# Helper function to calculate number of children
def get_number_of_children(mac, devices):
# Count children by checking devParentMAC for each device
return sum(1 for dev in devices if dev.get("devParentMAC", "").strip() == mac.strip())
#-------------------------------------------------------------------------------
# Function to convert IP to a long integer
def format_ip_long(ip_address):
try:
@@ -596,8 +759,6 @@ def add_json_list (row, list):
return list
#-------------------------------------------------------------------------------
# Checks if the object has a __dict__ attribute. If it does, it assumes that it's an instance of a class and serializes its attributes dynamically.
class NotiStrucEncoder(json.JSONEncoder):
@@ -607,19 +768,6 @@ class NotiStrucEncoder(json.JSONEncoder):
return obj.__dict__
return super().default(obj)
#-------------------------------------------------------------------------------
# Creates a JSON object from a DB row
def row_to_json(names, row):
rowEntry = {}
index = 0
for name in names:
rowEntry[name]= if_byte_then_to_str(row[name])
index += 1
return rowEntry
#-------------------------------------------------------------------------------
# Get language strings from plugin JSON
def collect_lang_strings(json, pref, stringSqlParams):
@@ -632,31 +780,6 @@ def collect_lang_strings(json, pref, stringSqlParams):
return stringSqlParams
#-------------------------------------------------------------------------------
# Misc
#-------------------------------------------------------------------------------
#-------------------------------------------------------------------------------
def print_table_schema(db, table):
sql = db.sql
sql.execute(f"PRAGMA table_info({table})")
result = sql.fetchall()
if not result:
mylog('none', f'[Schema] Table "{table}" not found or has no columns.')
return
mylog('debug', f'[Schema] Structure for table: {table}')
header = f"{'cid':<4} {'name':<20} {'type':<10} {'notnull':<8} {'default':<10} {'pk':<2}"
mylog('debug', header)
mylog('debug', '-' * len(header))
for row in result:
# row = (cid, name, type, notnull, dflt_value, pk)
line = f"{row[0]:<4} {row[1]:<20} {row[2]:<10} {row[3]:<8} {str(row[4]):<10} {row[5]:<2}"
mylog('debug', line)
#-------------------------------------------------------------------------------
def checkNewVersion():
mylog('debug', [f"[Version check] Checking if new version available"])
@@ -698,26 +821,9 @@ def checkNewVersion():
return newVersion
#-------------------------------------------------------------------------------
def initOrSetParam(db, parID, parValue):
sql = db.sql
sql.execute ("INSERT INTO Parameters(par_ID, par_Value) VALUES('"+str(parID)+"', '"+str(parValue)+"') ON CONFLICT(par_ID) DO UPDATE SET par_Value='"+str(parValue)+"' where par_ID='"+str(parID)+"'")
db.commitDB()
#-------------------------------------------------------------------------------
class json_obj:
def __init__(self, jsn, columnNames):
self.json = jsn
self.columnNames = columnNames
#-------------------------------------------------------------------------------
class noti_obj:
def __init__(self, json, text, html):
self.json = json
self.text = text
self.html = html

View File

@@ -12,7 +12,7 @@ import re
# Register NetAlertX libraries
import conf
from const import fullConfPath, applicationPath, fullConfFolder, default_tz
from helper import fixPermissions, collect_lang_strings, updateSubnets, initOrSetParam, isJsonObject, setting_value_to_python_type, timeNowTZ, get_setting_value, generate_random_string
from helper import fixPermissions, collect_lang_strings, updateSubnets, isJsonObject, setting_value_to_python_type, timeNowTZ, get_setting_value, generate_random_string
from app_state import updateState
from logger import mylog
from api import update_api
@@ -112,7 +112,7 @@ def update_or_append(settings_list, item_tuple, key):
#-------------------------------------------------------------------------------
def importConfigs (db, all_plugins):
def importConfigs (pm, db, all_plugins):
sql = db.sql
@@ -123,7 +123,7 @@ def importConfigs (db, all_plugins):
# this avoids time zone issues as we just compare the previous timestamp to the current time stamp
# rename settings that have changed names due to code cleanup and migration to plugins
renameSettings(config_file)
# renameSettings(config_file)
fileModifiedTime = os.path.getmtime(config_file)
@@ -134,7 +134,7 @@ def importConfigs (db, all_plugins):
if (fileModifiedTime == conf.lastImportedConfFile) and all_plugins is not None:
mylog('debug', ['[Import Config] skipping config file import'])
return all_plugins, False
return pm, all_plugins, False
# Header
updateState("Import config", showSpinner = True)
@@ -176,7 +176,7 @@ def importConfigs (db, all_plugins):
conf.API_TOKEN = ccd('API_TOKEN', 't_' + generate_random_string(20) , c_d, 'API token', '{"dataType": "string","elements": [{"elementType": "input","elementHasInputValue": 1,"elementOptions": [{ "cssClasses": "col-xs-12" }],"transformers": []},{"elementType": "button","elementOptions": [{ "getStringKey": "Gen_Generate" },{ "customParams": "API_TOKEN" },{ "onClick": "generateApiToken(this, 20)" },{ "cssClasses": "col-xs-12" }],"transformers": []}]}', '[]', 'General')
# UI
conf.UI_LANG = ccd('UI_LANG', 'English' , c_d, 'Language Interface', '{"dataType":"string", "elements": [{"elementType" : "select", "elementOptions" : [] ,"transformers": []}]}', "['English', 'German', 'Spanish', 'French', 'Norwegian', 'Russian', 'Italian (it_it)', 'Portuguese (pt_br)', 'Polish (pl_pl)', 'Chinese (zh_cn)', 'Turkish (tr_tr)', 'Czech (cs_cz)', 'Arabic (ar_ar)', 'Catalan (ca_ca)', 'Ukrainian (uk_ua)' ]", 'UI')
conf.UI_LANG = ccd('UI_LANG', 'English' , c_d, 'Language Interface', '{"dataType":"string", "elements": [{"elementType" : "select", "elementOptions" : [] ,"transformers": []}]}', "['English', 'German', 'Spanish', 'French', 'Norwegian', 'Russian', 'Italian (it_it)', 'Portuguese (pt_br)', 'Portuguese (pt_pt)', 'Polish (pl_pl)', 'Chinese (zh_cn)', 'Turkish (tr_tr)', 'Czech (cs_cz)', 'Arabic (ar_ar)', 'Catalan (ca_ca)', 'Ukrainian (uk_ua)' ]", 'UI')
# Init timezone in case it changed and handle invalid values
try:
@@ -275,6 +275,15 @@ def importConfigs (db, all_plugins):
# Save the user defined value into the object
set["value"] = v
# Now check for popupForm inside elements → elementOptions
elements = set.get("type", {}).get("elements", [])
for element in elements:
for option in element.get("elementOptions", []):
if "popupForm" in option:
for popup_entry in option["popupForm"]:
popup_pref = key + "_popupform_" + popup_entry.get("function", "")
stringSqlParams = collect_lang_strings(popup_entry, popup_pref, stringSqlParams)
# Collect settings related language strings
# Creates an entry with key, for example ARPSCAN_CMD_name
stringSqlParams = collect_lang_strings(set, pref + "_" + set["function"], stringSqlParams)
@@ -404,6 +413,7 @@ def importConfigs (db, all_plugins):
# run plugins that are modifying the config
pm = plugin_manager(db, all_plugins)
pm.clear_cache()
pm.run_plugin_scripts('before_config_save')
# Used to determine the next import
@@ -422,7 +432,7 @@ def importConfigs (db, all_plugins):
# front end app log loggging
write_notification(msg, 'info', timeNowTZ())
return all_plugins, True
return pm, all_plugins, True

View File

@@ -7,7 +7,6 @@ import time
import logging
# NetAlertX imports
import conf
from const import *
@@ -39,87 +38,80 @@ debugLevels = [
# use the LOG_LEVEL from the config, may be overridden
currentLevel = conf.LOG_LEVEL
# tracking log levels
setLvl = 0
reqLvl = 0
#-------------------------------------------------------------------------------
# Queue for log messages
log_queue = queue.Queue(maxsize=1000) # Increase size to handle spikes
log_thread = None # Will hold the thread reference
#-------------------------------------------------------------------------------
class Logger:
def __init__(self, LOG_LEVEL):
global currentLevel
currentLevel = LOG_LEVEL
conf.LOG_LEVEL = currentLevel
# Automatically set up custom logging handler
self.setup_logging()
def setup_logging(self):
root_logger = logging.getLogger()
# Clear existing handlers to prevent duplicates
if root_logger.hasHandlers():
root_logger.handlers.clear()
# Create the custom handler
my_log_handler = MyLogHandler()
# my_log_handler.setLevel(custom_to_logging_levels.get(currentLevel, logging.NOTSET))
# Optional: Add a formatter for consistent log message format
# formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')
formatter = logging.Formatter('%(message)s', datefmt='%H:%M:%S')
my_log_handler.setFormatter(formatter)
# Attach the handler to the root logger
root_logger.addHandler(my_log_handler)
root_logger.setLevel(custom_to_logging_levels.get(currentLevel, logging.NOTSET))
# for python logging
# Custom logging handler
class MyLogHandler(logging.Handler):
def emit(self, record):
log_entry = self.format(record)
log_queue.put(log_entry)
def mylog(requestedDebugLevel, n):
global setLvl, reqLvl
#-------------------------------------------------------------------------------
# Logger class
class Logger:
def __init__(self, LOG_LEVEL):
global currentLevel
currentLevel = LOG_LEVEL
conf.LOG_LEVEL = currentLevel
# Get debug urgency/relative weight
for lvl in debugLevels:
if currentLevel == lvl[0]:
setLvl = lvl[1]
if requestedDebugLevel == lvl[0]:
reqLvl = lvl[1]
# Numeric weights
self.setLvl = self._to_num(LOG_LEVEL)
self.reqLvl = None
if reqLvl <= setLvl:
file_print (*n)
# Setup Python logging
self.setup_logging()
def _to_num(self, level_str):
for lvl in debugLevels:
if level_str == lvl[0]:
return lvl[1]
return None
def setup_logging(self):
root_logger = logging.getLogger()
if root_logger.hasHandlers():
root_logger.handlers.clear()
my_log_handler = MyLogHandler()
formatter = logging.Formatter('%(message)s', datefmt='%H:%M:%S')
my_log_handler.setFormatter(formatter)
root_logger.addHandler(my_log_handler)
root_logger.setLevel(custom_to_logging_levels.get(currentLevel, logging.NOTSET))
def mylog(self, requestedDebugLevel, *args):
self.reqLvl = self._to_num(requestedDebugLevel)
if self.reqLvl is not None and self.reqLvl <= self.setLvl:
file_print(*args)
def isAbove(self, requestedDebugLevel):
reqLvl = self._to_num(requestedDebugLevel)
return reqLvl is not None and self.setLvl >= reqLvl
#-------------------------------------------------------------------------------
# Queue for log messages
log_queue = queue.Queue(maxsize=1000) # Increase size to handle spikes
# Dedicated thread for writing logs
log_thread = None # Will hold the thread reference
def log_writer():
buffer = []
while True:
try:
log_entry = log_queue.get(timeout=1) # Wait for 1 second for logs
if log_entry is None: # Graceful exit signal
log_entry = log_queue.get(timeout=1)
if log_entry is None:
break
buffer.append(log_entry)
if len(buffer) >= 10: # Write in batches of 10
if len(buffer) >= 10:
with open(logPath + "/app.log", 'a') as log_file:
log_file.write('\n'.join(buffer) + '\n')
buffer.clear()
except queue.Empty:
# Flush buffer periodically if no new logs
if buffer:
with open(logPath + "/app.log", 'a') as log_file:
log_file.write('\n'.join(buffer) + '\n')
buffer.clear()
#-------------------------------------------------------------------------------
# Function to start the log writer thread if it doesn't exist
def start_log_writer_thread():
global log_thread
if log_thread is None or not log_thread.is_alive():
@@ -128,40 +120,39 @@ def start_log_writer_thread():
#-------------------------------------------------------------------------------
def file_print(*args):
result = timeNowTZ().strftime('%H:%M:%S') + ' '
for arg in args:
result += str(arg)
result = timeNowTZ().strftime('%H:%M:%S') + ' '
for arg in args:
result += str(arg)
logging.log(custom_to_logging_levels.get(currentLevel, logging.NOTSET), result) # Forward to Python's logging system
logging.log(custom_to_logging_levels.get(currentLevel, logging.NOTSET), result)
print(result)
# Ensure the log writer thread is running
start_log_writer_thread()
#-------------------------------------------------------------------------------
def append_file_binary(file_path, input_data):
with open(file_path, 'ab') as file:
if isinstance(input_data, str):
input_data = input_data.encode('utf-8') # Encode string as bytes
input_data = input_data.encode('utf-8')
file.write(input_data)
#-------------------------------------------------------------------------------
def logResult(stdout, stderr):
if stderr is not None:
append_file_binary(logPath + '/stderr.log', stderr)
if stdout is not None:
append_file_binary(logPath + '/stdout.log', stdout)
#-------------------------------------------------------------------------------
def append_line_to_file(pPath, pText):
# append the line using the correct python version
if sys.version_info < (3, 0):
file = io.open(pPath, mode='a', encoding='utf-8')
file.write(pText.decode('unicode_escape'))
file.close()
file.close()
else:
file = open(pPath, 'a', encoding='utf-8')
file = open(pPath, 'a', encoding='utf-8')
file.write(pText)
file.close()
file.close()
#-------------------------------------------------------------------------------
# Create default logger instance and backward-compatible global mylog
logger = Logger(conf.LOG_LEVEL)
mylog = logger.mylog

View File

@@ -24,43 +24,46 @@ from helper import generate_mac_links, removeDuplicateNewLines, timeNowTZ, get_f
NOTIFICATION_API_FILE = apiPath + 'user_notifications.json'
# Show Frontend User Notification
def write_notification(content, level, timestamp):
def write_notification(content, level='alert', timestamp=None):
# Generate GUID
guid = str(uuid.uuid4())
if timestamp is None:
timestamp = timeNowTZ()
# Prepare notification dictionary
notification = {
'timestamp': str(timestamp),
'guid': guid,
'read': 0,
'level': level,
'content': content
}
# Generate GUID
guid = str(uuid.uuid4())
# If file exists, load existing data, otherwise initialize as empty list
if os.path.exists(NOTIFICATION_API_FILE):
with open(NOTIFICATION_API_FILE, 'r') as file:
# Check if the file object is of type _io.TextIOWrapper
if isinstance(file, _io.TextIOWrapper):
file_contents = file.read() # Read file contents
if file_contents == '':
file_contents = '[]' # If file is empty, initialize as empty list
# Prepare notification dictionary
notification = {
'timestamp': str(timestamp),
'guid': guid,
'read': 0,
'level': level,
'content': content
}
# mylog('debug', ['[Notification] User Notifications file: ', file_contents])
notifications = json.loads(file_contents) # Parse JSON data
else:
mylog('none', '[Notification] File is not of type _io.TextIOWrapper')
notifications = []
else:
notifications = []
# If file exists, load existing data, otherwise initialize as empty list
if os.path.exists(NOTIFICATION_API_FILE):
with open(NOTIFICATION_API_FILE, 'r') as file:
# Check if the file object is of type _io.TextIOWrapper
if isinstance(file, _io.TextIOWrapper):
file_contents = file.read() # Read file contents
if file_contents == '':
file_contents = '[]' # If file is empty, initialize as empty list
# Append new notification
notifications.append(notification)
# mylog('debug', ['[Notification] User Notifications file: ', file_contents])
notifications = json.loads(file_contents) # Parse JSON data
else:
mylog('none', '[Notification] File is not of type _io.TextIOWrapper')
notifications = []
else:
notifications = []
# Write updated data back to file
with open(NOTIFICATION_API_FILE, 'w') as file:
json.dump(notifications, file, indent=4)
# Append new notification
notifications.append(notification)
# Write updated data back to file
with open(NOTIFICATION_API_FILE, 'w') as file:
json.dump(notifications, file, indent=4)
# Trim notifications
def remove_old(keepNumberOfEntries):

View File

@@ -27,9 +27,29 @@ class plugin_manager:
self.db = db
self.all_plugins = all_plugins
# object cache of settings and schedules for faster lookups
self._cache = {}
self._build_cache()
# Make sure log level is initialized correctly
Logger(get_setting_value('LOG_LEVEL'))
def _build_cache(self):
"""Build a cache of settings and schedules for faster lookups."""
self._cache["settings"] = {
p["unique_prefix"]: {
"RUN": get_plugin_setting_obj(p, "RUN"),
"CMD": get_plugin_setting_obj(p, "CMD"),
}
for p in self.all_plugins
}
self._cache["schedules"] = {s.service: s for s in conf.mySchedules}
def clear_cache(self):
"""Force rebuild of the cache (e.g. after config reload)."""
self._cache = {}
self._build_cache()
#-------------------------------------------------------------------------------
def run_plugin_scripts(self, runType):
@@ -43,34 +63,65 @@ class plugin_manager:
shouldRun = False
prefix = plugin["unique_prefix"]
set = get_plugin_setting_obj(plugin, "RUN")
# 🔹 Lookup RUN setting from cache instead of calling get_plugin_setting_obj each time
run_setting = self._cache["settings"].get(prefix, {}).get("RUN")
# set = get_plugin_setting_obj(plugin, "RUN")
# mylog('debug', [f'[run_plugin_scripts] plugin: {plugin}'])
# mylog('debug', [f'[run_plugin_scripts] set: {set}'])
if set != None and set['value'] == runType:
# if set != None and set['value'] == runType:
# if runType != "schedule":
# shouldRun = True
# elif runType == "schedule":
# # run if overdue scheduled time
# # check schedules if any contains a unique plugin prefix matching the current plugin
# for schd in conf.mySchedules:
# if schd.service == prefix:
# # Check if schedule overdue
# shouldRun = schd.runScheduleCheck()
if run_setting != None and run_setting['value'] == runType:
if runType != "schedule":
shouldRun = True
elif runType == "schedule":
# run if overdue scheduled time
# check schedules if any contains a unique plugin prefix matching the current plugin
for schd in conf.mySchedules:
if schd.service == prefix:
# Check if schedule overdue
shouldRun = schd.runScheduleCheck()
elif runType == "schedule":
# run if overdue scheduled time
# 🔹 Lookup schedule from cache instead of scanning conf.mySchedules
schd = self._cache["schedules"].get(prefix)
if schd:
# Check if schedule overdue
shouldRun = schd.runScheduleCheck()
# if shouldRun:
# # Header
# updateState(f"Plugin: {prefix}")
# print_plugin_info(plugin, ['display_name'])
# mylog('debug', ['[Plugins] CMD: ', get_plugin_setting_obj(plugin, "CMD")["value"]])
# execute_plugin(self.db, self.all_plugins, plugin)
# # update last run time
# if runType == "schedule":
# for schd in conf.mySchedules:
# if schd.service == prefix:
# # note the last time the scheduled plugin run was executed
# schd.last_run = timeNowTZ()
if shouldRun:
# Header
updateState(f"Plugin: {prefix}")
print_plugin_info(plugin, ['display_name'])
mylog('debug', ['[Plugins] CMD: ', get_plugin_setting_obj(plugin, "CMD")["value"]])
# 🔹 CMD also retrieved from cache
cmd_setting = self._cache["settings"].get(prefix, {}).get("CMD")
mylog('debug', ['[Plugins] CMD: ', cmd_setting["value"] if cmd_setting else None])
execute_plugin(self.db, self.all_plugins, plugin)
# update last run time
# update last run time
if runType == "schedule":
for schd in conf.mySchedules:
if schd.service == prefix:
# note the last time the scheduled plugin run was executed
schd.last_run = timeNowTZ()
schd = self._cache["schedules"].get(prefix)
if schd:
# note the last time the scheduled plugin run was executed
schd.last_run = timeNowTZ()
#===============================================================================
# Handling of user initialized front-end events

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