Files
NetAlertX/CLAUDE.md

8.3 KiB

CLAUDE.md

This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.

Project

NetAlertX is a network visibility / asset-intelligence platform: continuous device discovery, presence/intruder detection, IPAM drift tracking, notifications, and multi-site sync, aimed at homelabs, MSPs, and NOCs. Backend is Python, frontend is PHP/JS served by Nginx, data lives in SQLite plus flat config files.

Commands

Almost everything must run inside the devcontainer (Docker) — the host machine lacks the runtime environment (DB, /data/config, dependencies). Check with ls -d /workspaces/NetAlertX; if absent, you're on the host.

# Full test suite (default — comprehensive coverage over speed, don't optimize for time unless asked)
cd /workspaces/NetAlertX; pytest test/

# One file or directory
pytest test/plugins/test_adguard_export.py
pytest test/plugins/

# Fast/unit-only (only when explicitly asked for "fast"/"quick" tests)
pytest test/ -m 'not docker and not feature_complete'

# Reset the environment / pick up code changes / get a fresh API_TOKEN
bash /workspaces/NetAlertX/.devcontainer/scripts/setup.sh
sleep 5
python3 -c "from helper import get_setting_value; print(get_setting_value('API_TOKEN'))"

# Lint (matches CI in .github/workflows/code-checks.yml)
flake8 . --max-line-length=180 --ignore=E221,E222,E251,E203

# Full CI-equivalent run (regenerates devcontainer Dockerfile, rebuilds, runs everything)
./scripts/run_tests_in_docker_environment.sh

Rebuild the test image (docker buildx build -t netalertx-test .) only if the Dockerfile or dependencies changed — otherwise skip it, it's slow.

Outside the container, most plugin unit tests (test/plugins/test_*.py) still run standalone — they stub NetAlertX modules into sys.modules before importing the plugin script. See the stubbing pitfall below before adding one.

Architecture

Backend layout

  • server/__main__.py — entry point. server/plugin.py — plugin runner/scheduler. server/api_server/ — Flask + GraphQL API.
  • server/const.py / server/config_paths.py — resolve the three runtime path roots. server/conf.py — process-wide config variables (a deliberate workaround for cross-module globals).
  • server/db/ — the only layer allowed to touch SQLite directly (db_helper.py). server/models/ — domain handlers on top of it (e.g. DeviceInstance in models/device_instance.py). Never query the DB from elsewhere — go through a model or db_helper.py.
  • server/scan/, server/messaging/, server/workflows/, server/utils/ — scanning pipeline, notification dispatch, the workflow-automation engine, and shared utilities (utils/datetime_utils.py's timeNowUTC() is the only place datetime.now() should be called — everything is stored in UTC).

Frontend

front/ is PHP + vanilla JS served by Nginx — no build step, no bundler, no package.json. Pages are top-level .php files; shared logic under front/php/.

Data & path conventions

Three distinct roots, each with a different persistence contract — get this wrong and data silently disappears on restart:

  • dbFolderPath (/data/db) — durable. Plugin-internal state (caches, "what did I already do" trackers) belongs here.
  • configPath (/data/config) — durable, user-facing. app.conf lives here; config-like plugin artifacts (exports, backups) belong here too.
  • logPath (/tmp/log, plus /tmp/api, /tmp/db_is_locked, nginx state) — ephemeral tmpfs, wiped on every container restart. Never put anything here you need to survive a restart. (server/plugins/adguard_export, unifi_import were both fixed this way after shipping with state files rooted in logPath — check any plugin that opens a file outside its RESULT_FILE against this before assuming it's fine.)

All three, plus dataPath (/data, the bare parent of dbFolderPath/configPath - avoid writing loose files directly under it; pick one of the two subpaths above instead), are exported from server/const.py and importable by any plugin.

Plugin system (server/plugins/*/)

Every plugin is a folder with config.json (manifest: settings, data contract, DB column mapping), an optional script.py-equivalent, and a README.md. Start from server/plugins/__template/. Full reference: docs/PLUGINS_DEV.md (its "Conventions Checklist" section is CI-enforced — see below).

Non-obvious things that have caused real, shipped bugs in this codebase:

  • RUN_TIMEOUT is the whole subprocess's kill-timeout, enforced by server/plugin.py, not a safe per-call HTTP/subprocess timeout. A plugin that loops over N things and reuses RUN_TIMEOUT as each individual call's timeout can have one slow call burn the whole budget and get SIGKILLed before it writes its result file — silently losing the entire run. Two correct answers depending on the loop shape:
    • Looping over a config-declared, known-length list (e.g. a subnets setting) → mark that params[] entry "timeoutMultiplier": true in config.json; the framework scales the outer kill-timeout by the list length. See arp_scan/config.json.
    • Looping over a runtime-variable-length collection (e.g. a notification queue) → plugin_helper.per_item_timeout(run_timeout, item_count) divides the inner per-call budget instead. See server/plugins/_publisher_ntfy/ntfy.py.
    • test/plugins/test_plugin_conventions.py mechanically checks for the unguarded reuse pattern (AST-based, including the case where the loop calls a helper function that does the risky call) — run it after touching any plugin that makes network/subprocess calls in a loop.
  • plugin_helper.Plugin_Object: helpVal1-4 and watchedValue1-4 both preserve a real 0/False you pass explicitly (checked via is not None) — only an actually-omitted (None) value defaults to "". Don't reintroduce a bare x or "" coercion here; it silently discards legitimate falsy values (this was a real, if narrowly-triggered, bug).
  • A plugin's hardcoded Python fallback (get_setting_value("X") or <literal>) must match that setting's config.json default_valuetest_plugin_conventions.py checks this too. RUN should default to "disabled" for every non-core plugin; description strings render directly in the Settings UI and should stay short (README is for implementation detail).
  • Plugin unit tests that stub NetAlertX modules into sys.modules (so a script imports standalone outside the container) must pop every stubbed name back out immediately after the one-time import — otherwise the fake module leaks and shadows the real one for every other test file collected in the same pytest session, regardless of file/alphabetical order. See test/plugins/test_ntfy_custom_headers.py for the pattern, or docs/PLUGINS_DEV.md / the testing-workflow skill for the full writeup.

Data contract (plugin → DB)

Plugins write pipe-delimited rows to RESULT_FILE via plugin_helper.Plugin_Objects/Plugin_Object — 9 required columns, 4 optional helpVal* ones. Full column spec and validation rules: docs/PLUGINS_DEV_DATA_CONTRACT.md.

Skills

Procedural/how-to knowledge (running tests, resetting the DB, devcontainer management, PR analysis, etc.) lives as paired files in .gemini/skills/<name>/ and .github/skills/<name>/ (see .gemini/skills/skills-index/SKILL.md for the pairing map) — Claude Code should treat both as equally authoritative sources for the same procedures. The pairing convention is "keep body content identical"; a CI job (check-skill-pairs in .github/workflows/code-checks.yml) flags PRs that edit one side of a pair without the other, but it only checks presence, not content — if you edit one side, check whether the other needs the same update.

Code conventions

  • DB columns are camelCase, never snake_case (deviceInstanceId, not device_instance_id).
  • Every subprocess call needs an explicit timeout; a nested subprocess call needs its own — an outer timeout doesn't propagate.
  • Always run MACs through normalize_mac() (plugin_helper.py) before writing to DB; MAC literals in tests must be lowercase.
  • No inline imports — everything at module top level.
  • Reuse test/db_test_helpers.py for DB mocks/fixtures in tests rather than redefining DummyDB/make_db locally.
  • Keep files under ~500 lines; split rather than grow.