Compare commits

..

57 Commits

Author SHA1 Message Date
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
jokob-sk
4712a2ff29 css fixes, nav menu update, searchable devParentNodeMac
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-06 09:15:45 +10:00
jokob-sk
f9179a1e89 safe device name if number #1131 2025-08-06 07:20:04 +10:00
jokob-sk
a6df204721 github timeout #1124, css fixes, change button on LOADED_PLUGINS
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-05 21:32:35 +10:00
jokob-sk
101189ae7c devParentNodeMac chips in devices list 2025-08-05 20:54:28 +10:00
jokob-sk
f25c012fbe external ip rework #1124
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-05 14:42:00 +10:00
jokob-sk
868a85d84c Merge branch 'main' of https://github.com/jokob-sk/NetAlertX 2025-08-05 14:03:18 +10:00
jokob-sk
771dd4b176 docs 2025-08-05 14:02:48 +10:00
Hosted Weblate
ed4d3bf17c Merge branch 'origin/main' into Weblate. 2025-08-05 03:27:56 +00:00
Massimo Pissarello
7c728fbe36 Translated using Weblate (Italian)
Currently translated at 100.0% (761 of 761 strings)

Translation: NetAlertX/core
Translate-URL: https://hosted.weblate.org/projects/pialert/core/it/
2025-08-05 03:27:56 +00:00
jokob-sk
4ff9d01ef5 heuristics docs 2025-08-05 13:27:30 +10:00
jokob-sk
1bce2e80e8 replace external IP check AJAX #1124
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-05 08:15:49 +10:00
jokob-sk
1556d74406 Merge branch 'main' of https://github.com/jokob-sk/NetAlertX 2025-08-05 08:06:48 +10:00
jokob-sk
9b3947cc90 device tools init loading #1130 2025-08-05 08:06:42 +10:00
Sylvain Pichon
18b0309ac4 Translated using Weblate (French)
Currently translated at 100.0% (761 of 761 strings)

Translation: NetAlertX/core
Translate-URL: https://hosted.weblate.org/projects/pialert/core/fr/
2025-08-04 19:02:09 +00:00
jokob-sk
0afd4ae115 prometheus metrics docs
Some checks failed
docker / docker_dev (push) Has been cancelled
Code checks / check-url-paths (push) Has been cancelled
Deploy MkDocs / deploy (push) Has been cancelled
2025-08-04 18:13:35 +10:00
jokob-sk
09e360c746 prometheus metrics endpoint 2025-08-04 15:12:51 +10:00
jokob-sk
5dbe79ba2f Merge branch 'main' of https://github.com/jokob-sk/NetAlertX 2025-08-04 13:25:23 +10:00
jokob-sk
779707761f heuristics refactor #1129 2025-08-04 13:25:17 +10:00
Hosted Weblate
16992bb2bd Merge branch 'origin/main' into 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
2025-08-03 15:08:18 +02:00
Максим Горпиніч
3374f83255 Translated using Weblate (Ukrainian)
Currently translated at 100.0% (761 of 761 strings)

Translation: NetAlertX/core
Translate-URL: https://hosted.weblate.org/projects/pialert/core/uk/
2025-08-03 15:08:17 +02:00
jokob-sk
8f420a14cd fix 2025-08-03 23:06:43 +10:00
jokob-sk
57024c0cb1 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-03 09:37:36 +10:00
jokob-sk
db7fb825fe Copy to clipboard IP 2025-08-03 09:37:18 +10:00
Hosted Weblate
49e8c6a4f2 Merge branch 'origin/main' into Weblate. 2025-08-02 22:40:40 +00:00
Максим Горпиніч
66bf4241b2 Translated using Weblate (Ukrainian)
Currently translated at 100.0% (760 of 760 strings)

Translation: NetAlertX/core
Translate-URL: https://hosted.weblate.org/projects/pialert/core/uk/
2025-08-02 22:40:39 +00:00
Massimo Pissarello
76a5dda553 Translated using Weblate (Italian)
Currently translated at 100.0% (760 of 760 strings)

Translation: NetAlertX/core
Translate-URL: https://hosted.weblate.org/projects/pialert/core/it/
2025-08-02 22:40:38 +00:00
Sylvain Pichon
6393aa7f2c Translated using Weblate (French)
Currently translated at 100.0% (760 of 760 strings)

Translation: NetAlertX/core
Translate-URL: https://hosted.weblate.org/projects/pialert/core/fr/
2025-08-02 22:40:37 +00:00
Ettore Atalan
c5f938113f Translated using Weblate (German)
Currently translated at 81.3% (618 of 760 strings)

Translation: NetAlertX/core
Translate-URL: https://hosted.weblate.org/projects/pialert/core/de/
2025-08-02 22:40:35 +00:00
jokob-sk
dac7eaba6d localized spinner support 2025-08-03 08:40:09 +10:00
jokob-sk
35e6059068 Merge branch 'main' of https://github.com/jokob-sk/NetAlertX 2025-08-03 07:44:17 +10:00
jokob-sk
afebc8dc39 systeminfo refactor #1124 2025-08-03 07:44:11 +10:00
Hosted Weblate
34151a86b1 Merge branch 'origin/main' into 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
2025-08-01 21:57:38 +00:00
Massimo Pissarello
72d6934345 Translated using Weblate (Italian)
Currently translated at 100.0% (759 of 759 strings)

Translation: NetAlertX/core
Translate-URL: https://hosted.weblate.org/projects/pialert/core/it/
2025-08-01 21:57:37 +00:00
jokob-sk
f5f7031030 copy icon issue #1128 2025-08-02 07:56:57 +10:00
jokob-sk
ffccca9424 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-01 08:15:38 +10:00
jokob-sk
3f5ae334a2 new device button #1126 2025-08-01 08:15:32 +10:00
Максим Горпиніч
bb45c4d345 Translated using Weblate (Ukrainian)
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% (759 of 759 strings)

Translation: NetAlertX/core
Translate-URL: https://hosted.weblate.org/projects/pialert/core/uk/
2025-07-31 16:02:16 +02:00
Sylvain Pichon
bad3c76de9 Translated using Weblate (French)
Currently translated at 100.0% (759 of 759 strings)

Translation: NetAlertX/core
Translate-URL: https://hosted.weblate.org/projects/pialert/core/fr/
2025-07-31 16:02:14 +02:00
94 changed files with 7336 additions and 1899 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 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 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

@@ -24,7 +24,7 @@ LOADED_PLUGINS=['ARPSCAN', 'AVAHISCAN', 'CSVBCKP','DBCLNP', 'DIGSCAN', 'INTRNT',
DAYS_TO_KEEP_EVENTS=90
# Used for generating links in emails. Make sure not to add a trailing slash!
REPORT_DASHBOARD_URL='http://127.0.0.1'
REPORT_DASHBOARD_URL='update_REPORT_DASHBOARD_URL_setting'
# Make sure at least these scanners are enabled for new installs, other defaults are taken from the config.json
INTRNT_RUN='schedule'

200
back/device_heuristics_rules.json Executable file
View File

@@ -0,0 +1,200 @@
[
{
"dev_type": "Gateway",
"icon_html": "<i class=\"fa fa-globe\"></i>",
"matching_pattern": [
{ "mac_prefix": "INTERNET", "vendor": "" }
],
"name_pattern": []
},
{
"dev_type": "Access Point",
"icon_html": "<i class=\"fa fa-network-wired\"></i>",
"matching_pattern": [
{ "mac_prefix": "74ACB9", "vendor": "Ubiquiti" },
{ "mac_prefix": "002468", "vendor": "Cisco" },
{ "mac_prefix": "F4F5D8", "vendor": "TP-Link" },
{ "mac_prefix": "F88E85", "vendor": "Netgear" }
],
"name_pattern": ["router", "gateway", "ap", "access point", "access-point", "switch"]
},
{
"dev_type": "Phone",
"icon_html": "<i class=\"fa-brands fa-apple\"></i>",
"matching_pattern": [
{ "mac_prefix": "001A79", "vendor": "Apple" },
{ "mac_prefix": "B0BE83", "vendor": "Samsung" },
{ "mac_prefix": "BC926B", "vendor": "Motorola" }
],
"name_pattern": ["iphone", "ipad", "pixel", "galaxy", "redmi"]
},
{
"dev_type": "Phone",
"icon_html": "<i class=\"fa-solid fa-mobile\"></i>",
"matching_pattern": [
],
"name_pattern": ["android","samsung"]
},
{
"dev_type": "Tablet",
"icon_html": "<i class=\"fa fa-tablet\"></i>",
"matching_pattern": [
{ "mac_prefix": "001B63", "vendor": "Apple" },
{ "mac_prefix": "BC4C4C", "vendor": "Samsung" }
],
"name_pattern": ["tablet", "pad"]
},
{
"dev_type": "IoT",
"icon_html": "<i class=\"fa-brands fa-raspberry-pi\"></i>",
"matching_pattern": [
{ "mac_prefix": "B827EB", "vendor": "Raspberry Pi" },
{ "mac_prefix": "DCA632", "vendor": "Raspberry Pi" }
],
"name_pattern": ["raspberry", "pi"]
},
{
"dev_type": "IoT",
"icon_html": "<i class=\"fa-solid fa-microchip\"></i>",
"matching_pattern": [
{ "mac_prefix": "840D8E", "vendor": "Espressif" },
{ "mac_prefix": "ECFABC", "vendor": "Espressif" },
{ "mac_prefix": "7C9EBD", "vendor": "Espressif" }
],
"name_pattern": ["raspberry", "pi"]
},
{
"dev_type": "Desktop",
"icon_html": "<i class=\"fa fa-desktop\"></i>",
"matching_pattern": [
{ "mac_prefix": "001422", "vendor": "Dell" },
{ "mac_prefix": "001874", "vendor": "Lenovo" },
{ "mac_prefix": "00E04C", "vendor": "Hewlett Packard" }
],
"name_pattern": ["desktop", "pc", "computer"]
},
{
"dev_type": "Laptop",
"icon_html": "<i class=\"fa fa-laptop\"></i>",
"matching_pattern": [
{ "mac_prefix": "3C0754", "vendor": "HP" },
{ "mac_prefix": "0017A4", "vendor": "Dell" },
{ "mac_prefix": "F4CE46", "vendor": "Lenovo" },
{ "mac_prefix": "409F38", "vendor": "Acer" }
],
"name_pattern": ["macbook", "imac", "laptop", "notebook"]
},
{
"dev_type": "Server",
"icon_html": "<i class=\"fa fa-server\"></i>",
"matching_pattern": [
{ "mac_prefix": "001CBF", "vendor": "Supermicro" },
{ "mac_prefix": "002186", "vendor": "Dell" },
{ "mac_prefix": "D02788", "vendor": "Hewlett Packard" },
{ "mac_prefix": "002590", "vendor": "IBM" }
],
"name_pattern": ["server", "nas"]
},
{
"dev_type": "VM",
"icon_html": "<i class=\"fa fa-server\"></i>",
"matching_pattern": [
{ "mac_prefix": "525400", "vendor": "QEMU" },
{ "mac_prefix": "005056", "vendor": "VMware" },
{ "mac_prefix": "000C29", "vendor": "VMware" },
{ "mac_prefix": "000569", "vendor": "VMware" },
{ "mac_prefix": "00163E", "vendor": "Xen" },
{ "mac_prefix": "080027", "vendor": "VirtualBox" }
]
},
{
"dev_type": "TV",
"icon_html": "<i class=\"fa fa-tv\"></i>",
"matching_pattern": [
{ "mac_prefix": "0013CE", "vendor": "Samsung" },
{ "mac_prefix": "0017C8", "vendor": "LG" },
{ "mac_prefix": "D46E0E", "vendor": "Sony" }
],
"name_pattern": ["tv", "television", "smarttv"]
},
{
"dev_type": "Gaming Console",
"icon_html": "<i class=\"fa fa-gamepad\"></i>",
"matching_pattern": [
{ "mac_prefix": "001FA7", "vendor": "Sony" },
{ "mac_prefix": "7C04D0", "vendor": "Nintendo" },
{ "mac_prefix": "EC26CA", "vendor": "Sony" }
],
"name_pattern": ["playstation", "xbox"]
},
{
"dev_type": "Camera",
"icon_html": "<i class=\"fa fa-camera\"></i>",
"matching_pattern": [
{ "mac_prefix": "A45E60", "vendor": "Hikvision" },
{ "mac_prefix": "00408C", "vendor": "Axis" },
{ "mac_prefix": "00156D", "vendor": "Amcrest" },
{ "mac_prefix": "AC9E17", "vendor": "Reolink" }
],
"name_pattern": ["camera", "cam", "webcam"]
},
{
"dev_type": "Smart Speaker",
"icon_html": "<i class=\"fa fa-volume-up\"></i>",
"matching_pattern": [
{ "mac_prefix": "44650D", "vendor": "Amazon" },
{ "mac_prefix": "74ACB9", "vendor": "Google" }
],
"name_pattern": ["echo", "alexa", "dot"]
},
{
"dev_type": "Router",
"icon_html": "<i class=\"fa fa-random\"></i>",
"matching_pattern": [
{ "mac_prefix": "000C29", "vendor": "Cisco" },
{ "mac_prefix": "00155D", "vendor": "MikroTik" }
],
"name_pattern": ["router", "gateway", "ap", "access point", "access-point"],
"ip_pattern": [
"^192\\.168\\.[0-1]\\.1$",
"^10\\.0\\.0\\.1$"
]
},
{
"dev_type": "Smart Light",
"icon_html": "<i class=\"fa fa-lightbulb\"></i>",
"matching_pattern": [],
"name_pattern": ["hue", "lifx", "bulb"]
},
{
"dev_type": "Smart Home",
"icon_html": "<i class=\"fa fa-house\"></i>",
"matching_pattern": [],
"name_pattern": ["google", "chromecast", "nest"]
},
{
"dev_type": "Smartwatch",
"icon_html": "<i class=\"fa fa-watch\"></i>",
"matching_pattern": [],
"name_pattern": ["watch", "wear"]
},
{
"dev_type": "Printer",
"icon_html": "<i class=\"fa fa-print\"></i>",
"matching_pattern": [],
"name_pattern": ["printer", "print"]
},
{
"dev_type": "Security Device",
"icon_html": "<i class=\"fa fa-shield-alt\"></i>",
"matching_pattern": [],
"name_pattern": ["doorbell", "lock", "security"]
},
{
"dev_type": "Smart Light",
"icon_html": "<i class=\"fa-solid fa-lightbulb\"></i>",
"matching_pattern": [
],
"name_pattern": ["light","bulb"]
}
]

View File

@@ -59,6 +59,9 @@ services:
- ${DEV_LOCATION}/front/presence.php:/app/front/presence.php
- ${DEV_LOCATION}/front/settings.php:/app/front/settings.php
- ${DEV_LOCATION}/front/systeminfo.php:/app/front/systeminfo.php
- ${DEV_LOCATION}/front/systeminfoNetwork.php:/app/front/systeminfoNetwork.php
- ${DEV_LOCATION}/front/systeminfoServer.php:/app/front/systeminfoServer.php
- ${DEV_LOCATION}/front/systeminfoStorage.php:/app/front/systeminfoStorage.php
- ${DEV_LOCATION}/front/cloud_services.php:/app/front/cloud_services.php
- ${DEV_LOCATION}/front/report.php:/app/front/report.php
- ${DEV_LOCATION}/front/workflows.php:/app/front/workflows.php

View File

@@ -221,6 +221,112 @@ Example JSON of the `table_devices.json` endpoint with two Devices (database row
```
## 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.

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.

114
docs/DEVICE_HEURISTICS.md Executable file
View File

@@ -0,0 +1,114 @@
# Device Heuristics: Icon and Type Guessing
This module is responsible for inferring the most likely **device type** and **icon** based on minimal identifying data like MAC address, vendor, IP, or device name.
It does this using a set of heuristics defined in an external JSON rules file, which it evaluates **in priority order**.
>[!NOTE]
> You can find the full source code of the heuristics module in the `device_heuristics.py` file.
---
## JSON Rule Format
Rules are defined in a file called `device_heuristics_rules.json` (located under `/back`), structured like:
```json
[
{
"dev_type": "Phone",
"icon_html": "<i class=\"fa-brands fa-apple\"></i>",
"matching_pattern": [
{ "mac_prefix": "001A79", "vendor": "Apple" }
],
"name_pattern": ["iphone", "pixel"]
}
]
```
>[!NOTE]
> Feel free to raise a PR in case you'd like to add any rules into the `device_heuristics_rules.json` file. Please place new rules into the correct position and consider the priority of already available rules.
### Supported fields:
| Field | Type | Description |
| ------------------ | -------------------- | --------------------------------------------------------------- |
| `dev_type` | `string` | Type to assign if rule matches (e.g. `"Gateway"`, `"Phone"`) |
| `icon_html` | `string` | Icon (HTML string) to assign if rule matches. Encoded to base64 at load time. |
| `matching_pattern` | `array` | List of `{ mac_prefix, vendor }` objects for first strict and then loose matching |
| `name_pattern` | `array` *(optional)* | List of lowercase substrings (used with regex) |
| `ip_pattern` | `array` *(optional)* | Regex patterns to match IPs |
**Order in this array defines priority** — rules are checked top-down and short-circuit on first match.
---
## Matching Flow (in Priority Order)
The function `guess_device_attributes(...)` runs a series of matching functions in strict order:
1. MAC + Vendor → `match_mac_and_vendor()`
2. Vendor only → `match_vendor()`
3. Name pattern → `match_name()`
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.
```python
# Still considered a match attempt if current values are defaults
if (not type_ or type_ == default_type) or (not icon or icon == default_icon):
type_, icon = match_ip(ip, default_type, default_icon)
```
In other words: if the type or icon is still `"unknown"` (or matches the default), the system assumes the match isnt final — and keeps looking. It stops only when both values are non-default (defaults are defined in the `NEWDEV_devIcon` and `NEWDEV_devType` settings).
---
## Match Behavior (per function)
These functions are executed in the following order:
### `match_mac_and_vendor(mac_clean, vendor, ...)`
* Looks for MAC prefix **and** vendor substring match
* Most precise
* Stops as soon as a match is found
### `match_vendor(vendor, ...)`
* Falls back to substring match on vendor only
* Ignores rules where `mac_prefix` is present (ensures this is really a fallback)
### `match_name(name, ...)`
* Lowercase name is compared against all `name_pattern` values using regex
* Good for user-assigned labels (e.g. "AP Office", "iPhone")
### `match_ip(ip, ...)`
* If IP is present and matches regex patterns under any rule, it returns that type/icon
* Usually used for gateways or local IP ranges
---
## Icons
* Each rule can define an `icon_html`, which is converted to a `icon_base64` on load
* If missing, it falls back to the passed-in `default_icon` (`NEWDEV_devIcon` setting)
* If a match is found but icon is still blank, default is used
**TL;DR:** Type and icon must both be matched. If only one is matched, the other falls back to the default.
---
## Priority Mechanics
* JSON rules are evaluated **top-to-bottom**
* Matching is **first-hit wins** — no scoring, no weights
* Rules that are more specific (e.g. exact MAC prefixes) should be listed earlier

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

File diff suppressed because it is too large Load Diff

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
@@ -387,6 +388,16 @@ body
margin-bottom: 0px;
}
.plugin-content #tabs-location .nav-tabs-custom > .nav-tabs > li
{
display: contents;
}
.plugin-content .left-nav
{
display: contents;
}
.pa-small-box-2 .inner h3 {
margin-left: 0em;
margin-bottom: 1.3em;
@@ -594,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
----------------------------------------------------------------------------- */
@@ -1411,6 +1436,7 @@ input[readonly] {
.iconPreview svg{
min-width: 20px;
max-width: 20px;
margin-bottom: -3px;
}
@@ -1489,7 +1515,7 @@ input[readonly] {
}
#tableDevicesBox td svg, #tableDevicesBox td i{
height: 1.5em !important;
height: 1em !important;
}
#TileCards .tile .inner
@@ -1649,6 +1675,21 @@ input[readonly] {
pointer-events: none;
}
.custom-badge a
{
color: #fff !important;
font-size: 14px;
}
.custom-badge
{
border: 1px solid #aaa;
border-radius: 4px;
border-style: solid;
padding: 0 5px;
font-size: 14px;
display: inline-block;
}
#deviceDetailsEdit .form-control
{
min-height: 42px;
@@ -1700,6 +1741,16 @@ input[readonly] {
width: 92%;
}
#modal-ok
{
z-index: 1051; /*highest priority*/
}
#modal-form-plc
{
display: grid;
}
/* ----------------------------------------------------------------- */
/* NETWORK page */
/* ----------------------------------------------------------------- */
@@ -2093,10 +2144,10 @@ input[readonly] {
#loadingSpinner {
position: fixed;
z-index: 1000;
top: 0;
left: 0;
width: 100%;
height: 100%;
/* top: 0; */
/* left: 0; */
/* width: 100%; */
/* height: 100%; */
opacity: 0;
transition: opacity 0.3s ease-in-out;
pointer-events: none;
@@ -2114,16 +2165,16 @@ input[readonly] {
pointer-events: auto;
}
.pa_semitransparent-panel {
.nax_semitransparent-panel {
position: absolute;
width: 100%;
height: 100%;
background-color: #fff;
opacity: 0.8;
opacity: 0.5;
z-index: 99;
}
.pa_spinner {
.nax_spinner {
position: absolute;
top: 100px;
left: 50%;
@@ -2160,9 +2211,10 @@ input[readonly] {
}
.pia-top-left-logo
.top-left-logo
{
height:50px;
height:35px;
width:35px;
}
/* -----------------------------------------------------------------------------

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;
@@ -744,7 +750,7 @@ table.dataTable tbody tr.selected, table.dataTable tbody tr .selected
top: 0.01em;
font-size: 3.25em;
}
.pa_semitransparent-panel{
.nax_semitransparent-panel{
background-color: #000 !important;
}

View File

@@ -20,6 +20,7 @@
--color-yellow: #f39c12;
--color-red: #dd4b39;
--color-gray: #8c8c8c;
--color-white: #fff;
}
:root {
@@ -421,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;
@@ -746,7 +753,7 @@
top: 0.01em;
font-size: 3.25em;
}
.pa_semitransparent-panel{
.nax_semitransparent-panel{
background-color: #000 !important;
}
@@ -793,5 +800,5 @@
.btn:hover
{
color: var(--color-gray);
color: var(--color-white);
}

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>
@@ -123,7 +123,7 @@
</div>
</ul>
<div class="tab-content" style="min-height: 430px;">
<div class="tab-content spinnerTarget" style="min-height: 430px;">
<!-- tab page 1 ------------------------------------------------------------ -->
@@ -497,7 +497,7 @@ function updateDevicePageName(mac) {
let owner = getDevDataByMac(mac, "devOwner");
// If data is missing, re-cache and retry once
if (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(() => {
@@ -541,7 +541,7 @@ function updateDevicePageName(mac) {
window.onload = function async()
{
initializeTabs();
// initializeTabs();
updateChevrons(mac);
updateDevicePageName(mac);

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

@@ -443,6 +443,37 @@
}
// init first time
initNmapButtons();
initCopyFromDevice();
// -----------------------------------------------------------
var toolsPageInitialized = false;
function initDeviceToolsPage()
{
// Only proceed if .panTools is visible
if (!$('#panTools:visible').length) {
return; // exit early if nothing is visible
}
// init page once
if (toolsPageInitialized) return;
toolsPageInitialized = true;
initNmapButtons();
initCopyFromDevice();
hideSpinner();
}
// -----------------------------------------------------------------------------
// Recurring function to monitor the URL and reinitialize if needed
function deviceToolsPageUpdater() {
initDeviceToolsPage();
// Run updater again after delay
setTimeout(deviceToolsPageUpdater, 200);
}
// start updater
deviceToolsPageUpdater();
</script>

View File

@@ -503,36 +503,36 @@ function collectFilters() {
function mapColumnIndexToFieldName(index, tableColumnVisible) {
// the order is important, don't change it!
const columnNames = [
"devName",
"devOwner",
"devType",
"devIcon",
"devFavorite",
"devGroup",
"devFirstConnection",
"devLastConnection",
"devLastIP",
"devIsRandomMac", // resolved on the fly
"devStatus", // resolved on the fly
"devMac",
"devIpLong", //formatIPlong(device.devLastIP) || "", // IP orderable
"rowid",
"devParentMAC",
"devParentChildrenCount", // resolved on the fly
"devLocation",
"devVendor",
"devParentPort",
"devGUID",
"devSyncHubNode",
"devSite",
"devSSID",
"devSourcePlugin",
"devPresentLastScan",
"devAlertDown",
"devCustomProps",
"devFQDN",
"devParentRelType",
"devReqNicsOnline"
"devName", // 0
"devOwner", // 1
"devType", // 2
"devIcon", // 3
"devFavorite", // 4
"devGroup", // 5
"devFirstConnection", // 6
"devLastConnection", // 7
"devLastIP", // 8
"devIsRandomMac", // 9 resolved on the fly
"devStatus", // 10 resolved on the fly
"devMac", // 11
"devIpLong", // 12 formatIPlong(device.devLastIP) || "", // IP orderable
"rowid", // 13
"devParentMAC", // 14
"devParentChildrenCount", // 15 resolved on the fly
"devLocation", // 16
"devVendor", // 17
"devParentPort", // 18
"devGUID", // 19
"devSyncHubNode", // 20
"devSite", // 21
"devSSID", // 22
"devSourcePlugin", // 23
"devPresentLastScan", // 24
"devAlertDown", // 25
"devCustomProps", // 26
"devFQDN", // 27
"devParentRelType", // 28
"devReqNicsOnline" // 29
];
// console.log("OrderBy: " + columnNames[tableColumnOrder[index]]);
@@ -899,6 +899,28 @@ function initializeDatatable (status) {
}
} },
// Parent Mac
{targets: [mapIndx(14)],
'createdCell': function (td, cellData, rowData, row, col) {
if (!isValidMac(cellData)) {
$(td).html('');
return;
}
const data = {
id: cellData, // MAC address
text: cellData // Optional display text (you could use a name or something else)
};
spanWrap = $(`<span class="custom-badge text-white"></span>`)
$(td).html(spanWrap);
const chipHtml = renderDeviceLink(data, spanWrap, true); // pass the td as container
$(spanWrap).append(chipHtml);
}
},
// Status color
{targets: [mapIndx(10)],
'createdCell': function (td, cellData, rowData, row, col) {
@@ -982,9 +1004,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;
@@ -609,7 +612,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 +816,6 @@ function forceLoadUrl(relativeUrl) {
}
// -----------------------------------------------------------------------------
function navigateToDeviceWithIp (ip) {
@@ -836,11 +838,6 @@ function navigateToDeviceWithIp (ip) {
});
}
// -----------------------------------------------------------------------------
function getNameByMacAddress(macAddress) {
return getDevDataByMac(macAddress, "devName")
}
// -----------------------------------------------------------------------------
// Check if MAC or Internet
function checkMacOrInternet(inputStr) {
@@ -1013,7 +1010,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 +1029,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
}
// -----------------------------------------------------------------------------
@@ -1112,38 +1110,92 @@ let animationTime = 300
function showSpinner(stringKey = 'Loading') {
const text = isEmpty(stringKey) ? "Loading" : getString(stringKey || "Loading");
const spinner = $("#loadingSpinner");
if (spinner.length && spinner.is(':visible')) {
clearTimeout(spinnerTimeout);
$("#loadingSpinnerText").text(text);
spinner.addClass("visible");
const target = $(".spinnerTarget").first(); // Only use the first one if multiple exist
spinner.fadeIn(animationTime);
$("#loadingSpinnerText").text(text);
if (target.length) {
// Position relative to target
const offset = target.offset();
const width = target.outerWidth();
const height = target.outerHeight();
spinner.css({
position: "absolute",
top: offset.top,
left: offset.left,
width: width,
height: height,
zIndex: 800
});
} else {
$("#loadingSpinnerText").text(text);
requestAnimationFrame(() => {
spinner.addClass("visible");
spinner.fadeIn(animationTime);
// Fullscreen fallback
spinner.css({
position: "fixed",
top: 0,
left: 0,
width: "100%",
height: "100%",
zIndex: 800
});
}
requestAnimationFrame(() => {
spinner.addClass("visible");
spinner.fadeIn(animationTime);
});
}
function hideSpinner() {
clearTimeout(spinnerTimeout);
const spinner = $("#loadingSpinner");
if (spinner.length) {
spinner.removeClass("visible");
spinner.fadeOut(animationTime);
if (!spinner.length) return;
spinnerTimeout = setTimeout(() => {
spinner.removeClass("visible");
spinner.fadeOut(animationTime); // optional remove or hide again
}, 300);
const target = $(".spinnerTarget").first();
if (target.length) {
// Lock position to target
const offset = target.offset();
const width = target.outerWidth();
const height = target.outerHeight();
spinner.css({
position: "absolute",
top: offset.top,
left: offset.left,
width: width,
height: height,
zIndex: 800
});
} else {
// Fullscreen fallback
spinner.css({
position: "fixed",
top: 0,
left: 0,
width: "100%",
height: "100%",
zIndex: 800
});
}
// Trigger fade-out and only remove styles AFTER fade completes AND display is none
spinner.removeClass("visible").fadeOut(animationTime, () => {
// Ensure it's really hidden before resetting styles
spinner.css({
display: "none"
});
spinner.css({
position: "",
top: "",
left: "",
width: "",
height: "",
zIndex: ""
});
});
}

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
@@ -224,7 +357,7 @@ function modalWarningOK() {
} else if (typeof modalCallbackFunction === "string" && typeof window[modalCallbackFunction] === "function") {
window[modalCallbackFunction](); // Call via window
} else {
console.error("Invalid callback function");
console.error("Invalid callback function: " + modalCallbackFunction);
}
}, 100);
}

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

@@ -336,7 +336,8 @@ function execute_settingEvent(element) {
getString('DevDetail_button_OverwriteIcons_Warning'),
getString('Gen_Cancel'),
getString('Gen_Okay'),
'overwriteIconType'
'overwriteIconType',
feSourceId // triggered by id
);
} else if (["go_to_device"].includes(feEvent)) {
@@ -347,9 +348,43 @@ function execute_settingEvent(element) {
} else {
console.warn(`🔺Not implemented: ${feEvent}`)
}
}
// -----------------------------------------------------------------------------
// Go to the correct network node in the Network section
function overwriteIconType()
{
const mac = getMac();
if (!isValidMac(mac)) {
showModalOK("Error", getString("Gen_InvalidMac"))
return;
}
// Construct SQL query
const rawSql = `
UPDATE Devices
SET devIcon = (
SELECT devIcon FROM Devices WHERE devMac = "${mac}"
)
WHERE devType IN (
SELECT devType FROM Devices WHERE devMac = "${mac}"
)
`;
const apiUrl = `php/server/dbHelper.php?action=write&rawSql=${btoa(encodeURIComponent(rawSql))}`;
$.get(apiUrl, function(response) {
if (response === 'OK') {
showMessage (response);
updateApi("devices")
} else {
showMessage (response, 3000, "modal_red");
}
});
}
@@ -680,45 +715,7 @@ function initSelect2() {
{
var selectEl = $(this).select2({
templateSelection: function (data, container) {
if (!data.id) return data.text; // default for placeholder etc.
const device = getDevDataByMac(data.id);
const badge = getStatusBadgeParts(
device.devPresentLastScan,
device.devAlertDown,
device.devMac
)
$(container).addClass(badge.cssClass);
// Custom HTML
const html = $(`
<a href="${badge.url}" target="_blank">
<span class="custom-chip hover-node-info"
data-name="${device.devName}"
data-ip="${device.devLastIP}"
data-mac="${device.devMac}"
data-vendor="${device.devVendor}"
data-type="${device.devType}"
data-lastseen="${device.devLastConnection}"
data-firstseen="${device.devFirstConnection}"
data-relationship="${device.devParentRelType}"
data-status="${device.devStatus}"
data-present="${device.devPresentLastScan}"
data-alert="${device.devAlertDown}"
data-icon="${device.devIcon}"
>
<span class="iconPreview">${atob(device.devIcon)}</span>
${data.text}
<span>
(${badge.iconHtml})
</span
</span>
</a>
`);
return html;
return $(renderDeviceLink(data, container));
},
escapeMarkup: function (m) {
return m; // Allow HTML
@@ -782,6 +779,57 @@ function initSelect2() {
}
}
// ------------------------------------------
// Render a device link with hover-over functionality
function renderDeviceLink(data, container, useName = false) {
// 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
);
// badge class and hover-info class to container
$(container)
.addClass(`${badge.cssClass} hover-node-info`)
.attr({
'data-name': device.devName,
'data-ip': device.devLastIP,
'data-mac': device.devMac,
'data-vendor': device.devVendor,
'data-type': device.devType,
'data-lastseen': device.devLastConnection,
'data-firstseen': device.devFirstConnection,
'data-relationship': device.devParentRelType,
'data-status': device.devStatus,
'data-present': device.devPresentLastScan,
'data-alert': device.devAlertDown,
'data-icon': device.devIcon
});
return `
<a href="${badge.url}" target="_blank">
<span class="custom-chip">
<span class="iconPreview">${atob(device.devIcon)}</span>
${useName ? device.devName : data.text}
<span>
(${badge.iconHtml})
</span>
</span>
</a>
`;
}
// ------------------------------------------
// Display device info on hover (attach only once)
function initHoverNodeInfo() {

View File

@@ -1,6 +1,6 @@
<?php
require 'php/templates/header.php';
require 'php/templates/notification.php';
require 'php/templates/modals.php';
?>
<!-- Page ------------------------------------------------------------------ -->
@@ -8,7 +8,10 @@
<!-- Main content ---------------------------------------------------------- -->
<section class="content">
<script>
showSpinner();
</script>
<?php
@@ -143,7 +146,7 @@ $db->close();
</a>
</li>
</ul>
<div class="tab-content">
<div class="tab-content spinnerTarget">
<div class="tab-pane active" id="tab_DBTools">
<div class="db_info_table">
<div class="db_info_table_row">
@@ -182,6 +185,12 @@ $db->close();
</div>
<div class="db_tools_table_cell_b"><?= lang('Maintenance_Tool_del_ActHistory_text');?></div>
</div>
<div class="db_info_table_row">
<div class="db_tools_table_cell_a" >
<button type="button" class="btn btn-default pa-btn pa-btn-delete bg-red dbtools-button" id="btnRestartServer" onclick="askRestartBackend()"><?= lang('Maint_RestartServer');?></button>
</div>
<div class="db_tools_table_cell_b"><?= lang('Maint_Restart_Server_noti_text');?></div>
</div>
</div>
</div>
@@ -709,6 +718,8 @@ window.onload = function asyncFooter() {
</script>
<script>
hideSpinner();
</script>

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

@@ -76,6 +76,7 @@ require_once $_SERVER['DOCUMENT_ROOT'] . '/php/templates/security.php';
switch ($action) {
case 'create': create($defaultValue, $expireMinutes, $dbtable, $columns, $values ); break;
case 'read' : read($rawSql); break;
case 'write' : write($rawSql); break;
case 'update': update($columnName, $id, $defaultValue, $expireMinutes, $dbtable, $columns, $values); break;
case 'delete': delete($columnName, $id, $dbtable); break;
case 'lockDatabase': lockDatabase($delay); break;
@@ -120,6 +121,31 @@ function read($rawSql) {
}
}
//------------------------------------------------------------------------------
// write
//------------------------------------------------------------------------------
function write($rawSql) {
global $db;
// Construct the SQL query to select values
$sql = $rawSql;
// Execute the SQL query
$result = $db->query($sql);
// Check if the query executed successfully
if (! $result == TRUE) {
// Output an error message if the query failed
echo "Error writing data\n\n " .$sql." \n\n". $db->lastErrorMsg();
return;
} else
{
// Output
echo "OK";
return;
}
}
//------------------------------------------------------------------------------
// update

View File

@@ -37,23 +37,17 @@
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 'resetDeviceProps': resetDeviceProps(); break;
case 'ExportCSV': ExportCSV(); break; // todo
case 'ImportCSV': ImportCSV(); break; // todo
case 'getDevicesTotals': getDevicesTotals(); break;
case 'getDevicesListCalendar': getDevicesListCalendar(); break; //todo: slowly deprecate this
case 'getDevicesTotals': getDevicesTotals(); break; // todo
case 'getDevicesListCalendar': getDevicesListCalendar(); break; // todo
case 'updateNetworkLeaf': updateNetworkLeaf(); break; // todo
case 'updateNetworkLeaf': updateNetworkLeaf(); break;
case 'overwriteIconType': overwriteIconType(); break;
case 'getIcons': getIcons(); break;
case 'getActions': getActions(); break;
case 'getDevices': getDevices(); break;
case 'copyFromDevice': copyFromDevice(); break;
case 'wakeonlan': wakeonlan(); break;
case 'wakeonlan': wakeonlan(); break; // todo
default: logServerConsole ('Action: '. $action); break;
}
@@ -518,92 +512,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
//------------------------------------------------------------------------------
@@ -828,75 +736,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()
{
@@ -924,33 +763,6 @@ function updateNetworkLeaf()
}
// ----------------------------------------------------------------------------------------
function overwriteIconType()
{
$mac = $_REQUEST['mac'];
$icon = $_REQUEST['icon'];
if ((false === filter_var($mac , FILTER_VALIDATE_MAC) && $mac != "Internet" && $mac != "") ) {
throw new Exception('Invalid mac address');
}
else
{
global $db;
// sql
$sql = 'UPDATE Devices SET "devIcon" = "'. $icon .'" where devType in (select devType from Devices where devMac = "' . $mac.'")' ;
// update Data
$result = $db->query($sql);
// check result
if ($result == TRUE) {
echo 'OK';
} else {
echo lang('BackDevices_Device_UpdDevError');
}
}
}
//------------------------------------------------------------------------------
// Wake-on-LAN
// Inspired by @leiweibau: https://github.com/leiweibau/Pi.Alert/commit/30427c7fea180670c71a2b790699e5d9e9e88ffd

View File

@@ -139,8 +139,8 @@
<body class="hold-transition fixed <?php echo $pia_skin_selected;?> theme-<?php echo $UI_THEME;?> sidebar-mini" onLoad="update_servertime();" >
<div id="loadingSpinner">
<div class="pa_semitransparent-panel"></div>
<div class="panel panel-default pa_spinner">
<div class="nax_semitransparent-panel"></div>
<div class="panel panel-default nax_spinner">
<table>
<td id="loadingSpinnerText" width="130px" ></td>
<td><i class="fa-solid fa-spinner fa-spin-pulse"></i></td>
@@ -160,7 +160,7 @@
<a href="devices.php" class="logo">
<!-- mini logo for sidebar mini 50x50 pixels -->
<span class="logo-mini">
<img src="img/NetAlertX_logo.png" class="pia-top-left-logo" alt="NetAlertX Logo"/>
<img src="img/NetAlertX_logo.png" class="top-left-logo" alt="NetAlertX Logo"/>
</span>
<!-- logo for regular state and mobile devices -->
<span class="logo-lg">Net<b>Alert</b><sup>x</sup>
@@ -436,8 +436,24 @@
</li>
<!-- system info menu item -->
<li class=" <?php if (in_array (basename($_SERVER['SCRIPT_NAME']), array('systeminfo.php') ) ){ echo 'active'; } ?>">
<a href="systeminfo.php"><i class="fa fa-fw fa-info-circle"></i> <span><?= lang('Navigation_SystemInfo');?></span></a>
<li class=" treeview <?php if (in_array (basename($_SERVER['SCRIPT_NAME']), array('systeminfo.php') ) ){ echo 'active menu-open'; } ?>">
<a href="#">
<i class="fa fa-fw fa-info-circle"></i> <span><?= lang('Navigation_SystemInfo');?></span>
<span class="pull-right-container">
<i class="fa fa-angle-left pull-right"></i>
</span>
</a>
<ul class="treeview-menu " style="display: <?php if (in_array (basename($_SERVER['SCRIPT_NAME']), array('systeminfo.php') ) ){ echo 'block'; } else {echo 'none';} ?>;">
<li>
<a href="systeminfo.php#panServer" onclick="setCache('activeSysinfoTab','tabServer');initializeTabs()"><?= lang('Systeminfo_System');?></a>
</li>
<li>
<a href="systeminfo.php#panNetwork" onclick="setCache('activeSysinfoTab','tabNetwork');initializeTabs()"><?= lang('Systeminfo_Network');?></a>
</li>
<li>
<a href="systeminfo.php#panStorage" onclick="setCache('activeSysinfoTab','tabStorage');initializeTabs()"><?= lang('Systeminfo_Storage');?></a>
</li>
</ul>
</li>
</ul>
@@ -450,24 +466,6 @@
<script defer>
// Generate work-in-progress icons
function workInProgress() {
if($(".work-in-progress").length > 0 && $(".work-in-progress").html().trim() == "")
{
$(".work-in-progress").append(`
<a href="https://github.com/jokob-sk/NetAlertX/issues" target="_blank">
<b class="pointer" title="${getString("Gen_Work_In_Progress")}">🦺</b>
</a>
`)
}
}
//--------------------------------------------------------------
//--------------------------------------------------------------
function toggleFullscreen() {
if (document.fullscreenElement) {
@@ -485,6 +483,5 @@ function workInProgress() {
// Update server state in the header
updateState()
workInProgress()
</script>

View File

@@ -301,6 +301,7 @@
"Gen_Cancel": "إلغاء",
"Gen_Change": "تغيير",
"Gen_Copy": "نسخ",
"Gen_CopyToClipboard": "",
"Gen_DataUpdatedUITakesTime": "تم تحديث البيانات. قد يستغرق تحديث واجهة المستخدم بعض الوقت",
"Gen_Delete": "حذف",
"Gen_DeleteAll": "حذف الكل",
@@ -308,6 +309,7 @@
"Gen_Error": "خطأ",
"Gen_Filter": "تصفية",
"Gen_Generate": "إنشاء",
"Gen_InvalidMac": "",
"Gen_LockedDB": "قاعدة البيانات مقفلة",
"Gen_NetworkMask": "",
"Gen_Offline": "غير متصل",

View File

@@ -301,6 +301,7 @@
"Gen_Cancel": "Cancel·lar",
"Gen_Change": "Canviar",
"Gen_Copy": "Executar",
"Gen_CopyToClipboard": "",
"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",
@@ -308,6 +309,7 @@
"Gen_Error": "Error",
"Gen_Filter": "Filtrar",
"Gen_Generate": "Generar",
"Gen_InvalidMac": "",
"Gen_LockedDB": "ERROR - DB podria estar bloquejada - Fes servir F12 Eines desenvolupament -> Consola o provar-ho més tard.",
"Gen_NetworkMask": "",
"Gen_Offline": "Fora de línia",

View File

@@ -301,6 +301,7 @@
"Gen_Cancel": "Zrušit",
"Gen_Change": "Změnit",
"Gen_Copy": "Spustit",
"Gen_CopyToClipboard": "",
"Gen_DataUpdatedUITakesTime": "OK - může zabrat chvíli aktualizovat rozhraní, pokud běží scan.",
"Gen_Delete": "Smazat",
"Gen_DeleteAll": "Smazat vše",
@@ -308,6 +309,7 @@
"Gen_Error": "Chyba",
"Gen_Filter": "Filtr",
"Gen_Generate": "Vygenerovat",
"Gen_InvalidMac": "",
"Gen_LockedDB": "CHYBA - Databáze je možná zamčená - Zkontrolujte F12 -> Nástroje pro vývojáře -> Konzole. nebo to zkuste později.",
"Gen_NetworkMask": "",
"Gen_Offline": "Offline",

View File

@@ -305,6 +305,7 @@
"Gen_Cancel": "Abbrechen",
"Gen_Change": "Ändern",
"Gen_Copy": "Ausführen",
"Gen_CopyToClipboard": "",
"Gen_DataUpdatedUITakesTime": "OK Es kann einen Moment dauern, bis die Benutzeroberfläche aktualisiert wird, während ein Scan ausgeführt wird.",
"Gen_Delete": "Löschen",
"Gen_DeleteAll": "Alles löschen",
@@ -312,6 +313,7 @@
"Gen_Error": "Fehler",
"Gen_Filter": "Filter",
"Gen_Generate": "Generieren",
"Gen_InvalidMac": "Ungültige MAC-Adresse.",
"Gen_LockedDB": "ERROR - DB eventuell gesperrt - Nutze die Konsole in den Entwickler Werkzeugen (F12) zur Überprüfung oder probiere es später erneut.",
"Gen_NetworkMask": "",
"Gen_Offline": "Offline",

View File

@@ -301,6 +301,7 @@
"Gen_Cancel": "Cancel",
"Gen_Change": "Change",
"Gen_Copy": "Run",
"Gen_CopyToClipboard": "Copy to clipboard",
"Gen_DataUpdatedUITakesTime": "OK - It may take a while for the UI to update if a scan is running.",
"Gen_Delete": "Delete",
"Gen_DeleteAll": "Delete all",
@@ -308,6 +309,7 @@
"Gen_Error": "Error",
"Gen_Filter": "Filter",
"Gen_Generate": "Generate",
"Gen_InvalidMac": "Invalid Mac address.",
"Gen_LockedDB": "ERROR - DB might be locked - Check F12 Dev tools -> Console or try later.",
"Gen_NetworkMask": "Network mask",
"Gen_Offline": "Offline",

View File

@@ -303,6 +303,7 @@
"Gen_Cancel": "Cancelar",
"Gen_Change": "Cambiar",
"Gen_Copy": "Ejecutar",
"Gen_CopyToClipboard": "",
"Gen_DataUpdatedUITakesTime": "Correcto - La interfaz puede tardar en actualizarse si se está ejecutando un escaneo.",
"Gen_Delete": "Eliminar",
"Gen_DeleteAll": "Eliminar todo",
@@ -310,6 +311,7 @@
"Gen_Error": "Error",
"Gen_Filter": "Filtro",
"Gen_Generate": "Generar",
"Gen_InvalidMac": "",
"Gen_LockedDB": "Fallo - La base de datos puede estar bloqueada - Pulsa F1 -> Ajustes de desarrolladores -> Consola o prueba más tarde.",
"Gen_NetworkMask": "",
"Gen_Offline": "Desconectado",

View File

@@ -301,6 +301,7 @@
"Gen_Cancel": "Annuler",
"Gen_Change": "Changement",
"Gen_Copy": "Lancer",
"Gen_CopyToClipboard": "Copier vers le presse-papier",
"Gen_DataUpdatedUITakesTime": "OK - cela peut prendre du temps à l'interface pour se mettre à jour si un scan est en cours.",
"Gen_Delete": "Supprimer",
"Gen_DeleteAll": "Supprimer tous",
@@ -308,8 +309,9 @@
"Gen_Error": "Erreur",
"Gen_Filter": "Filtrer",
"Gen_Generate": "Générer",
"Gen_InvalidMac": "Adresse MAC invalide.",
"Gen_LockedDB": "Erreur - La base de données est peut-être verrouillée - Vérifier avec les outils de dév via F12 -> Console ou essayer plus tard.",
"Gen_NetworkMask": "",
"Gen_NetworkMask": "Masque réseau",
"Gen_Offline": "Hors ligne",
"Gen_Okay": "OK",
"Gen_Online": "En ligne",
@@ -327,7 +329,7 @@
"Gen_SelectIcon": "<i class=\"fa-solid fa-chevron-down fa-fade\"></i>",
"Gen_SelectToPreview": "Sélectionnez pour prévisualiser",
"Gen_Selected_Devices": "Appareils sélectionnés :",
"Gen_Subnet": "",
"Gen_Subnet": "Sous-réseau",
"Gen_Switch": "Basculer",
"Gen_Upd": "Mise à jour réussie",
"Gen_Upd_Fail": "Échec de la mise à jour",
@@ -596,7 +598,7 @@
"Settings_device_Scanners_desync": "⚠ La planification des différents scanners d'appareils est désynchronisée.",
"Settings_device_Scanners_desync_popup": "La planification des scanners (<code>*_RUN_SCHD</code>) n'est pas identique entre scanners. Cela va entraîner des notifications en ligne/hors-ligne non cohérentes. À moins que cela soit attendu, utilisez la même planification pour tous les <b>🔍scanners d'appareils</b> activés.",
"Speedtest_Results": "Résultats du test de débit",
"Systeminfo_AvailableIps": "",
"Systeminfo_AvailableIps": "Adresses IP disponibles",
"Systeminfo_CPU": "Processeur",
"Systeminfo_CPU_Cores": "Cœurs de processeur:",
"Systeminfo_CPU_Name": "Nom du processeur:",

View File

@@ -301,6 +301,7 @@
"Gen_Cancel": "Annulla",
"Gen_Change": "Modifica",
"Gen_Copy": "Esegui",
"Gen_CopyToClipboard": "Copia negli appunti",
"Gen_DataUpdatedUITakesTime": "OK: l'aggiornamento dell'interfaccia utente potrebbe richiedere del tempo se è in esecuzione una scansione.",
"Gen_Delete": "Elimina",
"Gen_DeleteAll": "Elimina tutti",
@@ -308,8 +309,9 @@
"Gen_Error": "Errore",
"Gen_Filter": "Filtro",
"Gen_Generate": "Genera",
"Gen_InvalidMac": "Indirizzo Mac non valido.",
"Gen_LockedDB": "ERRORE: il DB potrebbe essere bloccato, controlla F12 Strumenti di sviluppo -> Console o riprova più tardi.",
"Gen_NetworkMask": "",
"Gen_NetworkMask": "Maschera di rete",
"Gen_Offline": "Offline",
"Gen_Okay": "Ok",
"Gen_Online": "Online",
@@ -327,7 +329,7 @@
"Gen_SelectIcon": "<i class=\"fa-solid fa-chevron-down fa-fade\"></i>",
"Gen_SelectToPreview": "Seleziona per anteprima",
"Gen_Selected_Devices": "Dispositivi selezionati:",
"Gen_Subnet": "",
"Gen_Subnet": "Sottorete",
"Gen_Switch": "Cambia",
"Gen_Upd": "Aggiornato correttamente",
"Gen_Upd_Fail": "Aggiornamento fallito",
@@ -596,7 +598,7 @@
"Settings_device_Scanners_desync": "⚠ Le pianificazioni dello scanner del dispositivo non sono sincronizzate.",
"Settings_device_Scanners_desync_popup": "Gli orari degli scanner dei dispositivi (<code>*_RUN_SCHD</code>) non sono gli stessi. Questo comporterà notifiche online/offline incoerenti del dispositivo. A meno che ciò non sia previsto, utilizza la stessa pianificazione per tutti gli <b>🔍 scanner dispositivi</b> abilitati.",
"Speedtest_Results": "Risultati test di velocità",
"Systeminfo_AvailableIps": "",
"Systeminfo_AvailableIps": "IP disponibili",
"Systeminfo_CPU": "CPU",
"Systeminfo_CPU_Cores": "Core CPU:",
"Systeminfo_CPU_Name": "Nome CPU:",

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

@@ -301,6 +301,7 @@
"Gen_Cancel": "Avbryt",
"Gen_Change": "",
"Gen_Copy": "Kjør",
"Gen_CopyToClipboard": "",
"Gen_DataUpdatedUITakesTime": "OK - Det kan ta litt tid før brukergrensesnittet oppdateres hvis en skanning kjøres.",
"Gen_Delete": "Slett",
"Gen_DeleteAll": "Slett alle",
@@ -308,6 +309,7 @@
"Gen_Error": "Feil",
"Gen_Filter": "Filter",
"Gen_Generate": "",
"Gen_InvalidMac": "",
"Gen_LockedDB": "FEIL - DB kan være låst - Sjekk F12 Dev tools -> Konsoll eller prøv senere.",
"Gen_NetworkMask": "",
"Gen_Offline": "Frakoblet",

View File

@@ -301,6 +301,7 @@
"Gen_Cancel": "Anuluj",
"Gen_Change": "Zmiana",
"Gen_Copy": "Wykonaj",
"Gen_CopyToClipboard": "",
"Gen_DataUpdatedUITakesTime": "OK Może to potrwać chwilę, zanim interfejs użytkownika się zaktualizuje, jeśli trwa skan.",
"Gen_Delete": "Usuń",
"Gen_DeleteAll": "Usuń wszystko",
@@ -308,6 +309,7 @@
"Gen_Error": "Błąd",
"Gen_Filter": "Filtr",
"Gen_Generate": "Wygeneruj",
"Gen_InvalidMac": "",
"Gen_LockedDB": "Błąd - Baza danych może być zablokowana - Sprawdź narzędzia deweloperskie F12 -> Konsola lub spróbuj później.",
"Gen_NetworkMask": "",
"Gen_Offline": "Niedostępne",

View File

@@ -301,6 +301,7 @@
"Gen_Cancel": "Cancelar",
"Gen_Change": "Alterar",
"Gen_Copy": "Executar",
"Gen_CopyToClipboard": "",
"Gen_DataUpdatedUITakesTime": "OK - Pode levar um tempo para a interface do usuário ser atualizada se uma verificação estiver em execução.",
"Gen_Delete": "Excluir",
"Gen_DeleteAll": "Excluir todos",
@@ -308,6 +309,7 @@
"Gen_Error": "Erro",
"Gen_Filter": "Filtro",
"Gen_Generate": "Gerar",
"Gen_InvalidMac": "",
"Gen_LockedDB": "ERRO - O banco de dados pode estar bloqueado - Verifique F12 Ferramentas de desenvolvimento -> Console ou tente mais tarde.",
"Gen_NetworkMask": "",
"Gen_Offline": "Offline",

View File

@@ -0,0 +1,763 @@
{
"API_CUSTOM_SQL_description": "",
"API_CUSTOM_SQL_name": "",
"API_TOKEN_description": "",
"API_TOKEN_name": "",
"API_display_name": "",
"API_icon": "",
"About_Design": "",
"About_Exit": "",
"About_Title": "",
"AppEvents_AppEventProcessed": "",
"AppEvents_DateTimeCreated": "",
"AppEvents_Extra": "",
"AppEvents_GUID": "",
"AppEvents_Helper1": "",
"AppEvents_Helper2": "",
"AppEvents_Helper3": "",
"AppEvents_ObjectForeignKey": "",
"AppEvents_ObjectIndex": "",
"AppEvents_ObjectIsArchived": "",
"AppEvents_ObjectIsNew": "",
"AppEvents_ObjectPlugin": "",
"AppEvents_ObjectPrimaryID": "",
"AppEvents_ObjectSecondaryID": "",
"AppEvents_ObjectStatus": "",
"AppEvents_ObjectStatusColumn": "",
"AppEvents_ObjectType": "",
"AppEvents_Plugin": "",
"AppEvents_Type": "",
"BackDevDetail_Actions_Ask_Run": "",
"BackDevDetail_Actions_Not_Registered": "",
"BackDevDetail_Actions_Title_Run": "",
"BackDevDetail_Copy_Ask": "",
"BackDevDetail_Copy_Title": "",
"BackDevDetail_Tools_WOL_error": "",
"BackDevDetail_Tools_WOL_okay": "",
"BackDevices_Arpscan_disabled": "",
"BackDevices_Arpscan_enabled": "",
"BackDevices_Backup_CopError": "",
"BackDevices_Backup_Failed": "",
"BackDevices_Backup_okay": "",
"BackDevices_DBTools_DelDevError_a": "",
"BackDevices_DBTools_DelDevError_b": "",
"BackDevices_DBTools_DelDev_a": "",
"BackDevices_DBTools_DelDev_b": "",
"BackDevices_DBTools_DelEvents": "",
"BackDevices_DBTools_DelEventsError": "",
"BackDevices_DBTools_ImportCSV": "",
"BackDevices_DBTools_ImportCSVError": "",
"BackDevices_DBTools_ImportCSVMissing": "",
"BackDevices_DBTools_Purge": "",
"BackDevices_DBTools_UpdDev": "",
"BackDevices_DBTools_UpdDevError": "",
"BackDevices_DBTools_Upgrade": "",
"BackDevices_DBTools_UpgradeError": "",
"BackDevices_Device_UpdDevError": "",
"BackDevices_Restore_CopError": "",
"BackDevices_Restore_Failed": "",
"BackDevices_Restore_okay": "",
"BackDevices_darkmode_disabled": "",
"BackDevices_darkmode_enabled": "",
"CLEAR_NEW_FLAG_description": "",
"CLEAR_NEW_FLAG_name": "",
"CustProps_cant_remove": "",
"DAYS_TO_KEEP_EVENTS_description": "",
"DAYS_TO_KEEP_EVENTS_name": "",
"DISCOVER_PLUGINS_description": "",
"DISCOVER_PLUGINS_name": "",
"DevDetail_Children_Title": "",
"DevDetail_Copy_Device_Title": "",
"DevDetail_Copy_Device_Tooltip": "",
"DevDetail_CustomProperties_Title": "",
"DevDetail_CustomProps_reset_info": "",
"DevDetail_DisplayFields_Title": "",
"DevDetail_EveandAl_AlertAllEvents": "",
"DevDetail_EveandAl_AlertDown": "",
"DevDetail_EveandAl_Archived": "",
"DevDetail_EveandAl_NewDevice": "",
"DevDetail_EveandAl_NewDevice_Tooltip": "",
"DevDetail_EveandAl_RandomMAC": "",
"DevDetail_EveandAl_ScanCycle": "",
"DevDetail_EveandAl_ScanCycle_a": "",
"DevDetail_EveandAl_ScanCycle_z": "",
"DevDetail_EveandAl_Skip": "",
"DevDetail_EveandAl_Title": "",
"DevDetail_Events_CheckBox": "",
"DevDetail_GoToNetworkNode": "",
"DevDetail_Icon": "",
"DevDetail_Icon_Descr": "",
"DevDetail_Loading": "",
"DevDetail_MainInfo_Comments": "",
"DevDetail_MainInfo_Favorite": "",
"DevDetail_MainInfo_Group": "",
"DevDetail_MainInfo_Location": "",
"DevDetail_MainInfo_Name": "",
"DevDetail_MainInfo_Network": "",
"DevDetail_MainInfo_Network_Port": "",
"DevDetail_MainInfo_Network_Site": "",
"DevDetail_MainInfo_Network_Title": "",
"DevDetail_MainInfo_Owner": "",
"DevDetail_MainInfo_SSID": "",
"DevDetail_MainInfo_Title": "",
"DevDetail_MainInfo_Type": "",
"DevDetail_MainInfo_Vendor": "",
"DevDetail_MainInfo_mac": "",
"DevDetail_NavToChildNode": "",
"DevDetail_Network_Node_hover": "",
"DevDetail_Network_Port_hover": "",
"DevDetail_Nmap_Scans": "",
"DevDetail_Nmap_Scans_desc": "",
"DevDetail_Nmap_buttonDefault": "",
"DevDetail_Nmap_buttonDefault_text": "",
"DevDetail_Nmap_buttonDetail": "",
"DevDetail_Nmap_buttonDetail_text": "",
"DevDetail_Nmap_buttonFast": "",
"DevDetail_Nmap_buttonFast_text": "",
"DevDetail_Nmap_buttonSkipDiscovery": "",
"DevDetail_Nmap_buttonSkipDiscovery_text": "",
"DevDetail_Nmap_resultsLink": "",
"DevDetail_Owner_hover": "",
"DevDetail_Periodselect_All": "",
"DevDetail_Periodselect_LastMonth": "",
"DevDetail_Periodselect_LastWeek": "",
"DevDetail_Periodselect_LastYear": "",
"DevDetail_Periodselect_today": "",
"DevDetail_Run_Actions_Title": "",
"DevDetail_Run_Actions_Tooltip": "",
"DevDetail_SessionInfo_FirstSession": "",
"DevDetail_SessionInfo_LastIP": "",
"DevDetail_SessionInfo_LastSession": "",
"DevDetail_SessionInfo_StaticIP": "",
"DevDetail_SessionInfo_Status": "",
"DevDetail_SessionInfo_Title": "",
"DevDetail_SessionTable_Additionalinfo": "",
"DevDetail_SessionTable_Connection": "",
"DevDetail_SessionTable_Disconnection": "",
"DevDetail_SessionTable_Duration": "",
"DevDetail_SessionTable_IP": "",
"DevDetail_SessionTable_Order": "",
"DevDetail_Shortcut_CurrentStatus": "",
"DevDetail_Shortcut_DownAlerts": "",
"DevDetail_Shortcut_Presence": "",
"DevDetail_Shortcut_Sessions": "",
"DevDetail_Tab_Details": "",
"DevDetail_Tab_Events": "",
"DevDetail_Tab_EventsTableDate": "",
"DevDetail_Tab_EventsTableEvent": "",
"DevDetail_Tab_EventsTableIP": "",
"DevDetail_Tab_EventsTableInfo": "",
"DevDetail_Tab_Nmap": "",
"DevDetail_Tab_NmapEmpty": "",
"DevDetail_Tab_NmapTableExtra": "",
"DevDetail_Tab_NmapTableHeader": "",
"DevDetail_Tab_NmapTableIndex": "",
"DevDetail_Tab_NmapTablePort": "",
"DevDetail_Tab_NmapTableService": "",
"DevDetail_Tab_NmapTableState": "",
"DevDetail_Tab_NmapTableText": "",
"DevDetail_Tab_NmapTableTime": "",
"DevDetail_Tab_Plugins": "",
"DevDetail_Tab_Presence": "",
"DevDetail_Tab_Sessions": "",
"DevDetail_Tab_Tools": "",
"DevDetail_Tab_Tools_Internet_Info_Description": "",
"DevDetail_Tab_Tools_Internet_Info_Error": "",
"DevDetail_Tab_Tools_Internet_Info_Start": "",
"DevDetail_Tab_Tools_Internet_Info_Title": "",
"DevDetail_Tab_Tools_Nslookup_Description": "",
"DevDetail_Tab_Tools_Nslookup_Error": "",
"DevDetail_Tab_Tools_Nslookup_Start": "",
"DevDetail_Tab_Tools_Nslookup_Title": "",
"DevDetail_Tab_Tools_Speedtest_Description": "",
"DevDetail_Tab_Tools_Speedtest_Start": "",
"DevDetail_Tab_Tools_Speedtest_Title": "",
"DevDetail_Tab_Tools_Traceroute_Description": "",
"DevDetail_Tab_Tools_Traceroute_Error": "",
"DevDetail_Tab_Tools_Traceroute_Start": "",
"DevDetail_Tab_Tools_Traceroute_Title": "",
"DevDetail_Tools_WOL": "",
"DevDetail_Tools_WOL_noti": "",
"DevDetail_Tools_WOL_noti_text": "",
"DevDetail_Type_hover": "",
"DevDetail_Vendor_hover": "",
"DevDetail_WOL_Title": "",
"DevDetail_button_AddIcon": "",
"DevDetail_button_AddIcon_Help": "",
"DevDetail_button_AddIcon_Tooltip": "",
"DevDetail_button_Delete": "",
"DevDetail_button_DeleteEvents": "",
"DevDetail_button_DeleteEvents_Warning": "",
"DevDetail_button_Delete_ask": "",
"DevDetail_button_OverwriteIcons": "",
"DevDetail_button_OverwriteIcons_Tooltip": "",
"DevDetail_button_OverwriteIcons_Warning": "",
"DevDetail_button_Reset": "",
"DevDetail_button_Save": "",
"DeviceEdit_ValidMacIp": "",
"Device_MultiEdit": "",
"Device_MultiEdit_Backup": "",
"Device_MultiEdit_Fields": "",
"Device_MultiEdit_MassActions": "",
"Device_MultiEdit_Tooltip": "",
"Device_Searchbox": "",
"Device_Shortcut_AllDevices": "",
"Device_Shortcut_AllNodes": "",
"Device_Shortcut_Archived": "",
"Device_Shortcut_Connected": "",
"Device_Shortcut_Devices": "",
"Device_Shortcut_DownAlerts": "",
"Device_Shortcut_DownOnly": "",
"Device_Shortcut_Favorites": "",
"Device_Shortcut_NewDevices": "",
"Device_Shortcut_OnlineChart": "",
"Device_TableHead_AlertDown": "",
"Device_TableHead_Connected_Devices": "",
"Device_TableHead_CustomProps": "",
"Device_TableHead_FQDN": "",
"Device_TableHead_Favorite": "",
"Device_TableHead_FirstSession": "",
"Device_TableHead_GUID": "",
"Device_TableHead_Group": "",
"Device_TableHead_Icon": "",
"Device_TableHead_LastIP": "",
"Device_TableHead_LastIPOrder": "",
"Device_TableHead_LastSession": "",
"Device_TableHead_Location": "",
"Device_TableHead_MAC": "",
"Device_TableHead_MAC_full": "",
"Device_TableHead_Name": "",
"Device_TableHead_NetworkSite": "",
"Device_TableHead_Owner": "",
"Device_TableHead_ParentRelType": "",
"Device_TableHead_Parent_MAC": "",
"Device_TableHead_Port": "",
"Device_TableHead_PresentLastScan": "",
"Device_TableHead_ReqNicsOnline": "",
"Device_TableHead_RowID": "",
"Device_TableHead_Rowid": "",
"Device_TableHead_SSID": "",
"Device_TableHead_SourcePlugin": "",
"Device_TableHead_Status": "",
"Device_TableHead_SyncHubNodeName": "",
"Device_TableHead_Type": "",
"Device_TableHead_Vendor": "",
"Device_Table_Not_Network_Device": "",
"Device_Table_info": "",
"Device_Table_nav_next": "",
"Device_Table_nav_prev": "",
"Device_Tablelenght": "",
"Device_Tablelenght_all": "",
"Device_Title": "",
"Devices_Filters": "",
"ENABLE_PLUGINS_description": "",
"ENABLE_PLUGINS_name": "",
"ENCRYPTION_KEY_description": "",
"ENCRYPTION_KEY_name": "",
"Email_display_name": "",
"Email_icon": "",
"Events_Loading": "",
"Events_Periodselect_All": "",
"Events_Periodselect_LastMonth": "",
"Events_Periodselect_LastWeek": "",
"Events_Periodselect_LastYear": "",
"Events_Periodselect_today": "",
"Events_Searchbox": "",
"Events_Shortcut_AllEvents": "",
"Events_Shortcut_DownAlerts": "",
"Events_Shortcut_Events": "",
"Events_Shortcut_MissSessions": "",
"Events_Shortcut_NewDevices": "",
"Events_Shortcut_Sessions": "",
"Events_Shortcut_VoidSessions": "",
"Events_TableHead_AdditionalInfo": "",
"Events_TableHead_Connection": "",
"Events_TableHead_Date": "",
"Events_TableHead_Device": "",
"Events_TableHead_Disconnection": "",
"Events_TableHead_Duration": "",
"Events_TableHead_DurationOrder": "",
"Events_TableHead_EventType": "",
"Events_TableHead_IP": "",
"Events_TableHead_IPOrder": "",
"Events_TableHead_Order": "",
"Events_TableHead_Owner": "",
"Events_TableHead_PendingAlert": "",
"Events_Table_info": "",
"Events_Table_nav_next": "",
"Events_Table_nav_prev": "",
"Events_Tablelenght": "",
"Events_Tablelenght_all": "",
"Events_Title": "",
"GRAPHQL_PORT_description": "",
"GRAPHQL_PORT_name": "",
"Gen_Action": "",
"Gen_Add": "",
"Gen_AddDevice": "",
"Gen_Add_All": "",
"Gen_All_Devices": "",
"Gen_AreYouSure": "",
"Gen_Backup": "",
"Gen_Cancel": "",
"Gen_Change": "",
"Gen_Copy": "",
"Gen_CopyToClipboard": "",
"Gen_DataUpdatedUITakesTime": "",
"Gen_Delete": "",
"Gen_DeleteAll": "",
"Gen_Description": "",
"Gen_Error": "",
"Gen_Filter": "",
"Gen_Generate": "",
"Gen_InvalidMac": "",
"Gen_LockedDB": "",
"Gen_NetworkMask": "",
"Gen_Offline": "",
"Gen_Okay": "",
"Gen_Online": "",
"Gen_Purge": "",
"Gen_ReadDocs": "",
"Gen_Remove_All": "",
"Gen_Remove_Last": "",
"Gen_Reset": "",
"Gen_Restore": "",
"Gen_Run": "",
"Gen_Save": "",
"Gen_Saved": "",
"Gen_Search": "",
"Gen_Select": "",
"Gen_SelectIcon": "",
"Gen_SelectToPreview": "",
"Gen_Selected_Devices": "",
"Gen_Subnet": "",
"Gen_Switch": "",
"Gen_Upd": "",
"Gen_Upd_Fail": "",
"Gen_Update": "",
"Gen_Update_Value": "",
"Gen_ValidIcon": "",
"Gen_Warning": "",
"Gen_Work_In_Progress": "",
"Gen_create_new_device": "",
"Gen_create_new_device_info": "",
"General_display_name": "",
"General_icon": "",
"HRS_TO_KEEP_NEWDEV_description": "",
"HRS_TO_KEEP_NEWDEV_name": "",
"HRS_TO_KEEP_OFFDEV_description": "",
"HRS_TO_KEEP_OFFDEV_name": "",
"LOADED_PLUGINS_description": "",
"LOADED_PLUGINS_name": "",
"LOG_LEVEL_description": "",
"LOG_LEVEL_name": "",
"Loading": "",
"Login_Box": "",
"Login_Default_PWD": "",
"Login_Info": "",
"Login_Psw-box": "",
"Login_Psw_alert": "",
"Login_Psw_folder": "",
"Login_Psw_new": "",
"Login_Psw_run": "",
"Login_Remember": "",
"Login_Remember_small": "",
"Login_Submit": "",
"Login_Toggle_Alert_headline": "",
"Login_Toggle_Info": "",
"Login_Toggle_Info_headline": "",
"Maint_PurgeLog": "",
"Maint_RestartServer": "",
"Maint_Restart_Server_noti_text": "",
"Maintenance_InitCheck": "",
"Maintenance_InitCheck_Checking": "",
"Maintenance_InitCheck_QuickSetupGuide": "",
"Maintenance_InitCheck_Success": "",
"Maintenance_ReCheck": "",
"Maintenance_Running_Version": "",
"Maintenance_Status": "",
"Maintenance_Title": "",
"Maintenance_Tool_DownloadConfig": "",
"Maintenance_Tool_DownloadConfig_text": "",
"Maintenance_Tool_DownloadWorkflows": "",
"Maintenance_Tool_DownloadWorkflows_text": "",
"Maintenance_Tool_ExportCSV": "",
"Maintenance_Tool_ExportCSV_noti": "",
"Maintenance_Tool_ExportCSV_noti_text": "",
"Maintenance_Tool_ExportCSV_text": "",
"Maintenance_Tool_ImportCSV": "",
"Maintenance_Tool_ImportCSV_noti": "",
"Maintenance_Tool_ImportCSV_noti_text": "",
"Maintenance_Tool_ImportCSV_text": "",
"Maintenance_Tool_ImportConfig_noti": "",
"Maintenance_Tool_ImportPastedCSV": "",
"Maintenance_Tool_ImportPastedCSV_noti_text": "",
"Maintenance_Tool_ImportPastedCSV_text": "",
"Maintenance_Tool_ImportPastedConfig": "",
"Maintenance_Tool_ImportPastedConfig_noti_text": "",
"Maintenance_Tool_ImportPastedConfig_text": "",
"Maintenance_Tool_arpscansw": "",
"Maintenance_Tool_arpscansw_noti": "",
"Maintenance_Tool_arpscansw_noti_text": "",
"Maintenance_Tool_arpscansw_text": "",
"Maintenance_Tool_backup": "",
"Maintenance_Tool_backup_noti": "",
"Maintenance_Tool_backup_noti_text": "",
"Maintenance_Tool_backup_text": "",
"Maintenance_Tool_check_visible": "",
"Maintenance_Tool_darkmode": "",
"Maintenance_Tool_darkmode_noti": "",
"Maintenance_Tool_darkmode_noti_text": "",
"Maintenance_Tool_darkmode_text": "",
"Maintenance_Tool_del_ActHistory": "",
"Maintenance_Tool_del_ActHistory_noti": "",
"Maintenance_Tool_del_ActHistory_noti_text": "",
"Maintenance_Tool_del_ActHistory_text": "",
"Maintenance_Tool_del_alldev": "",
"Maintenance_Tool_del_alldev_noti": "",
"Maintenance_Tool_del_alldev_noti_text": "",
"Maintenance_Tool_del_alldev_text": "",
"Maintenance_Tool_del_allevents": "",
"Maintenance_Tool_del_allevents30": "",
"Maintenance_Tool_del_allevents30_noti": "",
"Maintenance_Tool_del_allevents30_noti_text": "",
"Maintenance_Tool_del_allevents30_text": "",
"Maintenance_Tool_del_allevents_noti": "",
"Maintenance_Tool_del_allevents_noti_text": "",
"Maintenance_Tool_del_allevents_text": "",
"Maintenance_Tool_del_empty_macs": "",
"Maintenance_Tool_del_empty_macs_noti": "",
"Maintenance_Tool_del_empty_macs_noti_text": "",
"Maintenance_Tool_del_empty_macs_text": "",
"Maintenance_Tool_del_selecteddev": "",
"Maintenance_Tool_del_selecteddev_text": "",
"Maintenance_Tool_del_unknowndev": "",
"Maintenance_Tool_del_unknowndev_noti": "",
"Maintenance_Tool_del_unknowndev_noti_text": "",
"Maintenance_Tool_del_unknowndev_text": "",
"Maintenance_Tool_displayed_columns_text": "",
"Maintenance_Tool_drag_me": "",
"Maintenance_Tool_order_columns_text": "",
"Maintenance_Tool_purgebackup": "",
"Maintenance_Tool_purgebackup_noti": "",
"Maintenance_Tool_purgebackup_noti_text": "",
"Maintenance_Tool_purgebackup_text": "",
"Maintenance_Tool_restore": "",
"Maintenance_Tool_restore_noti": "",
"Maintenance_Tool_restore_noti_text": "",
"Maintenance_Tool_restore_text": "",
"Maintenance_Tool_upgrade_database_noti": "",
"Maintenance_Tool_upgrade_database_noti_text": "",
"Maintenance_Tool_upgrade_database_text": "",
"Maintenance_Tools_Tab_BackupRestore": "",
"Maintenance_Tools_Tab_Logging": "",
"Maintenance_Tools_Tab_Settings": "",
"Maintenance_Tools_Tab_Tools": "",
"Maintenance_Tools_Tab_UISettings": "",
"Maintenance_arp_status": "",
"Maintenance_arp_status_off": "",
"Maintenance_arp_status_on": "",
"Maintenance_built_on": "",
"Maintenance_current_version": "",
"Maintenance_database_backup": "",
"Maintenance_database_backup_found": "",
"Maintenance_database_backup_total": "",
"Maintenance_database_lastmod": "",
"Maintenance_database_path": "",
"Maintenance_database_rows": "",
"Maintenance_database_size": "",
"Maintenance_lang_selector_apply": "",
"Maintenance_lang_selector_empty": "",
"Maintenance_lang_selector_lable": "",
"Maintenance_lang_selector_text": "",
"Maintenance_new_version": "",
"Maintenance_themeselector_apply": "",
"Maintenance_themeselector_empty": "",
"Maintenance_themeselector_lable": "",
"Maintenance_themeselector_text": "",
"Maintenance_version": "",
"NETWORK_DEVICE_TYPES_description": "",
"NETWORK_DEVICE_TYPES_name": "",
"Navigation_About": "",
"Navigation_AppEvents": "",
"Navigation_Devices": "",
"Navigation_Donations": "",
"Navigation_Events": "",
"Navigation_Integrations": "",
"Navigation_Maintenance": "",
"Navigation_Monitoring": "",
"Navigation_Network": "",
"Navigation_Notifications": "",
"Navigation_Plugins": "",
"Navigation_Presence": "",
"Navigation_Report": "",
"Navigation_Settings": "",
"Navigation_SystemInfo": "",
"Navigation_Workflows": "",
"Network_Assign": "",
"Network_Cant_Assign": "",
"Network_Cant_Assign_No_Node_Selected": "",
"Network_Configuration_Error": "",
"Network_Connected": "",
"Network_Devices": "",
"Network_ManageAdd": "",
"Network_ManageAdd_Name": "",
"Network_ManageAdd_Name_text": "",
"Network_ManageAdd_Port": "",
"Network_ManageAdd_Port_text": "",
"Network_ManageAdd_Submit": "",
"Network_ManageAdd_Type": "",
"Network_ManageAdd_Type_text": "",
"Network_ManageAssign": "",
"Network_ManageDel": "",
"Network_ManageDel_Name": "",
"Network_ManageDel_Name_text": "",
"Network_ManageDel_Submit": "",
"Network_ManageDevices": "",
"Network_ManageEdit": "",
"Network_ManageEdit_ID": "",
"Network_ManageEdit_ID_text": "",
"Network_ManageEdit_Name": "",
"Network_ManageEdit_Name_text": "",
"Network_ManageEdit_Port": "",
"Network_ManageEdit_Port_text": "",
"Network_ManageEdit_Submit": "",
"Network_ManageEdit_Type": "",
"Network_ManageEdit_Type_text": "",
"Network_ManageLeaf": "",
"Network_ManageUnassign": "",
"Network_NoAssignedDevices": "",
"Network_NoDevices": "",
"Network_Node": "",
"Network_Node_Name": "",
"Network_Parent": "",
"Network_Root": "",
"Network_Root_Not_Configured": "",
"Network_Root_Unconfigurable": "",
"Network_ShowArchived": "",
"Network_ShowOffline": "",
"Network_Table_Hostname": "",
"Network_Table_IP": "",
"Network_Table_State": "",
"Network_Title": "",
"Network_UnassignedDevices": "",
"Notifications_All": "",
"Notifications_Mark_All_Read": "",
"PIALERT_WEB_PASSWORD_description": "",
"PIALERT_WEB_PASSWORD_name": "",
"PIALERT_WEB_PROTECTION_description": "",
"PIALERT_WEB_PROTECTION_name": "",
"PLUGINS_KEEP_HIST_description": "",
"PLUGINS_KEEP_HIST_name": "",
"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": ""
}

View File

@@ -301,6 +301,7 @@
"Gen_Cancel": "Отмена",
"Gen_Change": "Изменить",
"Gen_Copy": "Запустить",
"Gen_CopyToClipboard": "",
"Gen_DataUpdatedUITakesTime": "ОК - Обновление UI может занять некоторое время, если сканирование выполняется.",
"Gen_Delete": "Удалить",
"Gen_DeleteAll": "Удалить все",
@@ -308,6 +309,7 @@
"Gen_Error": "Ошибка",
"Gen_Filter": "Фильтр",
"Gen_Generate": "Генерировать",
"Gen_InvalidMac": "",
"Gen_LockedDB": "ОШИБКА - Возможно, база данных заблокирована. Проверьте инструменты разработчика F12 -> Консоль или повторите попытку позже.",
"Gen_NetworkMask": "",
"Gen_Offline": "Оффлайн",

View File

@@ -301,6 +301,7 @@
"Gen_Cancel": "İptal",
"Gen_Change": "Değiştir",
"Gen_Copy": "Çalıştır",
"Gen_CopyToClipboard": "",
"Gen_DataUpdatedUITakesTime": "TAMAM - Eğer bir tarama çalışıyorsa arayüzün güncellenmesi biraz zaman alabilir.",
"Gen_Delete": "Sil",
"Gen_DeleteAll": "Tümünü sil",
@@ -308,6 +309,7 @@
"Gen_Error": "Hata",
"Gen_Filter": "Filtre",
"Gen_Generate": "Oluştur",
"Gen_InvalidMac": "",
"Gen_LockedDB": "HATA - Veritabanı kilitlenmiş olabilir - F12 Geliştirici araçlarını -> Konsol kısmını kontrol edin veya daha sonra tekrar deneyin.",
"Gen_NetworkMask": "",
"Gen_Offline": "Çevrimdışı",

8
front/php/templates/language/uk_ua.json Normal file → Executable file
View File

@@ -301,6 +301,7 @@
"Gen_Cancel": "Скасувати",
"Gen_Change": "Зміна",
"Gen_Copy": "Запустити",
"Gen_CopyToClipboard": "Копіювати в буфер обміну",
"Gen_DataUpdatedUITakesTime": "Добре. Оновлення інтерфейсу може зайняти деякий час, якщо сканування виконується.",
"Gen_Delete": "Видалити",
"Gen_DeleteAll": "Видалити все",
@@ -308,8 +309,9 @@
"Gen_Error": "Помилка",
"Gen_Filter": "Фільтр",
"Gen_Generate": "Генерувати",
"Gen_InvalidMac": "Недійсна Mac-адреса.",
"Gen_LockedDB": "ПОМИЛКА БД може бути заблоковано перевірте F12 Інструменти розробника -> Консоль або спробуйте пізніше.",
"Gen_NetworkMask": "",
"Gen_NetworkMask": "Маска мережі",
"Gen_Offline": "Офлайн",
"Gen_Okay": "Гаразд",
"Gen_Online": "Онлайн",
@@ -629,7 +631,7 @@
"Systeminfo_Network_HTTP_Referer": "HTTP реферер:",
"Systeminfo_Network_HTTP_Referer_String": "Немає реферера HTTP",
"Systeminfo_Network_Hardware": "Мережеве обладнання",
"Systeminfo_Network_Hardware_Interface_Mask": "Маска мережі",
"Systeminfo_Network_Hardware_Interface_Mask": "Маска Мережі",
"Systeminfo_Network_Hardware_Interface_Name": "Назва інтерфейсу",
"Systeminfo_Network_Hardware_Interface_RX": "Отримано",
"Systeminfo_Network_Hardware_Interface_TX": "Передано",
@@ -758,4 +760,4 @@
"settings_system_label": "Система",
"settings_update_item_warning": "Оновіть значення нижче. Слідкуйте за попереднім форматом. <b>Перевірка не виконана.</b>",
"test_event_tooltip": "Перш ніж перевіряти налаштування, збережіть зміни."
}
}

View File

@@ -301,6 +301,7 @@
"Gen_Cancel": "取消",
"Gen_Change": "修改",
"Gen_Copy": "运行",
"Gen_CopyToClipboard": "",
"Gen_DataUpdatedUITakesTime": "好的 - 如果扫描正在运行UI 可能需要一段时间才能更新。",
"Gen_Delete": "删除",
"Gen_DeleteAll": "全部删除",
@@ -308,6 +309,7 @@
"Gen_Error": "错误",
"Gen_Filter": "筛选",
"Gen_Generate": "生成",
"Gen_InvalidMac": "",
"Gen_LockedDB": "错误 - DB 可能被锁定 - 检查 F12 开发工具 -> 控制台或稍后重试。",
"Gen_NetworkMask": "",
"Gen_Offline": "离线",

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

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

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

@@ -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": "The URL of the hub (target instance) with the targets <code>GRAPHQL_PORT</code> set as port. Set on the Node. Without a trailig slash, for example <code>http://192.168.1.82:20212</code>"
}
]
},

View File

@@ -265,66 +265,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">
@@ -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'; ?>
<!-- ----------------------------------------------------------------------- -->
@@ -21,6 +21,7 @@
// show spinning icon
showSpinner()
</script>
<!-- Page ------------------------------------------------------------------ -->
@@ -28,674 +29,112 @@
<!-- Main content ---------------------------------------------------------- -->
<section class="content">
<?php
//General stats
// Date & Time
$date = new DateTime();
$formatted_date = $date->format('l, F j, Y H:i:s'); // Get date
$formatted_date2 = $date->format('d/m/Y H:i:s'); // Get date2
$formatted_date3 = $date->format('Y/m/d H:i:s'); // Get date3
//System stats
// OS-Version
$os_version = '';
// Raspbian
if ($os_version == '') {$os_version = exec('cat /etc/os-release | grep PRETTY_NAME');}
// Dietpi
if ($os_version == '') {$os_version = exec('uname -o');}
//$os_version_arr = explode("\n", trim($os_version));
$stat['os_version'] = str_replace('"', '', str_replace('PRETTY_NAME=', '', $os_version));
$stat['uptime'] = str_replace('up ', '', shell_exec("uptime -p")); // Get system uptime
$system_namekernel = shell_exec("uname"); // Get system name kernel
$system_namesystem = shell_exec("uname -o"); // Get name system
$system_full = shell_exec("uname -a"); // Get system full
$system_architecture = shell_exec("uname -m"); // Get system Architecture
$load_average = sys_getloadavg(); // Get load average
$system_process_count = shell_exec("ps -e --no-headers | wc -l"); // Count processes
//Motherboard stats
$motherboard_name = shell_exec('cat /sys/class/dmi/id/board_name'); // Get the Motherboard name
$motherboard_manufactured = shell_exec('cat /sys/class/dmi/id/board_vendor'); // Get the Motherboard manufactured
$motherboard_revision = shell_exec('cat /sys/class/dmi/id/board_version'); // Get the Motherboard revision
$motherboard_bios = shell_exec('cat /sys/class/dmi/id/bios_version'); // Get the Motherboard BIOS
$motherboard_biosdate = shell_exec('cat /sys/class/dmi/id/bios_date'); // Get the Motherboard BIOS date
$motherboard_biosvendor = shell_exec('cat /sys/class/dmi/id/bios_vendor'); // Get the Motherboard BIOS vendor
//CPU stats
$prevVal = shell_exec("cat /proc/cpuinfo | grep processor");
$prevArr = explode("\n", trim($prevVal));
$stat['cpu'] = sizeof($prevArr);
$cpu_result = shell_exec("cat /proc/cpuinfo | grep Model");
$stat['cpu_model'] = strstr($cpu_result, "\n", true);
$stat['cpu_model'] = str_replace(":", "", trim(str_replace("Model", "", $stat['cpu_model'])));
if ($stat['cpu_model'] == '') {
$cpu_result = shell_exec("cat /proc/cpuinfo | grep model\ name");
$stat['cpu_model'] = strstr($cpu_result, "\n", true);
$stat['cpu_model'] = str_replace(":", "", trim(str_replace("model name", "", $stat['cpu_model'])));
}
if (file_exists('/sys/devices/system/cpu/cpu0/cpufreq/scaling_max_freq')) {
// RaspbianOS
$stat['cpu_frequ'] = exec('cat /sys/devices/system/cpu/cpu0/cpufreq/scaling_max_freq') / 1000;
} elseif (is_numeric(str_replace(',', '.', exec('lscpu | grep "MHz" | awk \'{print $3}\'')))) {
// Ubuntu Server, DietPi event. others
$stat['cpu_frequ'] = round(exec('lscpu | grep "MHz" | awk \'{print $3}\''), 0);
} elseif (is_numeric(str_replace(',', '.', exec('lscpu | grep "max MHz" | awk \'{print $4}\'')))) {
// RaspbianOS and event. others
$stat['cpu_frequ'] = round(str_replace(',', '.', exec('lscpu | grep "max MHz" | awk \'{print $4}\'')), 0);
} else {
// Fallback
$stat['cpu_frequ'] = "unknown";
}
$cpu_temp = shell_exec('cat /sys/class/hwmon/hwmon0/temp1_input'); // Get the CPU temperature
$cpu_temp = floatval($cpu_temp) / 1000; // Convert the temperature to degrees Celsius
$cpu_vendor = exec('cat /proc/cpuinfo | grep "vendor_id" | cut -d ":" -f 2' ); // Get the CPU vendor
//Memory stats
$total_memorykb = shell_exec("cat /proc/meminfo | grep MemTotal | awk '{print $2}'");
$total_memorykb = trim($total_memorykb);
$total_memorykb = number_format($total_memorykb, 0, '.', '.');
$total_memorymb = shell_exec("cat /proc/meminfo | grep MemTotal | awk '{print $2/1024}'");
$total_memorymb = trim($total_memorymb);
$total_memorymb = number_format($total_memorymb, 0, '.', '.');
$mem_used = round(memory_get_usage() / 1048576 * 100, 2);
$memory_usage_percent = round(($mem_used / $total_memorymb), 2);
//HDD stats
$hdd_result = shell_exec(" df -P | awk '{print $1}'");
$hdd_devices = explode("\n", trim($hdd_result));
$hdd_result = shell_exec(" df -P | awk '{print $2}'");
$hdd_devices_total = explode("\n", trim($hdd_result));
$hdd_result = shell_exec(" df -P | awk '{print $3}'");
$hdd_devices_used = explode("\n", trim($hdd_result));
$hdd_result = shell_exec(" df -P | awk '{print $4}'");
$hdd_devices_free = explode("\n", trim($hdd_result));
$hdd_result = shell_exec(" df -P | awk '{print $5}'");
$hdd_devices_percent = explode("\n", trim($hdd_result));
$hdd_result = shell_exec(" df -P | awk '{print $6}'");
$hdd_devices_mount = explode("\n", trim($hdd_result));
//Network stats
// Check Server name
if (!empty(gethostname())) { $network_NAME = gethostname(); } else { $network_NAME = lang('Systeminfo_Network_Server_Name_String'); }
// Check HTTPS
if (isset($_SERVER['HTTPS'])) { $network_HTTPS = 'Yes (HTTPS)'; } else { $network_HTTPS = lang('Systeminfo_Network_Secure_Connection_String'); }
// Check Query String
if (empty($_SERVER['QUERY_STRING'])) { $network_QueryString = lang('Systeminfo_Network_Server_Query_String'); } else { $network_QueryString = $_SERVER['QUERY_STRING']; }
// Check HTTP referer
if (empty($_SERVER['HTTP_REFERER'])) { $network_referer = lang('Systeminfo_Network_HTTP_Referer_String'); } else { $network_referer = $_SERVER['HTTP_REFERER']; }
//Network Hardware stat
$network_result = shell_exec("cat /proc/net/dev | tail -n +3 | awk '{print $1}'");
$net_interfaces = explode("\n", trim($network_result));
$network_result = shell_exec("cat /proc/net/dev | tail -n +3 | awk '{print $2}'");
$net_interfaces_rx = explode("\n", trim($network_result));
$network_result = shell_exec("cat /proc/net/dev | tail -n +3 | awk '{print $10}'");
$net_interfaces_tx = explode("\n", trim($network_result));
//USB devices
$usb_result = shell_exec("lsusb");
$usb_devices_mount = explode("\n", trim($usb_result));
// General ----------------------------------------------------------
echo '<div class="box box-solid">
<div class="box-header">
<h3 class="box-title sysinfo_headline"><i class="fa fa-info-circle"></i> ' . lang('Systeminfo_General') . '</h3>
<div class="row">
<div class="col-lg-12 col-sm-12 col-xs-12">
<!-- <div class="box-transparent"> -->
<div id="navSysInfo" class="nav-tabs-custom">
<ul class="nav nav-tabs" style="font-size:16px;">
<li>
<a id="tabServer" href="#panServer" data-toggle="tab">
<i class="fa fa-info-circle"></i>
<span class="dev-detail-tab-name">
<?= lang('Systeminfo_System');?>
</span>
</a>
</li>
<li>
<a id="tabNetwork" href="#panNetwork" data-toggle="tab">
<i class="fa fa-sitemap fa-rotate-270"></i>
<span class="dev-detail-tab-name">
<?= lang('Systeminfo_Network');?>
</span>
</a>
</li>
<li>
<a id="tabStorage" href="#panStorage" data-toggle="tab">
<i class="fa fa-hdd"></i>
<span class="dev-detail-tab-name">
<?= lang('Systeminfo_Storage');?>
</span>
</a>
</li>
</ul>
<div class="tab-content spinnerTarget" style="min-height: 430px;">
<div class="tab-pane fade" data-php-file="systeminfoServer.php" id="panServer">
<!-- PLACEHOLDER -->
</div>
<div class="tab-pane fade" data-php-file="systeminfoNetwork.php" id="panNetwork">
<!-- PLACEHOLDER -->
</div>
<div class="tab-pane fade table-responsive" data-php-file="systeminfoStorage.php" id="panStorage">
<!-- PLACEHOLDER -->
</div>
</div>
<div class="box-body">
<div class="row">
<div class="col-sm-3 sysinfo_general_a">' . lang('Systeminfo_General_Full_Date') . '</div>
<div class="col-sm-9 sysinfo_general_b">' . $formatted_date . '</div>
</div>
<div class="row">
<div class="col-sm-3 sysinfo_general_a">' . lang('Systeminfo_General_Date') . '</div>
<div class="col-sm-9 sysinfo_general_b">' . $formatted_date2 . '</div>
</div>
<div class="row">
<div class="col-sm-3 sysinfo_general_a">' . lang('Systeminfo_General_Date2') . '</div>
<div class="col-sm-9 sysinfo_general_b">' . $formatted_date3 . '</div>
</div>
<div class="row">
<div class="col-sm-3 sysinfo_general_a">' . lang('Systeminfo_General_TimeZone') . '</div>
<div class="col-sm-9 sysinfo_general_b">' . $timeZone . '</div>
</div>
</div>
</div>';
// Network Hardware ----------------------------------------------------------
echo '<div class="box box-solid">
<div class="box-header">
<h3 class="box-title sysinfo_headline"><i class="fa fa-sitemap fa-rotate-270"></i> ' . lang('Systeminfo_Network_Hardware') . '</h3>
<!-- /.tab-content -->
</div>
<!-- /.nav-tabs-custom -->
<!-- </div> -->
</div>
<div class="box-body">
<table id="networkTable" class="table table-bordered table-hover">
<thead>
<tr>
<th>' . lang('Systeminfo_Network_Hardware_Interface_Name') . '</th>
<th>' . lang('Systeminfo_Network_Hardware_Interface_Mask') . '</th>
<th>' . lang('Systeminfo_Network_Hardware_Interface_RX') . '</th>
<th>' . lang('Systeminfo_Network_Hardware_Interface_TX') . '</th>
</tr>
</thead>
<tbody>';
<!-- /.col -->
</div>
for ($x = 0; $x < sizeof($net_interfaces); $x++) {
$interface_name = str_replace(':', '', $net_interfaces[$x]);
$interface_ip_temp = exec('ip addr show ' . $interface_name . ' | grep "inet "');
$interface_ip_arr = explode(' ', trim($interface_ip_temp));
if (!isset($interface_ip_arr[1])) {
$interface_ip_arr[1] = '--';
}
if ($net_interfaces_rx[$x] == 0) {
$temp_rx = 0;
} else {
$temp_rx = number_format(round(($net_interfaces_rx[$x] / 1024 / 1024), 2), 2, ',', '.');
}
if ($net_interfaces_tx[$x] == 0) {
$temp_tx = 0;
} else {
$temp_tx = number_format(round(($net_interfaces_tx[$x] / 1024 / 1024), 2), 2, ',', '.');
}
echo '<tr>';
echo '<td>' . $interface_name . '</td>';
echo '<td>' . $interface_ip_arr[1] . '</td>';
echo '<td>' . $temp_rx . ' MB</td>';
echo '<td>' . $temp_tx . ' MB</td>';
echo '</tr>';
}
echo ' </tbody>
</table>
</div>
</div>';
// Available IPs ----------------------------------------------------------
echo '<div class="box box-solid">
<div class="box-header">
<h3 class="box-title availableips_headline"><i class="fa fa-list"></i> ' . lang('Systeminfo_AvailableIps') . '</h3>
</div>
<div class="box-body">
<table id="availableIpsTable" class="display" style="width:100%"></table>
</div>
</div>';
// Client ----------------------------------------------------------
echo '<div class="box box-solid">
<div class="box-header">
<h3 class="box-title sysinfo_headline"><i class="fa fa-globe"></i> ' . lang('Systeminfo_This_Client') . '</h3>
</div>
<div class="box-body">
<div class="row">
<div class="col-sm-3 sysinfo_client_a">' . lang('Systeminfo_Client_User_Agent') . '</div>
<div class="col-sm-9 sysinfo_client_b">' . $_SERVER['HTTP_USER_AGENT'] . '</div>
</div>
<div class="row">
<div class="col-sm-3 sysinfo_client_a">' . lang('Systeminfo_Client_Resolution') . '</div>
<div class="col-sm-9 sysinfo_client_b" id="resolution"></div>
</div>
</div>
</div>';
echo '<script>
var ratio = window.devicePixelRatio || 1;
var w = window.innerWidth;
var h = window.innerHeight;
var rw = window.innerWidth * ratio;
var rh = window.innerHeight * ratio;
var resolutionDiv = document.getElementById("resolution");
resolutionDiv.innerHTML = "Width: " + w + "px / Height: " + h + "px<br> " + "Width: " + rw + "px / Height: " + rh + "px (native)";
</script>';
// System ----------------------------------------------------------
echo '<div class="box box-solid">
<div class="box-header">
<h3 class="box-title sysinfo_headline"><i class="fa fa-computer"></i> ' . lang('Systeminfo_System') . '</h3>
</div>
<div class="box-body">
<div class="row">
<div class="col-sm-3 sysinfo_system_a">' . lang('Systeminfo_System_Uptime') . '</div>
<div class="col-sm-9 sysinfo_system_b">' . $stat['uptime'] . '</div>
</div>
<div class="row">
<div class="col-sm-3 sysinfo_system_a">' . lang('Systeminfo_System_Kernel') . '</div>
<div class="col-sm-9 sysinfo_system_b">' . $system_namekernel . '</div>
</div>
<div class="row">
<div class="col-sm-3 sysinfo_system_a">' . lang('Systeminfo_System_System') . '</div>
<div class="col-sm-9 sysinfo_system_b">' . $system_namesystem . '</div>
</div>
<div class="row">
<div class="col-sm-3 sysinfo_system_a">' . lang('Systeminfo_System_OSVersion') . '</div>
<div class="col-sm-9 sysinfo_system_b">' . $stat['os_version'] . '</div>
</div>
<div class="row">
<div class="col-sm-3 sysinfo_system_a">' . lang('Systeminfo_System_Uname') . '</div>
<div class="col-sm-9 sysinfo_system_b">' . $system_full . '</div>
</div>
<div class="row">
<div class="col-sm-3 sysinfo_system_a">' . lang('Systeminfo_System_Architecture') . '</div>
<div class="col-sm-9 sysinfo_system_b">' . $system_architecture . '</div>
</div>
<div class="row">
<div class="col-sm-3 sysinfo_system_a">' . lang('Systeminfo_System_AVG') . '</div>
<div class="col-sm-9 sysinfo_system_b">'. $load_average[0] .' '. $load_average[1] .' '. $load_average[2] .'</div>
</div>
<div class="row">
<div class="col-sm-3 sysinfo_system_a">' . lang('Systeminfo_System_Running_Processes') . '</div>
<div class="col-sm-9 sysinfo_system_b">' . $system_process_count . '</div>
</div>
</div>
</div>';
// Motherboard ----------------------------------------------------------
echo '<div class="box box-solid">
<div class="box-header">
<h3 class="box-title sysinfo_headline"><i class="fa fa-laptop-code"></i> ' . lang('Systeminfo_Motherboard') . '</h3>
</div>
<div class="box-body">
<div class="row">
<div class="col-sm-3 sysinfo_motherboard_a">' . lang('Systeminfo_Motherboard_Name') . '</div>
<div class="col-sm-9 sysinfo_motherboard_b">' . $motherboard_name . '</div>
</div>
<div class="row">
<div class="col-sm-3 sysinfo_motherboard_a">' . lang('Systeminfo_Motherboard_Manufactured') . '</div>
<div class="col-sm-9 sysinfo_motherboard_b">' . $motherboard_manufactured . '</div>
</div>
<div class="row">
<div class="col-sm-3 sysinfo_motherboard_a">' . lang('Systeminfo_Motherboard_Revision') . '</div>
<div class="col-sm-9 sysinfo_motherboard_b">' . $motherboard_revision. '</div>
</div>
<div class="row">
<div class="col-sm-3 sysinfo_motherboard_a">' . lang('Systeminfo_Motherboard_BIOS') . '</div>
<div class="col-sm-9 sysinfo_motherboard_b">' . $motherboard_bios . '</div>
</div>
<div class="row">
<div class="col-sm-3 sysinfo_motherboard_a">' . lang('Systeminfo_Motherboard_BIOS_Date') . '</div>
<div class="col-sm-9 sysinfo_motherboard_b">' . $motherboard_biosdate . '</div>
</div>
<div class="row">
<div class="col-sm-3 sysinfo_motherboard_a">' . lang('Systeminfo_Motherboard_BIOS_Vendor') . '</div>
<div class="col-sm-9 sysinfo_motherboard_b">' . $motherboard_biosvendor . '</div>
</div>
</div>
</div>';
// CPU ----------------------------------------------------------
echo '<div class="box box-solid">
<div class="box-header">
<h3 class="box-title sysinfo_headline"><i class="fa fa-microchip"></i> ' . lang('Systeminfo_CPU') . '</h3>
</div>
<div class="box-body">
<div class="row">
<div class="col-sm-3 sysinfo_cpu_a">' . lang('Systeminfo_CPU_Vendor') . '</div>
<div class="col-sm-9 sysinfo_cpu_b">' . $cpu_vendor . '</div>
</div>
<div class="row">
<div class="col-sm-3 sysinfo_cpu_a">' . lang('Systeminfo_CPU_Name') . '</div>
<div class="col-sm-9 sysinfo_cpu_b">' . $stat['cpu_model'] . '</div>
</div>
<div class="row">
<div class="col-sm-3 sysinfo_cpu_a">' . lang('Systeminfo_CPU_Cores') . '</div>
<div class="col-sm-9 sysinfo_cpu_b">' . $stat['cpu'] . '</div>
</div>
<div class="row">
<div class="col-sm-3 sysinfo_cpu_a">' . lang('Systeminfo_CPU_Speed') . '</div>
<div class="col-sm-9 sysinfo_cpu_b">' . $stat['cpu_frequ'] . ' MHz</div>
</div>
<div class="row">
<div class="col-sm-3 sysinfo_cpu_a">' . lang('Systeminfo_CPU_Temp') . '</div>
<div class="col-sm-9 sysinfo_cpu_b">'. $cpu_temp .' °C</div>
</div>';
// Get the number of CPU cores
$num_cpus = $stat['cpu'];
$num_cpus = $num_cpus +2;
// Iterate over the CPU cores
for ($i = 2,$a = 0; $i < $num_cpus; $i++,$a++) {
// Get the CPU temperature
$cpu_tempxx = shell_exec('cat /sys/class/hwmon/hwmon0/temp' . $i . '_input');
// Convert the temperature to degrees Celsius
$cpu_tempxx = floatval($cpu_tempxx) / 1000;
// Print the CPU temperature
echo '<div class="row">
<div class="col-sm-3 sysinfo_cpu_a">CPU Temp ' . $a . ':</div>
<div class="col-sm-9 sysinfo_cpu_b">' . $cpu_tempxx . ' °C</div>
</div>';
}
echo '
</div>
</div>';
// Memory ----------------------------------------------------------
echo '<div class="box box-solid">
<div class="box-header">
<h3 class="box-title sysinfo_headline"><i class="fa fa-memory"></i> ' . lang('Systeminfo_Memory') . '</h3>
</div>
<div class="box-body">
<div class="row">
<div class="col-sm-3 sysinfo_memory_a">' . lang('Systeminfo_Memory_Usage_Percent') . '</div>
<div class="col-sm-9 sysinfo_memory_b">' . $memory_usage_percent . ' %</div>
</div>
<div class="row">
<div class="col-sm-3 sysinfo_memory_a">' . lang('Systeminfo_Memory_Usage') . '</div>
<div class="col-sm-9 sysinfo_memory_b">' . $mem_used . ' MB / ' . $total_memorymb . ' MB</div>
</div>
<div class="row">
<div class="col-sm-3 sysinfo_memory_a">' . lang('Systeminfo_Memory_Total_Memory') . '</div>
<div class="col-sm-9 sysinfo_memory_b">' . $total_memorymb . ' MB (' . $total_memorykb . ' KB)</div>
</div>
</div>
</div>';
// Storage ----------------------------------------------------------
echo '<div class="box box-solid">
<div class="box-header">
<h3 class="box-title sysinfo_headline"><i class="fa fa-hdd"></i> ' . lang('Systeminfo_Storage') . '</h3>
</div>
<div class="box-body">';
$storage_lsblk = shell_exec("lsblk -io NAME,SIZE,TYPE,MOUNTPOINT,MODEL --list | tail -n +2 | awk '{print $1\"#\"$2\"#\"$3\"#\"$4\"#\"$5}'");
$storage_lsblk_line = explode("\n", $storage_lsblk);
$storage_lsblk_line = array_filter($storage_lsblk_line);
for ($x = 0; $x < sizeof($storage_lsblk_line); $x++) {
$temp = array();
$temp = explode("#", $storage_lsblk_line[$x]);
$storage_lsblk_line[$x] = $temp;
}
for ($x = 0; $x < sizeof($storage_lsblk_line); $x++) {
echo '<div class="row">';
if (preg_match('~[0-9]+~', $storage_lsblk_line[$x][0])) {
echo '<div class="col-sm-4 sysinfo_storage_a">"' . lang('Systeminfo_Storage_Mount') . ' ' . $storage_lsblk_line[$x][3] . '"</div>';
} else {
echo '<div class="col-sm-4 sysinfo_storage_a">"' . str_replace('_', ' ', $storage_lsblk_line[$x][3]) . '"</div>';
}
echo '<div class="col-sm-3 sysinfo_storage_b">' . lang('Systeminfo_Storage_Device') . ' /dev/' . $storage_lsblk_line[$x][0] . '</div>';
echo '<div class="col-sm-2 sysinfo_storage_b">' . lang('Systeminfo_Storage_Size') . ' ' . $storage_lsblk_line[$x][1] . '</div>';
echo '<div class="col-sm-2 sysinfo_storage_b">' . lang('Systeminfo_Storage_Type') . ' ' . $storage_lsblk_line[$x][2] . '</div>';
echo '</div>';
}
echo ' </div>
</div>';
// Storage usage ----------------------------------------------------------
echo '<div class="box box-solid">
<div class="box-header">
<h3 class="box-title sysinfo_headline"><i class="fa fa-hdd"></i> ' . lang('Systeminfo_Storage_Usage') . '</h3>
</div>
<div class="box-body">';
for ($x = 0; $x < sizeof($hdd_devices); $x++) {
if (stristr($hdd_devices[$x], '/dev/')) {
if (!stristr($hdd_devices[$x], '/loop')) {
if ($hdd_devices_total[$x] == 0 || $hdd_devices_total[$x] == '') {$temp_total = 0;} else { $temp_total = number_format(round(($hdd_devices_total[$x] / 1024 / 1024), 2), 2, ',', '.'); $temp_total = trim($temp_total);}
if ($hdd_devices_used[$x] == 0 || $hdd_devices_used[$x] == '') {$temp_used = 0;} else { $temp_used = number_format(round(($hdd_devices_used[$x] / 1024 / 1024), 2), 2, ',', '.'); $temp_used = trim($temp_total);}
if ($hdd_devices_free[$x] == 0 || $hdd_devices_free[$x] == '') {$temp_free = 0;} else { $temp_free = number_format(round(($hdd_devices_free[$x] / 1024 / 1024), 2), 2, ',', '.'); $temp_free = trim($temp_total);}
echo '<div class="row">';
echo '<div class="col-sm-4 sysinfo_storage_usage_a">"' . lang('Systeminfo_Storage_Usage_Mount') . ' ' . $hdd_devices_mount[$x] . '"</div>';
echo '<div class="col-sm-2 sysinfo_storage_usage_b">' . lang('Systeminfo_Storage_Usage_Total') . ' ' . $temp_total . ' GB</div>';
echo '<div class="col-sm-3 sysinfo_storage_usage_b">' . lang('Systeminfo_Storage_Usage_Used') . ' ' . $temp_used . ' GB (' . $hdd_devices_percent[$x]. ')</div>';
echo '<div class="col-sm-2 sysinfo_storage_usage_b">' . lang('Systeminfo_Storage_Usage_Free') . ' ' . $temp_free . ' GB</div>';
echo '</div>';
}
}
}
#echo '<br>' . $lang['SysInfo_storage_note'];
echo ' </div>
</div>';
// Network ----------------------------------------------------------
echo '<div class="box box-solid">
<div class="box-header">
<h3 class="box-title sysinfo_headline"><i class="fas fa-ethernet"></i> ' . lang('Systeminfo_Network') . '</h3>
</div>
<div class="box-body">
<div class="row">
<div class="col-sm-3 sysinfo_network_a">' . lang('Systeminfo_Network_IP') . '</div>
<div class="col-sm-9 sysinfo_network_b">' . shell_exec("curl https://ifconfig.co") . '</div>
</div>
<div class="row">
<div class="col-sm-3 sysinfo_network_a">' . lang('Systeminfo_Network_IP_Connection') . '</div>
<div class="col-sm-9 sysinfo_network_b">' . $_SERVER['REMOTE_ADDR'] . '</div>
</div>
<div class="row">
<div class="col-sm-3 sysinfo_network_a">' . lang('Systeminfo_Network_IP_Server') . '</div>
<div class="col-sm-9 sysinfo_network_b">' . $_SERVER['SERVER_ADDR'] . '</div>
</div>
<div class="row">
<div class="col-sm-3 sysinfo_network_a">' . lang('Systeminfo_Network_Server_Name') . '</div>
<div class="col-sm-9 sysinfo_network_b">' . $network_NAME . '</div>
</div>
<div class="row">
<div class="col-sm-3 sysinfo_network_a">' . lang('Systeminfo_Network_Connection_Port') . '</div>
<div class="col-sm-9 sysinfo_network_b">' . $_SERVER['REMOTE_PORT'] . '</div>
</div>
<div class="row">
<div class="col-sm-3 sysinfo_network_a">' . lang('Systeminfo_Network_Secure_Connection') . '</div>
<div class="col-sm-9 sysinfo_network_b">' . $network_HTTPS . '</div>
</div>
<div class="row">
<div class="col-sm-3 sysinfo_network_a">' . lang('Systeminfo_Network_Server_Version') . '</div>
<div class="col-sm-9 sysinfo_network_b">' . $_SERVER['SERVER_SOFTWARE'] . '</div>
</div>
<div class="row">
<div class="col-sm-3 sysinfo_gerneral_a">' . lang('Systeminfo_Network_Request_URI') . '</div>
<div class="col-sm-9 sysinfo_gerneral_b">' . $_SERVER['REQUEST_URI'] . '</div>
</div>
<div class="row">
<div class="col-sm-3 sysinfo_network_a">' . lang('Systeminfo_Network_Server_Query') . '</div>
<div class="col-sm-9 sysinfo_network_b">' . $network_QueryString . '</div>
</div>
<div class="row">
<div class="col-sm-3 sysinfo_network_a">' . lang('Systeminfo_Network_HTTP_Host') . '</div>
<div class="col-sm-9 sysinfo_network_b">' . $_SERVER['HTTP_HOST'] . '</div>
</div>
<div class="row">
<div class="col-sm-3 sysinfo_network_a">' . lang('Systeminfo_Network_HTTP_Referer') . '</div>
<div class="col-sm-9 sysinfo_network_b">' . $network_referer . '</div>
</div>
<div class="row">
<div class="col-sm-3 sysinfo_network_a">' . lang('Systeminfo_Network_MIME') . '</div>
<div class="col-sm-9 sysinfo_network_b">' . $_SERVER['HTTP_ACCEPT'] . '</div>
</div>
<div class="row">
<div class="col-sm-3 sysinfo_network_a">' . lang('Systeminfo_Network_Accept_Language') . '</div>
<div class="col-sm-9 sysinfo_network_b">' . $_SERVER['HTTP_ACCEPT_LANGUAGE'] . '</div>
</div>
<div class="row">
<div class="col-sm-3 sysinfo_network_a">' . lang('Systeminfo_Network_Accept_Encoding') . '</div>
<div class="col-sm-9 sysinfo_network_b">' . $_SERVER['HTTP_ACCEPT_ENCODING'] . '</div>
</div>
<div class="row">
<div class="col-sm-3 sysinfo_network_a">' . lang('Systeminfo_Network_Request_Method') . '</div>
<div class="col-sm-9 sysinfo_network_b">' . $_SERVER['REQUEST_METHOD'] . '</div>
</div>
<div class="row">
<div class="col-sm-3 sysinfo_network_a">' . lang('Systeminfo_Network_Request_Time') . '</div>
<div class="col-sm-9 sysinfo_network_b">' . $_SERVER['REQUEST_TIME'] . '</div>
</div>
</div>
</div>';
// Services ----------------------------------------------------------
echo '<div class="box box-solid">
<div class="box-header">
<h3 class="box-title sysinfo_headline"><i class="fa fa-database"></i> ' . lang('Systeminfo_Services') . '</h3>
</div>
<div class="box-body">';
echo '<div style="height: 300px; overflow: scroll;">';
exec('systemctl --type=service --state=running', $running_services);
echo '<table class="table table-bordered table-hover table-striped dataTable no-footer" style="margin-bottom: 10px;">';
echo '<thead>
<tr role="row">
<th style="padding: 8px;">' . lang('Systeminfo_Services_Name') . '</th>
<th style="padding: 8px;">' . lang('Systeminfo_Services_Description') . '</th>
</tr>
</thead>';
$table_color = 'odd';
for ($x = 0; $x < sizeof($running_services); $x++) {
if (stristr($running_services[$x], '.service')) {
$temp_services_arr = array_values(array_filter(explode(' ', trim($running_services[$x]))));
$servives_name = $temp_services_arr[0];
unset($temp_services_arr[0], $temp_services_arr[1], $temp_services_arr[2], $temp_services_arr[3]);
$servives_description = implode(" ", $temp_services_arr);
if ($table_color == 'odd') {$table_color = 'even';} else { $table_color = 'odd';}
echo '<tr class="' . $table_color . '"><td style="padding: 3px; padding-left: 10px;">' . substr($servives_name, 0, -8) . '</td><td style="padding: 3px; padding-left: 10px;">' . $servives_description . '</td></tr>';
}
}
echo '</table>';
echo '</div>';
echo ' </div>
</div>';
// USB ----------------------------------------------------------
echo '<div class="box box-solid">
<div class="box-header">
<h3 class="box-title sysinfo_headline"><i class="fab fa-usb"></i> ' . lang('Systeminfo_USB_Devices') . '</h3>
</div>
<div class="box-body">';
echo ' <table class="table table-bordered table-hover table-striped dataTable no-footer" style="margin-bottom: 10px;">';
$table_color = 'odd';
sort($usb_devices_mount);
for ($x = 0; $x < sizeof($usb_devices_mount); $x++) {
$cut_pos = strpos($usb_devices_mount[$x], ':');
$usb_bus = substr($usb_devices_mount[$x], 0, $cut_pos);
$usb_dev = substr($usb_devices_mount[$x], $cut_pos + 1);
if ($table_color == 'odd') {$table_color = 'even';} else { $table_color = 'odd';}
echo '<tr class="' . $table_color . '"><td style="padding: 3px; padding-left: 10px; width: 150px;"><b>' . str_replace('Device', 'Dev.', $usb_bus) . '</b></td><td style="padding: 3px; padding-left: 10px;">' . $usb_dev . '</td></tr>';
}
echo ' </table>';
echo ' </div>
</div>';
// ----------------------------------------------------------
echo '<br>';
?>
</div>
</section>
</section>
<!-- /.content -->
<?php
require 'php/templates/footer.php';
?>
</div>
<!-- /.content-wrapper -->
<?php
require 'php/templates/footer.php';
?>
<!-- ----------------------------------------------------------------------- -->
<!-- DataTable initialization -->
<script>
// --------------------------------------------------------
// Available free IPS functionality
function inferNetworkRange(usedIps) {
if (!usedIps || usedIps.length === 0) return [];
function loadTabContent(target) {
const $tab = $(target);
const phpFile = $tab.data('php-file');
const subnetMap = {};
if (phpFile && !$tab.data('loaded')) {
showSpinner();
$tab.load(phpFile, function () {
$tab.data('loaded', true);
});
}
}
// Group IPs by /24 subnet
for (const ip of usedIps) {
const parts = ip.split('.');
if (parts.length !== 4) continue;
const subnet = `${parts[0]}.${parts[1]}.${parts[2]}`;
if (!subnetMap[subnet]) subnetMap[subnet] = [];
subnetMap[subnet].push(ip);
function initializeTabs() {
const key = "activeSysinfoTab";
let selectedTab = "tabServer"; // fallback default
const cached = getCache(key);
if (!emptyArr.includes(cached)) {
selectedTab = cached;
}
const result = [];
// Activate the correct tab
const $tabLink = $('.nav-tabs a[id="' + selectedTab + '"]');
$tabLink.tab('show');
for (const [subnet, ips] of Object.entries(subnetMap)) {
if (ips.length > 5) {
for (let i = 2; i < 255; i++) {
const ip = `${subnet}.${i}`;
result.push({ subnet, ip });
}
}
}
// Get the pane's ID from the tab's href attribute
const targetSelector = $tabLink.attr("href");
loadTabContent(targetSelector);
return result;
}
// On tab change
$('a[data-toggle="tab"]').on('shown.bs.tab', function (e) {
const newTabId = $(e.target).attr('id');
setCache(key, newTabId);
function fetchUsedIps(callback) {
$.ajax({
url: 'php/server/query_graphql.php',
type: 'POST',
contentType: 'application/json',
data: JSON.stringify({
query: `
query devices($options: PageQueryOptionsInput) {
devices(options: $options) {
devices {
devLastIP
}
}
}
`,
variables: {
options: {
status: "all_devices"
}
}
}),
success: function(response) {
console.log(response);
const usedIps = (response?.devices?.devices || [])
.map(d => d.devLastIP)
.filter(ip => ip && ip.includes('.'));
callback(usedIps);
},
error: function(err) {
console.error("Error fetching IPs:", err);
callback([]);
}
const newTarget = $(e.target).attr("href");
loadTabContent(newTarget);
});
}
function renderAvailableIpsTable(allIps, usedIps) {
const availableIps = allIps.filter(row => !usedIps.includes(row.ip));
console.log(allIps);
console.log(usedIps);
console.log(availableIps);
$('#availableIpsTable').DataTable({
destroy: true,
data: availableIps,
columns: [
{ title: getString("Gen_Subnet"), data: "subnet" },
{ title: getString("Systeminfo_AvailableIps"), data: "ip" }
],
pageLength: 10
});
window.onload = function async() {
initializeTabs();
}
// INIT
$(document).ready(function() {
fetchUsedIps(usedIps => {
const allIps = inferNetworkRange(usedIps);
renderAvailableIpsTable(allIps, usedIps);
});
setTimeout(() => {
$('#networkTable').DataTable({
searching: true,
order: [[0, "desc"]],
initComplete: function(settings, json) {
hideSpinner(); // Called after the DataTable is fully initialized
}
});
}, 200);
});
</script>

316
front/systeminfoNetwork.php Executable file
View File

@@ -0,0 +1,316 @@
<?php
//------------------------------------------------------------------------------
// check if authenticated
require_once $_SERVER['DOCUMENT_ROOT'] . '/php/templates/security.php';
require_once $_SERVER['DOCUMENT_ROOT'] . '/php/server/db.php';
require_once $_SERVER['DOCUMENT_ROOT'] . '/php/templates/language/lang.php';
?>
<?php
function getExternalIp() {
$ch = curl_init('https://api64.ipify.org?format=json');
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_TIMEOUT, 10);
$response = curl_exec($ch);
if (curl_errno($ch)) {
curl_close($ch);
return 'ERROR: ' . curl_error($ch);
}
curl_close($ch);
$data = json_decode($response, true);
if (isset($data['ip'])) {
return htmlspecialchars($data['ip']);
}
return 'ERROR: Invalid response';
}
// ----------------------------------------------------------
// Network
// ----------------------------------------------------------
//Network stats
// Server IP
$externalIp = getExternalIp();
// Check Server name
if (!empty(gethostname())) { $network_NAME = gethostname(); } else { $network_NAME = lang('Systeminfo_Network_Server_Name_String'); }
// Check HTTPS
if (isset($_SERVER['HTTPS'])) { $network_HTTPS = 'Yes (HTTPS)'; } else { $network_HTTPS = lang('Systeminfo_Network_Secure_Connection_String'); }
// Check Query String
if (empty($_SERVER['QUERY_STRING'])) { $network_QueryString = lang('Systeminfo_Network_Server_Query_String'); } else { $network_QueryString = $_SERVER['QUERY_STRING']; }
// Check HTTP referer
if (empty($_SERVER['HTTP_REFERER'])) { $network_referer = lang('Systeminfo_Network_HTTP_Referer_String'); } else { $network_referer = $_SERVER['HTTP_REFERER']; }
//Network Hardware stat
$network_result = shell_exec("cat /proc/net/dev | tail -n +3 | awk '{print $1}'");
$net_interfaces = explode("\n", trim($network_result));
$network_result = shell_exec("cat /proc/net/dev | tail -n +3 | awk '{print $2}'");
$net_interfaces_rx = explode("\n", trim($network_result));
$network_result = shell_exec("cat /proc/net/dev | tail -n +3 | awk '{print $10}'");
$net_interfaces_tx = explode("\n", trim($network_result));
// Network Hardware ----------------------------------------------------------
echo '<div class="box box-solid">
<div class="box-header">
<h3 class="box-title sysinfo_headline"><i class="fa fa-sitemap fa-rotate-270"></i> ' . lang('Systeminfo_Network_Hardware') . '</h3>
</div>
<div class="box-body">
<table id="networkTable" class="table table-bordered table-hover">
<thead>
<tr>
<th>' . lang('Systeminfo_Network_Hardware_Interface_Name') . '</th>
<th>' . lang('Systeminfo_Network_Hardware_Interface_Mask') . '</th>
<th>' . lang('Systeminfo_Network_Hardware_Interface_RX') . '</th>
<th>' . lang('Systeminfo_Network_Hardware_Interface_TX') . '</th>
</tr>
</thead>
<tbody>';
for ($x = 0; $x < sizeof($net_interfaces); $x++) {
$interface_name = str_replace(':', '', $net_interfaces[$x]);
$interface_ip_temp = exec('ip addr show ' . $interface_name . ' | grep "inet "');
$interface_ip_arr = explode(' ', trim($interface_ip_temp));
if (!isset($interface_ip_arr[1])) {
$interface_ip_arr[1] = '--';
}
if ($net_interfaces_rx[$x] == 0) {
$temp_rx = 0;
} else {
$temp_rx = number_format(round(($net_interfaces_rx[$x] / 1024 / 1024), 2), 2, ',', '.');
}
if ($net_interfaces_tx[$x] == 0) {
$temp_tx = 0;
} else {
$temp_tx = number_format(round(($net_interfaces_tx[$x] / 1024 / 1024), 2), 2, ',', '.');
}
echo '<tr>';
echo '<td>' . $interface_name . '</td>';
echo '<td>' . $interface_ip_arr[1] . '</td>';
echo '<td>' . $temp_rx . ' MB</td>';
echo '<td>' . $temp_tx . ' MB</td>';
echo '</tr>';
}
echo ' </tbody>
</table>
</div>
</div>';
// Available IPs ----------------------------------------------------------
echo '<div class="box box-solid">
<div class="box-header">
<h3 class="box-title availableips_headline"><i class="fa fa-list"></i> ' . lang('Systeminfo_AvailableIps') . '</h3>
</div>
<div class="box-body">
<table id="availableIpsTable" class="display table table-bordered table-hover dataTable no-footer" style="width:100%"></table>
</div>
</div>';
// Network ----------------------------------------------------------
echo '<div class="box box-solid">
<div class="box-header">
<h3 class="box-title sysinfo_headline"><i class="fas fa-ethernet"></i> ' . lang('Systeminfo_Network') . '</h3>
</div>
<div class="box-body">
<div class="row">
<div class="col-sm-3 sysinfo_network_a">' . lang('Systeminfo_Network_IP') . '</div>
<div class="col-sm-9 sysinfo_network_b" id="external-ip">' .$externalIp. '</div>
</div>
<div class="row">
<div class="col-sm-3 sysinfo_network_a">' . lang('Systeminfo_Network_IP_Connection') . '</div>
<div class="col-sm-9 sysinfo_network_b">' . $_SERVER['REMOTE_ADDR'] . '</div>
</div>
<div class="row">
<div class="col-sm-3 sysinfo_network_a">' . lang('Systeminfo_Network_IP_Server') . '</div>
<div class="col-sm-9 sysinfo_network_b">' . $_SERVER['SERVER_ADDR'] . '</div>
</div>
<div class="row">
<div class="col-sm-3 sysinfo_network_a">' . lang('Systeminfo_Network_Server_Name') . '</div>
<div class="col-sm-9 sysinfo_network_b">' . $network_NAME . '</div>
</div>
<div class="row">
<div class="col-sm-3 sysinfo_network_a">' . lang('Systeminfo_Network_Connection_Port') . '</div>
<div class="col-sm-9 sysinfo_network_b">' . $_SERVER['REMOTE_PORT'] . '</div>
</div>
<div class="row">
<div class="col-sm-3 sysinfo_network_a">' . lang('Systeminfo_Network_Secure_Connection') . '</div>
<div class="col-sm-9 sysinfo_network_b">' . $network_HTTPS . '</div>
</div>
<div class="row">
<div class="col-sm-3 sysinfo_network_a">' . lang('Systeminfo_Network_Server_Version') . '</div>
<div class="col-sm-9 sysinfo_network_b">' . $_SERVER['SERVER_SOFTWARE'] . '</div>
</div>
<div class="row">
<div class="col-sm-3 sysinfo_gerneral_a">' . lang('Systeminfo_Network_Request_URI') . '</div>
<div class="col-sm-9 sysinfo_gerneral_b">' . $_SERVER['REQUEST_URI'] . '</div>
</div>
<div class="row">
<div class="col-sm-3 sysinfo_network_a">' . lang('Systeminfo_Network_Server_Query') . '</div>
<div class="col-sm-9 sysinfo_network_b">' . $network_QueryString . '</div>
</div>
<div class="row">
<div class="col-sm-3 sysinfo_network_a">' . lang('Systeminfo_Network_HTTP_Host') . '</div>
<div class="col-sm-9 sysinfo_network_b">' . $_SERVER['HTTP_HOST'] . '</div>
</div>
<div class="row">
<div class="col-sm-3 sysinfo_network_a">' . lang('Systeminfo_Network_HTTP_Referer') . '</div>
<div class="col-sm-9 sysinfo_network_b">' . $network_referer . '</div>
</div>
<div class="row">
<div class="col-sm-3 sysinfo_network_a">' . lang('Systeminfo_Network_MIME') . '</div>
<div class="col-sm-9 sysinfo_network_b">' . $_SERVER['HTTP_ACCEPT'] . '</div>
</div>
<div class="row">
<div class="col-sm-3 sysinfo_network_a">' . lang('Systeminfo_Network_Accept_Language') . '</div>
<div class="col-sm-9 sysinfo_network_b">' . $_SERVER['HTTP_ACCEPT_LANGUAGE'] . '</div>
</div>
<div class="row">
<div class="col-sm-3 sysinfo_network_a">' . lang('Systeminfo_Network_Accept_Encoding') . '</div>
<div class="col-sm-9 sysinfo_network_b">' . $_SERVER['HTTP_ACCEPT_ENCODING'] . '</div>
</div>
<div class="row">
<div class="col-sm-3 sysinfo_network_a">' . lang('Systeminfo_Network_Request_Method') . '</div>
<div class="col-sm-9 sysinfo_network_b">' . $_SERVER['REQUEST_METHOD'] . '</div>
</div>
<div class="row">
<div class="col-sm-3 sysinfo_network_a">' . lang('Systeminfo_Network_Request_Time') . '</div>
<div class="col-sm-9 sysinfo_network_b">' . $_SERVER['REQUEST_TIME'] . '</div>
</div>
</div>
</div>';
?>
<script>
// --------------------------------------------------------
// Available free IPS functionality
function inferNetworkRange(usedIps) {
if (!usedIps || usedIps.length === 0) return [];
const subnetMap = {};
// Group IPs by /24 subnet
for (const ip of usedIps) {
const parts = ip.split('.');
if (parts.length !== 4) continue;
const subnet = `${parts[0]}.${parts[1]}.${parts[2]}`;
if (!subnetMap[subnet]) subnetMap[subnet] = [];
subnetMap[subnet].push(ip);
}
const result = [];
for (const [subnet, ips] of Object.entries(subnetMap)) {
if (ips.length > 5) {
for (let i = 2; i < 255; i++) {
const ip = `${subnet}.${i}`;
result.push({ subnet, ip });
}
}
}
return result;
}
function fetchUsedIps(callback) {
$.ajax({
url: 'php/server/query_graphql.php',
type: 'POST',
contentType: 'application/json',
data: JSON.stringify({
query: `
query devices($options: PageQueryOptionsInput) {
devices(options: $options) {
devices {
devLastIP
}
}
}
`,
variables: {
options: {
status: "all_devices"
}
}
}),
success: function(response) {
console.log(response);
const usedIps = (response?.devices?.devices || [])
.map(d => d.devLastIP)
.filter(ip => ip && ip.includes('.'));
callback(usedIps);
},
error: function(err) {
console.error("Error fetching IPs:", err);
callback([]);
}
});
}
function renderAvailableIpsTable(allIps, usedIps) {
const availableIps = allIps.filter(row => !usedIps.includes(row.ip));
console.log(availableIps);
$('#availableIpsTable').DataTable({
destroy: true,
data: availableIps,
columns: [
{
title: getString("Gen_Subnet"),
data: "subnet"
},
{
title: getString("Systeminfo_AvailableIps"),
data: "ip",
render: function (data, type, row, meta) {
return `
<span>${data}</span>
<button class="copy-btn btn btn-sm btn-info ml-2 alignRight" data-text="${data}" title="${getString("Gen_CopyToClipboard")}" onclick="copyToClipboard(this)">
<i class="fa-solid fa-copy"></i>
</button>
`;
}
}
],
pageLength: 10
});
}
// INIT
$(document).ready(function() {
// available IPs
fetchUsedIps(usedIps => {
const allIps = inferNetworkRange(usedIps);
renderAvailableIpsTable(allIps, usedIps);
});
setTimeout(() => {
// Available IPs datatable
$('#networkTable').DataTable({
searching: true,
order: [[0, "desc"]],
initComplete: function(settings, json) {
hideSpinner(); // Called after the DataTable is fully initialized
}
});
}, 200);
});
</script>

327
front/systeminfoServer.php Executable file
View File

@@ -0,0 +1,327 @@
<?php
//------------------------------------------------------------------------------
// check if authenticated
require_once $_SERVER['DOCUMENT_ROOT'] . '/php/templates/security.php';
require_once $_SERVER['DOCUMENT_ROOT'] . '/php/server/db.php';
require_once $_SERVER['DOCUMENT_ROOT'] . '/php/templates/language/lang.php';
?>
<?php
// ----------------------------------------------------------
// Server
// ----------------------------------------------------------
//General stats
// Date & Time
$date = new DateTime();
$formatted_date = $date->format('l, F j, Y H:i:s'); // Get date
$formatted_date2 = $date->format('d/m/Y H:i:s'); // Get date2
$formatted_date3 = $date->format('Y/m/d H:i:s'); // Get date3
//System stats
// OS-Version
$os_version = '';
// Raspbian
if ($os_version == '') {$os_version = exec('cat /etc/os-release | grep PRETTY_NAME');}
// Dietpi
if ($os_version == '') {$os_version = exec('uname -o');}
//$os_version_arr = explode("\n", trim($os_version));
$stat['os_version'] = str_replace('"', '', str_replace('PRETTY_NAME=', '', $os_version));
$stat['uptime'] = str_replace('up ', '', shell_exec("uptime -p")); // Get system uptime
$system_namekernel = shell_exec("uname"); // Get system name kernel
$system_namesystem = shell_exec("uname -o"); // Get name system
$system_full = shell_exec("uname -a"); // Get system full
$system_architecture = shell_exec("uname -m"); // Get system Architecture
$load_average = sys_getloadavg(); // Get load average
$system_process_count = shell_exec("ps -e --no-headers | wc -l"); // Count processes
//Motherboard stats
$motherboard_name = shell_exec('cat /sys/class/dmi/id/board_name'); // Get the Motherboard name
$motherboard_manufactured = shell_exec('cat /sys/class/dmi/id/board_vendor'); // Get the Motherboard manufactured
$motherboard_revision = shell_exec('cat /sys/class/dmi/id/board_version'); // Get the Motherboard revision
$motherboard_bios = shell_exec('cat /sys/class/dmi/id/bios_version'); // Get the Motherboard BIOS
$motherboard_biosdate = shell_exec('cat /sys/class/dmi/id/bios_date'); // Get the Motherboard BIOS date
$motherboard_biosvendor = shell_exec('cat /sys/class/dmi/id/bios_vendor'); // Get the Motherboard BIOS vendor
//CPU stats
$prevVal = shell_exec("cat /proc/cpuinfo | grep processor");
$prevArr = explode("\n", trim($prevVal));
$stat['cpu'] = sizeof($prevArr);
$cpu_result = shell_exec("cat /proc/cpuinfo | grep Model");
$stat['cpu_model'] = strstr($cpu_result, "\n", true);
$stat['cpu_model'] = str_replace(":", "", trim(str_replace("Model", "", $stat['cpu_model'])));
if ($stat['cpu_model'] == '') {
$cpu_result = shell_exec("cat /proc/cpuinfo | grep model\ name");
$stat['cpu_model'] = strstr($cpu_result, "\n", true);
$stat['cpu_model'] = str_replace(":", "", trim(str_replace("model name", "", $stat['cpu_model'])));
}
if (file_exists('/sys/devices/system/cpu/cpu0/cpufreq/scaling_max_freq')) {
// RaspbianOS
$stat['cpu_frequ'] = exec('cat /sys/devices/system/cpu/cpu0/cpufreq/scaling_max_freq') / 1000;
} elseif (is_numeric(str_replace(',', '.', exec('lscpu | grep "MHz" | awk \'{print $3}\'')))) {
// Ubuntu Server, DietPi event. others
$stat['cpu_frequ'] = round(exec('lscpu | grep "MHz" | awk \'{print $3}\''), 0);
} elseif (is_numeric(str_replace(',', '.', exec('lscpu | grep "max MHz" | awk \'{print $4}\'')))) {
// RaspbianOS and event. others
$stat['cpu_frequ'] = round(str_replace(',', '.', exec('lscpu | grep "max MHz" | awk \'{print $4}\'')), 0);
} else {
// Fallback
$stat['cpu_frequ'] = "unknown";
}
$cpu_temp = shell_exec('cat /sys/class/hwmon/hwmon0/temp1_input'); // Get the CPU temperature
$cpu_temp = floatval($cpu_temp) / 1000; // Convert the temperature to degrees Celsius
$cpu_vendor = exec('cat /proc/cpuinfo | grep "vendor_id" | cut -d ":" -f 2' ); // Get the CPU vendor
//Memory stats
$total_memorykb = shell_exec("cat /proc/meminfo | grep MemTotal | awk '{print $2}'");
$total_memorykb = trim($total_memorykb);
$total_memorykb = number_format($total_memorykb, 0, '.', '.');
$total_memorymb = shell_exec("cat /proc/meminfo | grep MemTotal | awk '{print $2/1024}'");
$total_memorymb = trim($total_memorymb);
$total_memorymb = number_format($total_memorymb, 0, '.', '.');
$mem_used = round(memory_get_usage() / 1048576 * 100, 2);
$memory_usage_percent = round(($mem_used / $total_memorymb), 2);
// General ----------------------------------------------------------
echo '<div class="box box-solid">
<div class="box-header">
<h3 class="box-title sysinfo_headline"><i class="fa fa-info-circle"></i> ' . lang('Systeminfo_General') . '</h3>
</div>
<div class="box-body">
<div class="row">
<div class="col-sm-3 sysinfo_general_a">' . lang('Systeminfo_General_Full_Date') . '</div>
<div class="col-sm-9 sysinfo_general_b">' . $formatted_date . '</div>
</div>
<div class="row">
<div class="col-sm-3 sysinfo_general_a">' . lang('Systeminfo_General_Date') . '</div>
<div class="col-sm-9 sysinfo_general_b">' . $formatted_date2 . '</div>
</div>
<div class="row">
<div class="col-sm-3 sysinfo_general_a">' . lang('Systeminfo_General_Date2') . '</div>
<div class="col-sm-9 sysinfo_general_b">' . $formatted_date3 . '</div>
</div>
<div class="row">
<div class="col-sm-3 sysinfo_general_a">' . lang('Systeminfo_General_TimeZone') . '</div>
<div class="col-sm-9 sysinfo_general_b">' . $timeZone . '</div>
</div>
</div>
</div>';
// Client ----------------------------------------------------------
echo '<div class="box box-solid">
<div class="box-header">
<h3 class="box-title sysinfo_headline"><i class="fa fa-globe"></i> ' . lang('Systeminfo_This_Client') . '</h3>
</div>
<div class="box-body">
<div class="row">
<div class="col-sm-3 sysinfo_client_a">' . lang('Systeminfo_Client_User_Agent') . '</div>
<div class="col-sm-9 sysinfo_client_b">' . $_SERVER['HTTP_USER_AGENT'] . '</div>
</div>
<div class="row">
<div class="col-sm-3 sysinfo_client_a">' . lang('Systeminfo_Client_Resolution') . '</div>
<div class="col-sm-9 sysinfo_client_b" id="resolution"></div>
</div>
</div>
</div>';
echo '<script>
var ratio = window.devicePixelRatio || 1;
var w = window.innerWidth;
var h = window.innerHeight;
var rw = window.innerWidth * ratio;
var rh = window.innerHeight * ratio;
var resolutionDiv = document.getElementById("resolution");
resolutionDiv.innerHTML = "Width: " + w + "px / Height: " + h + "px<br> " + "Width: " + rw + "px / Height: " + rh + "px (native)";
</script>';
// System ----------------------------------------------------------
echo '<div class="box box-solid">
<div class="box-header">
<h3 class="box-title sysinfo_headline"><i class="fa fa-computer"></i> ' . lang('Systeminfo_System') . '</h3>
</div>
<div class="box-body">
<div class="row">
<div class="col-sm-3 sysinfo_system_a">' . lang('Systeminfo_System_Uptime') . '</div>
<div class="col-sm-9 sysinfo_system_b">' . $stat['uptime'] . '</div>
</div>
<div class="row">
<div class="col-sm-3 sysinfo_system_a">' . lang('Systeminfo_System_Kernel') . '</div>
<div class="col-sm-9 sysinfo_system_b">' . $system_namekernel . '</div>
</div>
<div class="row">
<div class="col-sm-3 sysinfo_system_a">' . lang('Systeminfo_System_System') . '</div>
<div class="col-sm-9 sysinfo_system_b">' . $system_namesystem . '</div>
</div>
<div class="row">
<div class="col-sm-3 sysinfo_system_a">' . lang('Systeminfo_System_OSVersion') . '</div>
<div class="col-sm-9 sysinfo_system_b">' . $stat['os_version'] . '</div>
</div>
<div class="row">
<div class="col-sm-3 sysinfo_system_a">' . lang('Systeminfo_System_Uname') . '</div>
<div class="col-sm-9 sysinfo_system_b">' . $system_full . '</div>
</div>
<div class="row">
<div class="col-sm-3 sysinfo_system_a">' . lang('Systeminfo_System_Architecture') . '</div>
<div class="col-sm-9 sysinfo_system_b">' . $system_architecture . '</div>
</div>
<div class="row">
<div class="col-sm-3 sysinfo_system_a">' . lang('Systeminfo_System_AVG') . '</div>
<div class="col-sm-9 sysinfo_system_b">'. $load_average[0] .' '. $load_average[1] .' '. $load_average[2] .'</div>
</div>
<div class="row">
<div class="col-sm-3 sysinfo_system_a">' . lang('Systeminfo_System_Running_Processes') . '</div>
<div class="col-sm-9 sysinfo_system_b">' . $system_process_count . '</div>
</div>
</div>
</div>';
// Motherboard ----------------------------------------------------------
echo '<div class="box box-solid">
<div class="box-header">
<h3 class="box-title sysinfo_headline"><i class="fa fa-laptop-code"></i> ' . lang('Systeminfo_Motherboard') . '</h3>
</div>
<div class="box-body">
<div class="row">
<div class="col-sm-3 sysinfo_motherboard_a">' . lang('Systeminfo_Motherboard_Name') . '</div>
<div class="col-sm-9 sysinfo_motherboard_b">' . $motherboard_name . '</div>
</div>
<div class="row">
<div class="col-sm-3 sysinfo_motherboard_a">' . lang('Systeminfo_Motherboard_Manufactured') . '</div>
<div class="col-sm-9 sysinfo_motherboard_b">' . $motherboard_manufactured . '</div>
</div>
<div class="row">
<div class="col-sm-3 sysinfo_motherboard_a">' . lang('Systeminfo_Motherboard_Revision') . '</div>
<div class="col-sm-9 sysinfo_motherboard_b">' . $motherboard_revision. '</div>
</div>
<div class="row">
<div class="col-sm-3 sysinfo_motherboard_a">' . lang('Systeminfo_Motherboard_BIOS') . '</div>
<div class="col-sm-9 sysinfo_motherboard_b">' . $motherboard_bios . '</div>
</div>
<div class="row">
<div class="col-sm-3 sysinfo_motherboard_a">' . lang('Systeminfo_Motherboard_BIOS_Date') . '</div>
<div class="col-sm-9 sysinfo_motherboard_b">' . $motherboard_biosdate . '</div>
</div>
<div class="row">
<div class="col-sm-3 sysinfo_motherboard_a">' . lang('Systeminfo_Motherboard_BIOS_Vendor') . '</div>
<div class="col-sm-9 sysinfo_motherboard_b">' . $motherboard_biosvendor . '</div>
</div>
</div>
</div>';
// CPU ----------------------------------------------------------
echo '<div class="box box-solid">
<div class="box-header">
<h3 class="box-title sysinfo_headline"><i class="fa fa-microchip"></i> ' . lang('Systeminfo_CPU') . '</h3>
</div>
<div class="box-body">
<div class="row">
<div class="col-sm-3 sysinfo_cpu_a">' . lang('Systeminfo_CPU_Vendor') . '</div>
<div class="col-sm-9 sysinfo_cpu_b">' . $cpu_vendor . '</div>
</div>
<div class="row">
<div class="col-sm-3 sysinfo_cpu_a">' . lang('Systeminfo_CPU_Name') . '</div>
<div class="col-sm-9 sysinfo_cpu_b">' . $stat['cpu_model'] . '</div>
</div>
<div class="row">
<div class="col-sm-3 sysinfo_cpu_a">' . lang('Systeminfo_CPU_Cores') . '</div>
<div class="col-sm-9 sysinfo_cpu_b">' . $stat['cpu'] . '</div>
</div>
<div class="row">
<div class="col-sm-3 sysinfo_cpu_a">' . lang('Systeminfo_CPU_Speed') . '</div>
<div class="col-sm-9 sysinfo_cpu_b">' . $stat['cpu_frequ'] . ' MHz</div>
</div>
<div class="row">
<div class="col-sm-3 sysinfo_cpu_a">' . lang('Systeminfo_CPU_Temp') . '</div>
<div class="col-sm-9 sysinfo_cpu_b">'. $cpu_temp .' °C</div>
</div>';
// Get the number of CPU cores
$num_cpus = $stat['cpu'];
$num_cpus = $num_cpus +2;
// Iterate over the CPU cores
for ($i = 2,$a = 0; $i < $num_cpus; $i++,$a++) {
// Get the CPU temperature
$cpu_tempxx = shell_exec('cat /sys/class/hwmon/hwmon0/temp' . $i . '_input');
// Convert the temperature to degrees Celsius
$cpu_tempxx = floatval($cpu_tempxx) / 1000;
// Print the CPU temperature
echo '<div class="row">
<div class="col-sm-3 sysinfo_cpu_a">CPU Temp ' . $a . ':</div>
<div class="col-sm-9 sysinfo_cpu_b">' . $cpu_tempxx . ' °C</div>
</div>';
}
echo '
</div>
</div>';
// Memory ----------------------------------------------------------
echo '<div class="box box-solid">
<div class="box-header">
<h3 class="box-title sysinfo_headline"><i class="fa fa-memory"></i> ' . lang('Systeminfo_Memory') . '</h3>
</div>
<div class="box-body">
<div class="row">
<div class="col-sm-3 sysinfo_memory_a">' . lang('Systeminfo_Memory_Usage_Percent') . '</div>
<div class="col-sm-9 sysinfo_memory_b">' . $memory_usage_percent . ' %</div>
</div>
<div class="row">
<div class="col-sm-3 sysinfo_memory_a">' . lang('Systeminfo_Memory_Usage') . '</div>
<div class="col-sm-9 sysinfo_memory_b">' . $mem_used . ' MB / ' . $total_memorymb . ' MB</div>
</div>
<div class="row">
<div class="col-sm-3 sysinfo_memory_a">' . lang('Systeminfo_Memory_Total_Memory') . '</div>
<div class="col-sm-9 sysinfo_memory_b">' . $total_memorymb . ' MB (' . $total_memorykb . ' KB)</div>
</div>
</div>
</div>';
// Services ----------------------------------------------------------
echo '<div class="box box-solid">
<div class="box-header">
<h3 class="box-title sysinfo_headline"><i class="fa fa-database"></i> ' . lang('Systeminfo_Services') . '</h3>
</div>
<div class="box-body">';
echo '<div style="height: 300px; overflow: scroll;">';
exec('systemctl --type=service --state=running', $running_services);
echo '<table class="table table-bordered table-hover table-striped dataTable no-footer" style="margin-bottom: 10px;">';
echo '<thead>
<tr role="row">
<th style="padding: 8px;">' . lang('Systeminfo_Services_Name') . '</th>
<th style="padding: 8px;">' . lang('Systeminfo_Services_Description') . '</th>
</tr>
</thead>';
$table_color = 'odd';
for ($x = 0; $x < sizeof($running_services); $x++) {
if (stristr($running_services[$x], '.service')) {
$temp_services_arr = array_values(array_filter(explode(' ', trim($running_services[$x]))));
$servives_name = $temp_services_arr[0];
unset($temp_services_arr[0], $temp_services_arr[1], $temp_services_arr[2], $temp_services_arr[3]);
$servives_description = implode(" ", $temp_services_arr);
if ($table_color == 'odd')
{
$table_color = 'even';
} else
{
$table_color = 'odd';
}
echo '<tr class="' . $table_color . '"><td style="padding: 3px; padding-left: 10px;">' . substr($servives_name, 0, -8) . '</td><td style="padding: 3px; padding-left: 10px;">' . $servives_description . '</td></tr>';
}
}
echo '</table>';
echo '</div>';
echo ' </div>
</div>';
?>
<script>
hideSpinner();
</script>

124
front/systeminfoStorage.php Executable file
View File

@@ -0,0 +1,124 @@
<?php
//------------------------------------------------------------------------------
// check if authenticated
require_once $_SERVER['DOCUMENT_ROOT'] . '/php/templates/security.php';
require_once $_SERVER['DOCUMENT_ROOT'] . '/php/server/db.php';
require_once $_SERVER['DOCUMENT_ROOT'] . '/php/templates/language/lang.php';
?>
<?php
// ----------------------------------------------------------
// Storage
// ----------------------------------------------------------
//HDD stats
$hdd_result = shell_exec(" df -P | awk '{print $1}'");
$hdd_devices = explode("\n", trim($hdd_result));
$hdd_result = shell_exec(" df -P | awk '{print $2}'");
$hdd_devices_total = explode("\n", trim($hdd_result));
$hdd_result = shell_exec(" df -P | awk '{print $3}'");
$hdd_devices_used = explode("\n", trim($hdd_result));
$hdd_result = shell_exec(" df -P | awk '{print $4}'");
$hdd_devices_free = explode("\n", trim($hdd_result));
$hdd_result = shell_exec(" df -P | awk '{print $5}'");
$hdd_devices_percent = explode("\n", trim($hdd_result));
$hdd_result = shell_exec(" df -P | awk '{print $6}'");
$hdd_devices_mount = explode("\n", trim($hdd_result));
// Storage ----------------------------------------------------------
echo '<div class="box box-solid">
<div class="box-header">
<h3 class="box-title sysinfo_headline"><i class="fa fa-hdd"></i> ' . lang('Systeminfo_Storage') . '</h3>
</div>
<div class="box-body">';
$storage_lsblk = shell_exec("lsblk -io NAME,SIZE,TYPE,MOUNTPOINT,MODEL --list | tail -n +2 | awk '{print $1\"#\"$2\"#\"$3\"#\"$4\"#\"$5}'");
$storage_lsblk_line = explode("\n", $storage_lsblk);
$storage_lsblk_line = array_filter($storage_lsblk_line);
for ($x = 0; $x < sizeof($storage_lsblk_line); $x++) {
$temp = array();
$temp = explode("#", $storage_lsblk_line[$x]);
$storage_lsblk_line[$x] = $temp;
}
for ($x = 0; $x < sizeof($storage_lsblk_line); $x++) {
echo '<div class="row">';
if (preg_match('~[0-9]+~', $storage_lsblk_line[$x][0])) {
echo '<div class="col-sm-4 sysinfo_storage_a">"' . lang('Systeminfo_Storage_Mount') . ' ' . $storage_lsblk_line[$x][3] . '"</div>';
} else {
echo '<div class="col-sm-4 sysinfo_storage_a">"' . str_replace('_', ' ', $storage_lsblk_line[$x][3]) . '"</div>';
}
echo '<div class="col-sm-3 sysinfo_storage_b">' . lang('Systeminfo_Storage_Device') . ' /dev/' . $storage_lsblk_line[$x][0] . '</div>';
echo '<div class="col-sm-2 sysinfo_storage_b">' . lang('Systeminfo_Storage_Size') . ' ' . $storage_lsblk_line[$x][1] . '</div>';
echo '<div class="col-sm-2 sysinfo_storage_b">' . lang('Systeminfo_Storage_Type') . ' ' . $storage_lsblk_line[$x][2] . '</div>';
echo '</div>';
}
echo ' </div>
</div>';
// Storage usage ----------------------------------------------------------
echo '<div class="box box-solid">
<div class="box-header">
<h3 class="box-title sysinfo_headline"><i class="fa fa-hdd"></i> ' . lang('Systeminfo_Storage_Usage') . '</h3>
</div>
<div class="box-body">';
for ($x = 0; $x < sizeof($hdd_devices); $x++) {
if (stristr($hdd_devices[$x], '/dev/')) {
if (!stristr($hdd_devices[$x], '/loop')) {
if ($hdd_devices_total[$x] == 0 || $hdd_devices_total[$x] == '') {$temp_total = 0;} else { $temp_total = number_format(round(($hdd_devices_total[$x] / 1024 / 1024), 2), 2, ',', '.'); $temp_total = trim($temp_total);}
if ($hdd_devices_used[$x] == 0 || $hdd_devices_used[$x] == '') {$temp_used = 0;} else { $temp_used = number_format(round(($hdd_devices_used[$x] / 1024 / 1024), 2), 2, ',', '.'); $temp_used = trim($temp_total);}
if ($hdd_devices_free[$x] == 0 || $hdd_devices_free[$x] == '') {$temp_free = 0;} else { $temp_free = number_format(round(($hdd_devices_free[$x] / 1024 / 1024), 2), 2, ',', '.'); $temp_free = trim($temp_total);}
echo '<div class="row">';
echo '<div class="col-sm-4 sysinfo_storage_usage_a">"' . lang('Systeminfo_Storage_Usage_Mount') . ' ' . $hdd_devices_mount[$x] . '"</div>';
echo '<div class="col-sm-2 sysinfo_storage_usage_b">' . lang('Systeminfo_Storage_Usage_Total') . ' ' . $temp_total . ' GB</div>';
echo '<div class="col-sm-3 sysinfo_storage_usage_b">' . lang('Systeminfo_Storage_Usage_Used') . ' ' . $temp_used . ' GB (' . $hdd_devices_percent[$x]. ')</div>';
echo '<div class="col-sm-2 sysinfo_storage_usage_b">' . lang('Systeminfo_Storage_Usage_Free') . ' ' . $temp_free . ' GB</div>';
echo '</div>';
}
}
}
#echo '<br>' . $lang['SysInfo_storage_note'];
echo ' </div>
</div>';
// ----------------------------------------------------------
// USB devices
// ----------------------------------------------------------
$usb_result = shell_exec("lsusb");
$usb_devices_mount = explode("\n", trim($usb_result));
echo '<div class="box box-solid">
<div class="box-header">
<h3 class="box-title sysinfo_headline"><i class="fab fa-usb"></i> ' . lang('Systeminfo_USB_Devices') . '</h3>
</div>
<div class="box-body">';
echo ' <table class="table table-bordered table-hover table-striped dataTable no-footer" style="margin-bottom: 10px;">';
$table_color = 'odd';
sort($usb_devices_mount);
for ($x = 0; $x < sizeof($usb_devices_mount); $x++) {
$cut_pos = strpos($usb_devices_mount[$x], ':');
$usb_bus = substr($usb_devices_mount[$x], 0, $cut_pos);
$usb_dev = substr($usb_devices_mount[$x], $cut_pos + 1);
if ($table_color == 'odd') {$table_color = 'even';} else { $table_color = 'odd';}
echo '<tr class="' . $table_color . '"><td style="padding: 3px; padding-left: 10px; width: 150px;"><b>' . str_replace('Device', 'Dev.', $usb_bus) . '</b></td><td style="padding: 3px; padding-left: 10px;">' . $usb_dev . '</td></tr>';
}
echo ' </table>';
echo ' </div>
</div>';
// ----------------------------------------------------------
echo '<br>';
?>
<script>
hideSpinner();
</script>

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

@@ -64,6 +64,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
@@ -75,6 +76,7 @@ nav:
- Database: DATABASE.md
- Settings: SETTINGS_SYSTEM.md
- Versions: VERSIONS.md
- Icon and Type guessing: DEVICE_HEURISTICS.md
- Integrations:
- Webhook Secret: WEBHOOK_SECRET.md
- API: API.md

View File

@@ -13,7 +13,7 @@ from models.user_events_queue_instance import UserEventsQueueInstance
from messaging.in_app import write_notification
# Import the start_server function
from graphql_server.graphql_server_start import start_server
from api_server.api_server_start import start_server
apiEndpoints = []

View File

@@ -0,0 +1,282 @@
import threading
from flask import Flask, request, jsonify, Response
from flask_cors import CORS
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 delete_unknown_devices, delete_all_with_empty_macs, delete_devices, export_devices, import_csv
from .events_endpoint import delete_events, delete_events_30, get_events
from .history_endpoint import delete_online_history
from .prometheus_endpoint import getMetricStats
from .sync_endpoint import handle_sync_post, handle_sync_get
import sys
# Register NetAlertX directories
INSTALL_PATH = "/app"
sys.path.extend([f"{INSTALL_PATH}/server"])
from logger import mylog
from helper import get_setting_value, timeNowTZ
from app_state import updateState
from messaging.in_app import write_notification
# Flask application
app = Flask(__name__)
CORS(
app,
resources={
r"/metrics": {"origins": "*"},
r"/device/*": {"origins": "*"},
r"/devices/*": {"origins": "*"},
r"/history/*": {"origins": "*"},
r"/events/*": {"origins": "*"}
},
supports_credentials=True,
allow_headers=["Authorization", "Content-Type"]
)
# --------------------------
# GraphQL Endpoints
# --------------------------
# Endpoint used when accessed via browser
@app.route("/graphql", methods=["GET"])
def graphql_debug():
# Handles GET requests
return "NetAlertX GraphQL server running."
# Endpoint for GraphQL queries
@app.route("/graphql", methods=["POST"])
def graphql_endpoint():
# Check for API token in headers
if not is_authorized():
msg = '[graphql_server] Unauthorized access attempt - make sure your GRAPHQL_PORT and API_TOKEN settings are correct.'
mylog('verbose', [msg])
return jsonify({"error": msg}), 401
# Retrieve and log request data
data = request.get_json()
mylog('verbose', [f'[graphql_server] data: {data}'])
# Execute the GraphQL query
result = devicesSchema.execute(data.get("query"), variables=data.get("variables"))
# Return the result as JSON
return jsonify(result.data)
# --------------------------
# 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=["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/totals", methods=["GET"])
def api_get_devices_totals():
if not is_authorized():
return jsonify({"error": "Forbidden"}), 403
return get_devices_totals()
@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"))
# --------------------------
# 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/<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.json.get("mac") if request.is_json else None
return get_events(mac)
@app.route("/events/30days", methods=["DELETE"])
def api_delete_old_events():
if not is_authorized():
return jsonify({"error": "Forbidden"}), 403
return delete_events_30()
# --------------------------
# Prometheus metrics endpoint
# --------------------------
@app.route("/metrics")
def metrics():
# Check for API token in headers
if not is_authorized():
return jsonify({"error": "Forbidden"}), 403
# Return Prometheus metrics as plain text
return Response(getMetricStats(), 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")
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):
"""Start the GraphQL server in a background thread."""
if app_state.graphQLServerStarted == 0:
mylog('verbose', [f'[graphql endpoint] Starting on port: {graphql_port}'])
# Start Flask app in a separate thread
thread = threading.Thread(
target=lambda: app.run(
host="0.0.0.0",
port=graphql_port,
debug=True,
use_reloader=False
)
)
thread.start()
# Update the state to indicate the server has started
app_state = updateState("Process: Idle", None, None, None, 1)

View File

@@ -0,0 +1,334 @@
#!/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("name", ""),
data.get("owner", ""),
data.get("type", ""),
data.get("vendor", ""),
data.get("icon", ""),
data.get("favorite", 0),
data.get("group", ""),
data.get("location", ""),
data.get("comments", ""),
data.get("networknode", ""),
data.get("networknodeport", ""),
data.get("ssid", ""),
data.get("networksite", ""),
data.get("staticIP", 0),
data.get("scancycle", 0),
data.get("alertevents", 0),
data.get("alertdown", 0),
data.get("relType", "default"),
data.get("reqNics", 0),
data.get("skiprepeated", 0),
data.get("newdevice", 0),
data.get("archived", 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("ip", ""),
data.get("devGUID", ""),
data.get("devCustomProps", ""),
"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("name", ""),
data.get("owner", ""),
data.get("type", ""),
data.get("vendor", ""),
data.get("icon", ""),
data.get("favorite", 0),
data.get("group", ""),
data.get("location", ""),
data.get("comments", ""),
data.get("networknode", ""),
data.get("networknodeport", ""),
data.get("ssid", ""),
data.get("networksite", ""),
data.get("staticIP", 0),
data.get("scancycle", 0),
data.get("alertevents", 0),
data.get("alertdown", 0),
data.get("relType", "default"),
data.get("reqNics", 0),
data.get("skiprepeated", 0),
data.get("newdevice", 0),
data.get("archived", 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,195 @@
#!/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
# --------------------------
# Device Endpoints Functions
# --------------------------
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
})

View File

@@ -0,0 +1,72 @@
#!/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
# --------------------------
# Events Endpoints Functions
# --------------------------
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_30():
"""Delete all events older than 30 days"""
conn = get_temp_db_connection()
cur = conn.cursor()
sql = "DELETE FROM Events WHERE eve_DateTime <= date('now', '-30 day')"
cur.execute(sql)
conn.commit()
conn.close()
return jsonify({"success": True, "message": "Deleted events older than 30 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"})

View File

@@ -197,7 +197,7 @@ class Query(ObjectType):
searchable_fields = [
"devName", "devMac", "devOwner", "devType", "devVendor", "devLastIP",
"devGroup", "devComments", "devLocation", "devStatus", "devSSID",
"devSite", "devSourcePlugin", "devSyncHubNode", "devFQDN", "devParentRelType"
"devSite", "devSourcePlugin", "devSyncHubNode", "devFQDN", "devParentRelType", "devParentMAC"
]
search_term = options.search.lower()

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,76 @@
import json
import sys
# Register NetAlertX directories
INSTALL_PATH = "/app"
sys.path.extend([f"{INSTALL_PATH}/server"])
from logger import mylog
from const import apiPath
from helper import is_random_mac, get_number_of_children, format_ip_long, get_setting_value
def escape_label_value(val):
"""
Escape special characters for Prometheus labels.
"""
return str(val).replace('\\', '\\\\').replace('\n', '\\n').replace('"', '\\"')
# Define a base URL with the user's home directory
folder = apiPath
def getMetricStats():
output = []
# 1. Dashboard totals
try:
with open(folder + 'table_devices_tiles.json', 'r') as f:
tiles_data = json.load(f)["data"]
if isinstance(tiles_data, list) and tiles_data:
totals = tiles_data[0]
output.append(f'netalertx_connected_devices {totals.get("connected", 0)}')
output.append(f'netalertx_offline_devices {totals.get("offline", 0)}')
output.append(f'netalertx_down_devices {totals.get("down", 0)}')
output.append(f'netalertx_new_devices {totals.get("new", 0)}')
output.append(f'netalertx_archived_devices {totals.get("archived", 0)}')
output.append(f'netalertx_favorite_devices {totals.get("favorites", 0)}')
output.append(f'netalertx_my_devices {totals.get("my_devices", 0)}')
else:
output.append("# Unexpected format in table_devices_tiles.json")
except (FileNotFoundError, json.JSONDecodeError) as e:
mylog('none', f'[metrics] Error loading tiles data: {e}')
output.append(f"# Error loading tiles data: {e}")
except Exception as e:
output.append(f"# General error loading dashboard totals: {e}")
# 2. Device-level metrics
try:
with open(folder + 'table_devices.json', 'r') as f:
data = json.load(f)
devices = data.get("data", [])
for row in devices:
name = escape_label_value(row.get("devName", "unknown"))
mac = escape_label_value(row.get("devMac", "unknown"))
ip = escape_label_value(row.get("devLastIP", "unknown"))
vendor = escape_label_value(row.get("devVendor", "unknown"))
first_conn = escape_label_value(row.get("devFirstConnection", "unknown"))
last_conn = escape_label_value(row.get("devLastConnection", "unknown"))
dev_type = escape_label_value(row.get("devType", "unknown"))
raw_status = row.get("devStatus", "Unknown")
dev_status = raw_status.replace("-", "").capitalize()
output.append(
f'netalertx_device_status{{device="{name}", mac="{mac}", ip="{ip}", vendor="{vendor}", '
f'first_connection="{first_conn}", last_connection="{last_conn}", dev_type="{dev_type}", '
f'device_status="{dev_status}"}} 1'
)
except (FileNotFoundError, json.JSONDecodeError) as e:
mylog('none', f'[metrics] Error loading devices data: {e}')
output.append(f"# Error loading devices data: {e}")
except Exception as e:
output.append(f"# General error processing device metrics: {e}")
return "\n".join(output) + "\n"

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

@@ -1,69 +0,0 @@
import threading
from flask import Flask, request, jsonify
from .graphql_schema import devicesSchema
from graphene import Schema
import sys
# Register NetAlertX directories
INSTALL_PATH = "/app"
sys.path.extend([f"{INSTALL_PATH}/server"])
from logger import mylog
from helper import get_setting_value, timeNowTZ
from app_state import updateState
from messaging.in_app import write_notification
# Flask application
app = Flask(__name__)
# Retrieve API token and port
graphql_port_value = get_setting_value("GRAPHQL_PORT")
# Endpoint used when accessed via browser
@app.route("/graphql", methods=["GET"])
def graphql_debug():
# Handles GET requests
return "NetAlertX GraphQL server running."
# Endpoint for GraphQL queries
@app.route("/graphql", methods=["POST"])
def graphql_endpoint():
# Check for API token in headers
incoming_header_token = request.headers.get("Authorization")
api_token_value = get_setting_value("API_TOKEN")
if incoming_header_token != f"Bearer {api_token_value}":
msg = '[graphql_server] Unauthorized access attempt - make sure your GRAPHQL_PORT and API_TOKEN settings are correct.'
mylog('verbose', [msg])
return jsonify({"error": msg}), 401
# Retrieve and log request data
data = request.get_json()
mylog('verbose', [f'[graphql_server] data: {data}'])
# Execute the GraphQL query
result = devicesSchema.execute(data.get("query"), variables=data.get("variables"))
# Return the result as JSON
return jsonify(result.data)
def start_server(graphql_port, app_state):
"""Start the GraphQL server in a background thread."""
if app_state.graphQLServerStarted == 0:
mylog('verbose', [f'[graphql_server] Starting on port: {graphql_port}'])
# Start Flask app in a separate thread
thread = threading.Thread(
target=lambda: app.run(
host="0.0.0.0",
port=graphql_port,
debug=True,
use_reloader=False
)
)
thread.start()
# Update the state to indicate the server has started
app_state = updateState("Process: Idle", None, None, None, 1)

View File

@@ -18,7 +18,6 @@ import hashlib
import random
import string
import ipaddress
import dns.resolver
import conf
from const import *
@@ -53,22 +52,6 @@ def get_timezone_offset():
return offset_formatted
#-------------------------------------------------------------------------------
def updateSubnets(scan_subnets):
subnets = []
# multiple interfaces
if type(scan_subnets) is list:
for interface in scan_subnets :
subnets.append(interface)
# one interface only
else:
subnets.append(scan_subnets)
return subnets
#-------------------------------------------------------------------------------
# File system permission handling
#-------------------------------------------------------------------------------
@@ -217,12 +200,6 @@ def get_setting(key):
return None
#-------------------------------------------------------------------------------
# Settings
#-------------------------------------------------------------------------------
#-------------------------------------------------------------------------------
# Return setting value
def get_setting_value(key):
@@ -248,8 +225,6 @@ def get_setting_value(key):
#-------------------------------------------------------------------------------
# Convert the setting value to the corresponding python type
def setting_value_to_python_type(set_type, set_value):
value = '----not processed----'
@@ -341,6 +316,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 +359,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 +396,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 +451,6 @@ def removeDuplicateNewLines(text):
return text
#-------------------------------------------------------------------------------
def sanitize_string(input):
if isinstance(input, bytes):
input = input.decode('utf-8')
@@ -482,15 +458,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 +468,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 +502,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 +515,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 +529,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 +571,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 +580,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):
@@ -633,29 +593,33 @@ def collect_lang_strings(json, pref, stringSqlParams):
return stringSqlParams
#-------------------------------------------------------------------------------
# Misc
# Date and time methods
#-------------------------------------------------------------------------------
#-------------------------------------------------------------------------------
def print_table_schema(db, table):
sql = db.sql
sql.execute(f"PRAGMA table_info({table})")
result = sql.fetchall()
# -------------------------------------------------------------------------------------------
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')
if not result:
mylog('none', f'[Schema] Table "{table}" not found or has no columns.')
return
# -------------------------------------------------------------------------------------------
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
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))
days = delta.days
hours, remainder = divmod(delta.seconds, 3600)
minutes = remainder // 60
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)
return f"{days}d {hours:02}:{minutes:02}"
# -------------------------------------------------------------------------------------------
def format_date_iso(date1: str) -> str:
"""Return ISO 8601 string for a date"""
dt = datetime.datetime.fromisoformat(date1) if isinstance(date1, str) else date1
return dt.isoformat()
#-------------------------------------------------------------------------------
def checkNewVersion():
@@ -667,7 +631,10 @@ def checkNewVersion():
buildTimestamp = int(f.read().strip())
try:
response = requests.get("https://api.github.com/repos/jokob-sk/NetAlertX/releases")
response = requests.get(
"https://api.github.com/repos/jokob-sk/NetAlertX/releases",
timeout=5
)
response.raise_for_status() # Raise an exception for HTTP errors
text = response.text
except requests.exceptions.RequestException as e:
@@ -695,22 +662,6 @@ 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):

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
@@ -157,13 +157,13 @@ def importConfigs (db, all_plugins):
# ----------------------------------------
# ccd(key, default, config_dir, name, inputtype, options, group, events=[], desc = "", regex = "", setJsonMetadata = {}, overrideTemplate = {})
conf.LOADED_PLUGINS = ccd('LOADED_PLUGINS', [] , c_d, 'Loaded plugins', '{"dataType":"array", "elements": [{"elementType" : "select", "elementOptions" : [{"multiple":"true", "ordeable": "true"}] ,"transformers": []}]}', '[]', 'General')
conf.LOADED_PLUGINS = ccd('LOADED_PLUGINS', [] , c_d, 'Loaded plugins', '{"dataType":"array","elements":[{"elementType":"select","elementOptions":[{"multiple":"true","ordeable":"true"}],"transformers":[]},{"elementType":"button","elementOptions":[{"sourceSuffixes":[]},{"separator":""},{"cssClasses":"col-xs-12"},{"onClick":"selectChange(this)"},{"getStringKey":"Gen_Change"}],"transformers":[]}]}', '[]', 'General')
conf.DISCOVER_PLUGINS = ccd('DISCOVER_PLUGINS', True , c_d, 'Discover plugins', """{"dataType": "boolean","elements": [{"elementType": "input","elementOptions": [{ "type": "checkbox" }],"transformers": []}]}""", '[]', 'General')
conf.SCAN_SUBNETS = ccd('SCAN_SUBNETS', ['192.168.1.0/24 --interface=eth1', '192.168.1.0/24 --interface=eth0'] , c_d, 'Subnets to scan', '''{"dataType": "array","elements": [{"elementType": "input","elementOptions": [{"placeholder": "192.168.1.0/24 --interface=eth1"},{"suffix": "_in"},{"cssClasses": "col-sm-10"},{"prefillValue": "null"}],"transformers": []},{"elementType": "button","elementOptions": [{"sourceSuffixes": ["_in"]},{"separator": ""},{"cssClasses": "col-xs-12"},{"onClick": "addList(this, false)"},{"getStringKey": "Gen_Add"}],"transformers": []},{"elementType": "select","elementHasInputValue": 1,"elementOptions": [{"multiple": "true"},{"readonly": "true"},{"editable": "true"}],"transformers": []},{"elementType": "button","elementOptions": [{"sourceSuffixes": []},{"separator": ""},{"cssClasses": "col-xs-6"},{"onClick": "removeAllOptions(this)"},{"getStringKey": "Gen_Remove_All"}],"transformers": []},{"elementType": "button","elementOptions": [{"sourceSuffixes": []},{"separator": ""},{"cssClasses": "col-xs-6"},{"onClick": "removeFromList(this)"},{"getStringKey": "Gen_Remove_Last"}],"transformers": []}]}''', '[]', 'General')
conf.LOG_LEVEL = ccd('LOG_LEVEL', 'verbose' , c_d, 'Log verboseness', '{"dataType":"string", "elements": [{"elementType" : "select", "elementOptions" : [] ,"transformers": []}]}', "['none', 'minimal', 'verbose', 'debug', 'trace']", 'General')
conf.TIMEZONE = ccd('TIMEZONE', default_tz , c_d, 'Time zone', '{"dataType":"string", "elements": [{"elementType" : "input", "elementOptions" : [] ,"transformers": []}]}', '[]', 'General')
conf.PLUGINS_KEEP_HIST = ccd('PLUGINS_KEEP_HIST', 250 , c_d, 'Keep history entries', '{"dataType":"integer", "elements": [{"elementType" : "input", "elementOptions" : [{"type": "number"}] ,"transformers": []}]}', '[]', 'General')
conf.REPORT_DASHBOARD_URL = ccd('REPORT_DASHBOARD_URL', 'http://127.0.0.1/' , c_d, 'NetAlertX URL', '{"dataType":"string", "elements": [{"elementType" : "input", "elementOptions" : [] ,"transformers": []}]}', '[]', 'General')
conf.REPORT_DASHBOARD_URL = ccd('REPORT_DASHBOARD_URL', 'update_REPORT_DASHBOARD_URL_setting' , c_d, 'NetAlertX URL', '{"dataType":"string", "elements": [{"elementType" : "input", "elementOptions" : [] ,"transformers": []}]}', '[]', 'General')
conf.DAYS_TO_KEEP_EVENTS = ccd('DAYS_TO_KEEP_EVENTS', 90 , c_d, 'Delete events days', '{"dataType":"integer", "elements": [{"elementType" : "input", "elementOptions" : [{"type": "number"}] ,"transformers": []}]}', '[]', 'General')
conf.HRS_TO_KEEP_NEWDEV = ccd('HRS_TO_KEEP_NEWDEV', 0 , c_d, 'Keep new devices for', '{"dataType":"integer", "elements": [{"elementType" : "input", "elementOptions" : [{"type": "number"}] ,"transformers": []}]}', "[]", 'General')
conf.HRS_TO_KEEP_OFFDEV = ccd('HRS_TO_KEEP_OFFDEV', 0 , c_d, 'Keep offline devices for', '{"dataType":"integer", "elements": [{"elementType" : "input", "elementOptions" : [{"type": "number"}] ,"transformers": []}]}', "[]", 'General')
@@ -171,12 +171,12 @@ def importConfigs (db, all_plugins):
conf.REFRESH_FQDN = ccd('REFRESH_FQDN', False , c_d, 'Refresh FQDN', """{"dataType": "boolean","elements": [{"elementType": "input","elementOptions": [{ "type": "checkbox" }],"transformers": []}]}""", '[]', 'General')
conf.API_CUSTOM_SQL = ccd('API_CUSTOM_SQL', 'SELECT * FROM Devices WHERE devPresentLastScan = 0' , c_d, 'Custom endpoint', '{"dataType":"string", "elements": [{"elementType" : "input", "elementOptions" : [] ,"transformers": []}]}', '[]', 'General')
conf.VERSION = ccd('VERSION', '' , c_d, 'Version', '{"dataType":"string", "elements": [{"elementType" : "input", "elementOptions" : [{ "readonly": "true" }] ,"transformers": []}]}', '', 'General')
conf.NETWORK_DEVICE_TYPES = ccd('NETWORK_DEVICE_TYPES', ['AP', 'Gateway', 'Firewall', 'Hypervisor', 'Powerline', 'Switch', 'WLAN', 'PLC', 'Router','USB LAN Adapter', 'USB WIFI Adapter', 'Internet'] , c_d, 'Network device types', '{"dataType":"array","elements":[{"elementType":"input","elementOptions":[{"placeholder":"Enter value"},{"suffix":"_in"},{"cssClasses":"col-sm-10"},{"prefillValue":"null"}],"transformers":[]},{"elementType":"button","elementOptions":[{"sourceSuffixes":["_in"]},{"separator":""},{"cssClasses":"col-xs-12"},{"onClick":"addList(this,false)"},{"getStringKey":"Gen_Add"}],"transformers":[]},{"elementType":"select", "elementHasInputValue":1,"elementOptions":[{"multiple":"true"},{"readonly":"true"},{"editable":"true"}],"transformers":[]},{"elementType":"button","elementOptions":[{"sourceSuffixes":[]},{"separator":""},{"cssClasses":"col-xs-6"},{"onClick":"removeAllOptions(this)"},{"getStringKey":"Gen_Remove_All"}],"transformers":[]},{"elementType":"button","elementOptions":[{"sourceSuffixes":[]},{"separator":""},{"cssClasses":"col-xs-6"},{"onClick":"removeFromList(this)"},{"getStringKey":"Gen_Remove_Last"}],"transformers":[]}]}', '[]', 'General')
conf.NETWORK_DEVICE_TYPES = ccd('NETWORK_DEVICE_TYPES', ['AP', 'Access Point', 'Gateway', 'Firewall', 'Hypervisor', 'Powerline', 'Switch', 'WLAN', 'PLC', 'Router','USB LAN Adapter', 'USB WIFI Adapter', 'Internet'] , c_d, 'Network device types', '{"dataType":"array","elements":[{"elementType":"input","elementOptions":[{"placeholder":"Enter value"},{"suffix":"_in"},{"cssClasses":"col-sm-10"},{"prefillValue":"null"}],"transformers":[]},{"elementType":"button","elementOptions":[{"sourceSuffixes":["_in"]},{"separator":""},{"cssClasses":"col-xs-12"},{"onClick":"addList(this,false)"},{"getStringKey":"Gen_Add"}],"transformers":[]},{"elementType":"select", "elementHasInputValue":1,"elementOptions":[{"multiple":"true"},{"readonly":"true"},{"editable":"true"}],"transformers":[]},{"elementType":"button","elementOptions":[{"sourceSuffixes":[]},{"separator":""},{"cssClasses":"col-xs-6"},{"onClick":"removeAllOptions(this)"},{"getStringKey":"Gen_Remove_All"}],"transformers":[]},{"elementType":"button","elementOptions":[{"sourceSuffixes":[]},{"separator":""},{"cssClasses":"col-xs-6"},{"onClick":"removeFromList(this)"},{"getStringKey":"Gen_Remove_Last"}],"transformers":[]}]}', '[]', 'General')
conf.GRAPHQL_PORT = ccd('GRAPHQL_PORT', 20212 , c_d, 'GraphQL port', '{"dataType":"integer", "elements": [{"elementType" : "input", "elementOptions" : [{"type": "number"}] ,"transformers": []}]}', '[]', 'General')
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)

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

@@ -8,12 +8,13 @@ import re
INSTALL_PATH="/app"
sys.path.extend([f"{INSTALL_PATH}/server"])
from helper import timeNowTZ, get_setting_value, list_to_where, check_IP_format, sanitize_SQL_input
from helper import timeNowTZ, get_setting_value, check_IP_format
from logger import mylog
from const import vendorsPath, vendorsPathNewest, sql_generateGuid
from models.device_instance import DeviceInstance
from scan.name_resolution import NameResolver
from scan.device_heuristics import guess_icon, guess_type
from db.db_helper import sanitize_SQL_input, list_to_where
#-------------------------------------------------------------------------------
# Removing devices from the CurrentScan DB table which the user chose to ignore by MAC or IP
@@ -68,10 +69,23 @@ def update_devices_data_from_scan (db):
# Update IP
mylog('debug', '[Update Devices] - cur_IP -> devLastIP (always updated)')
sql.execute("""UPDATE Devices
SET devLastIP = (SELECT cur_IP FROM CurrentScan
WHERE devMac = cur_MAC)
WHERE EXISTS (SELECT 1 FROM CurrentScan
WHERE devMac = cur_MAC) """)
SET devLastIP = (
SELECT cur_IP
FROM CurrentScan
WHERE devMac = cur_MAC
AND cur_IP IS NOT NULL
AND cur_IP NOT IN ('', 'null', '(unknown)', '(Unknown)')
ORDER BY cur_DateTime DESC
LIMIT 1
)
WHERE EXISTS (
SELECT 1
FROM CurrentScan
WHERE devMac = cur_MAC
AND cur_IP IS NOT NULL
AND cur_IP NOT IN ('', 'null', '(unknown)', '(Unknown)')
)""")
# Update only devices with empty, NULL or (u(U)nknown) vendors
mylog('debug', '[Update Devices] - cur_Vendor -> (if empty) devVendor')
@@ -447,8 +461,8 @@ def create_new_devices (db):
cur_MAC, cur_Name, cur_Vendor, cur_ScanMethod, cur_IP, cur_SyncHubNodeName, cur_NetworkNodeMAC, cur_PORT, cur_NetworkSite, cur_SSID, cur_Type = row
# Handle NoneType
cur_Name = cur_Name.strip() if cur_Name else '(unknown)'
cur_Type = cur_Type.strip() if cur_Type else get_setting_value("NEWDEV_devType")
cur_Name = str(cur_Name).strip() if cur_Name else '(unknown)'
cur_Type = str(cur_Type).strip() if cur_Type else get_setting_value("NEWDEV_devType")
cur_NetworkNodeMAC = cur_NetworkNodeMAC.strip() if cur_NetworkNodeMAC else ''
cur_NetworkNodeMAC = cur_NetworkNodeMAC if cur_NetworkNodeMAC and cur_MAC != "Internet" else (get_setting_value("NEWDEV_devParentMAC") if cur_MAC != "Internet" else "null")
cur_SyncHubNodeName = cur_SyncHubNodeName if cur_SyncHubNodeName and cur_SyncHubNodeName != "null" else (get_setting_value("SYNC_node_name"))

View File

@@ -1,5 +1,8 @@
import sys
import re
import json
import base64
from pathlib import Path
from typing import Optional, List, Tuple, Dict
# Register NetAlertX directories
@@ -11,57 +14,167 @@ from const import *
from logger import mylog
from helper import timeNowTZ, get_setting_value
# Load MAC/device-type/icon rules from external file
MAC_TYPE_ICON_PATH = Path(f"{INSTALL_PATH}/back/device_heuristics_rules.json")
try:
with open(MAC_TYPE_ICON_PATH, "r", encoding="utf-8") as f:
MAC_TYPE_ICON_RULES = json.load(f)
# Precompute base64-encoded icon_html once for each rule
for rule in MAC_TYPE_ICON_RULES:
icon_html = rule.get("icon_html", "")
if icon_html:
# encode icon_html to base64 string
b64_bytes = base64.b64encode(icon_html.encode("utf-8"))
rule["icon_base64"] = b64_bytes.decode("utf-8")
else:
rule["icon_base64"] = ""
except Exception as e:
MAC_TYPE_ICON_RULES = []
mylog('none', f"[guess_device_attributes] Failed to load device_heuristics_rules.json: {e}")
# -----------------------------------------
# Match device type and base64-encoded icon using MAC prefix and vendor patterns.
def match_mac_and_vendor(
mac_clean: str,
vendor: str,
default_type: str,
default_icon: str
) -> Tuple[str, str]:
"""
Match device type and base64-encoded icon using MAC prefix and vendor patterns.
Args:
mac_clean: Cleaned MAC address (uppercase, no colons).
vendor: Normalized vendor name (lowercase).
default_type: Fallback device type.
default_icon: Fallback base64 icon.
Returns:
Tuple containing (device_type, base64_icon)
"""
for rule in MAC_TYPE_ICON_RULES:
dev_type = rule.get("dev_type")
base64_icon = rule.get("icon_base64", "")
patterns = rule.get("matching_pattern", [])
for pattern in patterns:
mac_prefix = pattern.get("mac_prefix", "").upper()
vendor_pattern = pattern.get("vendor", "").lower()
if mac_clean.startswith(mac_prefix):
if not vendor_pattern or vendor_pattern in vendor:
mylog('debug', f"[guess_device_attributes] Matched via MAC+Vendor")
type_ = dev_type
icon = base64_icon or default_icon
return type_, icon
return default_type, default_icon
# ---------------------------------------------------
# Match device type and base64-encoded icon using vendor patterns.
def match_vendor(
vendor: str,
default_type: str,
default_icon: str
) -> Tuple[str, str]:
vendor_lc = vendor.lower()
for rule in MAC_TYPE_ICON_RULES:
dev_type = rule.get("dev_type")
base64_icon = rule.get("icon_base64", "")
patterns = rule.get("matching_pattern", [])
for pattern in patterns:
# Only apply fallback when no MAC prefix is specified
mac_prefix = pattern.get("mac_prefix", "")
vendor_pattern = pattern.get("vendor", "").lower()
if vendor_pattern and vendor_pattern in vendor_lc:
mylog('debug', f"[guess_device_attributes] Matched via Vendor")
icon = base64_icon or default_icon
return dev_type, icon
return default_type, default_icon
# ---------------------------------------------------
# Match device type and base64-encoded icon using name patterns.
def match_name(
name: str,
default_type: str,
default_icon: str
) -> Tuple[str, str]:
"""
Match device type and base64-encoded icon using name patterns from global MAC_TYPE_ICON_RULES.
Args:
name: Normalized device name (lowercase).
default_type: Fallback device type.
default_icon: Fallback base64 icon.
Returns:
Tuple containing (device_type, base64_icon)
"""
name_lower = name.lower() if name else ""
for rule in MAC_TYPE_ICON_RULES:
dev_type = rule.get("dev_type")
base64_icon = rule.get("icon_base64", "")
name_patterns = rule.get("name_pattern", [])
for pattern in name_patterns:
# Use regex search to allow pattern substrings
if re.search(pattern, name_lower, re.IGNORECASE):
mylog('debug', f"[guess_device_attributes] Matched via Name")
type_ = dev_type
icon = base64_icon or default_icon
return type_, icon
return default_type, default_icon
#-------------------------------------------------------------------------------
# Base64 encoded HTML strings for FontAwesome icons, now with an extended icons dictionary for broader device coverage
ICONS = {
"globe": "PGkgY2xhc3M9ImZhcyBmYS1nbG9iZSI+PC9pPg==", # Internet or global network
"phone": "PGkgY2xhc3M9ImZhcyBmYS1tb2JpbGUtYWx0Ij48L2k+", # Smartphone
"laptop": "PGkgY2xhc3M9ImZhIGZhLWxhcHRvcCI+PC9pPg==", # Laptop
"printer": "PGkgY2xhc3M9ImZhIGZhLXByaW50ZXIiPjwvaT4=", # Printer
"router": "PGkgY2xhc3M9ImZhcyBmYS1yYW5kb20iPjwvaT4=", # Router or network switch
"tv": "PGkgY2xhc3M9ImZhIGZhLXR2Ij48L2k+", # Television
"desktop": "PGkgY2xhc3M9ImZhIGZhLWRlc2t0b3AiPjwvaT4=", # Desktop PC
"tablet": "PGkgY2xhc3M9ImZhIGZhLXRhYmxldCI+PC9pPg==", # Tablet
"watch": "PGkgY2xhc3M9ImZhcyBmYS1jbG9jayI+PC9pPg==", # Fallback to clock since smartwatch is nonfree in FontAwesome
"camera": "PGkgY2xhc3M9ImZhIGZhLWNhbWVyYSI+PC9pPg==", # Camera or webcam
"home": "PGkgY2xhc3M9ImZhIGZhLWhvbWUiPjwvaT4=", # Smart home device
"apple": "PGkgY2xhc3M9ImZhYiBmYS1hcHBsZSI+PC9pPg==", # Apple device
"ethernet": "PGkgY2xhc3M9ImZhcyBmYS1uZXR3b3JrLXdpcmVkIj48L2k+", # Free alternative for ethernet icon in FontAwesome
"google": "PGkgY2xhc3M9ImZhYiBmYS1nb29nbGUiPjwvaT4=", # Google device
"raspberry": "PGkgY2xhc3M9ImZhYiBmYS1yYXNwYmVycnktcGkiPjwvaT4=", # Raspberry Pi
"microchip": "PGkgY2xhc3M9ImZhcyBmYS1taWNyb2NoaXAiPjwvaT4=", # IoT or embedded device
"server": "PGkgY2xhc3M9ImZhcyBmYS1zZXJ2ZXIiPjwvaT4=", # Server
"gamepad": "PGkgY2xhc3M9ImZhcyBmYS1nYW1lcGFkIj48L2k+", # Gaming console
"lightbulb": "PGkgY2xhc3M9ImZhcyBmYS1saWdodGJ1bGIiPjwvaT4=", # Smart light
"speaker": "PGkgY2xhc3M9ImZhcyBmYS12b2x1bWUtdXAiPjwvaT4=", # Free speaker alt icon for smart speakers in FontAwesome
"lock": "PGkgY2xhc3M9ImZhcyBmYS1sb2NrIj48L2k+", # Security device
}
#
def match_ip(
ip: str,
default_type: str,
default_icon: str
) -> Tuple[str, str]:
"""
Match device type and base64-encoded icon using IP regex patterns from global JSON.
# Extended device types for comprehensive classification
DEVICE_TYPES = {
"Internet": "Internet Gateway",
"Phone": "Smartphone",
"Laptop": "Laptop",
"Printer": "Printer",
"Router": "Router",
"TV": "Television",
"Desktop": "Desktop PC",
"Tablet": "Tablet",
"Smartwatch": "Smartwatch",
"Camera": "Camera",
"SmartHome": "Smart Home Device",
"Server": "Server",
"GamingConsole": "Gaming Console",
"IoT": "IoT Device",
"NetworkSwitch": "Network Switch",
"AccessPoint": "Access Point",
"SmartLight": "Smart Light",
"SmartSpeaker": "Smart Speaker",
"SecurityDevice": "Security Device",
"Unknown": "Unknown Device",
}
Args:
ip: Device IP address as string.
default_type: Fallback device type.
default_icon: Fallback base64 icon.
Returns:
Tuple containing (device_type, base64_icon)
"""
if not ip:
return default_type, default_icon
for rule in MAC_TYPE_ICON_RULES:
ip_patterns = rule.get("ip_pattern", [])
dev_type = rule.get("dev_type")
base64_icon = rule.get("icon_base64", "")
for pattern in ip_patterns:
if re.match(pattern, ip):
mylog('debug', f"[guess_device_attributes] Matched via IP")
type_ = dev_type
icon = base64_icon or default_icon
return type_, icon
return default_type, default_icon
#-------------------------------------------------------------------------------
# Guess device attributes such as type of device and associated device icon
@@ -72,197 +185,46 @@ def guess_device_attributes(
name: Optional[str],
default_icon: str,
default_type: str
) -> Tuple[str, str]:
"""
Guess the appropriate FontAwesome icon and device type based on device attributes.
Args:
vendor: Device vendor name.
mac: Device MAC address.
ip: Device IP address.
name: Device name.
default_icon: Default icon to return if no match is found.
default_type: Default type to return if no match is found.
Returns:
Tuple[str, str]: A tuple containing the guessed icon (Base64-encoded HTML string)
and the guessed device type (string).
"""
) -> Tuple[str, str]:
mylog('debug', f"[guess_device_attributes] Guessing attributes for (vendor|mac|ip|name): ('{vendor}'|'{mac}'|'{ip}'|'{name}')")
# Normalize inputs
# --- Normalize inputs ---
vendor = str(vendor).lower().strip() if vendor else "unknown"
mac = str(mac).upper().strip() if mac else "00:00:00:00:00:00"
ip = str(ip).strip() if ip else "169.254.0.0" # APIPA address for unknown IPs per RFC 3927
ip = str(ip).strip() if ip else "169.254.0.0"
name = str(name).lower().strip() if name else "(unknown)"
mac_clean = mac.replace(':', '').replace('-', '').upper()
# --- Icon Guessing Logic ---
if mac == "INTERNET":
icon = ICONS.get("globe", default_icon)
else:
# Vendor-based icon guessing
icon_vendor_patterns = {
"apple": "apple",
"samsung|motorola|xiaomi|huawei": "phone",
"dell|lenovo|asus|acer": "laptop",
"hp|epson|canon|brother": "printer",
"cisco|ubiquiti|netgear|tp-link|d-link|mikrotik": "router",
"lg|samsung electronics|sony|vizio": "tv",
"raspberry pi": "raspberry",
"google": "google",
"espressif|particle": "microchip",
"intel|amd": "desktop",
"amazon": "speaker",
"philips hue|lifx": "lightbulb",
"aruba|meraki": "ethernet",
"qnap|synology": "server",
"nintendo|sony interactive|microsoft": "gamepad",
"ring|blink|arlo": "camera",
"nest": "home",
}
for pattern, icon_key in icon_vendor_patterns.items():
if re.search(pattern, vendor, re.IGNORECASE):
icon = ICONS.get(icon_key, default_icon)
break
else:
# MAC-based icon guessing
mac_clean = mac.replace(':', '').replace('-', '').upper()
icon_mac_patterns = {
"001A79|B0BE83|BC926B": "apple",
"001B63|BC4C4C": "tablet",
"74ACB9|002468": "ethernet",
"B827EB": "raspberry",
"001422|001874": "desktop",
"001CBF|002186": "server",
}
for pattern_str, icon_key in icon_mac_patterns.items():
patterns = [p.replace(':', '').replace('-', '').upper() for p in pattern_str.split('|')]
if any(mac_clean.startswith(p) for p in patterns):
icon = ICONS.get(icon_key, default_icon)
break
else:
# Name-based icon guessing
icon_name_patterns = {
"iphone|ipad|macbook|imac": "apple",
"pixel|galaxy|redmi": "phone",
"laptop|notebook": "laptop",
"printer|print": "printer",
"router|gateway|ap|access[ -]?point": "router",
"tv|television|smarttv": "tv",
"desktop|pc|computer": "desktop",
"tablet|pad": "tablet",
"watch|wear": "watch",
"camera|cam|webcam": "camera",
"echo|alexa|dot": "speaker",
"hue|lifx|bulb": "lightbulb",
"server|nas": "server",
"playstation|xbox|switch": "gamepad",
"raspberry|pi": "raspberry",
"google|chromecast|nest": "google",
"doorbell|lock|security": "lock",
}
for pattern, icon_key in icon_name_patterns.items():
if re.search(pattern, name, re.IGNORECASE):
icon = ICONS.get(icon_key, default_icon)
break
else:
# IP-based icon guessing
icon_ip_patterns = {
r"^192\.168\.[0-1]\.1$": "router",
r"^10\.0\.0\.1$": "router",
r"^192\.168\.[0-1]\.[2-9]$": "desktop",
r"^192\.168\.[0-1]\.1\d{2}$": "phone",
}
for pattern, icon_key in icon_ip_patterns.items():
if re.match(pattern, ip):
icon = ICONS.get(icon_key, default_icon)
break
else:
icon = default_icon
# # Internet shortcut
# if mac == "INTERNET":
# return ICONS.get("globe", default_icon), DEVICE_TYPES.get("Internet", default_type)
# --- Type Guessing Logic ---
if mac == "INTERNET":
type_ = DEVICE_TYPES.get("Internet", default_type)
else:
# Vendor-based type guessing
type_vendor_patterns = {
"apple|samsung|motorola|xiaomi|huawei": "Phone",
"dell|lenovo|asus|acer|hp": "Laptop",
"epson|canon|brother": "Printer",
"cisco|ubiquiti|netgear|tp-link|d-link|mikrotik|aruba|meraki": "Router",
"lg|samsung electronics|sony|vizio": "TV",
"raspberry pi": "IoT",
"google|nest": "SmartHome",
"espressif|particle": "IoT",
"intel|amd": "Desktop",
"amazon": "SmartSpeaker",
"philips hue|lifx": "SmartLight",
"qnap|synology": "Server",
"nintendo|sony interactive|microsoft": "GamingConsole",
"ring|blink|arlo": "Camera",
}
for pattern, type_key in type_vendor_patterns.items():
if re.search(pattern, vendor, re.IGNORECASE):
type_ = DEVICE_TYPES.get(type_key, default_type)
break
else:
# MAC-based type guessing
mac_clean = mac.replace(':', '').replace('-', '').upper()
type_mac_patterns = {
"00:1A:79|B0:BE:83|BC:92:6B": "Phone",
"00:1B:63|BC:4C:4C": "Tablet",
"74:AC:B9|00:24:68": "AccessPoint",
"B8:27:EB": "IoT",
"00:14:22|00:18:74": "Desktop",
"00:1C:BF|00:21:86": "Server",
}
for pattern_str, type_key in type_mac_patterns.items():
patterns = [p.replace(':', '').replace('-', '').upper() for p in pattern_str.split('|')]
if any(mac_clean.startswith(p) for p in patterns):
type_ = DEVICE_TYPES.get(type_key, default_type)
break
else:
# Name-based type guessing
type_name_patterns = {
"iphone|ipad": "Phone",
"macbook|imac": "Laptop",
"pixel|galaxy|redmi": "Phone",
"laptop|notebook": "Laptop",
"printer|print": "Printer",
"router|gateway|ap|access[ -]?point": "Router",
"tv|television|smarttv": "TV",
"desktop|pc|computer": "Desktop",
"tablet|pad": "Tablet",
"watch|wear": "Smartwatch",
"camera|cam|webcam": "Camera",
"echo|alexa|dot": "SmartSpeaker",
"hue|lifx|bulb": "SmartLight",
"server|nas": "Server",
"playstation|xbox|switch": "GamingConsole",
"raspberry|pi": "IoT",
"google|chromecast|nest": "SmartHome",
"doorbell|lock|security": "SecurityDevice",
}
for pattern, type_key in type_name_patterns.items():
if re.search(pattern, name, re.IGNORECASE):
type_ = DEVICE_TYPES.get(type_key, default_type)
break
else:
# IP-based type guessing
type_ip_patterns = {
r"^192\.168\.[0-1]\.1$": "Router",
r"^10\.0\.0\.1$": "Router",
r"^192\.168\.[0-1]\.[2-9]$": "Desktop",
r"^192\.168\.[0-1]\.1\d{2}$": "Phone",
}
for pattern, type_key in type_ip_patterns.items():
if re.match(pattern, ip):
type_ = DEVICE_TYPES.get(type_key, default_type)
break
else:
type_ = default_type
type_ = None
icon = None
# --- Strict MAC + vendor rule matching from external file ---
type_, icon = match_mac_and_vendor(mac_clean, vendor, default_type, default_icon)
# --- Loose Vendor-based fallback ---
if not type_ or type_ == default_type:
type_, icon = match_vendor(vendor, default_type, default_icon)
# --- Loose Name-based fallback ---
if not type_ or type_ == default_type:
type_, icon = match_name(name, default_type, default_icon)
# --- Loose IP-based fallback ---
if (not type_ or type_ == default_type) or (not icon or icon == default_icon):
type_, icon = match_ip(ip, default_type, default_icon)
# Final fallbacks
type_ = type_ or default_type
icon = icon or default_icon
mylog('debug', f"[guess_device_attributes] Guessed attributes (icon|type_): ('{icon}'|'{type_}')")
return icon, type_
# Deprecated functions with redirects (To be removed once all calls for these have been adjusted to use the updated function)
def guess_icon(
vendor: Optional[str],
@@ -308,7 +270,7 @@ def guess_type(
default: Default type to return if no match is found.
Returns:
str: Device type from DEVICE_TYPES dictionary.
str: Device type.
"""
_, type_ = guess_device_attributes(vendor, mac, ip, name, "unknown_icon", default)

View File

@@ -6,7 +6,8 @@ sys.path.extend([f"{INSTALL_PATH}/server"])
import conf
from scan.device_handling import create_new_devices, print_scan_stats, save_scanned_devices, exclude_ignored_devices, update_devices_data_from_scan
from helper import timeNowTZ, print_table_schema, get_setting_value
from helper import timeNowTZ, get_setting_value
from db.db_helper import print_table_schema
from logger import mylog, Logger
from messaging.reporting import skip_repeated_notifications

121
test/test_device_endpoints.py Executable file
View File

@@ -0,0 +1,121 @@
import sys
import pathlib
import sqlite3
import random
import string
import uuid
import pytest
INSTALL_PATH = "/app"
sys.path.extend([f"{INSTALL_PATH}/front/plugins", f"{INSTALL_PATH}/server"])
from helper import timeNowTZ, get_setting_value
from api_server.api_server_start import app
@pytest.fixture(scope="session")
def api_token():
return get_setting_value("API_TOKEN")
@pytest.fixture
def client():
with app.test_client() as client:
yield client
@pytest.fixture
def test_mac():
# Generate a unique MAC for each test run
return "AA:BB:CC:" + ":".join(f"{random.randint(0,255):02X}" for _ in range(3))
def auth_headers(token):
return {"Authorization": f"Bearer {token}"}
def test_create_device(client, api_token, test_mac):
payload = {
"createNew": True,
"name": "Test Device",
"owner": "Unit Test",
"type": "Router",
"vendor": "TestVendor",
}
resp = client.post(f"/device/{test_mac}", json=payload, headers=auth_headers(api_token))
assert resp.status_code == 200
assert resp.json.get("success") is True
def test_get_device(client, api_token, test_mac):
# First create it
client.post(f"/device/{test_mac}", json={"createNew": True}, headers=auth_headers(api_token))
# Then retrieve it
resp = client.get(f"/device/{test_mac}", headers=auth_headers(api_token))
assert resp.status_code == 200
assert resp.json.get("devMac") == test_mac
def test_reset_device_props(client, api_token, test_mac):
client.post(f"/device/{test_mac}", json={"createNew": True}, headers=auth_headers(api_token))
resp = client.post(f"/device/{test_mac}/reset-props", json={}, headers=auth_headers(api_token))
assert resp.status_code == 200
assert resp.json.get("success") is True
def test_delete_device_events(client, api_token, test_mac):
client.post(f"/device/{test_mac}", json={"createNew": True}, headers=auth_headers(api_token))
resp = client.delete(f"/device/{test_mac}/events/delete", headers=auth_headers(api_token))
assert resp.status_code == 200
assert resp.json.get("success") is True
def test_delete_device(client, api_token, test_mac):
client.post(f"/device/{test_mac}", json={"createNew": True}, headers=auth_headers(api_token))
resp = client.delete(f"/device/{test_mac}/delete", headers=auth_headers(api_token))
assert resp.status_code == 200
assert resp.json.get("success") is True
def test_copy_device(client, api_token, test_mac):
# Step 1: Create the source device
payload = {"createNew": True, "name": "Source Device"}
resp = client.post(f"/device/{test_mac}", json=payload, headers=auth_headers(api_token))
assert resp.status_code == 200
assert resp.json.get("success") is True
# Step 2: Generate a target MAC
target_mac = "AA:BB:CC:" + ":".join(f"{random.randint(0,255):02X}" for _ in range(3))
# Step 3: Copy device
copy_payload = {"macFrom": test_mac, "macTo": target_mac}
resp = client.post("/device/copy", json=copy_payload, headers=auth_headers(api_token))
assert resp.status_code == 200
assert resp.json.get("success") is True
# Step 4: Verify new device exists
resp = client.get(f"/device/{target_mac}", headers=auth_headers(api_token))
assert resp.status_code == 200
assert resp.json.get("devMac") == target_mac
# Cleanup: delete both devices
client.delete(f"/device/{test_mac}/delete", headers=auth_headers(api_token))
client.delete(f"/device/{target_mac}/delete", headers=auth_headers(api_token))
def test_update_device_column(client, api_token, test_mac):
# First, create the device
client.post(
f"/device/{test_mac}",
json={"createNew": True},
headers=auth_headers(api_token),
)
# Update its parent MAC
resp = client.post(
f"/device/{test_mac}/update-column",
json={"columnName": "devParentMAC", "columnValue": "Internet"},
headers=auth_headers(api_token),
)
assert resp.status_code == 200
assert resp.json.get("success") is True
# Try updating a non-existent device
resp_missing = client.post(
"/device/11:22:33:44:55:66/update-column",
json={"columnName": "devParentMAC", "columnValue": "Internet"},
headers=auth_headers(api_token),
)
assert resp_missing.status_code == 404
assert resp_missing.json.get("success") is False

135
test/test_devices_endpoints.py Executable file
View File

@@ -0,0 +1,135 @@
import sys
import pathlib
import sqlite3
import base64
import random
import string
import uuid
import pytest
INSTALL_PATH = "/app"
sys.path.extend([f"{INSTALL_PATH}/front/plugins", f"{INSTALL_PATH}/server"])
from helper import timeNowTZ, get_setting_value
from api_server.api_server_start import app
@pytest.fixture(scope="session")
def api_token():
return get_setting_value("API_TOKEN")
@pytest.fixture
def client():
with app.test_client() as client:
yield client
@pytest.fixture
def test_mac():
# Generate a unique MAC for each test run
return "AA:BB:CC:" + ":".join(f"{random.randint(0,255):02X}" for _ in range(3))
def auth_headers(token):
return {"Authorization": f"Bearer {token}"}
def create_dummy(client, api_token, test_mac):
payload = {
"createNew": True,
"name": "Test Device",
"owner": "Unit Test",
"type": "Router",
"vendor": "TestVendor",
}
resp = client.post(f"/device/{test_mac}", json=payload, headers=auth_headers(api_token))
def test_delete_devices_with_macs(client, api_token, test_mac):
# First create device so it exists
create_dummy(client, api_token, test_mac)
client.post(f"/device/{test_mac}", json={"createNew": True}, headers=auth_headers(api_token))
# Delete by MAC
resp = client.delete("/devices", json={"macs": [test_mac]}, headers=auth_headers(api_token))
assert resp.status_code == 200
assert resp.json.get("success") is True
def test_delete_all_empty_macs(client, api_token):
resp = client.delete("/devices/empty-macs", headers=auth_headers(api_token))
assert resp.status_code == 200
# Expect success flag in response
assert resp.json.get("success") is True
def test_delete_unknown_devices(client, api_token):
resp = client.delete("/devices/unknown", headers=auth_headers(api_token))
assert resp.status_code == 200
assert resp.json.get("success") is True
def test_export_devices_csv(client, api_token, test_mac):
# Create a device first
create_dummy(client, api_token, test_mac)
# Export devices as CSV
resp = client.get("/devices/export/csv", headers=auth_headers(api_token))
assert resp.status_code == 200
assert resp.mimetype == "text/csv"
assert "attachment; filename=devices.csv" in resp.headers.get("Content-disposition", "")
# CSV should contain test_mac
assert test_mac in resp.data.decode()
def test_export_devices_json(client, api_token, test_mac):
# Create a device first
create_dummy(client, api_token, test_mac)
# Export devices as JSON
resp = client.get("/devices/export/json", headers=auth_headers(api_token))
assert resp.status_code == 200
assert resp.is_json
data = resp.get_json()
assert any(dev.get("devMac") == test_mac for dev in data["data"])
def test_export_devices_invalid_format(client, api_token):
# Request with unsupported format
resp = client.get("/devices/export/invalid", headers=auth_headers(api_token))
assert resp.status_code == 400
assert "Unsupported format" in resp.json.get("error")
def test_export_import_cycle_base64(client, api_token, test_mac):
# 1. Create a dummy device
create_dummy(client, api_token, test_mac)
# 2. Export devices as CSV
resp = client.get("/devices/export/csv", headers=auth_headers(api_token))
assert resp.status_code == 200
csv_data = resp.data.decode("utf-8")
# Ensure our dummy device is in the CSV
assert test_mac in csv_data
assert "Test Device" in csv_data
# 3. Base64-encode the CSV for JSON payload
csv_base64 = base64.b64encode(csv_data.encode("utf-8")).decode("utf-8")
json_payload = {"content": csv_base64}
# 4. POST to import endpoint with JSON content
resp = client.post(
"/devices/import",
json=json_payload,
headers={**auth_headers(api_token), "Content-Type": "application/json"}
)
assert resp.status_code == 200
assert resp.json.get("success") is True
# 5. Verify import results
assert resp.json.get("inserted") >= 1
assert resp.json.get("skipped_lines") == []
def test_delete_test_devices(client, api_token, test_mac):
# Delete by MAC
resp = client.delete("/devices", json={"macs": ["AA:BB:CC:*"]}, headers=auth_headers(api_token))
assert resp.status_code == 200
assert resp.json.get("success") is True

104
test/test_events_endpoints.py Executable file
View File

@@ -0,0 +1,104 @@
import sys
import pathlib
import sqlite3
import random
import string
import uuid
import pytest
INSTALL_PATH = "/app"
sys.path.extend([f"{INSTALL_PATH}/front/plugins", f"{INSTALL_PATH}/server"])
from helper import timeNowTZ, get_setting_value
from api_server.api_server_start import app
@pytest.fixture(scope="session")
def api_token():
return get_setting_value("API_TOKEN")
@pytest.fixture
def client():
with app.test_client() as client:
yield client
@pytest.fixture
def test_mac():
# Generate a unique MAC for each test run
return "AA:BB:CC:" + ":".join(f"{random.randint(0,255):02X}" for _ in range(3))
def auth_headers(token):
return {"Authorization": f"Bearer {token}"}
def create_event(client, api_token, mac, event="UnitTest Event", days_old=None):
"""
Create event using API (POST /event/<mac>).
If days_old is set, adds it to payload for backdating support.
"""
payload = {
"event": event,
}
if days_old:
payload["days_old"] = days_old
return client.post(f"/event/{mac}", json=payload, headers=auth_headers(api_token))
def list_events(client, api_token, mac=None):
url = "/events" if mac is None else f"/events/{mac}"
return client.get(url, headers=auth_headers(api_token))
def test_delete_events_for_mac(client, api_token, test_mac):
# create event
resp = create_event(client, api_token, test_mac)
assert resp.status_code == 200
# confirm exists
resp = list_events(client, api_token, test_mac)
assert resp.status_code == 200
assert any(ev["eve_MAC"] == test_mac for ev in resp.json)
# delete
resp = client.delete(f"/events/{test_mac}", headers=auth_headers(api_token))
assert resp.status_code == 200
assert resp.json.get("success") is True
# confirm deleted
resp = list_events(client, api_token, test_mac)
assert resp.status_code == 200
assert len(resp.json) == 0
def test_delete_all_events(client, api_token, test_mac):
# create two events
create_event(client, api_token, test_mac)
create_event(client, api_token, "FF:FF:FF:FF:FF:FF")
resp = list_events(client, api_token)
assert len(resp.json) >= 2
# delete all
resp = client.delete("/events", headers=auth_headers(api_token))
assert resp.status_code == 200
assert resp.json.get("success") is True
# confirm no events
resp = list_events(client, api_token)
assert len(resp.json) == 0
def test_delete_events_30days(client, api_token, test_mac):
# create old + new events
create_event(client, api_token, test_mac, days_old=40) # should be deleted
create_event(client, api_token, test_mac, days_old=5) # should remain
resp = list_events(client, api_token, test_mac)
assert len(resp.json) == 2
# delete events older than 30 days
resp = client.delete("/events/30days", headers=auth_headers(api_token))
assert resp.status_code == 200
assert resp.json.get("success") is True
# confirm only recent remains
resp = list_events(client, api_token, test_mac)
mac_events = [ev for ev in resp.json if ev["eve_MAC"] == test_mac]
assert len(mac_events) == 1

View File

@@ -1,191 +0,0 @@
import sys
import pathlib
import sqlite3
import random
import string
import uuid
from datetime import datetime, timedelta
sys.path.append(str(pathlib.Path(__file__).parent.parent.resolve()) + "/server/")
from helper import timeNowTZ, updateSubnets
# -------------------------------------------------------------------------------
def test_helper():
assert timeNow() == datetime.datetime.now().replace(microsecond=0)
# -------------------------------------------------------------------------------
def test_updateSubnets():
# test single subnet
subnet = "192.168.1.0/24 --interface=eth0"
result = updateSubnets(subnet)
assert type(result) is list
assert len(result) == 1
# test multiple subnets
subnet = ["192.168.1.0/24 --interface=eth0", "192.168.2.0/24 --interface=eth1"]
result = updateSubnets(subnet)
assert type(result) is list
assert len(result) == 2
# -------------------------------------------------------------------------------
# Function to insert N random device entries
def insert_devices(db_path, num_entries=1):
conn = sqlite3.connect(db_path)
cursor = conn.cursor()
print(f"{num_entries} entries to generate.")
# Function to generate a random MAC address
def generate_mac():
return '00:1A:2B:{:02X}:{:02X}:{:02X}'.format(random.randint(0, 255), random.randint(0, 255), random.randint(0, 255))
# Function to generate a random string of given length
def generate_random_string(length):
return ''.join(random.choices(string.ascii_letters + string.digits, k=length))
# Function to generate a random date within the last `n` days
def generate_random_date(n_days=365):
start_date = datetime.now() - timedelta(days=random.randint(0, n_days))
return start_date.strftime('%Y-%m-%d %H:%M:%S')
# Function to generate a GUID (Globally Unique Identifier)
def generate_guid():
return str(uuid.uuid4()) # Generates a unique GUID
# SQL query to insert a new row into Devices table
insert_query = """
INSERT INTO Devices (
devMac,
devName,
devOwner,
devType,
devVendor,
devFavorite,
devGroup,
devComments,
devFirstConnection,
devLastConnection,
devLastIP,
devStaticIP,
devScan,
devLogEvents,
devAlertEvents,
devAlertDown,
devSkipRepeated,
devLastNotification,
devPresentLastScan,
devIsNew,
devLocation,
devIsArchived,
devParentMAC,
devParentPort,
devIcon,
devGUID,
devSite,
devSSID,
devSyncHubNode,
devSourcePlugin,
devCustomProps,
devFQDN,
devParentRelType,
devReqNicsOnline
)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?);
"""
# List of device types, vendors, groups, locations
device_types = ['Phone', 'Laptop', 'Tablet', 'Other']
vendors = ['Vendor A', 'Vendor B', 'Vendor C']
groups = ['Group1', 'Group2']
locations = ['Location A', 'Location B']
# Insert the specified number of rows (default is 10,000)
for i in range(num_entries):
dev_mac = generate_mac()
dev_name = f'Device_{i:04d}'
dev_owner = f'Owner_{i % 100:03d}'
dev_type = random.choice(device_types)
dev_vendor = random.choice(vendors)
dev_favorite = random.choice([0, 1])
dev_group = random.choice(groups)
dev_comments = "" # Left as NULL
dev_first_connection = generate_random_date(365) # Within last 365 days
dev_last_connection = generate_random_date(30) # Within last 30 days
dev_last_ip = f'192.168.0.{random.randint(0, 255)}'
dev_static_ip = random.choice([0, 1])
dev_scan = random.randint(1, 10)
dev_log_events = random.choice([0, 1])
dev_alert_events = random.choice([0, 1])
dev_alert_down = random.choice([0, 1])
dev_skip_repeated = random.randint(0, 5)
dev_last_notification = "" # Left as NULL
dev_present_last_scan = random.choice([0, 1])
dev_is_new = random.choice([0, 1])
dev_location = random.choice(locations)
dev_is_archived = random.choice([0, 1])
dev_parent_mac = "" # Left as NULL
dev_parent_port = "" # Left as NULL
dev_icon = "" # Left as NULL
dev_guid = generate_guid() # Left as NULL
dev_site = "" # Left as NULL
dev_ssid = "" # Left as NULL
dev_sync_hub_node = "" # Left as NULL
dev_source_plugin = "" # Left as NULL
dev_devCustomProps = "" # Left as NULL
dev_devFQDN = "" # Left as NULL
# Execute the insert query
cursor.execute(insert_query, (
dev_mac,
dev_name,
dev_owner,
dev_type,
dev_vendor,
dev_favorite,
dev_group,
dev_comments,
dev_first_connection,
dev_last_connection,
dev_last_ip,
dev_static_ip,
dev_scan,
dev_log_events,
dev_alert_events,
dev_alert_down,
dev_skip_repeated,
dev_last_notification,
dev_present_last_scan,
dev_is_new,
dev_location,
dev_is_archived,
dev_parent_mac,
dev_parent_port,
dev_icon,
dev_guid,
dev_site,
dev_ssid,
dev_sync_hub_node,
dev_source_plugin,
dev_devCustomProps,
dev_devFQDN
))
# Commit after every 1000 rows to improve performance
if i % 1000 == 0:
conn.commit()
# Final commit to save all remaining data
conn.commit()
# Close the database connection
conn.close()
print(f"{num_entries} entries have been successfully inserted into the Devices table.")
# -------------------------------------------------------------------------------
if __name__ == "__main__":
# Call insert_devices with database path and number of entries as arguments
db_path = "/app/db/app.db"
num_entries = int(sys.argv[1]) if len(sys.argv) > 1 else 10000
insert_devices(db_path, num_entries)

35
test/test_history_endpoints.py Executable file
View File

@@ -0,0 +1,35 @@
import sys
import pathlib
import sqlite3
import random
import string
import uuid
import pytest
INSTALL_PATH = "/app"
sys.path.extend([f"{INSTALL_PATH}/front/plugins", f"{INSTALL_PATH}/server"])
from helper import timeNowTZ, get_setting_value
from api_server.api_server_start import app
@pytest.fixture(scope="session")
def api_token():
return get_setting_value("API_TOKEN")
@pytest.fixture
def client():
with app.test_client() as client:
yield client
@pytest.fixture
def test_mac():
# Generate a unique MAC for each test run
return "AA:BB:CC:" + ":".join(f"{random.randint(0,255):02X}" for _ in range(3))
def auth_headers(token):
return {"Authorization": f"Bearer {token}"}
def test_delete_history(client, api_token):
resp = client.delete(f"/history", headers=auth_headers(api_token))
assert resp.status_code == 200
assert resp.json.get("success") is True