Skip to content

feat: add adguard_export plugin#1649

Open
natecj wants to merge 2 commits into
netalertx:mainfrom
natecj:feat/adguard-export-plugin
Open

feat: add adguard_export plugin#1649
natecj wants to merge 2 commits into
netalertx:mainfrom
natecj:feat/adguard-export-plugin

Conversation

@natecj
Copy link
Copy Markdown

@natecj natecj commented May 20, 2026

Summary

  • Adds a new adguard_export plugin that syncs known devices from the NetAlertX database to AdGuard Home as persistent clients, keeping names, MAC addresses, IP addresses, and device-type tags in sync
  • Supports scheduled runs, optional offline/new-device filtering, MAC-or-IP identifier preference, and safe scoped deletion (only removes clients the plugin previously created, tracked via a local state file)
  • Fixes the adguard_import config.json description placeholder and a minor indentation inconsistency

New files

  • front/plugins/adguard_export/script.py — main sync script
  • front/plugins/adguard_export/config.json — plugin metadata and settings
  • front/plugins/adguard_export/README.md — documentation
  • test/plugins/test_adguard_export.py — 31 pytest tests covering device-type tag mapping, client building, state file round-trips, SQLite queries, and the full sync logic

Test plan

  • pytest test/plugins/test_adguard_export.py -v passes (31 tests)
  • Plugin appears in NetAlertX Settings → Plugins after container restart
  • Configure with a real AdGuard Home instance, run once, verify clients are created with correct names/MACs/tags
  • Rename a device in NetAlertX, re-run, verify AdGuard client name updates
  • Enable ADGUARDEXP_DELETE, remove a device from NetAlertX, re-run, verify only that client is removed
  • Verify manually-added AdGuard clients are never deleted

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Added AdGuard Home device export plugin: scheduled runs, connection/auth settings, TLS option, controls for including offline/new devices, choice of identifier (MAC/IP), and optional safe deletion of previously exported clients.
  • Documentation

    • Added full README for the export plugin (usage, config, logging, troubleshooting).
    • Updated AdGuard import plugin description for clarity.
  • Tests

    • Added unit tests covering mapping, client construction, persistence, DB queries, and sync behaviors.

Review Change Stack

Adds a new NetAlertX plugin that syncs known devices from the NetAlertX
database to AdGuard Home as persistent clients, keeping names, MACs, IP
addresses, and device-type tags in sync.

Also fixes the adguard_import config.json description placeholder and a
minor indentation inconsistency in that file.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 20, 2026

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 71f1ad9e-ce14-44e0-a927-ee895c17d8c8

📥 Commits

Reviewing files that changed from the base of the PR and between 197e3a3 and ca7a699.

📒 Files selected for processing (4)
  • front/plugins/adguard_export/README.md
  • front/plugins/adguard_export/config.json
  • front/plugins/adguard_export/script.py
  • test/plugins/test_adguard_export.py
✅ Files skipped from review due to trivial changes (1)
  • front/plugins/adguard_export/README.md

📝 Walkthrough

Walkthrough

Adds a new AdGuard Home device export plugin: configuration and README, Python implementation that syncs NetAlertX Devices to AdGuard Home persistent clients with managed-name state and optional deletions, and a comprehensive pytest suite; plus a minor description update to the AdGuard import plugin.

Changes

AdGuard Export Plugin

Layer / File(s) Summary
Plugin configuration and documentation
front/plugins/adguard_export/config.json, front/plugins/adguard_export/README.md
Plugin metadata, settings schema (scheduling, command, URL/creds, TLS, inclusion flags, deletion guard), database column mappings, and full README describing behavior, identifier/tag rules, state file, logs, troubleshooting, and release notes.
Core plugin implementation
front/plugins/adguard_export/script.py
Module initialization and paths, STATE_FILE JSON persistence, _TYPE_TAG_MAP mapping with substring fallback, AdGuardClient HTTP wrapper, get_netalertx_devices() SQLite querying and normalization, build_agrd_client() payload builder, sync_to_adguard() indexing/add/update/delete logic, and main() with settings parsing and result output.
Comprehensive test suite
test/plugins/test_adguard_export.py
Pytest module with container-aware stubs, SQLite helpers, mocked AdGuardClient, and tests covering device-type→tag mapping, client payload construction, managed-name persistence, device DB filtering, and sync behaviors (add/update/skip/delete and custom-field preservation).

Import Plugin Documentation Update

Layer / File(s) Summary
Import plugin description clarification
front/plugins/adguard_import/config.json
Refines English description to state the import plugin imports known AdGuard devices as persistent devices and keeps device names and IP/MAC identifiers synchronized; ensures settings array placement.

Sequence Diagram(s):

sequenceDiagram
  participant Main as main()
  participant DB as get_netalertx_devices()
  participant Builder as build_agrd_client()
  participant Sync as sync_to_adguard()
  participant AdGuard as AdGuardClient
  participant State as managed_names

  Main->>DB: query devices with filters
  DB-->>Main: list of devices
  Main->>Sync: pass devices, flags
  Sync->>State: load_managed_names()
  State-->>Sync: managed name set
  Sync->>AdGuard: get_clients()
  AdGuard-->>Sync: existing clients list
  Sync->>Builder: build_agrd_client(device)
  Builder-->>Sync: client payload (ids,tags,name)
  Sync->>AdGuard: add_client()/update_client()/delete_client()
  AdGuard-->>Sync: HTTP responses
  Sync-->>Main: counts (added, updated, skipped, deleted)
  Main->>State: save_managed_names()
  Main-->>Main: write result file
Loading

Suggested reviewers

  • jokob-sk

"I'm a rabbit with a sync in my hop,
I map MACs and IPs until they stop,
Names kept in a file,
AdGuard and NetAlertX smile,
Hop, sync, and then home we drop!"

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 15.38% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately and concisely describes the main change: adding a new AdGuard export plugin to the NetAlertX project.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@natecj natecj marked this pull request as ready for review May 23, 2026 02:48
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

🧹 Nitpick comments (2)
front/plugins/adguard_export/README.md (1)

136-140: ⚡ Quick win

Standardize the maintainer metadata block to ### Other info format.

This README uses ## Notes; plugin READMEs are expected to use the standard ### Other info block with canonical labels and date format.

Suggested patch
-## Notes
+### Other info
 
 - Version: 1.0.0
 - Author: [natecj](https://github.com/natecj)
-- Release Date: 2026-05-10
+- Release Date: 10-May-2026

Based on learnings: For each NetAlertX plugin README at front/plugins/**/README.md, include a ### Other info section with the exact standard field labels and DD-Mon-YYYY release date format.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@front/plugins/adguard_export/README.md` around lines 136 - 140, Replace the
non-standard "## Notes" block with the canonical "### Other info" metadata block
in this README: change the heading to "### Other info", use the exact standard
field labels (Version, Maintainer, Release Date) and convert the Release Date to
the DD-Mon-YYYY format (e.g., 10-May-2026); update the Author label to
"Maintainer" and ensure the maintainer value uses the same notation as other
plugins (e.g., [natecj](https://github.com/natecj)).
test/plugins/test_adguard_export.py (1)

339-347: ⚡ Quick win

Add a regression test to ensure ID-matched manual clients are not adopted into managed state.

Current tests verify unmanaged clients aren’t deleted when state is empty, but not the case where a manual client is matched by ID during sync and must still remain unmanaged.

Suggested test addition
 class TestSyncToAdguard:
@@
     def test_unmanaged_device_not_deleted(self, tmp_path):
         # State file is empty — we never added this client
         state = tmp_path / "state.json"
         state.write_text(json.dumps({"managed": []}))
         existing = [{"name": "Manual Client", "ids": ["10.0.0.50"], "tags": []}]
         agrd = _mock_agrd(existing=existing)
         with patch("script.STATE_FILE", str(state)):
             sync_to_adguard(agrd, [], use_mac=True, delete_missing=True)
         agrd.delete_client.assert_not_called()
+
+    def test_id_matched_manual_client_is_not_marked_managed(self, tmp_path):
+        state = tmp_path / "state.json"
+        state.write_text(json.dumps({"managed": []}))
+        existing = [{"name": "Manual Client", "ids": ["aa:bb:cc:00:00:01", "10.0.0.1"], "tags": ["device_pc"]}]
+        agrd = _mock_agrd(existing=existing)
+        with patch("script.STATE_FILE", str(state)):
+            sync_to_adguard(agrd, [self._device(name="Manual Client")], use_mac=True, delete_missing=False)
+            managed = load_managed_names()
+        assert managed == set()
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@test/plugins/test_adguard_export.py` around lines 339 - 347, Add a regression
test ensuring manual clients matched by ID are not adopted: create a test (e.g.
test_unmanaged_device_not_deleted_by_id) that writes an empty managed state to
STATE_FILE, mocks existing AdGuard clients with an entry containing the matching
ID (use _mock_agrd to return existing), then call sync_to_adguard(agrd, [],
use_mac=True, delete_missing=True) and assert
agrd.delete_client.assert_not_called(); this mirrors
test_unmanaged_device_not_deleted but verifies ID-based matching doesn't convert
a manual client into managed state during sync.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@front/plugins/adguard_export/config.json`:
- Around line 340-533: The visible column objects in the config (the column
definitions mapped to cur_MAC, cur_IP, cur_Name, etc.) are missing show: true so
getColumnDefinitions() (which filters by colDef.show) hides them; update each
visible column object (e.g., the entries with "column": "Object_PrimaryID",
"Object_SecondaryID", "Watched_Value1", "Watched_Value2", "Watched_Value3",
"Watched_Value4", "Extra", "ForeignKey") to include show: true so they render in
the plugin output table.

In `@front/plugins/adguard_export/README.md`:
- Around line 96-114: Update the three fenced code blocks that show file paths
so they include a language identifier (e.g., "text") to satisfy markdownlint
MD040: change the blocks containing "/app/db/state.ADGUARDEXP.json",
"/tmp/log/plugins/script.ADGUARDEXP.log", and
"/tmp/log/plugins/last_result.ADGUARDEXP.log" to use triple backticks with a
language tag (text) instead of plain fences; keep the content unchanged and only
add the language identifier to each fence in README.md.
- Line 46: Update the README entry for ADGUARDEXP_URL to match the actual config
default: change the documented default URL from http://192.168.11.1:3000 to
http://localhost:3000 so it matches the default set for ADGUARDEXP_URL in the
plugin config (check the default value in config.json where ADGUARDEXP_URL is
defined).

In `@front/plugins/adguard_export/script.py`:
- Around line 320-342: The current logic marks any matched existing AdGuard
clients as plugin-managed by adding to managed_names inside the update and skip
branches (references: managed_names, agrd.update_client, existing, device,
client_data), which may cause deletion of manually-created clients later; change
it so we do NOT add or rename entries in managed_names when we merely match or
update an existing client (remove managed_names.add(old_name) /
managed_names.add(device["name"]) from the update and skip branches), and only
add names to managed_names at the single location where we actually create a new
client (the create_client/create branch) or when we can prove the plugin already
owned it; ensure rename logic still updates names for created-managed clients
but never converts a pre-existing manual client into managed-owned.

---

Nitpick comments:
In `@front/plugins/adguard_export/README.md`:
- Around line 136-140: Replace the non-standard "## Notes" block with the
canonical "### Other info" metadata block in this README: change the heading to
"### Other info", use the exact standard field labels (Version, Maintainer,
Release Date) and convert the Release Date to the DD-Mon-YYYY format (e.g.,
10-May-2026); update the Author label to "Maintainer" and ensure the maintainer
value uses the same notation as other plugins (e.g.,
[natecj](https://github.com/natecj)).

In `@test/plugins/test_adguard_export.py`:
- Around line 339-347: Add a regression test ensuring manual clients matched by
ID are not adopted: create a test (e.g. test_unmanaged_device_not_deleted_by_id)
that writes an empty managed state to STATE_FILE, mocks existing AdGuard clients
with an entry containing the matching ID (use _mock_agrd to return existing),
then call sync_to_adguard(agrd, [], use_mac=True, delete_missing=True) and
assert agrd.delete_client.assert_not_called(); this mirrors
test_unmanaged_device_not_deleted but verifies ID-based matching doesn't convert
a manual client into managed state during sync.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: e0fd4190-ac85-4f3d-aa4f-7f0ae59be37c

📥 Commits

Reviewing files that changed from the base of the PR and between 198ca5d and 197e3a3.

📒 Files selected for processing (5)
  • front/plugins/adguard_export/README.md
  • front/plugins/adguard_export/config.json
  • front/plugins/adguard_export/script.py
  • front/plugins/adguard_import/config.json
  • test/plugins/test_adguard_export.py

Comment thread front/plugins/adguard_export/config.json
Comment thread front/plugins/adguard_export/README.md Outdated
Comment thread front/plugins/adguard_export/README.md Outdated
Comment thread front/plugins/adguard_export/script.py
- config.json: add show:true to all visible column definitions so they
  render in the plugin output table
- script.py: fix managed_names adoption bug — update/skip branches no
  longer add unowned clients to managed state; rename tracking now
  scoped to plugin-created clients only
- README.md: fix ADGUARDEXP_URL default (localhost:3000, not local IP),
  add language tags to fenced code blocks, normalise metadata block to
  Other info / Maintainer / DD-Mon-YYYY format
- test_adguard_export.py: add regression test for manual client matched
  by ID not being adopted into managed state

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@jokob-sk
Copy link
Copy Markdown
Collaborator

Hi @natecj - thanks a lot for the PR. Let me know once you think it's ready to go and I'll do a close review.

devices = []
conn = None
try:
conn = sqlite3.connect(db_path)
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ideally use existing methods, don't query the DB directly see e.g. server/models/device_instance.py

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants