From 5ffb6f26e542801474d35304bc702680beb45aba Mon Sep 17 00:00:00 2001 From: Adam Outler Date: Fri, 19 Sep 2025 16:41:28 -0400 Subject: [PATCH 1/6] feat: setup devcontainer --- .coverage | Bin 0 -> 143360 bytes .devcontainer/Dockerfile | 112 ++++++++++ .devcontainer/README.md | 30 +++ .devcontainer/devcontainer.json | 75 +++++++ .devcontainer/resources/99-xdebug.ini | 8 + .../resources/devcontainer-Dockerfile | 51 +++++ .../resources/netalertx-devcontainer.conf | 26 +++ .devcontainer/scripts/generate-dockerfile.sh | 38 ++++ .devcontainer/scripts/restart-backend.sh | 24 +++ .devcontainer/scripts/run-tests.sh | 13 ++ .devcontainer/scripts/setup.sh | 191 ++++++++++++++++++ .devcontainer/scripts/stream-logs.sh | 40 ++++ .devcontainer/xdebug-trigger.ini | 11 + .github/copilot-instructions.md | 48 +++++ .vscode/launch.json | 34 ++++ .vscode/settings.json | 13 ++ .vscode/tasks.json | 94 +++++++++ front/.gitignore | 1 + pyproject.toml | 5 + 19 files changed, 814 insertions(+) create mode 100644 .coverage create mode 100755 .devcontainer/Dockerfile create mode 100644 .devcontainer/README.md create mode 100755 .devcontainer/devcontainer.json create mode 100644 .devcontainer/resources/99-xdebug.ini create mode 100644 .devcontainer/resources/devcontainer-Dockerfile create mode 100644 .devcontainer/resources/netalertx-devcontainer.conf create mode 100755 .devcontainer/scripts/generate-dockerfile.sh create mode 100755 .devcontainer/scripts/restart-backend.sh create mode 100755 .devcontainer/scripts/run-tests.sh create mode 100755 .devcontainer/scripts/setup.sh create mode 100755 .devcontainer/scripts/stream-logs.sh create mode 100644 .devcontainer/xdebug-trigger.ini create mode 100644 .github/copilot-instructions.md create mode 100644 .vscode/launch.json create mode 100644 .vscode/settings.json create mode 100644 .vscode/tasks.json create mode 100644 front/.gitignore create mode 100644 pyproject.toml diff --git a/.coverage b/.coverage new file mode 100644 index 0000000000000000000000000000000000000000..96d3d1ac12ef357f2ffe04268e964ce55e58c276 GIT binary patch literal 143360 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 !x6@%#8qn8QDdZ{x@E3atMBwHf@+vD@D(*aP5x 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 Q=v2kk8qHE1R_SAZ234u<9{>OV literal 0 HcmV?d00001 diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile new file mode 100755 index 00000000..d735514c --- /dev/null +++ b/.devcontainer/Dockerfile @@ -0,0 +1,112 @@ +# DO NOT MODIFY THIS FILE DIRECTLY. IT IS AUTO-GENERATED BY .devcontainer/scripts/generate-dockerfile.sh + +# ---/Dockerfile--- +FROM alpine:3.22 AS builder + +ARG INSTALL_DIR=/app + +ENV PYTHONUNBUFFERED=1 + +# Install build dependencies +RUN apk add --no-cache bash shadow python3 python3-dev gcc musl-dev libffi-dev openssl-dev git \ + && python -m venv /opt/venv + +# Enable venv +ENV PATH="/opt/venv/bin:$PATH" + + +RUN pip install openwrt-luci-rpc asusrouter asyncio aiohttp graphene flask flask-cors unifi-sm-api tplink-omada-client wakeonlan pycryptodome requests paho-mqtt scapy cron-converter pytz json2table dhcp-leases pyunifi speedtest-cli chardet python-nmap dnspython librouteros yattag git+https://github.com/foreign-sub/aiofreepybox.git + +# Append Iliadbox certificate to aiofreepybox + +# second stage +FROM alpine:3.22 AS runner + +ARG INSTALL_DIR=/app + +COPY --from=builder /opt/venv /opt/venv +COPY --from=builder /usr/sbin/usermod /usr/sbin/groupmod /usr/sbin/ + +# Enable venv +ENV PATH="/opt/venv/bin:$PATH" + +# default port and listen address +ENV PORT=20211 LISTEN_ADDR=0.0.0.0 + +# needed for s6-overlay +ENV S6_CMD_WAIT_FOR_SERVICES_MAXTIME=0 + +# ❗ IMPORTANT - if you modify this file modify the /install/install_dependecies.sh file as well ❗ + +RUN apk update --no-cache \ + && apk add --no-cache bash libbsd zip lsblk gettext-envsubst sudo mtr tzdata s6-overlay \ + && apk add --no-cache curl arp-scan iproute2 iproute2-ss nmap nmap-scripts traceroute nbtscan avahi avahi-tools openrc dbus net-tools net-snmp-tools bind-tools awake ca-certificates \ + && apk add --no-cache sqlite php83 php83-fpm php83-cgi php83-curl php83-sqlite3 php83-session \ + && apk add --no-cache python3 nginx \ + && ln -s /usr/bin/awake /usr/bin/wakeonlan \ + && rm -f /etc/nginx/http.d/default.conf + + +# Add crontab file +COPY --chmod=600 --chown=root:root install/crontab /etc/crontabs/root + +# Start all required services + +HEALTHCHECK --interval=30s --timeout=5s --start-period=15s --retries=2 \ + CMD curl -sf -o /dev/null ${LISTEN_ADDR}:${PORT}/php/server/query_json.php?file=app_state.json + +ENTRYPOINT ["/init"] + +# ---/resources/devcontainer-Dockerfile--- + +# Devcontainer build stage (do not build directly) +# This file is combined with the root /Dockerfile by +# .devcontainer/scripts/generate-dockerfile.sh +# The generator appends this stage to produce .devcontainer/Dockerfile. +# Prefer to place dev-only setup here; use setup.sh only for runtime fixes. + +FROM runner AS devcontainer +ENV INSTALL_DIR=/app +ENV PYTHONPATH=/workspaces/NetAlertX/test:/workspaces/NetAlertX/server:/app:/app/server:/opt/venv/lib/python3.12/site-packages + +# Install common tools, create user, and set up sudo +RUN apk add --no-cache git nano vim jq php83-pecl-xdebug py3-pip nodejs sudo gpgconf pytest pytest-cov && \ + adduser -D -s /bin/sh netalertx && \ + addgroup netalertx nginx && \ + addgroup netalertx www-data && \ + echo "netalertx ALL=(ALL) NOPASSWD:ALL" > /etc/sudoers.d/90-netalertx && \ + chmod 440 /etc/sudoers.d/90-netalertx +# Install debugpy in the virtualenv if present, otherwise into system python3 +RUN /bin/sh -c '(/opt/venv/bin/python3 -m pip install --no-cache-dir debugpy) || (python3 -m pip install --no-cache-dir debugpy) || true' +# setup nginx +COPY .devcontainer/resources/netalertx-devcontainer.conf /etc/nginx/http.d/netalert-frontend.conf +RUN set -e; \ + chown netalertx:nginx /etc/nginx/http.d/netalert-frontend.conf; \ + install -d -o netalertx -g www-data -m 775 /app; \ + install -d -o netalertx -g www-data -m 755 /run/nginx; \ + install -d -o netalertx -g www-data -m 755 /var/lib/nginx/logs; \ + rm -f /var/lib/nginx/logs/* || true; \ + for f in error access; do : > /var/lib/nginx/logs/$f.log; done; \ + install -d -o netalertx -g www-data -m 777 /run/php; \ + install -d -o netalertx -g www-data -m 775 /var/log/php; \ + chown -R netalertx:www-data /etc/nginx/http.d; \ + chmod -R 775 /etc/nginx/http.d; \ + chown -R netalertx:www-data /var/lib/nginx; \ + chmod -R 755 /var/lib/nginx && \ + chown -R netalertx:www-data /var/log/nginx/ && \ + sed -i '/^user /d' /etc/nginx/nginx.conf; \ + sed -i 's|^error_log .*|error_log /dev/stderr warn;|' /etc/nginx/nginx.conf; \ + sed -i 's|^access_log .*|access_log /dev/stdout main;|' /etc/nginx/nginx.conf; \ + sed -i 's|error_log .*|error_log /dev/stderr warn;|g' /etc/nginx/http.d/*.conf 2>/dev/null || true; \ + sed -i 's|access_log .*|access_log /dev/stdout main;|g' /etc/nginx/http.d/*.conf 2>/dev/null || true; \ + mkdir -p /run/openrc; \ + chown netalertx:nginx /run/openrc/; \ + rm -Rf /run/openrc/*; + +# setup pytest +RUN sudo /opt/venv/bin/python -m pip install -U pytest pytest-cov + +WORKDIR /workspaces/NetAlertX + + +ENTRYPOINT ["/bin/sh","-c","sleep infinity"] \ No newline at end of file diff --git a/.devcontainer/README.md b/.devcontainer/README.md new file mode 100644 index 00000000..9fa909e7 --- /dev/null +++ b/.devcontainer/README.md @@ -0,0 +1,30 @@ +# NetAlertX Devcontainer Notes + +This devcontainer replicates the production container as closely as practical, with a few development-oriented differences. + +Key behavior +- No init process: Services are managed by shell scripts using killall, setsid, and nohup. Startup and restarts are script-driven rather than supervised by an init system. +- Autogenerated Dockerfile: The effective devcontainer Dockerfile is generated on demand by `.devcontainer/scripts/generate-dockerfile.sh`. It combines the root `Dockerfile` (with certain COPY instructions removed) and an extra "devcontainer" stage from `.devcontainer/resources/devcontainer-Dockerfile`. When you change the resource Dockerfile, re-run the generator to refresh `.devcontainer/Dockerfile`. +- Where to put setup: Prefer baking setup into `.devcontainer/resources/devcontainer-Dockerfile`. Use `.devcontainer/scripts/setup.sh` only for steps that must happen at container start (e.g., cleaning up nginx/php ownership, creating directories, touching runtime files) or depend on runtime paths. + +Debugging (F5) +The Frontend and backend run in debug mode always. You can attach your debugger at any time. +- Python Backend Debug: Attach - The backend runs with a debugger on port 5678. Set breakpoints in the code and press F5 to begin triggering them. +- PHP Frontend (XDebug) Xdebug listens on 9003. Start listening and use an Xdebug extension in your browser to debug PHP. + +Common workflows (F1->Tasks: Run Task) +- Regenerate the devcontainer Dockerfile: Run the VS Code task "Generate Dockerfile" or execute `.devcontainer/scripts/generate-dockerfile.sh`. The result is `.devcontainer/Dockerfile`. +- Re-run startup provisioning: Use the task "Re-Run Startup Script" to execute `.devcontainer/scripts/setup.sh` in the container. +- Start services: + - Backend (GraphQL/Flask): `.devcontainer/scripts/restart-backend.sh` starts it under debugpy and logs to `/app/log/app.log` + - Frontend (nginx + PHP-FPM): Started via setup.sh; can be restarted by the task "Start Frontend (nginx and PHP-FPM)". + +Testing +- pytest is installed via Alpine packages (py3-pytest, py3-pytest-cov). +- PYTHONPATH includes workspace and venv site-packages so tests can import `server/*` modules and third-party libs. +- Run tests via VS Code Pytest Runner or `pytest -q` from the workspace root. + +Conventions +- Don’t edit `.devcontainer/Dockerfile` directly; edit `.devcontainer/resources/devcontainer-Dockerfile` and regenerate. +- Keep setup in the resource Dockerfile when possible; reserve `setup.sh` for runtime fixes. +- Avoid hardcoding ports/secrets; prefer existing settings and helpers (see `.github/copilot-instructions.md`). \ No newline at end of file diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json new file mode 100755 index 00000000..f9f6440e --- /dev/null +++ b/.devcontainer/devcontainer.json @@ -0,0 +1,75 @@ +{ + "name": "NetAlertX DevContainer", + "remoteUser": "netalertx", + "build": { + "dockerfile": "Dockerfile", + "context": "..", + "target": "devcontainer" + }, + "workspaceFolder": "/workspaces/NetAlertX", + + "runArgs": [ + "--privileged", + "--cap-add=NET_ADMIN", + "--cap-add=NET_RAW", + // Ensure containers can resolve host.docker.internal to the host (required on Linux) + "--add-host=host.docker.internal:host-gateway" + ], + + "postStartCommand": "${containerWorkspaceFolder}/.devcontainer/scripts/setup.sh", + + "customizations": { + "vscode": { + "extensions": [ + "ms-python.python", + "ms-azuretools.vscode-docker", + "felixfbecker.php-debug", + "bmewburn.vscode-intelephense-client", + "xdebug.php-debug", + "ms-python.vscode-pylance", + "pamaron.pytest-runner", + "coderabbit.coderabbit-vscode", + "ms-python.black-formatter" + ] + , + "settings": { + "terminal.integrated.cwd": "${containerWorkspaceFolder}", + // Python testing configuration + "python.testing.pytestEnabled": true, + "python.testing.unittestEnabled": false, + "python.testing.pytestArgs": [ + "test" + ], + // Make sure we discover tests and import server correctly + "python.analysis.extraPaths": [ + "/workspaces/NetAlertX", + "/workspaces/NetAlertX/server", + "/app", + "/app/server" + ] + } + } + }, + "forwardPorts": [5678, 9000, 9003, 20211, 20212], + + "portsAttributes": { + "20211": { + "label": "Frontend:Nginx+PHP" + }, + "20212": { + "label": "Backend:GraphQL" + }, + "9003": { + "label": "PHP Debug:Xdebug" + }, + "9000": { + "label": "PHP-FPM:FastCGI" + }, + "5678": { + "label": "Python Debug:debugpy" + } + }, + + // Optional: ensures compose services are stopped when you close the window + "shutdownAction": "stopContainer" +} \ No newline at end of file diff --git a/.devcontainer/resources/99-xdebug.ini b/.devcontainer/resources/99-xdebug.ini new file mode 100644 index 00000000..37452d58 --- /dev/null +++ b/.devcontainer/resources/99-xdebug.ini @@ -0,0 +1,8 @@ +zend_extension="xdebug.so" +[xdebug] +xdebug.mode=develop,debug +xdebug.log_level=0 +xdebug.client_host=host.docker.internal +xdebug.client_port=9003 +xdebug.start_with_request=yes +xdebug.discover_client_host=1 diff --git a/.devcontainer/resources/devcontainer-Dockerfile b/.devcontainer/resources/devcontainer-Dockerfile new file mode 100644 index 00000000..88ef4ece --- /dev/null +++ b/.devcontainer/resources/devcontainer-Dockerfile @@ -0,0 +1,51 @@ +# Devcontainer build stage (do not build directly) +# This file is combined with the root /Dockerfile by +# .devcontainer/scripts/generate-dockerfile.sh +# The generator appends this stage to produce .devcontainer/Dockerfile. +# Prefer to place dev-only setup here; use setup.sh only for runtime fixes. + +FROM runner AS devcontainer +ENV INSTALL_DIR=/app +ENV PYTHONPATH=/workspaces/NetAlertX/test:/workspaces/NetAlertX/server:/app:/app/server:/opt/venv/lib/python3.12/site-packages + +# Install common tools, create user, and set up sudo +RUN apk add --no-cache git nano vim jq php83-pecl-xdebug py3-pip nodejs sudo gpgconf pytest pytest-cov && \ + adduser -D -s /bin/sh netalertx && \ + addgroup netalertx nginx && \ + addgroup netalertx www-data && \ + echo "netalertx ALL=(ALL) NOPASSWD:ALL" > /etc/sudoers.d/90-netalertx && \ + chmod 440 /etc/sudoers.d/90-netalertx +# Install debugpy in the virtualenv if present, otherwise into system python3 +RUN /bin/sh -c '(/opt/venv/bin/python3 -m pip install --no-cache-dir debugpy) || (python3 -m pip install --no-cache-dir debugpy) || true' +# setup nginx +COPY .devcontainer/resources/netalertx-devcontainer.conf /etc/nginx/http.d/netalert-frontend.conf +RUN set -e; \ + chown netalertx:nginx /etc/nginx/http.d/netalert-frontend.conf; \ + install -d -o netalertx -g www-data -m 775 /app; \ + install -d -o netalertx -g www-data -m 755 /run/nginx; \ + install -d -o netalertx -g www-data -m 755 /var/lib/nginx/logs; \ + rm -f /var/lib/nginx/logs/* || true; \ + for f in error access; do : > /var/lib/nginx/logs/$f.log; done; \ + install -d -o netalertx -g www-data -m 777 /run/php; \ + install -d -o netalertx -g www-data -m 775 /var/log/php; \ + chown -R netalertx:www-data /etc/nginx/http.d; \ + chmod -R 775 /etc/nginx/http.d; \ + chown -R netalertx:www-data /var/lib/nginx; \ + chmod -R 755 /var/lib/nginx && \ + chown -R netalertx:www-data /var/log/nginx/ && \ + sed -i '/^user /d' /etc/nginx/nginx.conf; \ + sed -i 's|^error_log .*|error_log /dev/stderr warn;|' /etc/nginx/nginx.conf; \ + sed -i 's|^access_log .*|access_log /dev/stdout main;|' /etc/nginx/nginx.conf; \ + sed -i 's|error_log .*|error_log /dev/stderr warn;|g' /etc/nginx/http.d/*.conf 2>/dev/null || true; \ + sed -i 's|access_log .*|access_log /dev/stdout main;|g' /etc/nginx/http.d/*.conf 2>/dev/null || true; \ + mkdir -p /run/openrc; \ + chown netalertx:nginx /run/openrc/; \ + rm -Rf /run/openrc/*; + +# setup pytest +RUN sudo /opt/venv/bin/python -m pip install -U pytest pytest-cov + +WORKDIR /workspaces/NetAlertX + + +ENTRYPOINT ["/bin/sh","-c","sleep infinity"] \ No newline at end of file diff --git a/.devcontainer/resources/netalertx-devcontainer.conf b/.devcontainer/resources/netalertx-devcontainer.conf new file mode 100644 index 00000000..be8f1cca --- /dev/null +++ b/.devcontainer/resources/netalertx-devcontainer.conf @@ -0,0 +1,26 @@ +log_format netalertx '$remote_addr - $remote_user [$time_local] "$request" ' + '$status $body_bytes_sent "$http_referer" ' + '"$http_user_agent" "$http_x_forwarded_for"'; +access_log /var/log/nginx/access.log netalertx flush=1s; +error_log /var/log/nginx/error.log warn; + +server { + listen 20211 default_server; + root /app/front; + index index.php; + + add_header X-Forwarded-Prefix "/netalertx" always; + proxy_set_header X-Forwarded-Prefix "/netalertx"; + + location ~* \.php$ { + add_header Cache-Control "no-store"; + fastcgi_pass 127.0.0.1:9000; + include fastcgi_params; + fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; + fastcgi_param SCRIPT_NAME $fastcgi_script_name; + fastcgi_param PHP_VALUE "xdebug.remote_enable=1"; + fastcgi_connect_timeout 75; + fastcgi_send_timeout 600; + fastcgi_read_timeout 600; + } +} diff --git a/.devcontainer/scripts/generate-dockerfile.sh b/.devcontainer/scripts/generate-dockerfile.sh new file mode 100755 index 00000000..d97cefd9 --- /dev/null +++ b/.devcontainer/scripts/generate-dockerfile.sh @@ -0,0 +1,38 @@ +#!/bin/sh + +# Generator for .devcontainer/Dockerfile +# Combines the root /Dockerfile (with some COPY lines removed) and +# the dev-only stage in .devcontainer/resources/devcontainer-Dockerfile. +# Run this script after modifying the resource Dockerfile to refresh +# the final .devcontainer/Dockerfile used by the devcontainer. + +# Make a copy of the original Dockerfile to the .devcontainer folder +# but remove the COPY . ${INSTALL_DIR}/ command from it. This avoids +# overwriting /app (which uses symlinks to the workspace) and preserves +# debugging capabilities inside the devcontainer. + +SCRIPT_DIR="$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)" +DEVCONTAINER_DIR="${SCRIPT_DIR%/scripts}" +ROOT_DIR="${DEVCONTAINER_DIR%/.devcontainer}" + +OUT_FILE="${DEVCONTAINER_DIR}/Dockerfile" + +echo "# DO NOT MODIFY THIS FILE DIRECTLY. IT IS AUTO-GENERATED BY .devcontainer/scripts/generate-dockerfile.sh" > "$OUT_FILE" +echo "" >> "$OUT_FILE" +echo "# ---/Dockerfile---" >> "$OUT_FILE" + +sed '/${INSTALL_DIR}/d' "${ROOT_DIR}/Dockerfile" >> "$OUT_FILE" + +# sed the line https://github.com/foreign-sub/aiofreepybox.git \\ to remove trailing backslash +sed -i '/aiofreepybox.git/ s/ \\$//' "$OUT_FILE" + +# don't cat the file, just copy it in because it doesn't exist at build time +sed -i 's|^ RUN cat ${INSTALL_DIR}/install/freebox_certificate.pem >> /opt/venv/lib/python3.12/site-packages/aiofreepybox/freebox_certificates.pem$| COPY install/freebox_certificate.pem /opt/venv/lib/python3.12/site-packages/aiofreepybox/freebox_certificates.pem |' "$OUT_FILE" + +echo "" >> "$OUT_FILE" +echo "# ---/resources/devcontainer-Dockerfile---" >> "$OUT_FILE" +echo "" >> "$OUT_FILE" + +cat "${DEVCONTAINER_DIR}/resources/devcontainer-Dockerfile" >> "$OUT_FILE" + +echo "Generated $OUT_FILE using root dir $ROOT_DIR" >&2 diff --git a/.devcontainer/scripts/restart-backend.sh b/.devcontainer/scripts/restart-backend.sh new file mode 100755 index 00000000..3416d561 --- /dev/null +++ b/.devcontainer/scripts/restart-backend.sh @@ -0,0 +1,24 @@ +#!/bin/sh +# Start (or restart) the NetAlertX Python backend under debugpy in background. +# This script is invoked by the VS Code task "Restart GraphQL". +# It exists to avoid complex inline command chains that were being mangled by the task runner. + +set -e + +LOG_DIR=/app/log +APP_DIR=/app/server +PY=python3 +PORT_DEBUG=5678 + +# Kill any prior debug/run instances +sudo killall python3 2>/dev/null || true +sleep 2 + + +cd "$APP_DIR" + +# Launch using absolute module path for clarity; rely on cwd for local imports +setsid nohup ${PY} -m debugpy --listen 0.0.0.0:${PORT_DEBUG} /app/server/__main__.py >/dev/null 2>&1 & +PID=$! +sleep 2 + diff --git a/.devcontainer/scripts/run-tests.sh b/.devcontainer/scripts/run-tests.sh new file mode 100755 index 00000000..80eaf013 --- /dev/null +++ b/.devcontainer/scripts/run-tests.sh @@ -0,0 +1,13 @@ +#!/bin/sh +# shellcheck shell=sh +# Simple helper to run pytest inside the devcontainer with correct paths +set -eu + +# Ensure we run from the workspace root +cd /workspaces/NetAlertX + +# Make sure PYTHONPATH includes server and workspace +export PYTHONPATH="/workspaces/NetAlertX:/workspaces/NetAlertX/server:/app:/app/server:${PYTHONPATH:-}" + +# Default to running the full test suite under /workspaces/NetAlertX/test +pytest -q --maxfail=1 --disable-warnings test "$@" diff --git a/.devcontainer/scripts/setup.sh b/.devcontainer/scripts/setup.sh new file mode 100755 index 00000000..4bff171b --- /dev/null +++ b/.devcontainer/scripts/setup.sh @@ -0,0 +1,191 @@ +#! /bin/bash +# Runtime setup for devcontainer (executed after container starts). +# Prefer building setup into resources/devcontainer-Dockerfile when possible. +# Use this script for runtime-only adjustments (permissions, sockets, ownership, +# and services managed without init) that are difficult at build time. +id + +# Define variables (paths, ports, environment) + +export APP_DIR="/app" +export APP_COMMAND="/workspaces/NetAlertX/.devcontainer/scripts/restart-backend.sh" +export PHP_FPM_BIN="/usr/sbin/php-fpm83" +export NGINX_BIN="/usr/sbin/nginx" +export CROND_BIN="/usr/sbin/crond -f" + + +export ALWAYS_FRESH_INSTALL=false +export INSTALL_DIR=/app +export APP_DATA_LOCATION=/app/config +export APP_CONFIG_LOCATION=/app/config +export LOGS_LOCATION=/app/logs +export CONF_FILE="app.conf" +export NGINX_CONF_FILE=netalertx.conf +export DB_FILE="app.db" +export FULL_FILEDB_PATH="${INSTALL_DIR}/db/${DB_FILE}" +export NGINX_CONFIG_FILE="/etc/nginx/http.d/${NGINX_CONF_FILE}" +export OUI_FILE="/usr/share/arp-scan/ieee-oui.txt" # Define the path to ieee-oui.txt and ieee-iab.txt +export TZ=Europe/Paris +export PORT=20211 +export SOURCE_DIR="/workspaces/NetAlertX" + + + +main() { + echo "=== NetAlertX Development Container Setup ===" + echo "Setting up ${SOURCE_DIR}..." + configure_source + + echo "--- Starting Development Services ---" + configure_php + + + start_services +} + +# safe_link: create a symlink from source to target, removing existing target if necessary +# bypassing the default behavior of symlinking the directory into the target directory if it is a directory +safe_link() { + # usage: safe_link + local src="$1" + local dst="$2" + + # Ensure parent directory exists + install -d -m 775 "$(dirname "$dst")" >/dev/null 2>&1 || true + + # If target exists, remove it without dereferencing symlinks + if [ -L "$dst" ] || [ -e "$dst" ]; then + rm -rf "$dst" + fi + + # Create link; -n prevents deref, -f replaces if somehow still exists + ln -sfn "$src" "$dst" +} + +# Setup source directory +configure_source() { + echo "[1/3] Configuring Source..." + echo " -> Linking source to ${INSTALL_DIR}" + echo "Dev">${INSTALL_DIR}/.VERSION + safe_link ${SOURCE_DIR}/api ${INSTALL_DIR}/api + safe_link ${SOURCE_DIR}/back ${INSTALL_DIR}/back + if [ ! -f "${INSTALL_DIR}/config/app.conf" ]; then + rm -Rf ${INSTALL_DIR}/config + install -d -o netalertx -g www-data -m 750 ${INSTALL_DIR}/config + cp -R ${SOURCE_DIR}/config/* ${INSTALL_DIR}/config/ + fi + + safe_link "${SOURCE_DIR}/db" "${INSTALL_DIR}/db" + safe_link "${SOURCE_DIR}/docs" "${INSTALL_DIR}/docs" + safe_link "${SOURCE_DIR}/front" "${INSTALL_DIR}/front" + safe_link "${SOURCE_DIR}/install" "${INSTALL_DIR}/install" + safe_link "${SOURCE_DIR}/scripts" "${INSTALL_DIR}/scripts" + safe_link "${SOURCE_DIR}/server" "${INSTALL_DIR}/server" + safe_link "${SOURCE_DIR}/test" "${INSTALL_DIR}/test" + safe_link "${SOURCE_DIR}/mkdocs.yml" "${INSTALL_DIR}/mkdocs.yml" + + echo " -> Copying static files to ${INSTALL_DIR}" + cp -R ${SOURCE_DIR}/CODE_OF_CONDUCT.md ${INSTALL_DIR}/ + cp -R ${SOURCE_DIR}/dockerfiles ${INSTALL_DIR}/dockerfiles + sudo cp -na "${INSTALL_DIR}/back/${CONF_FILE}" "${INSTALL_DIR}/config/${CONF_FILE}" + sudo cp -na "${INSTALL_DIR}/back/${DB_FILE}" "${FULL_FILEDB_PATH}" + if [ -e "${INSTALL_DIR}/api/user_notifications.json" ]; then + echo " -> Removing existing user_notifications.json" + sudo rm "${INSTALL_DIR}"/api/user_notifications.json + fi + + echo " -> Setting ownership and permissions" + sudo find ${INSTALL_DIR}/ -type d -exec chmod 775 {} \; + sudo find ${INSTALL_DIR}/ -type f -exec chmod 664 {} \; + sudo date +%s > "${INSTALL_DIR}/front/buildtimestamp.txt" + sudo chmod 640 "${INSTALL_DIR}/config/${CONF_FILE}" || true + + echo " -> Setting up log directory" + sudo rm -Rf ${INSTALL_DIR}/log + install -d -o netalertx -g www-data -m 777 ${INSTALL_DIR}/log + install -d -o netalertx -g www-data -m 777 ${INSTALL_DIR}/log/plugins + + echo " -> Empty log"|tee ${INSTALL_DIR}/log/app.log \ + ${INSTALL_DIR}/log/app_front.log \ + ${INSTALL_DIR}/log/app_front.log \ + ${INSTALL_DIR}/log/execution_queue.log \ + ${INSTALL_DIR}/log/db_is_locked.log \ + ${INSTALL_DIR}/log/stdout.log \ + ${INSTALL_DIR}/log/stderr.log \ + /var/log/nginx/error.log + date +%s > /app/front/buildtimestamp.txt + + killall python &>/dev/null + sleep 1 +} + +# + +# start_services: start crond, PHP-FPM, nginx and the application +start_services() { + echo "[3/3] Starting services..." + + killall nohup &>/dev/null || true + + killall php-fpm83 &>/dev/null || true + killall crond &>/dev/null || true + # Give the OS a moment to release the php-fpm socket + sleep 0.3 + echo " -> Starting CronD" + setsid nohup $CROND_BIN &>/dev/null & + + echo " -> Starting PHP-FPM" + setsid nohup $PHP_FPM_BIN &>/dev/null & + + sudo killall nginx &>/dev/null || true + # Wait for the previous nginx processes to exit and for the port to free up + tries=0 + while ss -ltn | grep -q ":${PORT}[[:space:]]" && [ $tries -lt 10 ]; do + echo " -> Waiting for port ${PORT} to free..." + sleep 0.2 + tries=$((tries+1)) + done + sleep 0.2 + echo " -> Starting Nginx" + setsid nohup $NGINX_BIN &>/dev/null & + echo " -> Starting Backend ${APP_DIR}/server..." + $APP_COMMAND + sleep 2 +} + +# configure_php: configure PHP-FPM and enable dev debug options +configure_php() { + echo "[2/3] Configuring PHP-FPM..." + sudo killall php-fpm83 &>/dev/null || true + install -d -o nginx -g www-data /run/php/ &>/dev/null + sudo sed -i "/^;pid/c\pid = /run/php/php8.3-fpm.pid" /etc/php83/php-fpm.conf + sudo sed -i 's|^listen = .*|listen = 127.0.0.1:9000|' /etc/php83/php-fpm.d/www.conf + sudo sed -i 's|fastcgi_pass .*|fastcgi_pass 127.0.0.1:9000;|' /etc/nginx/http.d/*.conf + + # find any line in php-fmp that starts with either ;error_log or error_log = and replace it with error_log = /app/log/app.php_errors.log + sudo sed -i '/^;*error_log\s*=/c\error_log = /app/log/app.php_errors.log' /etc/php83/php-fpm.conf + # If the line was not found, append it to the end of the file + if ! grep -q '^error_log\s*=' /etc/php83/php-fpm.conf; then + echo 'error_log = /app/log/app.php_errors.log' | sudo tee -a /etc/php83/php-fpm.conf + fi + + sudo mkdir -p /etc/php83/conf.d + sudo cp /workspaces/NetAlertX/.devcontainer/resources/99-xdebug.ini /etc/php83/conf.d/99-xdebug.ini + + sudo rm -R /var/log/php83 &>/dev/null || true + install -d -o netalertx -g www-data -m 755 var/log/php83; + + sudo chmod 644 /etc/php83/conf.d/99-xdebug.ini || true + +} + +# (duplicate start_services removed) + + + +echo "$(git rev-parse --short=8 HEAD)">/app/.VERSION +# Run the main function +main + + + diff --git a/.devcontainer/scripts/stream-logs.sh b/.devcontainer/scripts/stream-logs.sh new file mode 100755 index 00000000..f9864b29 --- /dev/null +++ b/.devcontainer/scripts/stream-logs.sh @@ -0,0 +1,40 @@ +#!/bin/sh +# Stream NetAlertX logs to stdout so the Dev Containers output channel shows them. +# This script waits briefly for the files to appear and then tails them with -F. + +LOG_FILES="/app/log/app.log /app/log/db_is_locked.log /app/log/execution_queue.log /app/log/app_front.log /app/log/app.php_errors.log /app/log/IP_changes.log /app/stderr.log /app/stdout.log" + +wait_for_files() { + # Wait up to ~10s for at least one of the files to exist + attempts=0 + while [ $attempts -lt 20 ]; do + for f in $LOG_FILES; do + if [ -f "$f" ]; then + return 0 + fi + done + attempts=$((attempts+1)) + sleep 0.5 + done + return 1 +} + +if wait_for_files; then + echo "Starting log stream for:" + for f in $LOG_FILES; do + [ -f "$f" ] && echo " $f" + done + + # Use tail -F where available. If tail -F isn't supported, tail -f is used as fallback. + # Some minimal images may have busybox tail without -F; this handles both. + if tail --version >/dev/null 2>&1; then + # GNU tail supports -F + tail -n +1 -F $LOG_FILES + else + # Fallback to -f for busybox; will exit if files rotate or do not exist initially + tail -n +1 -f $LOG_FILES + fi +else + echo "No log files appeared after wait; exiting stream script." + exit 0 +fi diff --git a/.devcontainer/xdebug-trigger.ini b/.devcontainer/xdebug-trigger.ini new file mode 100644 index 00000000..fe3c856b --- /dev/null +++ b/.devcontainer/xdebug-trigger.ini @@ -0,0 +1,11 @@ +zend_extension=xdebug.so +xdebug.mode=debug +xdebug.start_with_request=trigger +xdebug.trigger_value=VSCODE +xdebug.client_host=host.docker.internal +xdebug.client_port=9003 +xdebug.log=/var/log/xdebug.log +xdebug.log_level=7 +xdebug.idekey=VSCODE +xdebug.discover_client_host=true +xdebug.max_nesting_level=512 diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md new file mode 100644 index 00000000..d7f55ba5 --- /dev/null +++ b/.github/copilot-instructions.md @@ -0,0 +1,48 @@ +This is NetAlertX — network monitoring & alerting. + +Purpose: Guide AI assistants to follow NetAlertX architecture, conventions, and safety practices. Be concise, opinionated, and prefer existing helpers/settings over new code or hardcoded values. + +## Architecture (what runs where) +- Backend (Python): main loop + GraphQL/REST endpoints orchestrate scans, plugins, workflows, notifications, and JSON export. + - Key: `server/__main__.py`, `server/plugin.py`, `server/initialise.py`, `server/api_server/api_server_start.py` +- Data (SQLite): persistent state in `db/app.db`; helpers in `server/database.py` and `server/db/*`. +- Frontend (Nginx + PHP + JS): UI reads JSON, triggers execution queue events. + - Key: `front/`, `front/js/common.js`, `front/php/server/*.php` +- Plugins (Python): acquisition/enrichment/publishers under `front/plugins/*` with `config.json` manifests. +- Messaging/Workflows: `server/messaging/*`, `server/workflows/*` +- API JSON Cache for UI: generated under `api/*.json` + +Backend loop phases (see `server/__main__.py` and `server/plugin.py`): `once`, `schedule`, `always_after_scan`, `before_name_updates`, `on_new_device`, `on_notification`, plus ad‑hoc `run` via execution queue. Plugins execute as scripts that write result logs for ingestion. + +## Plugin patterns that matter +- Manifest lives at `front/plugins//config.json`; `code_name` == folder, `unique_prefix` drives settings and filenames (e.g., `ARPSCAN`). +- Control via settings: `_RUN` (phase), `_RUN_SCHD` (cron-like), `_CMD` (script path), `_RUN_TIMEOUT`, `_WATCH` (diff columns). +- Data contract: scripts write `/app/log/plugins/last_result..log` (pipe‑delimited: 9 required cols + optional 4). Use `front/plugins/plugin_helper.py`’s `Plugin_Objects` to sanitize text and normalize MACs, then `write_result_file()`. +- Device import: define `database_column_definitions` when creating/updating devices; watched fields trigger notifications. + +## API/Endpoints quick map +- Flask app: `server/api_server/api_server_start.py` exposes routes like `/device/`, `/devices`, `/devices/export/{csv,json}`, `/devices/import`, `/devices/totals`, `/devices/by-status`, plus `nettools`, `events`, `sessions`, `dbquery`, `metrics`, `sync`. +- Authorization: all routes expect header `Authorization: Bearer ` via `get_setting_value('API_TOKEN')`. + +## Conventions & helpers to reuse +- Settings: add/modify via `ccd()` in `server/initialise.py` or per‑plugin manifest. Never hardcode ports or secrets; use `get_setting_value()`. +- Logging: use `logger.mylog(level, [message])`; levels: none/minimal/verbose/debug/trace. +- Time/MAC/strings: `helper.py` (`timeNowTZ`, `normalize_mac`, sanitizers). Validate MACs before DB writes. +- DB helpers: prefer `server/db/db_helper.py` functions (e.g., `get_table_json`, device condition helpers) over raw SQL in new paths. + +## Dev workflow (devcontainer) +- Services: use tasks to (re)start backend and nginx/PHP-FPM. Backend runs with debugpy on 5678; attach a Python debugger if needed. +- Run a plugin manually: `python3 front/plugins//script.py` (ensure `sys.path` includes `/app/front/plugins` and `/app/server` like the template). +- Testing: pytest available via Alpine packages. Tests live in `test/`; app code is under `server/`. PYTHONPATH is preconfigured to include workspace and `/opt/venv` site‑packages. + +## What “done right” looks like +- When adding a plugin, start from `front/plugins/__template`, implement with `plugin_helper`, define manifest settings, and wire phase via `_RUN`. Verify logs in `/app/log/plugins/` and data in `api/*.json`. +- When introducing new config, define it once (core `ccd()` or plugin manifest) and read it via helpers everywhere. +- When exposing new server functionality, add endpoints in `server/api_server/*` and keep authorization consistent; update UI by reading/writing JSON cache rather than bypassing the pipeline. + +## Useful references +- Docs: `docs/PLUGINS_DEV.md`, `docs/SETTINGS_SYSTEM.md`, `docs/API_*.md`, `docs/DEBUG_*.md` +- Logs: backend `/app/log/app.log`, plugin logs under `/app/log/plugins/`, nginx/php logs under `/var/log/*` + +Assistant expectations +- Reference concrete files/paths. Use existing helpers/settings. Keep changes idempotent and safe. Offer a quick validation step (log line, API hit, or JSON export) for anything you add. \ No newline at end of file diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 00000000..15d4af64 --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,34 @@ +{ + "version": "0.2.0", + "configurations": [ + { + "name": "Python Backend Debug: Attach", + "type": "debugpy", + "request": "attach", + "connect": { + "host": "localhost", + "port": 5678 + }, + "pathMappings": [ + { + // Map workspace root to /app for PHP and other resources, plus explicit server mapping for Python. + "localRoot": "${workspaceFolder}", + "remoteRoot": "/app" + }, + { + "localRoot": "${workspaceFolder}/server", + "remoteRoot": "/app/server" + } + ] + }, + { + "name": "PHP Frontend Xdebug: Listen", + "type": "php", + "request": "launch", + "port": 9003, + "pathMappings": { + "/app": "${workspaceFolder}" + } + } + ] +} \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 00000000..b3b546f5 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,13 @@ +{ + "terminal.integrated.suggest.enabled": true, + // Use pytest and look under the test/ folder + "python.testing.pytestEnabled": true, + "python.testing.unittestEnabled": false, + "python.testing.pytestArgs": [ + "test" + ], + // Ensure VS Code uses the devcontainer virtualenv + "python.defaultInterpreterPath": "/opt/venv/bin/python", + // Let the Python extension invoke pytest via the interpreter; avoid hardcoded paths + // Removed python.testing.pytestPath and legacy pytest.command overrides +} \ No newline at end of file diff --git a/.vscode/tasks.json b/.vscode/tasks.json new file mode 100644 index 00000000..673a0243 --- /dev/null +++ b/.vscode/tasks.json @@ -0,0 +1,94 @@ +{ + "version": "2.0.0", + "tasks": [ + { + "label": "Generate Dockerfile", + "type": "shell", + "command": "${workspaceFolder:NetAlertX}/.devcontainer/scripts/generate-dockerfile.sh", + "presentation": { + "echo": true, + "reveal": "always", + "panel": "shared", + "showReuseMessage": false + }, + "problemMatcher": [], + "group": { + "kind": "build", + "isDefault": false + }, + "options": { + "cwd": "${workspaceFolder:NetAlertX}" + }, + "icon": { + "id": "tools", + "color": "terminal.ansiYellow" + } + }, + { + "label": "Re-Run Startup Script", + "type": "shell", + "command": "${workspaceFolder:NetAlertX}/.devcontainer/scripts/setup.sh", + "presentation": { + "echo": true, + "reveal": "always", + "panel": "shared", + "showReuseMessage": false + }, + "problemMatcher": [], + "icon": { + "id": "beaker", + "color": "terminal.ansiBlue" + } + }, + { + "label": "Start Backend (Python)", + "type": "shell", + "command": "/workspaces/NetAlertX/.devcontainer/scripts/restart-backend.sh", + "presentation": { + "echo": true, + "reveal": "always", + "panel": "shared", + "showReuseMessage": false, + "clear": false + }, + "problemMatcher": [], + "icon": { + "id": "debug-restart", + "color": "terminal.ansiGreen" + } + }, + { + "label": "Start Frontend (nginx and PHP-FPM)", + "type": "shell", + "command": "killall php-fpm83 nginx 2>/dev/null || true; sleep 1; php-fpm83 & nginx", + "presentation": { + "echo": true, + "reveal": "always", + "panel": "shared", + "showReuseMessage": false, + "clear": false + }, + "problemMatcher": [], + "icon": { + "id": "debug-restart", + "color": "terminal.ansiGreen" + } + }, + { + "label": "Stop Frontend & Backend Services", + "type": "shell", + "command": "pkill -f 'php-fpm83|nginx|crond|python3' || true", + "presentation": { + "echo": true, + "reveal": "always", + "panel": "shared", + "showReuseMessage": false + }, + "problemMatcher": [], + "icon": { + "id": "debug-stop", + "color": "terminal.ansiRed" + } + } + ] +} \ No newline at end of file diff --git a/front/.gitignore b/front/.gitignore new file mode 100644 index 00000000..ec7c331e --- /dev/null +++ b/front/.gitignore @@ -0,0 +1 @@ +buildtimestamp.txt \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 00000000..015a7986 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,5 @@ +[tool.pytest.ini_options] +python_classes = ["Test", "Describe"] +python_functions = ["test_", "it_", "and_", "but_", "they_"] +python_files = ["test_*.py",] +testpaths = ["test",] \ No newline at end of file From dfc06d141901809b18336bc5738e37c351e3df52 Mon Sep 17 00:00:00 2001 From: Adam Outler Date: Sat, 20 Sep 2025 13:03:59 +0000 Subject: [PATCH 2/6] setup initial app.conf and app.db --- .devcontainer/scripts/setup.sh | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.devcontainer/scripts/setup.sh b/.devcontainer/scripts/setup.sh index 4bff171b..1f34e4ec 100755 --- a/.devcontainer/scripts/setup.sh +++ b/.devcontainer/scripts/setup.sh @@ -69,12 +69,12 @@ configure_source() { echo "Dev">${INSTALL_DIR}/.VERSION safe_link ${SOURCE_DIR}/api ${INSTALL_DIR}/api safe_link ${SOURCE_DIR}/back ${INSTALL_DIR}/back - if [ ! -f "${INSTALL_DIR}/config/app.conf" ]; then - rm -Rf ${INSTALL_DIR}/config - install -d -o netalertx -g www-data -m 750 ${INSTALL_DIR}/config - cp -R ${SOURCE_DIR}/config/* ${INSTALL_DIR}/config/ + if [ ! -f "${SOURCE_DIR}/config/app.conf" ]; then + cp ${SOURCE_DIR}/back/app.conf ${INSTALL_DIR}/config/ + rm /workspaces/NetAlertX/db/app.db fi + safe_link "${SOURCE_DIR}/config" "${INSTALL_DIR}/config" safe_link "${SOURCE_DIR}/db" "${INSTALL_DIR}/db" safe_link "${SOURCE_DIR}/docs" "${INSTALL_DIR}/docs" safe_link "${SOURCE_DIR}/front" "${INSTALL_DIR}/front" From d3770373d4e3d427f2393d14f3ae4fb805a74f6a Mon Sep 17 00:00:00 2001 From: Adam Outler Date: Sat, 20 Sep 2025 13:56:50 +0000 Subject: [PATCH 3/6] change default database encryption key of `null` to empty string, to prevent exception. --- front/php/server/db.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/front/php/server/db.php b/front/php/server/db.php index f0ee9f1a..9a16c9ca 100755 --- a/front/php/server/db.php +++ b/front/php/server/db.php @@ -82,7 +82,8 @@ class CustomDatabaseWrapper { private $maxRetries; private $retryDelay; - public function __construct($filename, $flags = SQLITE3_OPEN_READWRITE | SQLITE3_OPEN_CREATE, $maxRetries = 3, $retryDelay = 1000, $encryptionKey = null) { + public function __construct($filename, $flags = SQLITE3_OPEN_READWRITE | SQLITE3_OPEN_CREATE, + $maxRetries = 3, $retryDelay = 1000, $encryptionKey = "") { $this->sqlite = new SQLite3($filename, $flags, $encryptionKey); $this->maxRetries = $maxRetries; $this->retryDelay = $retryDelay; From 773580e51b64e9f396a2447d1c9835ef62c3aaf8 Mon Sep 17 00:00:00 2001 From: Adam Outler Date: Sat, 20 Sep 2025 14:21:03 +0000 Subject: [PATCH 4/6] Increase max php executors from 5 to 10. --- .devcontainer/scripts/setup.sh | 3 +++ NetAlertX | 1 + 2 files changed, 4 insertions(+) create mode 160000 NetAlertX diff --git a/.devcontainer/scripts/setup.sh b/.devcontainer/scripts/setup.sh index 1f34e4ec..98f7c658 100755 --- a/.devcontainer/scripts/setup.sh +++ b/.devcontainer/scripts/setup.sh @@ -162,6 +162,9 @@ configure_php() { sudo sed -i 's|^listen = .*|listen = 127.0.0.1:9000|' /etc/php83/php-fpm.d/www.conf sudo sed -i 's|fastcgi_pass .*|fastcgi_pass 127.0.0.1:9000;|' /etc/nginx/http.d/*.conf + #increase max child process count to 10 + sudo sed -i -e 's/pm.max_children = 5/pm.max_children = 10/' /etc/php83/php-fpm.d/www.conf + # find any line in php-fmp that starts with either ;error_log or error_log = and replace it with error_log = /app/log/app.php_errors.log sudo sed -i '/^;*error_log\s*=/c\error_log = /app/log/app.php_errors.log' /etc/php83/php-fpm.conf # If the line was not found, append it to the end of the file diff --git a/NetAlertX b/NetAlertX new file mode 160000 index 00000000..9adcd4c5 --- /dev/null +++ b/NetAlertX @@ -0,0 +1 @@ +Subproject commit 9adcd4c5ee08d81a4afb3f58f6e942e10c6e4bcf From 6831c9e0f4cd0dce57a56ffe5f6d77a9b67f0614 Mon Sep 17 00:00:00 2001 From: Adam Outler Date: Sat, 20 Sep 2025 14:39:42 +0000 Subject: [PATCH 5/6] fix app event queue --- .devcontainer/scripts/setup.sh | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/.devcontainer/scripts/setup.sh b/.devcontainer/scripts/setup.sh index 98f7c658..d806f4bc 100755 --- a/.devcontainer/scripts/setup.sh +++ b/.devcontainer/scripts/setup.sh @@ -107,12 +107,10 @@ configure_source() { echo " -> Empty log"|tee ${INSTALL_DIR}/log/app.log \ ${INSTALL_DIR}/log/app_front.log \ - ${INSTALL_DIR}/log/app_front.log \ - ${INSTALL_DIR}/log/execution_queue.log \ - ${INSTALL_DIR}/log/db_is_locked.log \ - ${INSTALL_DIR}/log/stdout.log \ - ${INSTALL_DIR}/log/stderr.log \ - /var/log/nginx/error.log + ${INSTALL_DIR}/log/stdout.log + touch ${INSTALL_DIR}/log/stderr.log \ + ${INSTALL_DIR}/log/execution_queue.log + date +%s > /app/front/buildtimestamp.txt killall python &>/dev/null From c3dc04c1e5b3390656f217eb3adfbd797aee6c3e Mon Sep 17 00:00:00 2001 From: Adam Outler Date: Sat, 20 Sep 2025 21:26:09 +0000 Subject: [PATCH 6/6] use proper db for setup --- .devcontainer/scripts/setup.sh | 10 +++++----- NetAlertX | 1 - 2 files changed, 5 insertions(+), 6 deletions(-) delete mode 160000 NetAlertX diff --git a/.devcontainer/scripts/setup.sh b/.devcontainer/scripts/setup.sh index d806f4bc..efba3270 100755 --- a/.devcontainer/scripts/setup.sh +++ b/.devcontainer/scripts/setup.sh @@ -69,13 +69,13 @@ configure_source() { echo "Dev">${INSTALL_DIR}/.VERSION safe_link ${SOURCE_DIR}/api ${INSTALL_DIR}/api safe_link ${SOURCE_DIR}/back ${INSTALL_DIR}/back - if [ ! -f "${SOURCE_DIR}/config/app.conf" ]; then - cp ${SOURCE_DIR}/back/app.conf ${INSTALL_DIR}/config/ - rm /workspaces/NetAlertX/db/app.db - fi - safe_link "${SOURCE_DIR}/config" "${INSTALL_DIR}/config" safe_link "${SOURCE_DIR}/db" "${INSTALL_DIR}/db" + if [ ! -f "${SOURCE_DIR}/config/app.conf" ]; then + cp ${SOURCE_DIR}/back/app.conf ${INSTALL_DIR}/config/ + cp ${SOURCE_DIR}/back/app.db ${INSTALL_DIR}/db/ + fi + safe_link "${SOURCE_DIR}/docs" "${INSTALL_DIR}/docs" safe_link "${SOURCE_DIR}/front" "${INSTALL_DIR}/front" safe_link "${SOURCE_DIR}/install" "${INSTALL_DIR}/install" diff --git a/NetAlertX b/NetAlertX deleted file mode 160000 index 9adcd4c5..00000000 --- a/NetAlertX +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 9adcd4c5ee08d81a4afb3f58f6e942e10c6e4bcf