diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile index a7640f266..966f297fd 100755 --- a/.devcontainer/Dockerfile +++ b/.devcontainer/Dockerfile @@ -35,6 +35,7 @@ RUN apk add --no-cache \ shadow \ python3 \ python3-dev \ + py3-psutil \ gcc \ musl-dev \ libffi-dev \ @@ -136,7 +137,7 @@ ENV LANG=C.UTF-8 RUN apk add --no-cache bash mtr libbsd zip lsblk tzdata curl arp-scan iproute2 iproute2-ss nmap fping \ nmap-scripts traceroute nbtscan net-tools net-snmp-tools bind-tools awake ca-certificates \ - sqlite php83 php83-fpm php83-cgi php83-curl php83-sqlite3 php83-session python3 envsubst \ + sqlite php83 php83-fpm php83-cgi php83-curl php83-sqlite3 php83-session python3 py3-psutil envsubst \ nginx supercronic shadow su-exec jq && \ rm -Rf /var/cache/apk/* && \ rm -Rf /etc/nginx && \ diff --git a/.github/skills/code-standards/SKILL.md b/.github/skills/code-standards/SKILL.md index 7257e8bec..8b9e6ad66 100644 --- a/.github/skills/code-standards/SKILL.md +++ b/.github/skills/code-standards/SKILL.md @@ -5,6 +5,14 @@ description: NetAlertX coding standards and conventions. Use this when writing c # Code Standards +- ask me to review before going to each next step (mention n step out of x) +- before starting, prepare implementation plan +- ask me to review it and ask any clarifying questions first +- add test creation as last step - follow repo architecture patterns - do not place in the root of /test +- code has to be maintainable, no duplicate code +- follow DRY principle +- code files should be less than 500 LOC for better maintainability + ## File Length Keep code files under 500 lines. Split larger files into modules. @@ -42,11 +50,18 @@ Nested subprocess calls need their own timeout—outer timeout won't save you. ## Time Utilities ```python -from utils.datetime_utils import timeNowDB +from utils.datetime_utils import timeNowUTC -timestamp = timeNowDB() +timestamp = timeNowUTC() ``` +This is the ONLY function that calls datetime.datetime.now() in the entire codebase. + +⚠️ CRITICAL: ALL database timestamps MUST be stored in UTC +This is the SINGLE SOURCE OF TRUTH for current time in NetAlertX +Use timeNowUTC() for DB writes (returns UTC string by default) +Use timeNowUTC(as_string=False) for datetime operations (scheduling, comparisons, logging) + ## String Sanitization Use sanitizers from `server/helper.py` before storing user input. diff --git a/.github/skills/settings-management/SKILL.md b/.github/skills/settings-management/SKILL.md index 137ea4a2e..36d5adf64 100644 --- a/.github/skills/settings-management/SKILL.md +++ b/.github/skills/settings-management/SKILL.md @@ -37,11 +37,3 @@ Define in plugin's `config.json` manifest under the settings section. ## Environment Override Use `APP_CONF_OVERRIDE` environment variable for settings that must be set before startup. - -## Backend API URL - -For Codespaces, set `BACKEND_API_URL` to your Codespace URL: - -``` -BACKEND_API_URL=https://something-20212.app.github.dev/ -``` diff --git a/.github/workflows/run-all-tests.yml b/.github/workflows/run-all-tests.yml index 45118577a..dd4fc2f83 100644 --- a/.github/workflows/run-all-tests.yml +++ b/.github/workflows/run-all-tests.yml @@ -12,7 +12,7 @@ on: type: boolean default: false run_backend: - description: '📂 backend/ (SQL Builder & Security)' + description: '📂 backend/ & db/ (SQL Builder, Security & Migration)' type: boolean default: false run_docker_env: @@ -43,9 +43,9 @@ jobs: run: | PATHS="" # Folder Mapping with 'test/' prefix - if [ "${{ github.event.inputs.scan }}" == "true" ]; then PATHS="$PATHS test/scan/"; fi + if [ "${{ github.event.inputs.run_scan }}" == "true" ]; then PATHS="$PATHS test/scan/"; fi if [ "${{ github.event.inputs.run_api }}" == "true" ]; then PATHS="$PATHS test/api_endpoints/ test/server/"; fi - if [ "${{ github.event.inputs.run_backend }}" == "true" ]; then PATHS="$PATHS test/backend/"; fi + if [ "${{ github.event.inputs.run_backend }}" == "true" ]; then PATHS="$PATHS test/backend/ test/db/"; fi if [ "${{ github.event.inputs.run_docker_env }}" == "true" ]; then PATHS="$PATHS test/docker_tests/"; fi if [ "${{ github.event.inputs.run_ui }}" == "true" ]; then PATHS="$PATHS test/ui/"; fi diff --git a/.gitignore b/.gitignore index cf9ed1629..bc932eff1 100755 --- a/.gitignore +++ b/.gitignore @@ -25,6 +25,7 @@ front/api/* **/plugins/**/*.log **/plugins/cloud_services/* **/plugins/cloud_connector/* +**/plugins/heartbeat/* **/%40eaDir/ **/@eaDir/ diff --git a/Dockerfile b/Dockerfile index 0836f7fde..d5081be72 100755 --- a/Dockerfile +++ b/Dockerfile @@ -32,6 +32,7 @@ RUN apk add --no-cache \ shadow \ python3 \ python3-dev \ + py3-psutil \ gcc \ musl-dev \ libffi-dev \ @@ -133,7 +134,7 @@ ENV LANG=C.UTF-8 RUN apk add --no-cache bash mtr libbsd zip lsblk tzdata curl arp-scan iproute2 iproute2-ss nmap fping \ nmap-scripts traceroute nbtscan net-tools net-snmp-tools bind-tools awake ca-certificates \ - sqlite php83 php83-fpm php83-cgi php83-curl php83-sqlite3 php83-session python3 envsubst \ + sqlite php83 php83-fpm php83-cgi php83-curl php83-sqlite3 php83-session python3 py3-psutil envsubst \ nginx supercronic shadow su-exec jq && \ rm -Rf /var/cache/apk/* && \ rm -Rf /etc/nginx && \ diff --git a/Dockerfile.debian b/Dockerfile.debian index da41aad00..4b73d1b34 100755 --- a/Dockerfile.debian +++ b/Dockerfile.debian @@ -10,6 +10,7 @@ RUN apt-get update && apt-get install -y --no-install-recommends \ python3 \ python3-dev \ python3-pip \ + python3-psutil \ python3-venv \ gcc \ git \ @@ -193,7 +194,7 @@ RUN for vfile in .VERSION .VERSION_PREV; do \ # setcap cap_net_raw,cap_net_admin+eip $(readlink -f ${VIRTUAL_ENV_BIN}/python) && \ /bin/bash /build/init-nginx.sh && \ /bin/bash /build/init-php-fpm.sh && \ - # /bin/bash /build/init-cron.sh && \ + # /bin/bash /build/init-cron.sh && \ # Debian cron init might differ, skipping for now or need to check init-cron.sh content # Checking init-backend.sh /bin/bash /build/init-backend.sh && \ diff --git a/back/app.sql b/back/app.sql deleted file mode 100755 index ce49d3fcb..000000000 --- a/back/app.sql +++ /dev/null @@ -1,427 +0,0 @@ -CREATE TABLE sqlite_stat1(tbl,idx,stat); -CREATE TABLE Events (eve_MAC STRING (50) NOT NULL COLLATE NOCASE, eve_IP STRING (50) NOT NULL COLLATE NOCASE, eve_DateTime DATETIME NOT NULL, eve_EventType STRING (30) NOT NULL COLLATE NOCASE, eve_AdditionalInfo STRING (250) DEFAULT (''), eve_PendingAlertEmail BOOLEAN NOT NULL CHECK (eve_PendingAlertEmail IN (0, 1)) DEFAULT (1), eve_PairEventRowid INTEGER); -CREATE TABLE Sessions (ses_MAC STRING (50) COLLATE NOCASE, ses_IP STRING (50) COLLATE NOCASE, ses_EventTypeConnection STRING (30) COLLATE NOCASE, ses_DateTimeConnection DATETIME, ses_EventTypeDisconnection STRING (30) COLLATE NOCASE, ses_DateTimeDisconnection DATETIME, ses_StillConnected BOOLEAN, ses_AdditionalInfo STRING (250)); -CREATE TABLE IF NOT EXISTS "Online_History" ( - "Index" INTEGER, - "Scan_Date" TEXT, - "Online_Devices" INTEGER, - "Down_Devices" INTEGER, - "All_Devices" INTEGER, - "Archived_Devices" INTEGER, - "Offline_Devices" INTEGER, - PRIMARY KEY("Index" AUTOINCREMENT) - ); -CREATE TABLE sqlite_sequence(name,seq); -CREATE TABLE Devices ( - devMac STRING (50) PRIMARY KEY NOT NULL COLLATE NOCASE, - devName STRING (50) NOT NULL DEFAULT "(unknown)", - devOwner STRING (30) DEFAULT "(unknown)" NOT NULL, - devType STRING (30), - devVendor STRING (250), - devFavorite BOOLEAN CHECK (devFavorite IN (0, 1)) DEFAULT (0) NOT NULL, - devGroup STRING (10), - devComments TEXT, - devFirstConnection DATETIME NOT NULL, - devLastConnection DATETIME NOT NULL, - devLastIP STRING (50) NOT NULL COLLATE NOCASE, - devPrimaryIPv4 TEXT, - devPrimaryIPv6 TEXT, - devVlan TEXT, - devForceStatus TEXT, - devStaticIP BOOLEAN DEFAULT (0) NOT NULL CHECK (devStaticIP IN (0, 1)), - devScan INTEGER DEFAULT (1) NOT NULL, - devLogEvents BOOLEAN NOT NULL DEFAULT (1) CHECK (devLogEvents IN (0, 1)), - devAlertEvents BOOLEAN NOT NULL DEFAULT (1) CHECK (devAlertEvents IN (0, 1)), - devAlertDown BOOLEAN NOT NULL DEFAULT (0) CHECK (devAlertDown IN (0, 1)), - devSkipRepeated INTEGER DEFAULT 0 NOT NULL, - devLastNotification DATETIME, - devPresentLastScan BOOLEAN NOT NULL DEFAULT (0) CHECK (devPresentLastScan IN (0, 1)), - devIsNew BOOLEAN NOT NULL DEFAULT (1) CHECK (devIsNew IN (0, 1)), - devLocation STRING (250) COLLATE NOCASE, - devIsArchived BOOLEAN NOT NULL DEFAULT (0) CHECK (devIsArchived IN (0, 1)), - devParentMAC TEXT, - devParentPort INTEGER, - devParentRelType TEXT, - devIcon TEXT, - devGUID TEXT, - devSite TEXT, - devSSID TEXT, - devSyncHubNode TEXT, - devSourcePlugin TEXT, - devMacSource TEXT, - devNameSource TEXT, - devFQDNSource TEXT, - devLastIPSource TEXT, - devVendorSource TEXT, - devSSIDSource TEXT, - devParentMACSource TEXT, - devParentPortSource TEXT, - devParentRelTypeSource TEXT, - devVlanSource TEXT, - "devCustomProps" TEXT); -CREATE TABLE IF NOT EXISTS "Settings" ( - "setKey" TEXT, - "setName" TEXT, - "setDescription" TEXT, - "setType" TEXT, - "setOptions" TEXT, - "setGroup" TEXT, - "setValue" TEXT, - "setEvents" TEXT, - "setOverriddenByEnv" INTEGER - ); -CREATE TABLE IF NOT EXISTS "Parameters" ( - "par_ID" TEXT PRIMARY KEY, - "par_Value" TEXT - ); -CREATE TABLE Plugins_Objects( - "Index" INTEGER, - Plugin TEXT NOT NULL, - Object_PrimaryID TEXT NOT NULL, - Object_SecondaryID TEXT NOT NULL, - DateTimeCreated TEXT NOT NULL, - DateTimeChanged TEXT NOT NULL, - Watched_Value1 TEXT NOT NULL, - Watched_Value2 TEXT NOT NULL, - Watched_Value3 TEXT NOT NULL, - Watched_Value4 TEXT NOT NULL, - Status TEXT NOT NULL, - Extra TEXT NOT NULL, - UserData TEXT NOT NULL, - ForeignKey TEXT NOT NULL, - SyncHubNodeName TEXT, - "HelpVal1" TEXT, - "HelpVal2" TEXT, - "HelpVal3" TEXT, - "HelpVal4" TEXT, - ObjectGUID TEXT, - PRIMARY KEY("Index" AUTOINCREMENT) - ); -CREATE TABLE Plugins_Events( - "Index" INTEGER, - Plugin TEXT NOT NULL, - Object_PrimaryID TEXT NOT NULL, - Object_SecondaryID TEXT NOT NULL, - DateTimeCreated TEXT NOT NULL, - DateTimeChanged TEXT NOT NULL, - Watched_Value1 TEXT NOT NULL, - Watched_Value2 TEXT NOT NULL, - Watched_Value3 TEXT NOT NULL, - Watched_Value4 TEXT NOT NULL, - Status TEXT NOT NULL, - Extra TEXT NOT NULL, - UserData TEXT NOT NULL, - ForeignKey TEXT NOT NULL, - SyncHubNodeName TEXT, - "HelpVal1" TEXT, - "HelpVal2" TEXT, - "HelpVal3" TEXT, - "HelpVal4" TEXT, "ObjectGUID" TEXT, - PRIMARY KEY("Index" AUTOINCREMENT) - ); -CREATE TABLE Plugins_History( - "Index" INTEGER, - Plugin TEXT NOT NULL, - Object_PrimaryID TEXT NOT NULL, - Object_SecondaryID TEXT NOT NULL, - DateTimeCreated TEXT NOT NULL, - DateTimeChanged TEXT NOT NULL, - Watched_Value1 TEXT NOT NULL, - Watched_Value2 TEXT NOT NULL, - Watched_Value3 TEXT NOT NULL, - Watched_Value4 TEXT NOT NULL, - Status TEXT NOT NULL, - Extra TEXT NOT NULL, - UserData TEXT NOT NULL, - ForeignKey TEXT NOT NULL, - SyncHubNodeName TEXT, - "HelpVal1" TEXT, - "HelpVal2" TEXT, - "HelpVal3" TEXT, - "HelpVal4" TEXT, "ObjectGUID" TEXT, - PRIMARY KEY("Index" AUTOINCREMENT) - ); -CREATE TABLE Plugins_Language_Strings( - "Index" INTEGER, - Language_Code TEXT NOT NULL, - String_Key TEXT NOT NULL, - String_Value TEXT NOT NULL, - Extra TEXT NOT NULL, - PRIMARY KEY("Index" AUTOINCREMENT) - ); -CREATE TABLE CurrentScan ( - scanMac STRING(50) NOT NULL COLLATE NOCASE, - scanLastIP STRING(50) NOT NULL COLLATE NOCASE, - scanVendor STRING(250), - scanSourcePlugin STRING(10), - scanName STRING(250), - scanLastQuery STRING(250), - scanLastConnection STRING(250), - scanSyncHubNode STRING(50), - scanSite STRING(250), - scanSSID STRING(250), - scanVlan STRING(250), - scanParentMAC STRING(250), - scanParentPort STRING(250), - scanType STRING(250), - UNIQUE(scanMac) - ); -CREATE TABLE IF NOT EXISTS "AppEvents" ( - "Index" INTEGER PRIMARY KEY AUTOINCREMENT, - "GUID" TEXT UNIQUE, - "AppEventProcessed" BOOLEAN, - "DateTimeCreated" TEXT, - "ObjectType" TEXT, - "ObjectGUID" TEXT, - "ObjectPlugin" TEXT, - "ObjectPrimaryID" TEXT, - "ObjectSecondaryID" TEXT, - "ObjectForeignKey" TEXT, - "ObjectIndex" TEXT, - "ObjectIsNew" BOOLEAN, - "ObjectIsArchived" BOOLEAN, - "ObjectStatusColumn" TEXT, - "ObjectStatus" TEXT, - "AppEventType" TEXT, - "Helper1" TEXT, - "Helper2" TEXT, - "Helper3" TEXT, - "Extra" TEXT - ); -CREATE TABLE IF NOT EXISTS "Notifications" ( - "Index" INTEGER, - "GUID" TEXT UNIQUE, - "DateTimeCreated" TEXT, - "DateTimePushed" TEXT, - "Status" TEXT, - "JSON" TEXT, - "Text" TEXT, - "HTML" TEXT, - "PublishedVia" TEXT, - "Extra" TEXT, - PRIMARY KEY("Index" AUTOINCREMENT) - ); -CREATE INDEX IDX_eve_DateTime ON Events (eve_DateTime); -CREATE INDEX IDX_eve_EventType ON Events (eve_EventType COLLATE NOCASE); -CREATE INDEX IDX_eve_MAC ON Events (eve_MAC COLLATE NOCASE); -CREATE INDEX IDX_eve_PairEventRowid ON Events (eve_PairEventRowid); -CREATE INDEX IDX_ses_EventTypeDisconnection ON Sessions (ses_EventTypeDisconnection COLLATE NOCASE); -CREATE INDEX IDX_ses_EventTypeConnection ON Sessions (ses_EventTypeConnection COLLATE NOCASE); -CREATE INDEX IDX_ses_DateTimeDisconnection ON Sessions (ses_DateTimeDisconnection); -CREATE INDEX IDX_ses_MAC ON Sessions (ses_MAC COLLATE NOCASE); -CREATE INDEX IDX_ses_DateTimeConnection ON Sessions (ses_DateTimeConnection); -CREATE INDEX IDX_dev_PresentLastScan ON Devices (devPresentLastScan); -CREATE INDEX IDX_dev_FirstConnection ON Devices (devFirstConnection); -CREATE INDEX IDX_dev_AlertDeviceDown ON Devices (devAlertDown); -CREATE INDEX IDX_dev_StaticIP ON Devices (devStaticIP); -CREATE INDEX IDX_dev_ScanCycle ON Devices (devScan); -CREATE INDEX IDX_dev_Favorite ON Devices (devFavorite); -CREATE INDEX IDX_dev_LastIP ON Devices (devLastIP); -CREATE INDEX IDX_dev_NewDevice ON Devices (devIsNew); -CREATE INDEX IDX_dev_Archived ON Devices (devIsArchived); -CREATE VIEW Events_Devices AS - SELECT * - FROM Events - LEFT JOIN Devices ON eve_MAC = devMac -/* Events_Devices(eve_MAC,eve_IP,eve_DateTime,eve_EventType,eve_AdditionalInfo,eve_PendingAlertEmail,eve_PairEventRowid,devMac,devName,devOwner,devType,devVendor,devFavorite,devGroup,devComments,devFirstConnection,devLastConnection,devLastIP,devStaticIP,devScan,devLogEvents,devAlertEvents,devAlertDown,devSkipRepeated,devLastNotification,devPresentLastScan,devIsNew,devLocation,devIsArchived,devParentMAC,devParentPort,devIcon,devGUID,devSite,devSSID,devSyncHubNode,devSourcePlugin,devCustomProps) */; -CREATE VIEW LatestEventsPerMAC AS - WITH RankedEvents AS ( - SELECT - e.*, - ROW_NUMBER() OVER (PARTITION BY e.eve_MAC ORDER BY e.eve_DateTime DESC) AS row_num - FROM Events AS e - ) - SELECT - e.*, - d.*, - c.* - FROM RankedEvents AS e - LEFT JOIN Devices AS d ON e.eve_MAC = d.devMac - INNER JOIN CurrentScan AS c ON e.eve_MAC = c.scanMac - WHERE e.row_num = 1 -/* LatestEventsPerMAC(eve_MAC,eve_IP,eve_DateTime,eve_EventType,eve_AdditionalInfo,eve_PendingAlertEmail,eve_PairEventRowid,row_num,devMac,devName,devOwner,devType,devVendor,devFavorite,devGroup,devComments,devFirstConnection,devLastConnection,devLastIP,devStaticIP,devScan,devLogEvents,devAlertEvents,devAlertDown,devSkipRepeated,devLastNotification,devPresentLastScan,devIsNew,devLocation,devIsArchived,devParentMAC,devParentPort,devIcon,devGUID,devSite,devSSID,devSyncHubNode,devSourcePlugin,devCustomProps,scanMac,scanLastIP,scanVendor,scanSourcePlugin,scanName,scanLastQuery,scanLastConnection,scanSyncHubNode,scanSite,scanSSID,scanParentMAC,scanParentPort,scanType) */; -CREATE VIEW Sessions_Devices AS SELECT * FROM Sessions LEFT JOIN "Devices" ON ses_MAC = devMac -/* Sessions_Devices(ses_MAC,ses_IP,ses_EventTypeConnection,ses_DateTimeConnection,ses_EventTypeDisconnection,ses_DateTimeDisconnection,ses_StillConnected,ses_AdditionalInfo,devMac,devName,devOwner,devType,devVendor,devFavorite,devGroup,devComments,devFirstConnection,devLastConnection,devLastIP,devStaticIP,devScan,devLogEvents,devAlertEvents,devAlertDown,devSkipRepeated,devLastNotification,devPresentLastScan,devIsNew,devLocation,devIsArchived,devParentMAC,devParentPort,devIcon,devGUID,devSite,devSSID,devSyncHubNode,devSourcePlugin,devCustomProps) */; -CREATE VIEW Convert_Events_to_Sessions AS SELECT EVE1.eve_MAC, - EVE1.eve_IP, - EVE1.eve_EventType AS eve_EventTypeConnection, - EVE1.eve_DateTime AS eve_DateTimeConnection, - CASE WHEN EVE2.eve_EventType IN ('Disconnected', 'Device Down') OR - EVE2.eve_EventType IS NULL THEN EVE2.eve_EventType ELSE '' END AS eve_EventTypeDisconnection, - CASE WHEN EVE2.eve_EventType IN ('Disconnected', 'Device Down') THEN EVE2.eve_DateTime ELSE NULL END AS eve_DateTimeDisconnection, - CASE WHEN EVE2.eve_EventType IS NULL THEN 1 ELSE 0 END AS eve_StillConnected, - EVE1.eve_AdditionalInfo - FROM Events AS EVE1 - LEFT JOIN - Events AS EVE2 ON EVE1.eve_PairEventRowID = EVE2.RowID - WHERE EVE1.eve_EventType IN ('New Device', 'Connected','Down Reconnected') - UNION - SELECT eve_MAC, - eve_IP, - '' AS eve_EventTypeConnection, - NULL AS eve_DateTimeConnection, - eve_EventType AS eve_EventTypeDisconnection, - eve_DateTime AS eve_DateTimeDisconnection, - 0 AS eve_StillConnected, - eve_AdditionalInfo - FROM Events AS EVE1 - WHERE (eve_EventType = 'Device Down' OR - eve_EventType = 'Disconnected') AND - EVE1.eve_PairEventRowID IS NULL -/* Convert_Events_to_Sessions(eve_MAC,eve_IP,eve_EventTypeConnection,eve_DateTimeConnection,eve_EventTypeDisconnection,eve_DateTimeDisconnection,eve_StillConnected,eve_AdditionalInfo) */; -CREATE TRIGGER "trg_insert_devices" - AFTER INSERT ON "Devices" - WHEN NOT EXISTS ( - SELECT 1 FROM AppEvents - WHERE AppEventProcessed = 0 - AND ObjectType = 'Devices' - AND ObjectGUID = NEW.devGUID - AND ObjectStatus = CASE WHEN NEW.devPresentLastScan = 1 THEN 'online' ELSE 'offline' END - AND AppEventType = 'insert' - ) - BEGIN - INSERT INTO "AppEvents" ( - "GUID", - "DateTimeCreated", - "AppEventProcessed", - "ObjectType", - "ObjectGUID", - "ObjectPrimaryID", - "ObjectSecondaryID", - "ObjectStatus", - "ObjectStatusColumn", - "ObjectIsNew", - "ObjectIsArchived", - "ObjectForeignKey", - "ObjectPlugin", - "AppEventType" - ) - VALUES ( - - lower( - hex(randomblob(4)) || '-' || hex(randomblob(2)) || '-' || '4' || - substr(hex( randomblob(2)), 2) || '-' || - substr('AB89', 1 + (abs(random()) % 4) , 1) || - substr(hex(randomblob(2)), 2) || '-' || - hex(randomblob(6)) - ) - , - DATETIME('now'), - FALSE, - 'Devices', - NEW.devGUID, -- ObjectGUID - NEW.devMac, -- ObjectPrimaryID - NEW.devLastIP, -- ObjectSecondaryID - CASE WHEN NEW.devPresentLastScan = 1 THEN 'online' ELSE 'offline' END, -- ObjectStatus - 'devPresentLastScan', -- ObjectStatusColumn - NEW.devIsNew, -- ObjectIsNew - NEW.devIsArchived, -- ObjectIsArchived - NEW.devGUID, -- ObjectForeignKey - 'DEVICES', -- ObjectForeignKey - 'insert' - ); - END; -CREATE TRIGGER "trg_update_devices" - AFTER UPDATE ON "Devices" - WHEN NOT EXISTS ( - SELECT 1 FROM AppEvents - WHERE AppEventProcessed = 0 - AND ObjectType = 'Devices' - AND ObjectGUID = NEW.devGUID - AND ObjectStatus = CASE WHEN NEW.devPresentLastScan = 1 THEN 'online' ELSE 'offline' END - AND AppEventType = 'update' - ) - BEGIN - INSERT INTO "AppEvents" ( - "GUID", - "DateTimeCreated", - "AppEventProcessed", - "ObjectType", - "ObjectGUID", - "ObjectPrimaryID", - "ObjectSecondaryID", - "ObjectStatus", - "ObjectStatusColumn", - "ObjectIsNew", - "ObjectIsArchived", - "ObjectForeignKey", - "ObjectPlugin", - "AppEventType" - ) - VALUES ( - - lower( - hex(randomblob(4)) || '-' || hex(randomblob(2)) || '-' || '4' || - substr(hex( randomblob(2)), 2) || '-' || - substr('AB89', 1 + (abs(random()) % 4) , 1) || - substr(hex(randomblob(2)), 2) || '-' || - hex(randomblob(6)) - ) - , - DATETIME('now'), - FALSE, - 'Devices', - NEW.devGUID, -- ObjectGUID - NEW.devMac, -- ObjectPrimaryID - NEW.devLastIP, -- ObjectSecondaryID - CASE WHEN NEW.devPresentLastScan = 1 THEN 'online' ELSE 'offline' END, -- ObjectStatus - 'devPresentLastScan', -- ObjectStatusColumn - NEW.devIsNew, -- ObjectIsNew - NEW.devIsArchived, -- ObjectIsArchived - NEW.devGUID, -- ObjectForeignKey - 'DEVICES', -- ObjectForeignKey - 'update' - ); - END; -CREATE TRIGGER "trg_delete_devices" - AFTER DELETE ON "Devices" - WHEN NOT EXISTS ( - SELECT 1 FROM AppEvents - WHERE AppEventProcessed = 0 - AND ObjectType = 'Devices' - AND ObjectGUID = OLD.devGUID - AND ObjectStatus = CASE WHEN OLD.devPresentLastScan = 1 THEN 'online' ELSE 'offline' END - AND AppEventType = 'delete' - ) - BEGIN - INSERT INTO "AppEvents" ( - "GUID", - "DateTimeCreated", - "AppEventProcessed", - "ObjectType", - "ObjectGUID", - "ObjectPrimaryID", - "ObjectSecondaryID", - "ObjectStatus", - "ObjectStatusColumn", - "ObjectIsNew", - "ObjectIsArchived", - "ObjectForeignKey", - "ObjectPlugin", - "AppEventType" - ) - VALUES ( - - lower( - hex(randomblob(4)) || '-' || hex(randomblob(2)) || '-' || '4' || - substr(hex( randomblob(2)), 2) || '-' || - substr('AB89', 1 + (abs(random()) % 4) , 1) || - substr(hex(randomblob(2)), 2) || '-' || - hex(randomblob(6)) - ) - , - DATETIME('now'), - FALSE, - 'Devices', - OLD.devGUID, -- ObjectGUID - OLD.devMac, -- ObjectPrimaryID - OLD.devLastIP, -- ObjectSecondaryID - CASE WHEN OLD.devPresentLastScan = 1 THEN 'online' ELSE 'offline' END, -- ObjectStatus - 'devPresentLastScan', -- ObjectStatusColumn - OLD.devIsNew, -- ObjectIsNew - OLD.devIsArchived, -- ObjectIsArchived - OLD.devGUID, -- ObjectForeignKey - 'DEVICES', -- ObjectForeignKey - 'delete' - ); - END; diff --git a/docs/ADVISORY_EYES_ON_GLASS.md b/docs/ADVISORY_EYES_ON_GLASS.md new file mode 100644 index 000000000..ace8f0dcc --- /dev/null +++ b/docs/ADVISORY_EYES_ON_GLASS.md @@ -0,0 +1,56 @@ +### Build an MSP Wallboard for Network Monitoring + +For Managed Service Providers (MSPs) and Network Operations Centers (NOC), "Eyes on Glass" monitoring requires a UI that is both self-healing (auto-refreshing) and focused only on critical data. By leveraging the **UI Settings Plugin**, you can transform NetAlertX from a management tool into a dedicated live monitor. + +![filters](./img/ADVISORIES/filters.png) + +--- + +### 1. Configure Auto-Refresh for Live Monitoring + +Static dashboards are the enemy of real-time response. NetAlertX allows you to force the UI to pull fresh data without manual page reloads. + +* **Setting:** Locate the `UI_REFRESH` (or similar "Auto-refresh UI") setting within the **UI Settings plugin**. +* **Optimal Interval:** Set this between **60 to 120 seconds**. +* *Note:* Refreshing too frequently (e.g., <30s) on large networks can lead to high browser and server CPU usage. + +![ui_customization_settings](./img/ADVISORIES/ui_customization_settings.png) + +### 2. Streamlining the Dashboard (MSP Mode) + +An MSP's focus is on what is *broken*, not what is working. Hide the noise to increase reaction speed. + +* **Hide Unnecessary Blocks:** Under UI Settings, disable dashboard blocks that don't provide immediate utility, such as **Online presence** or **Tiles**. +* **Hide virtual connections:** You can specify which relationships shoudl be hidden from the main view to remove any virtual devices that are not essential from your views. +* **Browser Full-Screen:** Use the built-in "Full Screen" toggle in the top bar to remove browser chrome (URL bars/tabs) for a cleaner "Wallboard" look. + +### 3. Creating Custom NOC Views + +Use the UI Filters in tandem with UI Settings to create custom views. + +![NetAlertX NOC dashboard showing offline network devices for MSP monitoring](./img/ADVISORIES/down_devices.png) + +| Feature | NOC/MSP Application | +| --- | --- | +| **Site-Specific Nodes** | Filter the view by a specific "Sync Node" or "Location" filter to monitor a single client site. | +| **Filter by Criticality** | Filter devices where `Group == "Infrastructure"` or `"Server"`. (depending on your predefined values) | +| **Predefined "Down" View** | Bookmark the URL with the `/devices.php#down` path to ensure the dashboard always loads into an "Alert Only" mode. | + +### 4. Browser & Cache Stability + +Because the UI is a web application, long-running sessions can occasionally experience cache drift. + +* **Cache Refresh:** If you notice the "Show # Entries" resetting or icons failing to load after days of uptime, use the **Reload** icon in the application header (not the browser refresh) to clear the internal app cache. +* **Dedicated Hardware:** For 24/7 monitoring, use a dedicated thin client or Raspberry Pi running in "Kiosk Mode" to prevent OS-level popups from obscuring the dashboard. + +> [!TIP] +> [NetAlertX - Detailed Dashboard Guide](https://www.youtube.com/watch?v=umh1c_40HW8) +> This video provides a visual walkthrough of the NetAlertX dashboard features, including how to map and visualize devices which is crucial for setting up a clear "Eyes on Glass" monitoring environment. + +### Summary Checklist + +* [ ] **Automate Refresh:** Set `UI_REFRESH` to **60-120s** in UI Settings to ensure the dashboard stays current without manual intervention. +* [ ] **Filter for Criticality:** Bookmark the **`/devices.php#down`** view to instantly focus on offline assets rather than the entire inventory. +* [ ] **Remove UI Noise:** Use UI Settings to hide non-essential dashboard blocks (e.g., **Tiles** or remove **Virtual Connections** devices) to maximize screen real estate for alerts. +* [ ] **Segment by Site:** Use **Location** or **Sync Node** filters to create dedicated views for specific client networks or physical branches. +* [ ] **Ensure Stability:** Run on a dedicated "Kiosk" browser and use the internal **Reload icon** occasionally to maintain a clean application cache. diff --git a/docs/ADVISORY_MULTI_NETWORK.md b/docs/ADVISORY_MULTI_NETWORK.md new file mode 100644 index 000000000..7a0dd0948 --- /dev/null +++ b/docs/ADVISORY_MULTI_NETWORK.md @@ -0,0 +1,121 @@ +## ADVISORY: Best Practices for Monitoring Multiple Networks with NetAlertX + +### 1. Define Monitoring Scope & Architecture + +Effective multi-network monitoring starts with understanding how NetAlertX "sees" your traffic. + +* **A. Understand Network Accessibility:** Local ARP-based scanning (**ARPSCAN**) only discovers devices on directly accessible subnets due to Layer 2 limitations. It cannot traverse VPNs or routed borders without specific configuration. +* **B. Plan Subnet & Scan Interfaces:** Explicitly configure each accessible segment in `SCAN_SUBNETS` with the corresponding interfaces. +* **C. Remote & Inaccessible Networks:** For networks unreachable via ARP, use these strategies: +* **Alternate Plugins:** Supplement discovery with [SNMPDSC](SNMPDSC) or [DHCP lease imports](https://docs.netalertx.com/PLUGINS/?h=DHCPLSS#available-plugins). +* **Centralized Multi-Tenant Management using Sync Nodes:** Run secondary NetAlertX instances on isolated networks and aggregate data using the **SYNC plugin**. +* **Manual Entry:** For static assets where only ICMP (ping) status is needed. + +> [!TIP] +> Explore the [remote networks](./REMOTE_NETWORKS.md) documentation for more details on how to set up the approaches menationed above. + +--- + +### 2. Automating IT Asset Inventory with Workflows + +[Workflows](./WORKFLOWS.md) are the "engine" of NetAlertX, reducing manual overhead as your device list grows. + +* **A. Logical Ownership & VLAN Tagging:** Create a workflow triggered on **Device Creation** to: +1. Inspect the IP/Subnet. +2. Set `devVlan` or `devOwner` custom fields automatically. + + +* **B. Auto-Grouping:** Use conditional logic to categorize devices. +* *Example:* If `devLastIP == 10.10.20.*`, then `Set devLocation = "BranchOffice"`. + +```json +{ + "name": "Assign Location - BranchOffice", + "trigger": { + "object_type": "Devices", + "event_type": "update" + }, + "conditions": [ + { + "logic": "AND", + "conditions": [ + { + "field": "devLastIP", + "operator": "contains", + "value": "10.10.20." + } + ] + } + ], + "actions": [ + { + "type": "update_field", + "field": "devLocation", + "value": "BranchOffice" + } + ] +} +``` + + +* **C. Sync Node Tracking:** When using multiple instances, ensure all synchub nodes have a descriptive `SYNC_node_name` name to distinguish between sites. + +> [!TIP] +> Always test new workflows in a "Staging" instance. A misconfigured workflow can trigger thousands of unintended updates across your database. + +--- + +### 3. Notification Strategy: Low Noise, High Signal + +A multi-network environment can generate significant "alert fatigue." Use a layered filtering approach. + +| Level | Strategy | Recommended Action | +| --- | --- | --- | +| **Device** | Silence Flapping | Use "Skip repeated notifications" for unstable IoT devices. | +| **Plugin** | Tune Watchers | Only enable `_WATCH` on reliable plugins (e.g., ICMP/SNMP). | +| **Global** | Filter Sections | Limit `NTFPRCS_INCLUDED_SECTIONS` to `new_devices` and `down_devices`. | + + +> [!TIP] +> **Ignore Rules:** Maintain strict **Ignored MAC** (`NEWDEV_ignored_MACs`) and **Ignored IP** (`NEWDEV_ignored_IPs`) lists for guest networks or broadcast scanners to keep your logs clean. + +--- + +### 4. UI Filters for Multi-Network Clarity + +Don't let a massive device list overwhelm you. Use the [Multi-edit features](./DEVICES_BULK_EDITING.md) to categorize devices and create focused views: + +* **By Zone:** Filter by "Location", "Site" or "Sync Node" you et up in Section 2. +* **By Criticality:** Use custom the device Type field to separate "Core Infrastructure" from "Ephemeral Clients." +* **By Status:** Use predefined views specifically for "Devices currently Down" to act as a Network Operations Center (NOC) dashboard. + +> [!TIP] +> If you are providing services as a Managed Service Provider (MSP) customize your default UI to be exactly how you need it, by hiding parts of the UI that you are not interested in, or by configuring a auto-refreshed screen monitoring your most important clients. See the [Eyes on glass](./ADVISORY_EYES_ON_GLASS.md) advisory for more details. + +--- + +### 5. Operational Stability & Sync Health + +* **Health Checks:** Regularly monitor the [Logs](https://docs.netalertx.com/LOGGING/?h=logs) to ensure remote nodes are reporting in. +* **Backups:** Use the **CSV Devices Backup** plugin. Standardize your workflow templates and [back up](./BACKUPS.md) you `/config` folders so that if a node fails, you can redeploy it with the same logic instantly. + + +### 6. Optimize Performance + +As your environment grows, tuning the underlying engine is vital to maintain a snappy UI and reliable discovery cycles. + +* **Plugin Scheduling:** Avoid "Scan Storms" by staggering plugin execution. Running intensive tasks like `NMAP` or `MASS_DNS` simultaneously can spike CPU and cause database locks. +* **Database Health:** Large-scale monitoring generates massive event logs. Use the **[DBCLNP (Database Cleanup)](https://www.google.com/search?q=https://docs.netalertx.com/PLUGINS/%23dbclnp)** plugin to prune old records and keep the SQLite database performant. +* **Resource Management:** For high-device counts, consider increasing the memory limit for the container and utilizing `tmpfs` for temporary files to reduce SD card/disk I/O bottlenecks. + +> [!IMPORTANT] +> For a deep dive into hardware requirements, database vacuuming, and specific environment variables for high-load instances, refer to the full **[Performance Optimization Guide](https://docs.netalertx.com/PERFORMANCE/)**. + +--- + +### Summary Checklist + +* [ ] **Discovery:** Are all subnets explicitly defined? +* [ ] **Automation:** Do new devices get auto-assigned to a VLAN/Owner? +* [ ] **Noise Control:** Are transient "Down" alerts delayed via `NTFPRCS_alert_down_time`? +* [ ] **Remote Sites:** Is the SYNC plugin authenticated and heartbeat-active? diff --git a/docs/DEVICE_MANAGEMENT.md b/docs/DEVICE_MANAGEMENT.md index 51685323e..658edc584 100755 --- a/docs/DEVICE_MANAGEMENT.md +++ b/docs/DEVICE_MANAGEMENT.md @@ -39,9 +39,24 @@ The **MAC** field and the **Last IP** field will then become editable. ![Save Dummy Device](./img/DEVICE_MANAGEMENT/DeviceEdit_SaveDummyDevice.png) -> [!NOTE] -> -> You can couple this with the `ICMP` plugin which can be used to monitor the status of these devices, if they are actual devices reachable with the `ping` command. If not, you can use a loopback IP address so they appear online, such as `0.0.0.0` or `127.0.0.1`. +## Dummy or Manually Created Device Status + +You can control a dummy device’s status either via `ICMP` (automatic) or the `Force Status` field (manual). Choose based on whether the device is real and how important **data hygiene** is. + +### `ICMP` (Real Devices) + +Use a real IP that responds to ping so status is updated automatically. + +### `Force Status` (Best for Data Hygiene) + +Manually set the status when the device is not reachable or is purely logical. +This keeps your data clean and avoids fake IPs. + +### Loopback IP (`127.0.0.1`, `0.0.0.0`) + +Use when you want the device to always appear online via `ICMP`. +Note this simulates reachability and introduces artificial data. This approach might be preferred, if you want to filter and distinguish dummy devices based on IP when filtering your asset lists. + ## Copying data from an existing device. diff --git a/docs/MIGRATION.md b/docs/MIGRATION.md index 079b5a462..177577159 100755 --- a/docs/MIGRATION.md +++ b/docs/MIGRATION.md @@ -215,7 +215,7 @@ services: ### 1.3 Migration from NetAlertX `v25.10.1` -Starting from v25.10.1, the container uses a [more secure, read-only runtime environment](./SECURITY_FEATURES.md), which requires all writable paths (e.g., logs, API cache, temporary data) to be mounted as `tmpfs` or permanent writable volumes, with sufficient access [permissions](./FILE_PERMISSIONS.md). The data location has also hanged from `/app/db` and `/app/config` to `/data/db` and `/data/config`. See detailed steps below. +Starting from `v25.10.1`, the container uses a [more secure, read-only runtime environment](./SECURITY_FEATURES.md), which requires all writable paths (e.g., logs, API cache, temporary data) to be mounted as `tmpfs` or permanent writable volumes, with sufficient access [permissions](./FILE_PERMISSIONS.md). The data location has also hanged from `/app/db` and `/app/config` to `/data/db` and `/data/config`. See detailed steps below. #### STEPS: @@ -248,7 +248,7 @@ services: services: netalertx: container_name: netalertx - image: "ghcr.io/jokob-sk/netalertx" # 🆕 This has changed + image: "ghcr.io/jokob-sk/netalertx:25.11.29" # 🆕 This has changed network_mode: "host" cap_drop: # 🆕 New line - ALL # 🆕 New line diff --git a/docs/README.md b/docs/README.md index 5342b1206..a9ae299b0 100755 --- a/docs/README.md +++ b/docs/README.md @@ -63,7 +63,7 @@ There is also an in-app Help / FAQ section that should be answering frequently a #### ♻ Misc -- [Reverse proxy (Nginx, Apache, SWAG)](./REVERSE_PROXY.md) +- [Reverse Proxy](./REVERSE_PROXY.md) - [Installing Updates](./UPDATES.md) - [Setting up Authelia](./AUTHELIA.md) (DRAFT) diff --git a/docs/REMOTE_NETWORKS.md b/docs/REMOTE_NETWORKS.md index 8ff4848ca..4e0663442 100755 --- a/docs/REMOTE_NETWORKS.md +++ b/docs/REMOTE_NETWORKS.md @@ -51,7 +51,7 @@ If you don't need to discover new devices and only need to report on their statu For more information on how to add devices manually (or dummy devices), refer to the [Device Management](./DEVICE_MANAGEMENT.md) documentation. -To create truly dummy devices, you can use a loopback IP address (e.g., `0.0.0.0` or `127.0.0.1`) so they appear online. +To create truly dummy devices, you can use a loopback IP address (e.g., `0.0.0.0` or `127.0.0.1`) or the `Force Status` field so they appear online. ## NMAP and Fake MAC Addresses diff --git a/docs/REVERSE_PROXY.md b/docs/REVERSE_PROXY.md old mode 100755 new mode 100644 index d7c4d0989..0d354ede7 --- a/docs/REVERSE_PROXY.md +++ b/docs/REVERSE_PROXY.md @@ -1,526 +1,135 @@ # Reverse Proxy Configuration -> [!NOTE] -> This is community-contributed. Due to environment, setup, or networking differences, results may vary. Please open a PR to improve it instead of creating an issue, as the maintainer is not actively maintaining it. +A reverse proxy is a server that sits between users and your NetAlertX instance. It allows you to: +- Access NetAlertX via a domain name (e.g., `https://netalertx.example.com`). +- Add HTTPS/SSL encryption. +- Enforce authentication (like SSO). -> [!NOTE] -> NetAlertX requires access to both the **web UI** (default `20211`) and the **GraphQL backend `GRAPHQL_PORT`** (default `20212`) ports. -> Ensure your reverse proxy allows traffic to both for proper functionality. - -> [!IMPORTANT] -> You will need to specify 2 entries in your reverse proxy, one for the front end, one for the backend URL. The custom backend URL, including the `GRAPHQL_PORT`, needs to be aslo specified in the `BACKEND_API_URL` setting.This is the URL that points to the backend API server. -> -> ![BACKEND_API_URL setting](./img/REVERSE_PROXY/BACKEND_API_URL.png) -> -> ![NPM set up](./img/REVERSE_PROXY/nginx_proxy_manager_npm.png) - -See also: - -- [CADDY + AUTHENTIK](./REVERSE_PROXY_CADDY.md) -- [TRAEFIK](./REVERSE_PROXY_TRAEFIK.md) - - -## NGINX HTTP Configuration (Direct Path) - -> Submitted by amazing [cvc90](https://github.com/cvc90) 🙏 - -> [!NOTE] -> There are various NGINX config files for NetAlertX, some for the bare-metal install, currently Debian 12 and Ubuntu 24 (`netalertx.conf`), and one for the docker container (`netalertx.template.conf`). -> -> The first one you can find in the respective bare metal installer folder `/app/install/\/netalertx.conf`. -> The docker one can be found in the [install](https://github.com/jokob-sk/NetAlertX/tree/main/install) folder. Map, or use, the one appropriate for your setup. - -
- -1. On your NGINX server, create a new file called /etc/nginx/sites-available/netalertx - -2. In this file, paste the following code: - -``` - server { - listen 80; - server_name netalertx; - proxy_preserve_host on; - proxy_pass http://localhost:20211/; - proxy_pass_reverse http://localhost:20211/; - } -``` - -3. Activate the new website by running the following command: - - `nginx -s reload` or `systemctl restart nginx` - -4. Check your config with `nginx -t`. If there are any issues, it will tell you. - -5. Once NGINX restarts, you should be able to access the proxy website at http://netalertx/ - -
- -## NGINX HTTP Configuration (Sub Path) - -1. On your NGINX server, create a new file called /etc/nginx/sites-available/netalertx - -2. In this file, paste the following code: - -``` - server { - listen 80; - server_name netalertx; - proxy_preserve_host on; - location ^~ /netalertx/ { - proxy_pass http://localhost:20211/; - proxy_pass_reverse http://localhost:20211/; - proxy_redirect ~^/(.*)$ /netalertx/$1; - rewrite ^/netalertx/?(.*)$ /$1 break; - } - } -``` - -3. Check your config with `nginx -t`. If there are any issues, it will tell you. - -4. Activate the new website by running the following command: - - `nginx -s reload` or `systemctl restart nginx` - -5. Once NGINX restarts, you should be able to access the proxy website at http://netalertx/netalertx/ - -
- -## NGINX HTTP Configuration (Sub Path) with module ngx_http_sub_module - -1. On your NGINX server, create a new file called /etc/nginx/sites-available/netalertx - -2. In this file, paste the following code: - -``` - server { - listen 80; - server_name netalertx; - proxy_preserve_host on; - location ^~ /netalertx/ { - proxy_pass http://localhost:20211/; - proxy_pass_reverse http://localhost:20211/; - proxy_redirect ~^/(.*)$ /netalertx/$1; - rewrite ^/netalertx/?(.*)$ /$1 break; - sub_filter_once off; - sub_filter_types *; - sub_filter 'href="/' 'href="/netalertx/'; - sub_filter '(?>$host)/css' '/netalertx/css'; - sub_filter '(?>$host)/js' '/netalertx/js'; - sub_filter '/img' '/netalertx/img'; - sub_filter '/lib' '/netalertx/lib'; - sub_filter '/php' '/netalertx/php'; - } - } -``` - -3. Check your config with `nginx -t`. If there are any issues, it will tell you. - -4. Activate the new website by running the following command: - - `nginx -s reload` or `systemctl restart nginx` - -5. Once NGINX restarts, you should be able to access the proxy website at http://netalertx/netalertx/ - -
- -**NGINX HTTPS Configuration (Direct Path)** - -1. On your NGINX server, create a new file called /etc/nginx/sites-available/netalertx - -2. In this file, paste the following code: - -``` - server { - listen 443; - server_name netalertx; - SSLEngine On; - SSLCertificateFile /etc/ssl/certs/netalertx.pem; - SSLCertificateKeyFile /etc/ssl/private/netalertx.key; - proxy_preserve_host on; - proxy_pass http://localhost:20211/; - proxy_pass_reverse http://localhost:20211/; - } -``` - -3. Check your config with `nginx -t`. If there are any issues, it will tell you. - -4. Activate the new website by running the following command: - - `nginx -s reload` or `systemctl restart nginx` - -5. Once NGINX restarts, you should be able to access the proxy website at https://netalertx/ - -
- -**NGINX HTTPS Configuration (Sub Path)** - -1. On your NGINX server, create a new file called /etc/nginx/sites-available/netalertx - -2. In this file, paste the following code: - -``` - server { - listen 443; - server_name netalertx; - SSLEngine On; - SSLCertificateFile /etc/ssl/certs/netalertx.pem; - SSLCertificateKeyFile /etc/ssl/private/netalertx.key; - location ^~ /netalertx/ { - proxy_pass http://localhost:20211/; - proxy_pass_reverse http://localhost:20211/; - proxy_redirect ~^/(.*)$ /netalertx/$1; - rewrite ^/netalertx/?(.*)$ /$1 break; - } - } +```mermaid +flowchart LR + Browser --HTTPS--> Proxy[Reverse Proxy] --HTTP--> Container[NetAlertX Container] ``` -3. Check your config with `nginx -t`. If there are any issues, it will tell you. +## NetAlertX Ports -4. Activate the new website by running the following command: +NetAlertX exposes two ports that serve different purposes. Your reverse proxy can target one or both, depending on your needs. - `nginx -s reload` or `systemctl restart nginx` +| Port | Service | Purpose | +|------|---------|---------| +| **20211** | Nginx (Web UI) | The main interface. | +| **20212** | Backend API | Direct access to the API and GraphQL. Includes API docs you can view with a browser. | -5. Once NGINX restarts, you should be able to access the proxy website at https://netalertx/netalertx/ +> [!WARNING] +> **Do not document or use `/server` as an external API endpoint.** It is an internal route used by the Nginx frontend to communicate with the backend. -
+## Connection Patterns -## NGINX HTTPS Configuration (Sub Path) with module ngx_http_sub_module +### 1. Default (No Proxy) +For local testing or LAN access. The browser accesses the UI on port 20211. Code and API docs are accessible on 20212. -1. On your NGINX server, create a new file called /etc/nginx/sites-available/netalertx - -2. In this file, paste the following code: - -``` - server { - listen 443; - server_name netalertx; - SSLEngine On; - SSLCertificateFile /etc/ssl/certs/netalertx.pem; - SSLCertificateKeyFile /etc/ssl/private/netalertx.key; - location ^~ /netalertx/ { - proxy_pass http://localhost:20211/; - proxy_pass_reverse http://localhost:20211/; - proxy_redirect ~^/(.*)$ /netalertx/$1; - rewrite ^/netalertx/?(.*)$ /$1 break; - sub_filter_once off; - sub_filter_types *; - sub_filter 'href="/' 'href="/netalertx/'; - sub_filter '(?>$host)/css' '/netalertx/css'; - sub_filter '(?>$host)/js' '/netalertx/js'; - sub_filter '/img' '/netalertx/img'; - sub_filter '/lib' '/netalertx/lib'; - sub_filter '/php' '/netalertx/php'; - } - } -``` - -3. Check your config with `nginx -t`. If there are any issues, it will tell you. - -4. Activate the new website by running the following command: - - `nginx -s reload` or `systemctl restart nginx` - -5. Once NGINX restarts, you should be able to access the proxy website at https://netalertx/netalertx/ - -
- -## Apache HTTP Configuration (Direct Path) - -1. On your Apache server, create a new file called /etc/apache2/sites-available/netalertx.conf. - -2. In this file, paste the following code: - -``` - - ServerName netalertx - ProxyPreserveHost On - ProxyPass / http://localhost:20211/ - ProxyPassReverse / http://localhost:20211/ - +```mermaid +flowchart LR + B[Browser] + subgraph NAC[NetAlertX Container] + N[Nginx listening on port 20211] + A[Service on port 20212] + N -->|Proxy /server to localhost:20212| A + end + B -->|port 20211| NAC + B -->|port 20212| NAC ``` -3. Check your config with `httpd -t` (or `apache2ctl -t` on Debian/Ubuntu). If there are any issues, it will tell you. +### 2. Direct API Consumer (Not Recommended) +Connecting directly to the backend API port (20212). -4. Activate the new website by running the following command: +> [!CAUTION] +> This exposes the API directly to the network without additional protection. Avoid this on untrusted networks. - `a2ensite netalertx` or `service apache2 reload` - -5. Once Apache restarts, you should be able to access the proxy website at http://netalertx/ - -
- -## Apache HTTP Configuration (Sub Path) - -1. On your Apache server, create a new file called /etc/apache2/sites-available/netalertx.conf. - -2. In this file, paste the following code: - -``` - - ServerName netalertx - location ^~ /netalertx/ { - ProxyPreserveHost On - ProxyPass / http://localhost:20211/ - ProxyPassReverse / http://localhost:20211/ - } - +```mermaid +flowchart LR + B[Browser] -->|HTTPS| S[Any API Consumer app] + subgraph NAC[NetAlertX Container] + N[Nginx listening on port 20211] + N -->|Proxy /server to localhost:20212| A[Service on port 20212] + end + S -->|Port 20212| NAC ``` -3. Check your config with `httpd -t` (or `apache2ctl -t` on Debian/Ubuntu). If there are any issues, it will tell you. - -4. Activate the new website by running the following command: - - `a2ensite netalertx` or `service apache2 reload` - -5. Once Apache restarts, you should be able to access the proxy website at http://netalertx/ - -
- -## Apache HTTPS Configuration (Direct Path) - -1. On your Apache server, create a new file called /etc/apache2/sites-available/netalertx.conf. - -2. In this file, paste the following code: +### 3. Recommended: Reverse Proxy to Web UI +Using a reverse proxy (Nginx, Traefik, Caddy, etc.) to handle HTTPS and Auth in front of the main UI. +```mermaid +flowchart LR + B[Browser] -->|HTTPS| S[Any Auth/SSL proxy] + subgraph NAC[NetAlertX Container] + N[Nginx listening on port 20211] + N -->|Proxy /server to localhost:20212| A[Service on port 20212] + end + S -->|port 20211| NAC ``` - - ServerName netalertx - SSLEngine On - SSLCertificateFile /etc/ssl/certs/netalertx.pem - SSLCertificateKeyFile /etc/ssl/private/netalertx.key - ProxyPreserveHost On - ProxyPass / http://localhost:20211/ - ProxyPassReverse / http://localhost:20211/ - -``` - -3. Check your config with `httpd -t` (or `apache2ctl -t` on Debian/Ubuntu). If there are any issues, it will tell you. - -4. Activate the new website by running the following command: - - `a2ensite netalertx` or `service apache2 reload` - -5. Once Apache restarts, you should be able to access the proxy website at https://netalertx/ - -
- -## Apache HTTPS Configuration (Sub Path) -1. On your Apache server, create a new file called /etc/apache2/sites-available/netalertx.conf. +### 4. Recommended: Proxied API Consumer +Using a proxy to secure API access with TLS or IP limiting. -2. In this file, paste the following code: +**Why is this important?** +The backend API (`:20212`) is powerful—more so than the Web UI, which is a safer, password-protectable interface. By using a reverse proxy to **limit sources** (e.g., allowing only your Home Assistant server's IP), you ensure that only trusted devices can talk to your backend. +```mermaid +flowchart LR + B[Browser] -->|HTTPS| S[Any API Consumer app] + C[HTTPS/source-limiting Proxy] + subgraph NAC[NetAlertX Container] + N[Nginx listening on port 20211] + N -->|Proxy /server to localhost:20212| A[Service on port 20212] + end + S -->|HTTPS| C + C -->|Port 20212| NAC ``` - - ServerName netalertx - SSLEngine On - SSLCertificateFile /etc/ssl/certs/netalertx.pem - SSLCertificateKeyFile /etc/ssl/private/netalertx.key - location ^~ /netalertx/ { - ProxyPreserveHost On - ProxyPass / http://localhost:20211/ - ProxyPassReverse / http://localhost:20211/ - } - -``` - -3. Check your config with `httpd -t` (or `apache2ctl -t` on Debian/Ubuntu). If there are any issues, it will tell you. - -4. Activate the new website by running the following command: - - `a2ensite netalertx` or `service apache2 reload` - -5. Once Apache restarts, you should be able to access the proxy website at https://netalertx/netalertx/ - -
- -## Reverse proxy example by using LinuxServer's SWAG container. -> Submitted by [s33d1ing](https://github.com/s33d1ing). 🙏 +## Getting Started: Nginx Proxy Manager -## [linuxserver/swag](https://github.com/linuxserver/docker-swag) +For beginners, we recommend **[Nginx Proxy Manager](https://nginxproxymanager.com/)**. It provides a user-friendly interface to manage proxy hosts and free SSL certificates via Let's Encrypt. -In the SWAG container create `/config/nginx/proxy-confs/netalertx.subfolder.conf` with the following contents: +1. Install Nginx Proxy Manager alongside NetAlertX. +2. Create a **Proxy Host** pointing to your NetAlertX IP and Port `20211` for the Web UI. +3. (Optional) Create a second host for the API on Port `20212`. -``` nginx -## Version 2023/02/05 -# make sure that your netalertx container is named netalertx -# netalertx does not require a base url setting +![NPM Setup](./img/REVERSE_PROXY/nginx_proxy_manager_npm.png) -# Since NetAlertX uses a Host network, you may need to use the IP address of the system running NetAlertX for $upstream_app. +### Configuration Settings -location /netalertx { - return 301 $scheme://$host/netalertx/; -} +When using a reverse proxy, you should verify two settings in **Settings > Core > General**: -location ^~ /netalertx/ { - # enable the next two lines for http auth - #auth_basic "Restricted"; - #auth_basic_user_file /config/nginx/.htpasswd; +1. **BACKEND_API_URL**: This should be set to `/server`. + * *Reason:* The frontend should communicate with the backend via the internal Nginx proxy rather than routing out to the internet and back. - # enable for ldap auth (requires ldap-server.conf in the server block) - #include /config/nginx/ldap-location.conf; +2. **REPORT_DASHBOARD_URL**: Set this to your external proxy URL (e.g., `https://netalertx.example.com`). + * *Reason:* This URL is used to generate proper clickable links in emails and HTML reports. - # enable for Authelia (requires authelia-server.conf in the server block) - #include /config/nginx/authelia-location.conf; +![Configuration Settings](./img/REVERSE_PROXY/BACKEND_API_URL.png) - # enable for Authentik (requires authentik-server.conf in the server block) - #include /config/nginx/authentik-location.conf; +## Other Reverse Proxies - include /config/nginx/proxy.conf; - include /config/nginx/resolver.conf; +NetAlertX uses standard HTTP. Any reverse proxy will work. Simply forward traffic to the appropriate port (`20211` or `20212`). - set $upstream_app netalertx; - set $upstream_port 20211; - set $upstream_proto http; +For configuration details, consult the documentation for your preferred proxy: - proxy_pass $upstream_proto://$upstream_app:$upstream_port; - proxy_set_header Accept-Encoding ""; +* **[NGINX](https://nginx.org/en/docs/http/ngx_http_proxy_module.html)** +* **[Apache (mod_proxy)](https://httpd.apache.org/docs/current/mod/mod_proxy.html)** +* **[Caddy](https://caddyserver.com/docs/caddyfile/directives/reverse_proxy)** +* **[Traefik](https://doc.traefik.io/traefik/routing/services/)** - proxy_redirect ~^/(.*)$ /netalertx/$1; - rewrite ^/netalertx/?(.*)$ /$1 break; +## Authentication - sub_filter_once off; - sub_filter_types *; +If you wish to add Single Sign-On (SSO) or other authentication in front of NetAlertX, refer to the documentation for your identity provider: - sub_filter 'href="/' 'href="/netalertx/'; +* **[Authentik](https://docs.goauthentik.io/)** +* **[Authelia](https://www.authelia.com/docs/)** - sub_filter '(?>$host)/css' '/netalertx/css'; - sub_filter '(?>$host)/js' '/netalertx/js'; - - sub_filter '/img' '/netalertx/img'; - sub_filter '/lib' '/netalertx/lib'; - sub_filter '/php' '/netalertx/php'; -} -``` - -
- -## Traefik - -> Submitted by [Isegrimm](https://github.com/Isegrimm) 🙏 (based on this [discussion](https://github.com/jokob-sk/NetAlertX/discussions/449#discussioncomment-7281442)) - -Assuming the user already has a working Traefik setup, this is what's needed to make NetAlertX work at a URL like www.domain.com/netalertx/. - -Note: Everything in these configs assumes '**www.domain.com**' as your domainname and '**section31**' as an arbitrary name for your certificate setup. You will have to substitute these with your own. - -Also, I use the prefix '**netalertx**'. If you want to use another prefix, change it in these files: dynamic.toml and default. - -Content of my yaml-file (this is the generic Traefik config, which defines which ports to listen on, redirect http to https and sets up the certificate process). -It also contains Authelia, which I use for authentication. -This part contains nothing specific to NetAlertX. - -```yaml -version: '3.8' - -services: - traefik: - image: traefik - container_name: traefik - command: - - "--api=true" - - "--api.insecure=true" - - "--api.dashboard=true" - - "--entrypoints.web.address=:80" - - "--entrypoints.web.http.redirections.entryPoint.to=websecure" - - "--entrypoints.web.http.redirections.entryPoint.scheme=https" - - "--entrypoints.websecure.address=:443" - - "--providers.file.filename=/traefik-config/dynamic.toml" - - "--providers.file.watch=true" - - "--log.level=ERROR" - - "--certificatesresolvers.section31.acme.email=postmaster@domain.com" - - "--certificatesresolvers.section31.acme.storage=/traefik-config/acme.json" - - "--certificatesresolvers.section31.acme.httpchallenge=true" - - "--certificatesresolvers.section31.acme.httpchallenge.entrypoint=web" - ports: - - "80:80" - - "443:443" - - "8080:8080" - volumes: - - "/var/run/docker.sock:/var/run/docker.sock:ro" - - /appl/docker/traefik/config:/traefik-config - depends_on: - - authelia - restart: unless-stopped - authelia: - container_name: authelia - image: authelia/authelia:latest - ports: - - "9091:9091" - volumes: - - /appl/docker/authelia:/config - restart: u - nless-stopped -``` -Snippet of the dynamic.toml file (referenced in the yml-file above) that defines the config for NetAlertX: -The following are self-defined keywords, everything else is traefik keywords: -- netalertx-router -- netalertx-service -- auth -- netalertx-stripprefix - - -```toml -[http.routers] - [http.routers.netalertx-router] - entryPoints = ["websecure"] - rule = "Host(`www.domain.com`) && PathPrefix(`/netalertx`)" - service = "netalertx-service" - middlewares = "auth,netalertx-stripprefix" - [http.routers.netalertx-router.tls] - certResolver = "section31" - [[http.routers.netalertx-router.tls.domains]] - main = "www.domain.com" - -[http.services] - [http.services.netalertx-service] - [[http.services.netalertx-service.loadBalancer.servers]] - url = "http://internal-ip-address:20211/" - -[http.middlewares] - [http.middlewares.auth.forwardAuth] - address = "http://authelia:9091/api/verify?rd=https://www.domain.com/authelia/" - trustForwardHeader = true - authResponseHeaders = ["Remote-User", "Remote-Groups", "Remote-Name", "Remote-Email"] - [http.middlewares.netalertx-stripprefix.stripprefix] - prefixes = "/netalertx" - forceSlash = false - -``` -To make NetAlertX work with this setup I modified the default file at `/etc/nginx/sites-available/default` in the docker container by copying it to my local filesystem, adding the changes as specified by [cvc90](https://github.com/cvc90) and mounting the new file into the docker container, overwriting the original one. By mapping the file instead of changing the file in-place, the changes persist if an updated dockerimage is pulled. This is also a downside when the default file is updated, so I only use this as a temporary solution, until the dockerimage is updated with this change. - -Default-file: - -``` -server { - listen 80 default_server; - root /var/www/html; - index index.php; - #rewrite /netalertx/(.*) / permanent; - add_header X-Forwarded-Prefix "/netalertx" always; - proxy_set_header X-Forwarded-Prefix "/netalertx"; - - location ~* \.php$ { - fastcgi_pass unix:/run/php/php8.2-fpm.sock; - include fastcgi_params; - fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; - fastcgi_param SCRIPT_NAME $fastcgi_script_name; - fastcgi_connect_timeout 75; - fastcgi_send_timeout 600; - fastcgi_read_timeout 600; - } -} -``` - -Mapping the updated file (on the local filesystem at `/appl/docker/netalertx/default`) into the docker container: - - -```yaml -... - volumes: - - /appl/docker/netalertx/default:/etc/nginx/sites-available/default -... -``` +## Further Reading +If you want to understand more about reverse proxies and networking concepts: +* [What is a Reverse Proxy? (Cloudflare)](https://www.cloudflare.com/learning/cdn/glossary/reverse-proxy/) +* [Proxy vs Reverse Proxy (StrongDM)](https://www.strongdm.com/blog/difference-between-proxy-and-reverse-proxy) +* [Nginx Reverse Proxy Glossary](https://www.nginx.com/resources/glossary/reverse-proxy-server/) diff --git a/docs/REVERSE_PROXY_CADDY.md b/docs/REVERSE_PROXY_CADDY.md deleted file mode 100644 index 0bde799db..000000000 --- a/docs/REVERSE_PROXY_CADDY.md +++ /dev/null @@ -1,892 +0,0 @@ -## Caddy + Authentik Outpost Proxy SSO -> Submitted by [luckylinux](https://github.com/luckylinux) 🙏. - -> [!NOTE] -> This is community-contributed. Due to environment, setup, or networking differences, results may vary. Please open a PR to improve it instead of creating an issue, as the maintainer is not actively maintaining it. - -> [!NOTE] -> NetAlertX requires access to both the **web UI** (default `20211`) and the **GraphQL backend `GRAPHQL_PORT`** (default `20212`) ports. -> Ensure your reverse proxy allows traffic to both for proper functionality. - -### Introduction - -This Setup assumes: - -1. Authentik Installation running on a separate Host at `https://authentik.MYDOMAIN.TLD` -2. Container Management is done on Baremetal OR in a Virtual Machine (KVM/Xen/ESXi/..., no LXC Containers !): - i. Docker and Docker Compose configured locally running as Root (needed for `network_mode: host`) OR - ii. Podman (optionally `podman-compose`) configured locally running as Root (needed for `network_mode: host`) -3. TLS Certificates are already pre-obtained and located at `/var/lib/containers/certificates/letsencrypt/MYDOMAIN.TLD`. - I use the `certbot/dns-cloudflare` Podman Container on a separate Host to obtain the Certificates which I then distribute internally. - This Container uses the Wildcard Top-Level Domain Certificate which is valid for `MYDOMAIN.TLD` and `*.MYDOMAIN.TLD`. -4. Proxied Access - i. NetAlertX Web Interface is accessible via Caddy Reverse Proxy at `https://netalertx.MYDOMAIN.TLD` (default HTTPS Port 443: `https://netalertx.MYDOMAIN.TLD:443`) with `REPORT_DASHBOARD_URL=https://netalertx.MYDOMAIN.TLD` - ii. NetAlertX GraphQL Interface is accessible via Caddy Reverse Proxy at `https://netalertx.MYDOMAIN.TLD:20212` with `BACKEND_API_URL=https://netalertx.MYDOMAIN.TLD:20212` - iii. Authentik Proxy Outpost is accessible via Caddy Reverse Proxy at `https://netalertx.MYDOMAIN.TLD:9443` -5. Internal Ports - i. NGINX Web Server is set to listen on internal Port 20211 set via `PORT=20211` - ii. Python Web Server is set to listen on internal Port `GRAPHQL_PORT=20219` - iii. Authentik Proxy Outpost is listening on internal Port `AUTHENTIK_LISTEN__HTTP=[::1]:6000` (unencrypted) and Port `AUTHENTIK_LISTEN__HTTPS=[::1]:6443` (encrypted) - -8. Some further Configuration for Caddy is performed in Terms of Logging, SSL Certificates, etc - -It's also possible to [let Caddy automatically request & keep TLS Certificates up-to-date](https://caddyserver.com/docs/automatic-https), although please keep in mind that: - -1. You risk enumerating your LAN. Every Domain/Subdomain for which Caddy requests a TLS Certificate for you will result in that Host to be listed on [List of Letsencrypt Certificates issued](https://crt.sh/). -2. You need to either: - i. Open Port 80 for external Access ([HTTP challenge](https://caddyserver.com/docs/automatic-https#http-challenge)) in order for Letsencrypt to verify the Ownership of the Domain/Subdomain - ii. Open Port 443 for external Access ([TLS-ALPN challenge](https://caddyserver.com/docs/automatic-https#tls-alpn-challenge)) in order for Letsencrypt to verify the Ownership of the Domain/Subdomain - iii. Give Caddy the Credentials to update the DNS Records at your DNS Provider ([DNS challenge](https://caddyserver.com/docs/automatic-https#dns-challenge)) - -You can also decide to deploy your own Certificates & Certification Authority, either manually with OpenSSL, or by using something like [mkcert](https://github.com/FiloSottile/mkcert). - -In Terms of IP Stack Used: -- External: Caddy listens on both IPv4 and IPv6. -- Internal: - - Authentik Outpost Proxy listens on IPv6 `[::1]` - - NetAlertX listens on IPv4 `0.0.0.0` - -### Flow -The Traffic Flow will therefore be as follows: - -- Web GUI: - i. Client accesses `http://authentik.MYDOMAIN.TLD:80`: default (built-in Caddy) Redirect to `https://authentik.MYDOMAIN.TLD:443` - ii. Client accesses `https://authentik.MYDOMAIN.TLD:443` -> reverse Proxy to internal Port 20211 (NetAlertX Web GUI / NGINX - unencrypted) -- GraphQL: Client accesses `https://authentik.MYDOMAIN.TLD:20212` -> reverse Proxy to internal Port 20219 (NetAlertX GraphQL - unencrypted) -- Authentik Outpost: Client accesses `https://authentik.MYDOMAIN.TLD:9443` -> reverse Proxy to internal Port 6000 (Authentik Outpost Proxy - unencrypted) - -An Overview of the Flow is provided in the Picture below: - -![Reverse Proxy Traffic Flow with Authentik SSSO](./img/REVERSE_PROXY/reverse_proxy_flow.svg) - -### Security Considerations - -#### Caddy should be run rootless - -> [!WARNING] -> By default Caddy runs as `root` which is a Security Risk. -> In order to solve this, it's recommended to create an unprivileged User `caddy` and Group `caddy` on the Host: -> ``` -> groupadd --gid 980 caddy -> useradd --shell /usr/sbin/nologin --gid 980 --uid 980 -c "Caddy web server" --base-dir /var/lib/caddy -> ``` - -At least using Quadlets with Usernames (NOT required with UID/GID), but possibly using Compose in certain Cases as well, a custom `/etc/passwd` and `/etc/group` might need to be bind-mounted inside the Container. -`passwd`: -``` -root:x:0:0:root:/root:/bin/sh -bin:x:1:1:bin:/bin:/sbin/nologin -daemon:x:2:2:daemon:/sbin:/sbin/nologin -lp:x:4:7:lp:/var/spool/lpd:/sbin/nologin -sync:x:5:0:sync:/sbin:/bin/sync -shutdown:x:6:0:shutdown:/sbin:/sbin/shutdown -halt:x:7:0:halt:/sbin:/sbin/halt -mail:x:8:12:mail:/var/mail:/sbin/nologin -news:x:9:13:news:/usr/lib/news:/sbin/nologin -uucp:x:10:14:uucp:/var/spool/uucppublic:/sbin/nologin -cron:x:16:16:cron:/var/spool/cron:/sbin/nologin -ftp:x:21:21::/var/lib/ftp:/sbin/nologin -sshd:x:22:22:sshd:/dev/null:/sbin/nologin -games:x:35:35:games:/usr/games:/sbin/nologin -ntp:x:123:123:NTP:/var/empty:/sbin/nologin -guest:x:405:100:guest:/dev/null:/sbin/nologin -nobody:x:65534:65534:nobody:/:/sbin/nologin -caddy:x:980:980:caddy:/var/lib/caddy:/bin/sh -``` - -`group`: -``` -root:x:0:root -bin:x:1:root,bin,daemon -daemon:x:2:root,bin,daemon -sys:x:3:root,bin -adm:x:4:root,daemon -tty:x:5: -disk:x:6:root -lp:x:7:lp -kmem:x:9: -wheel:x:10:root -floppy:x:11:root -mail:x:12:mail -news:x:13:news -uucp:x:14:uucp -cron:x:16:cron -audio:x:18: -cdrom:x:19: -dialout:x:20:root -ftp:x:21: -sshd:x:22: -input:x:23: -tape:x:26:root -video:x:27:root -netdev:x:28: -kvm:x:34:kvm -games:x:35: -shadow:x:42: -www-data:x:82: -users:x:100:games -ntp:x:123: -abuild:x:300: -utmp:x:406: -ping:x:999: -nogroup:x:65533: -nobody:x:65534: -caddy:x:980: -``` - -#### Authentication of GraphQL Endpoint - -> [!WARNING] -> Currently the GraphQL Endpoint is NOT authenticated ! - -### Environment Files -Depending on the Preference of the User (Environment Variables defined in Compose/Quadlet or in external `.env` File[s]), it might be prefereable to place at least some Environment Variables in external `.env` and `.env.` Files. - -The following is proposed: - -- `.env`: common Settings (empty by Default) -- `.env.caddy`: Caddy Settings -- `.env.server`: NetAlertX Server/Application Settings -- `.env.outpost.proxy`: Authentik Proxy Outpost Settings - -The following Contents is assumed. - -`.env.caddy`: -``` -# Define Application Hostname -APPLICATION_HOSTNAME=netalertx.MYDOMAIN.TLD - -# Define Certificate Domain -# In this case: use Wildcard Certificate -APPLICATION_CERTIFICATE_DOMAIN=MYDOMAIN.TLD -APPLICATION_CERTIFICATE_CERT_FILE=fullchain.pem -APPLICATION_CERTIFICATE_KEY_FILE=privkey.pem - -# Define Outpost Hostname -OUTPOST_HOSTNAME=netalertx.MYDOMAIN.TLD - -# Define Outpost External Port (TLS) -OUTPOST_EXTERNAL_PORT=9443 -``` - -`.env.server`: -``` -PORT=20211 -PORT_SSL=443 -NETALERTX_NETWORK_MODE=host -LISTEN_ADDR=0.0.0.0 -GRAPHQL_PORT=20219 -NETALERTX_DEBUG=1 -BACKEND_API_URL=https://netalertx.MYDOMAIN.TLD:20212 -``` - -`.env.outpost.proxy`: -``` -AUTHENTIK_TOKEN=XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX -AUTHENTIK_LISTEN__HTTP=[::1]:6000 -AUTHENTIK_LISTEN__HTTPS=[::1]:6443 -``` - -### Compose Setup -``` -version: "3.8" -services: - netalertx-caddy: - container_name: netalertx-caddy - - network_mode: host - image: docker.io/library/caddy:latest - pull: missing - - env_file: - - .env - - .env.caddy - - environment: - CADDY_DOCKER_CADDYFILE_PATH: "/etc/caddy/Caddyfile" - - volumes: - - ./Caddyfile:/etc/caddy/Caddyfile:ro,z - - /var/lib/containers/data/netalertx/caddy:/data/caddy:rw,z - - /var/lib/containers/log/netalertx/caddy:/var/log:rw,z - - /var/lib/containers/config/netalertx/caddy:/config/caddy:rw,z - - /var/lib/containers/certificates/letsencrypt:/certificates:ro,z - - # Set User - user: "caddy:caddy" - - # Automatically restart Container - restart: unless-stopped - - netalertx-server: - container_name: netalertx-server # The name when you docker contiainer ls - - network_mode: host # Use host networking for ARP scanning and other services - - depends_on: - netalertx-caddy: - condition: service_started - restart: true - netalertx-outpost-proxy: - condition: service_started - restart: true - - # Local built Image including latest Changes - image: localhost/netalertx-dev:dev-20260109-232454 - - read_only: true # Make the container filesystem read-only - - # It is most secure to start with user 20211, but then we lose provisioning capabilities. - # user: "${NETALERTX_UID:-20211}:${NETALERTX_GID:-20211}" - cap_drop: # Drop all capabilities for enhanced security - - ALL - cap_add: # Add only the necessary capabilities - - NET_ADMIN # Required for scanning with arp-scan, nmap, nbtscan, traceroute, and zero-conf - - NET_RAW # Required for raw socket operations with arp-scan, nmap, nbtscan, traceroute and zero-conf - - NET_BIND_SERVICE # Required to bind to privileged ports with nbtscan - - CHOWN # Required for root-entrypoint to chown /data + /tmp before dropping privileges - - SETUID # Required for root-entrypoint to switch to non-root user - - SETGID # Required for root-entrypoint to switch to non-root group - volumes: - - # Override NGINX Configuration Template - - type: bind - source: /var/lib/containers/config/netalertx/server/nginx/netalertx.conf.template - target: /services/config/nginx/netalertx.conf.template - read_only: true - bind: - selinux: Z - - # Letsencrypt Certificates - - type: bind - source: /var/lib/containers/certificates/letsencrypt/MYDOMAIN.TLD - target: /certificates - read_only: true - bind: - selinux: Z - - # Data Storage for NetAlertX - - type: bind # Persistent Docker-managed Named Volume for storage - source: /var/lib/containers/data/netalertx/server - target: /data # consolidated configuration and database storage - read_only: false # writable volume - bind: - selinux: Z - - # Set the Timezone - - type: bind # Bind mount for timezone consistency - source: /etc/localtime - target: /etc/localtime - read_only: true - bind: - selinux: Z - - # tmpfs mounts for writable directories in a read-only container and improve system performance - # All writes now live under /tmp/* subdirectories which are created dynamically by entrypoint.d scripts - # mode=1700 gives rwx------ permissions; ownership is set by /root-entrypoint.sh - - type: tmpfs - target: /tmp - tmpfs-mode: 1700 - uid: 0 - gid: 0 - rw: true - noexec: true - nosuid: true - nodev: true - async: true - noatime: true - nodiratime: true - bind: - selinux: Z - - env_file: - - .env - - .env.server - - environment: - PUID: ${NETALERTX_UID:-20211} # Runtime UID after priming (Synology/no-copy-up safe) - PGID: ${NETALERTX_GID:-20211} # Runtime GID after priming (Synology/no-copy-up safe) - LISTEN_ADDR: ${LISTEN_ADDR:-0.0.0.0} # Listen for connections on all interfaces - PORT: ${PORT:-20211} # Application port - PORT_SSL: ${PORT_SSL:-443} - GRAPHQL_PORT: ${GRAPHQL_PORT:-20212} # GraphQL API port - ALWAYS_FRESH_INSTALL: ${ALWAYS_FRESH_INSTALL:-false} # Set to true to reset your config and database on each container start - NETALERTX_DEBUG: ${NETALERTX_DEBUG:-0} # 0=kill all services and restart if any dies. 1 keeps running dead services. - BACKEND_API_URL: ${BACKEND_API_URL-"https://netalertx.MYDOMAIN.TLD:20212"} - - # Resource limits to prevent resource exhaustion - mem_limit: 4096m # Maximum memory usage - mem_reservation: 2048m # Soft memory limit - cpu_shares: 512 # Relative CPU weight for CPU contention scenarios - pids_limit: 512 # Limit the number of processes/threads to prevent fork bombs - logging: - driver: "json-file" # Use JSON file logging driver - options: - max-size: "10m" # Rotate log files after they reach 10MB - max-file: "3" # Keep a maximum of 3 log files - - # Always restart the container unless explicitly stopped - restart: unless-stopped - - # To sign Out, you need to visit - # {$OUTPOST_HOSTNAME}:{$OUTPOST_EXTERNAL_PORT}/outpost.goauthentik.io/sign_out - netalertx-outpost-proxy: - container_name: netalertx-outpost-proxy - - network_mode: host - - depends_on: - netalertx-caddy: - condition: service_started - restart: true - - restart: unless-stopped - - image: ghcr.io/goauthentik/proxy:2025.10 - pull: missing - - env_file: - - .env - - .env.outpost.proxy - - environment: - AUTHENTIK_HOST: "https://authentik.MYDOMAIN.TLD" - AUTHENTIK_INSECURE: false - AUTHENTIK_LISTEN__HTTP: "[::1]:6000" - AUTHENTIK_LISTEN__HTTPS: "[::1]:6443" -``` - -### Quadlet Setup -`netalertx.pod`: -``` -[Pod] -# Name of the Pod -PodName=netalertx - -# Network Mode Host is required for ARP to work -Network=host - -# Automatically start Pod at Boot Time -[Install] -WantedBy=default.target -``` - -`netalertx-caddy.container`: -``` -[Unit] -Description=NetAlertX Caddy Container - -[Service] -Restart=always - -[Container] -ContainerName=netalertx-caddy - -Pod=netalertx.pod -StartWithPod=true - -# Generic Environment Configuration -EnvironmentFile=.env - -# Caddy Specific Environment Configuration -EnvironmentFile=.env.caddy - -Environment=CADDY_DOCKER_CADDYFILE_PATH=/etc/caddy/Caddyfile - -Image=docker.io/library/caddy:latest -Pull=missing - -# Run as rootless -# Specifying User & Group by Name requires to mount a custom passwd & group File inside the Container -# Otherwise an Error like the following will result: netalertx-caddy[593191]: Error: unable to find user caddy: no matching entries in passwd file -# User=caddy -# Group=caddy -# Volume=/var/lib/containers/config/netalertx/caddy-rootless/passwd:/etc/passwd:ro,z -# Volume=/var/lib/containers/config/netalertx/caddy-rootless/group:/etc/group:ro,z - -# Run as rootless -# Specifying User & Group by UID/GID will NOT require a custom passwd / group File to be bind-mounted inside the Container -User=980 -Group=980 - -Volume=./Caddyfile:/etc/caddy/Caddyfile:ro,z -Volume=/var/lib/containers/data/netalertx/caddy:/data/caddy:z -Volume=/var/lib/containers/log/netalertx/caddy:/var/log:z -Volume=/var/lib/containers/config/netalertx/caddy:/config/caddy:z -Volume=/var/lib/containers/certificates/letsencrypt:/certificates:ro,z -``` - -`netalertx-server.container`: -``` -[Unit] -Description=NetAlertX Server Container -Requires=netalertx-caddy.service netalertx-outpost-proxy.service -After=netalertx-caddy.service netalertx-outpost-proxy.service - -[Service] -Restart=always - -[Container] -ContainerName=netalertx-server - -Pod=netalertx.pod -StartWithPod=true - -# Local built Image including latest Changes -Image=localhost/netalertx-dev:dev-20260109-232454 -Pull=missing - -# Make the container filesystem read-only -ReadOnly=true - -# Drop all capabilities for enhanced security -DropCapability=ALL - -# It is most secure to start with user 20211, but then we lose provisioning capabilities. -# User=20211:20211 - -# Required for scanning with arp-scan, nmap, nbtscan, traceroute, and zero-conf -AddCapability=NET_ADMIN - -# Required for raw socket operations with arp-scan, nmap, nbtscan, traceroute and zero-conf -AddCapability=NET_RAW - -# Required to bind to privileged ports with nbtscan -AddCapability=NET_BIND_SERVICE - -# Required for root-entrypoint to chown /data + /tmp before dropping privileges -AddCapability=CHOWN - -# Required for root-entrypoint to switch to non-root user -AddCapability=SETUID - -# Required for root-entrypoint to switch to non-root group -AddCapability=SETGID - -# Override the Configuration Template -Volume=/var/lib/containers/config/netalertx/server/nginx/netalertx.conf.template:/services/config/nginx/netalertx.conf.template:ro,Z - -# Letsencrypt Certificates -Volume=/var/lib/containers/certificates/letsencrypt/MYDOMAIN.TLD:/certificates:ro,Z - -# Data Storage for NetAlertX -Volume=/var/lib/containers/data/netalertx/server:/data:rw,Z - -# Set the Timezone -Volume=/etc/localtime:/etc/localtime:ro,Z - -# tmpfs mounts for writable directories in a read-only container and improve system performance -# All writes now live under /tmp/* subdirectories which are created dynamically by entrypoint.d scripts -# mode=1700 gives rwx------ permissions; ownership is set by /root-entrypoint.sh -# Mount=type=tmpfs,destination=/tmp,tmpfs-mode=1700,uid=0,gid=0,rw=true,noexec=true,nosuid=true,nodev=true,async=true,noatime=true,nodiratime=true,relabel=private -Mount=type=tmpfs,destination=/tmp,tmpfs-mode=1700,rw=true,noexec=true,nosuid=true,nodev=true - -# Environment Configuration -EnvironmentFile=.env -EnvironmentFile=.env.server - -# Runtime UID after priming (Synology/no-copy-up safe) -Environment=PUID=20211 - -# Runtime GID after priming (Synology/no-copy-up safe) -Environment=PGID=20211 - -# Listen for connections on all interfaces (IPv4) -Environment=LISTEN_ADDR=0.0.0.0 - -# Application port -Environment=PORT=20211 - -# SSL Port -Environment=PORT_SSL=443 - -# GraphQL API port -Environment=GRAPHQL_PORT=20212 - -# Set to true to reset your config and database on each container start -Environment=ALWAYS_FRESH_INSTALL=false - -# 0=kill all services and restart if any dies. 1 keeps running dead services. -Environment=NETALERTX_DEBUG=0 - -# Set the GraphQL URL for external Access (via Caddy Reverse Proxy) -Environment=BACKEND_API_URL=https://netalertx-fedora.MYDOMAIN.TLD:20212 - -# Resource limits to prevent resource exhaustion -# Maximum memory usage -Memory=4g - -# Limit the number of processes/threads to prevent fork bombs -PidsLimit=512 - -# Relative CPU weight for CPU contention scenarios -PodmanArgs=--cpus=2 -PodmanArgs=--cpu-shares=512 - -# Soft memory limit -PodmanArgs=--memory-reservation=2g - -# !! The following Keys are unfortunately not [yet] supported !! - -# Relative CPU weight for CPU contention scenarios -#CpuShares=512 - -# Soft memory limit -#MemoryReservation=2g -``` - -`netalertx-outpost-proxy.container`: -``` -[Unit] -Description=NetAlertX Authentik Proxy Outpost Container -Requires=netalertx-caddy.service -After=netalertx-caddy.service - -[Service] -Restart=always - -[Container] -ContainerName=netalertx-outpost-proxy - -Pod=netalertx.pod -StartWithPod=true - -# General Configuration -EnvironmentFile=.env - -# Authentik Outpost Proxy Specific Configuration -EnvironmentFile=.env.outpost.proxy - -Environment=AUTHENTIK_HOST=https://authentik.MYDOMAIN.TLD -Environment=AUTHENTIK_INSECURE=false - -# Overrides Value from .env.outpost.rac -# Environment=AUTHENTIK_TOKEN=XXXXXXXXXXXXXXXXXXXXXXXXXXXXXX - -# Optional setting to be used when `authentik_host` for internal communication doesn't match the public URL -# Environment=AUTHENTIK_HOST_BROWSER=https://authentik.MYDOMAIN.TLD - -# Container Image -Image=ghcr.io/goauthentik/proxy:2025.10 -Pull=missing - -# Network Configuration -Network=container:supermicro-ikvm-pve031-caddy - -# Security Configuration -NoNewPrivileges=true -``` - -### Firewall Setup - -Depending on which GNU/Linux Distribution you are running, it might be required to open up some Firewall Ports in order to be able to access the Endpoints from outside the Host itself. - -This is for instance the Case for Fedora Linux, where I had to open: - -- Port 20212 for external GraphQL Access (both TCP & UDP are open, unsure if UDP is required) -- Port 9443 for external Authentik Outpost Proxy Access (both TCP & UDP are open, unsure if UDP is required) - -![Fedora Firewall Configuration](./img/REVERSE_PROXY/fedora-firewall.png) - -### Authentik Setup - -In order to enable Single Sign On (SSO) with Authentik, you will need to create a Provider, an Application and an Outpost. - -![Authentik Left Sidebar](./img/REVERSE_PROXY/authentik-sidebar.png) - -First of all, using the Left Sidebar, navigate to `Applications` → `Providers`, click on `Create` (Blue Button at the Top of the Screen), select `Proxy Provider`, then click `Next`: -![Authentik Provider Setup (Part 1)](./img/REVERSE_PROXY/authentik-provider-setup-01.png) - -Fill in the required Fields: - -- Name: choose a Name for the Provider (e.g. `netalertx`) -- Authorization Flow: choose the Authorization Flow. I typically use `default-provider-authorization-implicit-consent (Authorize Application)`. If you select the `default-provider-authorization-explicit-consent (Authorize Application)` you will need to authorize Authentik every Time you want to log in NetAlertX, which can make the Experience less User-friendly -- Type: Click on `Forward Auth (single application)` -- External Host: set to `https://netalertx.MYDOMAIN.TLD` - -Click `Finish`. - -![Authentik Provider Setup (Part 2)](./img/REVERSE_PROXY/authentik-provider-setup-02.png) - -Now, using the Left Sidebar, navigate to `Applications` → `Applications`, click on `Create` (Blue Button at the Top of the Screen) and fill in the required Fields: - -- Name: choose a Name for the Application (e.g. `netalertx`) -- Slug: choose a Slug for the Application (e.g. `netalertx`) -- Group: optionally you can assign this Application to a Group of Applications of your Choosing (for grouping Purposes within Authentik User Interface) -- Provider: select the Provider you created the the `Providers` Section previosly (e.g. `netalertx`) - -Then click `Create`. - -![Authentik Application Setup (Part 1)](./img/REVERSE_PROXY/authentik-application-setup-01.png) - -Now, using the Left Sidebar, navigate to `Applications` → `Outposts`, click on `Create` (Blue Button at the Top of the Screen) and fill in the required Fields: - -- Name: choose a Name for the Outpost (e.g. `netalertx`) -- Type: `Proxy` -- Integration: open the Dropdown and click on `---------`. Make sure it is NOT set to `Local Docker connection` ! - -In the `Available Applications` Section, select the Application you created in the Previous Step, then click the right Arrow (approx. located in the Center of the Screen), so that it gets copied in the `Selected Applications` Section. - -Then click `Create`. - -![Authentik Outpost Setup (Part 1)](./img/REVERSE_PROXY/authentik-outpost-setup-01.png) - -Wait a few Seconds for the Outpost to be created. Once it appears in the List, click on `Deployment Info` on the Right Side of the relevant Line. - -![Authentik Outpost Setup (Part 2)](./img/REVERSE_PROXY/authentik-outpost-setup-02.png) - -Take note of that Token. You will need it for the Authentik Outpost Proxy Container, which will read it as the `AUTHENTIK_TOKEN` Environment Variable. - -### NGINX Configuration inside NetAlertX Container -> [!NOTE] -> This is something that was implemented based on the previous Content of this Reverse Proxy Document. -> Due to some Buffer Warnings/Errors in the Logs as well as some other Issues I was experiencing, I increased a lot the client_body_buffer_size and large_client_header_buffers Parameters, although these might not be required anymore. -> Further Testing might be required. - -``` -# Set number of worker processes automatically based on number of CPU cores. -worker_processes auto; - -# Enables the use of JIT for regular expressions to speed-up their processing. -pcre_jit on; - -# Configures default error logger. -error_log /tmp/log/nginx-error.log warn; - -pid /tmp/run/nginx.pid; - -events { - # The maximum number of simultaneous connections that can be opened by - # a worker process. - worker_connections 1024; -} - -http { - - # Mapping of temp paths for various nginx modules. - client_body_temp_path /tmp/nginx/client_body; - proxy_temp_path /tmp/nginx/proxy; - fastcgi_temp_path /tmp/nginx/fastcgi; - uwsgi_temp_path /tmp/nginx/uwsgi; - scgi_temp_path /tmp/nginx/scgi; - - # Includes mapping of file name extensions to MIME types of responses - # and defines the default type. - include /services/config/nginx/mime.types; - default_type application/octet-stream; - - # Name servers used to resolve names of upstream servers into addresses. - # It's also needed when using tcpsocket and udpsocket in Lua modules. - #resolver 1.1.1.1 1.0.0.1 [2606:4700:4700::1111] [2606:4700:4700::1001]; - - # Don't tell nginx version to the clients. Default is 'on'. - server_tokens off; - - # Specifies the maximum accepted body size of a client request, as - # indicated by the request header Content-Length. If the stated content - # length is greater than this size, then the client receives the HTTP - # error code 413. Set to 0 to disable. Default is '1m'. - client_max_body_size 1m; - - # Sendfile copies data between one FD and other from within the kernel, - # which is more efficient than read() + write(). Default is off. - sendfile on; - - # Causes nginx to attempt to send its HTTP response head in one packet, - # instead of using partial frames. Default is 'off'. - tcp_nopush on; - - - # Enables the specified protocols. Default is TLSv1 TLSv1.1 TLSv1.2. - # TIP: If you're not obligated to support ancient clients, remove TLSv1.1. - ssl_protocols TLSv1.2 TLSv1.3; - - # Path of the file with Diffie-Hellman parameters for EDH ciphers. - # TIP: Generate with: `openssl dhparam -out /etc/ssl/nginx/dh2048.pem 2048` - #ssl_dhparam /etc/ssl/nginx/dh2048.pem; - - # Specifies that our cipher suits should be preferred over client ciphers. - # Default is 'off'. - ssl_prefer_server_ciphers on; - - # Enables a shared SSL cache with size that can hold around 8000 sessions. - # Default is 'none'. - ssl_session_cache shared:SSL:2m; - - # Specifies a time during which a client may reuse the session parameters. - # Default is '5m'. - ssl_session_timeout 1h; - - # Disable TLS session tickets (they are insecure). Default is 'on'. - ssl_session_tickets off; - - - # Enable gzipping of responses. - gzip on; - - # Set the Vary HTTP header as defined in the RFC 2616. Default is 'off'. - gzip_vary on; - - - # Specifies the main log format. - log_format main '$remote_addr - $remote_user [$time_local] "$request" ' - '$status $body_bytes_sent "$http_referer" ' - '"$http_user_agent" "$http_x_forwarded_for"'; - - # Sets the path, format, and configuration for a buffered log write. - access_log /tmp/log/nginx-access.log main; - - - # Virtual host config (unencrypted) - server { - listen ${LISTEN_ADDR}:${PORT} default_server; - root /app/front; - index index.php; - add_header X-Forwarded-Prefix "/app" always; - - server_name netalertx-server; - proxy_set_header Host $host; - proxy_set_header X-Real-IP $remote_addr; - proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - proxy_set_header X-Forwarded-Proto $scheme; - - client_body_buffer_size 512k; - large_client_header_buffers 64 128k; - - location ~* \.php$ { - # Set Cache-Control header to prevent caching on the first load - add_header Cache-Control "no-store"; - fastcgi_pass unix:/tmp/run/php.sock; - include /services/config/nginx/fastcgi_params; - fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; - fastcgi_param SCRIPT_NAME $fastcgi_script_name; - fastcgi_connect_timeout 75; - fastcgi_send_timeout 600; - fastcgi_read_timeout 600; - } - } -} -``` - -### Caddyfile -``` -# Example and Guide -# https://caddyserver.com/docs/caddyfile/options - -# General Options -{ - # (Optional) Debug Mode - # debug - - # (Optional ) Enable / Disable Admin API - admin off - - # TLS Options - # (Optional) Disable Certificates Management (only if SSL/TLS Certificates are managed by certbot or other external Tools) - auto_https disable_certs -} - -# (Optional Enable Admin API) -# localhost { -# reverse_proxy /api/* localhost:9001 -# } - -# NetAlertX Web GUI (HTTPS Port 443) -# (Optional) Only if SSL/TLS Certificates are managed by certbot or other external Tools and Custom Logging is required -{$APPLICATION_HOSTNAME}:443 { - tls /certificates/{$APPLICATION_CERTIFICATE_DOMAIN}/{$APPLICATION_CERTIFICATE_CERT_FILE:fullchain.pem} /certificates/{$APPLICATION_CERTIFICATE_DOMAIN}/{$APPLICATION_CERTIFICATE_KEY_FILE:privkey.pem} - - log { - output file /var/log/{$APPLICATION_HOSTNAME}/access_web.json { - roll_size 100MiB - roll_keep 5000 - roll_keep_for 720h - roll_uncompressed - } - - format json - } - - route { - # Always forward outpost path to actual outpost - reverse_proxy /outpost.goauthentik.io/* https://{$OUTPOST_HOSTNAME}:{$OUTPOST_EXTERNAL_PORT} { - header_up Host {http.reverse_proxy.upstream.hostport} - } - - # Forward authentication to outpost - forward_auth https://{$OUTPOST_HOSTNAME}:{$OUTPOST_EXTERNAL_PORT} { - uri /outpost.goauthentik.io/auth/caddy - - # Capitalization of the headers is important, otherwise they will be empty - copy_headers X-Authentik-Username X-Authentik-Groups X-Authentik-Email X-Authentik-Name X-Authentik-Uid X-Authentik-Jwt X-Authentik-Meta-Jwks X-Authentik-Meta-Outpost X-Authentik-Meta-Provider X-Authentik-Meta-App X-Authentik-Meta-Version - - # (Optional) - # If not set, trust all private ranges, but for Security Reasons, this should be set to the outposts IP - trusted_proxies private_ranges - } - } - - # IPv4 Reverse Proxy to NetAlertX Web GUI (internal unencrypted Host) - reverse_proxy http://0.0.0.0:20211 - - # IPv6 Reverse Proxy to NetAlertX Web GUI (internal unencrypted Host) - # reverse_proxy http://[::1]:20211 -} - -# NetAlertX GraphQL Endpoint (HTTPS Port 20212) -# (Optional) Only if SSL/TLS Certificates are managed by certbot or other external Tools and Custom Logging is required -{$APPLICATION_HOSTNAME}:20212 { - tls /certificates/{$APPLICATION_CERTIFICATE_DOMAIN}/{$APPLICATION_CERTIFICATE_CERT_FILE:fullchain.pem} /certificates/{$APPLICATION_CERTIFICATE_DOMAIN}/{$APPLICATION_CERTIFICATE_KEY_FILE:privkey.pem} - - log { - output file /var/log/{$APPLICATION_HOSTNAME}/access_graphql.json { - roll_size 100MiB - roll_keep 5000 - roll_keep_for 720h - roll_uncompressed - } - - format json - } - - # IPv4 Reverse Proxy to NetAlertX GraphQL Endpoint (internal unencrypted Host) - reverse_proxy http://0.0.0.0:20219 - - # IPv6 Reverse Proxy to NetAlertX GraphQL Endpoint (internal unencrypted Host) - # reverse_proxy http://[::1]:6000 -} - -# Authentik Outpost -# (Optional) Only if SSL/TLS Certificates are managed by certbot or other external Tools and Custom Logging is required -{$OUTPOST_HOSTNAME}:{$OUTPOST_EXTERNAL_PORT} { - tls /certificates/{$APPLICATION_CERTIFICATE_DOMAIN}/{$APPLICATION_CERTIFICATE_CERT_FILE:fullchain.pem} /certificates/{$APPLICATION_CERTIFICATE_DOMAIN}/{$APPLICATION_CERTIFICATE_KEY_FILE:privkey.pem} - - log { - output file /var/log/outpost/{$OUTPOST_HOSTNAME}/access.json { - roll_size 100MiB - roll_keep 5000 - roll_keep_for 720h - roll_uncompressed - } - - format json - } - - # IPv4 Reverse Proxy to internal unencrypted Host - # reverse_proxy http://0.0.0.0:6000 - - # IPv6 Reverse Proxy to internal unencrypted Host - reverse_proxy http://[::1]:6000 -} -``` - -### Login -Now try to login by visiting `https://netalertx.MYDOMAIN.TLD`. - -You should be greeted with a Login Screen by Authentik. - -If you are already logged in Authentik, log out first. You can do that by visiting `https://netalertx.MYDOMAIN.TLD/outpost.goauthentik.io/sign_out`, then click on `Log out of authentik` (2nd Button). Or you can just sign out from your Authentik Admin Panel at `https://authentik.MYDOMAIN.TLD`. - -If everything works as expected, then you can now set `SETPWD_enable_password=false` to disable double Authentication. - -![Authentik Login Screen](./img/REVERSE_PROXY/authentik-login.png) \ No newline at end of file diff --git a/docs/REVERSE_PROXY_TRAEFIK.md b/docs/REVERSE_PROXY_TRAEFIK.md deleted file mode 100644 index 8766cfc6d..000000000 --- a/docs/REVERSE_PROXY_TRAEFIK.md +++ /dev/null @@ -1,86 +0,0 @@ -# Guide: Routing NetAlertX API via Traefik v3 - -> [!NOTE] -> NetAlertX requires access to both the **web UI** (default `20211`) and the **GraphQL backend `GRAPHQL_PORT`** (default `20212`) ports. -> Ensure your reverse proxy allows traffic to both for proper functionality. - - -> [!NOTE] -> This is community-contributed. Due to environment, setup, or networking differences, results may vary. Please open a PR to improve it instead of creating an issue, as the maintainer is not actively maintaining it. - - -Traefik v3 requires the following setup to route traffic properly. This guide shows a working configuration using a dedicated `PathPrefix`. - ---- - -## 1. Configure NetAlertX Backend URL - -1. Open the NetAlertX UI: **Settings → Core → General**. -2. Set the `BACKEND_API_URL` to include a custom path prefix, for example: - -``` -https://netalertx.yourdomain.com/netalertx-api -``` - -This tells the frontend where to reach the backend API. - ---- - -## 2. Create a Traefik Router for the API - -Define a router specifically for the API with a higher priority and a `PathPrefix` rule: - -```yaml -netalertx-api: - rule: "Host(`netalertx.yourdomain.com`) && PathPrefix(`/netalertx-api`)" - service: netalertx-api-service - middlewares: - - netalertx-stripprefix - priority: 100 -``` - -**Notes:** - -* `Host(...)` ensures requests are only routed for your domain. -* `PathPrefix(...)` routes anything under `/netalertx-api` to the backend. -* Priority `100` ensures this router takes precedence over other routes. - ---- - -## 3. Add a Middleware to Strip the Prefix - -NetAlertX expects requests at the root (`/`). Use Traefik’s `StripPrefix` middleware: - -```yaml -middlewares: - netalertx-stripprefix: - stripPrefix: - prefixes: - - "/netalertx-api" -``` - -This removes `/netalertx-api` before forwarding the request to the backend container. - ---- - -## 4. Map the API Service to the Backend Container - -Point the service to the internal GraphQL/Backend port (20212): - -```yaml -netalertx-api-service: - loadBalancer: - servers: - - url: "http://:20212" -``` - -Replace `` with your NetAlertX container’s internal address. - ---- - -✅ With this setup: - -* `https://netalertx.yourdomain.com` → Web interface (port 20211) -* `https://netalertx.yourdomain.com/netalertx-api` → API/GraphQL backend (port 20212) - -This cleanly separates API requests from frontend requests while keeping everything under the same domain. diff --git a/docs/img/ADVISORIES/down_devices.png b/docs/img/ADVISORIES/down_devices.png new file mode 100644 index 000000000..9c474c7d1 Binary files /dev/null and b/docs/img/ADVISORIES/down_devices.png differ diff --git a/docs/img/ADVISORIES/filters.png b/docs/img/ADVISORIES/filters.png new file mode 100644 index 000000000..92a4de819 Binary files /dev/null and b/docs/img/ADVISORIES/filters.png differ diff --git a/docs/img/ADVISORIES/ui_customization_settings.png b/docs/img/ADVISORIES/ui_customization_settings.png new file mode 100644 index 000000000..83b4cddcc Binary files /dev/null and b/docs/img/ADVISORIES/ui_customization_settings.png differ diff --git a/docs/img/REVERSE_PROXY/authentik-application-setup-01.png b/docs/img/REVERSE_PROXY/authentik-application-setup-01.png deleted file mode 100644 index ae36591ad..000000000 Binary files a/docs/img/REVERSE_PROXY/authentik-application-setup-01.png and /dev/null differ diff --git a/docs/img/REVERSE_PROXY/authentik-login.png b/docs/img/REVERSE_PROXY/authentik-login.png deleted file mode 100644 index 5a034e7d3..000000000 Binary files a/docs/img/REVERSE_PROXY/authentik-login.png and /dev/null differ diff --git a/docs/img/REVERSE_PROXY/authentik-outpost-setup-01.png b/docs/img/REVERSE_PROXY/authentik-outpost-setup-01.png deleted file mode 100644 index e0e60bfd9..000000000 Binary files a/docs/img/REVERSE_PROXY/authentik-outpost-setup-01.png and /dev/null differ diff --git a/docs/img/REVERSE_PROXY/authentik-outpost-setup-02.png b/docs/img/REVERSE_PROXY/authentik-outpost-setup-02.png deleted file mode 100644 index 9e43e3c10..000000000 Binary files a/docs/img/REVERSE_PROXY/authentik-outpost-setup-02.png and /dev/null differ diff --git a/docs/img/REVERSE_PROXY/authentik-provider-setup-01.png b/docs/img/REVERSE_PROXY/authentik-provider-setup-01.png deleted file mode 100644 index 4cf2c12ea..000000000 Binary files a/docs/img/REVERSE_PROXY/authentik-provider-setup-01.png and /dev/null differ diff --git a/docs/img/REVERSE_PROXY/authentik-provider-setup-02.png b/docs/img/REVERSE_PROXY/authentik-provider-setup-02.png deleted file mode 100644 index 700f5edd7..000000000 Binary files a/docs/img/REVERSE_PROXY/authentik-provider-setup-02.png and /dev/null differ diff --git a/docs/img/REVERSE_PROXY/authentik-sidebar.png b/docs/img/REVERSE_PROXY/authentik-sidebar.png deleted file mode 100644 index 727ffe320..000000000 Binary files a/docs/img/REVERSE_PROXY/authentik-sidebar.png and /dev/null differ diff --git a/docs/img/REVERSE_PROXY/fedora-firewall.png b/docs/img/REVERSE_PROXY/fedora-firewall.png deleted file mode 100644 index 18a5a9e4f..000000000 Binary files a/docs/img/REVERSE_PROXY/fedora-firewall.png and /dev/null differ diff --git a/docs/img/REVERSE_PROXY/reverse_proxy_flow.drawio b/docs/img/REVERSE_PROXY/reverse_proxy_flow.drawio deleted file mode 100644 index d0466c3e0..000000000 --- a/docs/img/REVERSE_PROXY/reverse_proxy_flow.drawio +++ /dev/null @@ -1,202 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/docs/img/REVERSE_PROXY/reverse_proxy_flow.png b/docs/img/REVERSE_PROXY/reverse_proxy_flow.png deleted file mode 100644 index 18828e6bc..000000000 Binary files a/docs/img/REVERSE_PROXY/reverse_proxy_flow.png and /dev/null differ diff --git a/docs/img/REVERSE_PROXY/reverse_proxy_flow.svg b/docs/img/REVERSE_PROXY/reverse_proxy_flow.svg deleted file mode 100644 index 8577959ac..000000000 --- a/docs/img/REVERSE_PROXY/reverse_proxy_flow.svg +++ /dev/null @@ -1,3 +0,0 @@ - - -
NetAlertX Pod
NetAlertX Pod
Web UI
(NGINX + PHP)
Web UI...
API GraphQL
(Python)
API GraphQL...
443
443
20212
20212
Authentik SSO for Web UI
Authentik SSO for...
9443
9443
NetAlertX
NetAlertX
Authentik Outpost Proxy
Authentik Outpost Proxy
Caddy
Caddy
Web UI
(NGINX + PHP)
Web UI...
API GraphQL
(Python)
API GraphQL...
Authenticated & Authorized ?
Authenticated & Aut...
20211
20211
20219
20219
HTTPS
HTTPS
HTTPS
HTTPS
HTTPS
HTTPS
NO
NO
YES
YES
HTTP
HTTP
HTTP
HTTP
TLS Termination
TLS Termina...
TLS Termination
TLS Termina...
Check Authentication
Check Authent...
TLS Termination
TLS Termina...
\ No newline at end of file diff --git a/front/deviceDetailsEdit.php b/front/deviceDetailsEdit.php index c9acb3a52..d121e8c80 100755 --- a/front/deviceDetailsEdit.php +++ b/front/deviceDetailsEdit.php @@ -479,7 +479,12 @@ function setDeviceData(direction = '', refreshCallback = '') { if (resp && resp.success) { showMessage(getString("Device_Saved_Success")); } else { - showMessage(getString("Device_Saved_Unexpected")); + + console.log(resp); + + errorMessage = resp?.error; + + showMessage(`${getString("Device_Saved_Unexpected")}: ${errorMessage}`, 5000, "modal_red"); } // Remove navigation prompt diff --git a/front/deviceDetailsEvents.php b/front/deviceDetailsEvents.php index b11e4b523..a592d8ce4 100755 --- a/front/deviceDetailsEvents.php +++ b/front/deviceDetailsEvents.php @@ -116,7 +116,7 @@ function initializeEventsDatatable (eventsRows) { { targets: [0], 'createdCell': function (td, cellData, rowData, row, col) { - $(td).html(translateHTMLcodes(localizeTimestamp(cellData))); + $(td).html(translateHTMLcodes((cellData))); } } ], diff --git a/front/js/common.js b/front/js/common.js index ceeb82a54..c5d109dea 100755 --- a/front/js/common.js +++ b/front/js/common.js @@ -12,7 +12,11 @@ var timerRefreshData = '' var emptyArr = ['undefined', "", undefined, null, 'null']; var UI_LANG = "English (en_us)"; -const allLanguages = ["ar_ar","ca_ca","cs_cz","de_de","en_us","es_es","fa_fa","fr_fr","it_it","ja_jp","nb_no","pl_pl","pt_br","pt_pt","ru_ru","sv_sv","tr_tr","uk_ua","zh_cn"]; // needs to be same as in lang.php +const allLanguages = ["ar_ar","ca_ca","cs_cz","de_de", + "en_us","es_es","fa_fa","fr_fr", + "it_it","ja_jp","nb_no","pl_pl", + "pt_br","pt_pt","ru_ru","sv_sv", + "tr_tr","uk_ua","vi_vn","zh_cn"]; // needs to be same as in lang.php var settingsJSON = {} @@ -364,6 +368,9 @@ function getLangCode() { case 'Ukrainian (uk_uk)': lang_code = 'uk_ua'; break; + case 'Vietnamese (vi_vn)': + lang_code = 'vi_vn'; + break; } return lang_code; @@ -447,21 +454,36 @@ function localizeTimestamp(input) { return formatSafe(input, tz); function formatSafe(str, tz) { - const date = new Date(str); + + // CHECK: Does the input string have timezone information? + // - Ends with Z: "2026-02-11T11:37:02Z" + // - Has GMT±offset: "Wed Feb 11 2026 12:34:12 GMT+1100 (...)" + // - Has offset at end: "2026-02-11 11:37:02+11:00" + // - Has timezone name in parentheses: "(Australian Eastern Daylight Time)" + const hasOffset = /Z$/i.test(str.trim()) || + /GMT[+-]\d{2,4}/.test(str) || + /[+-]\d{2}:?\d{2}$/.test(str.trim()) || + /\([^)]+\)$/.test(str.trim()); + + // ⚠️ CRITICAL: All DB timestamps are stored in UTC without timezone markers. + // If no offset is present, we must explicitly mark it as UTC by appending 'Z' + // so JavaScript doesn't interpret it as local browser time. + let isoStr = str.trim(); + if (!hasOffset) { + // Ensure proper ISO format before appending Z + // Replace space with 'T' if needed: "2026-02-11 11:37:02" → "2026-02-11T11:37:02Z" + isoStr = isoStr.trim().replace(/^(\d{4}-\d{2}-\d{2}) (\d{2}:\d{2}:\d{2})$/, '$1T$2') + 'Z'; + } + + const date = new Date(isoStr); if (!isFinite(date)) { console.error(`ERROR: Couldn't parse date: '${str}' with TIMEZONE ${tz}`); return 'Failed conversion'; } - // CHECK: Does the input string have an offset (e.g., +11:00 or Z)? - // If it does, and we apply a 'tz' again, we double-shift. - const hasOffset = /[Z|[+-]\d{2}:?\d{2}]$/.test(str.trim()); - return new Intl.DateTimeFormat(LOCALE, { - // If it has an offset, we display it as-is (UTC mode in Intl - // effectively means "don't add more hours"). - // If no offset, apply your variable 'tz'. - timeZone: hasOffset ? 'UTC' : tz, + // Convert from UTC to user's configured timezone + timeZone: tz, year: 'numeric', month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit', second: '2-digit', hour12: false diff --git a/front/lib/treeviz/treeviz.iife.js b/front/lib/treeviz/treeviz.iife.js new file mode 100644 index 000000000..127724271 --- /dev/null +++ b/front/lib/treeviz/treeviz.iife.js @@ -0,0 +1,23 @@ +var Za=Object.defineProperty;var Qa=(tt,X,et)=>X in tt?Za(tt,X,{enumerable:!0,configurable:!0,writable:!0,value:et}):tt[X]=et;var ft=(tt,X,et)=>(Qa(tt,typeof X!="symbol"?X+"":X,et),et);(function(){"use strict";function tt(t){var e=0,n=t.children,r=n&&n.length;if(!r)e=1;else for(;--r>=0;)e+=n[r].value;t.value=e}function X(){return this.eachAfter(tt)}function et(t,e){let n=-1;for(const r of this)t.call(e,r,++n,this);return this}function $n(t,e){for(var n=this,r=[n],i,o,a=-1;n=r.pop();)if(t.call(e,n,++a,this),i=n.children)for(o=i.length-1;o>=0;--o)r.push(i[o]);return this}function zn(t,e){for(var n=this,r=[n],i=[],o,a,u,f=-1;n=r.pop();)if(i.push(n),o=n.children)for(a=0,u=o.length;a=0;)n+=r[i].value;e.value=n})}function En(t){return this.eachBefore(function(e){e.children&&e.children.sort(t)})}function Mn(t){for(var e=this,n=Tn(e,t),r=[e];e!==n;)e=e.parent,r.push(e);for(var i=r.length;t!==n;)r.splice(i,0,t),t=t.parent;return r}function Tn(t,e){if(t===e)return t;var n=t.ancestors(),r=e.ancestors(),i=null;for(t=n.pop(),e=r.pop();t===e;)i=t,t=n.pop(),e=r.pop();return i}function Cn(){for(var t=this,e=[t];t=t.parent;)e.push(t);return e}function Ln(){return Array.from(this)}function In(){var t=[];return this.eachBefore(function(e){e.children||t.push(e)}),t}function Hn(){var t=this,e=[];return t.each(function(n){n!==t&&e.push({source:n.parent,target:n})}),e}function*Fn(){var t=this,e,n=[t],r,i,o;do for(e=n.reverse(),n=[];t=e.pop();)if(yield t,r=t.children)for(i=0,o=r.length;i=0;--u)i.push(o=a[u]=new at(a[u])),o.parent=r,o.depth=r.depth+1;return n.eachBefore(Ne)}function Dn(){return Ut(this).eachBefore(On)}function qn(t){return t.children}function Rn(t){return Array.isArray(t)?t[1]:null}function On(t){t.data.value!==void 0&&(t.value=t.data.value),t.data=t.data.data}function Ne(t){var e=0;do t.height=e;while((t=t.parent)&&t.height<++e)}function at(t){this.data=t,this.depth=this.height=0,this.parent=null}at.prototype=Ut.prototype={constructor:at,count:X,each:et,eachAfter:zn,eachBefore:$n,find:An,sum:Sn,sort:En,path:Mn,ancestors:Cn,descendants:Ln,leaves:In,links:Hn,copy:Dn,[Symbol.iterator]:Fn};function Kt(t){return t==null?null:$e(t)}function $e(t){if(typeof t!="function")throw new Error;return t}function ht(){return 0}function dt(t){return function(){return t}}function Pn(t){t.x0=Math.round(t.x0),t.y0=Math.round(t.y0),t.x1=Math.round(t.x1),t.y1=Math.round(t.y1)}function Vn(t,e,n,r,i){for(var o=t.children,a,u=-1,f=o.length,s=t.value&&(r-e)/t.value;++uGn(n(z,E,i))),w=y.map(Ae),A=new Set(y).add("");for(const z of w)A.has(z)||(A.add(z),y.push(z),w.push(Ae(z)),o.push(Zt));a=(z,E)=>y[E],u=(z,E)=>w[E]}for(l=0,f=o.length;l=0&&(p=o[y],p.data===Zt);--y)p.data=null}if(d.parent=Wn,d.eachBefore(function(y){y.depth=y.parent.depth+1,--f}).eachBefore(Ne),d.parent=null,f>0)throw new Error("cycle");return d}return r.id=function(i){return arguments.length?(t=Kt(i),r):t},r.parentId=function(i){return arguments.length?(e=Kt(i),r):e},r.path=function(i){return arguments.length?(n=Kt(i),r):n},r}function Gn(t){t=`${t}`;let e=t.length;return Qt(t,e-1)&&!Qt(t,e-2)&&(t=t.slice(0,-1)),t[0]==="/"?t:`/${t}`}function Ae(t){let e=t.length;if(e<2)return"";for(;--e>1&&!Qt(t,e););return t.slice(0,e)}function Qt(t,e){if(t[e]==="/"){let n=0;for(;e>0&&t[--e]==="\\";)++n;if(!(n&1))return!0}return!1}function Un(t,e){return t.parent===e.parent?1:2}function Jt(t){var e=t.children;return e?e[0]:t.t}function jt(t){var e=t.children;return e?e[e.length-1]:t.t}function Kn(t,e,n){var r=n/(e.i-t.i);e.c-=r,e.s+=n,t.c+=r,e.z+=n,e.m+=n}function Zn(t){for(var e=0,n=0,r=t.children,i=r.length,o;--i>=0;)o=r[i],o.z+=e,o.m+=e,e+=o.s+(n+=o.c)}function Qn(t,e,n){return t.a.parent===e.parent?t.a:n}function zt(t,e){this._=t,this.parent=null,this.children=null,this.A=null,this.a=this,this.z=0,this.m=0,this.c=0,this.s=0,this.t=null,this.i=e}zt.prototype=Object.create(at.prototype);function Jn(t){for(var e=new zt(t,0),n,r=[e],i,o,a,u;n=r.pop();)if(o=n._.children)for(n.children=new Array(u=o.length),a=u-1;a>=0;--a)r.push(i=n.children[a]=new zt(o[a],a)),i.parent=n;return(e.parent=new zt(null,0)).children=[e],e}function jn(){var t=Un,e=1,n=1,r=null;function i(s){var l=Jn(s);if(l.eachAfter(o),l.parent.m=-l.z,l.eachBefore(a),r)s.eachBefore(f);else{var d=s,c=s,p=s;s.eachBefore(function(w){w.xc.x&&(c=w),w.depth>p.depth&&(p=w)});var m=d===c?1:t(d,c)/2,x=m-d.x,_=e/(c.x+m+x),y=n/(p.depth||1);s.eachBefore(function(w){w.x=(w.x+x)*_,w.y=w.depth*y})}return s}function o(s){var l=s.children,d=s.parent.children,c=s.i?d[s.i-1]:null;if(l){Zn(s);var p=(l[0].z+l[l.length-1].z)/2;c?(s.z=c.z+t(s._,c._),s.m=s.z-p):s.z=p}else c&&(s.z=c.z+t(s._,c._));s.parent.A=u(s,c,s.parent.A||d[0])}function a(s){s._.x=s.z+s.parent.m,s.m+=s.parent.m}function u(s,l,d){if(l){for(var c=s,p=s,m=l,x=c.parent.children[0],_=c.m,y=p.m,w=m.m,A=x.m,z;m=jt(m),c=Jt(c),m&&c;)x=Jt(x),p=jt(p),p.a=s,z=m.z+w-c.z-_+t(m._,c._),z>0&&(Kn(Qn(m,s,d),s,z),_+=z,y+=z),w+=m.m,_+=c.m,A+=x.m,y+=p.m;m&&!jt(p)&&(p.t=m,p.m+=w-y),c&&!Jt(x)&&(x.t=c,x.m+=_-A,d=s)}return d}function f(s){s.x*=e,s.y=s.depth*n}return i.separation=function(s){return arguments.length?(t=s,i):t},i.size=function(s){return arguments.length?(r=!1,e=+s[0],n=+s[1],i):r?null:[e,n]},i.nodeSize=function(s){return arguments.length?(r=!0,e=+s[0],n=+s[1],i):r?[e,n]:null},i}function tr(t,e,n,r,i){for(var o=t.children,a,u=-1,f=o.length,s=t.value&&(i-n)/t.value;++uw&&(w=s),L=_*_*E,A=Math.max(w/L,L/y),A>z){_-=s;break}z=A}a.push(f={value:_,dice:p1?r:1)},n}(er);function ir(){var t=rr,e=!1,n=1,r=1,i=[0],o=ht,a=ht,u=ht,f=ht,s=ht;function l(c){return c.x0=c.y0=0,c.x1=n,c.y1=r,c.eachBefore(d),i=[0],e&&c.eachBefore(Pn),c}function d(c){var p=i[c.depth],m=c.x0+p,x=c.y0+p,_=c.x1-p,y=c.y1-p;_=0&&(e=t.slice(0,n))!=="xmlns"&&(t=t.slice(n+1)),Se.hasOwnProperty(e)?{space:Se[e],local:t}:t}function or(t){return function(){var e=this.ownerDocument,n=this.namespaceURI;return n===te&&e.documentElement.namespaceURI===te?e.createElement(t):e.createElementNS(n,t)}}function ar(t){return function(){return this.ownerDocument.createElementNS(t.space,t.local)}}function Ee(t){var e=At(t);return(e.local?ar:or)(e)}function sr(){}function ee(t){return t==null?sr:function(){return this.querySelector(t)}}function ur(t){typeof t!="function"&&(t=ee(t));for(var e=this._groups,n=e.length,r=new Array(n),i=0;i=A&&(A=w+1);!(E=_[A])&&++A=0;)(a=r[i])&&(o&&a.compareDocumentPosition(o)^4&&o.parentNode.insertBefore(a,o),o=a);return this}function Cr(t){t||(t=Lr);function e(d,c){return d&&c?t(d.__data__,c.__data__):!d-!c}for(var n=this._groups,r=n.length,i=new Array(r),o=0;oe?1:t>=e?0:NaN}function Ir(){var t=arguments[0];return arguments[0]=this,t.apply(null,arguments),this}function Hr(){return Array.from(this)}function Fr(){for(var t=this._groups,e=0,n=t.length;e1?this.each((e==null?Gr:typeof e=="function"?Kr:Ur)(t,e,n??"")):st(this.node(),t)}function st(t,e){return t.style.getPropertyValue(e)||He(t).getComputedStyle(t,null).getPropertyValue(e)}function Qr(t){return function(){delete this[t]}}function Jr(t,e){return function(){this[t]=e}}function jr(t,e){return function(){var n=e.apply(this,arguments);n==null?delete this[t]:this[t]=n}}function ti(t,e){return arguments.length>1?this.each((e==null?Qr:typeof e=="function"?jr:Jr)(t,e)):this.node()[t]}function Fe(t){return t.trim().split(/^|\s+/)}function ne(t){return t.classList||new De(t)}function De(t){this._node=t,this._names=Fe(t.getAttribute("class")||"")}De.prototype={add:function(t){var e=this._names.indexOf(t);e<0&&(this._names.push(t),this._node.setAttribute("class",this._names.join(" ")))},remove:function(t){var e=this._names.indexOf(t);e>=0&&(this._names.splice(e,1),this._node.setAttribute("class",this._names.join(" ")))},contains:function(t){return this._names.indexOf(t)>=0}};function qe(t,e){for(var n=ne(t),r=-1,i=e.length;++r=0&&(n=e.slice(r+1),e=e.slice(0,r)),{type:e,name:n}})}function Si(t){return function(){var e=this.__on;if(e){for(var n=0,r=-1,i=e.length,o;n{}};function ie(){for(var t=0,e=arguments.length,n={},r;t=0&&(r=n.slice(i+1),n=n.slice(0,i)),n&&!e.hasOwnProperty(n))throw new Error("unknown type: "+n);return{type:n,name:r}})}Et.prototype=ie.prototype={constructor:Et,on:function(t,e){var n=this._,r=Ri(t+"",n),i,o=-1,a=r.length;if(arguments.length<2){for(;++o0)for(var n=new Array(i),r=0,i,o;r>8&15|e>>4&240,e>>4&15|e&240,(e&15)<<4|e&15,1):n===8?Tt(e>>24&255,e>>16&255,e>>8&255,(e&255)/255):n===4?Tt(e>>12&15|e>>8&240,e>>8&15|e>>4&240,e>>4&15|e&240,((e&15)<<4|e&15)/255):null):(e=Bi.exec(t))?new F(e[1],e[2],e[3],1):(e=Xi.exec(t))?new F(e[1]*255/100,e[2]*255/100,e[3]*255/100,1):(e=Yi.exec(t))?Tt(e[1],e[2],e[3],e[4]):(e=Gi.exec(t))?Tt(e[1]*255/100,e[2]*255/100,e[3]*255/100,e[4]):(e=Ui.exec(t))?Ke(e[1],e[2]/100,e[3]/100,1):(e=Ki.exec(t))?Ke(e[1],e[2]/100,e[3]/100,e[4]):We.hasOwnProperty(t)?Ye(We[t]):t==="transparent"?new F(NaN,NaN,NaN,0):null}function Ye(t){return new F(t>>16&255,t>>8&255,t&255,1)}function Tt(t,e,n,r){return r<=0&&(t=e=n=NaN),new F(t,e,n,r)}function Ji(t){return t instanceof gt||(t=xt(t)),t?(t=t.rgb(),new F(t.r,t.g,t.b,t.opacity)):new F}function ue(t,e,n,r){return arguments.length===1?Ji(t):new F(t,e,n,r??1)}function F(t,e,n,r){this.r=+t,this.g=+e,this.b=+n,this.opacity=+r}se(F,ue,Ve(gt,{brighter(t){return t=t==null?Mt:Math.pow(Mt,t),new F(this.r*t,this.g*t,this.b*t,this.opacity)},darker(t){return t=t==null?yt:Math.pow(yt,t),new F(this.r*t,this.g*t,this.b*t,this.opacity)},rgb(){return this},clamp(){return new F(rt(this.r),rt(this.g),rt(this.b),Ct(this.opacity))},displayable(){return-.5<=this.r&&this.r<255.5&&-.5<=this.g&&this.g<255.5&&-.5<=this.b&&this.b<255.5&&0<=this.opacity&&this.opacity<=1},hex:Ge,formatHex:Ge,formatHex8:ji,formatRgb:Ue,toString:Ue}));function Ge(){return`#${it(this.r)}${it(this.g)}${it(this.b)}`}function ji(){return`#${it(this.r)}${it(this.g)}${it(this.b)}${it((isNaN(this.opacity)?1:this.opacity)*255)}`}function Ue(){const t=Ct(this.opacity);return`${t===1?"rgb(":"rgba("}${rt(this.r)}, ${rt(this.g)}, ${rt(this.b)}${t===1?")":`, ${t})`}`}function Ct(t){return isNaN(t)?1:Math.max(0,Math.min(1,t))}function rt(t){return Math.max(0,Math.min(255,Math.round(t)||0))}function it(t){return t=rt(t),(t<16?"0":"")+t.toString(16)}function Ke(t,e,n,r){return r<=0?t=e=n=NaN:n<=0||n>=1?t=e=NaN:e<=0&&(t=NaN),new q(t,e,n,r)}function Ze(t){if(t instanceof q)return new q(t.h,t.s,t.l,t.opacity);if(t instanceof gt||(t=xt(t)),!t)return new q;if(t instanceof q)return t;t=t.rgb();var e=t.r/255,n=t.g/255,r=t.b/255,i=Math.min(e,n,r),o=Math.max(e,n,r),a=NaN,u=o-i,f=(o+i)/2;return u?(e===o?a=(n-r)/u+(n0&&f<1?0:a,new q(a,u,f,t.opacity)}function to(t,e,n,r){return arguments.length===1?Ze(t):new q(t,e,n,r??1)}function q(t,e,n,r){this.h=+t,this.s=+e,this.l=+n,this.opacity=+r}se(q,to,Ve(gt,{brighter(t){return t=t==null?Mt:Math.pow(Mt,t),new q(this.h,this.s,this.l*t,this.opacity)},darker(t){return t=t==null?yt:Math.pow(yt,t),new q(this.h,this.s,this.l*t,this.opacity)},rgb(){var t=this.h%360+(this.h<0)*360,e=isNaN(t)||isNaN(this.s)?0:this.s,n=this.l,r=n+(n<.5?n:1-n)*e,i=2*n-r;return new F(le(t>=240?t-240:t+120,i,r),le(t,i,r),le(t<120?t+240:t-120,i,r),this.opacity)},clamp(){return new q(Qe(this.h),Lt(this.s),Lt(this.l),Ct(this.opacity))},displayable(){return(0<=this.s&&this.s<=1||isNaN(this.s))&&0<=this.l&&this.l<=1&&0<=this.opacity&&this.opacity<=1},formatHsl(){const t=Ct(this.opacity);return`${t===1?"hsl(":"hsla("}${Qe(this.h)}, ${Lt(this.s)*100}%, ${Lt(this.l)*100}%${t===1?")":`, ${t})`}`}}));function Qe(t){return t=(t||0)%360,t<0?t+360:t}function Lt(t){return Math.max(0,Math.min(1,t||0))}function le(t,e,n){return(t<60?e+(n-e)*t/60:t<180?n:t<240?e+(n-e)*(240-t)/60:e)*255}const Je=t=>()=>t;function eo(t,e){return function(n){return t+n*e}}function no(t,e,n){return t=Math.pow(t,n),e=Math.pow(e,n)-t,n=1/n,function(r){return Math.pow(t+r*e,n)}}function ro(t){return(t=+t)==1?je:function(e,n){return n-e?no(e,n,t):Je(isNaN(e)?n:e)}}function je(t,e){var n=e-t;return n?eo(t,n):Je(isNaN(t)?e:t)}const tn=function t(e){var n=ro(e);function r(i,o){var a=n((i=ue(i)).r,(o=ue(o)).r),u=n(i.g,o.g),f=n(i.b,o.b),s=je(i.opacity,o.opacity);return function(l){return i.r=a(l),i.g=u(l),i.b=f(l),i.opacity=s(l),i+""}}return r.gamma=t,r}(1);function Q(t,e){return t=+t,e=+e,function(n){return t*(1-n)+e*n}}var ce=/[-+]?(?:\d+\.?\d*|\.?\d+)(?:[eE][-+]?\d+)?/g,fe=new RegExp(ce.source,"g");function io(t){return function(){return t}}function oo(t){return function(e){return t(e)+""}}function ao(t,e){var n=ce.lastIndex=fe.lastIndex=0,r,i,o,a=-1,u=[],f=[];for(t=t+"",e=e+"";(r=ce.exec(t))&&(i=fe.exec(e));)(o=i.index)>n&&(o=e.slice(n,o),u[a]?u[a]+=o:u[++a]=o),(r=r[0])===(i=i[0])?u[a]?u[a]+=i:u[++a]=i:(u[++a]=null,f.push({i:a,x:Q(r,i)})),n=fe.lastIndex;return n180?l+=360:l-s>180&&(s+=360),c.push({i:d.push(i(d)+"rotate(",null,r)-2,x:Q(s,l)})):l&&d.push(i(d)+"rotate("+l+r)}function u(s,l,d,c){s!==l?c.push({i:d.push(i(d)+"skewX(",null,r)-2,x:Q(s,l)}):l&&d.push(i(d)+"skewX("+l+r)}function f(s,l,d,c,p,m){if(s!==d||l!==c){var x=p.push(i(p)+"scale(",null,",",null,")");m.push({i:x-4,x:Q(s,d)},{i:x-2,x:Q(l,c)})}else(d!==1||c!==1)&&p.push(i(p)+"scale("+d+","+c+")")}return function(s,l){var d=[],c=[];return s=t(s),l=t(l),o(s.translateX,s.translateY,l.translateX,l.translateY,d,c),a(s.rotate,l.rotate,d,c),u(s.skewX,l.skewX,d,c),f(s.scaleX,s.scaleY,l.scaleX,l.scaleY,d,c),s=l=null,function(p){for(var m=-1,x=c.length,_;++m=0&&t._call.call(void 0,e),t=t._next;--lt}function ln(){ot=(Ft=bt.now())+Dt,lt=_t=0;try{mo()}finally{lt=0,_o(),ot=0}}function xo(){var t=bt.now(),e=t-Ft;e>an&&(Dt-=e,Ft=t)}function _o(){for(var t,e=Ht,n,r=1/0;e;)e._call?(r>e._time&&(r=e._time),t=e,e=e._next):(n=e._next,e._next=null,e=t?t._next=n:Ht=n);vt=t,pe(r)}function pe(t){if(!lt){_t&&(_t=clearTimeout(_t));var e=t-ot;e>24?(t<1/0&&(_t=setTimeout(ln,t-bt.now()-Dt)),wt&&(wt=clearInterval(wt))):(wt||(Ft=bt.now(),wt=setInterval(xo,an)),lt=1,sn(ln))}}function cn(t,e,n){var r=new qt;return e=e==null?0:+e,r.restart(i=>{r.stop(),t(i+e)},e,n),r}var wo=ie("start","end","cancel","interrupt"),vo=[],fn=0,hn=1,ge=2,Rt=3,dn=4,ye=5,Ot=6;function Pt(t,e,n,r,i,o){var a=t.__transition;if(!a)t.__transition={};else if(n in a)return;bo(t,n,{name:e,index:r,group:i,on:wo,tween:vo,time:o.time,delay:o.delay,duration:o.duration,ease:o.ease,timer:null,state:fn})}function me(t,e){var n=R(t,e);if(n.state>fn)throw new Error("too late; already scheduled");return n}function W(t,e){var n=R(t,e);if(n.state>Rt)throw new Error("too late; already running");return n}function R(t,e){var n=t.__transition;if(!n||!(n=n[e]))throw new Error("transition not found");return n}function bo(t,e,n){var r=t.__transition,i;r[e]=n,n.timer=un(o,0,n.time);function o(s){n.state=hn,n.timer.restart(a,n.delay,n.time),n.delay<=s&&a(s-n.delay)}function a(s){var l,d,c,p;if(n.state!==hn)return f();for(l in r)if(p=r[l],p.name===n.name){if(p.state===Rt)return cn(a);p.state===dn?(p.state=Ot,p.timer.stop(),p.on.call("interrupt",t,t.__data__,p.index,p.group),delete r[l]):+lge&&r.state=0&&(e=e.slice(0,n)),!e||e==="start"})}function Jo(t,e,n){var r,i,o=Qo(e)?me:W;return function(){var a=o(this,t),u=a.on;u!==r&&(i=(r=u).copy()).on(e,n),a.on=i}}function jo(t,e){var n=this._id;return arguments.length<2?R(this.node(),n).on.on(t):this.each(Jo(n,t,e))}function ta(t){return function(){var e=this.parentNode;for(var n in this.__transition)if(+n!==t)return;e&&e.removeChild(this)}}function ea(){return this.on("end.remove",ta(this._id))}function na(t){var e=this._name,n=this._id;typeof t!="function"&&(t=ee(t));for(var r=this._groups,i=r.length,o=new Array(i),a=0;a()=>t;function Aa(t,{sourceEvent:e,target:n,transform:r,dispatch:i}){Object.defineProperties(this,{type:{value:t,enumerable:!0,configurable:!0},sourceEvent:{value:e,enumerable:!0,configurable:!0},target:{value:n,enumerable:!0,configurable:!0},transform:{value:r,enumerable:!0,configurable:!0},_:{value:i}})}function U(t,e,n){this.k=t,this.x=e,this.y=n}U.prototype={constructor:U,scale:function(t){return t===1?this:new U(this.k*t,this.x,this.y)},translate:function(t,e){return t===0&e===0?this:new U(this.k,this.x+this.k*t,this.y+this.k*e)},apply:function(t){return[t[0]*this.k+this.x,t[1]*this.k+this.y]},applyX:function(t){return t*this.k+this.x},applyY:function(t){return t*this.k+this.y},invert:function(t){return[(t[0]-this.x)/this.k,(t[1]-this.y)/this.k]},invertX:function(t){return(t-this.x)/this.k},invertY:function(t){return(t-this.y)/this.k},rescaleX:function(t){return t.copy().domain(t.range().map(this.invertX,this).map(t.invert,t))},rescaleY:function(t){return t.copy().domain(t.range().map(this.invertY,this).map(t.invert,t))},toString:function(){return"translate("+this.x+","+this.y+") scale("+this.k+")"}};var mn=new U(1,0,0);U.prototype;function _e(t){t.stopImmediatePropagation()}function kt(t){t.preventDefault(),t.stopImmediatePropagation()}function Sa(t){return(!t.ctrlKey||t.type==="wheel")&&!t.button}function Ea(){var t=this;return t instanceof SVGElement?(t=t.ownerSVGElement||t,t.hasAttribute("viewBox")?(t=t.viewBox.baseVal,[[t.x,t.y],[t.x+t.width,t.y+t.height]]):[[0,0],[t.width.baseVal.value,t.height.baseVal.value]]):[[0,0],[t.clientWidth,t.clientHeight]]}function xn(){return this.__zoom||mn}function Ma(t){return-t.deltaY*(t.deltaMode===1?.05:t.deltaMode?1:.002)*(t.ctrlKey?10:1)}function Ta(){return navigator.maxTouchPoints||"ontouchstart"in this}function Ca(t,e,n){var r=t.invertX(e[0][0])-n[0][0],i=t.invertX(e[1][0])-n[1][0],o=t.invertY(e[0][1])-n[0][1],a=t.invertY(e[1][1])-n[1][1];return t.translate(i>r?(r+i)/2:Math.min(0,r)||Math.max(0,i),a>o?(o+a)/2:Math.min(0,o)||Math.max(0,a))}function La(){var t=Sa,e=Ea,n=Ca,r=Ma,i=Ta,o=[0,1/0],a=[[-1/0,-1/0],[1/0,1/0]],u=250,f=go,s=ie("start","zoom","end"),l,d,c,p=500,m=150,x=0,_=10;function y(h){h.property("__zoom",xn).on("wheel.zoom",Xt,{passive:!1}).on("mousedown.zoom",Yt).on("dblclick.zoom",Gt).filter(i).on("touchstart.zoom",Ga).on("touchmove.zoom",Ua).on("touchend.zoom touchcancel.zoom",Ka).style("-webkit-tap-highlight-color","rgba(0,0,0,0)")}y.transform=function(h,v,g,b){var k=h.selection?h.selection():h;k.property("__zoom",xn),h!==k?E(h,v,g,b):k.interrupt().each(function(){L(this,arguments).event(b).start().zoom(null,typeof v=="function"?v.apply(this,arguments):v).end()})},y.scaleBy=function(h,v,g,b){y.scaleTo(h,function(){var k=this.__zoom.k,N=typeof v=="function"?v.apply(this,arguments):v;return k*N},g,b)},y.scaleTo=function(h,v,g,b){y.transform(h,function(){var k=e.apply(this,arguments),N=this.__zoom,$=g==null?z(k):typeof g=="function"?g.apply(this,arguments):g,S=N.invert($),T=typeof v=="function"?v.apply(this,arguments):v;return n(A(w(N,T),$,S),k,a)},g,b)},y.translateBy=function(h,v,g,b){y.transform(h,function(){return n(this.__zoom.translate(typeof v=="function"?v.apply(this,arguments):v,typeof g=="function"?g.apply(this,arguments):g),e.apply(this,arguments),a)},null,b)},y.translateTo=function(h,v,g,b,k){y.transform(h,function(){var N=e.apply(this,arguments),$=this.__zoom,S=b==null?z(N):typeof b=="function"?b.apply(this,arguments):b;return n(mn.translate(S[0],S[1]).scale($.k).translate(typeof v=="function"?-v.apply(this,arguments):-v,typeof g=="function"?-g.apply(this,arguments):-g),N,a)},b,k)};function w(h,v){return v=Math.max(o[0],Math.min(o[1],v)),v===h.k?h:new U(v,h.x,h.y)}function A(h,v,g){var b=v[0]-g[0]*h.k,k=v[1]-g[1]*h.k;return b===h.x&&k===h.y?h:new U(h.k,b,k)}function z(h){return[(+h[0][0]+ +h[1][0])/2,(+h[0][1]+ +h[1][1])/2]}function E(h,v,g,b){h.on("start.zoom",function(){L(this,arguments).event(b).start()}).on("interrupt.zoom end.zoom",function(){L(this,arguments).event(b).end()}).tween("zoom",function(){var k=this,N=arguments,$=L(k,N).event(b),S=e.apply(k,N),T=g==null?z(S):typeof g=="function"?g.apply(k,N):g,B=Math.max(S[1][0]-S[0][0],S[1][1]-S[0][1]),I=k.__zoom,O=typeof v=="function"?v.apply(k,N):v,K=f(I.invert(T).concat(B/I.k),O.invert(T).concat(B/O.k));return function(P){if(P===1)P=O;else{var Z=K(P),ke=B/Z[2];P=new U(ke,T[0]-Z[0]*ke,T[1]-Z[1]*ke)}$.zoom(null,P)}})}function L(h,v,g){return!g&&h.__zooming||new j(h,v)}function j(h,v){this.that=h,this.args=v,this.active=0,this.sourceEvent=null,this.extent=e.apply(h,v),this.taps=0}j.prototype={event:function(h){return h&&(this.sourceEvent=h),this},start:function(){return++this.active===1&&(this.that.__zooming=this,this.emit("start")),this},zoom:function(h,v){return this.mouse&&h!=="mouse"&&(this.mouse[1]=v.invert(this.mouse[0])),this.touch0&&h!=="touch"&&(this.touch0[1]=v.invert(this.touch0[0])),this.touch1&&h!=="touch"&&(this.touch1[1]=v.invert(this.touch1[0])),this.that.__zoom=v,this.emit("zoom"),this},end:function(){return--this.active===0&&(delete this.that.__zooming,this.emit("end")),this},emit:function(h){var v=D(this.that).datum();s.call(h,this.that,new Aa(h,{sourceEvent:this.sourceEvent,target:y,transform:this.that.__zoom,dispatch:s}),v)}};function Xt(h,...v){if(!t.apply(this,arguments))return;var g=L(this,v).event(h),b=this.__zoom,k=Math.max(o[0],Math.min(o[1],b.k*Math.pow(2,r.apply(this,arguments)))),N=nt(h);if(g.wheel)(g.mouse[0][0]!==N[0]||g.mouse[0][1]!==N[1])&&(g.mouse[1]=b.invert(g.mouse[0]=N)),clearTimeout(g.wheel);else{if(b.k===k)return;g.mouse=[N,b.invert(N)],Vt(this),g.start()}kt(h),g.wheel=setTimeout($,m),g.zoom("mouse",n(A(w(b,k),g.mouse[0],g.mouse[1]),g.extent,a));function $(){g.wheel=null,g.end()}}function Yt(h,...v){if(c||!t.apply(this,arguments))return;var g=h.currentTarget,b=L(this,v,!0).event(h),k=D(h.view).on("mousemove.zoom",T,!0).on("mouseup.zoom",B,!0),N=nt(h,g),$=h.clientX,S=h.clientY;Pi(h.view),_e(h),b.mouse=[N,this.__zoom.invert(N)],Vt(this),b.start();function T(I){if(kt(I),!b.moved){var O=I.clientX-$,K=I.clientY-S;b.moved=O*O+K*K>x}b.event(I).zoom("mouse",n(A(b.that.__zoom,b.mouse[0]=nt(I,g),b.mouse[1]),b.extent,a))}function B(I){k.on("mousemove.zoom mouseup.zoom",null),Vi(I.view,b.moved),kt(I),b.event(I).end()}}function Gt(h,...v){if(t.apply(this,arguments)){var g=this.__zoom,b=nt(h.changedTouches?h.changedTouches[0]:h,this),k=g.invert(b),N=g.k*(h.shiftKey?.5:2),$=n(A(w(g,N),b,k),e.apply(this,v),a);kt(h),u>0?D(this).transition().duration(u).call(E,$,b,h):D(this).call(y.transform,$,b,h)}}function Ga(h,...v){if(t.apply(this,arguments)){var g=h.touches,b=g.length,k=L(this,v,h.changedTouches.length===b).event(h),N,$,S,T;for(_e(h),$=0;${const e=document.querySelector(`#${t}`);if(e===null)throw new Error(`Cannot find dom element with id:${t}`);const n=e.clientWidth,r=e.clientHeight;if(r===0||n===0)throw new Error("The tree can't be display because the svg height or width of the container is null");return{areaWidth:n,areaHeight:r}},Nt=(t,e,n)=>{try{const r=t.find(a=>a.id===n),i=r.ancestors()[1].id;return e.some(a=>a.id===i)?r.ancestors()[1]:Nt(t,e,i)}catch{return t.find(i=>i.id===n)}},wn=(t,e,n)=>n.isHorizontal?"translate("+e+","+t+")":"translate("+t+","+e+")";class ct{static add(e,n){this.queue.push({delayNextCallback:e+this.extraDelayBetweenCallbacks,callback:n}),this.log(this.queue.map(r=>r.delayNextCallback),"<-- New task !!!"),this.runner||(this.runnerFunction(),this.runner=setInterval(()=>this.runnerFunction(),this.runnerSpeed))}static runnerFunction(){if(this.queue[0]){if(this.queue[0].callback){this.log("Executing task, delaying next task...");try{this.queue[0].callback()}catch(e){console.error(e)}finally{this.queue[0].callback=null}}this.queue[0].delayNextCallback-=this.runnerSpeed,this.log(this.queue.map(e=>e.delayNextCallback)),this.queue[0].delayNextCallback<=0&&this.queue.shift()}else this.log("No task found"),clearInterval(this.runner),this.runner=0}static log(...e){this.showQueueLog&&console.log(...e)}}ft(ct,"queue",[]),ft(ct,"runner"),ft(ct,"runnerSpeed",100),ft(ct,"extraDelayBetweenCallbacks",100),ft(ct,"showQueueLog",!1);const Ia=t=>{const{htmlId:e,isHorizontal:n,hasPan:r,hasZoom:i,mainAxisNodeSpacing:o,nodeHeight:a,nodeWidth:u,marginBottom:f,marginLeft:s,marginRight:l,marginTop:d}=t,c={top:d,right:l,bottom:f,left:s},{areaHeight:p,areaWidth:m}=_n(t.htmlId),x=m-c.left-c.right,_=p-c.top-c.bottom,y=J.select("#"+e).append("svg").attr("width",m).attr("height",p),w=y.append("g"),A=J.zoom().on("zoom",E=>{w.attr("transform",()=>E.transform)});return y.call(A),r||y.on("mousedown.zoom",null).on("touchstart.zoom",null).on("touchmove.zoom",null).on("touchend.zoom",null),i||y.on("wheel.zoom",null).on("mousewheel.zoom",null).on("mousemove.zoom",null).on("DOMMouseScroll.zoom",null).on("dblclick.zoom",null),w.append("g").attr("transform",o==="auto"?"translate(0,0)":n?"translate("+c.left+","+(c.top+_/2-a/2)+")":"translate("+(c.left+x/2-u/2)+","+c.top+")")},we=(t,e,n)=>{const{isHorizontal:r,nodeHeight:i,nodeWidth:o,linkShape:a}=n;return a==="orthogonal"?r?`M ${t.y} ${t.x+i/2} + L ${(t.y+e.y+o)/2} ${t.x+i/2} + L ${(t.y+e.y+o)/2} ${e.x+i/2} + ${e.y+o} ${e.x+i/2}`:`M ${t.x+o/2} ${t.y} + L ${t.x+o/2} ${(t.y+e.y+i)/2} + L ${e.x+o/2} ${(t.y+e.y+i)/2} + ${e.x+o/2} ${e.y+i} `:a==="curve"?r?`M ${t.y} ${t.x+i/2} + L ${t.y-(t.y-e.y-o)/2+15} ${t.x+i/2} + Q${t.y-(t.y-e.y-o)/2} ${t.x+i/2} + ${t.y-(t.y-e.y-o)/2} ${t.x+i/2-vn(t.x,e.x,15)} + L ${t.y-(t.y-e.y-o)/2} ${e.x+i/2} + L ${e.y+o} ${e.x+i/2}`:`M ${t.x+o/2} ${t.y} + L ${t.x+o/2} ${t.y-(t.y-e.y-i)/2+15} + Q${t.x+o/2} ${t.y-(t.y-e.y-i)/2} + ${t.x+o/2-vn(t.x,e.x,15)} ${t.y-(t.y-e.y-i)/2} + L ${e.x+o/2} ${t.y-(t.y-e.y-i)/2} + L ${e.x+o/2} ${e.y+i} `:r?`M ${t.y} ${t.x+i/2} + C ${(t.y+e.y+o)/2} ${t.x+i/2} + ${(t.y+e.y+o)/2} ${e.x+i/2} + ${e.y+o} ${e.x+i/2}`:`M ${t.x+o/2} ${t.y} + C ${t.x+o/2} ${(t.y+e.y+i)/2} + ${e.x+o/2} ${(t.y+e.y+i)/2} + ${e.x+o/2} ${e.y+i} `},vn=(t,e,n)=>t>e?n:tt.enter().insert("path","g").attr("class","link").attr("d",i=>{const o=Nt(n,r,i.id),a={x:o.x0,y:o.y0};return we(a,a,e)}).attr("fill","none").attr("stroke-width",i=>e.linkWidth(i)).attr("stroke",i=>e.linkColor(i)),Fa=(t,e,n,r)=>{t.exit().transition().duration(e.duration).style("opacity",0).attr("d",i=>{const o=Nt(r,n,i.id),a={x:o.x0,y:o.y0};return we(a,a,e)}).remove()},bn=(t,e)=>{var n,r,i,o;if(t.nodeType===3){const a=(n=t.textContent)==null?void 0:n.trim();a&&e.append("tspan").text(a)}else if(t.nodeType===1)if(t.tagName==="TSPAN"||t.tagName==="tspan"){const a=e.append("tspan").text(((r=t.textContent)==null?void 0:r.trim())||"");t.getAttribute("dy")&&a.attr("dy",t.getAttribute("dy"))}else if(t.tagName==="STRONG"||t.tagName==="strong")e.append("tspan").attr("font-weight","bold").text(((i=t.textContent)==null?void 0:i.trim())||"");else if(t.tagName==="I"||t.tagName==="i")e.append("tspan").attr("font-style","italic").text(((o=t.textContent)==null?void 0:o.trim())||"");else for(let a=0;at==="quadraticBeziers"?e?0:20:0,Da=(t,e,n)=>{var i;const r=t.merge(e);if(r.transition().duration(n.duration).attr("d",o=>we(o,o.parent,n)).attr("fill","none").attr("stroke-width",o=>n.linkWidth(o)).attr("stroke",o=>n.linkColor(o)),n.linkLabel){const o=(i=r.node())==null?void 0:i.parentNode,u=D(o).selectAll("text.link-label").data(r.data(),(s,l)=>`link-label-${l}`);u.exit().remove(),u.enter().append("text").attr("class","link-label").attr("text-anchor","middle").attr("dominant-baseline","middle").attr("fill",n.linkLabel.color||"#000000").attr("font-size",n.linkLabel.fontSize||12).attr("pointer-events","none").attr("opacity",0).merge(u).attr("x",function(s){const l=kn(n.linkShape||"quadraticBeziers",n.isHorizontal);return n.isHorizontal?s.parent.y+(s.y-s.parent.y)-n.nodeWidth/4+l:s.parent.x+(s.x-s.parent.x)+n.nodeWidth/2}).attr("y",function(s){const l=kn(n.linkShape||"quadraticBeziers",n.isHorizontal);return n.isHorizontal?s.parent.x+(s.x-s.parent.x)+n.nodeHeight/2:s.parent.y+(s.y-s.parent.y)-n.nodeHeight/2+l}).text("").each(function(s){D(this).selectAll("tspan").remove();const l={...s.parent,data:s.parent.data,settings:n},d={...s,data:s.data,settings:n},c=n.linkLabel.render(l,d),p=D(this);if(c.includes("")){const x=new DOMParser().parseFromString(`${c}`,"text/xml");bn(x.documentElement,p)}else p.text(c)}).transition().delay(n.duration).duration(300).attr("opacity",1)}},qa=(t,e,n,r)=>{const i=t.enter().append("g").attr("class","node").attr("id",o=>o==null?void 0:o.id).attr("transform",o=>{const a=Nt(n,r,o.id);return wn(a.x0,a.y0,e)});return i.append("foreignObject").attr("width",e.nodeWidth).attr("height",e.nodeHeight),i},Ra=(t,e,n,r)=>{const i=t.exit().transition().duration(e.duration).style("opacity",0).attr("transform",o=>{const a=Nt(r,n,o.id);return wn(a.x0,a.y0,e)}).remove();i.select("rect").style("fill-opacity",1e-6),i.select("circle").attr("r",1e-6),i.select("text").style("fill-opacity",1e-6)},Oa=(t,e,n)=>{const r=t.merge(e);r.transition().duration(n.duration).attr("transform",i=>n.isHorizontal?"translate("+i.y+","+i.x+")":"translate("+i.x+","+i.y+")"),r.select("foreignObject").attr("width",n.nodeWidth).attr("height",n.nodeHeight).style("overflow","visible").on("click",(i,o)=>n.onNodeClick({...o,settings:n})).on("mouseenter",(i,o)=>n.onNodeMouseEnter({...o,settings:n})).on("mouseleave",(i,o)=>n.onNodeMouseLeave({...o,settings:n})).html(i=>n.renderNode({...i,settings:n}))},Pa=(t,e)=>{const{idKey:n,relationnalField:r,hasFlatData:i}=e;return i?J.stratify().id(o=>o[n]).parentId(o=>o[r])(t):J.hierarchy(t,o=>o[r])},Va=t=>{const{areaHeight:e,areaWidth:n}=_n(t.htmlId);return t.mainAxisNodeSpacing==="auto"&&t.isHorizontal?J.tree().size([e-t.nodeHeight,n-t.nodeWidth]):t.mainAxisNodeSpacing==="auto"&&!t.isHorizontal?J.tree().size([n-t.nodeWidth,e-t.nodeHeight]):t.isHorizontal===!0?J.tree().nodeSize([t.nodeHeight*t.secondaryAxisNodeSpacing,t.nodeWidth]):J.tree().nodeSize([t.nodeWidth*t.secondaryAxisNodeSpacing,t.nodeHeight])},ve={create:Wa};typeof window<"u"&&(window.Treeviz=ve);function Wa(t){let n={...{data:[],htmlId:"",idKey:"id",relationnalField:"father",hasFlatData:!0,nodeWidth:160,nodeHeight:100,mainAxisNodeSpacing:300,renderNode:()=>"Node",linkColor:()=>"#ffcc80",linkWidth:()=>10,linkShape:"quadraticBeziers",isHorizontal:!0,hasPan:!1,hasZoom:!1,duration:600,onNodeClick:()=>{},onNodeMouseEnter:()=>{},onNodeMouseLeave:()=>{},marginBottom:0,marginLeft:0,marginRight:0,marginTop:0,secondaryAxisNodeSpacing:1.25},...t},r=[];function i(s,l){const d=l.descendants(),c=l.descendants().slice(1),{mainAxisNodeSpacing:p}=n;p!=="auto"&&d.forEach(w=>{w.y=w.depth*n.nodeWidth*p}),d.forEach(w=>{const A=r.find(z=>z.id===w.id);w.x0=A?A.x0:w.x,w.y0=A?A.y0:w.y});const m=s.selectAll("g.node").data(d,w=>w[n.idKey]),x=qa(m,n,d,r);Oa(x,m,n),Ra(m,n,d,r);const _=s.selectAll("path.link").data(c,w=>w.id),y=Ha(_,n,d,r);Da(y,_,n),Fa(_,n,d,r),r=[...d]}function o(s,l){ct.add(n.duration,()=>{l&&(n={...n,...l});const d=Pa(s,n),p=Va(n)(d);i(f,p)})}function a(s){const l=s?document.querySelector(`#${n.htmlId} svg g`):document.querySelector(`#${n.htmlId}`);if(l)for(;l.firstChild;)l.removeChild(l.firstChild);r=[]}const u={refresh:o,clean:a},f=Ia(n);return u}var $t=[{id:1,text_1:"Chaos",text_2:"Void",father:null,color:"#FF5722"},{id:2,text_1:"Tartarus",text_2:"Abyss",father:1,color:"#FFC107"},{id:3,text_1:"Gaia",text_2:"Earth",father:1,color:"#8BC34A"},{id:4,text_1:"Eros",text_2:"Desire",father:1,color:"#00BCD4"}],Ba=[{id:1,text_1:"Chaos",text_2:" Void",father:null,color:"#2196F3"},{id:2,text_1:"Tartarus",text_2:"Abyss",father:1,color:"#F44336"},{id:3,text_1:"Gaia",text_2:"Earth",father:1,color:"#673AB7"},{id:4,text_1:"Eros",text_2:"Desire",father:1,color:"#009688"},{id:5,text_1:"Uranus",text_2:"Sky",father:3,color:"#4CAF50"},{id:6,text_1:"Ourea",text_2:"Mountains",father:3,color:"#FF9800"}],Xa=[{id:1,text_1:"Chaos",text_2:"Void",father:null,color:"#2196F3"},{id:2,text_1:"Tartarus",text_2:"Abyss",father:1,color:"#F44336"},{id:3,text_1:"Gaia",text_2:"Earth",father:1,color:"#673AB7"},{id:4,text_1:"Eros",text_2:"Desire",father:1,color:"#009688"},{id:5,text_1:"Uranus",text_2:"Sky",father:3,color:"#4CAF50"},{id:6,text_1:"Ourea",text_2:"Mountains",father:3,color:"#FF9800"},{id:7,text_1:"Hermes",text_2:" Sky",father:4,color:"#2196F3"},{id:8,text_1:"Aphrodite",text_2:"Love",father:4,color:"#8BC34A"},{id:3.3,text_1:"Love",text_2:"Peace",father:8,color:"#c72e99"},{id:4.1,text_1:"Hope",text_2:"Life",father:8,color:"#2eecc7"}],Bt=ve.create({data:$t,htmlId:"tree",idKey:"id",hasFlatData:!0,relationnalField:"father",nodeWidth:120,hasPan:!0,hasZoom:!0,nodeHeight:80,mainAxisNodeSpacing:2,isHorizontal:!1,renderNode:function(e){return"
"+e.data.text_1+"
is
"+e.data.text_2+"
"},linkWidth:t=>t.data.id*2,linkColor:()=>"#B0BEC5",linkLabel:{render:(t,e)=>"is child",color:"#455A64",fontSize:11},onNodeClick:t=>{console.log(t.data)},onNodeMouseEnter:t=>{console.log(t.data)}});Bt.refresh($t);var Nn=!0;const C=document.querySelector("#add"),M=document.querySelector("#remove"),be=document.querySelector("#doTasks");C==null||C.addEventListener("click",function(){console.log("addButton clicked"),Nn?Bt.refresh(Ba):Bt.refresh(Xa),Nn=!1}),M==null||M.addEventListener("click",function(){console.log("removeButton clicked"),Bt.refresh($t)}),be==null||be.addEventListener("click",function(){C==null||C.click(),M==null||M.click(),C==null||C.click(),M==null||M.click(),M==null||M.click(),C==null||C.click(),M==null||M.click(),C==null||C.click(),C==null||C.click(),M==null||M.click(),M==null||M.click()});var Ya=ve.create({data:$t,htmlId:"tree-horizontal",idKey:"id",hasFlatData:!0,relationnalField:"father",nodeWidth:120,hasPan:!0,hasZoom:!0,nodeHeight:80,mainAxisNodeSpacing:2,isHorizontal:!0,renderNode:function(e){return"
"+e.data.text_1+"
is
"+e.data.text_2+"
"},linkWidth:t=>t.data.id*2,linkShape:"curve",linkColor:()=>"#B0BEC5",linkLabel:{render:(t,e)=>"is child",color:"#455A64",fontSize:11},onNodeClick:t=>{console.log(t.data)}});Ya.refresh($t)})(); diff --git a/front/lib/treeviz/bundle.js b/front/lib/treeviz/treeviz.iife.old.js old mode 100755 new mode 100644 similarity index 100% rename from front/lib/treeviz/bundle.js rename to front/lib/treeviz/treeviz.iife.old.js diff --git a/front/network.php b/front/network.php index dbf328e98..671628fcf 100755 --- a/front/network.php +++ b/front/network.php @@ -69,7 +69,8 @@ require 'php/templates/footer.php'; ?> - + +