Skip to content

Refactor/librenms - #12

Closed
marcinpsk wants to merge 28 commits into
developfrom
refactor/librenms_id
Closed

Refactor/librenms#12
marcinpsk wants to merge 28 commits into
developfrom
refactor/librenms_id

Conversation

@marcinpsk

@marcinpsk marcinpsk commented Feb 28, 2026

Copy link
Copy Markdown
Owner

Summary

Briefly describe what this PR does in plain English, and provide as much of the following information as possible.

Motivation / Problem

What issue does this solve?

  • Bug
  • Feature
  • Refactor
  • Maintenance / cleanup

Link any related issues if applicable.

Scope of Change

Tick all that apply:

  • Sync/Import logic
  • NetBox models / ORM
  • LibreNMS API interaction
  • Config / settings
  • Web UI / templates
  • Database migrations
  • Tests
  • Docs only
  • Other (please descibe)?

How Was This Tested?

Tick all that apply and describe briefly.

  • Unit tests
  • Manual testing
  • Not tested (explain why)

Manual Test Steps (if applicable)

Risk Assessment

  • Does this change affect existing users?
  • Could this cause unintended imports / updates?

Explain briefly.

Backwards Compatibility

  • No breaking changes
  • Breaking change (explain and document)

Other Notes

Anything the maintainer(s) should pay particular attention to?

Summary by CodeRabbit

  • New Features

    • Bulk device & VM import with progress/cancellation, virtual-chassis detection/creation, conflict-action modal, update-device-name and remove-server-mapping endpoints, per-server LibreNMS connections UI, and naming preferences (use_sysname/strip_domain).
  • Bug Fixes

    • Improved caching resilience and metadata, safer transactional rollbacks, consistent modal lifecycle/teardown, and clearer error messages.
  • Refactor

    • Import utilities reorganized; per-server LibreNMS ID handling and migration helpers added.
  • Tests

    • Expanded coverage for imports, validation, conflict actions, naming prefs, and ID migration.

marcinpsk and others added 6 commits February 26, 2026 22:44
Break the monolithic import_utils.py into focused modules:

- permissions.py: user permission checks
- cache.py: cache key generation and management
- filters.py: device filtering and retrieval from LibreNMS
- virtual_chassis.py: VC detection, creation, member management
- device_operations.py: device validation, import, and fetch
- vm_operations.py: VM creation and bulk import
- bulk_import.py: bulk device import orchestration and filter processing

The __init__.py re-exports all public names, so existing callers
(views, jobs, tests) continue working without import changes.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
After splitting import_utils.py into a package, mock.patch decorators
must target the actual submodule where each name is looked up, not the
package __init__.py. Update all patch paths in test_import_utils.py and
test_permissions.py to point to the correct submodules.
Keep Cluster as a top-level import so mock.patch targets can resolve it.
Remove the redundant inline import inside validate_device_for_import.
Now that Cluster is a module-level import in device_operations,
the test only needs to mock device_operations.Cluster — the extra
patch on virtualization.models.Cluster was leftover from the
inline-import era and caused the mock to target the wrong object.
bonzo81#227)

- Add use_sysname/strip_domain params to validate_device_for_import
- Store resolved name in validation result as resolved_name
- Update import_single_device, bulk_import_devices_shared, bulk_import_vms
  to extract and pass naming prefs from sync_options
- Add _resolve_naming_preferences() helper in actions.py with fallback chain:
  POST data -> user pref -> LibreNMSSettings -> plugin default
- Update DeviceImportHelperMixin and BulkImportConfirmView to pass prefs
- Replace _determine_device_name() in BulkImportConfirmView with resolved_name
- Add TestDeviceNamingPreferences (5 tests) to test_import_utils.py
@coderabbitai

coderabbitai Bot commented Feb 28, 2026

Copy link
Copy Markdown
📝 Walkthrough

Walkthrough

The monolithic import utilities were split into a package of focused submodules (cache, filters, device_operations, bulk_import, vm_operations, virtual_chassis, permissions) with a re-exporting init.py; librenms_id handling became server-scoped with migration utilities; views, tables, JS, templates, jobs, and tests updated to propagate server_key and naming preferences and to add conflict/name-update flows.

Changes

Cohort / File(s) Summary
Import utils package
netbox_librenms_plugin/import_utils.py (removed), netbox_librenms_plugin/import_utils/__init__.py
Removed monolithic module; added package with __init__ re-exporting original public API from new submodules to preserve imports.
Bulk import & orchestration
netbox_librenms_plugin/import_utils/bulk_import.py
New orchestration for multi-device import: process_device_filters, bulk_import_devices_shared/bulk_import_devices, job-aware progress/cancellation, per-device validation/import, VC handling, and result aggregation.
Cache utilities
netbox_librenms_plugin/import_utils/cache.py
Deterministic cache key generation, validated-device and raw-device cache keys, metadata index and active cached-search listing with expiry cleanup.
Device operations
netbox_librenms_plugin/import_utils/device_operations.py
Name determination, validate_device_for_import, import_single_device, get_librenms_device_by_id, fetch_device_with_cache, and caching/lookup helpers for import flows.
Filters & retrieval
netbox_librenms_plugin/import_utils/filters.py
API + client-side filtering with caching: get_librenms_devices_for_import, _apply_client_filters, get_device_count_for_filters, and optional cache-status return.
VM import
netbox_librenms_plugin/import_utils/vm_operations.py
create_vm_from_librenms and bulk_import_vms with permission checks, per-device aggregation, and job-aware logging.
Virtual chassis
netbox_librenms_plugin/import_utils/virtual_chassis.py
VC detection from inventory, member naming pattern, versioned VC cache keys, prefetching, and transactional create_virtual_chassis_with_members.
Permissions
netbox_librenms_plugin/import_utils/permissions.py
check_user_permissions and require_permissions helpers (raises PermissionDenied when missing).
Per-server librenms_id utilities
netbox_librenms_plugin/utils.py
get_librenms_device_id, set_librenms_device_id, find_by_librenms_id, and migrate_legacy_librenms_id to support server-scoped JSON cf and legacy int migration.
API delegation
netbox_librenms_plugin/librenms_api.py
get_librenms_id and _store_librenms_id now delegate to get_/set_librenms_device_id in utils.
Views, actions & URLs
netbox_librenms_plugin/views/imports/actions.py, netbox_librenms_plugin/views/sync/device_fields.py, netbox_librenms_plugin/urls.py, netbox_librenms_plugin/views/__init__.py
Added DeviceConflictActionView, UpdateDeviceNameView, RemoveServerMappingView; naming-preference resolution; HTMX conflict actions; new URL endpoints and re-exports.
Server-key propagation
netbox_librenms_plugin/tables/interfaces.py, netbox_librenms_plugin/views/object_sync/*.py, netbox_librenms_plugin/views/base/*, netbox_librenms_plugin/views/sync/interfaces.py
Propagated server_key through tables/views; replaced direct custom_field_data access with get_/set_librenms_device_id usage; constructors updated to accept server_key.
UI & JS
netbox_librenms_plugin/templates/.../device_validation_details.html, netbox_librenms_plugin/templates/librenms_import.html, netbox_librenms_plugin/static/.../librenms_import.js
Redesigned validation modal to a two-column layout with many HTMX actions, enlarged HTMX modals, and centralized modal hide logic in JS.
Tables
netbox_librenms_plugin/tables/device_status.py
Added per-row details/conflict HTMX button next to NetBox link to surface validation/conflict modal.
Jobs & naming prefs
netbox_librenms_plugin/jobs.py, netbox_librenms_plugin/views/imports/list.py
Added use_sysname and strip_domain flags to job and view flows; propagated to process_device_filters and cache key generation.
Tests
netbox_librenms_plugin/tests/*
Updated tests to new submodules and utilities, adjusted patch targets, expanded serial/conflict/migration tests, and updated handling for duplicate-device body-based error.
Misc views
netbox_librenms_plugin/views/base/cables_view.py, netbox_librenms_plugin/views/base/ip_addresses_view.py
Switched lookups to server-keyed librenms_id access with fallback and updated query filters accordingly.

Sequence Diagram(s)

sequenceDiagram
    participant User
    participant View as BulkImportConfirmView
    participant Filter as process_device_filters
    participant Cache as DjangoCache
    participant API as LibreNMSAPI
    participant Validator as validate_device_for_import
    participant NetBox as NetBoxDB
    participant Job

    User->>View: POST bulk import request
    View->>Filter: process_device_filters(api, filters, vc_detection, job, use_sysname, strip_domain)
    Filter->>Cache: lookup cache metadata
    alt cached
        Cache-->>Filter: cached device list
    else not cached
        Filter->>API: list_devices(api_filters)
        API-->>Filter: devices
        Filter->>Cache: store devices & metadata
    end
    Filter-->>View: validated_devices
    loop per device
        View->>API: fetch_device_with_cache(device_id)
        API-->>View: libre_device
        View->>Validator: validate_device_for_import(libre_device, use_sysname, strip_domain)
        Validator->>NetBox: query by librenms_id/hostname/serial/IP
        NetBox-->>Validator: existing matches (or none)
        Validator-->>View: validation result
        alt importable
            View->>View: import_single_device(...)
            View->>NetBox: create/update Device/VM
            View-->>Job: update progress
        else skip/failure
            View-->>Job: log failure/skip
        end
    end
    View-->>User: import summary (success/failed/skipped)
Loading
sequenceDiagram
    participant Browser
    participant View as DeviceConflictActionView
    participant Validator as validate_device_for_import
    participant Utils as utils.set_librenms_device_id
    participant Cache as DjangoCache
    participant NetBox as NetBoxDB

    Browser->>View: POST conflict action (link/sync/update...)
    View->>Validator: validate_device_for_import(libre_device)
    Validator->>NetBox: lookup existing by librenms_id/hostname/serial
    NetBox-->>Validator: validation result
    alt link action
        View->>Utils: set_librenms_device_id(device, librenms_id, server_key)
        Utils-->>NetBox: persist custom_field_data
    else sync/update action
        View->>NetBox: update fields (name/serial/type/platform)
    end
    View->>Cache: invalidate import cache for device
    View->>Validator: re-validate device
    Validator-->>View: updated validation
    View-->>Browser: return updated row HTML (HTMX)
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~75 minutes

Possibly related PRs

Poem

🐇 I pried a giant file apart with care,
New modules nest helpers everywhere.
Server keys whisper, conflicts find a home,
Modals tuck in neatly — no more frantic roam.
Hop — imports tidy, code now light as air!

🚥 Pre-merge checks | ✅ 1 | ❌ 2

❌ Failed checks (1 warning, 1 inconclusive)

Check name Status Explanation Resolution
Description check ⚠️ Warning The PR description is completely empty—it contains only the template structure with unchecked checkboxes and no actual content, summary, motivation, scope, testing details, or risk assessment. Fill in all required sections of the PR description: summary, motivation/problem, scope checklist, testing approach, risk assessment, backwards compatibility, and any maintainer notes.
Title check ❓ Inconclusive The PR title is too vague and generic. 'Refactor/librenms' does not clearly convey the scope or nature of the changes made in this substantial refactor. Provide a more descriptive title that captures the main objective, such as 'Refactor LibreNMS import utilities into submodules with multi-server support' or similar.
✅ Passed checks (1 passed)
Check name Status Explanation
Docstring Coverage ✅ Passed Docstring coverage is 84.82% which is sufficient. The required threshold is 80.00%.

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

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch refactor/librenms_id

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.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 18

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
netbox_librenms_plugin/views/imports/actions.py (1)

501-507: 🧹 Nitpick | 🔵 Trivial

Deduplicate the repeated cache build loop.

The same parsed_ids -> fetch_device_with_cache() loop is duplicated for background and synchronous paths. Extract once and reuse.

Also applies to: 563-569

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@netbox_librenms_plugin/views/imports/actions.py` around lines 501 - 507,
Duplicate loops building libre_devices_cache by iterating parsed_ids and calling
fetch_device_with_cache should be consolidated: extract the cache-building logic
into a single helper (e.g., build_libre_devices_cache(parsed_ids, librenms_api))
or a small private method used by both the background and synchronous branches,
so both branches reuse the same libre_devices_cache population instead of
repeating the for-device_id -> fetch_device_with_cache(...) loop; ensure the
helper returns the libre_devices_cache dict and replace the duplicated loops at
the locations around the existing libre_devices_cache usage (including the other
occurrence around lines 563-569) with calls to that helper.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@netbox_librenms_plugin/import_utils/bulk_import.py`:
- Around line 380-383: process_device_filters currently returns [] on several
early-exit exception/interrupt paths which breaks callers that expect the
(devices, from_cache) tuple when return_cache_status=True; update every
early-return branch within process_device_filters (the except/if request blocks
that log client disconnects and cancellations) to return an appropriate
two-tuple: an empty devices list and the from_cache boolean (e.g., ([], False)
or ([], from_cache) depending on context) so callers always receive (devices,
from_cache); ensure the change covers all similar branches referencing request
and logger inside process_device_filters.

In `@netbox_librenms_plugin/import_utils/cache.py`:
- Around line 65-66: The cache key "librenms_locations_choices" is global and
can mix labels across LibreNMS servers; update the key construction where
location_cache_key is defined/used (the variable named location_cache_key and
the cache.get/cache.set calls that use it, e.g., the cached_locations lookup) to
include the server-specific identifier (server_key) so the key becomes unique
per server (for example by interpolating or concatenating server_key into the
key string) and ensure all reads/writes use the new server-scoped key.
- Around line 134-136: The current cache key uses Python's built-in hash() which
is process-randomized; change the computed filter_hash to a deterministic
SHA-256 of the filters by serializing filters (or filters.items()) with
canonical JSON (json.dumps(..., sort_keys=True, separators=(',',':'))), then
compute hashlib.sha256(serialized.encode()).hexdigest() (optionally truncating
for length) and use that value in the return string (the same
"validated_device_{server_key}_{filter_hash}_{device_id}_{vc_part}"
construction); update any references to filter_hash in this function so all
workers generate identical keys.
- Around line 74-78: The metadata parsing around cached_at should be made
robust: wrap the datetime.fromisoformat/age calculation (the cached_at,
cache_timeout, now, age_seconds, remaining_seconds logic) in a try/except that
catches TypeError and ValueError, logs or skips the malformed entry, and
continues the loop; additionally, normalize naive datetimes by checking if the
parsed cached_at is timezone-naive and, if so, make it timezone-aware (e.g.,
attach timezone.utc) before subtracting from now to avoid TypeError from
mismatched tzinfo. Ensure the code in the loop that computes age_seconds and
remaining_seconds for a given metadata entry gracefully skips any bad or missing
cached_at instead of propagating an exception.

In `@netbox_librenms_plugin/import_utils/filters.py`:
- Around line 174-175: The cache key construction uses the raw server_key and
Python's builtin hash(), which is unstable and can collide; replace that with a
deterministic server identifier and a stable digest of the filters: derive a
stable_server_key = server_key if server_key is not None else getattr(api,
"server_key", getattr(api, "base_url", None)) and compute api_digest and
client_digest using hashlib.sha256(json.dumps(api_filters,
sort_keys=True).encode()).hexdigest() (and same for client_filters), then build
cache_key =
f"librenms_devices_import_{stable_server_key}_{api_digest}_{client_digest}";
update references to cache_key and keep from_cache logic unchanged.

In `@netbox_librenms_plugin/import_utils/permissions.py`:
- Around line 3-7: Remove the unused module-level logger by deleting the "import
logging" and the "logger = logging.getLogger(__name__)" line so only the
required import (django.core.exceptions.PermissionDenied) remains; this
eliminates the unused symbol "logger" from the module (check at top-level where
"logger" is defined).

In `@netbox_librenms_plugin/import_utils/virtual_chassis.py`:
- Around line 257-263: The code currently calls LibreNMSSettings.objects.first()
inside a hot path to compute pattern on every member-name generation; instead
resolve and cache the pattern once and reuse it. Move the settings lookup and
try/except out of the per-member code (e.g., perform it at module load or once
when starting VC processing), store the result in a module-level constant like
VC_MEMBER_NAME_PATTERN (falling back to "-M{position}" on error and logging via
logger), and update the member-name generation function to use that cached
VC_MEMBER_NAME_PATTERN rather than calling LibreNMSSettings.objects.first() each
time.
- Around line 178-179: Change the falsy check on parent_index to an explicit
None check so numeric zero isn't treated as missing: replace the condition "if
not parent_index:" with "if parent_index is None:" in the function/method where
parent_index is evaluated (look for the parent_index variable usage in
virtual_chassis-related functions in virtual_chassis.py) to ensure 0 is handled
as a valid identifier.

In `@netbox_librenms_plugin/tables/device_status.py`:
- Around line 483-497: The icon-only button lacks an accessible name; in the
buttons.append construction (the block building the <button> using btn_class,
btn_icon, btn_label, btn_title, details_url and device_id) add an aria-label
attribute when btn_label is empty — e.g., if btn_label == "" set
aria-label="{btn_title}" on the button element (but do not duplicate when
btn_label is present) so the icon-only control has an explicit accessible name.

In
`@netbox_librenms_plugin/templates/netbox_librenms_plugin/htmx/device_validation_details.html`:
- Around line 100-101: Update all anchor tags that open links in a new tab to
include rel="noopener noreferrer": find the <a> elements using target="_blank"
in this template (e.g., the anchor rendering href="{{ existing_device_url }}"
with text "{{ validation.existing_device.name }}") and add rel="noopener
noreferrer"; repeat the same change for the other target="_blank" anchors noted
in the comment (the anchors around the other URL/context occurrences mentioned)
so every new-tab link uses rel="noopener noreferrer".

In `@netbox_librenms_plugin/tests/test_import_utils.py`:
- Around line 2052-2060: The test patches dcim.models.Platform but
DeviceConflictActionView.post() resolves platforms through
netbox_librenms_plugin.utils.find_matching_platform for the "sync_platform"
action, so update the test to patch the actual resolution point: patch
netbox_librenms_plugin.utils.find_matching_platform (or the corresponding import
used by import_utils/process handling) to return the mock_platform instead of
patching dcim.models.Platform; keep existing mocks for
DeviceConflictActionView.get_validated_device_with_selections and
render_device_row, and ensure the patched find_matching_platform is used by
DeviceConflictActionView.post when exercising the sync_platform path.

In `@netbox_librenms_plugin/utils.py`:
- Around line 49-53: The write to obj.custom_field_data["librenms_id"] assumes
cf_value is int or dict and can crash on other types; update the logic around
cf_value (the block that reads obj.custom_field_data.get("librenms_id") and
writes back using server_key and device_id) to defensively handle unexpected
types by: if cf_value is int convert to {"default": cf_value}, if cf_value is
dict use it, otherwise replace with a new dict (optionally preserving a
string/iterable by storing under "legacy" or discarding) before assigning
cf_value[server_key] = device_id and writing
obj.custom_field_data["librenms_id"] = cf_value; include a warning/log when
coercion occurs to aid debugging.
- Around line 56-69: The current find_by_librenms_id only checks the nested JSON
key custom_field_data__librenms_id__{server_key} and thus misses legacy records
that stored librenms_id as a top-level key (integer) in custom_field_data;
update find_by_librenms_id to perform an OR query using django.db.models.Q to
search both f"custom_field_data__librenms_id__{server_key}" == librenms_id and
"custom_field_data__librenms_id" == librenms_id (and coerce librenms_id to
int/str if needed), import Q, and return .first() of that combined queryset so
legacy integer records are matched.

In `@netbox_librenms_plugin/views/base/cables_view.py`:
- Line 85: The current Device lookup (e.g., the expression using
Device.objects.get(**{f"custom_field_data__librenms_id__{server_key}":
remote_device_id})) only matches the JSON-per-server format and drops
compatibility with legacy integer librenms_id values; update all lookup sites
(the occurrences around the shown line and at the other locations called out) to
use a combined lookup that checks either the JSON path OR the legacy integer
form (for example using Q(...) | Q(...)) or, better, centralize this logic in a
helper and call LibreNMSAPI.get_librenms_id to resolve the mapping instead of
directly touching custom_field_data; ensure both device and interface resolution
code uses this helper/combined-Q pattern so partially migrated datasets still
match.

In `@netbox_librenms_plugin/views/imports/actions.py`:
- Around line 897-899: The POST toggle reads (use_sysname and strip_domain)
currently default to False when the keys are absent, bypassing the earlier
naming-preference fallback; change the assignment so each toggle only overrides
the existing preference when the key is present in request.POST (e.g. if
"use-sysname-toggle" in request.POST: use_sysname =
request.POST.get("use-sysname-toggle") == "on", otherwise keep the
previously-determined use_sysname), and do the same for "strip-domain-toggle";
apply this fix to the other occurrences that set these flags (the blocks that
call _determine_device_name with use_sysname/strip_domain around the other
locations referenced).
- Around line 791-800: The code currently sets device_type_synced=True even when
librenms_hardware exists but match_librenms_hardware_to_device_type() returns no
match; update the logic in the block handling librenms_hardware (and the
analogous block at lines 802-813) to set device_type_synced=False when
hw_match.get("matched") is False or when librenms_hardware is present but
hw_match lacks a device_type, and only set librenms_device_type and leave
device_type_synced True when a valid hw_match["device_type"] is found and
matches existing_device.device_type.pk; use the existing identifiers
(librenms_hardware, hw_match, match_librenms_hardware_to_device_type,
librenms_device_type, existing_device.device_type, device_type_synced) so the
check reliably flips to False whenever hardware exists but no mapping is found.
- Around line 893-900: Validate that librenms_id (from
libre_device.get("device_id")) is present and a numeric string before calling
int(librenms_id); if it's missing or not an integer, handle it gracefully (e.g.,
raise a BadRequest/return a 400, log the invalid payload, or skip the link
action) instead of letting int() raise. Update all places that call
int(librenms_id) (the code that reads librenms_id and calls
set_librenms_device_id and any similar casts around the link/update flows) to
perform the check and only call set_librenms_device_id(existing_device,
int(librenms_id), self.librenms_api.server_key) when validation passes,
otherwise return or handle the error path consistently.

In `@netbox_librenms_plugin/views/sync/interfaces.py`:
- Line 238: The call to set_librenms_device_id is persisting a None port_id and
can overwrite existing mappings; change the code around
set_librenms_device_id(interface, librenms_interface.get("port_id"),
self.librenms_api.server_key) to first read port_id =
librenms_interface.get("port_id"), then only call set_librenms_device_id when
port_id is not None/empty (and otherwise skip or log/return) so you never
persist a null librenms_id for the given interface and server key.

---

Outside diff comments:
In `@netbox_librenms_plugin/views/imports/actions.py`:
- Around line 501-507: Duplicate loops building libre_devices_cache by iterating
parsed_ids and calling fetch_device_with_cache should be consolidated: extract
the cache-building logic into a single helper (e.g.,
build_libre_devices_cache(parsed_ids, librenms_api)) or a small private method
used by both the background and synchronous branches, so both branches reuse the
same libre_devices_cache population instead of repeating the for-device_id ->
fetch_device_with_cache(...) loop; ensure the helper returns the
libre_devices_cache dict and replace the duplicated loops at the locations
around the existing libre_devices_cache usage (including the other occurrence
around lines 563-569) with calls to that helper.

ℹ️ Review info

Configuration used: Repository UI (base), Organization UI (inherited)

Review profile: ASSERTIVE

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between f06629d and 45064cb.

📒 Files selected for processing (30)
  • netbox_librenms_plugin/import_utils.py
  • netbox_librenms_plugin/import_utils/__init__.py
  • netbox_librenms_plugin/import_utils/bulk_import.py
  • netbox_librenms_plugin/import_utils/cache.py
  • netbox_librenms_plugin/import_utils/device_operations.py
  • netbox_librenms_plugin/import_utils/filters.py
  • netbox_librenms_plugin/import_utils/permissions.py
  • netbox_librenms_plugin/import_utils/virtual_chassis.py
  • netbox_librenms_plugin/import_utils/vm_operations.py
  • netbox_librenms_plugin/librenms_api.py
  • netbox_librenms_plugin/static/netbox_librenms_plugin/js/librenms_import.js
  • netbox_librenms_plugin/tables/device_status.py
  • netbox_librenms_plugin/tables/interfaces.py
  • netbox_librenms_plugin/templates/netbox_librenms_plugin/htmx/device_validation_details.html
  • netbox_librenms_plugin/templates/netbox_librenms_plugin/librenms_import.html
  • netbox_librenms_plugin/templates/netbox_librenms_plugin/librenms_sync_base.html
  • netbox_librenms_plugin/tests/test_import_utils.py
  • netbox_librenms_plugin/tests/test_librenms_api.py
  • netbox_librenms_plugin/tests/test_permissions.py
  • netbox_librenms_plugin/urls.py
  • netbox_librenms_plugin/utils.py
  • netbox_librenms_plugin/views/__init__.py
  • netbox_librenms_plugin/views/base/cables_view.py
  • netbox_librenms_plugin/views/base/ip_addresses_view.py
  • netbox_librenms_plugin/views/imports/__init__.py
  • netbox_librenms_plugin/views/imports/actions.py
  • netbox_librenms_plugin/views/object_sync/devices.py
  • netbox_librenms_plugin/views/object_sync/vms.py
  • netbox_librenms_plugin/views/sync/device_fields.py
  • netbox_librenms_plugin/views/sync/interfaces.py
💤 Files with no reviewable changes (1)
  • netbox_librenms_plugin/import_utils.py
📜 Review details
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
  • GitHub Check: test-netbox (3.14)
  • GitHub Check: test-netbox (3.13)
🧰 Additional context used
📓 Path-based instructions (6)
netbox_librenms_plugin/templates/**/*.html

📄 CodeRabbit inference engine (.github/instructions/frontend.instructions.md)

netbox_librenms_plugin/templates/**/*.html: HTMX 2.x is the primary async layer. Table row updates should return <tr hx-swap-oob="true">.
Avoid outerHTML swaps in HTMX; use OOB or targeted innerHTML swaps to keep table layout intact.
Modals use Tabler (Bootstrap-like) but without bootstrap.Modal helpers.
Modal buttons should target the htmx-modal-content element. JavaScript in librenms_import.html toggles the wrapper; do not reintroduce data-bs-toggle or duplicate modal IDs.
Keep <select class="device-role-select"> markup stable to preserve JavaScript hook-up with TomSelect decorators.
Styling assumes Tabler defaults. Do not add table-responsive wrappers as they were deliberately removed to prevent dropdown clipping.

Files:

  • netbox_librenms_plugin/templates/netbox_librenms_plugin/librenms_import.html
  • netbox_librenms_plugin/templates/netbox_librenms_plugin/htmx/device_validation_details.html
  • netbox_librenms_plugin/templates/netbox_librenms_plugin/librenms_sync_base.html
netbox_librenms_plugin/templates/netbox_librenms_plugin/**/*.html

📄 CodeRabbit inference engine (.github/instructions/frontend.instructions.md)

Templates should live in templates/netbox_librenms_plugin/. Reusable template includes should be placed under inc/ subdirectory.

Files:

  • netbox_librenms_plugin/templates/netbox_librenms_plugin/librenms_import.html
  • netbox_librenms_plugin/templates/netbox_librenms_plugin/htmx/device_validation_details.html
  • netbox_librenms_plugin/templates/netbox_librenms_plugin/librenms_sync_base.html
**/*.py

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

**/*.py: Reuse the librenms_api.py LibreNMS client instead of making new requests calls; it handles multi-server configs via LibreNMSSettings model and the servers plugin config, plus caching
Always call LibreNMSAPI.get_librenms_id to retrieve the device/VM LibreNMS mapping via the librenms_id custom field instead of touching the field directly
Matching must be exact-only for site, platform, device type, and role; do not add fuzzy matching; use the functions find_matching_site, match_librenms_hardware_to_device_type, and find_matching_platform from utils.py

Files:

  • netbox_librenms_plugin/tables/device_status.py
  • netbox_librenms_plugin/tables/interfaces.py
  • netbox_librenms_plugin/views/sync/device_fields.py
  • netbox_librenms_plugin/utils.py
  • netbox_librenms_plugin/import_utils/bulk_import.py
  • netbox_librenms_plugin/views/base/cables_view.py
  • netbox_librenms_plugin/import_utils/device_operations.py
  • netbox_librenms_plugin/import_utils/permissions.py
  • netbox_librenms_plugin/import_utils/cache.py
  • netbox_librenms_plugin/views/__init__.py
  • netbox_librenms_plugin/views/object_sync/vms.py
  • netbox_librenms_plugin/views/imports/actions.py
  • netbox_librenms_plugin/urls.py
  • netbox_librenms_plugin/views/imports/__init__.py
  • netbox_librenms_plugin/views/base/ip_addresses_view.py
  • netbox_librenms_plugin/librenms_api.py
  • netbox_librenms_plugin/import_utils/vm_operations.py
  • netbox_librenms_plugin/views/object_sync/devices.py
  • netbox_librenms_plugin/import_utils/__init__.py
  • netbox_librenms_plugin/views/sync/interfaces.py
  • netbox_librenms_plugin/import_utils/virtual_chassis.py
  • netbox_librenms_plugin/import_utils/filters.py
  • netbox_librenms_plugin/tests/test_permissions.py
  • netbox_librenms_plugin/tests/test_librenms_api.py
  • netbox_librenms_plugin/tests/test_import_utils.py
netbox_librenms_plugin/templates/netbox_librenms_plugin/htmx/**/*.html

📄 CodeRabbit inference engine (.github/instructions/frontend.instructions.md)

HTMX fragments should live in templates/netbox_librenms_plugin/htmx/. Keep server responses and HTMX targets in sync.

Files:

  • netbox_librenms_plugin/templates/netbox_librenms_plugin/htmx/device_validation_details.html
netbox_librenms_plugin/templates/netbox_librenms_plugin/*sync*.html

📄 CodeRabbit inference engine (.github/instructions/frontend.instructions.md)

Sync pages should extend librenms_sync_base.html.

Files:

  • netbox_librenms_plugin/templates/netbox_librenms_plugin/librenms_sync_base.html
**/views/imports/**

📄 CodeRabbit inference engine (.github/instructions/background-jobs.instructions.md)

**/views/imports/**: Use Job UUID (job.job_id) for RQ API endpoints: /api/core/background-tasks/{uuid}/
Use Job PK (job.pk) for database endpoints and result loading
Check rq_job.is_stopped or rq_job.is_failed flags in Redis for cancellation detection, not database status
Call /api/core/background-tasks/{uuid}/stop/ to stop RQ job in the job cancellation flow
Call plugin's sync endpoint /api/plugins/librenms_plugin/jobs/{pk}/sync-status/ to update database after stopping RQ job
Poll /api/core/background-tasks/{uuid}/ for real-time RQ status instead of polling the database
api/views.py::sync_job_status() must sync database Job status with RQ job status because NetBox worker doesn't always update DB when jobs stop before processing starts

Files:

  • netbox_librenms_plugin/views/imports/actions.py
  • netbox_librenms_plugin/views/imports/__init__.py
🧠 Learnings (25)
📓 Common learnings
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-02-15T15:38:54.189Z
Learning: Applies to views/imports/**/*.py : REST endpoints for imports live in `views/imports/actions.py` with the list view in `views/imports/list.py`, surface via `urls.py`, and emit HTMX fragments in `templates/netbox_librenms_plugin/htmx/`; keep server responses and HTMX targets in sync
📚 Learning: 2026-02-15T15:39:11.578Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/frontend.instructions.md:0-0
Timestamp: 2026-02-15T15:39:11.578Z
Learning: Applies to netbox_librenms_plugin/templates/**/*.html : Modal buttons should target the `htmx-modal-content` element. JavaScript in `librenms_import.html` toggles the wrapper; do not reintroduce `data-bs-toggle` or duplicate modal IDs.

Applied to files:

  • netbox_librenms_plugin/templates/netbox_librenms_plugin/librenms_import.html
  • netbox_librenms_plugin/tables/device_status.py
  • netbox_librenms_plugin/templates/netbox_librenms_plugin/htmx/device_validation_details.html
  • netbox_librenms_plugin/static/netbox_librenms_plugin/js/librenms_import.js
📚 Learning: 2026-02-15T15:39:11.578Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/frontend.instructions.md:0-0
Timestamp: 2026-02-15T15:39:11.578Z
Learning: Applies to netbox_librenms_plugin/templates/**/*.html : Avoid `outerHTML` swaps in HTMX; use OOB or targeted `innerHTML` swaps to keep table layout intact.

Applied to files:

  • netbox_librenms_plugin/templates/netbox_librenms_plugin/librenms_import.html
  • netbox_librenms_plugin/tables/device_status.py
  • netbox_librenms_plugin/templates/netbox_librenms_plugin/htmx/device_validation_details.html
  • netbox_librenms_plugin/templates/netbox_librenms_plugin/librenms_sync_base.html
  • netbox_librenms_plugin/static/netbox_librenms_plugin/js/librenms_import.js
📚 Learning: 2026-02-15T15:39:11.578Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/frontend.instructions.md:0-0
Timestamp: 2026-02-15T15:39:11.578Z
Learning: Applies to netbox_librenms_plugin/templates/**/*.html : HTMX 2.x is the primary async layer. Table row updates should return `<tr hx-swap-oob="true">`.

Applied to files:

  • netbox_librenms_plugin/templates/netbox_librenms_plugin/librenms_import.html
  • netbox_librenms_plugin/tables/device_status.py
  • netbox_librenms_plugin/templates/netbox_librenms_plugin/htmx/device_validation_details.html
  • netbox_librenms_plugin/templates/netbox_librenms_plugin/librenms_sync_base.html
📚 Learning: 2026-02-15T15:39:11.578Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/frontend.instructions.md:0-0
Timestamp: 2026-02-15T15:39:11.578Z
Learning: Applies to netbox_librenms_plugin/templates/netbox_librenms_plugin/htmx/**/*.html : HTMX fragments should live in `templates/netbox_librenms_plugin/htmx/`. Keep server responses and HTMX targets in sync.

Applied to files:

  • netbox_librenms_plugin/templates/netbox_librenms_plugin/librenms_import.html
  • netbox_librenms_plugin/tables/device_status.py
  • netbox_librenms_plugin/templates/netbox_librenms_plugin/htmx/device_validation_details.html
  • netbox_librenms_plugin/templates/netbox_librenms_plugin/librenms_sync_base.html
📚 Learning: 2026-02-15T15:39:11.578Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/frontend.instructions.md:0-0
Timestamp: 2026-02-15T15:39:11.578Z
Learning: Applies to netbox_librenms_plugin/templates/**/*.html : Modals use Tabler (Bootstrap-like) but without `bootstrap.Modal` helpers.

Applied to files:

  • netbox_librenms_plugin/templates/netbox_librenms_plugin/librenms_import.html
  • netbox_librenms_plugin/templates/netbox_librenms_plugin/htmx/device_validation_details.html
  • netbox_librenms_plugin/static/netbox_librenms_plugin/js/librenms_import.js
📚 Learning: 2026-02-15T15:38:54.189Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-02-15T15:38:54.189Z
Learning: Applies to views/imports/**/*.py : REST endpoints for imports live in `views/imports/actions.py` with the list view in `views/imports/list.py`, surface via `urls.py`, and emit HTMX fragments in `templates/netbox_librenms_plugin/htmx/`; keep server responses and HTMX targets in sync

Applied to files:

  • netbox_librenms_plugin/templates/netbox_librenms_plugin/librenms_import.html
  • netbox_librenms_plugin/tables/device_status.py
  • netbox_librenms_plugin/views/sync/device_fields.py
  • netbox_librenms_plugin/import_utils/bulk_import.py
  • netbox_librenms_plugin/import_utils/device_operations.py
  • netbox_librenms_plugin/views/__init__.py
  • netbox_librenms_plugin/views/imports/actions.py
  • netbox_librenms_plugin/urls.py
  • netbox_librenms_plugin/views/imports/__init__.py
  • netbox_librenms_plugin/views/base/ip_addresses_view.py
  • netbox_librenms_plugin/import_utils/vm_operations.py
  • netbox_librenms_plugin/import_utils/__init__.py
  • netbox_librenms_plugin/views/sync/interfaces.py
  • netbox_librenms_plugin/tests/test_import_utils.py
📚 Learning: 2026-02-15T15:39:11.578Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/frontend.instructions.md:0-0
Timestamp: 2026-02-15T15:39:11.578Z
Learning: Applies to netbox_librenms_plugin/templates/**/*.html : Styling assumes Tabler defaults. Do not add `table-responsive` wrappers as they were deliberately removed to prevent dropdown clipping.

Applied to files:

  • netbox_librenms_plugin/templates/netbox_librenms_plugin/librenms_import.html
  • netbox_librenms_plugin/templates/netbox_librenms_plugin/htmx/device_validation_details.html
  • netbox_librenms_plugin/templates/netbox_librenms_plugin/librenms_sync_base.html
📚 Learning: 2026-02-15T15:39:11.578Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/frontend.instructions.md:0-0
Timestamp: 2026-02-15T15:39:11.578Z
Learning: Applies to netbox_librenms_plugin/templates/**/*.html : Keep `<select class="device-role-select">` markup stable to preserve JavaScript hook-up with TomSelect decorators.

Applied to files:

  • netbox_librenms_plugin/templates/netbox_librenms_plugin/librenms_import.html
  • netbox_librenms_plugin/tables/device_status.py
  • netbox_librenms_plugin/templates/netbox_librenms_plugin/htmx/device_validation_details.html
  • netbox_librenms_plugin/templates/netbox_librenms_plugin/librenms_sync_base.html
📚 Learning: 2026-02-15T15:39:11.578Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/frontend.instructions.md:0-0
Timestamp: 2026-02-15T15:39:11.578Z
Learning: Applies to netbox_librenms_plugin/templates/netbox_librenms_plugin/*sync*.html : Sync pages should extend `librenms_sync_base.html`.

Applied to files:

  • netbox_librenms_plugin/templates/netbox_librenms_plugin/librenms_import.html
  • netbox_librenms_plugin/templates/netbox_librenms_plugin/htmx/device_validation_details.html
  • netbox_librenms_plugin/views/sync/device_fields.py
  • netbox_librenms_plugin/views/__init__.py
  • netbox_librenms_plugin/templates/netbox_librenms_plugin/librenms_sync_base.html
  • netbox_librenms_plugin/views/sync/interfaces.py
📚 Learning: 2026-02-15T15:38:54.189Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-02-15T15:38:54.189Z
Learning: Applies to tables/**/*.py : Prefer updating the table renderer in `tables/*.py` rather than templates when changing row actions, since tables emit HTMX-enabled columns and buttons

Applied to files:

  • netbox_librenms_plugin/tables/device_status.py
📚 Learning: 2026-02-15T15:39:11.578Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/frontend.instructions.md:0-0
Timestamp: 2026-02-15T15:39:11.578Z
Learning: Tables drive most UIs via `tables/*.py` renderers that emit HTMX-enabled columns and buttons. Prefer updating the table renderer in Python rather than templates when changing row actions.

Applied to files:

  • netbox_librenms_plugin/tables/device_status.py
📚 Learning: 2026-02-15T15:38:54.189Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-02-15T15:38:54.189Z
Learning: Applies to templates/**/*.html : Reuse template includes under `templates/netbox_librenms_plugin/inc/` and ensure sync pages extend `librenms_sync_base.html`

Applied to files:

  • netbox_librenms_plugin/templates/netbox_librenms_plugin/htmx/device_validation_details.html
  • netbox_librenms_plugin/templates/netbox_librenms_plugin/librenms_sync_base.html
📚 Learning: 2026-02-15T15:38:54.189Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-02-15T15:38:54.189Z
Learning: Follow sync pipeline flow: fetch LibreNMS data via `librenms_api.py`, cache it using `CacheMixin`, build comparison tables in `tables/`, and render HTMX fragments in `templates/netbox_librenms_plugin/htmx/`

Applied to files:

  • netbox_librenms_plugin/templates/netbox_librenms_plugin/htmx/device_validation_details.html
  • netbox_librenms_plugin/templates/netbox_librenms_plugin/librenms_sync_base.html
  • netbox_librenms_plugin/views/imports/actions.py
  • netbox_librenms_plugin/views/sync/interfaces.py
📚 Learning: 2026-02-15T15:38:54.189Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-02-15T15:38:54.189Z
Learning: Applies to **/*.py : Always call `LibreNMSAPI.get_librenms_id` to retrieve the device/VM LibreNMS mapping via the `librenms_id` custom field instead of touching the field directly

Applied to files:

  • netbox_librenms_plugin/tables/interfaces.py
  • netbox_librenms_plugin/utils.py
  • netbox_librenms_plugin/views/base/cables_view.py
  • netbox_librenms_plugin/views/base/ip_addresses_view.py
  • netbox_librenms_plugin/librenms_api.py
  • netbox_librenms_plugin/views/sync/interfaces.py
📚 Learning: 2026-02-15T15:39:20.744Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/testing.instructions.md:0-0
Timestamp: 2026-02-15T15:39:20.744Z
Learning: Applies to tests/test_import_utils.py : Cache key tests must patch `get_validated_device_cache_key` from `import_utils.py`; never hardcode key formats like `job_123_device_1`

Applied to files:

  • netbox_librenms_plugin/import_utils/bulk_import.py
  • netbox_librenms_plugin/import_utils/cache.py
  • netbox_librenms_plugin/tests/test_permissions.py
  • netbox_librenms_plugin/tests/test_import_utils.py
📚 Learning: 2026-02-15T15:39:20.744Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/testing.instructions.md:0-0
Timestamp: 2026-02-15T15:39:20.744Z
Learning: Applies to tests/**/*.py : Patch deferred/inline imports at their source module (e.g., `netbox_librenms_plugin.import_utils.process_device_filters`), not the consuming module

Applied to files:

  • netbox_librenms_plugin/import_utils/device_operations.py
  • netbox_librenms_plugin/views/imports/actions.py
  • netbox_librenms_plugin/views/imports/__init__.py
  • netbox_librenms_plugin/import_utils/vm_operations.py
  • netbox_librenms_plugin/import_utils/__init__.py
  • netbox_librenms_plugin/import_utils/filters.py
  • netbox_librenms_plugin/tests/test_permissions.py
  • netbox_librenms_plugin/tests/test_import_utils.py
📚 Learning: 2026-02-15T15:39:20.744Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/testing.instructions.md:0-0
Timestamp: 2026-02-15T15:39:20.744Z
Learning: Applies to tests/test_background_jobs.py : Patch `cache` where imported: `netbox_librenms_plugin.views.imports.list.cache`, not `django.core.cache.cache`

Applied to files:

  • netbox_librenms_plugin/import_utils/cache.py
  • netbox_librenms_plugin/tests/test_permissions.py
  • netbox_librenms_plugin/tests/test_import_utils.py
📚 Learning: 2026-02-15T15:38:54.189Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-02-15T15:38:54.189Z
Learning: Applies to {navigation.py,urls.py,api/**} : Respect NetBox plugin APIs in `navigation.py`, `urls.py`, and `api/` directories

Applied to files:

  • netbox_librenms_plugin/views/__init__.py
  • netbox_librenms_plugin/urls.py
📚 Learning: 2026-02-15T15:38:54.189Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-02-15T15:38:54.189Z
Learning: Applies to views/**/*.py : Views should follow layered structure: extend the closest base class from `views/base/` and compose mixins like `LibreNMSAPIMixin` and `CacheMixin`

Applied to files:

  • netbox_librenms_plugin/views/__init__.py
📚 Learning: 2026-02-15T15:38:54.189Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-02-15T15:38:54.189Z
Learning: Applies to **/*.py : Reuse the `librenms_api.py` LibreNMS client instead of making new `requests` calls; it handles multi-server configs via `LibreNMSSettings` model and the `servers` plugin config, plus caching

Applied to files:

  • netbox_librenms_plugin/librenms_api.py
📚 Learning: 2026-02-15T15:39:03.405Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/background-jobs.instructions.md:0-0
Timestamp: 2026-02-15T15:39:03.405Z
Learning: Update modal messages based on RQ status values: 'Job queued...', 'Processing...', 'Job completed!' with explicit handling for all RQ status values to avoid infinite polling

Applied to files:

  • netbox_librenms_plugin/static/netbox_librenms_plugin/js/librenms_import.js
📚 Learning: 2026-02-15T15:39:20.744Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/testing.instructions.md:0-0
Timestamp: 2026-02-15T15:39:20.744Z
Learning: Applies to tests/**/*.py : Test coverage mapping: `librenms_api.py` → `test_librenms_api.py`, `import_utils.py`/`import_validation_helpers.py`/`utils.py` → `test_import_utils.py`/`test_import_validation_helpers.py`/`test_utils.py`, `jobs.py`/`views/imports/list.py` → `test_background_jobs.py`

Applied to files:

  • netbox_librenms_plugin/tests/test_permissions.py
  • netbox_librenms_plugin/tests/test_import_utils.py
📚 Learning: 2026-02-15T15:39:20.744Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/testing.instructions.md:0-0
Timestamp: 2026-02-15T15:39:20.744Z
Learning: Applies to tests/**/*.py : Mock NetBox models (Device, Job, User) with `MagicMock()` instead of creating real instances

Applied to files:

  • netbox_librenms_plugin/tests/test_librenms_api.py
  • netbox_librenms_plugin/tests/test_import_utils.py
📚 Learning: 2026-02-15T15:38:54.189Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-02-15T15:38:54.189Z
Learning: Applies to **/*.py : Matching must be exact-only for site, platform, device type, and role; do not add fuzzy matching; use the functions `find_matching_site`, `match_librenms_hardware_to_device_type`, and `find_matching_platform` from `utils.py`

Applied to files:

  • netbox_librenms_plugin/tests/test_import_utils.py
🧬 Code graph analysis (15)
netbox_librenms_plugin/tables/device_status.py (1)
netbox_librenms_plugin/views/imports/actions.py (2)
  • get (707-727)
  • get (733-757)
netbox_librenms_plugin/tables/interfaces.py (1)
netbox_librenms_plugin/utils.py (2)
  • get_librenms_device_id (12-35)
  • get_interface_name_field (221-249)
netbox_librenms_plugin/views/sync/device_fields.py (1)
netbox_librenms_plugin/librenms_api.py (2)
  • get_librenms_id (173-231)
  • get_device_info (314-337)
netbox_librenms_plugin/utils.py (1)
netbox_librenms_plugin/views/base/librenms_sync_view.py (1)
  • get (25-46)
netbox_librenms_plugin/import_utils/bulk_import.py (5)
netbox_librenms_plugin/librenms_api.py (2)
  • LibreNMSAPI (15-1039)
  • get_device_info (314-337)
netbox_librenms_plugin/import_utils/cache.py (3)
  • get_cache_metadata_key (10-24)
  • get_import_device_cache_key (139-158)
  • get_validated_device_cache_key (112-136)
netbox_librenms_plugin/import_utils/filters.py (1)
  • get_librenms_devices_for_import (43-213)
netbox_librenms_plugin/import_utils/permissions.py (1)
  • require_permissions (31-48)
netbox_librenms_plugin/import_utils/virtual_chassis.py (1)
  • create_virtual_chassis_with_members (310-440)
netbox_librenms_plugin/views/base/cables_view.py (2)
netbox_librenms_plugin/views/mixins.py (1)
  • librenms_api (217-231)
netbox_librenms_plugin/utils.py (1)
  • get_virtual_chassis_member (109-133)
netbox_librenms_plugin/views/object_sync/vms.py (2)
netbox_librenms_plugin/tables/interfaces.py (1)
  • LibreNMSVMInterfaceTable (592-616)
netbox_librenms_plugin/views/mixins.py (1)
  • librenms_api (217-231)
netbox_librenms_plugin/urls.py (2)
netbox_librenms_plugin/views/imports/actions.py (1)
  • DeviceConflictActionView (855-1036)
netbox_librenms_plugin/views/sync/device_fields.py (1)
  • UpdateDeviceNameView (12-56)
netbox_librenms_plugin/views/imports/__init__.py (1)
netbox_librenms_plugin/views/imports/actions.py (1)
  • DeviceConflictActionView (855-1036)
netbox_librenms_plugin/views/base/ip_addresses_view.py (2)
netbox_librenms_plugin/utils.py (1)
  • get_librenms_device_id (12-35)
netbox_librenms_plugin/views/mixins.py (1)
  • librenms_api (217-231)
netbox_librenms_plugin/librenms_api.py (1)
netbox_librenms_plugin/utils.py (2)
  • get_librenms_device_id (12-35)
  • set_librenms_device_id (38-53)
netbox_librenms_plugin/views/object_sync/devices.py (2)
netbox_librenms_plugin/views/mixins.py (1)
  • librenms_api (217-231)
netbox_librenms_plugin/tables/interfaces.py (2)
  • VCInterfaceTable (510-589)
  • LibreNMSInterfaceTable (25-507)
netbox_librenms_plugin/views/sync/interfaces.py (2)
netbox_librenms_plugin/utils.py (1)
  • set_librenms_device_id (38-53)
netbox_librenms_plugin/views/mixins.py (1)
  • librenms_api (217-231)
netbox_librenms_plugin/import_utils/virtual_chassis.py (2)
netbox_librenms_plugin/librenms_api.py (2)
  • get_device_info (314-337)
  • get_inventory_filtered (726-803)
netbox_librenms_plugin/models.py (1)
  • LibreNMSSettings (7-48)
netbox_librenms_plugin/tests/test_import_utils.py (3)
netbox_librenms_plugin/import_utils/device_operations.py (1)
  • validate_device_for_import (79-579)
netbox_librenms_plugin/views/imports/actions.py (10)
  • get (707-727)
  • get (733-757)
  • DeviceConflictActionView (855-1036)
  • post (248-397)
  • post (424-701)
  • post (819-826)
  • post (832-839)
  • post (845-852)
  • post (858-1036)
  • post (1048-1062)
netbox_librenms_plugin/views/base/librenms_sync_view.py (1)
  • get (25-46)
🔇 Additional comments (20)
netbox_librenms_plugin/tables/device_status.py (1)

447-488: Good contextual action-state mapping for existing objects.

The added NetBox link plus conflict/details state derivation is clear and keeps the existing HTMX modal workflow consistent.

netbox_librenms_plugin/views/object_sync/vms.py (1)

48-50: Looks good: server_key propagation is correctly wired.

Passing self.librenms_api.server_key in Line 49 keeps VM interface table lookups aligned with the active LibreNMS server context.

netbox_librenms_plugin/tests/test_permissions.py (1)

636-637: Patch targets are correctly updated to source modules.

The new patch paths are consistent with the refactor split and should keep mocks attached to the symbols actually used at runtime.

Based on learnings: Patch deferred/inline imports at their source module (e.g., netbox_librenms_plugin.import_utils.process_device_filters), not the consuming module.

Also applies to: 661-662, 685-685, 706-706, 732-732, 751-751, 876-877

netbox_librenms_plugin/tests/test_librenms_api.py (1)

646-646: Good test adjustment for body-driven duplicate detection.

Using HTTP 200 on Line 646 with an error payload reflects the real API behavior this client path is expected to handle.

netbox_librenms_plugin/librenms_api.py (1)

193-196: Nice refactor: centralized librenms_id read/write handling.

Routing both retrieval and storage through shared utils reduces format drift and keeps per-server behavior consistent.

Also applies to: 259-262

netbox_librenms_plugin/views/base/ip_addresses_view.py (1)

14-14: Good update: per-server interface matching now uses shared helper.

The server_key-aware lookup in Lines 107-111 is consistent with the new JSON librenms_id format and keeps IP enrichment aligned with active server context.

Also applies to: 107-112

netbox_librenms_plugin/tables/interfaces.py (2)

50-55: LGTM on server_key propagation and librenms_id lookup.

The changes correctly add server_key parameter to LibreNMSInterfaceTable.__init__ and use get_librenms_device_id(netbox_interface, self.server_key) for per-server librenms_id resolution. This aligns with the coding guidelines to use the centralized ID retrieval helper.

Also applies to: 365-365


523-527: LGTM - server_key propagates via **kwargs.

The VCInterfaceTable.__init__ signature doesn't explicitly list server_key, but it's passed through **kwargs to the parent class, which correctly accepts and stores it.

netbox_librenms_plugin/views/object_sync/devices.py (1)

82-98: LGTM - server_key correctly propagated to interface tables.

The view retrieves server_key from self.librenms_api.server_key and passes it to both VCInterfaceTable and LibreNMSInterfaceTable constructors, enabling per-server librenms_id resolution.

netbox_librenms_plugin/templates/netbox_librenms_plugin/librenms_import.html (1)

402-402: LGTM - modal width increased for richer content.

The change from modal-lg to modal-xl accommodates the expanded validation details and conflict resolution UI introduced in this PR. The HTMX patterns remain correct with hx-target="#htmx-modal-content" and hx-swap="innerHTML".

netbox_librenms_plugin/views/imports/__init__.py (1)

7-7: LGTM - DeviceConflictActionView properly exported.

The new view is correctly imported from actions.py and added to __all__, following the established pattern for import workflow views.

Also applies to: 20-20

netbox_librenms_plugin/templates/netbox_librenms_plugin/librenms_sync_base.html (1)

245-271: LGTM - Name sync row follows existing patterns.

The new Name row correctly implements:

  • Conditional rendering when sysName differs from object.name
  • Standard form POST with CSRF protection (consistent with other field sync buttons)
  • Checkmark display when names already match
netbox_librenms_plugin/views/sync/device_fields.py (1)

12-56: LGTM - UpdateDeviceNameView follows established patterns.

The implementation correctly:

  • Uses self.librenms_api.get_librenms_id() per coding guidelines
  • Reuses the LibreNMS client via self.librenms_api.get_device_info()
  • Follows the same permission, validation, and error-handling patterns as sibling views (UpdateDeviceSerialView, etc.)
netbox_librenms_plugin/import_utils/permissions.py (1)

10-48: LGTM - Clean permission helper design.

The two-tier approach (check_user_permissions for testable checks, require_permissions for raise-on-failure) provides good flexibility. Error messages are descriptive and include the specific missing permissions.

netbox_librenms_plugin/urls.py (1)

13-13: LGTM - URL routes properly added.

The new endpoints follow established conventions:

  • devices/<int:pk>/update-name/ grouped with other device field update routes
  • device-import/conflict-action/<str:device_id>/ grouped with other import action routes

Import statements are correctly placed alphabetically.

Also applies to: 47-47, 193-198, 269-273

netbox_librenms_plugin/static/netbox_librenms_plugin/js/librenms_import.js (1)

323-330: Good consolidation of modal teardown paths.

Routing all terminal states through hideModal(filterModal) is a solid cleanup and avoids divergent close behavior across job outcomes.

Also applies to: 351-352, 416-418, 459-461, 477-479, 491-493, 507-509, 589-591, 600-602, 607-609, 618-620, 1018-1020

netbox_librenms_plugin/views/__init__.py (1)

4-6: Re-export intent is clearly documented.

The docstring plus targeted # noqa: F401 usage makes this module’s public surface explicit for urls.py/import consumers.

Also applies to: 14-65

netbox_librenms_plugin/import_utils/vm_operations.py (1)

146-168: Good reuse of shared import utilities in VM flow.

Using fetch_device_with_cache, validate_device_for_import, and _determine_device_name keeps VM imports aligned with device import behavior and avoids duplicated logic.

Also applies to: 201-214

netbox_librenms_plugin/import_utils/device_operations.py (1)

418-431: Matching path is correctly centralized through shared utilities.

Site/platform/device-type matching goes through the expected helper functions, which keeps import validation behavior consistent.

As per coding guidelines: "Matching must be exact-only for site, platform, device type, and role; do not add fuzzy matching; use the functions find_matching_site, match_librenms_hardware_to_device_type, and find_matching_platform from utils.py."

Also applies to: 498-499

netbox_librenms_plugin/import_utils/__init__.py (1)

17-52: Re-export surface looks clean and backward-compatible.

The grouping is clear and preserves existing from netbox_librenms_plugin.import_utils import X call sites.

Comment on lines +380 to +383
except (BrokenPipeError, ConnectionError, IOError) as e:
if request:
logger.info(f"Client disconnected during VC prefetch: {e}")
return []

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

process_device_filters() breaks its return contract on early exits.

When return_cache_status=True, these branches return [] instead of (devices, from_cache). Callers expecting tuple unpacking can fail during cancellation/disconnect paths.

💡 Proposed fix
 def process_device_filters(
@@
 ) -> List[dict] | tuple[List[dict], bool]:
@@
-    # Validate each device
+    def _empty_result():
+        return ([], from_cache) if return_cache_status else []
+
+    # Validate each device
@@
-                return []
+                return _empty_result()
@@
-                return []
+                return _empty_result()
@@
-                        return []
+                        return _empty_result()
@@
-                        return []
+                        return _empty_result()
@@
-                    return []
+                    return _empty_result()
@@
-                return []
+                return _empty_result()

Also applies to: 401-404, 407-409, 428-433, 436-438, 444-446, 490-493

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@netbox_librenms_plugin/import_utils/bulk_import.py` around lines 380 - 383,
process_device_filters currently returns [] on several early-exit
exception/interrupt paths which breaks callers that expect the (devices,
from_cache) tuple when return_cache_status=True; update every early-return
branch within process_device_filters (the except/if request blocks that log
client disconnects and cancellations) to return an appropriate two-tuple: an
empty devices list and the from_cache boolean (e.g., ([], False) or ([],
from_cache) depending on context) so callers always receive (devices,
from_cache); ensure the change covers all similar branches referencing request
and logger inside process_device_filters.

Comment thread netbox_librenms_plugin/import_utils/cache.py Outdated
Comment thread netbox_librenms_plugin/import_utils/cache.py Outdated
Comment thread netbox_librenms_plugin/import_utils/cache.py Outdated
Comment thread netbox_librenms_plugin/import_utils/filters.py Outdated
Comment thread netbox_librenms_plugin/views/base/cables_view.py Outdated
Comment on lines +791 to +800
device_type_synced = True
librenms_device_type = None
if librenms_hardware and librenms_hardware != "-":
from netbox_librenms_plugin.utils import match_librenms_hardware_to_device_type

hw_match = match_librenms_hardware_to_device_type(librenms_hardware)
if hw_match.get("matched"):
librenms_device_type = hw_match["device_type"]
if not existing_device.device_type or existing_device.device_type.pk != librenms_device_type.pk:
device_type_synced = False

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

device_type_synced can be incorrectly reported as synced.

If LibreNMS hardware is present but no matching NetBox device type is found, device_type_synced remains True. That can make all_synced true for a non-synced record.

💡 Proposed fix
-        device_type_synced = True
+        device_type_synced = librenms_hardware in ("", "-")
         librenms_device_type = None
         if librenms_hardware and librenms_hardware != "-":
@@
             hw_match = match_librenms_hardware_to_device_type(librenms_hardware)
             if hw_match.get("matched"):
                 librenms_device_type = hw_match["device_type"]
-                if not existing_device.device_type or existing_device.device_type.pk != librenms_device_type.pk:
-                    device_type_synced = False
+                device_type_synced = bool(
+                    existing_device.device_type and existing_device.device_type.pk == librenms_device_type.pk
+                )
+            else:
+                device_type_synced = False

Also applies to: 802-813

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@netbox_librenms_plugin/views/imports/actions.py` around lines 791 - 800, The
code currently sets device_type_synced=True even when librenms_hardware exists
but match_librenms_hardware_to_device_type() returns no match; update the logic
in the block handling librenms_hardware (and the analogous block at lines
802-813) to set device_type_synced=False when hw_match.get("matched") is False
or when librenms_hardware is present but hw_match lacks a device_type, and only
set librenms_device_type and leave device_type_synced True when a valid
hw_match["device_type"] is found and matches existing_device.device_type.pk; use
the existing identifiers (librenms_hardware, hw_match,
match_librenms_hardware_to_device_type, librenms_device_type,
existing_device.device_type, device_type_synced) so the check reliably flips to
False whenever hardware exists but no mapping is found.

Comment thread netbox_librenms_plugin/views/imports/actions.py Outdated
Comment thread netbox_librenms_plugin/views/imports/actions.py Outdated
Comment thread netbox_librenms_plugin/views/sync/interfaces.py Outdated
@marcinpsk
marcinpsk force-pushed the refactor/librenms_id branch from 7d4a2af to 585dc7d Compare February 28, 2026 21:57
…ort_utils package

Port 4 commits from feat/serial-matching-and-conflict-resolution:
- 097ea42: Fix validation readiness (VMs require cluster.found), hostname
  match always sets has_actions, guard platform sync forms for Device only,
  use _resolve_naming_preferences in BulkImportConfirmView, add else branch
  for unmatched hardware in _build_sync_info, reuse resolved_name in
  DeviceConflictActionView actions, fix test patch targets
- c0d7bfc: Block import when issues present (can_import/is_ready require
  no issues), modal title uses resolved_name, dedup LibreNMSSettings DB
  query in _resolve_naming_preferences, guard hostname form for VMs
- 13bafe1: Compare existing device/VM name against resolved_name instead
  of raw sysName, scope mismatch force gate to link/update/update_serial/
  update_type only, add librenms_id collision check before linking
- dad0d22: Update device_role dict in-place during refresh to preserve
  schema keys (available_roles etc.)
The librenms_id conflict check added in the port requires
Device.objects.filter().exclude().first() to be mocked for all
tests using link/update/update_serial actions.
- utils.py set_librenms_device_id: defensively handle unexpected types
  (non-int, non-dict) by resetting to empty dict with a warning log
- utils.py find_by_librenms_id: add OR query to also match legacy records
  where librenms_id is stored as a bare integer
- cables_view.py: update all 6 librenms_id lookup sites to use Q()|Q()
  so legacy integer records are matched alongside new JSON format
- actions.py: validate librenms_id (device_id) before int() cast and
  return 400 if missing/non-numeric; use pre-validated int throughout
  the link/update/update_serial action blocks
- interfaces.py: guard against None port_id before set_librenms_device_id
  to avoid clobbering an existing server-key mapping
find_by_librenms_id now calls .filter(Q(...)|Q(...)) passing a positional
Q object. The three TestSerialNumberMatching mocks only accepted **kwargs,
causing TypeError which was silently caught by the except block in
device_operations.py, making existing_device None.

Update all three device_filter side_effects to accept *args and check
str(arg) for 'librenms_id' so Q-based lookups are detected correctly.
@marcinpsk
marcinpsk force-pushed the refactor/librenms_id branch from 585dc7d to 16dfb65 Compare March 1, 2026 15:37
- Add migrate_legacy_librenms_id(obj, server_key) helper in utils.py
  Converts a bare-integer librenms_id to {server_key: int_value}.
  Returns True if migration happened, False if already JSON or absent.
  Does not call save() — caller is responsible.

- Detect legacy int format in validate_device_for_import()
  When find_by_librenms_id matches a Device or VM whose librenms_id CF
  is still a bare integer, set result['librenms_id_needs_migration']=True
  so the import page can surface a migration action.

- Add 'migrate_librenms_id' action in DeviceConflictActionView
  Verifies CF is still an int, requires serial_confirmed or force checkbox,
  calls migrate_legacy_librenms_id() + save().

- Fix collision check to use find_by_librenms_id instead of raw queryset
  The old Device.objects.filter(custom_field_data__librenms_id=int(...))
  only matched the legacy integer format; the new helper matches both
  the JSON dict format and the legacy format.

- Show 'Legacy ID format' badge + 'Migrate ID format' button in template
  Appears in the existing_match_type=='librenms_id' section when
  librenms_id_needs_migration is set. Button is disabled (requires force
  checkbox) when serial not confirmed.

- Add TestLegacyLibreNMSIdMigration test class (6 tests)
- Update TestDeviceConflictActionView mocks for new filter().first() chain

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 4

♻️ Duplicate comments (3)
netbox_librenms_plugin/import_utils/bulk_import.py (1)

383-387: ⚠️ Potential issue | 🟠 Major

Keep process_device_filters() return shape consistent on early exits.

When return_cache_status=True, these branches still return [] instead of (devices, from_cache), which can break tuple-unpack callers.

🔧 Proposed fix
 def process_device_filters(
@@
 ) -> List[dict] | tuple[List[dict], bool]:
@@
     libre_devices, from_cache = get_librenms_devices_for_import(
         api,
         filters=filters,
         force_refresh=clear_cache,
         return_cache_status=True,
     )
+
+    def _empty_result():
+        return ([], from_cache) if return_cache_status else []
@@
         except (BrokenPipeError, ConnectionError, IOError) as e:
             if request:
                 logger.info(f"Client disconnected during VC prefetch: {e}")
-                return []
+                return _empty_result()
             raise
@@
             if rq_job.is_failed or rq_job.is_stopped:
                 job.logger.warning("Job was already stopped before validation started")
-                return []
+                return _empty_result()
@@
             if job.job.status == JobStatusChoices.STATUS_FAILED:
                 job.logger.warning("Job was stopped before validation started")
-                return []
+                return _empty_result()
@@
                     if rq_job.is_failed or rq_job.is_stopped:
                         job.logger.info(
                             f"Job stopped at device {idx}/{total} (RQ status: {rq_job.get_status()}). Exiting gracefully."
                         )
-                        return []
+                        return _empty_result()
@@
                     if job.job.status == JobStatusChoices.STATUS_FAILED:
                         job.logger.info(f"Job stopped at device {idx}/{total}. Exiting gracefully.")
-                        return []
+                        return _empty_result()
@@
                 except (BrokenPipeError, ConnectionError, IOError):
                     logger.info(f"Client disconnected during validation at device {idx}")
-                    return []
+                    return _empty_result()
@@
         except (BrokenPipeError, ConnectionError, IOError) as e:
             if request:
                 logger.info(f"Client disconnected during device validation: {e}")
-                return []
+                return _empty_result()
             raise

Also applies to: 404-407, 410-413, 431-436, 439-442, 447-450, 493-497

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@netbox_librenms_plugin/import_utils/bulk_import.py` around lines 383 - 387,
The early-exit branches in process_device_filters are returning [] on
exceptions/early exits which breaks callers expecting a (devices, from_cache)
tuple when return_cache_status=True; update each early return (including the
except blocks and other early-return spots noted) to inspect the
return_cache_status parameter and return either [] (when False) or ([], False)
(or the appropriate from_cache value) so the return shape stays consistent for
callers that unpack the tuple.
netbox_librenms_plugin/tables/device_status.py (1)

483-497: ⚠️ Potential issue | 🟡 Minor

Add aria-label for accessibility when button is icon-only.

When btn_label is empty (Line 485), the button becomes icon-only. The title attribute alone is not a reliable accessible name; screen readers need aria-label.

♿ Proposed fix
             buttons.append(
                 f'<button type="button" '
                 f'class="btn btn-sm {btn_class}" '
                 f'hx-get="{details_url}" '
                 f'hx-include="[name=cluster_{device_id}], [name=role_{device_id}], [name=rack_{device_id}]" '
                 f'hx-target="#htmx-modal-content" '
                 f'hx-swap="innerHTML" '
-                f'title="{btn_title}">'
+                f'title="{btn_title}" '
+                f'aria-label="{btn_title}">'
                 f'<i class="mdi {btn_icon}"></i>{btn_label}</button>'
             )
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@netbox_librenms_plugin/tables/device_status.py` around lines 483 - 497, In
the buttons.append block in device_status.py where btn_label may be empty
(icon-only button), add an aria-label attribute when btn_label == "" so the
button has an accessible name; use btn_title (or a localized equivalent) as the
aria-label and include it in the rendered button markup (referencing variables
btn_label, btn_title, details_url, device_id and the buttons.append call).
netbox_librenms_plugin/templates/netbox_librenms_plugin/htmx/device_validation_details.html (1)

100-102: ⚠️ Potential issue | 🟡 Minor

Add rel="noopener noreferrer" to all target="_blank" links.

Multiple anchor tags open links in new tabs without the security attribute, which can allow reverse-tabnabbing attacks.

Affected locations:

  • Line 100: Name cell link
  • Line 422: Hostname match alert link
  • Line 482: Serial match alert link
  • Line 520: IP match alert link
  • Line 529: Exists alert link
  • Lines 571-584: Footer "View in NetBox" and "Full Sync Page" links
🔒 Example fix pattern
-<a href="{{ existing_device_url }}" target="_blank">
+<a href="{{ existing_device_url }}" target="_blank" rel="noopener noreferrer">

Apply this pattern to all target="_blank" anchors in the template.

Also applies to: 420-424, 480-484, 518-522, 527-530, 571-584

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@netbox_librenms_plugin/templates/netbox_librenms_plugin/htmx/device_validation_details.html`
around lines 100 - 102, Update every anchor element that uses target="_blank" in
the template to include rel="noopener noreferrer"; specifically add this
attribute to the link surrounding {{ validation.existing_device.name }}
(href="{{ existing_device_url }}"), to the alert links for
hostname/serial/IP/existence (the anchors referenced near the
Hostname/Serial/IP/Exists alert blocks), and to the footer links ("View in
NetBox" and "Full Sync Page") so all external-tab opens use rel="noopener
noreferrer". Ensure each anchor tag that has target="_blank" is updated
consistently to include rel="noopener noreferrer".
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@netbox_librenms_plugin/import_utils/device_operations.py`:
- Around line 633-656: The code uses the literal "default" when server_key is
None, causing cross-server mismatches; change to derive an effective_server_key
from the LibreNMSAPI instance and use it everywhere instead of hardcoding
"default". Specifically, after creating api = LibreNMSAPI(server_key=server_key)
determine effective_server_key = server_key or getattr(api, "server_key", None)
or "default" and pass effective_server_key into validate_device_for_import (call
site in validate_device_for_import(..., server_key=...)) and use the same
effective_server_key when persisting or constructing librenms_id later (the code
that currently uses the string "default"), ensuring all references consistently
use the resolved value.
- Around line 438-457: The code overwrites result["device_type"] with dt_match
which removes the expected schema (keys like "found") when dt_match["matched"]
is False, causing later readiness checks to fail; instead, preserve the
result["device_type"] schema and set/rename keys explicitly: always ensure
result["device_type"]["found"] is set (use dt_match["matched"]), set
result["device_type"]["device_type"] and result["device_type"]["match_type"]
when present, and when unmatched populate result["device_type"]["suggestions"]
using DeviceType.objects.all()[:10] rather than replacing the whole dict; apply
the same change to the similar block referenced around lines 568-573 so
unmatched hardware keeps the expected keys.

In `@netbox_librenms_plugin/tests/test_import_utils.py`:
- Around line 1765-1774: The helper _create_view currently constructs the view
via object.__new__(DeviceConflictActionView), bypassing __init__; replace this
with a real instantiation (call DeviceConflictActionView()) and then attach the
mocked dependencies (set view._librenms_api with MagicMock and configure
view.request and user permissions) or, if the view __init__ requires external
services, patch/mock those services before calling DeviceConflictActionView() so
the real initializer runs; ensure you still set view._librenms_api.server_key
and view.request.user.has_perm as in the original test.
- Around line 2364-2395: The _setup_no_existing helper currently indexes into
the variadic mocks tuple (mocks[-1], mocks[-2], etc.), which is fragile; change
it to explicitly unpack the mocks tuple into named variables (e.g., mock_vm,
mock_device, mock_find_site, mock_find_platform, mock_match_type, mock_role,
mock_rack, mock_site_model) in the documented order that matches the `@patch`
decorators, then use those named variables where the code currently uses
mocks[-N]; alternatively refactor _setup_no_existing to accept named keyword
arguments matching the patched target names so callers/passengers can pass mocks
by name instead of positional indexing.

---

Duplicate comments:
In `@netbox_librenms_plugin/import_utils/bulk_import.py`:
- Around line 383-387: The early-exit branches in process_device_filters are
returning [] on exceptions/early exits which breaks callers expecting a
(devices, from_cache) tuple when return_cache_status=True; update each early
return (including the except blocks and other early-return spots noted) to
inspect the return_cache_status parameter and return either [] (when False) or
([], False) (or the appropriate from_cache value) so the return shape stays
consistent for callers that unpack the tuple.

In `@netbox_librenms_plugin/tables/device_status.py`:
- Around line 483-497: In the buttons.append block in device_status.py where
btn_label may be empty (icon-only button), add an aria-label attribute when
btn_label == "" so the button has an accessible name; use btn_title (or a
localized equivalent) as the aria-label and include it in the rendered button
markup (referencing variables btn_label, btn_title, details_url, device_id and
the buttons.append call).

In
`@netbox_librenms_plugin/templates/netbox_librenms_plugin/htmx/device_validation_details.html`:
- Around line 100-102: Update every anchor element that uses target="_blank" in
the template to include rel="noopener noreferrer"; specifically add this
attribute to the link surrounding {{ validation.existing_device.name }}
(href="{{ existing_device_url }}"), to the alert links for
hostname/serial/IP/existence (the anchors referenced near the
Hostname/Serial/IP/Exists alert blocks), and to the footer links ("View in
NetBox" and "Full Sync Page") so all external-tab opens use rel="noopener
noreferrer". Ensure each anchor tag that has target="_blank" is updated
consistently to include rel="noopener noreferrer".

ℹ️ Review info

Configuration used: Repository UI (base), Organization UI (inherited)

Review profile: ASSERTIVE

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 45064cb and e0e9c40.

📒 Files selected for processing (15)
  • netbox_librenms_plugin/import_utils/bulk_import.py
  • netbox_librenms_plugin/import_utils/device_operations.py
  • netbox_librenms_plugin/import_utils/vm_operations.py
  • netbox_librenms_plugin/librenms_api.py
  • netbox_librenms_plugin/tables/device_status.py
  • netbox_librenms_plugin/tables/interfaces.py
  • netbox_librenms_plugin/templates/netbox_librenms_plugin/htmx/device_validation_details.html
  • netbox_librenms_plugin/tests/test_import_utils.py
  • netbox_librenms_plugin/utils.py
  • netbox_librenms_plugin/views/base/cables_view.py
  • netbox_librenms_plugin/views/base/ip_addresses_view.py
  • netbox_librenms_plugin/views/imports/actions.py
  • netbox_librenms_plugin/views/object_sync/devices.py
  • netbox_librenms_plugin/views/object_sync/vms.py
  • netbox_librenms_plugin/views/sync/interfaces.py
📜 Review details
🧰 Additional context used
📓 Path-based instructions (5)
**/*.py

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

**/*.py: Reuse the librenms_api.py LibreNMS client instead of making new requests calls; it handles multi-server configs via LibreNMSSettings model and the servers plugin config, plus caching
Always call LibreNMSAPI.get_librenms_id to retrieve the device/VM LibreNMS mapping via the librenms_id custom field instead of touching the field directly
Matching must be exact-only for site, platform, device type, and role; do not add fuzzy matching; use the functions find_matching_site, match_librenms_hardware_to_device_type, and find_matching_platform from utils.py

Files:

  • netbox_librenms_plugin/views/base/ip_addresses_view.py
  • netbox_librenms_plugin/import_utils/vm_operations.py
  • netbox_librenms_plugin/views/object_sync/devices.py
  • netbox_librenms_plugin/views/object_sync/vms.py
  • netbox_librenms_plugin/import_utils/device_operations.py
  • netbox_librenms_plugin/views/base/cables_view.py
  • netbox_librenms_plugin/librenms_api.py
  • netbox_librenms_plugin/tables/interfaces.py
  • netbox_librenms_plugin/tables/device_status.py
  • netbox_librenms_plugin/utils.py
  • netbox_librenms_plugin/views/imports/actions.py
  • netbox_librenms_plugin/import_utils/bulk_import.py
  • netbox_librenms_plugin/views/sync/interfaces.py
  • netbox_librenms_plugin/tests/test_import_utils.py
netbox_librenms_plugin/templates/**/*.html

📄 CodeRabbit inference engine (.github/instructions/frontend.instructions.md)

netbox_librenms_plugin/templates/**/*.html: HTMX 2.x is the primary async layer. Table row updates should return <tr hx-swap-oob="true">.
Avoid outerHTML swaps in HTMX; use OOB or targeted innerHTML swaps to keep table layout intact.
Modals use Tabler (Bootstrap-like) but without bootstrap.Modal helpers.
Modal buttons should target the htmx-modal-content element. JavaScript in librenms_import.html toggles the wrapper; do not reintroduce data-bs-toggle or duplicate modal IDs.
Keep <select class="device-role-select"> markup stable to preserve JavaScript hook-up with TomSelect decorators.
Styling assumes Tabler defaults. Do not add table-responsive wrappers as they were deliberately removed to prevent dropdown clipping.

Files:

  • netbox_librenms_plugin/templates/netbox_librenms_plugin/htmx/device_validation_details.html
netbox_librenms_plugin/templates/netbox_librenms_plugin/htmx/**/*.html

📄 CodeRabbit inference engine (.github/instructions/frontend.instructions.md)

HTMX fragments should live in templates/netbox_librenms_plugin/htmx/. Keep server responses and HTMX targets in sync.

Files:

  • netbox_librenms_plugin/templates/netbox_librenms_plugin/htmx/device_validation_details.html
netbox_librenms_plugin/templates/netbox_librenms_plugin/**/*.html

📄 CodeRabbit inference engine (.github/instructions/frontend.instructions.md)

Templates should live in templates/netbox_librenms_plugin/. Reusable template includes should be placed under inc/ subdirectory.

Files:

  • netbox_librenms_plugin/templates/netbox_librenms_plugin/htmx/device_validation_details.html
**/views/imports/**

📄 CodeRabbit inference engine (.github/instructions/background-jobs.instructions.md)

**/views/imports/**: Use Job UUID (job.job_id) for RQ API endpoints: /api/core/background-tasks/{uuid}/
Use Job PK (job.pk) for database endpoints and result loading
Check rq_job.is_stopped or rq_job.is_failed flags in Redis for cancellation detection, not database status
Call /api/core/background-tasks/{uuid}/stop/ to stop RQ job in the job cancellation flow
Call plugin's sync endpoint /api/plugins/librenms_plugin/jobs/{pk}/sync-status/ to update database after stopping RQ job
Poll /api/core/background-tasks/{uuid}/ for real-time RQ status instead of polling the database
api/views.py::sync_job_status() must sync database Job status with RQ job status because NetBox worker doesn't always update DB when jobs stop before processing starts

Files:

  • netbox_librenms_plugin/views/imports/actions.py
🧠 Learnings (22)
📓 Common learnings
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-02-15T15:38:54.189Z
Learning: Applies to views/imports/**/*.py : REST endpoints for imports live in `views/imports/actions.py` with the list view in `views/imports/list.py`, surface via `urls.py`, and emit HTMX fragments in `templates/netbox_librenms_plugin/htmx/`; keep server responses and HTMX targets in sync
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/testing.instructions.md:0-0
Timestamp: 2026-02-15T15:39:20.744Z
Learning: Applies to tests/**/*.py : Patch deferred/inline imports at their source module (e.g., `netbox_librenms_plugin.import_utils.process_device_filters`), not the consuming module
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/testing.instructions.md:0-0
Timestamp: 2026-02-15T15:39:20.744Z
Learning: Applies to tests/**/*.py : Test coverage mapping: `librenms_api.py` → `test_librenms_api.py`, `import_utils.py`/`import_validation_helpers.py`/`utils.py` → `test_import_utils.py`/`test_import_validation_helpers.py`/`test_utils.py`, `jobs.py`/`views/imports/list.py` → `test_background_jobs.py`
📚 Learning: 2026-02-15T15:38:54.189Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-02-15T15:38:54.189Z
Learning: Applies to **/*.py : Always call `LibreNMSAPI.get_librenms_id` to retrieve the device/VM LibreNMS mapping via the `librenms_id` custom field instead of touching the field directly

Applied to files:

  • netbox_librenms_plugin/views/base/ip_addresses_view.py
  • netbox_librenms_plugin/views/base/cables_view.py
  • netbox_librenms_plugin/librenms_api.py
  • netbox_librenms_plugin/tables/interfaces.py
  • netbox_librenms_plugin/utils.py
  • netbox_librenms_plugin/views/imports/actions.py
  • netbox_librenms_plugin/views/sync/interfaces.py
📚 Learning: 2026-02-15T15:38:54.189Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-02-15T15:38:54.189Z
Learning: Applies to views/imports/**/*.py : REST endpoints for imports live in `views/imports/actions.py` with the list view in `views/imports/list.py`, surface via `urls.py`, and emit HTMX fragments in `templates/netbox_librenms_plugin/htmx/`; keep server responses and HTMX targets in sync

Applied to files:

  • netbox_librenms_plugin/views/base/ip_addresses_view.py
  • netbox_librenms_plugin/templates/netbox_librenms_plugin/htmx/device_validation_details.html
  • netbox_librenms_plugin/import_utils/vm_operations.py
  • netbox_librenms_plugin/import_utils/device_operations.py
  • netbox_librenms_plugin/tables/device_status.py
  • netbox_librenms_plugin/views/imports/actions.py
  • netbox_librenms_plugin/import_utils/bulk_import.py
  • netbox_librenms_plugin/views/sync/interfaces.py
  • netbox_librenms_plugin/tests/test_import_utils.py
📚 Learning: 2026-02-15T15:39:11.578Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/frontend.instructions.md:0-0
Timestamp: 2026-02-15T15:39:11.578Z
Learning: Applies to netbox_librenms_plugin/templates/**/*.html : Modal buttons should target the `htmx-modal-content` element. JavaScript in `librenms_import.html` toggles the wrapper; do not reintroduce `data-bs-toggle` or duplicate modal IDs.

Applied to files:

  • netbox_librenms_plugin/templates/netbox_librenms_plugin/htmx/device_validation_details.html
  • netbox_librenms_plugin/tables/device_status.py
📚 Learning: 2026-02-15T15:39:11.578Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/frontend.instructions.md:0-0
Timestamp: 2026-02-15T15:39:11.578Z
Learning: Applies to netbox_librenms_plugin/templates/**/*.html : Avoid `outerHTML` swaps in HTMX; use OOB or targeted `innerHTML` swaps to keep table layout intact.

Applied to files:

  • netbox_librenms_plugin/templates/netbox_librenms_plugin/htmx/device_validation_details.html
  • netbox_librenms_plugin/tables/device_status.py
📚 Learning: 2026-02-15T15:39:11.578Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/frontend.instructions.md:0-0
Timestamp: 2026-02-15T15:39:11.578Z
Learning: Applies to netbox_librenms_plugin/templates/**/*.html : HTMX 2.x is the primary async layer. Table row updates should return `<tr hx-swap-oob="true">`.

Applied to files:

  • netbox_librenms_plugin/templates/netbox_librenms_plugin/htmx/device_validation_details.html
  • netbox_librenms_plugin/tables/device_status.py
📚 Learning: 2026-02-15T15:39:11.578Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/frontend.instructions.md:0-0
Timestamp: 2026-02-15T15:39:11.578Z
Learning: Applies to netbox_librenms_plugin/templates/netbox_librenms_plugin/htmx/**/*.html : HTMX fragments should live in `templates/netbox_librenms_plugin/htmx/`. Keep server responses and HTMX targets in sync.

Applied to files:

  • netbox_librenms_plugin/templates/netbox_librenms_plugin/htmx/device_validation_details.html
📚 Learning: 2026-02-15T15:39:11.578Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/frontend.instructions.md:0-0
Timestamp: 2026-02-15T15:39:11.578Z
Learning: Applies to netbox_librenms_plugin/templates/**/*.html : Keep `<select class="device-role-select">` markup stable to preserve JavaScript hook-up with TomSelect decorators.

Applied to files:

  • netbox_librenms_plugin/templates/netbox_librenms_plugin/htmx/device_validation_details.html
  • netbox_librenms_plugin/tables/device_status.py
📚 Learning: 2026-02-15T15:39:11.578Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/frontend.instructions.md:0-0
Timestamp: 2026-02-15T15:39:11.578Z
Learning: Applies to netbox_librenms_plugin/templates/**/*.html : Modals use Tabler (Bootstrap-like) but without `bootstrap.Modal` helpers.

Applied to files:

  • netbox_librenms_plugin/templates/netbox_librenms_plugin/htmx/device_validation_details.html
📚 Learning: 2026-02-15T15:39:11.578Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/frontend.instructions.md:0-0
Timestamp: 2026-02-15T15:39:11.578Z
Learning: Applies to netbox_librenms_plugin/templates/netbox_librenms_plugin/*sync*.html : Sync pages should extend `librenms_sync_base.html`.

Applied to files:

  • netbox_librenms_plugin/templates/netbox_librenms_plugin/htmx/device_validation_details.html
  • netbox_librenms_plugin/views/imports/actions.py
  • netbox_librenms_plugin/views/sync/interfaces.py
📚 Learning: 2026-02-15T15:39:11.578Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/frontend.instructions.md:0-0
Timestamp: 2026-02-15T15:39:11.578Z
Learning: Applies to netbox_librenms_plugin/templates/**/*.html : Styling assumes Tabler defaults. Do not add `table-responsive` wrappers as they were deliberately removed to prevent dropdown clipping.

Applied to files:

  • netbox_librenms_plugin/templates/netbox_librenms_plugin/htmx/device_validation_details.html
📚 Learning: 2026-02-15T15:38:54.189Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-02-15T15:38:54.189Z
Learning: Applies to templates/**/*.html : Reuse template includes under `templates/netbox_librenms_plugin/inc/` and ensure sync pages extend `librenms_sync_base.html`

Applied to files:

  • netbox_librenms_plugin/templates/netbox_librenms_plugin/htmx/device_validation_details.html
📚 Learning: 2026-02-15T15:39:20.744Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/testing.instructions.md:0-0
Timestamp: 2026-02-15T15:39:20.744Z
Learning: Applies to tests/**/*.py : Patch deferred/inline imports at their source module (e.g., `netbox_librenms_plugin.import_utils.process_device_filters`), not the consuming module

Applied to files:

  • netbox_librenms_plugin/import_utils/device_operations.py
  • netbox_librenms_plugin/views/imports/actions.py
  • netbox_librenms_plugin/import_utils/bulk_import.py
  • netbox_librenms_plugin/tests/test_import_utils.py
📚 Learning: 2026-02-15T15:38:54.189Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-02-15T15:38:54.189Z
Learning: Applies to **/*.py : Reuse the `librenms_api.py` LibreNMS client instead of making new `requests` calls; it handles multi-server configs via `LibreNMSSettings` model and the `servers` plugin config, plus caching

Applied to files:

  • netbox_librenms_plugin/librenms_api.py
📚 Learning: 2026-02-15T15:38:54.189Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-02-15T15:38:54.189Z
Learning: Follow sync pipeline flow: fetch LibreNMS data via `librenms_api.py`, cache it using `CacheMixin`, build comparison tables in `tables/`, and render HTMX fragments in `templates/netbox_librenms_plugin/htmx/`

Applied to files:

  • netbox_librenms_plugin/tables/interfaces.py
  • netbox_librenms_plugin/views/imports/actions.py
  • netbox_librenms_plugin/views/sync/interfaces.py
📚 Learning: 2026-02-15T15:39:11.578Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/frontend.instructions.md:0-0
Timestamp: 2026-02-15T15:39:11.578Z
Learning: Tables drive most UIs via `tables/*.py` renderers that emit HTMX-enabled columns and buttons. Prefer updating the table renderer in Python rather than templates when changing row actions.

Applied to files:

  • netbox_librenms_plugin/tables/device_status.py
📚 Learning: 2026-02-15T15:38:54.189Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-02-15T15:38:54.189Z
Learning: Applies to tables/**/*.py : Prefer updating the table renderer in `tables/*.py` rather than templates when changing row actions, since tables emit HTMX-enabled columns and buttons

Applied to files:

  • netbox_librenms_plugin/tables/device_status.py
📚 Learning: 2026-02-15T15:39:20.744Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/testing.instructions.md:0-0
Timestamp: 2026-02-15T15:39:20.744Z
Learning: Applies to tests/test_import_utils.py : Cache key tests must patch `get_validated_device_cache_key` from `import_utils.py`; never hardcode key formats like `job_123_device_1`

Applied to files:

  • netbox_librenms_plugin/import_utils/bulk_import.py
  • netbox_librenms_plugin/tests/test_import_utils.py
📚 Learning: 2026-02-15T15:39:20.744Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/testing.instructions.md:0-0
Timestamp: 2026-02-15T15:39:20.744Z
Learning: Applies to tests/test_background_jobs.py : Patch `cache` where imported: `netbox_librenms_plugin.views.imports.list.cache`, not `django.core.cache.cache`

Applied to files:

  • netbox_librenms_plugin/tests/test_import_utils.py
📚 Learning: 2026-02-15T15:39:20.744Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/testing.instructions.md:0-0
Timestamp: 2026-02-15T15:39:20.744Z
Learning: Applies to tests/**/*.py : Test coverage mapping: `librenms_api.py` → `test_librenms_api.py`, `import_utils.py`/`import_validation_helpers.py`/`utils.py` → `test_import_utils.py`/`test_import_validation_helpers.py`/`test_utils.py`, `jobs.py`/`views/imports/list.py` → `test_background_jobs.py`

Applied to files:

  • netbox_librenms_plugin/tests/test_import_utils.py
📚 Learning: 2026-02-15T15:39:20.744Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/testing.instructions.md:0-0
Timestamp: 2026-02-15T15:39:20.744Z
Learning: Applies to tests/**/*.py : Mock NetBox models (Device, Job, User) with `MagicMock()` instead of creating real instances

Applied to files:

  • netbox_librenms_plugin/tests/test_import_utils.py
📚 Learning: 2026-02-15T15:38:54.189Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-02-15T15:38:54.189Z
Learning: Applies to **/*.py : Matching must be exact-only for site, platform, device type, and role; do not add fuzzy matching; use the functions `find_matching_site`, `match_librenms_hardware_to_device_type`, and `find_matching_platform` from `utils.py`

Applied to files:

  • netbox_librenms_plugin/tests/test_import_utils.py
🧬 Code graph analysis (9)
netbox_librenms_plugin/views/base/cables_view.py (2)
netbox_librenms_plugin/views/mixins.py (1)
  • librenms_api (217-231)
netbox_librenms_plugin/utils.py (1)
  • get_virtual_chassis_member (162-186)
netbox_librenms_plugin/librenms_api.py (1)
netbox_librenms_plugin/utils.py (2)
  • get_librenms_device_id (19-42)
  • set_librenms_device_id (45-67)
netbox_librenms_plugin/tables/interfaces.py (1)
netbox_librenms_plugin/utils.py (2)
  • get_librenms_device_id (19-42)
  • get_interface_name_field (274-302)
netbox_librenms_plugin/tables/device_status.py (1)
netbox_librenms_plugin/views/imports/actions.py (2)
  • get (707-727)
  • get (733-757)
netbox_librenms_plugin/utils.py (2)
netbox_librenms_plugin/views/imports/actions.py (2)
  • get (707-727)
  • get (733-757)
netbox_librenms_plugin/views/base/librenms_sync_view.py (1)
  • get (25-46)
netbox_librenms_plugin/views/imports/actions.py (8)
netbox_librenms_plugin/utils.py (7)
  • get_user_pref (258-262)
  • save_user_pref (265-271)
  • set_librenms_device_id (45-67)
  • find_matching_platform (390-420)
  • match_librenms_hardware_to_device_type (305-353)
  • find_by_librenms_id (70-89)
  • migrate_legacy_librenms_id (92-122)
netbox_librenms_plugin/views/mixins.py (2)
  • LibreNMSAPIMixin (196-282)
  • librenms_api (217-231)
netbox_librenms_plugin/models.py (1)
  • LibreNMSSettings (7-48)
netbox_librenms_plugin/views/settings_views.py (3)
  • get (26-43)
  • post (45-122)
  • post (131-194)
netbox_librenms_plugin/views/imports/list.py (1)
  • get (124-343)
netbox_librenms_plugin/views/base/librenms_sync_view.py (1)
  • get (25-46)
netbox_librenms_plugin/import_utils/device_operations.py (2)
  • validate_device_for_import (79-586)
  • _determine_device_name (23-76)
netbox_librenms_plugin/import_utils/cache.py (1)
  • get_import_device_cache_key (139-158)
netbox_librenms_plugin/import_utils/bulk_import.py (6)
netbox_librenms_plugin/librenms_api.py (2)
  • LibreNMSAPI (15-1039)
  • get_device_info (314-337)
netbox_librenms_plugin/import_utils/cache.py (3)
  • get_cache_metadata_key (10-24)
  • get_import_device_cache_key (139-158)
  • get_validated_device_cache_key (112-136)
netbox_librenms_plugin/import_utils/device_operations.py (2)
  • import_single_device (589-808)
  • validate_device_for_import (79-586)
netbox_librenms_plugin/import_utils/filters.py (1)
  • get_librenms_devices_for_import (43-213)
netbox_librenms_plugin/import_utils/permissions.py (1)
  • require_permissions (31-48)
netbox_librenms_plugin/import_utils/virtual_chassis.py (2)
  • create_virtual_chassis_with_members (310-440)
  • empty_virtual_chassis_data (15-23)
netbox_librenms_plugin/views/sync/interfaces.py (3)
netbox_librenms_plugin/utils.py (1)
  • set_librenms_device_id (45-67)
netbox_librenms_plugin/views/base/librenms_sync_view.py (1)
  • get (25-46)
netbox_librenms_plugin/views/mixins.py (1)
  • librenms_api (217-231)
netbox_librenms_plugin/tests/test_import_utils.py (2)
netbox_librenms_plugin/import_utils/device_operations.py (1)
  • validate_device_for_import (79-586)
netbox_librenms_plugin/views/imports/actions.py (12)
  • get (707-727)
  • get (733-757)
  • DeviceConflictActionView (857-1100)
  • post (249-397)
  • post (424-701)
  • post (821-828)
  • post (834-841)
  • post (847-854)
  • post (860-1100)
  • post (1112-1126)
  • DeviceValidationDetailsView (730-815)
  • _build_sync_info (760-815)
🔇 Additional comments (27)
netbox_librenms_plugin/tests/test_import_utils.py (10)

14-66: LGTM!

Cache key generation tests are well-structured and verify key format/content correctly.


73-142: LGTM!

Comprehensive test coverage for device name determination including edge cases like IP addresses, empty sysName, and domain stripping.


152-153: LGTM!

Patch targets correctly updated to reference the new submodule paths (filters.cache, filters.LibreNMSAPI, virtual_chassis.cache) following the refactored module structure. Based on learnings: "Patch deferred/inline imports at their source module".

Also applies to: 174-174, 192-192, 212-212, 228-228, 282-282


1104-1146: LGTM!

Good test structure using setup_method/teardown_method with centralized SERIAL_PATCHES list. The helper methods _start_patches, _stop_patches, and _setup_no_match_mocks reduce boilerplate and improve maintainability across the serial number matching tests.


1617-1760: LGTM!

Comprehensive tests for legacy librenms_id migration, covering both detection during validation and the migration helper function. Tests correctly verify the int-to-dict format conversion (42{"server_key": 42}).


2215-2229: Patch target for sync_platform action.

This test patches dcim.models.Platform, but previous feedback suggested DeviceConflictActionView.post() may resolve platforms via find_matching_platform. Since the previous review marked this as "✅ Addressed", the implementation may legitimately use Platform.objects.get() for the sync operation. Verify the view implementation matches this test's assumptions.


2246-2260: LGTM!

The sync_device_type test correctly patches netbox_librenms_plugin.utils.match_librenms_hardware_to_device_type at its source module location.


2263-2358: LGTM!

_build_sync_info tests correctly patch utility functions at their source module (netbox_librenms_plugin.utils) and comprehensively verify the all_synced flag logic across different sync scenarios.


2480-2500: LGTM!

Good test verifying that duplicate detection uses the resolved name (after applying use_sysname preference) rather than raw hostname. This ensures consistency between validation and import behavior.


301-309: The patch targets are correctly implemented and follow the guideline. VirtualMachine is imported inline within functions (lines 218, 224, 301 of device_operations.py), so it should be patched at its source module (virtualization.models.VirtualMachine). Device is imported at the module level, so it should be patched at the consuming module (netbox_librenms_plugin.import_utils.device_operations.Device). No changes are needed.

Likely an incorrect or invalid review comment.

netbox_librenms_plugin/utils.py (1)

56-67: Defensive librenms_id coercion is well implemented.

This safely handles malformed legacy values before write-back and prevents type-related sync crashes.

netbox_librenms_plugin/librenms_api.py (1)

193-196: Good centralization of LibreNMS ID handling.

Delegating both lookup and storage to shared helpers keeps behavior consistent across legacy and multi-server formats.

Also applies to: 259-262

netbox_librenms_plugin/views/base/cables_view.py (1)

82-89: Multi-format lookup handling is consistent across cable resolution paths.

The Q(json_path) | Q(legacy_int) pattern is applied uniformly for device and interface resolution.

Also applies to: 131-145, 170-184, 389-391

netbox_librenms_plugin/import_utils/vm_operations.py (1)

146-168: VM bulk-import flow is cleanly integrated with shared cache/validation helpers.

The path from cached fetch → validation → mapped creation is consistent and keeps server-key propagation intact.

Also applies to: 212-214

netbox_librenms_plugin/views/object_sync/devices.py (1)

82-98: Good server-key propagation into interface table constructors.

This keeps per-server ID resolution aligned throughout the device interface sync UI.

netbox_librenms_plugin/views/base/ip_addresses_view.py (1)

14-15: Nice move to centralized server-key-aware ID lookup in IP prefetch.

This improves consistency with the new multi-server librenms_id model.

Also applies to: 107-112

netbox_librenms_plugin/views/sync/interfaces.py (1)

12-12: LGTM! Proper use of centralized ID helper with null guard.

The refactor correctly uses set_librenms_device_id helper instead of direct field manipulation, and the null guard on port_id prevents clobbering existing mappings. This aligns with the coding guideline to use helpers for LibreNMS ID access.

Also applies to: 237-240

netbox_librenms_plugin/tables/device_status.py (1)

461-486: LGTM! Clear conditional styling based on validation state.

The branching logic for button appearance is well-structured with descriptive boolean flags (has_mismatch, has_actions, has_name_sync, has_sync_needed). Button classes correctly convey severity levels.

netbox_librenms_plugin/views/object_sync/vms.py (1)

46-50: LGTM! Consistent server_key propagation to VM interface table.

The server_key parameter is correctly passed from self.librenms_api.server_key to LibreNMSVMInterfaceTable, ensuring server-aware ID handling for VM interfaces.

netbox_librenms_plugin/tables/interfaces.py (1)

16-16: LGTM! Proper use of centralized ID getter with server_key support.

The refactor correctly:

  1. Imports get_librenms_device_id helper
  2. Accepts server_key parameter in constructor (defaulting to "default")
  3. Uses the helper in render_librenms_id instead of direct custom_field_data access

This enables multi-server support and aligns with the coding guideline to use helpers for LibreNMS ID access.

Also applies to: 50-55, 365-365

netbox_librenms_plugin/templates/netbox_librenms_plugin/htmx/device_validation_details.html (1)

104-114: LGTM! Well-structured inline forms with proper CSRF protection.

The inline action forms correctly:

  • Include {% csrf_token %} for security
  • Use hx-swap="none" since row updates happen via OOB swaps
  • Implement force checkbox patterns with JavaScript-enabled button state management
  • Cover comprehensive validation states (name sync, device type, serial, platform, migration)

Also applies to: 151-175, 206-216, 385-404

netbox_librenms_plugin/views/imports/actions.py (6)

38-65: LGTM! Robust naming preference resolution with proper fallback chain.

The _resolve_naming_preferences helper correctly implements the priority:

  1. POST data (explicit toggle)
  2. User preferences
  3. Plugin settings default

This addresses the previous concern about conflict actions bypassing naming preference fallback logic.


790-815: LGTM! Corrected device_type_synced logic.

The _build_sync_info method now correctly handles all cases:

  • Sets device_type_synced = False when hw_match.get("matched") fails (line 801-802)
  • Sets device_type_synced = False when types don't match (line 799-800)

This addresses the previous review concern about device_type_synced incorrectly remaining True when no matching device type is found.


1062-1084: LGTM! Migration action with proper safety checks.

The migrate_librenms_id action correctly:

  • Validates the current value is a bare integer before migrating (line 1069)
  • Requires either serial confirmation or explicit force flag (lines 1074-1078)
  • Uses the migrate_legacy_librenms_id helper for the actual migration
  • Logs the migration for audit purposes

914-931: LGTM! Link action with proper name resolution.

The link action correctly:

  • Prioritizes validation["resolved_name"] which is populated via _resolve_naming_preferences
  • Falls back to _determine_device_name with POST toggles (defensive path)
  • Uses set_librenms_device_id helper for ID assignment
  • Handles optional device type update when force is used with mismatch

1089-1100: LGTM! Proper cache invalidation and HTMX response handling.

After conflict resolution actions:

  • Cache is cleared to ensure fresh validation on next access
  • Device is re-validated and row is re-rendered with updated state
  • HX-Trigger: closeModal header correctly signals the UI to close the modal

903-912: The import path is correct. find_by_librenms_id is defined in netbox_librenms_plugin/utils.py at line 70, and importing it from netbox_librenms_plugin.utils at line 904 is the proper usage.

Comment thread netbox_librenms_plugin/import_utils/device_operations.py Outdated
Comment thread netbox_librenms_plugin/import_utils/device_operations.py
Comment thread netbox_librenms_plugin/tests/test_import_utils.py
Comment thread netbox_librenms_plugin/tests/test_import_utils.py
… accessibility

- device_operations.py: preserve device_type schema dict instead of replacing
  with raw dt_match; 'found' key is now always set regardless of match result
- device_operations.py: use api.server_key instead of server_key or 'default'
  to consistently use the resolved server key after LibreNMSAPI instantiation
- device_operations.py: move Q import to module level; remove inline
  'from dcim.models import Rack' so test mock patching works correctly
- bulk_import.py: fix 7 early-return sites to return ([], False) when
  return_cache_status=True, keeping the return shape consistent for callers
- device_status.py: add 'Legacy ID' warning badge when librenms_id_needs_migration
  is set on an existing device matched by librenms_id
- device_status.py: add aria-label to icon-only buttons for accessibility
- device_validation_details.html: add rel='noopener noreferrer' to all
  target='_blank' anchors (8 links)
- test_import_utils.py: add DeviceType to SERIAL_PATCHES so suggestions path
  is mocked; fix _create_view to use real instantiation; fix _setup_no_existing
  to use named variables instead of fragile negative indexing; remove dead
  mock_rack.objects.filter.return_value = [] lines (inline import bypass meant
  these were never effective); fix device_type assertions to use 'found' key

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 2

♻️ Duplicate comments (2)
netbox_librenms_plugin/tests/test_import_utils.py (2)

2469-2474: 🧹 Nitpick | 🔵 Trivial

Avoid positional mock indexing in this test path.

mocks[-2] is still fragile against decorator-order edits; explicit unpacking keeps the test stable and self-documenting.

♻️ Suggested cleanup
-        mock_device = mocks[-2]  # Device
+        (
+            _mock_site_model,
+            _mock_rack,
+            _mock_cluster,
+            _mock_role,
+            _mock_match_type,
+            _mock_find_platform,
+            _mock_find_site,
+            mock_device,
+            _mock_vm,
+        ) = mocks
#!/bin/bash
set -euo pipefail
rg -nP 'mocks\[-\d+\]' netbox_librenms_plugin/tests/test_import_utils.py

Expected verification result: no remaining positional mocks[-N] access in this file.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@netbox_librenms_plugin/tests/test_import_utils.py` around lines 2469 - 2474,
The test uses a fragile positional access mocks[-2] to get mock_device; replace
this with explicit unpacking or named assignment from the mocks list to make the
test stable (e.g., assign mock_device = mocks[<index_name>] by destructuring
like device_mock, other_mocks = mocks or explicitly pull out the device mock
from the returned fixtures), then keep the rest of the setup (existing =
MagicMock(); existing.name = "core-switch"; existing.serial = "";
mock_device.objects.filter.return_value.first.side_effect = [None, existing])
unchanged; update any other occurrences of mocks[-N] in this test to use named
variables instead.

2198-2206: ⚠️ Potential issue | 🟠 Major

Patch target for sync_platform looks misaligned with runtime resolution path.

This test may bypass the real platform-resolution logic by patching dcim.models.Platform directly, which can make the assertion pass without exercising the intended branch.

#!/bin/bash
set -euo pipefail

actions_file="$(fd '^actions\.py$' netbox_librenms_plugin/views/imports | head -n1)"
echo "Inspecting: ${actions_file}"
rg -n -C4 'sync_platform|find_matching_platform|Platform' "${actions_file}"

Expected verification result: confirm whether DeviceConflictActionView.post() resolves platforms via find_matching_platform(...) (or via a module-local alias) vs Platform.objects.get(...), then patch the exact symbol in that source module.

Based on learnings: "Patch deferred/inline imports at their source module (e.g., netbox_librenms_plugin.import_utils.process_device_filters), not the consuming module".

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@netbox_librenms_plugin/tests/test_import_utils.py` around lines 2198 - 2206,
The test patches dcim.models.Platform directly, but
DeviceConflictActionView.post resolves platforms via the module-local symbol
used by the view (e.g., find_matching_platform or a locally imported Platform
alias) so the patch is misaligned; update the test to patch the exact symbol the
view uses (patch the view module's find_matching_platform or its module-local
Platform alias used by DeviceConflictActionView.post/sync_platform) instead of
dcim.models.Platform, and ensure mock return values are wired to
DeviceConflictActionView.get_validated_device_with_selections,
sync_platform/find_matching_platform, and any module-local Platform lookup so
the real resolution branch is exercised.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@netbox_librenms_plugin/import_utils/device_operations.py`:
- Around line 234-235: After you set result["import_as_vm"] when an existing VM
is detected, downstream validation branches still read the original import_as_vm
parameter causing incorrect device-only validation; fix this by switching those
branches to use the effective import mode stored in result["import_as_vm"] (or
assign import_as_vm = result["import_as_vm"] immediately after detection) so all
subsequent checks use the updated mode. Update every occurrence where the
original import_as_vm argument is used after existing-object detection
(including the other blocks noted around the ranges 403-423, 512-513, 559-561)
to reference result["import_as_vm"] (or the reassigned local variable) instead.
Ensure the logic that sets result["can_import"] still respects the effective
import mode.

In
`@netbox_librenms_plugin/templates/netbox_librenms_plugin/htmx/device_validation_details.html`:
- Around line 111-113: The icon-only sync button in the template (the <button
... title="Sync name to {{ validation.suggested_name }}"> that wraps the <i
class="mdi mdi-sync">) lacks an explicit accessible name; add an aria-label
attribute (e.g., aria-label="Sync name to {{ validation.suggested_name }}") to
that button so screen-readers can announce its purpose, and apply the same
change to the other icon-only sync buttons in this modal (the buttons at the
blocks around the occurrences referenced by validation.suggested_name and
similar sync buttons at the other listed locations).

---

Duplicate comments:
In `@netbox_librenms_plugin/tests/test_import_utils.py`:
- Around line 2469-2474: The test uses a fragile positional access mocks[-2] to
get mock_device; replace this with explicit unpacking or named assignment from
the mocks list to make the test stable (e.g., assign mock_device =
mocks[<index_name>] by destructuring like device_mock, other_mocks = mocks or
explicitly pull out the device mock from the returned fixtures), then keep the
rest of the setup (existing = MagicMock(); existing.name = "core-switch";
existing.serial = ""; mock_device.objects.filter.return_value.first.side_effect
= [None, existing]) unchanged; update any other occurrences of mocks[-N] in this
test to use named variables instead.
- Around line 2198-2206: The test patches dcim.models.Platform directly, but
DeviceConflictActionView.post resolves platforms via the module-local symbol
used by the view (e.g., find_matching_platform or a locally imported Platform
alias) so the patch is misaligned; update the test to patch the exact symbol the
view uses (patch the view module's find_matching_platform or its module-local
Platform alias used by DeviceConflictActionView.post/sync_platform) instead of
dcim.models.Platform, and ensure mock return values are wired to
DeviceConflictActionView.get_validated_device_with_selections,
sync_platform/find_matching_platform, and any module-local Platform lookup so
the real resolution branch is exercised.

ℹ️ Review info

Configuration used: Repository UI (base), Organization UI (inherited)

Review profile: ASSERTIVE

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between e0e9c40 and c439c45.

📒 Files selected for processing (5)
  • netbox_librenms_plugin/import_utils/bulk_import.py
  • netbox_librenms_plugin/import_utils/device_operations.py
  • netbox_librenms_plugin/tables/device_status.py
  • netbox_librenms_plugin/templates/netbox_librenms_plugin/htmx/device_validation_details.html
  • netbox_librenms_plugin/tests/test_import_utils.py
📜 Review details
🧰 Additional context used
📓 Path-based instructions (4)
**/*.py

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

**/*.py: Reuse the librenms_api.py LibreNMS client instead of making new requests calls; it handles multi-server configs via LibreNMSSettings model and the servers plugin config, plus caching
Always call LibreNMSAPI.get_librenms_id to retrieve the device/VM LibreNMS mapping via the librenms_id custom field instead of touching the field directly
Matching must be exact-only for site, platform, device type, and role; do not add fuzzy matching; use the functions find_matching_site, match_librenms_hardware_to_device_type, and find_matching_platform from utils.py

Files:

  • netbox_librenms_plugin/import_utils/bulk_import.py
  • netbox_librenms_plugin/import_utils/device_operations.py
  • netbox_librenms_plugin/tables/device_status.py
  • netbox_librenms_plugin/tests/test_import_utils.py
netbox_librenms_plugin/templates/**/*.html

📄 CodeRabbit inference engine (.github/instructions/frontend.instructions.md)

netbox_librenms_plugin/templates/**/*.html: HTMX 2.x is the primary async layer. Table row updates should return <tr hx-swap-oob="true">.
Avoid outerHTML swaps in HTMX; use OOB or targeted innerHTML swaps to keep table layout intact.
Modals use Tabler (Bootstrap-like) but without bootstrap.Modal helpers.
Modal buttons should target the htmx-modal-content element. JavaScript in librenms_import.html toggles the wrapper; do not reintroduce data-bs-toggle or duplicate modal IDs.
Keep <select class="device-role-select"> markup stable to preserve JavaScript hook-up with TomSelect decorators.
Styling assumes Tabler defaults. Do not add table-responsive wrappers as they were deliberately removed to prevent dropdown clipping.

Files:

  • netbox_librenms_plugin/templates/netbox_librenms_plugin/htmx/device_validation_details.html
netbox_librenms_plugin/templates/netbox_librenms_plugin/htmx/**/*.html

📄 CodeRabbit inference engine (.github/instructions/frontend.instructions.md)

HTMX fragments should live in templates/netbox_librenms_plugin/htmx/. Keep server responses and HTMX targets in sync.

Files:

  • netbox_librenms_plugin/templates/netbox_librenms_plugin/htmx/device_validation_details.html
netbox_librenms_plugin/templates/netbox_librenms_plugin/**/*.html

📄 CodeRabbit inference engine (.github/instructions/frontend.instructions.md)

Templates should live in templates/netbox_librenms_plugin/. Reusable template includes should be placed under inc/ subdirectory.

Files:

  • netbox_librenms_plugin/templates/netbox_librenms_plugin/htmx/device_validation_details.html
🧠 Learnings (18)
📓 Common learnings
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-02-15T15:38:54.189Z
Learning: Applies to views/imports/**/*.py : REST endpoints for imports live in `views/imports/actions.py` with the list view in `views/imports/list.py`, surface via `urls.py`, and emit HTMX fragments in `templates/netbox_librenms_plugin/htmx/`; keep server responses and HTMX targets in sync
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/testing.instructions.md:0-0
Timestamp: 2026-02-15T15:39:20.744Z
Learning: Applies to tests/**/*.py : Patch deferred/inline imports at their source module (e.g., `netbox_librenms_plugin.import_utils.process_device_filters`), not the consuming module
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/testing.instructions.md:0-0
Timestamp: 2026-02-15T15:39:20.744Z
Learning: Applies to tests/**/*.py : Test coverage mapping: `librenms_api.py` → `test_librenms_api.py`, `import_utils.py`/`import_validation_helpers.py`/`utils.py` → `test_import_utils.py`/`test_import_validation_helpers.py`/`test_utils.py`, `jobs.py`/`views/imports/list.py` → `test_background_jobs.py`
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-02-15T15:38:54.189Z
Learning: Applies to **/*.py : Reuse the `librenms_api.py` LibreNMS client instead of making new `requests` calls; it handles multi-server configs via `LibreNMSSettings` model and the `servers` plugin config, plus caching
📚 Learning: 2026-02-15T15:38:54.189Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-02-15T15:38:54.189Z
Learning: Applies to views/imports/**/*.py : REST endpoints for imports live in `views/imports/actions.py` with the list view in `views/imports/list.py`, surface via `urls.py`, and emit HTMX fragments in `templates/netbox_librenms_plugin/htmx/`; keep server responses and HTMX targets in sync

Applied to files:

  • netbox_librenms_plugin/import_utils/bulk_import.py
  • netbox_librenms_plugin/import_utils/device_operations.py
  • netbox_librenms_plugin/tables/device_status.py
  • netbox_librenms_plugin/tests/test_import_utils.py
📚 Learning: 2026-02-15T15:38:54.189Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-02-15T15:38:54.189Z
Learning: Follow sync pipeline flow: fetch LibreNMS data via `librenms_api.py`, cache it using `CacheMixin`, build comparison tables in `tables/`, and render HTMX fragments in `templates/netbox_librenms_plugin/htmx/`

Applied to files:

  • netbox_librenms_plugin/import_utils/bulk_import.py
📚 Learning: 2026-02-15T15:39:20.744Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/testing.instructions.md:0-0
Timestamp: 2026-02-15T15:39:20.744Z
Learning: Applies to tests/**/*.py : Patch deferred/inline imports at their source module (e.g., `netbox_librenms_plugin.import_utils.process_device_filters`), not the consuming module

Applied to files:

  • netbox_librenms_plugin/import_utils/bulk_import.py
  • netbox_librenms_plugin/import_utils/device_operations.py
  • netbox_librenms_plugin/tests/test_import_utils.py
📚 Learning: 2026-02-15T15:39:11.578Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/frontend.instructions.md:0-0
Timestamp: 2026-02-15T15:39:11.578Z
Learning: Applies to netbox_librenms_plugin/templates/**/*.html : Modal buttons should target the `htmx-modal-content` element. JavaScript in `librenms_import.html` toggles the wrapper; do not reintroduce `data-bs-toggle` or duplicate modal IDs.

Applied to files:

  • netbox_librenms_plugin/templates/netbox_librenms_plugin/htmx/device_validation_details.html
  • netbox_librenms_plugin/tables/device_status.py
📚 Learning: 2026-02-15T15:39:11.578Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/frontend.instructions.md:0-0
Timestamp: 2026-02-15T15:39:11.578Z
Learning: Applies to netbox_librenms_plugin/templates/**/*.html : Avoid `outerHTML` swaps in HTMX; use OOB or targeted `innerHTML` swaps to keep table layout intact.

Applied to files:

  • netbox_librenms_plugin/templates/netbox_librenms_plugin/htmx/device_validation_details.html
  • netbox_librenms_plugin/tables/device_status.py
📚 Learning: 2026-02-15T15:39:11.578Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/frontend.instructions.md:0-0
Timestamp: 2026-02-15T15:39:11.578Z
Learning: Applies to netbox_librenms_plugin/templates/netbox_librenms_plugin/htmx/**/*.html : HTMX fragments should live in `templates/netbox_librenms_plugin/htmx/`. Keep server responses and HTMX targets in sync.

Applied to files:

  • netbox_librenms_plugin/templates/netbox_librenms_plugin/htmx/device_validation_details.html
📚 Learning: 2026-02-15T15:39:11.578Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/frontend.instructions.md:0-0
Timestamp: 2026-02-15T15:39:11.578Z
Learning: Applies to netbox_librenms_plugin/templates/**/*.html : HTMX 2.x is the primary async layer. Table row updates should return `<tr hx-swap-oob="true">`.

Applied to files:

  • netbox_librenms_plugin/templates/netbox_librenms_plugin/htmx/device_validation_details.html
  • netbox_librenms_plugin/tables/device_status.py
📚 Learning: 2026-02-15T15:39:11.578Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/frontend.instructions.md:0-0
Timestamp: 2026-02-15T15:39:11.578Z
Learning: Applies to netbox_librenms_plugin/templates/**/*.html : Keep `<select class="device-role-select">` markup stable to preserve JavaScript hook-up with TomSelect decorators.

Applied to files:

  • netbox_librenms_plugin/templates/netbox_librenms_plugin/htmx/device_validation_details.html
  • netbox_librenms_plugin/tables/device_status.py
📚 Learning: 2026-02-15T15:39:11.578Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/frontend.instructions.md:0-0
Timestamp: 2026-02-15T15:39:11.578Z
Learning: Applies to netbox_librenms_plugin/templates/netbox_librenms_plugin/*sync*.html : Sync pages should extend `librenms_sync_base.html`.

Applied to files:

  • netbox_librenms_plugin/templates/netbox_librenms_plugin/htmx/device_validation_details.html
📚 Learning: 2026-02-15T15:38:54.189Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-02-15T15:38:54.189Z
Learning: Applies to templates/**/*.html : Reuse template includes under `templates/netbox_librenms_plugin/inc/` and ensure sync pages extend `librenms_sync_base.html`

Applied to files:

  • netbox_librenms_plugin/templates/netbox_librenms_plugin/htmx/device_validation_details.html
📚 Learning: 2026-02-15T15:39:11.578Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/frontend.instructions.md:0-0
Timestamp: 2026-02-15T15:39:11.578Z
Learning: Applies to netbox_librenms_plugin/templates/**/*.html : Modals use Tabler (Bootstrap-like) but without `bootstrap.Modal` helpers.

Applied to files:

  • netbox_librenms_plugin/templates/netbox_librenms_plugin/htmx/device_validation_details.html
📚 Learning: 2026-02-15T15:38:54.189Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-02-15T15:38:54.189Z
Learning: Applies to **/*.py : Matching must be exact-only for site, platform, device type, and role; do not add fuzzy matching; use the functions `find_matching_site`, `match_librenms_hardware_to_device_type`, and `find_matching_platform` from `utils.py`

Applied to files:

  • netbox_librenms_plugin/import_utils/device_operations.py
  • netbox_librenms_plugin/tests/test_import_utils.py
📚 Learning: 2026-02-15T15:38:54.189Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-02-15T15:38:54.189Z
Learning: Applies to **/*.py : Always call `LibreNMSAPI.get_librenms_id` to retrieve the device/VM LibreNMS mapping via the `librenms_id` custom field instead of touching the field directly

Applied to files:

  • netbox_librenms_plugin/import_utils/device_operations.py
📚 Learning: 2026-02-15T15:39:20.744Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/testing.instructions.md:0-0
Timestamp: 2026-02-15T15:39:20.744Z
Learning: Applies to tests/**/*.py : Test coverage mapping: `librenms_api.py` → `test_librenms_api.py`, `import_utils.py`/`import_validation_helpers.py`/`utils.py` → `test_import_utils.py`/`test_import_validation_helpers.py`/`test_utils.py`, `jobs.py`/`views/imports/list.py` → `test_background_jobs.py`

Applied to files:

  • netbox_librenms_plugin/tests/test_import_utils.py
📚 Learning: 2026-02-15T15:39:20.744Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/testing.instructions.md:0-0
Timestamp: 2026-02-15T15:39:20.744Z
Learning: Applies to tests/test_background_jobs.py : Patch `cache` where imported: `netbox_librenms_plugin.views.imports.list.cache`, not `django.core.cache.cache`

Applied to files:

  • netbox_librenms_plugin/tests/test_import_utils.py
📚 Learning: 2026-02-15T15:39:20.744Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/testing.instructions.md:0-0
Timestamp: 2026-02-15T15:39:20.744Z
Learning: Applies to tests/test_import_utils.py : Cache key tests must patch `get_validated_device_cache_key` from `import_utils.py`; never hardcode key formats like `job_123_device_1`

Applied to files:

  • netbox_librenms_plugin/tests/test_import_utils.py
📚 Learning: 2026-02-15T15:39:20.744Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/testing.instructions.md:0-0
Timestamp: 2026-02-15T15:39:20.744Z
Learning: Applies to tests/**/*.py : Mock NetBox models (Device, Job, User) with `MagicMock()` instead of creating real instances

Applied to files:

  • netbox_librenms_plugin/tests/test_import_utils.py
🧬 Code graph analysis (2)
netbox_librenms_plugin/import_utils/device_operations.py (4)
netbox_librenms_plugin/librenms_api.py (2)
  • LibreNMSAPI (15-1039)
  • get_device_info (314-337)
netbox_librenms_plugin/utils.py (1)
  • find_by_librenms_id (70-89)
netbox_librenms_plugin/import_utils/cache.py (1)
  • get_import_device_cache_key (139-158)
netbox_librenms_plugin/import_utils/virtual_chassis.py (2)
  • empty_virtual_chassis_data (15-23)
  • get_virtual_chassis_data (60-80)
netbox_librenms_plugin/tables/device_status.py (2)
netbox_librenms_plugin/views/imports/actions.py (2)
  • get (707-727)
  • get (733-757)
netbox_librenms_plugin/views/base/librenms_sync_view.py (1)
  • get (25-46)
🔇 Additional comments (2)
netbox_librenms_plugin/import_utils/bulk_import.py (1)

386-387: Nice return-contract hardening on early exits.

The cancellation/disconnect paths now consistently honor return_cache_status=True by returning ( [], False ), which removes tuple-unpack hazards for callers.

Also applies to: 406-407, 412-413, 435-436, 441-442, 449-450, 496-497

netbox_librenms_plugin/tables/device_status.py (1)

491-503: Good accessibility improvement for icon-only state.

Adding conditional aria-label when the details button has no visible text is a solid fix for assistive-tech naming.

Comment thread netbox_librenms_plugin/import_utils/device_operations.py
- import_utils/cache.py: guard datetime.fromisoformat with try/except fallback;
  replace hash() with deterministic sha256 for cross-process stable cache keys
- import_utils/filters.py: replace hash() with deterministic sha256 for
  librenms_devices_import cache key; add hashlib/json imports
- import_utils/permissions.py: remove unused logger and logging import
- import_utils/virtual_chassis.py: fix blank serial skip logic - only skip
  member when serial is non-empty AND matches master serial; same fix in
  expected_members count
- device_validation_details.html: add aria-label to 6 icon-only sync buttons
  for screen-reader accessibility
- test_import_utils.py: replace fragile mocks[-2] indexing with named unpacking
- views/imports/actions.py: import escape(); HTML-escape untrusted values
  (id_conflict.name, librenms_os, hardware, action) in HttpResponse f-strings
- views/sync/interfaces.py: guard handle_mac_address with is_device_interface
  to avoid setting MAC on VMInterface objects
- import_utils/device_operations.py: reassign import_as_vm from result dict
  after existing-object detection so downstream branches use effective VM mode
When an existing device is deleted between cache and refresh, recompute
can_import and is_ready to match validate_device_for_import semantics:
- can_import = not bool(issues) (not unconditionally True)
- VMs: is_ready = can_import AND cluster.found (not site+cluster+device_role)
- Devices: is_ready = can_import AND site+device_type+device_role

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 3

♻️ Duplicate comments (5)
netbox_librenms_plugin/import_utils/cache.py (1)

67-70: ⚠️ Potential issue | 🟠 Major

Location-choice cache key should be server-scoped.

Using a global key can cross-contaminate location labels between LibreNMS servers.

🔧 Proposed fix
-    location_cache_key = "librenms_locations_choices"
+    location_cache_key = f"librenms_locations_choices_{server_key}"
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@netbox_librenms_plugin/import_utils/cache.py` around lines 67 - 70, The cache
key "librenms_locations_choices" is global and can mix labels between LibreNMS
servers; update the code that builds location_cache_key (used with cache.get and
to set location_choices) to include a server-specific identifier (e.g., server
id, hostname, or instance PK available in the surrounding context) so each
server has a unique key (for example "librenms_locations_choices:{server_id}"),
and ensure any cache.set/invalidations use the same server-scoped key.
netbox_librenms_plugin/import_utils/filters.py (1)

77-79: ⚠️ Potential issue | 🟠 Major

Use the resolved API server key when building cache keys.

When api is provided and server_key arg is None, keys collapse under None and can mix cache data across servers.

🔧 Proposed fix
         if api is None:
             api = LibreNMSAPI(server_key=server_key)
+        effective_server_key = server_key or getattr(api, "server_key", "default")
@@
-        cache_key = f"librenms_devices_import_{server_key}_{_hash(api_filters)}_{_hash(client_filters)}"
+        cache_key = (
+            f"librenms_devices_import_{effective_server_key}_{_hash(api_filters)}_{_hash(client_filters)}"
+        )

Also applies to: 181-181

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@netbox_librenms_plugin/import_utils/filters.py` around lines 77 - 79, The
cache keys are being built with the raw server_key argument which can be None
when an api instance is supplied, causing keys to collide; update the code to
resolve the actual server key before building keys by using the api's configured
key (e.g., read api.server_key or api.get_server_key() immediately after the api
is set in the function) and then use that resolved_server_key for all cache key
construction (apply the same change where cache keys are built around the code
referencing server_key at the other spot near line 181).
netbox_librenms_plugin/views/imports/actions.py (1)

923-925: ⚠️ Potential issue | 🟡 Minor

Fallback naming path still bypasses _resolve_naming_preferences().

The direct checkbox reads in these fallback branches can still diverge from the preference-resolution logic.

🔧 Proposed fix
+        use_sysname_pref, strip_domain_pref = _resolve_naming_preferences(request)
@@
-                    use_sysname=request.POST.get("use-sysname-toggle") == "on",
-                    strip_domain=request.POST.get("strip-domain-toggle") == "on",
+                    use_sysname=use_sysname_pref,
+                    strip_domain=strip_domain_pref,

Also applies to: 943-945, 995-997

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@netbox_librenms_plugin/views/imports/actions.py` around lines 923 - 925, The
fallback branches are reading checkboxes directly from request.POST (e.g.,
passing use_sysname=request.POST.get("use-sysname-toggle") == "on" and
strip_domain=request.POST.get("strip-domain-toggle") == "on") which bypasses the
shared naming preference logic in _resolve_naming_preferences(); update those
fallback branches to call and use _resolve_naming_preferences(...) instead of
reading request.POST directly (replace the direct use_sysname/strip_domain reads
with the values returned by _resolve_naming_preferences), and apply the same
change to the other occurrences mentioned (the blocks around the code at the
other similar spots).
netbox_librenms_plugin/import_utils/virtual_chassis.py (2)

178-179: ⚠️ Potential issue | 🟡 Minor

Use an explicit None check for parent_index.

if not parent_index can incorrectly reject a valid 0 index.

🔧 Proposed fix
-        if not parent_index:
+        if parent_index is None:
             return None
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@netbox_librenms_plugin/import_utils/virtual_chassis.py` around lines 178 -
179, Replace the falsy check on parent_index with an explicit None check: change
the conditional "if not parent_index:" to "if parent_index is None:" so a valid
index of 0 is not treated as missing; keep the existing return None behavior and
adjust any surrounding logic in the same function that relies on parent_index's
presence.

257-263: 🧹 Nitpick | 🔵 Trivial

Avoid querying settings on every VC member-name generation call.

LibreNMSSettings.objects.first() in this path adds repeated DB overhead during VC processing.

♻️ Refactor sketch
+from functools import lru_cache
+
+@lru_cache(maxsize=1)
+def _get_vc_member_name_pattern() -> str:
+    from ..models import LibreNMSSettings
+    try:
+        settings = LibreNMSSettings.objects.first()
+        return settings.vc_member_name_pattern if settings else "-M{position}"
+    except Exception:
+        return "-M{position}"
@@
-    try:
-        settings = LibreNMSSettings.objects.first()
-        pattern = settings.vc_member_name_pattern if settings else "-M{position}"
-    except Exception as e:
-        logger.warning(f"Could not load VC member name pattern from settings: {e}. Using default.")
-        pattern = "-M{position}"
+    pattern = _get_vc_member_name_pattern()
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@netbox_librenms_plugin/import_utils/virtual_chassis.py` around lines 257 -
263, The code repeatedly queries LibreNMSSettings.objects.first() inside the VC
member-name generation path (the try block that sets pattern from
LibreNMSSettings.vc_member_name_pattern), causing DB overhead; change this to
read and cache the pattern once (e.g., at module import or when the importer is
initialized) and reuse the cached value in the member-name generation function
instead of calling LibreNMSSettings.objects.first() each time, falling back to
"-M{position}" if no setting or on error; update references to pattern retrieval
(the code that assigns pattern) to use the cached_pattern variable or an
injected setting accessor rather than the live DB query.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@netbox_librenms_plugin/import_utils/bulk_import.py`:
- Around line 443-450: The current try/except around the no-op lookup of
request.META['wsgi.input'] cannot catch BrokenPipeError and should be removed or
replaced; remove the entire clause that performs only hasattr(request, "META")
and request.META.get("wsgi.input") with a pass, or if you need an actual probe
replace it with a real I/O probe on request.META['wsgi.input'] (e.g., attempt a
non-blocking peek/read on the wsgi.input stream and catch
BrokenPipeError/ConnectionError/IOError), and keep the existing
logger.info(f"Client disconnected during validation at device {idx}") and return
logic (return [], False if return_cache_status else []) intact.

In `@netbox_librenms_plugin/import_utils/cache.py`:
- Around line 25-26: The cache metadata key builder drops valid falsy filter
values because the comprehension uses "if v" which filters out 0/False; update
the filter_parts computation (the generator that builds filter_parts) to include
falsy-but-valid values by changing the predicate to "if v is not None" (and
ensure values are stringified, e.g., f"{k}={v}") so 0 and False are preserved
while still excluding None; keep the return string format that references
server_key and vc_enabled as before.

In `@netbox_librenms_plugin/views/imports/actions.py`:
- Around line 950-953: The HttpResponse messages leak unescaped user/device
values (incoming_serial and conflict_device.name); fix by applying the same
escaping pattern already used elsewhere: import and use django.utils.html.escape
(or the project's existing escape helper) to escape incoming_serial and
conflict_device.name before interpolating into the HttpResponse body for all
three serial-conflict responses (the ones that construct messages with
incoming_serial and conflict_device.name and return HttpResponse with status
409) so the returned HTML is safe from reflected XSS.

---

Duplicate comments:
In `@netbox_librenms_plugin/import_utils/cache.py`:
- Around line 67-70: The cache key "librenms_locations_choices" is global and
can mix labels between LibreNMS servers; update the code that builds
location_cache_key (used with cache.get and to set location_choices) to include
a server-specific identifier (e.g., server id, hostname, or instance PK
available in the surrounding context) so each server has a unique key (for
example "librenms_locations_choices:{server_id}"), and ensure any
cache.set/invalidations use the same server-scoped key.

In `@netbox_librenms_plugin/import_utils/filters.py`:
- Around line 77-79: The cache keys are being built with the raw server_key
argument which can be None when an api instance is supplied, causing keys to
collide; update the code to resolve the actual server key before building keys
by using the api's configured key (e.g., read api.server_key or
api.get_server_key() immediately after the api is set in the function) and then
use that resolved_server_key for all cache key construction (apply the same
change where cache keys are built around the code referencing server_key at the
other spot near line 181).

In `@netbox_librenms_plugin/import_utils/virtual_chassis.py`:
- Around line 178-179: Replace the falsy check on parent_index with an explicit
None check: change the conditional "if not parent_index:" to "if parent_index is
None:" so a valid index of 0 is not treated as missing; keep the existing return
None behavior and adjust any surrounding logic in the same function that relies
on parent_index's presence.
- Around line 257-263: The code repeatedly queries
LibreNMSSettings.objects.first() inside the VC member-name generation path (the
try block that sets pattern from LibreNMSSettings.vc_member_name_pattern),
causing DB overhead; change this to read and cache the pattern once (e.g., at
module import or when the importer is initialized) and reuse the cached value in
the member-name generation function instead of calling
LibreNMSSettings.objects.first() each time, falling back to "-M{position}" if no
setting or on error; update references to pattern retrieval (the code that
assigns pattern) to use the cached_pattern variable or an injected setting
accessor rather than the live DB query.

In `@netbox_librenms_plugin/views/imports/actions.py`:
- Around line 923-925: The fallback branches are reading checkboxes directly
from request.POST (e.g., passing
use_sysname=request.POST.get("use-sysname-toggle") == "on" and
strip_domain=request.POST.get("strip-domain-toggle") == "on") which bypasses the
shared naming preference logic in _resolve_naming_preferences(); update those
fallback branches to call and use _resolve_naming_preferences(...) instead of
reading request.POST directly (replace the direct use_sysname/strip_domain reads
with the values returned by _resolve_naming_preferences), and apply the same
change to the other occurrences mentioned (the blocks around the code at the
other similar spots).

ℹ️ Review info

Configuration used: Repository UI (base), Organization UI (inherited)

Review profile: ASSERTIVE

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between c439c45 and 6c3425f.

📒 Files selected for processing (10)
  • netbox_librenms_plugin/import_utils/bulk_import.py
  • netbox_librenms_plugin/import_utils/cache.py
  • netbox_librenms_plugin/import_utils/device_operations.py
  • netbox_librenms_plugin/import_utils/filters.py
  • netbox_librenms_plugin/import_utils/permissions.py
  • netbox_librenms_plugin/import_utils/virtual_chassis.py
  • netbox_librenms_plugin/templates/netbox_librenms_plugin/htmx/device_validation_details.html
  • netbox_librenms_plugin/tests/test_import_utils.py
  • netbox_librenms_plugin/views/imports/actions.py
  • netbox_librenms_plugin/views/sync/interfaces.py
📜 Review details
🧰 Additional context used
📓 Path-based instructions (5)
**/*.py

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

**/*.py: Reuse the librenms_api.py LibreNMS client instead of making new requests calls; it handles multi-server configs via LibreNMSSettings model and the servers plugin config, plus caching
Always call LibreNMSAPI.get_librenms_id to retrieve the device/VM LibreNMS mapping via the librenms_id custom field instead of touching the field directly
Matching must be exact-only for site, platform, device type, and role; do not add fuzzy matching; use the functions find_matching_site, match_librenms_hardware_to_device_type, and find_matching_platform from utils.py

Files:

  • netbox_librenms_plugin/import_utils/permissions.py
  • netbox_librenms_plugin/views/sync/interfaces.py
  • netbox_librenms_plugin/import_utils/filters.py
  • netbox_librenms_plugin/tests/test_import_utils.py
  • netbox_librenms_plugin/import_utils/bulk_import.py
  • netbox_librenms_plugin/views/imports/actions.py
  • netbox_librenms_plugin/import_utils/virtual_chassis.py
  • netbox_librenms_plugin/import_utils/cache.py
  • netbox_librenms_plugin/import_utils/device_operations.py
netbox_librenms_plugin/templates/**/*.html

📄 CodeRabbit inference engine (.github/instructions/frontend.instructions.md)

netbox_librenms_plugin/templates/**/*.html: HTMX 2.x is the primary async layer. Table row updates should return <tr hx-swap-oob="true">.
Avoid outerHTML swaps in HTMX; use OOB or targeted innerHTML swaps to keep table layout intact.
Modals use Tabler (Bootstrap-like) but without bootstrap.Modal helpers.
Modal buttons should target the htmx-modal-content element. JavaScript in librenms_import.html toggles the wrapper; do not reintroduce data-bs-toggle or duplicate modal IDs.
Keep <select class="device-role-select"> markup stable to preserve JavaScript hook-up with TomSelect decorators.
Styling assumes Tabler defaults. Do not add table-responsive wrappers as they were deliberately removed to prevent dropdown clipping.

Files:

  • netbox_librenms_plugin/templates/netbox_librenms_plugin/htmx/device_validation_details.html
netbox_librenms_plugin/templates/netbox_librenms_plugin/htmx/**/*.html

📄 CodeRabbit inference engine (.github/instructions/frontend.instructions.md)

HTMX fragments should live in templates/netbox_librenms_plugin/htmx/. Keep server responses and HTMX targets in sync.

Files:

  • netbox_librenms_plugin/templates/netbox_librenms_plugin/htmx/device_validation_details.html
netbox_librenms_plugin/templates/netbox_librenms_plugin/**/*.html

📄 CodeRabbit inference engine (.github/instructions/frontend.instructions.md)

Templates should live in templates/netbox_librenms_plugin/. Reusable template includes should be placed under inc/ subdirectory.

Files:

  • netbox_librenms_plugin/templates/netbox_librenms_plugin/htmx/device_validation_details.html
**/views/imports/**

📄 CodeRabbit inference engine (.github/instructions/background-jobs.instructions.md)

**/views/imports/**: Use Job UUID (job.job_id) for RQ API endpoints: /api/core/background-tasks/{uuid}/
Use Job PK (job.pk) for database endpoints and result loading
Check rq_job.is_stopped or rq_job.is_failed flags in Redis for cancellation detection, not database status
Call /api/core/background-tasks/{uuid}/stop/ to stop RQ job in the job cancellation flow
Call plugin's sync endpoint /api/plugins/librenms_plugin/jobs/{pk}/sync-status/ to update database after stopping RQ job
Poll /api/core/background-tasks/{uuid}/ for real-time RQ status instead of polling the database
api/views.py::sync_job_status() must sync database Job status with RQ job status because NetBox worker doesn't always update DB when jobs stop before processing starts

Files:

  • netbox_librenms_plugin/views/imports/actions.py
🧠 Learnings (21)
📓 Common learnings
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-02-15T15:38:54.189Z
Learning: Applies to views/imports/**/*.py : REST endpoints for imports live in `views/imports/actions.py` with the list view in `views/imports/list.py`, surface via `urls.py`, and emit HTMX fragments in `templates/netbox_librenms_plugin/htmx/`; keep server responses and HTMX targets in sync
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/testing.instructions.md:0-0
Timestamp: 2026-02-15T15:39:20.744Z
Learning: Applies to tests/**/*.py : Patch deferred/inline imports at their source module (e.g., `netbox_librenms_plugin.import_utils.process_device_filters`), not the consuming module
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/testing.instructions.md:0-0
Timestamp: 2026-02-15T15:39:20.744Z
Learning: Applies to tests/**/*.py : Test coverage mapping: `librenms_api.py` → `test_librenms_api.py`, `import_utils.py`/`import_validation_helpers.py`/`utils.py` → `test_import_utils.py`/`test_import_validation_helpers.py`/`test_utils.py`, `jobs.py`/`views/imports/list.py` → `test_background_jobs.py`
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-02-15T15:38:54.189Z
Learning: Applies to **/*.py : Reuse the `librenms_api.py` LibreNMS client instead of making new `requests` calls; it handles multi-server configs via `LibreNMSSettings` model and the `servers` plugin config, plus caching
📚 Learning: 2026-02-15T15:38:54.189Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-02-15T15:38:54.189Z
Learning: Applies to **/*.py : Always call `LibreNMSAPI.get_librenms_id` to retrieve the device/VM LibreNMS mapping via the `librenms_id` custom field instead of touching the field directly

Applied to files:

  • netbox_librenms_plugin/views/sync/interfaces.py
  • netbox_librenms_plugin/views/imports/actions.py
  • netbox_librenms_plugin/import_utils/device_operations.py
📚 Learning: 2026-02-15T15:39:11.578Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/frontend.instructions.md:0-0
Timestamp: 2026-02-15T15:39:11.578Z
Learning: Applies to netbox_librenms_plugin/templates/netbox_librenms_plugin/*sync*.html : Sync pages should extend `librenms_sync_base.html`.

Applied to files:

  • netbox_librenms_plugin/views/sync/interfaces.py
  • netbox_librenms_plugin/templates/netbox_librenms_plugin/htmx/device_validation_details.html
  • netbox_librenms_plugin/views/imports/actions.py
📚 Learning: 2026-02-15T15:38:54.189Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-02-15T15:38:54.189Z
Learning: Applies to views/imports/**/*.py : REST endpoints for imports live in `views/imports/actions.py` with the list view in `views/imports/list.py`, surface via `urls.py`, and emit HTMX fragments in `templates/netbox_librenms_plugin/htmx/`; keep server responses and HTMX targets in sync

Applied to files:

  • netbox_librenms_plugin/views/sync/interfaces.py
  • netbox_librenms_plugin/tests/test_import_utils.py
  • netbox_librenms_plugin/import_utils/bulk_import.py
  • netbox_librenms_plugin/templates/netbox_librenms_plugin/htmx/device_validation_details.html
  • netbox_librenms_plugin/views/imports/actions.py
  • netbox_librenms_plugin/import_utils/device_operations.py
📚 Learning: 2026-02-15T15:38:54.189Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-02-15T15:38:54.189Z
Learning: Applies to templates/**/*.html : Reuse template includes under `templates/netbox_librenms_plugin/inc/` and ensure sync pages extend `librenms_sync_base.html`

Applied to files:

  • netbox_librenms_plugin/views/sync/interfaces.py
  • netbox_librenms_plugin/templates/netbox_librenms_plugin/htmx/device_validation_details.html
📚 Learning: 2026-02-15T15:39:20.744Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/testing.instructions.md:0-0
Timestamp: 2026-02-15T15:39:20.744Z
Learning: Applies to tests/**/*.py : Patch deferred/inline imports at their source module (e.g., `netbox_librenms_plugin.import_utils.process_device_filters`), not the consuming module

Applied to files:

  • netbox_librenms_plugin/import_utils/filters.py
  • netbox_librenms_plugin/tests/test_import_utils.py
  • netbox_librenms_plugin/import_utils/bulk_import.py
  • netbox_librenms_plugin/views/imports/actions.py
  • netbox_librenms_plugin/import_utils/device_operations.py
📚 Learning: 2026-02-15T15:39:20.744Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/testing.instructions.md:0-0
Timestamp: 2026-02-15T15:39:20.744Z
Learning: Applies to tests/test_import_utils.py : Cache key tests must patch `get_validated_device_cache_key` from `import_utils.py`; never hardcode key formats like `job_123_device_1`

Applied to files:

  • netbox_librenms_plugin/import_utils/filters.py
  • netbox_librenms_plugin/tests/test_import_utils.py
  • netbox_librenms_plugin/import_utils/bulk_import.py
  • netbox_librenms_plugin/import_utils/cache.py
📚 Learning: 2026-02-15T15:39:20.744Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/testing.instructions.md:0-0
Timestamp: 2026-02-15T15:39:20.744Z
Learning: Applies to tests/test_background_jobs.py : Patch `cache` where imported: `netbox_librenms_plugin.views.imports.list.cache`, not `django.core.cache.cache`

Applied to files:

  • netbox_librenms_plugin/import_utils/filters.py
  • netbox_librenms_plugin/tests/test_import_utils.py
  • netbox_librenms_plugin/import_utils/cache.py
📚 Learning: 2026-02-15T15:39:20.744Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/testing.instructions.md:0-0
Timestamp: 2026-02-15T15:39:20.744Z
Learning: Applies to tests/**/*.py : Test coverage mapping: `librenms_api.py` → `test_librenms_api.py`, `import_utils.py`/`import_validation_helpers.py`/`utils.py` → `test_import_utils.py`/`test_import_validation_helpers.py`/`test_utils.py`, `jobs.py`/`views/imports/list.py` → `test_background_jobs.py`

Applied to files:

  • netbox_librenms_plugin/tests/test_import_utils.py
📚 Learning: 2026-02-15T15:39:20.744Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/testing.instructions.md:0-0
Timestamp: 2026-02-15T15:39:20.744Z
Learning: Applies to tests/**/*.py : Mock NetBox models (Device, Job, User) with `MagicMock()` instead of creating real instances

Applied to files:

  • netbox_librenms_plugin/tests/test_import_utils.py
📚 Learning: 2026-02-15T15:38:54.189Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-02-15T15:38:54.189Z
Learning: Applies to **/*.py : Matching must be exact-only for site, platform, device type, and role; do not add fuzzy matching; use the functions `find_matching_site`, `match_librenms_hardware_to_device_type`, and `find_matching_platform` from `utils.py`

Applied to files:

  • netbox_librenms_plugin/tests/test_import_utils.py
  • netbox_librenms_plugin/import_utils/device_operations.py
📚 Learning: 2026-02-15T15:38:54.189Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-02-15T15:38:54.189Z
Learning: Follow sync pipeline flow: fetch LibreNMS data via `librenms_api.py`, cache it using `CacheMixin`, build comparison tables in `tables/`, and render HTMX fragments in `templates/netbox_librenms_plugin/htmx/`

Applied to files:

  • netbox_librenms_plugin/import_utils/bulk_import.py
  • netbox_librenms_plugin/views/imports/actions.py
📚 Learning: 2026-02-15T15:39:03.405Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/background-jobs.instructions.md:0-0
Timestamp: 2026-02-15T15:39:03.405Z
Learning: Applies to **/jobs.py : Background jobs must use NetBox's `JobRunner` base class (`netbox.jobs.JobRunner`) for long-running operations like device filtering with VC detection

Applied to files:

  • netbox_librenms_plugin/import_utils/bulk_import.py
📚 Learning: 2026-02-15T15:39:11.578Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/frontend.instructions.md:0-0
Timestamp: 2026-02-15T15:39:11.578Z
Learning: Applies to netbox_librenms_plugin/templates/**/*.html : Modal buttons should target the `htmx-modal-content` element. JavaScript in `librenms_import.html` toggles the wrapper; do not reintroduce `data-bs-toggle` or duplicate modal IDs.

Applied to files:

  • netbox_librenms_plugin/templates/netbox_librenms_plugin/htmx/device_validation_details.html
📚 Learning: 2026-02-15T15:39:11.578Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/frontend.instructions.md:0-0
Timestamp: 2026-02-15T15:39:11.578Z
Learning: Applies to netbox_librenms_plugin/templates/**/*.html : Avoid `outerHTML` swaps in HTMX; use OOB or targeted `innerHTML` swaps to keep table layout intact.

Applied to files:

  • netbox_librenms_plugin/templates/netbox_librenms_plugin/htmx/device_validation_details.html
📚 Learning: 2026-02-15T15:39:11.578Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/frontend.instructions.md:0-0
Timestamp: 2026-02-15T15:39:11.578Z
Learning: Applies to netbox_librenms_plugin/templates/netbox_librenms_plugin/htmx/**/*.html : HTMX fragments should live in `templates/netbox_librenms_plugin/htmx/`. Keep server responses and HTMX targets in sync.

Applied to files:

  • netbox_librenms_plugin/templates/netbox_librenms_plugin/htmx/device_validation_details.html
📚 Learning: 2026-02-15T15:39:11.578Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/frontend.instructions.md:0-0
Timestamp: 2026-02-15T15:39:11.578Z
Learning: Applies to netbox_librenms_plugin/templates/**/*.html : HTMX 2.x is the primary async layer. Table row updates should return `<tr hx-swap-oob="true">`.

Applied to files:

  • netbox_librenms_plugin/templates/netbox_librenms_plugin/htmx/device_validation_details.html
📚 Learning: 2026-02-15T15:39:11.578Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/frontend.instructions.md:0-0
Timestamp: 2026-02-15T15:39:11.578Z
Learning: Applies to netbox_librenms_plugin/templates/**/*.html : Keep `<select class="device-role-select">` markup stable to preserve JavaScript hook-up with TomSelect decorators.

Applied to files:

  • netbox_librenms_plugin/templates/netbox_librenms_plugin/htmx/device_validation_details.html
📚 Learning: 2026-02-15T15:39:11.578Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/frontend.instructions.md:0-0
Timestamp: 2026-02-15T15:39:11.578Z
Learning: Applies to netbox_librenms_plugin/templates/**/*.html : Modals use Tabler (Bootstrap-like) but without `bootstrap.Modal` helpers.

Applied to files:

  • netbox_librenms_plugin/templates/netbox_librenms_plugin/htmx/device_validation_details.html
📚 Learning: 2026-02-15T15:39:11.578Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/frontend.instructions.md:0-0
Timestamp: 2026-02-15T15:39:11.578Z
Learning: Applies to netbox_librenms_plugin/templates/**/*.html : Styling assumes Tabler defaults. Do not add `table-responsive` wrappers as they were deliberately removed to prevent dropdown clipping.

Applied to files:

  • netbox_librenms_plugin/templates/netbox_librenms_plugin/htmx/device_validation_details.html
📚 Learning: 2026-02-15T15:38:54.189Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-02-15T15:38:54.189Z
Learning: Applies to {navigation.py,urls.py,api/**} : Respect NetBox plugin APIs in `navigation.py`, `urls.py`, and `api/` directories

Applied to files:

  • netbox_librenms_plugin/views/imports/actions.py
  • netbox_librenms_plugin/import_utils/device_operations.py
🧬 Code graph analysis (4)
netbox_librenms_plugin/views/sync/interfaces.py (2)
netbox_librenms_plugin/utils.py (1)
  • set_librenms_device_id (45-67)
netbox_librenms_plugin/views/mixins.py (1)
  • librenms_api (217-231)
netbox_librenms_plugin/import_utils/bulk_import.py (6)
netbox_librenms_plugin/librenms_api.py (2)
  • LibreNMSAPI (15-1039)
  • get_device_info (314-337)
netbox_librenms_plugin/import_utils/cache.py (3)
  • get_cache_metadata_key (12-26)
  • get_import_device_cache_key (147-166)
  • get_validated_device_cache_key (120-144)
netbox_librenms_plugin/import_utils/device_operations.py (2)
  • import_single_device (589-808)
  • validate_device_for_import (80-586)
netbox_librenms_plugin/import_utils/filters.py (1)
  • get_librenms_devices_for_import (45-220)
netbox_librenms_plugin/import_utils/permissions.py (1)
  • require_permissions (27-44)
netbox_librenms_plugin/import_utils/virtual_chassis.py (3)
  • create_virtual_chassis_with_members (310-442)
  • empty_virtual_chassis_data (15-23)
  • prefetch_vc_data_for_devices (83-117)
netbox_librenms_plugin/import_utils/virtual_chassis.py (3)
netbox_librenms_plugin/views/mixins.py (1)
  • librenms_api (217-231)
netbox_librenms_plugin/librenms_api.py (3)
  • LibreNMSAPI (15-1039)
  • get_device_info (314-337)
  • get_inventory_filtered (726-803)
netbox_librenms_plugin/models.py (1)
  • LibreNMSSettings (7-48)
netbox_librenms_plugin/import_utils/device_operations.py (4)
netbox_librenms_plugin/librenms_api.py (1)
  • LibreNMSAPI (15-1039)
netbox_librenms_plugin/utils.py (3)
  • find_matching_platform (390-420)
  • find_matching_site (356-387)
  • match_librenms_hardware_to_device_type (305-353)
netbox_librenms_plugin/import_utils/cache.py (1)
  • get_import_device_cache_key (147-166)
netbox_librenms_plugin/import_utils/virtual_chassis.py (2)
  • empty_virtual_chassis_data (15-23)
  • get_virtual_chassis_data (60-80)
🔇 Additional comments (9)
netbox_librenms_plugin/views/sync/interfaces.py (1)

237-240: Good guardrails on interface attribute syncing.

The port_id is not None check and is_device_interface gate prevent accidental ID/MAC corruption during sync.

Also applies to: 250-252

netbox_librenms_plugin/import_utils/permissions.py (1)

6-44: Permission helper extraction looks solid.

This is a clean, reusable permission layer for import operations.

netbox_librenms_plugin/templates/netbox_librenms_plugin/htmx/device_validation_details.html (1)

100-112: Nice accessibility and link-hardening follow-through.

aria-label on icon-only controls and rel="noopener noreferrer" on new-tab links are correctly applied.

Also applies to: 159-174, 212-214, 266-268, 290-292, 422-423, 482-483, 520-530, 571-583

netbox_librenms_plugin/views/imports/actions.py (1)

39-67: The naming-preference resolution cleanup is a good improvement.

Using POST → user prefs → plugin defaults consistently reduces drift across HTMX flows.

Also applies to: 147-158, 263-305

netbox_librenms_plugin/import_utils/device_operations.py (1)

403-406: Validation mode handoff and device_type schema handling are now stable.

Using effective import_as_vm and preserving result["device_type"] keys avoids downstream state/schema drift.

Also applies to: 442-447

netbox_librenms_plugin/tests/test_import_utils.py (1)

1-7: LGTM — Test file properly updated for new module structure.

The test patches correctly target the new submodule surfaces (device_operations, filters, virtual_chassis) rather than legacy paths, aligning with the guideline to patch at source modules. The explicit tuple unpacking in TestSerialNumberMatching, TestLegacyLibreNMSIdMigration, and TestDeviceNamingPreferences addresses prior fragility concerns about index-based mock access.

netbox_librenms_plugin/import_utils/bulk_import.py (3)

23-219: LGTM — Bulk import orchestration is well-structured.

Good design decisions:

  • Permission check at entry point (lines 74-79)
  • Race condition prevention for VC creation using processed_vc_domains set (lines 174-175)
  • Proper error isolation per device with aggregated results
  • Efficient API client reuse with single instantiation (line 89)

270-309: LGTM — Refresh logic properly handles deleted devices.

The recomputation of can_import and is_ready when an existing device has been deleted (lines 289-306) correctly mirrors the logic in validate_device_for_import, ensuring cache staleness doesn't lead to incorrect UI states.


380-388: Return contract fix verified.

Early exits now correctly return ([], False) when return_cache_status=True, addressing the previously flagged issue about broken return contracts.

Comment thread netbox_librenms_plugin/import_utils/bulk_import.py Outdated
Comment thread netbox_librenms_plugin/import_utils/cache.py Outdated
Comment thread netbox_librenms_plugin/views/imports/actions.py
- virtual_chassis.py: load VC member name pattern once before loops
  to avoid N DB queries per member creation/update
- virtual_chassis.py: fix parent_index check (0 is valid, use 'is None')
- actions.py: use _resolve_naming_preferences() in link/update/sync_name
  actions instead of reading POST toggles directly (respects user prefs)
- actions.py: HTML-escape incoming_serial and conflict_device.name in all
  serial-conflict HttpResponse messages (XSS fix)
- actions.py: verify existing_device_id matches validated conflict target
  before mutation (only enforced when validation has existing_device set)
- actions.py: add comment explaining collision check is functionally correct
- bulk_import.py: remove dead wsgi.input try/except (no-op, cannot catch
  BrokenPipeError as written)
- cache.py: fix filter_parts to use 'if v is not None' (preserves 0/False)
- cache.py: normalize naive datetime to UTC before age_seconds subtraction
- cache.py: update docstring example to reflect SHA-256 hex prefix format
- filters.py: use api.server_key (always resolved) in cache key construction
- device_operations.py: add comments explaining direct CF access is needed
  for legacy integer format detection (not a bug)
- test_import_utils.py: add comment explaining dcim.models.Platform patch
  is correct (inline import at call time)
- test_import_utils.py: fix _create_request to always include both toggle
  keys to prevent DB access in _resolve_naming_preferences fallback

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 3

♻️ Duplicate comments (2)
netbox_librenms_plugin/import_utils/cache.py (1)

68-69: ⚠️ Potential issue | 🟠 Major

Scope location-choice cache key by server.

Line 68 uses a global key (librenms_locations_choices), so active-search filter labels can bleed across LibreNMS servers when IDs overlap.

🔧 Proposed fix
-    location_cache_key = "librenms_locations_choices"
+    location_cache_key = f"librenms_locations_choices_{server_key}"
#!/bin/bash
# Verify all reads/writes of location choices cache key are consistently server-scoped.
rg -n "librenms_locations_choices" netbox_librenms_plugin
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@netbox_librenms_plugin/import_utils/cache.py` around lines 68 - 69, The cache
key "librenms_locations_choices" is global and must be scoped per server to
avoid cross-server label leakage; update the code that sets and reads
location_cache_key (the variable location_cache_key and calls like cache.get and
cache.set) to include a server-unique identifier (e.g., server.id, server.pk, or
server.hostname/slug) when building the key so both reads and writes use the
same per-server key (for example append or format the identifier into the key
string) and update any other occurrences of "librenms_locations_choices" to the
new scoped key.
netbox_librenms_plugin/import_utils/virtual_chassis.py (1)

215-218: 🧹 Nitpick | 🔵 Trivial

Still querying VC naming pattern per member during detection.

detect_virtual_chassis_from_inventory() calls _generate_vc_member_name() without a preloaded pattern, so settings lookup can still happen once per member in this loop.

🔧 Proposed fix
-        # Step 5: Extract member info
+        # Step 5: Extract member info
+        vc_pattern = _load_vc_member_name_pattern()
         members = []
         for idx, chassis in enumerate(chassis_items):
@@
             # Generate suggested name if we have master name
             if master_name:
-                member_data["suggested_name"] = _generate_vc_member_name(master_name, position + 1)
+                member_data["suggested_name"] = _generate_vc_member_name(
+                    master_name, position + 1, pattern=vc_pattern
+                )
             else:
                 member_data["suggested_name"] = f"Member-{position + 1}"
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@netbox_librenms_plugin/import_utils/virtual_chassis.py` around lines 215 -
218, detect_virtual_chassis_from_inventory is calling _generate_vc_member_name
inside a per-member loop without a preloaded naming pattern, causing a settings
lookup per member; fix by loading the VC naming pattern once before the loop (or
change _generate_vc_member_name to accept a precomputed pattern/cached value)
and pass that cached pattern into the function when creating
member_data["suggested_name"] (i.e., compute pattern once, then call
_generate_vc_member_name(master_name, position + 1, pattern) or supply pattern
via closure/cached variable to avoid repeated settings lookups).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@netbox_librenms_plugin/import_utils/bulk_import.py`:
- Line 387: Introduce a small helper function (e.g., def
_empty_return(return_cache_status): return ([], False) if return_cache_status
else []) in the module and replace each repeated occurrence of "([], False) if
return_cache_status else []" with a call to _empty_return(return_cache_status);
this centralizes the empty-return contract, reduces duplication, and makes
maintenance safer — update all spots in bulk_import.py that currently return
"([], False) if return_cache_status else []" to call _empty_return instead.

In `@netbox_librenms_plugin/views/imports/actions.py`:
- Around line 886-889: The handler currently proceeds when
validation.get("existing_device") is missing; update the logic in the
conflict-action code path (the variables validation, validated_existing,
existing_device) to require a validated conflict target before applying any
conflict actions: if validation is falsy or validation.get("existing_device") is
None, return a 400 error (e.g., "Missing validated conflict target") and do not
mutate existing_device; keep the existing mismatch check (validated_existing.pk
!= existing_device.pk) but only run it after confirmed validated_existing is
present.
- Around line 1067-1078: The migration currently reads
existing_device.custom_field_data["librenms_id"] directly and may overwrite a
mapping incorrectly; replace that direct access by calling
self.librenms_api.get_librenms_id(existing_device) to fetch the current mapping,
verify the returned value is an int and equals the LibreNMS payload device_id
(or the expected device identifier) before calling
migrate_legacy_librenms_id(existing_device, self.librenms_api.server_key), and
ensure you only proceed with migration when get_librenms_id confirms the legacy
id matches the active device_id (otherwise return the 400 response).

---

Duplicate comments:
In `@netbox_librenms_plugin/import_utils/cache.py`:
- Around line 68-69: The cache key "librenms_locations_choices" is global and
must be scoped per server to avoid cross-server label leakage; update the code
that sets and reads location_cache_key (the variable location_cache_key and
calls like cache.get and cache.set) to include a server-unique identifier (e.g.,
server.id, server.pk, or server.hostname/slug) when building the key so both
reads and writes use the same per-server key (for example append or format the
identifier into the key string) and update any other occurrences of
"librenms_locations_choices" to the new scoped key.

In `@netbox_librenms_plugin/import_utils/virtual_chassis.py`:
- Around line 215-218: detect_virtual_chassis_from_inventory is calling
_generate_vc_member_name inside a per-member loop without a preloaded naming
pattern, causing a settings lookup per member; fix by loading the VC naming
pattern once before the loop (or change _generate_vc_member_name to accept a
precomputed pattern/cached value) and pass that cached pattern into the function
when creating member_data["suggested_name"] (i.e., compute pattern once, then
call _generate_vc_member_name(master_name, position + 1, pattern) or supply
pattern via closure/cached variable to avoid repeated settings lookups).

ℹ️ Review info

Configuration used: Repository UI (base), Organization UI (inherited)

Review profile: ASSERTIVE

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 6c3425f and 3ac5a36.

📒 Files selected for processing (7)
  • netbox_librenms_plugin/import_utils/bulk_import.py
  • netbox_librenms_plugin/import_utils/cache.py
  • netbox_librenms_plugin/import_utils/device_operations.py
  • netbox_librenms_plugin/import_utils/filters.py
  • netbox_librenms_plugin/import_utils/virtual_chassis.py
  • netbox_librenms_plugin/tests/test_import_utils.py
  • netbox_librenms_plugin/views/imports/actions.py
📜 Review details
🧰 Additional context used
📓 Path-based instructions (2)
**/*.py

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

**/*.py: Reuse the librenms_api.py LibreNMS client instead of making new requests calls; it handles multi-server configs via LibreNMSSettings model and the servers plugin config, plus caching
Always call LibreNMSAPI.get_librenms_id to retrieve the device/VM LibreNMS mapping via the librenms_id custom field instead of touching the field directly
Matching must be exact-only for site, platform, device type, and role; do not add fuzzy matching; use the functions find_matching_site, match_librenms_hardware_to_device_type, and find_matching_platform from utils.py

Files:

  • netbox_librenms_plugin/import_utils/bulk_import.py
  • netbox_librenms_plugin/tests/test_import_utils.py
  • netbox_librenms_plugin/import_utils/filters.py
  • netbox_librenms_plugin/import_utils/device_operations.py
  • netbox_librenms_plugin/import_utils/virtual_chassis.py
  • netbox_librenms_plugin/views/imports/actions.py
  • netbox_librenms_plugin/import_utils/cache.py
**/views/imports/**

📄 CodeRabbit inference engine (.github/instructions/background-jobs.instructions.md)

**/views/imports/**: Use Job UUID (job.job_id) for RQ API endpoints: /api/core/background-tasks/{uuid}/
Use Job PK (job.pk) for database endpoints and result loading
Check rq_job.is_stopped or rq_job.is_failed flags in Redis for cancellation detection, not database status
Call /api/core/background-tasks/{uuid}/stop/ to stop RQ job in the job cancellation flow
Call plugin's sync endpoint /api/plugins/librenms_plugin/jobs/{pk}/sync-status/ to update database after stopping RQ job
Poll /api/core/background-tasks/{uuid}/ for real-time RQ status instead of polling the database
api/views.py::sync_job_status() must sync database Job status with RQ job status because NetBox worker doesn't always update DB when jobs stop before processing starts

Files:

  • netbox_librenms_plugin/views/imports/actions.py
🧠 Learnings (11)
📓 Common learnings
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-02-15T15:38:54.189Z
Learning: Applies to views/imports/**/*.py : REST endpoints for imports live in `views/imports/actions.py` with the list view in `views/imports/list.py`, surface via `urls.py`, and emit HTMX fragments in `templates/netbox_librenms_plugin/htmx/`; keep server responses and HTMX targets in sync
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/testing.instructions.md:0-0
Timestamp: 2026-02-15T15:39:20.744Z
Learning: Applies to tests/**/*.py : Patch deferred/inline imports at their source module (e.g., `netbox_librenms_plugin.import_utils.process_device_filters`), not the consuming module
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/testing.instructions.md:0-0
Timestamp: 2026-02-15T15:39:20.744Z
Learning: Applies to tests/**/*.py : Test coverage mapping: `librenms_api.py` → `test_librenms_api.py`, `import_utils.py`/`import_validation_helpers.py`/`utils.py` → `test_import_utils.py`/`test_import_validation_helpers.py`/`test_utils.py`, `jobs.py`/`views/imports/list.py` → `test_background_jobs.py`
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-02-15T15:38:54.189Z
Learning: Applies to **/*.py : Reuse the `librenms_api.py` LibreNMS client instead of making new `requests` calls; it handles multi-server configs via `LibreNMSSettings` model and the `servers` plugin config, plus caching
📚 Learning: 2026-02-15T15:38:54.189Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-02-15T15:38:54.189Z
Learning: Applies to views/imports/**/*.py : REST endpoints for imports live in `views/imports/actions.py` with the list view in `views/imports/list.py`, surface via `urls.py`, and emit HTMX fragments in `templates/netbox_librenms_plugin/htmx/`; keep server responses and HTMX targets in sync

Applied to files:

  • netbox_librenms_plugin/import_utils/bulk_import.py
  • netbox_librenms_plugin/tests/test_import_utils.py
  • netbox_librenms_plugin/import_utils/device_operations.py
  • netbox_librenms_plugin/views/imports/actions.py
📚 Learning: 2026-02-15T15:39:20.744Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/testing.instructions.md:0-0
Timestamp: 2026-02-15T15:39:20.744Z
Learning: Applies to tests/**/*.py : Patch deferred/inline imports at their source module (e.g., `netbox_librenms_plugin.import_utils.process_device_filters`), not the consuming module

Applied to files:

  • netbox_librenms_plugin/import_utils/bulk_import.py
  • netbox_librenms_plugin/tests/test_import_utils.py
  • netbox_librenms_plugin/import_utils/filters.py
  • netbox_librenms_plugin/import_utils/device_operations.py
  • netbox_librenms_plugin/import_utils/virtual_chassis.py
  • netbox_librenms_plugin/views/imports/actions.py
📚 Learning: 2026-02-15T15:39:20.744Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/testing.instructions.md:0-0
Timestamp: 2026-02-15T15:39:20.744Z
Learning: Applies to tests/test_import_utils.py : Cache key tests must patch `get_validated_device_cache_key` from `import_utils.py`; never hardcode key formats like `job_123_device_1`

Applied to files:

  • netbox_librenms_plugin/import_utils/bulk_import.py
  • netbox_librenms_plugin/tests/test_import_utils.py
  • netbox_librenms_plugin/import_utils/filters.py
  • netbox_librenms_plugin/import_utils/cache.py
📚 Learning: 2026-02-15T15:39:20.744Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/testing.instructions.md:0-0
Timestamp: 2026-02-15T15:39:20.744Z
Learning: Applies to tests/**/*.py : Test coverage mapping: `librenms_api.py` → `test_librenms_api.py`, `import_utils.py`/`import_validation_helpers.py`/`utils.py` → `test_import_utils.py`/`test_import_validation_helpers.py`/`test_utils.py`, `jobs.py`/`views/imports/list.py` → `test_background_jobs.py`

Applied to files:

  • netbox_librenms_plugin/tests/test_import_utils.py
📚 Learning: 2026-02-15T15:39:20.744Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/testing.instructions.md:0-0
Timestamp: 2026-02-15T15:39:20.744Z
Learning: Applies to tests/test_background_jobs.py : Patch `cache` where imported: `netbox_librenms_plugin.views.imports.list.cache`, not `django.core.cache.cache`

Applied to files:

  • netbox_librenms_plugin/tests/test_import_utils.py
  • netbox_librenms_plugin/import_utils/filters.py
  • netbox_librenms_plugin/import_utils/cache.py
📚 Learning: 2026-02-15T15:39:20.744Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/testing.instructions.md:0-0
Timestamp: 2026-02-15T15:39:20.744Z
Learning: Applies to tests/**/*.py : Mock NetBox models (Device, Job, User) with `MagicMock()` instead of creating real instances

Applied to files:

  • netbox_librenms_plugin/tests/test_import_utils.py
📚 Learning: 2026-02-15T15:38:54.189Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-02-15T15:38:54.189Z
Learning: Applies to **/*.py : Matching must be exact-only for site, platform, device type, and role; do not add fuzzy matching; use the functions `find_matching_site`, `match_librenms_hardware_to_device_type`, and `find_matching_platform` from `utils.py`

Applied to files:

  • netbox_librenms_plugin/tests/test_import_utils.py
  • netbox_librenms_plugin/import_utils/device_operations.py
📚 Learning: 2026-02-15T15:38:54.189Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-02-15T15:38:54.189Z
Learning: Applies to **/*.py : Always call `LibreNMSAPI.get_librenms_id` to retrieve the device/VM LibreNMS mapping via the `librenms_id` custom field instead of touching the field directly

Applied to files:

  • netbox_librenms_plugin/import_utils/device_operations.py
  • netbox_librenms_plugin/views/imports/actions.py
📚 Learning: 2026-02-15T15:38:54.189Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-02-15T15:38:54.189Z
Learning: Follow sync pipeline flow: fetch LibreNMS data via `librenms_api.py`, cache it using `CacheMixin`, build comparison tables in `tables/`, and render HTMX fragments in `templates/netbox_librenms_plugin/htmx/`

Applied to files:

  • netbox_librenms_plugin/views/imports/actions.py
📚 Learning: 2026-02-15T15:39:11.578Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/frontend.instructions.md:0-0
Timestamp: 2026-02-15T15:39:11.578Z
Learning: Applies to netbox_librenms_plugin/templates/netbox_librenms_plugin/*sync*.html : Sync pages should extend `librenms_sync_base.html`.

Applied to files:

  • netbox_librenms_plugin/views/imports/actions.py
🧬 Code graph analysis (3)
netbox_librenms_plugin/tests/test_import_utils.py (8)
netbox_librenms_plugin/import_utils/device_operations.py (1)
  • validate_device_for_import (80-592)
netbox_librenms_plugin/views/imports/actions.py (11)
  • get (708-728)
  • get (734-758)
  • post (250-398)
  • post (425-702)
  • post (822-829)
  • post (835-842)
  • post (848-855)
  • post (861-1099)
  • post (1111-1125)
  • DeviceValidationDetailsView (731-816)
  • _build_sync_info (761-816)
netbox_librenms_plugin/views/imports/list.py (1)
  • get (124-343)
netbox_librenms_plugin/views/base/librenms_sync_view.py (1)
  • get (25-46)
netbox_librenms_plugin/utils.py (1)
  • migrate_legacy_librenms_id (92-122)
netbox_librenms_plugin/views/sync/interfaces.py (2)
  • post (33-80)
  • post (304-381)
netbox_librenms_plugin/views/base/cables_view.py (2)
  • post (323-341)
  • post (349-465)
netbox_librenms_plugin/views/base/ip_addresses_view.py (2)
  • post (283-302)
  • post (402-490)
netbox_librenms_plugin/import_utils/filters.py (1)
netbox_librenms_plugin/librenms_api.py (2)
  • LibreNMSAPI (15-1039)
  • list_devices (805-870)
netbox_librenms_plugin/import_utils/virtual_chassis.py (3)
netbox_librenms_plugin/views/mixins.py (1)
  • librenms_api (217-231)
netbox_librenms_plugin/librenms_api.py (3)
  • LibreNMSAPI (15-1039)
  • get_device_info (314-337)
  • get_inventory_filtered (726-803)
netbox_librenms_plugin/models.py (1)
  • LibreNMSSettings (7-48)
🔇 Additional comments (4)
netbox_librenms_plugin/import_utils/device_operations.py (2)

449-453: Good schema-preserving update for device-type validation.

Line 449-Line 453 keeps result["device_type"] shape intact while mapping matchedfound, which avoids downstream key-shape regressions.


655-662: Resolved server key usage is consistent in validation and persisted mapping.

Line 661-Line 662 and Line 747 correctly use api.server_key, which keeps librenms_id linkage aligned with the effective LibreNMS server.

Also applies to: 740-748

netbox_librenms_plugin/import_utils/cache.py (1)

79-89: Nice hardening on cache timestamp parsing and deterministic key hashing.

The naive→UTC normalization and SHA-256 keying here are solid stability improvements.

Also applies to: 146-148

netbox_librenms_plugin/import_utils/filters.py (1)

174-183: Deterministic, server-scoped cache key construction looks good.

Line 182 correctly keys by api.server_key plus stable digests, which improves cross-worker cache hit consistency.

Comment thread netbox_librenms_plugin/import_utils/bulk_import.py Outdated
Comment thread netbox_librenms_plugin/views/imports/actions.py
Comment on lines +1067 to +1078
cf_value = existing_device.custom_field_data.get("librenms_id")
if not isinstance(cf_value, int):
return HttpResponse(
"Device librenms_id is already in JSON format; no migration needed.",
status=400,
)
if not validation.get("serial_confirmed") and not force:
return HttpResponse(
"Serial number not confirmed. Check the force checkbox to migrate without serial verification.",
status=400,
)
migrate_legacy_librenms_id(existing_device, self.librenms_api.server_key)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Legacy librenms_id migration can rewrite the wrong mapping and bypasses the mapping accessor contract.

The migration branch directly reads custom_field_data["librenms_id"] and does not verify that the legacy integer equals the current LibreNMS payload device_id before rewriting. That can migrate an unrelated legacy value to the active server key.

🔧 Proposed fix
-            cf_value = existing_device.custom_field_data.get("librenms_id")
-            if not isinstance(cf_value, int):
+            mapped_id = self.librenms_api.get_librenms_id(existing_device)
+            if mapped_id is not None:
                 return HttpResponse(
                     "Device librenms_id is already in JSON format; no migration needed.",
                     status=400,
                 )
+            cf_value = existing_device.custom_field_data.get("librenms_id")
+            if type(cf_value) is not int:
+                return HttpResponse("Device librenms_id has unsupported legacy format.", status=400)
+            if cf_value != librenms_id and not force:
+                return HttpResponse(
+                    "Legacy librenms_id does not match current LibreNMS device_id. "
+                    "Use force to migrate anyway.",
+                    status=400,
+                )
             if not validation.get("serial_confirmed") and not force:
                 return HttpResponse(
                     "Serial number not confirmed. Check the force checkbox to migrate without serial verification.",
                     status=400,
                 )
As per coding guidelines: "Always call `LibreNMSAPI.get_librenms_id` to retrieve the device/VM LibreNMS mapping via the `librenms_id` custom field instead of touching the field directly".
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@netbox_librenms_plugin/views/imports/actions.py` around lines 1067 - 1078,
The migration currently reads existing_device.custom_field_data["librenms_id"]
directly and may overwrite a mapping incorrectly; replace that direct access by
calling self.librenms_api.get_librenms_id(existing_device) to fetch the current
mapping, verify the returned value is an int and equals the LibreNMS payload
device_id (or the expected device identifier) before calling
migrate_legacy_librenms_id(existing_device, self.librenms_api.server_key), and
ensure you only proceed with migration when get_librenms_id confirms the legacy
id matches the active device_id (otherwise return the 400 response).

- bulk_import.py: add _empty_return() helper, replace 5 inline
  '([], False) if return_cache_status else []' with calls to it
- cache.py: fix docstring example (spurious '-' before hash digest)
- filters.py: normalize status filter value to int before comparison
  so string '1'/'0' from form fields is handled correctly
- virtual_chassis.py: preserve discovered SNMP vc_position when available;
  fall back to sequential counter only when position is absent
- actions.py: require validated_existing to be non-None for conflict
  actions (missing target now returns 400 instead of proceeding)
- actions.py: replace find_by_librenms_id collision check with an
  exclusion-aware queryset (.exclude(pk=).exists()) for cleaner intent
- actions.py: verify legacy cf_value matches librenms_id before migration
  to prevent migrating stale/incorrect associations
- test_import_utils.py: add existing_device to all conflict-action test
  validation dicts to match the new required-target enforcement
- test_import_utils.py: patch find_matching_platform at utility module
  level instead of dcim.models.Platform

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 8

♻️ Duplicate comments (1)
netbox_librenms_plugin/import_utils/cache.py (1)

68-69: ⚠️ Potential issue | 🟠 Major

Location-choice cache key is not server-scoped.

Using a global key can cross-contaminate location labels between LibreNMS servers.

Proposed fix
-    location_cache_key = "librenms_locations_choices"
+    location_cache_key = f"librenms_locations_choices_{server_key}"
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@netbox_librenms_plugin/import_utils/cache.py` around lines 68 - 69, The cache
key "location_cache_key" is global and can mix location labels across LibreNMS
servers; change the key to include a server-specific identifier (e.g. server ID,
hostname, base_url or API token fingerprint) when setting/reading the cache so
entries are namespaced per server. Update places that use location_cache_key and
cache.get/cache.set (the variables location_cache_key and cached_locations in
import_utils/cache.py) to compose the key like
"librenms_locations_choices:{server_identifier}" and ensure the same identifier
source (passed into the import function or read from the server config) is used
for both cache reads and writes.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@netbox_librenms_plugin/import_utils/bulk_import.py`:
- Around line 367-368: The disabled-device filtering uses a strict int
comparison which drops devices whose status comes back as a string; update the
filter around show_disabled and libre_devices to treat status values robustly by
normalizing d.get("status") before comparing (e.g., accept both 1 and "1" or
attempt an int() conversion with a safe fallback) so active devices aren't
incorrectly filtered out; target the filtering expression that references
show_disabled, libre_devices and d.get("status") and implement
normalization/try-except conversion to ensure both int and string statuses are
handled.

In `@netbox_librenms_plugin/import_utils/filters.py`:
- Around line 39-40: The filter currently checks device activity with
d.get("status") == 1 which fails when status is serialized as a string; update
the list comprehension that filters devices (the devices = [d for d in devices
if ...] block used when show_disabled is False) to compare a normalized value,
e.g. coerce the status to string or int safely (for example use
str(d.get("status")) == "1" or wrap int conversion with a try/except) so both
"1" and 1 are treated as active.

In `@netbox_librenms_plugin/import_utils/virtual_chassis.py`:
- Around line 201-205: The fallback for entPhysicalParentRelPos is using a
zero-based idx which yields position=0 for the first member; change the fallback
to be 1-based so vc_position is correct: in the block that parses raw_position
(variables raw_position, position, idx) replace the fallback assignment position
= idx with position = idx + 1 (and keep the int(...) parsing and the except
(TypeError, ValueError) branch), ensuring the computed position (and ultimately
vc_position) is 1-based.
- Around line 410-414: The current logic may let later fallbacks reuse an
already-assigned discovered position because position isn’t moved ahead when
discovered_pos is used; change the handling around chosen_pos so that after
computing chosen_pos = discovered_pos if discovered_pos is not None else
position you also advance the sequential counter: if discovered_pos is None then
position += 1 else set position = max(position, discovered_pos + 1) (or
otherwise bump position past discovered_pos) so the fallback counter always
stays ahead of any discovered_pos; update references in this block (chosen_pos,
discovered_pos, position) accordingly.

In `@netbox_librenms_plugin/tests/test_import_utils.py`:
- Around line 1949-1957: The test is hitting the "missing validated conflict
target" 400 because mock of
DeviceConflictActionView.get_validated_device_with_selections returns an empty
validation dict; to exercise the unknown-action branch return a validation that
includes a validated conflict target (e.g., set validation to include the
expected key like "conflict_target" pointing to the existing_device or its
identifier) while keeping the action value unknown, so
mock_validate.return_value = (libre_device, {"conflict_target":
existing_device}, {}) and then assert the response.status_code == 400 to
validate the unknown-action handling in
DeviceConflictActionView.get_validated_device_with_selections.
- Around line 1216-1219: The assertion expects the old warning text
"reinstalled" but the code now emits "hostname differs"; update the test in
tests/test_import_utils.py to assert the new warning text by replacing the check
assert "reinstalled" in result["warnings"][0] with assert "hostname differs" in
result["warnings"][0] (reference: result["serial_action"],
result["existing_match_type"], result["warnings"]).

In `@netbox_librenms_plugin/views/imports/actions.py`:
- Around line 893-899: Hoist the _FORCE_REQUIRED_ACTIONS set out of the request
handler to module scope (defined near the top of the file after imports) so it
is not recreated per request; replace the inline definition in the function with
a reference to that module-level constant (use an immutable frozenset for the
module-level constant, e.g., _FORCE_REQUIRED_ACTIONS = frozenset({"link",
"update", "update_serial", "update_type"}), and keep the existing logic that
checks action in _FORCE_REQUIRED_ACTIONS and request.POST.get("force") == "on").
- Line 312: The assignment device_name = validation["resolved_name"] is brittle
and can raise KeyError if validate_device_for_import didn't set resolved_name;
change it to use defensive access (e.g., device_name =
validation.get("resolved_name") with a sensible fallback such as
validation.get("name") or None) and add a clear handling path (raise a
descriptive error or skip/log) so functions like validate_device_for_import and
any downstream code handle the missing value safely.

---

Duplicate comments:
In `@netbox_librenms_plugin/import_utils/cache.py`:
- Around line 68-69: The cache key "location_cache_key" is global and can mix
location labels across LibreNMS servers; change the key to include a
server-specific identifier (e.g. server ID, hostname, base_url or API token
fingerprint) when setting/reading the cache so entries are namespaced per
server. Update places that use location_cache_key and cache.get/cache.set (the
variables location_cache_key and cached_locations in import_utils/cache.py) to
compose the key like "librenms_locations_choices:{server_identifier}" and ensure
the same identifier source (passed into the import function or read from the
server config) is used for both cache reads and writes.

ℹ️ Review info

Configuration used: Repository UI (base), Organization UI (inherited)

Review profile: ASSERTIVE

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 3ac5a36 and 0b9ae51.

📒 Files selected for processing (6)
  • netbox_librenms_plugin/import_utils/bulk_import.py
  • netbox_librenms_plugin/import_utils/cache.py
  • netbox_librenms_plugin/import_utils/filters.py
  • netbox_librenms_plugin/import_utils/virtual_chassis.py
  • netbox_librenms_plugin/tests/test_import_utils.py
  • netbox_librenms_plugin/views/imports/actions.py
📜 Review details
🧰 Additional context used
📓 Path-based instructions (2)
**/*.py

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

**/*.py: Reuse the librenms_api.py LibreNMS client instead of making new requests calls; it handles multi-server configs via LibreNMSSettings model and the servers plugin config, plus caching
Always call LibreNMSAPI.get_librenms_id to retrieve the device/VM LibreNMS mapping via the librenms_id custom field instead of touching the field directly
Matching must be exact-only for site, platform, device type, and role; do not add fuzzy matching; use the functions find_matching_site, match_librenms_hardware_to_device_type, and find_matching_platform from utils.py

Files:

  • netbox_librenms_plugin/import_utils/cache.py
  • netbox_librenms_plugin/tests/test_import_utils.py
  • netbox_librenms_plugin/import_utils/bulk_import.py
  • netbox_librenms_plugin/import_utils/filters.py
  • netbox_librenms_plugin/import_utils/virtual_chassis.py
  • netbox_librenms_plugin/views/imports/actions.py
**/views/imports/**

📄 CodeRabbit inference engine (.github/instructions/background-jobs.instructions.md)

**/views/imports/**: Use Job UUID (job.job_id) for RQ API endpoints: /api/core/background-tasks/{uuid}/
Use Job PK (job.pk) for database endpoints and result loading
Check rq_job.is_stopped or rq_job.is_failed flags in Redis for cancellation detection, not database status
Call /api/core/background-tasks/{uuid}/stop/ to stop RQ job in the job cancellation flow
Call plugin's sync endpoint /api/plugins/librenms_plugin/jobs/{pk}/sync-status/ to update database after stopping RQ job
Poll /api/core/background-tasks/{uuid}/ for real-time RQ status instead of polling the database
api/views.py::sync_job_status() must sync database Job status with RQ job status because NetBox worker doesn't always update DB when jobs stop before processing starts

Files:

  • netbox_librenms_plugin/views/imports/actions.py
🧠 Learnings (11)
📓 Common learnings
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-02-15T15:38:54.189Z
Learning: Applies to views/imports/**/*.py : REST endpoints for imports live in `views/imports/actions.py` with the list view in `views/imports/list.py`, surface via `urls.py`, and emit HTMX fragments in `templates/netbox_librenms_plugin/htmx/`; keep server responses and HTMX targets in sync
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/testing.instructions.md:0-0
Timestamp: 2026-02-15T15:39:20.744Z
Learning: Applies to tests/**/*.py : Patch deferred/inline imports at their source module (e.g., `netbox_librenms_plugin.import_utils.process_device_filters`), not the consuming module
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/testing.instructions.md:0-0
Timestamp: 2026-02-15T15:39:20.744Z
Learning: Applies to tests/**/*.py : Test coverage mapping: `librenms_api.py` → `test_librenms_api.py`, `import_utils.py`/`import_validation_helpers.py`/`utils.py` → `test_import_utils.py`/`test_import_validation_helpers.py`/`test_utils.py`, `jobs.py`/`views/imports/list.py` → `test_background_jobs.py`
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-02-15T15:38:54.189Z
Learning: Applies to **/*.py : Reuse the `librenms_api.py` LibreNMS client instead of making new `requests` calls; it handles multi-server configs via `LibreNMSSettings` model and the `servers` plugin config, plus caching
📚 Learning: 2026-02-15T15:39:20.744Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/testing.instructions.md:0-0
Timestamp: 2026-02-15T15:39:20.744Z
Learning: Applies to tests/test_import_utils.py : Cache key tests must patch `get_validated_device_cache_key` from `import_utils.py`; never hardcode key formats like `job_123_device_1`

Applied to files:

  • netbox_librenms_plugin/import_utils/cache.py
  • netbox_librenms_plugin/tests/test_import_utils.py
  • netbox_librenms_plugin/import_utils/bulk_import.py
  • netbox_librenms_plugin/import_utils/filters.py
📚 Learning: 2026-02-15T15:39:20.744Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/testing.instructions.md:0-0
Timestamp: 2026-02-15T15:39:20.744Z
Learning: Applies to tests/test_background_jobs.py : Patch `cache` where imported: `netbox_librenms_plugin.views.imports.list.cache`, not `django.core.cache.cache`

Applied to files:

  • netbox_librenms_plugin/import_utils/cache.py
  • netbox_librenms_plugin/tests/test_import_utils.py
  • netbox_librenms_plugin/import_utils/filters.py
📚 Learning: 2026-02-15T15:39:20.744Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/testing.instructions.md:0-0
Timestamp: 2026-02-15T15:39:20.744Z
Learning: Applies to tests/**/*.py : Patch deferred/inline imports at their source module (e.g., `netbox_librenms_plugin.import_utils.process_device_filters`), not the consuming module

Applied to files:

  • netbox_librenms_plugin/tests/test_import_utils.py
  • netbox_librenms_plugin/import_utils/bulk_import.py
  • netbox_librenms_plugin/import_utils/filters.py
  • netbox_librenms_plugin/views/imports/actions.py
📚 Learning: 2026-02-15T15:39:20.744Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/testing.instructions.md:0-0
Timestamp: 2026-02-15T15:39:20.744Z
Learning: Applies to tests/**/*.py : Test coverage mapping: `librenms_api.py` → `test_librenms_api.py`, `import_utils.py`/`import_validation_helpers.py`/`utils.py` → `test_import_utils.py`/`test_import_validation_helpers.py`/`test_utils.py`, `jobs.py`/`views/imports/list.py` → `test_background_jobs.py`

Applied to files:

  • netbox_librenms_plugin/tests/test_import_utils.py
📚 Learning: 2026-02-15T15:38:54.189Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-02-15T15:38:54.189Z
Learning: Applies to views/imports/**/*.py : REST endpoints for imports live in `views/imports/actions.py` with the list view in `views/imports/list.py`, surface via `urls.py`, and emit HTMX fragments in `templates/netbox_librenms_plugin/htmx/`; keep server responses and HTMX targets in sync

Applied to files:

  • netbox_librenms_plugin/tests/test_import_utils.py
  • netbox_librenms_plugin/import_utils/bulk_import.py
  • netbox_librenms_plugin/views/imports/actions.py
📚 Learning: 2026-02-15T15:39:20.744Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/testing.instructions.md:0-0
Timestamp: 2026-02-15T15:39:20.744Z
Learning: Applies to tests/**/*.py : Mock NetBox models (Device, Job, User) with `MagicMock()` instead of creating real instances

Applied to files:

  • netbox_librenms_plugin/tests/test_import_utils.py
📚 Learning: 2026-02-15T15:38:54.189Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-02-15T15:38:54.189Z
Learning: Applies to **/*.py : Matching must be exact-only for site, platform, device type, and role; do not add fuzzy matching; use the functions `find_matching_site`, `match_librenms_hardware_to_device_type`, and `find_matching_platform` from `utils.py`

Applied to files:

  • netbox_librenms_plugin/tests/test_import_utils.py
📚 Learning: 2026-02-15T15:38:54.189Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-02-15T15:38:54.189Z
Learning: Follow sync pipeline flow: fetch LibreNMS data via `librenms_api.py`, cache it using `CacheMixin`, build comparison tables in `tables/`, and render HTMX fragments in `templates/netbox_librenms_plugin/htmx/`

Applied to files:

  • netbox_librenms_plugin/views/imports/actions.py
📚 Learning: 2026-02-15T15:39:11.578Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/frontend.instructions.md:0-0
Timestamp: 2026-02-15T15:39:11.578Z
Learning: Applies to netbox_librenms_plugin/templates/netbox_librenms_plugin/*sync*.html : Sync pages should extend `librenms_sync_base.html`.

Applied to files:

  • netbox_librenms_plugin/views/imports/actions.py
📚 Learning: 2026-02-15T15:38:54.189Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-02-15T15:38:54.189Z
Learning: Applies to **/*.py : Always call `LibreNMSAPI.get_librenms_id` to retrieve the device/VM LibreNMS mapping via the `librenms_id` custom field instead of touching the field directly

Applied to files:

  • netbox_librenms_plugin/views/imports/actions.py
🧬 Code graph analysis (3)
netbox_librenms_plugin/tests/test_import_utils.py (2)
netbox_librenms_plugin/views/imports/actions.py (11)
  • get (708-728)
  • get (734-758)
  • post (250-398)
  • post (425-702)
  • post (822-829)
  • post (835-842)
  • post (848-855)
  • post (861-1117)
  • post (1129-1143)
  • DeviceValidationDetailsView (731-816)
  • _build_sync_info (761-816)
netbox_librenms_plugin/utils.py (1)
  • migrate_legacy_librenms_id (92-122)
netbox_librenms_plugin/import_utils/bulk_import.py (6)
netbox_librenms_plugin/librenms_api.py (2)
  • LibreNMSAPI (15-1039)
  • get_device_info (314-337)
netbox_librenms_plugin/import_utils/cache.py (3)
  • get_cache_metadata_key (12-27)
  • get_import_device_cache_key (151-170)
  • get_validated_device_cache_key (124-148)
netbox_librenms_plugin/import_utils/device_operations.py (2)
  • import_single_device (595-814)
  • validate_device_for_import (80-592)
netbox_librenms_plugin/import_utils/filters.py (1)
  • get_librenms_devices_for_import (45-226)
netbox_librenms_plugin/import_utils/permissions.py (1)
  • require_permissions (27-44)
netbox_librenms_plugin/import_utils/virtual_chassis.py (2)
  • create_virtual_chassis_with_members (321-464)
  • prefetch_vc_data_for_devices (83-117)
netbox_librenms_plugin/import_utils/virtual_chassis.py (4)
netbox_librenms_plugin/views/mixins.py (1)
  • librenms_api (217-231)
netbox_librenms_plugin/librenms_api.py (3)
  • LibreNMSAPI (15-1039)
  • get_device_info (314-337)
  • get_inventory_filtered (726-803)
netbox_librenms_plugin/views/imports/actions.py (2)
  • get (708-728)
  • get (734-758)
netbox_librenms_plugin/models.py (1)
  • LibreNMSSettings (7-48)
🔇 Additional comments (8)
netbox_librenms_plugin/views/imports/actions.py (8)

39-66: LGTM!

The naming preference resolution logic correctly implements the POST → user pref → plugin settings fallback chain. Reusing the settings variable avoids duplicate DB queries when both preferences need the settings fallback.


147-158: LGTM!

Clean integration of naming preferences through the helper function, with proper server_key propagation for multi-server support.


760-816: LGTM!

The sync comparison logic correctly handles all cases. The device_type_synced now properly returns False when hardware is present but no matching device type is found (lines 802-803), addressing the previous review concern.


886-890: Validated conflict target requirement implemented.

The code now correctly requires validated_existing before proceeding with conflict actions, addressing the previous review concern about bypassing validation context.


932-1066: LGTM!

The action handlers correctly:

  • Use set_librenms_device_id per coding guidelines instead of direct field access
  • Apply escape() to user-controlled values in error responses (addressing previous XSS concern)
  • Use _resolve_naming_preferences for consistent naming fallback behavior
  • Use find_matching_platform and match_librenms_hardware_to_device_type for exact matching

1068-1101: Legacy ID verification implemented correctly.

The migration now verifies that the stored legacy librenms_id matches the active LibreNMS device ID (lines 1085-1090), preventing incorrect associations. The direct custom_field_data access is appropriately justified in the comment—the accessor normalizes return values, but migration requires detecting the raw format.


1106-1117: LGTM!

The post-action flow correctly clears the cache, re-validates the device with updated state, and triggers the modal close via HTMX header.


670-671: LGTM!

Server key propagation to validate_device_for_import maintains consistency with multi-server configuration support.

Comment thread netbox_librenms_plugin/import_utils/bulk_import.py Outdated
Comment thread netbox_librenms_plugin/import_utils/filters.py Outdated
Comment thread netbox_librenms_plugin/import_utils/virtual_chassis.py Outdated
Comment thread netbox_librenms_plugin/import_utils/virtual_chassis.py
Comment thread netbox_librenms_plugin/tests/test_import_utils.py
Comment thread netbox_librenms_plugin/tests/test_import_utils.py
Comment thread netbox_librenms_plugin/views/imports/actions.py Outdated
Comment thread netbox_librenms_plugin/views/imports/actions.py Outdated
- bulk_import.py: normalize status (int/str) in show_disabled filter
- bulk_import.py: use api.server_key (resolved) in import_single_device call
- filters.py: normalize status in count_librenms_devices show_disabled filter
- virtual_chassis.py: fix 0-based fallback for vc_position (idx+1 not idx)
- virtual_chassis.py: advance sequential counter past discovered_pos; normalize pos=0 to absent
- actions.py: add django.db.transaction import
- actions.py: module-level _FORCE_REQUIRED_ACTIONS frozenset
- actions.py: defensive .get('resolved_name') instead of [] access
- actions.py: add NetBoxObjectPermissionMixin to DeviceConflictActionView
- actions.py: wrap librenms_id collision check+write in transaction.atomic()
- tests: fix unknown-action test to include existing_device in validation dict
- tests: update warning text assertion to 'hostname differs'
- tests: add transaction mock to 6 link/update/update_serial tests
- tests: add TestProcessDeviceFilters with 5 new tests for status norm + api.server_key

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 3

♻️ Duplicate comments (1)
netbox_librenms_plugin/views/imports/actions.py (1)

1093-1099: ⚠️ Potential issue | 🟠 Major

Avoid direct custom_field_data access for LibreNMS mapping reads.

This migration path reads librenms_id directly from existing_device.custom_field_data. Please move legacy-format detection behind a LibreNMS API/helper method and consume that method here to keep mapping access contract-consistent.

As per coding guidelines, "Always call LibreNMSAPI.get_librenms_id to retrieve the device/VM LibreNMS mapping via the librenms_id custom field instead of touching the field directly".

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@netbox_librenms_plugin/views/imports/actions.py` around lines 1093 - 1099,
The code reads the legacy librenms_id directly from
existing_device.custom_field_data to detect integer vs JSON formats; instead
call the helper LibreNMSAPI.get_librenms_id(existing_device) (or the equivalent
API method used elsewhere) to obtain the mapping and perform legacy-format
detection inside that helper, then consume its result here; remove direct access
to existing_device.custom_field_data.get("librenms_id") and update the
early-return branch to use the helper's output (e.g., treat non-legacy/JSON
result as "no migration needed") so mapping access contract remains consistent.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@netbox_librenms_plugin/import_utils/virtual_chassis.py`:
- Around line 201-222: The suggested VC member name is off-by-one because
position is already 1-based but the code calls
_generate_vc_member_name(master_name, position + 1) and likewise builds the
fallback f"Member-{position + 1}"; change both calls to use position (not
position + 1) so the generated suggested_name uses the normalized 1-based
position; update all occurrences (e.g., the block populating member_data where
suggested_name is set and the similar code around the other occurrence
mentioned) to pass position directly to _generate_vc_member_name and to format
f"Member-{position}".
- Around line 35-40: The code in virtual_chassis.py assigns a zero-based
fallback position (idx) when parsing member positions, which can produce
position 0; update the fallback to be 1-based by using idx+1 in the except
branch so member_copy["position"] = idx+1 instead of idx. Locate the block that
reads raw_position, tries int(raw_position), and appends member_copy (variables:
raw_position, member_copy["position"], members.append) and change the fallback
assignment to idx+1.

In `@netbox_librenms_plugin/views/imports/actions.py`:
- Around line 791-793: platform_synced can end up as None because the chained
expression (netbox_platform and matching_platform and netbox_platform.pk ==
matching_platform.pk) can return a non-boolean; update the assignment for
platform_synced to ensure a boolean is produced by casting that comparison
branch with bool(...) so platform_synced = librenms_os == "-" or
bool(netbox_platform and matching_platform and netbox_platform.pk ==
matching_platform.pk), and apply the same boolean cast to the analogous
expression referenced around the matching lines (809-810) to keep all sync flags
type-stable.

---

Duplicate comments:
In `@netbox_librenms_plugin/views/imports/actions.py`:
- Around line 1093-1099: The code reads the legacy librenms_id directly from
existing_device.custom_field_data to detect integer vs JSON formats; instead
call the helper LibreNMSAPI.get_librenms_id(existing_device) (or the equivalent
API method used elsewhere) to obtain the mapping and perform legacy-format
detection inside that helper, then consume its result here; remove direct access
to existing_device.custom_field_data.get("librenms_id") and update the
early-return branch to use the helper's output (e.g., treat non-legacy/JSON
result as "no migration needed") so mapping access contract remains consistent.

ℹ️ Review info

Configuration used: Repository UI (base), Organization UI (inherited)

Review profile: ASSERTIVE

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 0b9ae51 and 022293b.

📒 Files selected for processing (5)
  • netbox_librenms_plugin/import_utils/bulk_import.py
  • netbox_librenms_plugin/import_utils/filters.py
  • netbox_librenms_plugin/import_utils/virtual_chassis.py
  • netbox_librenms_plugin/tests/test_import_utils.py
  • netbox_librenms_plugin/views/imports/actions.py
📜 Review details
🧰 Additional context used
📓 Path-based instructions (2)
**/*.py

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

**/*.py: Reuse the librenms_api.py LibreNMS client instead of making new requests calls; it handles multi-server configs via LibreNMSSettings model and the servers plugin config, plus caching
Always call LibreNMSAPI.get_librenms_id to retrieve the device/VM LibreNMS mapping via the librenms_id custom field instead of touching the field directly
Matching must be exact-only for site, platform, device type, and role; do not add fuzzy matching; use the functions find_matching_site, match_librenms_hardware_to_device_type, and find_matching_platform from utils.py

Files:

  • netbox_librenms_plugin/import_utils/bulk_import.py
  • netbox_librenms_plugin/import_utils/filters.py
  • netbox_librenms_plugin/views/imports/actions.py
  • netbox_librenms_plugin/import_utils/virtual_chassis.py
**/views/imports/**

📄 CodeRabbit inference engine (.github/instructions/background-jobs.instructions.md)

**/views/imports/**: Use Job UUID (job.job_id) for RQ API endpoints: /api/core/background-tasks/{uuid}/
Use Job PK (job.pk) for database endpoints and result loading
Check rq_job.is_stopped or rq_job.is_failed flags in Redis for cancellation detection, not database status
Call /api/core/background-tasks/{uuid}/stop/ to stop RQ job in the job cancellation flow
Call plugin's sync endpoint /api/plugins/librenms_plugin/jobs/{pk}/sync-status/ to update database after stopping RQ job
Poll /api/core/background-tasks/{uuid}/ for real-time RQ status instead of polling the database
api/views.py::sync_job_status() must sync database Job status with RQ job status because NetBox worker doesn't always update DB when jobs stop before processing starts

Files:

  • netbox_librenms_plugin/views/imports/actions.py
🧠 Learnings (9)
📓 Common learnings
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/testing.instructions.md:0-0
Timestamp: 2026-02-15T15:39:20.744Z
Learning: Applies to tests/**/*.py : Patch deferred/inline imports at their source module (e.g., `netbox_librenms_plugin.import_utils.process_device_filters`), not the consuming module
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-02-15T15:38:54.189Z
Learning: Applies to views/imports/**/*.py : REST endpoints for imports live in `views/imports/actions.py` with the list view in `views/imports/list.py`, surface via `urls.py`, and emit HTMX fragments in `templates/netbox_librenms_plugin/htmx/`; keep server responses and HTMX targets in sync
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/testing.instructions.md:0-0
Timestamp: 2026-02-15T15:39:20.744Z
Learning: Applies to tests/**/*.py : Test coverage mapping: `librenms_api.py` → `test_librenms_api.py`, `import_utils.py`/`import_validation_helpers.py`/`utils.py` → `test_import_utils.py`/`test_import_validation_helpers.py`/`test_utils.py`, `jobs.py`/`views/imports/list.py` → `test_background_jobs.py`
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-02-15T15:38:54.189Z
Learning: Applies to **/*.py : Reuse the `librenms_api.py` LibreNMS client instead of making new `requests` calls; it handles multi-server configs via `LibreNMSSettings` model and the `servers` plugin config, plus caching
📚 Learning: 2026-02-15T15:38:54.189Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-02-15T15:38:54.189Z
Learning: Applies to views/imports/**/*.py : REST endpoints for imports live in `views/imports/actions.py` with the list view in `views/imports/list.py`, surface via `urls.py`, and emit HTMX fragments in `templates/netbox_librenms_plugin/htmx/`; keep server responses and HTMX targets in sync

Applied to files:

  • netbox_librenms_plugin/import_utils/bulk_import.py
  • netbox_librenms_plugin/views/imports/actions.py
📚 Learning: 2026-02-15T15:39:20.744Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/testing.instructions.md:0-0
Timestamp: 2026-02-15T15:39:20.744Z
Learning: Applies to tests/**/*.py : Patch deferred/inline imports at their source module (e.g., `netbox_librenms_plugin.import_utils.process_device_filters`), not the consuming module

Applied to files:

  • netbox_librenms_plugin/import_utils/bulk_import.py
  • netbox_librenms_plugin/import_utils/filters.py
  • netbox_librenms_plugin/views/imports/actions.py
  • netbox_librenms_plugin/import_utils/virtual_chassis.py
📚 Learning: 2026-02-15T15:38:54.189Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-02-15T15:38:54.189Z
Learning: Follow sync pipeline flow: fetch LibreNMS data via `librenms_api.py`, cache it using `CacheMixin`, build comparison tables in `tables/`, and render HTMX fragments in `templates/netbox_librenms_plugin/htmx/`

Applied to files:

  • netbox_librenms_plugin/import_utils/bulk_import.py
  • netbox_librenms_plugin/views/imports/actions.py
📚 Learning: 2026-02-15T15:39:20.744Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/testing.instructions.md:0-0
Timestamp: 2026-02-15T15:39:20.744Z
Learning: Applies to tests/test_import_utils.py : Cache key tests must patch `get_validated_device_cache_key` from `import_utils.py`; never hardcode key formats like `job_123_device_1`

Applied to files:

  • netbox_librenms_plugin/import_utils/bulk_import.py
  • netbox_librenms_plugin/import_utils/filters.py
📚 Learning: 2026-02-15T15:39:20.744Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/testing.instructions.md:0-0
Timestamp: 2026-02-15T15:39:20.744Z
Learning: Applies to tests/test_background_jobs.py : Patch `cache` where imported: `netbox_librenms_plugin.views.imports.list.cache`, not `django.core.cache.cache`

Applied to files:

  • netbox_librenms_plugin/import_utils/filters.py
  • netbox_librenms_plugin/views/imports/actions.py
📚 Learning: 2026-02-15T15:38:54.189Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-02-15T15:38:54.189Z
Learning: Applies to {navigation.py,urls.py,api/**} : Respect NetBox plugin APIs in `navigation.py`, `urls.py`, and `api/` directories

Applied to files:

  • netbox_librenms_plugin/views/imports/actions.py
📚 Learning: 2026-02-15T15:39:11.578Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/frontend.instructions.md:0-0
Timestamp: 2026-02-15T15:39:11.578Z
Learning: Applies to netbox_librenms_plugin/templates/netbox_librenms_plugin/*sync*.html : Sync pages should extend `librenms_sync_base.html`.

Applied to files:

  • netbox_librenms_plugin/views/imports/actions.py
📚 Learning: 2026-02-15T15:38:54.189Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-02-15T15:38:54.189Z
Learning: Applies to **/*.py : Always call `LibreNMSAPI.get_librenms_id` to retrieve the device/VM LibreNMS mapping via the `librenms_id` custom field instead of touching the field directly

Applied to files:

  • netbox_librenms_plugin/views/imports/actions.py
🧬 Code graph analysis (3)
netbox_librenms_plugin/import_utils/bulk_import.py (6)
netbox_librenms_plugin/librenms_api.py (2)
  • LibreNMSAPI (15-1039)
  • get_device_info (314-337)
netbox_librenms_plugin/import_utils/cache.py (3)
  • get_cache_metadata_key (12-27)
  • get_import_device_cache_key (151-170)
  • get_validated_device_cache_key (124-148)
netbox_librenms_plugin/import_utils/device_operations.py (2)
  • import_single_device (595-814)
  • validate_device_for_import (80-592)
netbox_librenms_plugin/import_utils/filters.py (1)
  • get_librenms_devices_for_import (53-234)
netbox_librenms_plugin/import_utils/permissions.py (1)
  • require_permissions (27-44)
netbox_librenms_plugin/import_utils/virtual_chassis.py (3)
  • create_virtual_chassis_with_members (323-473)
  • empty_virtual_chassis_data (15-23)
  • prefetch_vc_data_for_devices (83-117)
netbox_librenms_plugin/views/imports/actions.py (11)
netbox_librenms_plugin/utils.py (3)
  • get_user_pref (258-262)
  • save_user_pref (265-271)
  • set_librenms_device_id (45-67)
netbox_librenms_plugin/views/mixins.py (2)
  • LibreNMSAPIMixin (196-282)
  • librenms_api (217-231)
netbox_librenms_plugin/models.py (1)
  • LibreNMSSettings (7-48)
netbox_librenms_plugin/views/settings_views.py (3)
  • get (26-43)
  • post (45-122)
  • post (131-194)
netbox_librenms_plugin/views/imports/list.py (1)
  • get (124-343)
netbox_librenms_plugin/views/base/librenms_sync_view.py (1)
  • get (25-46)
netbox_librenms_plugin/import_utils/device_operations.py (2)
  • validate_device_for_import (80-592)
  • _determine_device_name (24-77)
netbox_librenms_plugin/views/sync/interfaces.py (2)
  • post (33-80)
  • post (304-381)
netbox_librenms_plugin/views/object_sync/devices.py (4)
  • post (106-146)
  • post (158-245)
  • post (284-320)
  • post (334-362)
netbox_librenms_plugin/views/sync/device_fields.py (6)
  • post (19-56)
  • post (66-110)
  • post (120-171)
  • post (181-238)
  • post (251-308)
  • post (318-384)
netbox_librenms_plugin/import_utils/cache.py (1)
  • get_import_device_cache_key (151-170)
netbox_librenms_plugin/import_utils/virtual_chassis.py (2)
netbox_librenms_plugin/librenms_api.py (2)
  • get_device_info (314-337)
  • get_inventory_filtered (726-803)
netbox_librenms_plugin/models.py (1)
  • LibreNMSSettings (7-48)
🔇 Additional comments (5)
netbox_librenms_plugin/import_utils/bulk_import.py (1)

366-376: Good status normalization for active-device filtering.

This safely handles both 1 and "1" responses and avoids dropping valid active devices.

netbox_librenms_plugin/import_utils/filters.py (2)

38-49: Active-status filtering is now robust.

The conversion-based check correctly handles mixed LibreNMS status types without false exclusions.


190-196: Deterministic cache-key construction looks solid.

Using a stable digest for API/client filter payloads is a good reliability improvement for cache reuse.

netbox_librenms_plugin/views/imports/actions.py (1)

922-941: Nice hardening of the conflict check + write path.

Grouping the LibreNMS ID collision check and mutation in a single transaction is a meaningful safety improvement.

netbox_librenms_plugin/import_utils/virtual_chassis.py (1)

415-423: Good fix for sequential fallback position advancement.

Advancing the counter past discovered positions prevents accidental position reuse for later fallback members.

Comment thread netbox_librenms_plugin/import_utils/virtual_chassis.py Outdated
Comment thread netbox_librenms_plugin/import_utils/virtual_chassis.py
Comment thread netbox_librenms_plugin/views/imports/actions.py Outdated
…m_synced bool

- bulk_import_devices_shared: check RQ/Redis job state first (is_stopped/is_failed)
  before falling back to DB status, matching pattern in process_device_filters();
  also check on idx==1 (first iteration) not just idx%5==0
- virtual_chassis._clone_virtual_chassis_data: position fallback now idx+1 (1-based)
  instead of idx (0-based) so position 0 is never produced
- virtual_chassis.detect_virtual_chassis_from_inventory: suggested_name uses
  position directly (not position+1) since position is already 1-based after
  batch 6 fallback fix
- actions._build_sync_info: wrap platform_synced expression in bool() so lazy
  and/or chain never returns None when matching_platform is None or netbox_platform
  is None; all sync flag types are now stable booleans
- tests: add TestVCPositionHandling (4 tests) and TestBulkImportCancellation (4 tests);
  add 2 TestBuildSyncInfo tests for platform_synced=False type-stability
- Template: cluster/rack badge → text+tick icon (0a24a51 completion)
- Template: naming preference badges in Device Information card header (d1c015f)
- Template: keep tooltip on device_type match (existing librenms_id feature)
- actions.py: _resolve_naming_preferences gains GET support for HTMX hx-include (d1c015f)
- actions.py: DeviceValidationDetailsView passes use_sysname/strip_domain to template
- actions.py: add _save_device() helper with full_clean()+ValidationError+IntegrityError handling
- actions.py: replace all existing_device.save() with _save_device() in conflict actions
- device_operations.py: add naming_criteria dict to validate_device_for_import result
- device_operations.py: VC-aware name comparison for librenms_id-matched devices
- device_operations.py: call update_vc_member_suggested_names when VC is a stack
- bulk_import.py: add use_sysname/strip_domain params to process_device_filters
- jobs.py: add use_sysname/strip_domain params to FilterDevicesJob.run()
- list.py: resolve naming prefs and pass use_sysname/strip_domain to process_device_filters
- list.py: pass use_sysname/strip_domain to FilterDevicesJob enqueue
- tables/device_status.py: add #use-sysname-toggle, #strip-domain-toggle to all 4 Details hx-include
- librenms_import.js: re-init Bootstrap tooltips after modal HTMX content swap (d1c015f)
- tests: add TestNameMatchesWithNamingPreferences (8 tests: VC-aware name matching)
- tests: fix existing MagicMock devices to set virtual_chassis=None/vc_position=None
)

Replace bare 'except Exception: pass' with logger.exception() so database
and config problems are visible in logs rather than silently swallowed.
Addresses Copilot review comment on bonzo81#227.
Without this, re-validation after import always uses defaults
(use_sysname=True, strip_domain=False) instead of the user's
actual choices, causing incorrect name-match status in the row.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 5

♻️ Duplicate comments (1)
netbox_librenms_plugin/import_utils/virtual_chassis.py (1)

309-317: ⚠️ Potential issue | 🟠 Major

Fix 1-based VC position handling in suggested-name regeneration.

Line 310 defaults to idx (0-based), and Line 315 applies +1, which shifts suggested names and can desync stored/displayed positions.

🔧 Proposed fix
-    for idx, member in enumerate(vc_data.get("members", [])):
-        raw_position = member.get("position", idx)
+    for idx, member in enumerate(vc_data.get("members", [])):
+        raw_position = member.get("position", idx + 1)
         try:
             base_position = int(raw_position)
         except (TypeError, ValueError):
-            base_position = idx
-        position = base_position + 1  # Convert to 1-based position
-        member["position"] = base_position
+            base_position = idx + 1
+        if base_position < 1:
+            base_position = idx + 1
+        position = base_position
+        member["position"] = position
         member["suggested_name"] = _generate_vc_member_name(
             master_name, position, serial=member.get("serial"), pattern=vc_pattern
         )
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@netbox_librenms_plugin/import_utils/virtual_chassis.py` around lines 309 -
317, The loop currently defaults raw_position to idx (0-based) and then adds +1
to create position, causing a mismatch between stored member["position"]
(base_position) and the suggested-name position; change the default to
member.get("position", idx + 1) so missing positions default to 1-based, compute
base_position = int(raw_position) as before, set position = base_position
(remove the +1 adjustment), and store the 1-based value into member["position"];
update usage of position passed to _generate_vc_member_name to use this 1-based
position so suggested names and stored/displayed positions stay in sync for
vc_data members.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@netbox_librenms_plugin/import_utils/bulk_import.py`:
- Around line 482-487: The cache key created by get_validated_device_cache_key
does not incorporate the naming flags (use_sysname and strip_domain), causing
cached validation results to be reused when those preferences change; update the
cache key construction to include the relevant naming preference flags
(use_sysname and strip_domain) so that the key is unique per naming
configuration, and apply the same change to the other callsite(s) that invoke
get_validated_device_cache_key (the calls around the second occurrence noted) to
avoid returning stale validation results.
- Around line 441-446: The DB fallback currently only treats
JobStatusChoices.STATUS_FAILED as terminal; update the checks that call
job.job.refresh_from_db() and then inspect job.job.status (the blocks that
return _empty_return(return_cache_status)) to treat both
JobStatusChoices.STATUS_FAILED and JobStatusChoices.STATUS_ERRORED as terminal
states, i.e., check for status in {JobStatusChoices.STATUS_FAILED,
JobStatusChoices.STATUS_ERRORED} and return _empty_return(return_cache_status)
when matched; apply this change to the occurrence around the try/except that
logs "Job was stopped before validation started" and the similar fallback block
later in the file.

In `@netbox_librenms_plugin/import_utils/device_operations.py`:
- Line 770: The mapping incorrectly treats only integer 1 as active because it
compares libre_device.get("status") == 1; normalize the LibreNMS status first
(e.g., coerce to int safely or compare against both "1" and 1) before mapping to
the NetBox "status" field so string "1" is treated as active; update the
expression that builds the dict (the call using libre_device.get("status")) to
perform the normalization (handle None/invalid values with a safe default) and
then set "status": "active" if normalized_status == 1 else "offline".

In `@netbox_librenms_plugin/static/netbox_librenms_plugin/js/librenms_import.js`:
- Around line 1098-1102: The current code calls
modalContent.querySelectorAll(...) without verifying modalContent is non-null,
which can throw; update the block that re-initializes tooltips (the
bootstrap.Tooltip section referencing modalContent and tooltipEls) to first
check that modalContent is truthy (e.g., if (!modalContent) return or skip)
before calling querySelectorAll, and only proceed to collect tooltipEls and
instantiate new bootstrap.Tooltip(el) when modalContent exists.

In `@netbox_librenms_plugin/views/imports/list.py`:
- Around line 262-273: The code currently ignores submitted form toggles when
user prefs are unset; update the assignment of _use_sysname and _strip_domain so
they first use the saved preference (_use_sysname_pref / _strip_domain_pref) if
not None, otherwise use the submitted form value (e.g.
request.POST.get('use_sysname') and request.POST.get('strip_domain') or the
form's cleaned_data equivalents) and only fall back to getattr(settings,
"use_sysname_default", True) / getattr(settings, "strip_domain_default", False)
if no form value is provided; apply the same fix for the duplicate logic around
the variables at the later block (the code referenced at lines ~442-451).

---

Duplicate comments:
In `@netbox_librenms_plugin/import_utils/virtual_chassis.py`:
- Around line 309-317: The loop currently defaults raw_position to idx (0-based)
and then adds +1 to create position, causing a mismatch between stored
member["position"] (base_position) and the suggested-name position; change the
default to member.get("position", idx + 1) so missing positions default to
1-based, compute base_position = int(raw_position) as before, set position =
base_position (remove the +1 adjustment), and store the 1-based value into
member["position"]; update usage of position passed to _generate_vc_member_name
to use this 1-based position so suggested names and stored/displayed positions
stay in sync for vc_data members.

ℹ️ Review info

Configuration used: Repository UI (base), Organization UI (inherited)

Review profile: ASSERTIVE

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 022293b and 0f72ceb.

📒 Files selected for processing (10)
  • netbox_librenms_plugin/import_utils/bulk_import.py
  • netbox_librenms_plugin/import_utils/device_operations.py
  • netbox_librenms_plugin/import_utils/virtual_chassis.py
  • netbox_librenms_plugin/jobs.py
  • netbox_librenms_plugin/static/netbox_librenms_plugin/js/librenms_import.js
  • netbox_librenms_plugin/tables/device_status.py
  • netbox_librenms_plugin/templates/netbox_librenms_plugin/htmx/device_validation_details.html
  • netbox_librenms_plugin/tests/test_import_utils.py
  • netbox_librenms_plugin/views/imports/actions.py
  • netbox_librenms_plugin/views/imports/list.py
📜 Review details
🧰 Additional context used
📓 Path-based instructions (6)
**/*.py

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

**/*.py: Reuse the librenms_api.py LibreNMS client instead of making new requests calls; it handles multi-server configs via LibreNMSSettings model and the servers plugin config, plus caching
Always call LibreNMSAPI.get_librenms_id to retrieve the device/VM LibreNMS mapping via the librenms_id custom field instead of touching the field directly
Matching must be exact-only for site, platform, device type, and role; do not add fuzzy matching; use the functions find_matching_site, match_librenms_hardware_to_device_type, and find_matching_platform from utils.py

Files:

  • netbox_librenms_plugin/tables/device_status.py
  • netbox_librenms_plugin/views/imports/list.py
  • netbox_librenms_plugin/import_utils/bulk_import.py
  • netbox_librenms_plugin/import_utils/virtual_chassis.py
  • netbox_librenms_plugin/import_utils/device_operations.py
  • netbox_librenms_plugin/jobs.py
  • netbox_librenms_plugin/views/imports/actions.py
netbox_librenms_plugin/templates/**/*.html

📄 CodeRabbit inference engine (.github/instructions/frontend.instructions.md)

netbox_librenms_plugin/templates/**/*.html: HTMX 2.x is the primary async layer. Table row updates should return <tr hx-swap-oob="true">.
Avoid outerHTML swaps in HTMX; use OOB or targeted innerHTML swaps to keep table layout intact.
Modals use Tabler (Bootstrap-like) but without bootstrap.Modal helpers.
Modal buttons should target the htmx-modal-content element. JavaScript in librenms_import.html toggles the wrapper; do not reintroduce data-bs-toggle or duplicate modal IDs.
Keep <select class="device-role-select"> markup stable to preserve JavaScript hook-up with TomSelect decorators.
Styling assumes Tabler defaults. Do not add table-responsive wrappers as they were deliberately removed to prevent dropdown clipping.

Files:

  • netbox_librenms_plugin/templates/netbox_librenms_plugin/htmx/device_validation_details.html
netbox_librenms_plugin/templates/netbox_librenms_plugin/htmx/**/*.html

📄 CodeRabbit inference engine (.github/instructions/frontend.instructions.md)

HTMX fragments should live in templates/netbox_librenms_plugin/htmx/. Keep server responses and HTMX targets in sync.

Files:

  • netbox_librenms_plugin/templates/netbox_librenms_plugin/htmx/device_validation_details.html
netbox_librenms_plugin/templates/netbox_librenms_plugin/**/*.html

📄 CodeRabbit inference engine (.github/instructions/frontend.instructions.md)

Templates should live in templates/netbox_librenms_plugin/. Reusable template includes should be placed under inc/ subdirectory.

Files:

  • netbox_librenms_plugin/templates/netbox_librenms_plugin/htmx/device_validation_details.html
**/views/imports/**

📄 CodeRabbit inference engine (.github/instructions/background-jobs.instructions.md)

**/views/imports/**: Use Job UUID (job.job_id) for RQ API endpoints: /api/core/background-tasks/{uuid}/
Use Job PK (job.pk) for database endpoints and result loading
Check rq_job.is_stopped or rq_job.is_failed flags in Redis for cancellation detection, not database status
Call /api/core/background-tasks/{uuid}/stop/ to stop RQ job in the job cancellation flow
Call plugin's sync endpoint /api/plugins/librenms_plugin/jobs/{pk}/sync-status/ to update database after stopping RQ job
Poll /api/core/background-tasks/{uuid}/ for real-time RQ status instead of polling the database
api/views.py::sync_job_status() must sync database Job status with RQ job status because NetBox worker doesn't always update DB when jobs stop before processing starts

Files:

  • netbox_librenms_plugin/views/imports/list.py
  • netbox_librenms_plugin/views/imports/actions.py
**/jobs.py

📄 CodeRabbit inference engine (.github/instructions/background-jobs.instructions.md)

**/jobs.py: Background jobs must use NetBox's JobRunner base class (netbox.jobs.JobRunner) for long-running operations like device filtering with VC detection
Jobs must run via Redis Queue (RQ) in Redis, separate from the database Job model, and real-time status must be checked via RQ, not the database
RQ status values must be: queued, started, finished, stopped, failed (NOT completed)
Database Job status values must be: pending, scheduled, running, completed, failed, errored (NO cancelled status exists)

Files:

  • netbox_librenms_plugin/jobs.py
🧠 Learnings (18)
📓 Common learnings
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-02-15T15:38:54.189Z
Learning: Applies to views/imports/**/*.py : REST endpoints for imports live in `views/imports/actions.py` with the list view in `views/imports/list.py`, surface via `urls.py`, and emit HTMX fragments in `templates/netbox_librenms_plugin/htmx/`; keep server responses and HTMX targets in sync
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/testing.instructions.md:0-0
Timestamp: 2026-02-15T15:39:20.744Z
Learning: Applies to tests/**/*.py : Patch deferred/inline imports at their source module (e.g., `netbox_librenms_plugin.import_utils.process_device_filters`), not the consuming module
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/testing.instructions.md:0-0
Timestamp: 2026-02-15T15:39:20.744Z
Learning: Applies to tests/**/*.py : Test coverage mapping: `librenms_api.py` → `test_librenms_api.py`, `import_utils.py`/`import_validation_helpers.py`/`utils.py` → `test_import_utils.py`/`test_import_validation_helpers.py`/`test_utils.py`, `jobs.py`/`views/imports/list.py` → `test_background_jobs.py`
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-02-15T15:38:54.189Z
Learning: Applies to **/*.py : Reuse the `librenms_api.py` LibreNMS client instead of making new `requests` calls; it handles multi-server configs via `LibreNMSSettings` model and the `servers` plugin config, plus caching
📚 Learning: 2026-02-15T15:39:11.578Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/frontend.instructions.md:0-0
Timestamp: 2026-02-15T15:39:11.578Z
Learning: Applies to netbox_librenms_plugin/templates/**/*.html : HTMX 2.x is the primary async layer. Table row updates should return `<tr hx-swap-oob="true">`.

Applied to files:

  • netbox_librenms_plugin/tables/device_status.py
  • netbox_librenms_plugin/templates/netbox_librenms_plugin/htmx/device_validation_details.html
  • netbox_librenms_plugin/static/netbox_librenms_plugin/js/librenms_import.js
📚 Learning: 2026-02-15T15:39:11.578Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/frontend.instructions.md:0-0
Timestamp: 2026-02-15T15:39:11.578Z
Learning: Applies to netbox_librenms_plugin/templates/**/*.html : Avoid `outerHTML` swaps in HTMX; use OOB or targeted `innerHTML` swaps to keep table layout intact.

Applied to files:

  • netbox_librenms_plugin/tables/device_status.py
  • netbox_librenms_plugin/templates/netbox_librenms_plugin/htmx/device_validation_details.html
  • netbox_librenms_plugin/static/netbox_librenms_plugin/js/librenms_import.js
📚 Learning: 2026-02-15T15:39:11.578Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/frontend.instructions.md:0-0
Timestamp: 2026-02-15T15:39:11.578Z
Learning: Applies to netbox_librenms_plugin/templates/**/*.html : Modal buttons should target the `htmx-modal-content` element. JavaScript in `librenms_import.html` toggles the wrapper; do not reintroduce `data-bs-toggle` or duplicate modal IDs.

Applied to files:

  • netbox_librenms_plugin/tables/device_status.py
  • netbox_librenms_plugin/templates/netbox_librenms_plugin/htmx/device_validation_details.html
  • netbox_librenms_plugin/static/netbox_librenms_plugin/js/librenms_import.js
📚 Learning: 2026-02-15T15:39:11.578Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/frontend.instructions.md:0-0
Timestamp: 2026-02-15T15:39:11.578Z
Learning: Applies to netbox_librenms_plugin/templates/**/*.html : Keep `<select class="device-role-select">` markup stable to preserve JavaScript hook-up with TomSelect decorators.

Applied to files:

  • netbox_librenms_plugin/tables/device_status.py
  • netbox_librenms_plugin/templates/netbox_librenms_plugin/htmx/device_validation_details.html
  • netbox_librenms_plugin/views/imports/actions.py
📚 Learning: 2026-02-15T15:39:11.578Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/frontend.instructions.md:0-0
Timestamp: 2026-02-15T15:39:11.578Z
Learning: Applies to netbox_librenms_plugin/templates/netbox_librenms_plugin/htmx/**/*.html : HTMX fragments should live in `templates/netbox_librenms_plugin/htmx/`. Keep server responses and HTMX targets in sync.

Applied to files:

  • netbox_librenms_plugin/tables/device_status.py
  • netbox_librenms_plugin/templates/netbox_librenms_plugin/htmx/device_validation_details.html
  • netbox_librenms_plugin/static/netbox_librenms_plugin/js/librenms_import.js
📚 Learning: 2026-02-15T15:38:54.189Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-02-15T15:38:54.189Z
Learning: Applies to views/imports/**/*.py : REST endpoints for imports live in `views/imports/actions.py` with the list view in `views/imports/list.py`, surface via `urls.py`, and emit HTMX fragments in `templates/netbox_librenms_plugin/htmx/`; keep server responses and HTMX targets in sync

Applied to files:

  • netbox_librenms_plugin/tables/device_status.py
  • netbox_librenms_plugin/templates/netbox_librenms_plugin/htmx/device_validation_details.html
  • netbox_librenms_plugin/views/imports/list.py
  • netbox_librenms_plugin/import_utils/bulk_import.py
  • netbox_librenms_plugin/import_utils/device_operations.py
  • netbox_librenms_plugin/static/netbox_librenms_plugin/js/librenms_import.js
  • netbox_librenms_plugin/views/imports/actions.py
📚 Learning: 2026-02-15T15:39:11.578Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/frontend.instructions.md:0-0
Timestamp: 2026-02-15T15:39:11.578Z
Learning: Tables drive most UIs via `tables/*.py` renderers that emit HTMX-enabled columns and buttons. Prefer updating the table renderer in Python rather than templates when changing row actions.

Applied to files:

  • netbox_librenms_plugin/tables/device_status.py
📚 Learning: 2026-02-15T15:38:54.189Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-02-15T15:38:54.189Z
Learning: Applies to tables/**/*.py : Prefer updating the table renderer in `tables/*.py` rather than templates when changing row actions, since tables emit HTMX-enabled columns and buttons

Applied to files:

  • netbox_librenms_plugin/tables/device_status.py
📚 Learning: 2026-02-15T15:39:11.578Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/frontend.instructions.md:0-0
Timestamp: 2026-02-15T15:39:11.578Z
Learning: Applies to netbox_librenms_plugin/templates/**/*.html : Modals use Tabler (Bootstrap-like) but without `bootstrap.Modal` helpers.

Applied to files:

  • netbox_librenms_plugin/templates/netbox_librenms_plugin/htmx/device_validation_details.html
  • netbox_librenms_plugin/static/netbox_librenms_plugin/js/librenms_import.js
📚 Learning: 2026-02-15T15:39:11.578Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/frontend.instructions.md:0-0
Timestamp: 2026-02-15T15:39:11.578Z
Learning: Applies to netbox_librenms_plugin/templates/netbox_librenms_plugin/*sync*.html : Sync pages should extend `librenms_sync_base.html`.

Applied to files:

  • netbox_librenms_plugin/templates/netbox_librenms_plugin/htmx/device_validation_details.html
  • netbox_librenms_plugin/views/imports/actions.py
📚 Learning: 2026-02-15T15:39:20.744Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/testing.instructions.md:0-0
Timestamp: 2026-02-15T15:39:20.744Z
Learning: Applies to tests/test_background_jobs.py : Patch `cache` where imported: `netbox_librenms_plugin.views.imports.list.cache`, not `django.core.cache.cache`

Applied to files:

  • netbox_librenms_plugin/views/imports/list.py
  • netbox_librenms_plugin/views/imports/actions.py
📚 Learning: 2026-02-15T15:39:20.744Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/testing.instructions.md:0-0
Timestamp: 2026-02-15T15:39:20.744Z
Learning: Applies to tests/**/*.py : Patch deferred/inline imports at their source module (e.g., `netbox_librenms_plugin.import_utils.process_device_filters`), not the consuming module

Applied to files:

  • netbox_librenms_plugin/views/imports/list.py
  • netbox_librenms_plugin/import_utils/bulk_import.py
  • netbox_librenms_plugin/import_utils/device_operations.py
📚 Learning: 2026-02-15T15:39:20.744Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/testing.instructions.md:0-0
Timestamp: 2026-02-15T15:39:20.744Z
Learning: Applies to tests/test_import_utils.py : Cache key tests must patch `get_validated_device_cache_key` from `import_utils.py`; never hardcode key formats like `job_123_device_1`

Applied to files:

  • netbox_librenms_plugin/import_utils/bulk_import.py
📚 Learning: 2026-02-15T15:38:54.189Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-02-15T15:38:54.189Z
Learning: Applies to **/*.py : Matching must be exact-only for site, platform, device type, and role; do not add fuzzy matching; use the functions `find_matching_site`, `match_librenms_hardware_to_device_type`, and `find_matching_platform` from `utils.py`

Applied to files:

  • netbox_librenms_plugin/import_utils/device_operations.py
📚 Learning: 2026-02-15T15:38:54.189Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-02-15T15:38:54.189Z
Learning: Applies to **/*.py : Always call `LibreNMSAPI.get_librenms_id` to retrieve the device/VM LibreNMS mapping via the `librenms_id` custom field instead of touching the field directly

Applied to files:

  • netbox_librenms_plugin/import_utils/device_operations.py
  • netbox_librenms_plugin/views/imports/actions.py
📚 Learning: 2026-02-15T15:39:03.405Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/background-jobs.instructions.md:0-0
Timestamp: 2026-02-15T15:39:03.405Z
Learning: Applies to **/jobs.py : Background jobs must use NetBox's `JobRunner` base class (`netbox.jobs.JobRunner`) for long-running operations like device filtering with VC detection

Applied to files:

  • netbox_librenms_plugin/jobs.py
📚 Learning: 2026-02-15T15:39:03.405Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/background-jobs.instructions.md:0-0
Timestamp: 2026-02-15T15:39:03.405Z
Learning: Update modal messages based on RQ status values: 'Job queued...', 'Processing...', 'Job completed!' with explicit handling for all RQ status values to avoid infinite polling

Applied to files:

  • netbox_librenms_plugin/static/netbox_librenms_plugin/js/librenms_import.js
🧬 Code graph analysis (3)
netbox_librenms_plugin/tables/device_status.py (3)
netbox_librenms_plugin/views/imports/actions.py (2)
  • get (736-756)
  • get (762-790)
netbox_librenms_plugin/views/imports/list.py (1)
  • get (124-366)
netbox_librenms_plugin/views/base/librenms_sync_view.py (1)
  • get (25-46)
netbox_librenms_plugin/import_utils/virtual_chassis.py (4)
netbox_librenms_plugin/views/mixins.py (1)
  • librenms_api (217-231)
netbox_librenms_plugin/librenms_api.py (3)
  • LibreNMSAPI (15-1039)
  • get_device_info (314-337)
  • get_inventory_filtered (726-803)
netbox_librenms_plugin/views/imports/actions.py (2)
  • get (736-756)
  • get (762-790)
netbox_librenms_plugin/models.py (1)
  • LibreNMSSettings (7-48)
netbox_librenms_plugin/views/imports/actions.py (3)
netbox_librenms_plugin/utils.py (5)
  • get_user_pref (258-262)
  • save_user_pref (265-271)
  • find_matching_platform (390-420)
  • match_librenms_hardware_to_device_type (305-353)
  • migrate_legacy_librenms_id (92-122)
netbox_librenms_plugin/views/mixins.py (4)
  • LibreNMSAPIMixin (196-282)
  • NetBoxObjectPermissionMixin (85-193)
  • librenms_api (217-231)
  • require_object_permissions (123-147)
netbox_librenms_plugin/models.py (1)
  • LibreNMSSettings (7-48)
🔇 Additional comments (5)
netbox_librenms_plugin/tables/device_status.py (2)

447-503: Good conflict/details action-state handling and icon-only accessibility fallback.

The state prioritization is clear, and the aria-label fallback for icon-only button rendering is a solid improvement.


520-556: Nice propagation of naming-preference toggles into modal detail requests.

Including #use-sysname-toggle and #strip-domain-toggle in all details/review HTMX actions keeps validation details consistent with the active naming mode.

Based on learnings: REST endpoints for imports should keep server responses and HTMX targets in sync.

netbox_librenms_plugin/static/netbox_librenms_plugin/js/librenms_import.js (2)

313-328: Good fallback layering in hideModal.

Recovering an existing modal instance before manual fallback improves close reliability across environments with/without Bootstrap helpers.

Based on learnings: Modals use Tabler (Bootstrap-like) but without bootstrap.Modal helpers.


351-351: Nice consolidation of modal teardown paths.

Using one filterModal reference and hideModal(...) consistently removes duplicated modal-close logic and makes status-path behavior more predictable.

Also applies to: 416-418, 459-461, 477-479, 491-493, 507-509, 589-591, 600-602, 607-609, 618-620, 1018-1020

netbox_librenms_plugin/jobs.py (1)

49-50: Good propagation of naming prefs into background filtering.

This cleanly threads use_sysname/strip_domain into the shared filter pipeline, keeping async behavior aligned with synchronous flows.

Also applies to: 95-97

Comment thread netbox_librenms_plugin/import_utils/bulk_import.py
Comment on lines +482 to +487
cache_key = get_validated_device_cache_key(
server_key=api.server_key,
filters=filters,
device_id=device_id,
vc_enabled=vc_detection_enabled,
)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Scope validated-cache keys by naming preferences.

Validation output depends on use_sysname/strip_domain, but current cache keys don’t include those flags. Reusing cached entries across toggle changes can yield stale matching and wrong conflict actions.

🔧 Proposed fix
+        cache_scoped_filters = {
+            **filters,
+            "__use_sysname": bool(use_sysname),
+            "__strip_domain": bool(strip_domain),
+        }
+
         cache_key = get_validated_device_cache_key(
             server_key=api.server_key,
-            filters=filters,
+            filters=cache_scoped_filters,
             device_id=device_id,
             vc_enabled=vc_detection_enabled,
         )
@@
         cache_metadata_key = get_cache_metadata_key(
-            server_key=api.server_key, filters=filters, vc_enabled=vc_detection_enabled
+            server_key=api.server_key, filters=cache_scoped_filters, vc_enabled=vc_detection_enabled
         )

Also applies to: 557-559

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@netbox_librenms_plugin/import_utils/bulk_import.py` around lines 482 - 487,
The cache key created by get_validated_device_cache_key does not incorporate the
naming flags (use_sysname and strip_domain), causing cached validation results
to be reused when those preferences change; update the cache key construction to
include the relevant naming preference flags (use_sysname and strip_domain) so
that the key is unique per naming configuration, and apply the same change to
the other callsite(s) that invoke get_validated_device_cache_key (the calls
around the second occurrence noted) to avoid returning stale validation results.

"site": site,
"device_type": device_type,
"role": device_role,
"status": "active" if libre_device.get("status") == 1 else "offline",

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Normalize LibreNMS status before mapping to NetBox status.

Line 770 treats only integer 1 as up. If LibreNMS returns "1" (string), active devices are imported as offline.

🔧 Proposed fix
-            device_data = {
+            status_raw = libre_device.get("status")
+            is_up = str(status_raw).strip() == "1"
+
+            device_data = {
                 "name": device_name,
                 "site": site,
                 "device_type": device_type,
                 "role": device_role,
-                "status": "active" if libre_device.get("status") == 1 else "offline",
+                "status": "active" if is_up else "offline",
                 "comments": f"Imported from LibreNMS by netbox-librenms-plugin on {import_time}",
                 "custom_field_data": {"librenms_id": {api.server_key: int(device_id)}},
             }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@netbox_librenms_plugin/import_utils/device_operations.py` at line 770, The
mapping incorrectly treats only integer 1 as active because it compares
libre_device.get("status") == 1; normalize the LibreNMS status first (e.g.,
coerce to int safely or compare against both "1" and 1) before mapping to the
NetBox "status" field so string "1" is treated as active; update the expression
that builds the dict (the call using libre_device.get("status")) to perform the
normalization (handle None/invalid values with a safe default) and then set
"status": "active" if normalized_status == 1 else "offline".

Comment on lines +1098 to +1102
// Re-initialize tooltips for newly swapped modal content
if (typeof bootstrap !== 'undefined' && bootstrap.Tooltip) {
const tooltipEls = modalContent.querySelectorAll('[data-bs-toggle="tooltip"]');
[...tooltipEls].map(el => new bootstrap.Tooltip(el));
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Guard modalContent before tooltip query to prevent runtime errors.

At Line 1100, modalContent.querySelectorAll(...) can throw if modalContent is null in pages/flows where the container is not present at initialization time.

✅ Minimal fix
-            if (typeof bootstrap !== 'undefined' && bootstrap.Tooltip) {
+            if (modalContent && typeof bootstrap !== 'undefined' && bootstrap.Tooltip) {
                 const tooltipEls = modalContent.querySelectorAll('[data-bs-toggle="tooltip"]');
                 [...tooltipEls].map(el => new bootstrap.Tooltip(el));
             }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// Re-initialize tooltips for newly swapped modal content
if (typeof bootstrap !== 'undefined' && bootstrap.Tooltip) {
const tooltipEls = modalContent.querySelectorAll('[data-bs-toggle="tooltip"]');
[...tooltipEls].map(el => new bootstrap.Tooltip(el));
}
// Re-initialize tooltips for newly swapped modal content
if (modalContent && typeof bootstrap !== 'undefined' && bootstrap.Tooltip) {
const tooltipEls = modalContent.querySelectorAll('[data-bs-toggle="tooltip"]');
[...tooltipEls].map(el => new bootstrap.Tooltip(el));
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@netbox_librenms_plugin/static/netbox_librenms_plugin/js/librenms_import.js`
around lines 1098 - 1102, The current code calls
modalContent.querySelectorAll(...) without verifying modalContent is non-null,
which can throw; update the block that re-initializes tooltips (the
bootstrap.Tooltip section referencing modalContent and tooltipEls) to first
check that modalContent is truthy (e.g., if (!modalContent) return or skip)
before calling querySelectorAll, and only proceed to collect tooltipEls and
instantiate new bootstrap.Tooltip(el) when modalContent exists.

Comment thread netbox_librenms_plugin/views/imports/list.py
…inventory

- _refresh_existing_device: extend with re-lookup path when existing_device was
  None at cache time (imports since caching are now detected); new signature
  (validation, libre_device=None, server_key='default'); add find_by_librenms_id
  import; update call site to pass libre_device=device, server_key=api.server_key

- Validation modal: show server name(s) alongside librenms_id badge
  (DeviceValidationDetailsView._build_id_server_info); falls back to plain ID for
  legacy bare-int format

- Sync page: show 'LibreNMS Connections' table card with per-server entries;
  configured servers get display name + clickable ID link; orphaned/stale server
  entries show in red with a 'Remove' button (RemoveServerMappingView, POST
  devices/<pk>/remove-server-mapping/); legacy bare-int falls back to old card

- Tests: TestBuildAllServerMappings (5 cases) and TestBuildIdServerInfo (5 cases)
  added; all 135 affected tests pass

Ported from inventory branch commits b3fed51 and 2d26019.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 5

♻️ Duplicate comments (3)
netbox_librenms_plugin/import_utils/bulk_import.py (2)

489-492: ⚠️ Potential issue | 🟠 Major

Include errored in DB fallback termination checks.

Both DB fallback branches only stop on failed, so errored jobs can continue processing unexpectedly.

🔧 Suggested fix
-            if job.job.status == JobStatusChoices.STATUS_FAILED:
+            job_status = job.job.status
+            status_value = job_status.value if hasattr(job_status, "value") else job_status
+            if status_value in (
+                JobStatusChoices.STATUS_FAILED,
+                JobStatusChoices.STATUS_ERRORED,
+                "failed",
+                "errored",
+            ):
                 job.logger.warning("Job was stopped before validation started")
                 return _empty_return(return_cache_status)
@@
-                    if job.job.status == JobStatusChoices.STATUS_FAILED:
+                    job_status = job.job.status
+                    status_value = job_status.value if hasattr(job_status, "value") else job_status
+                    if status_value in (
+                        JobStatusChoices.STATUS_FAILED,
+                        JobStatusChoices.STATUS_ERRORED,
+                        "failed",
+                        "errored",
+                    ):
                         job.logger.info(f"Job stopped at device {idx}/{total}. Exiting gracefully.")
                         return _empty_return(return_cache_status)

Based on learnings: Database Job status values include errored as a terminal state and should be treated as stopped for this flow.

Also applies to: 518-521

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@netbox_librenms_plugin/import_utils/bulk_import.py` around lines 489 - 492,
The termination checks currently only treat JobStatusChoices.STATUS_FAILED as a
terminal state; update both places that call job.job.refresh_from_db() and check
job.job.status (the block using JobStatusChoices.STATUS_FAILED and returning
_empty_return(return_cache_status)) to also treat
JobStatusChoices.STATUS_ERRORED (or the enum member representing "errored") as a
terminal state so errored jobs are stopped the same as failed ones; apply the
same change to the other occurrence around the block referenced (the second
refresh/check at the later location).

528-533: ⚠️ Potential issue | 🟠 Major

Scope validation cache keys by naming preferences.

Validation output depends on use_sysname and strip_domain, but the cache key/metadata key currently ignore both flags. That can reuse stale validation across preference toggles.

🔧 Suggested fix
-        cache_key = get_validated_device_cache_key(
+        cache_scoped_filters = {
+            **filters,
+            "__use_sysname": bool(use_sysname),
+            "__strip_domain": bool(strip_domain),
+        }
+        cache_key = get_validated_device_cache_key(
             server_key=api.server_key,
-            filters=filters,
+            filters=cache_scoped_filters,
             device_id=device_id,
             vc_enabled=vc_detection_enabled,
         )
@@
         cache_metadata_key = get_cache_metadata_key(
-            server_key=api.server_key, filters=filters, vc_enabled=vc_detection_enabled
+            server_key=api.server_key,
+            filters=cache_scoped_filters,
+            vc_enabled=vc_detection_enabled,
         )
@@
-                "filters": filters,
+                "filters": cache_scoped_filters,

Also applies to: 603-605

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@netbox_librenms_plugin/import_utils/bulk_import.py` around lines 528 - 533,
The cache key generation for device validation (calls to
get_validated_device_cache_key) ignores naming preference flags use_sysname and
strip_domain, causing stale cache reuse; update the
get_validated_device_cache_key signature and its callers (the usages at the
shown call near get_validated_device_cache_key and the other call around lines
603-605) to include the use_sysname and strip_domain flags, and ensure the
function incorporates those two booleans into both the cache key and any
metadata key computation so validation is scoped by naming preferences.
netbox_librenms_plugin/views/imports/actions.py (1)

861-863: ⚠️ Potential issue | 🟠 Major

Avoid direct custom_field_data["librenms_id"] access in mapping paths.

Line 861 and Line 1156 bypass the mapping accessor contract by reading the custom field directly. Please route through LibreNMSAPI mapping accessors/helpers for both display and migration decisions.

As per coding guidelines: "Always call LibreNMSAPI.get_librenms_id to retrieve the device/VM LibreNMS mapping via the librenms_id custom field instead of touching the field directly".

Also applies to: 1153-1158

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@netbox_librenms_plugin/views/imports/actions.py` around lines 861 - 863,
Replace direct reads of existing_device.custom_field_data.get("librenms_id")
with the LibreNMSAPI mapping accessor: call LibreNMSAPI.get_librenms_id(...) to
retrieve the mapping object and base your dict/type checks and migration/display
decisions on its return value; update both the check around cf_value in
imports/actions.py (function handling existing_device) and the similar block at
lines ~1153-1158 to use LibreNMSAPI.get_librenms_id(existing_device) (or the VM
variant if applicable) instead of touching custom_field_data directly.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@netbox_librenms_plugin/import_utils/bulk_import.py`:
- Around line 363-367: The current logic only attempts the sys_name lookup when
hostname is truthy, which misses matches when hostname is empty; change the flow
in the block that sets new_device so that after trying hostname
(Model.objects.filter(name__iexact=hostname).first()) you independently attempt
sys_name when new_device is still falsy (if not new_device and sys_name:
new_device = Model.objects.filter(name__iexact=sys_name).first()), i.e., remove
the hostname truthiness gating around the sys_name lookup so sys_name is used as
a fallback regardless of hostname presence.

In
`@netbox_librenms_plugin/templates/netbox_librenms_plugin/htmx/device_validation_details.html`:
- Around line 50-55: The template's comparisons against libre_device.status only
match integers 1/0 and mark string values like "1"/"0" as Unknown; update the
conditional checks to normalize the value before comparing (e.g., coerce
libre_device.status to an integer using the template filter or convert to string
and check both forms) so that both numeric and string responses from the API are
handled; modify the three conditionals that reference libre_device.status (the
Up/Down/Unknown badge branches) to perform the normalized comparison (e.g., use
libero_device.status|int == 1 and == 0 or equivalent).

In `@netbox_librenms_plugin/views/imports/actions.py`:
- Around line 997-1004: Extract the repeated resolved_name fallback logic into a
single helper (e.g., _resolve_hostname_for_import(request, validation,
libre_device)) that returns the final hostname; implement it to check
validation.get("resolved_name") and return that if present, otherwise call
_resolve_naming_preferences(request) to get use_sysname and strip_domain and
then call _determine_device_name(libre_device, use_sysname=use_sysname,
strip_domain=strip_domain). Replace the duplicated blocks that reference
resolved_name and call _determine_device_name (including the occurrences around
the existing resolved_name usage and the locations noted) to call the new helper
instead so all branches use the same canonical resolution path.
- Around line 979-988: The uniqueness check using transaction.atomic() + the
conflict_exists read is still race-prone; instead serialize on (server_key,
librenms_id) by introducing and using a DB-enforced mapping or lock before
mutating Device: inside transaction.atomic() create or lock a dedicated mapping
row (e.g., LibrenmsIdMapping with unique_together=('server_key','librenms_id'))
or SELECT ... FOR UPDATE on that mapping, then use that locked mapping to
decide/update the Device (replace the current conflict_exists flow tied to
server_key and librenms_id); alternatively add a unique constraint on the new
mapping model and catch IntegrityError to handle concurrent assignments. Ensure
references to transaction.atomic, server_key, conflict_exists, and Device are
replaced by this serialized mapping/lock logic wherever the check occurs (also
update the other occurrences noted).

In `@netbox_librenms_plugin/views/sync/device_fields.py`:
- Around line 405-418: The removal currently deletes any server_key present in
device.custom_field_data["librenms_id"]; change the logic in the view around
cf_value / cf / server_key (and inside the transaction where device_locked is
used) to only allow deletion when the existing mapping for server_key is
explicitly stale/orphaned: fetch mapping =
device.custom_field_data.get("librenms_id", {}).get(server_key) and validate a
stale indicator before deleting (e.g., mapping.get("stale") is True or
mapping.get("status") == "orphan" or mapping lacks a live identifier like
"device_id"); if the mapping is not stale, add messages.warning(request, ...)
and redirect without modifying device_locked or saving. Ensure the check is
applied both before acquiring the lock and re-checked after select_for_update()
on device_locked to avoid races.

---

Duplicate comments:
In `@netbox_librenms_plugin/import_utils/bulk_import.py`:
- Around line 489-492: The termination checks currently only treat
JobStatusChoices.STATUS_FAILED as a terminal state; update both places that call
job.job.refresh_from_db() and check job.job.status (the block using
JobStatusChoices.STATUS_FAILED and returning _empty_return(return_cache_status))
to also treat JobStatusChoices.STATUS_ERRORED (or the enum member representing
"errored") as a terminal state so errored jobs are stopped the same as failed
ones; apply the same change to the other occurrence around the block referenced
(the second refresh/check at the later location).
- Around line 528-533: The cache key generation for device validation (calls to
get_validated_device_cache_key) ignores naming preference flags use_sysname and
strip_domain, causing stale cache reuse; update the
get_validated_device_cache_key signature and its callers (the usages at the
shown call near get_validated_device_cache_key and the other call around lines
603-605) to include the use_sysname and strip_domain flags, and ensure the
function incorporates those two booleans into both the cache key and any
metadata key computation so validation is scoped by naming preferences.

In `@netbox_librenms_plugin/views/imports/actions.py`:
- Around line 861-863: Replace direct reads of
existing_device.custom_field_data.get("librenms_id") with the LibreNMSAPI
mapping accessor: call LibreNMSAPI.get_librenms_id(...) to retrieve the mapping
object and base your dict/type checks and migration/display decisions on its
return value; update both the check around cf_value in imports/actions.py
(function handling existing_device) and the similar block at lines ~1153-1158 to
use LibreNMSAPI.get_librenms_id(existing_device) (or the VM variant if
applicable) instead of touching custom_field_data directly.

ℹ️ Review info

Configuration used: Repository UI (base), Organization UI (inherited)

Review profile: ASSERTIVE

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 0f72ceb and e96d2bb.

📒 Files selected for processing (10)
  • netbox_librenms_plugin/import_utils/bulk_import.py
  • netbox_librenms_plugin/templates/netbox_librenms_plugin/htmx/device_validation_details.html
  • netbox_librenms_plugin/templates/netbox_librenms_plugin/librenms_sync_base.html
  • netbox_librenms_plugin/tests/test_import_utils.py
  • netbox_librenms_plugin/tests/test_sync_view_mismatch.py
  • netbox_librenms_plugin/urls.py
  • netbox_librenms_plugin/views/__init__.py
  • netbox_librenms_plugin/views/base/librenms_sync_view.py
  • netbox_librenms_plugin/views/imports/actions.py
  • netbox_librenms_plugin/views/sync/device_fields.py
📜 Review details
🧰 Additional context used
📓 Path-based instructions (6)
netbox_librenms_plugin/templates/**/*.html

📄 CodeRabbit inference engine (.github/instructions/frontend.instructions.md)

netbox_librenms_plugin/templates/**/*.html: HTMX 2.x is the primary async layer. Table row updates should return <tr hx-swap-oob="true">.
Avoid outerHTML swaps in HTMX; use OOB or targeted innerHTML swaps to keep table layout intact.
Modals use Tabler (Bootstrap-like) but without bootstrap.Modal helpers.
Modal buttons should target the htmx-modal-content element. JavaScript in librenms_import.html toggles the wrapper; do not reintroduce data-bs-toggle or duplicate modal IDs.
Keep <select class="device-role-select"> markup stable to preserve JavaScript hook-up with TomSelect decorators.
Styling assumes Tabler defaults. Do not add table-responsive wrappers as they were deliberately removed to prevent dropdown clipping.

Files:

  • netbox_librenms_plugin/templates/netbox_librenms_plugin/librenms_sync_base.html
  • netbox_librenms_plugin/templates/netbox_librenms_plugin/htmx/device_validation_details.html
netbox_librenms_plugin/templates/netbox_librenms_plugin/**/*.html

📄 CodeRabbit inference engine (.github/instructions/frontend.instructions.md)

Templates should live in templates/netbox_librenms_plugin/. Reusable template includes should be placed under inc/ subdirectory.

Files:

  • netbox_librenms_plugin/templates/netbox_librenms_plugin/librenms_sync_base.html
  • netbox_librenms_plugin/templates/netbox_librenms_plugin/htmx/device_validation_details.html
netbox_librenms_plugin/templates/netbox_librenms_plugin/*sync*.html

📄 CodeRabbit inference engine (.github/instructions/frontend.instructions.md)

Sync pages should extend librenms_sync_base.html.

Files:

  • netbox_librenms_plugin/templates/netbox_librenms_plugin/librenms_sync_base.html
**/*.py

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

**/*.py: Reuse the librenms_api.py LibreNMS client instead of making new requests calls; it handles multi-server configs via LibreNMSSettings model and the servers plugin config, plus caching
Always call LibreNMSAPI.get_librenms_id to retrieve the device/VM LibreNMS mapping via the librenms_id custom field instead of touching the field directly
Matching must be exact-only for site, platform, device type, and role; do not add fuzzy matching; use the functions find_matching_site, match_librenms_hardware_to_device_type, and find_matching_platform from utils.py

Files:

  • netbox_librenms_plugin/views/sync/device_fields.py
  • netbox_librenms_plugin/tests/test_sync_view_mismatch.py
  • netbox_librenms_plugin/views/base/librenms_sync_view.py
  • netbox_librenms_plugin/views/__init__.py
  • netbox_librenms_plugin/import_utils/bulk_import.py
  • netbox_librenms_plugin/urls.py
  • netbox_librenms_plugin/views/imports/actions.py
netbox_librenms_plugin/templates/netbox_librenms_plugin/htmx/**/*.html

📄 CodeRabbit inference engine (.github/instructions/frontend.instructions.md)

HTMX fragments should live in templates/netbox_librenms_plugin/htmx/. Keep server responses and HTMX targets in sync.

Files:

  • netbox_librenms_plugin/templates/netbox_librenms_plugin/htmx/device_validation_details.html
**/views/imports/**

📄 CodeRabbit inference engine (.github/instructions/background-jobs.instructions.md)

**/views/imports/**: Use Job UUID (job.job_id) for RQ API endpoints: /api/core/background-tasks/{uuid}/
Use Job PK (job.pk) for database endpoints and result loading
Check rq_job.is_stopped or rq_job.is_failed flags in Redis for cancellation detection, not database status
Call /api/core/background-tasks/{uuid}/stop/ to stop RQ job in the job cancellation flow
Call plugin's sync endpoint /api/plugins/librenms_plugin/jobs/{pk}/sync-status/ to update database after stopping RQ job
Poll /api/core/background-tasks/{uuid}/ for real-time RQ status instead of polling the database
api/views.py::sync_job_status() must sync database Job status with RQ job status because NetBox worker doesn't always update DB when jobs stop before processing starts

Files:

  • netbox_librenms_plugin/views/imports/actions.py
🧠 Learnings (25)
📓 Common learnings
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-02-15T15:38:54.189Z
Learning: Applies to views/imports/**/*.py : REST endpoints for imports live in `views/imports/actions.py` with the list view in `views/imports/list.py`, surface via `urls.py`, and emit HTMX fragments in `templates/netbox_librenms_plugin/htmx/`; keep server responses and HTMX targets in sync
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/testing.instructions.md:0-0
Timestamp: 2026-02-15T15:39:20.744Z
Learning: Applies to tests/**/*.py : Test coverage mapping: `librenms_api.py` → `test_librenms_api.py`, `import_utils.py`/`import_validation_helpers.py`/`utils.py` → `test_import_utils.py`/`test_import_validation_helpers.py`/`test_utils.py`, `jobs.py`/`views/imports/list.py` → `test_background_jobs.py`
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/testing.instructions.md:0-0
Timestamp: 2026-02-15T15:39:20.744Z
Learning: Applies to tests/**/*.py : Patch deferred/inline imports at their source module (e.g., `netbox_librenms_plugin.import_utils.process_device_filters`), not the consuming module
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-02-15T15:38:54.189Z
Learning: Applies to **/*.py : Reuse the `librenms_api.py` LibreNMS client instead of making new `requests` calls; it handles multi-server configs via `LibreNMSSettings` model and the `servers` plugin config, plus caching
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/testing.instructions.md:0-0
Timestamp: 2026-02-15T15:39:20.744Z
Learning: Applies to tests/test_import_utils.py : Cache key tests must patch `get_validated_device_cache_key` from `import_utils.py`; never hardcode key formats like `job_123_device_1`
📚 Learning: 2026-02-15T15:39:11.578Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/frontend.instructions.md:0-0
Timestamp: 2026-02-15T15:39:11.578Z
Learning: Applies to netbox_librenms_plugin/templates/netbox_librenms_plugin/*sync*.html : Sync pages should extend `librenms_sync_base.html`.

Applied to files:

  • netbox_librenms_plugin/templates/netbox_librenms_plugin/librenms_sync_base.html
  • netbox_librenms_plugin/views/sync/device_fields.py
  • netbox_librenms_plugin/templates/netbox_librenms_plugin/htmx/device_validation_details.html
  • netbox_librenms_plugin/views/base/librenms_sync_view.py
  • netbox_librenms_plugin/views/__init__.py
  • netbox_librenms_plugin/views/imports/actions.py
📚 Learning: 2026-02-15T15:38:54.189Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-02-15T15:38:54.189Z
Learning: Applies to templates/**/*.html : Reuse template includes under `templates/netbox_librenms_plugin/inc/` and ensure sync pages extend `librenms_sync_base.html`

Applied to files:

  • netbox_librenms_plugin/templates/netbox_librenms_plugin/librenms_sync_base.html
  • netbox_librenms_plugin/templates/netbox_librenms_plugin/htmx/device_validation_details.html
📚 Learning: 2026-02-15T15:39:11.578Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/frontend.instructions.md:0-0
Timestamp: 2026-02-15T15:39:11.578Z
Learning: Applies to netbox_librenms_plugin/templates/netbox_librenms_plugin/htmx/**/*.html : HTMX fragments should live in `templates/netbox_librenms_plugin/htmx/`. Keep server responses and HTMX targets in sync.

Applied to files:

  • netbox_librenms_plugin/templates/netbox_librenms_plugin/librenms_sync_base.html
  • netbox_librenms_plugin/templates/netbox_librenms_plugin/htmx/device_validation_details.html
  • netbox_librenms_plugin/urls.py
📚 Learning: 2026-02-15T15:38:54.189Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-02-15T15:38:54.189Z
Learning: Follow sync pipeline flow: fetch LibreNMS data via `librenms_api.py`, cache it using `CacheMixin`, build comparison tables in `tables/`, and render HTMX fragments in `templates/netbox_librenms_plugin/htmx/`

Applied to files:

  • netbox_librenms_plugin/templates/netbox_librenms_plugin/librenms_sync_base.html
📚 Learning: 2026-02-15T15:39:11.578Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/frontend.instructions.md:0-0
Timestamp: 2026-02-15T15:39:11.578Z
Learning: Applies to netbox_librenms_plugin/templates/**/*.html : Avoid `outerHTML` swaps in HTMX; use OOB or targeted `innerHTML` swaps to keep table layout intact.

Applied to files:

  • netbox_librenms_plugin/templates/netbox_librenms_plugin/librenms_sync_base.html
  • netbox_librenms_plugin/templates/netbox_librenms_plugin/htmx/device_validation_details.html
📚 Learning: 2026-02-15T15:39:11.578Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/frontend.instructions.md:0-0
Timestamp: 2026-02-15T15:39:11.578Z
Learning: Applies to netbox_librenms_plugin/templates/**/*.html : HTMX 2.x is the primary async layer. Table row updates should return `<tr hx-swap-oob="true">`.

Applied to files:

  • netbox_librenms_plugin/templates/netbox_librenms_plugin/librenms_sync_base.html
  • netbox_librenms_plugin/templates/netbox_librenms_plugin/htmx/device_validation_details.html
📚 Learning: 2026-02-15T15:39:11.578Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/frontend.instructions.md:0-0
Timestamp: 2026-02-15T15:39:11.578Z
Learning: Applies to netbox_librenms_plugin/templates/**/*.html : Keep `<select class="device-role-select">` markup stable to preserve JavaScript hook-up with TomSelect decorators.

Applied to files:

  • netbox_librenms_plugin/templates/netbox_librenms_plugin/librenms_sync_base.html
  • netbox_librenms_plugin/templates/netbox_librenms_plugin/htmx/device_validation_details.html
  • netbox_librenms_plugin/views/imports/actions.py
📚 Learning: 2026-02-15T15:38:54.189Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-02-15T15:38:54.189Z
Learning: Applies to views/imports/**/*.py : REST endpoints for imports live in `views/imports/actions.py` with the list view in `views/imports/list.py`, surface via `urls.py`, and emit HTMX fragments in `templates/netbox_librenms_plugin/htmx/`; keep server responses and HTMX targets in sync

Applied to files:

  • netbox_librenms_plugin/views/sync/device_fields.py
  • netbox_librenms_plugin/templates/netbox_librenms_plugin/htmx/device_validation_details.html
  • netbox_librenms_plugin/views/base/librenms_sync_view.py
  • netbox_librenms_plugin/views/__init__.py
  • netbox_librenms_plugin/import_utils/bulk_import.py
  • netbox_librenms_plugin/urls.py
  • netbox_librenms_plugin/views/imports/actions.py
📚 Learning: 2026-02-15T15:39:20.744Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/testing.instructions.md:0-0
Timestamp: 2026-02-15T15:39:20.744Z
Learning: Applies to tests/**/*.py : Test coverage mapping: `librenms_api.py` → `test_librenms_api.py`, `import_utils.py`/`import_validation_helpers.py`/`utils.py` → `test_import_utils.py`/`test_import_validation_helpers.py`/`test_utils.py`, `jobs.py`/`views/imports/list.py` → `test_background_jobs.py`

Applied to files:

  • netbox_librenms_plugin/tests/test_sync_view_mismatch.py
📚 Learning: 2026-02-15T15:39:11.578Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/frontend.instructions.md:0-0
Timestamp: 2026-02-15T15:39:11.578Z
Learning: Applies to netbox_librenms_plugin/templates/**/*.html : Modal buttons should target the `htmx-modal-content` element. JavaScript in `librenms_import.html` toggles the wrapper; do not reintroduce `data-bs-toggle` or duplicate modal IDs.

Applied to files:

  • netbox_librenms_plugin/templates/netbox_librenms_plugin/htmx/device_validation_details.html
📚 Learning: 2026-02-15T15:39:11.578Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/frontend.instructions.md:0-0
Timestamp: 2026-02-15T15:39:11.578Z
Learning: Applies to netbox_librenms_plugin/templates/**/*.html : Styling assumes Tabler defaults. Do not add `table-responsive` wrappers as they were deliberately removed to prevent dropdown clipping.

Applied to files:

  • netbox_librenms_plugin/templates/netbox_librenms_plugin/htmx/device_validation_details.html
📚 Learning: 2026-02-15T15:38:54.189Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-02-15T15:38:54.189Z
Learning: Applies to {navigation.py,urls.py,api/**} : Respect NetBox plugin APIs in `navigation.py`, `urls.py`, and `api/` directories

Applied to files:

  • netbox_librenms_plugin/views/__init__.py
  • netbox_librenms_plugin/urls.py
  • netbox_librenms_plugin/views/imports/actions.py
📚 Learning: 2026-02-15T15:39:20.744Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/testing.instructions.md:0-0
Timestamp: 2026-02-15T15:39:20.744Z
Learning: Applies to tests/**/*.py : Patch deferred/inline imports at their source module (e.g., `netbox_librenms_plugin.import_utils.process_device_filters`), not the consuming module

Applied to files:

  • netbox_librenms_plugin/views/__init__.py
  • netbox_librenms_plugin/import_utils/bulk_import.py
📚 Learning: 2026-02-15T15:38:54.189Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-02-15T15:38:54.189Z
Learning: Applies to views/**/*.py : Views should follow layered structure: extend the closest base class from `views/base/` and compose mixins like `LibreNMSAPIMixin` and `CacheMixin`

Applied to files:

  • netbox_librenms_plugin/views/__init__.py
📚 Learning: 2026-02-15T15:39:03.405Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/background-jobs.instructions.md:0-0
Timestamp: 2026-02-15T15:39:03.405Z
Learning: Applies to **/views/imports/** : Check `rq_job.is_stopped` or `rq_job.is_failed` flags in Redis for cancellation detection, not database status

Applied to files:

  • netbox_librenms_plugin/import_utils/bulk_import.py
📚 Learning: 2026-02-15T15:39:03.405Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/background-jobs.instructions.md:0-0
Timestamp: 2026-02-15T15:39:03.405Z
Learning: Applies to **/views/imports/** : `api/views.py::sync_job_status()` must sync database Job status with RQ job status because NetBox worker doesn't always update DB when jobs stop before processing starts

Applied to files:

  • netbox_librenms_plugin/import_utils/bulk_import.py
📚 Learning: 2026-02-15T15:39:03.405Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/background-jobs.instructions.md:0-0
Timestamp: 2026-02-15T15:39:03.405Z
Learning: Applies to **/jobs.py : Jobs must run via Redis Queue (RQ) in Redis, separate from the database Job model, and real-time status must be checked via RQ, not the database

Applied to files:

  • netbox_librenms_plugin/import_utils/bulk_import.py
📚 Learning: 2026-02-15T15:39:03.405Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/background-jobs.instructions.md:0-0
Timestamp: 2026-02-15T15:39:03.405Z
Learning: Applies to **/jobs.py : Database Job status values must be: `pending`, `scheduled`, `running`, `completed`, `failed`, `errored` (NO `cancelled` status exists)

Applied to files:

  • netbox_librenms_plugin/import_utils/bulk_import.py
📚 Learning: 2026-02-15T15:39:03.405Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/background-jobs.instructions.md:0-0
Timestamp: 2026-02-15T15:39:03.405Z
Learning: Applies to **/jobs.py : RQ status values must be: `queued`, `started`, `finished`, `stopped`, `failed` (NOT `completed`)

Applied to files:

  • netbox_librenms_plugin/import_utils/bulk_import.py
📚 Learning: 2026-02-15T15:39:03.405Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/background-jobs.instructions.md:0-0
Timestamp: 2026-02-15T15:39:03.405Z
Learning: Applies to **/jobs.py : Background jobs must use NetBox's `JobRunner` base class (`netbox.jobs.JobRunner`) for long-running operations like device filtering with VC detection

Applied to files:

  • netbox_librenms_plugin/import_utils/bulk_import.py
📚 Learning: 2026-02-15T15:39:03.405Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/background-jobs.instructions.md:0-0
Timestamp: 2026-02-15T15:39:03.405Z
Learning: Applies to **/views/imports/** : Call plugin's sync endpoint `/api/plugins/librenms_plugin/jobs/{pk}/sync-status/` to update database after stopping RQ job

Applied to files:

  • netbox_librenms_plugin/import_utils/bulk_import.py
📚 Learning: 2026-02-15T15:39:20.744Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/testing.instructions.md:0-0
Timestamp: 2026-02-15T15:39:20.744Z
Learning: Applies to tests/test_import_utils.py : Cache key tests must patch `get_validated_device_cache_key` from `import_utils.py`; never hardcode key formats like `job_123_device_1`

Applied to files:

  • netbox_librenms_plugin/import_utils/bulk_import.py
📚 Learning: 2026-02-15T15:38:54.189Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-02-15T15:38:54.189Z
Learning: Applies to **/*.py : Always call `LibreNMSAPI.get_librenms_id` to retrieve the device/VM LibreNMS mapping via the `librenms_id` custom field instead of touching the field directly

Applied to files:

  • netbox_librenms_plugin/views/imports/actions.py
📚 Learning: 2026-02-15T15:39:20.744Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/testing.instructions.md:0-0
Timestamp: 2026-02-15T15:39:20.744Z
Learning: Applies to tests/test_background_jobs.py : Patch `cache` where imported: `netbox_librenms_plugin.views.imports.list.cache`, not `django.core.cache.cache`

Applied to files:

  • netbox_librenms_plugin/views/imports/actions.py
🧬 Code graph analysis (2)
netbox_librenms_plugin/views/sync/device_fields.py (4)
netbox_librenms_plugin/utils.py (1)
  • match_librenms_hardware_to_device_type (305-353)
netbox_librenms_plugin/views/mixins.py (3)
  • LibreNMSAPIMixin (196-282)
  • require_all_permissions (167-179)
  • librenms_api (217-231)
netbox_librenms_plugin/views/sync/cables.py (1)
  • post (126-146)
netbox_librenms_plugin/librenms_api.py (2)
  • get_librenms_id (173-231)
  • get_device_info (314-337)
netbox_librenms_plugin/urls.py (2)
netbox_librenms_plugin/views/imports/actions.py (1)
  • DeviceConflictActionView (914-1197)
netbox_librenms_plugin/views/sync/device_fields.py (2)
  • RemoveServerMappingView (387-422)
  • UpdateDeviceNameView (12-56)
🔇 Additional comments (1)
netbox_librenms_plugin/views/imports/actions.py (1)

949-973: Good hardening on conflict safety paths.

Requiring a validated conflict target, validating device_id before conversion, and escaping serial-conflict response values materially improves safety in this endpoint.

Also applies to: 1029-1033, 1054-1058, 1105-1108

Comment thread netbox_librenms_plugin/import_utils/bulk_import.py
Comment thread netbox_librenms_plugin/views/imports/actions.py
Comment thread netbox_librenms_plugin/views/imports/actions.py Outdated
Comment on lines +405 to +418
cf_value = device.custom_field_data.get("librenms_id")
if not isinstance(cf_value, dict) or server_key not in cf_value:
messages.warning(request, f"No mapping found for server '{server_key}'.")
return redirect("plugins:netbox_librenms_plugin:device_librenms_sync", pk=pk)

with transaction.atomic():
device_locked = Device.objects.select_for_update().get(pk=pk)
cf = device_locked.custom_field_data.get("librenms_id", {})
if isinstance(cf, dict) and server_key in cf:
del cf[server_key]
device_locked.custom_field_data["librenms_id"] = cf if cf else None
device_locked.full_clean()
device_locked.save()
messages.success(request, f"Removed LibreNMS mapping for server '{server_key}'.")

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Enforce stale-only mapping removal in the backend.

This endpoint currently removes any server_key present in librenms_id, including configured/active mappings. The UI only offers removal for orphaned mappings, so the server should enforce the same constraint.

🔧 Suggested guard
 def post(self, request, pk):
@@
-        with transaction.atomic():
+        from netbox.plugins import get_plugin_config
+        servers_cfg = get_plugin_config("netbox_librenms_plugin", "servers") or {}
+        if server_key in servers_cfg:
+            messages.error(request, f"Server '{server_key}' is configured and cannot be removed here.")
+            return redirect("plugins:netbox_librenms_plugin:device_librenms_sync", pk=pk)
+
+        with transaction.atomic():
             device_locked = Device.objects.select_for_update().get(pk=pk)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@netbox_librenms_plugin/views/sync/device_fields.py` around lines 405 - 418,
The removal currently deletes any server_key present in
device.custom_field_data["librenms_id"]; change the logic in the view around
cf_value / cf / server_key (and inside the transaction where device_locked is
used) to only allow deletion when the existing mapping for server_key is
explicitly stale/orphaned: fetch mapping =
device.custom_field_data.get("librenms_id", {}).get(server_key) and validate a
stale indicator before deleting (e.g., mapping.get("stale") is True or
mapping.get("status") == "orphan" or mapping lacks a live identifier like
"device_id"); if the mapping is not stale, add messages.warning(request, ...)
and redirect without modifying device_locked or saving. Ensure the check is
applied both before acquiring the lock and re-checked after select_for_update()
on device_locked to avoid races.

…nation, sysname fallback, hostname helper, configured-server guard

- cache.py: add use_sysname/strip_domain params to get_cache_metadata_key and
  get_validated_device_cache_key so validation is scoped by naming preferences
- bulk_import.py: treat STATUS_ERRORED same as STATUS_FAILED in DB fallback
  termination checks; fix sys_name lookup to run independently of hostname
  (was gated inside 'if hostname:' block, missing matches when hostname empty)
- jobs.py: store use_sysname/strip_domain in FilterDevicesJob.data so
  _load_job_results can reconstruct correct cache keys for background jobs
- list.py: read use_sysname/strip_domain from job data; pass to cache key calls
- actions.py: extract _get_hostname_for_action helper to remove 3x duplication
  of resolved_name fallback logic across link/update/sync_name action handlers
- device_fields.py: RemoveServerMappingView now refuses to remove mappings for
  servers still present in PLUGINS_CONFIG; guard applied before lock and inside
  transaction for race-free enforcement
- device_validation_details.html: normalise libre_device.status comparisons to
  handle both int and string values from LibreNMS API ('1'/'0' vs 1/0)
- tests: add TestCacheKeyGeneration naming-mode isolation tests,
  TestBulkImportCancellation STATUS_ERRORED test,
  TestRefreshExistingDeviceSysNameFallback, TestGetHostnameForAction;
  update TestLoadJobResults assertions to include new cache key params

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1

♻️ Duplicate comments (3)
netbox_librenms_plugin/import_utils/cache.py (1)

72-73: ⚠️ Potential issue | 🟠 Major

Scope location-choice cache entries by server_key.

At Line 72, using a global "librenms_locations_choices" key can cross-contaminate location labels between servers.

Proposed fix
-    location_cache_key = "librenms_locations_choices"
+    location_cache_key = f"librenms_locations_choices_{server_key}"

Also update the corresponding cache writer(s) to use the same server-scoped key.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@netbox_librenms_plugin/import_utils/cache.py` around lines 72 - 73, The cache
key "librenms_locations_choices" is global and can mix location labels across
servers; change the getter to scope by server_key (e.g. compute
location_cache_key = f"librenms_locations_choices:{server_key}" before calling
cache.get in the code that references location_cache_key and cached_locations)
and update any cache writer(s) that set this same key to use the identical
server-scoped key format so reads and writes consistently use
"librenms_locations_choices:{server_key}" (ensure you locate usages around the
location_cache_key variable, cached_locations, and any cache.set/write calls).
netbox_librenms_plugin/views/imports/list.py (1)

266-277: ⚠️ Potential issue | 🟠 Major

Honor submitted naming toggles when user prefs are unset.

At Line 268 and Line 446, toggle values from the current form request are skipped whenever *_pref is None, so users without saved prefs get defaults instead of their submitted toggles.

Proposed fix
-            _use_sysname = (
-                _use_sysname_pref
-                if _use_sysname_pref is not None
-                else (getattr(settings, "use_sysname_default", True) if settings else True)
-            )
-            _strip_domain = (
-                _strip_domain_pref
-                if _strip_domain_pref is not None
-                else (getattr(settings, "strip_domain_default", False) if settings else False)
-            )
+            form_use_sysname = self._filter_form_data.get("use_sysname_toggle")
+            form_strip_domain = self._filter_form_data.get("strip_domain_toggle")
+            _use_sysname = (
+                form_use_sysname
+                if form_use_sysname is not None
+                else (
+                    _use_sysname_pref
+                    if _use_sysname_pref is not None
+                    else (getattr(settings, "use_sysname_default", True) if settings else True)
+                )
+            )
+            _strip_domain = (
+                form_strip_domain
+                if form_strip_domain is not None
+                else (
+                    _strip_domain_pref
+                    if _strip_domain_pref is not None
+                    else (getattr(settings, "strip_domain_default", False) if settings else False)
+                )
+            )

Apply the same precedence pattern in _get_import_queryset() for use_sysname/strip_domain.

Also applies to: 446-455

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@netbox_librenms_plugin/views/imports/list.py` around lines 266 - 277, The
current logic uses _use_sysname_pref/_strip_domain_pref and then falls back to
settings defaults when prefs are None, which ignores the submitted form toggles;
update the precedence so that when get_user_pref(...) returns None you first
check the current request/form values (e.g., request.POST.get("use_sysname") /
request.POST.get("strip_domain") or the view's submitted parameters) and parse
them into booleans before falling back to getattr(settings, "..._default", ...);
apply the same change inside _get_import_queryset() so the use_sysname and
strip_domain variables honor submitted toggles when user prefs are unset
(reference symbols: _use_sysname_pref, _strip_domain_pref, get_user_pref,
_get_import_queryset, use_sysname, strip_domain).
netbox_librenms_plugin/views/imports/actions.py (1)

993-1002: ⚠️ Potential issue | 🟠 Major

librenms_id uniqueness enforcement is still race-prone.

At Line 995, the conflict check is a read-before-write pattern without a DB-enforced unique key (or equivalent lock) for (server_key, librenms_id). Concurrent requests can both pass and assign duplicates.

Also applies to: 1009-1064

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@netbox_librenms_plugin/views/imports/actions.py` around lines 993 - 1002, The
read-before-write check using Device.objects.filter(...
custom_field_data__librenms_id ...) inside the transaction (where server_key =
self.librenms_api.server_key and librenms_id is being assigned) is race-prone;
replace it with a DB-enforced uniqueness strategy and handle IntegrityError: add
a dedicated DB-backed uniqueness (e.g., a new model like DeviceLibrenmsID with
fields server_key and librenms_id and a unique_together constraint) or create a
DB unique index that represents the (server_key, librenms_id) pair, then change
the create/update flow in the code paths around Device, transaction.atomic, and
any update logic in the block (including the code referenced at 1009-1064) to
perform the insert/update and catch django.db.IntegrityError to detect
duplicates instead of relying on the filter-exclude existence check;
alternatively, if adding a schema change is not possible immediately, acquire a
row-level lock by selecting the Device table row(s) with select_for_update()
keyed by server_key/librenms_id before writing to ensure only one transaction
proceeds.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@netbox_librenms_plugin/views/sync/device_fields.py`:
- Around line 424-433: Wrap the device_locked.full_clean() and
device_locked.save() calls in a try/except that catches
django.core.exceptions.ValidationError and a broad Exception, call
transaction.set_rollback(True) in the except block, add a messages.error(...)
containing the validation messages or exception string, and perform the same
redirect/return flow used on success instead of allowing the exception to
bubble; locate the block around device_locked.full_clean(),
device_locked.save(), device_locked.custom_field_data and messages.success to
implement this change.

---

Duplicate comments:
In `@netbox_librenms_plugin/import_utils/cache.py`:
- Around line 72-73: The cache key "librenms_locations_choices" is global and
can mix location labels across servers; change the getter to scope by server_key
(e.g. compute location_cache_key = f"librenms_locations_choices:{server_key}"
before calling cache.get in the code that references location_cache_key and
cached_locations) and update any cache writer(s) that set this same key to use
the identical server-scoped key format so reads and writes consistently use
"librenms_locations_choices:{server_key}" (ensure you locate usages around the
location_cache_key variable, cached_locations, and any cache.set/write calls).

In `@netbox_librenms_plugin/views/imports/actions.py`:
- Around line 993-1002: The read-before-write check using
Device.objects.filter(... custom_field_data__librenms_id ...) inside the
transaction (where server_key = self.librenms_api.server_key and librenms_id is
being assigned) is race-prone; replace it with a DB-enforced uniqueness strategy
and handle IntegrityError: add a dedicated DB-backed uniqueness (e.g., a new
model like DeviceLibrenmsID with fields server_key and librenms_id and a
unique_together constraint) or create a DB unique index that represents the
(server_key, librenms_id) pair, then change the create/update flow in the code
paths around Device, transaction.atomic, and any update logic in the block
(including the code referenced at 1009-1064) to perform the insert/update and
catch django.db.IntegrityError to detect duplicates instead of relying on the
filter-exclude existence check; alternatively, if adding a schema change is not
possible immediately, acquire a row-level lock by selecting the Device table
row(s) with select_for_update() keyed by server_key/librenms_id before writing
to ensure only one transaction proceeds.

In `@netbox_librenms_plugin/views/imports/list.py`:
- Around line 266-277: The current logic uses
_use_sysname_pref/_strip_domain_pref and then falls back to settings defaults
when prefs are None, which ignores the submitted form toggles; update the
precedence so that when get_user_pref(...) returns None you first check the
current request/form values (e.g., request.POST.get("use_sysname") /
request.POST.get("strip_domain") or the view's submitted parameters) and parse
them into booleans before falling back to getattr(settings, "..._default", ...);
apply the same change inside _get_import_queryset() so the use_sysname and
strip_domain variables honor submitted toggles when user prefs are unset
(reference symbols: _use_sysname_pref, _strip_domain_pref, get_user_pref,
_get_import_queryset, use_sysname, strip_domain).

ℹ️ Review info

Configuration used: Repository UI (base), Organization UI (inherited)

Review profile: ASSERTIVE

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between e96d2bb and 3b5d71d.

📒 Files selected for processing (9)
  • netbox_librenms_plugin/import_utils/bulk_import.py
  • netbox_librenms_plugin/import_utils/cache.py
  • netbox_librenms_plugin/jobs.py
  • netbox_librenms_plugin/templates/netbox_librenms_plugin/htmx/device_validation_details.html
  • netbox_librenms_plugin/tests/test_background_jobs.py
  • netbox_librenms_plugin/tests/test_import_utils.py
  • netbox_librenms_plugin/views/imports/actions.py
  • netbox_librenms_plugin/views/imports/list.py
  • netbox_librenms_plugin/views/sync/device_fields.py
📜 Review details
🧰 Additional context used
📓 Path-based instructions (6)
netbox_librenms_plugin/templates/**/*.html

📄 CodeRabbit inference engine (.github/instructions/frontend.instructions.md)

netbox_librenms_plugin/templates/**/*.html: HTMX 2.x is the primary async layer. Table row updates should return <tr hx-swap-oob="true">.
Avoid outerHTML swaps in HTMX; use OOB or targeted innerHTML swaps to keep table layout intact.
Modals use Tabler (Bootstrap-like) but without bootstrap.Modal helpers.
Modal buttons should target the htmx-modal-content element. JavaScript in librenms_import.html toggles the wrapper; do not reintroduce data-bs-toggle or duplicate modal IDs.
Keep <select class="device-role-select"> markup stable to preserve JavaScript hook-up with TomSelect decorators.
Styling assumes Tabler defaults. Do not add table-responsive wrappers as they were deliberately removed to prevent dropdown clipping.

Files:

  • netbox_librenms_plugin/templates/netbox_librenms_plugin/htmx/device_validation_details.html
netbox_librenms_plugin/templates/netbox_librenms_plugin/htmx/**/*.html

📄 CodeRabbit inference engine (.github/instructions/frontend.instructions.md)

HTMX fragments should live in templates/netbox_librenms_plugin/htmx/. Keep server responses and HTMX targets in sync.

Files:

  • netbox_librenms_plugin/templates/netbox_librenms_plugin/htmx/device_validation_details.html
netbox_librenms_plugin/templates/netbox_librenms_plugin/**/*.html

📄 CodeRabbit inference engine (.github/instructions/frontend.instructions.md)

Templates should live in templates/netbox_librenms_plugin/. Reusable template includes should be placed under inc/ subdirectory.

Files:

  • netbox_librenms_plugin/templates/netbox_librenms_plugin/htmx/device_validation_details.html
**/*.py

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

**/*.py: Reuse the librenms_api.py LibreNMS client instead of making new requests calls; it handles multi-server configs via LibreNMSSettings model and the servers plugin config, plus caching
Always call LibreNMSAPI.get_librenms_id to retrieve the device/VM LibreNMS mapping via the librenms_id custom field instead of touching the field directly
Matching must be exact-only for site, platform, device type, and role; do not add fuzzy matching; use the functions find_matching_site, match_librenms_hardware_to_device_type, and find_matching_platform from utils.py

Files:

  • netbox_librenms_plugin/jobs.py
  • netbox_librenms_plugin/views/sync/device_fields.py
  • netbox_librenms_plugin/tests/test_background_jobs.py
  • netbox_librenms_plugin/import_utils/bulk_import.py
  • netbox_librenms_plugin/views/imports/actions.py
  • netbox_librenms_plugin/import_utils/cache.py
  • netbox_librenms_plugin/views/imports/list.py
**/jobs.py

📄 CodeRabbit inference engine (.github/instructions/background-jobs.instructions.md)

**/jobs.py: Background jobs must use NetBox's JobRunner base class (netbox.jobs.JobRunner) for long-running operations like device filtering with VC detection
Jobs must run via Redis Queue (RQ) in Redis, separate from the database Job model, and real-time status must be checked via RQ, not the database
RQ status values must be: queued, started, finished, stopped, failed (NOT completed)
Database Job status values must be: pending, scheduled, running, completed, failed, errored (NO cancelled status exists)

Files:

  • netbox_librenms_plugin/jobs.py
**/views/imports/**

📄 CodeRabbit inference engine (.github/instructions/background-jobs.instructions.md)

**/views/imports/**: Use Job UUID (job.job_id) for RQ API endpoints: /api/core/background-tasks/{uuid}/
Use Job PK (job.pk) for database endpoints and result loading
Check rq_job.is_stopped or rq_job.is_failed flags in Redis for cancellation detection, not database status
Call /api/core/background-tasks/{uuid}/stop/ to stop RQ job in the job cancellation flow
Call plugin's sync endpoint /api/plugins/librenms_plugin/jobs/{pk}/sync-status/ to update database after stopping RQ job
Poll /api/core/background-tasks/{uuid}/ for real-time RQ status instead of polling the database
api/views.py::sync_job_status() must sync database Job status with RQ job status because NetBox worker doesn't always update DB when jobs stop before processing starts

Files:

  • netbox_librenms_plugin/views/imports/actions.py
  • netbox_librenms_plugin/views/imports/list.py
🧠 Learnings (25)
📓 Common learnings
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-02-15T15:38:54.189Z
Learning: Applies to views/imports/**/*.py : REST endpoints for imports live in `views/imports/actions.py` with the list view in `views/imports/list.py`, surface via `urls.py`, and emit HTMX fragments in `templates/netbox_librenms_plugin/htmx/`; keep server responses and HTMX targets in sync
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-02-15T15:38:54.189Z
Learning: Applies to **/*.py : Reuse the `librenms_api.py` LibreNMS client instead of making new `requests` calls; it handles multi-server configs via `LibreNMSSettings` model and the `servers` plugin config, plus caching
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/testing.instructions.md:0-0
Timestamp: 2026-02-15T15:39:20.744Z
Learning: Applies to tests/**/*.py : Patch deferred/inline imports at their source module (e.g., `netbox_librenms_plugin.import_utils.process_device_filters`), not the consuming module
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/testing.instructions.md:0-0
Timestamp: 2026-02-15T15:39:20.744Z
Learning: Applies to tests/**/*.py : Test coverage mapping: `librenms_api.py` → `test_librenms_api.py`, `import_utils.py`/`import_validation_helpers.py`/`utils.py` → `test_import_utils.py`/`test_import_validation_helpers.py`/`test_utils.py`, `jobs.py`/`views/imports/list.py` → `test_background_jobs.py`
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-02-15T15:38:54.189Z
Learning: Applies to **/*.py : Always call `LibreNMSAPI.get_librenms_id` to retrieve the device/VM LibreNMS mapping via the `librenms_id` custom field instead of touching the field directly
📚 Learning: 2026-02-15T15:39:11.578Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/frontend.instructions.md:0-0
Timestamp: 2026-02-15T15:39:11.578Z
Learning: Applies to netbox_librenms_plugin/templates/**/*.html : Avoid `outerHTML` swaps in HTMX; use OOB or targeted `innerHTML` swaps to keep table layout intact.

Applied to files:

  • netbox_librenms_plugin/templates/netbox_librenms_plugin/htmx/device_validation_details.html
📚 Learning: 2026-02-15T15:39:11.578Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/frontend.instructions.md:0-0
Timestamp: 2026-02-15T15:39:11.578Z
Learning: Applies to netbox_librenms_plugin/templates/netbox_librenms_plugin/htmx/**/*.html : HTMX fragments should live in `templates/netbox_librenms_plugin/htmx/`. Keep server responses and HTMX targets in sync.

Applied to files:

  • netbox_librenms_plugin/templates/netbox_librenms_plugin/htmx/device_validation_details.html
📚 Learning: 2026-02-15T15:39:11.578Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/frontend.instructions.md:0-0
Timestamp: 2026-02-15T15:39:11.578Z
Learning: Applies to netbox_librenms_plugin/templates/**/*.html : HTMX 2.x is the primary async layer. Table row updates should return `<tr hx-swap-oob="true">`.

Applied to files:

  • netbox_librenms_plugin/templates/netbox_librenms_plugin/htmx/device_validation_details.html
📚 Learning: 2026-02-15T15:39:11.578Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/frontend.instructions.md:0-0
Timestamp: 2026-02-15T15:39:11.578Z
Learning: Applies to netbox_librenms_plugin/templates/**/*.html : Modal buttons should target the `htmx-modal-content` element. JavaScript in `librenms_import.html` toggles the wrapper; do not reintroduce `data-bs-toggle` or duplicate modal IDs.

Applied to files:

  • netbox_librenms_plugin/templates/netbox_librenms_plugin/htmx/device_validation_details.html
📚 Learning: 2026-02-15T15:39:11.578Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/frontend.instructions.md:0-0
Timestamp: 2026-02-15T15:39:11.578Z
Learning: Applies to netbox_librenms_plugin/templates/**/*.html : Keep `<select class="device-role-select">` markup stable to preserve JavaScript hook-up with TomSelect decorators.

Applied to files:

  • netbox_librenms_plugin/templates/netbox_librenms_plugin/htmx/device_validation_details.html
  • netbox_librenms_plugin/views/imports/actions.py
📚 Learning: 2026-02-15T15:39:11.578Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/frontend.instructions.md:0-0
Timestamp: 2026-02-15T15:39:11.578Z
Learning: Applies to netbox_librenms_plugin/templates/netbox_librenms_plugin/*sync*.html : Sync pages should extend `librenms_sync_base.html`.

Applied to files:

  • netbox_librenms_plugin/templates/netbox_librenms_plugin/htmx/device_validation_details.html
  • netbox_librenms_plugin/views/sync/device_fields.py
  • netbox_librenms_plugin/views/imports/actions.py
📚 Learning: 2026-02-15T15:38:54.189Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-02-15T15:38:54.189Z
Learning: Applies to templates/**/*.html : Reuse template includes under `templates/netbox_librenms_plugin/inc/` and ensure sync pages extend `librenms_sync_base.html`

Applied to files:

  • netbox_librenms_plugin/templates/netbox_librenms_plugin/htmx/device_validation_details.html
📚 Learning: 2026-02-15T15:38:54.189Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-02-15T15:38:54.189Z
Learning: Applies to views/imports/**/*.py : REST endpoints for imports live in `views/imports/actions.py` with the list view in `views/imports/list.py`, surface via `urls.py`, and emit HTMX fragments in `templates/netbox_librenms_plugin/htmx/`; keep server responses and HTMX targets in sync

Applied to files:

  • netbox_librenms_plugin/templates/netbox_librenms_plugin/htmx/device_validation_details.html
  • netbox_librenms_plugin/views/sync/device_fields.py
  • netbox_librenms_plugin/import_utils/bulk_import.py
  • netbox_librenms_plugin/views/imports/actions.py
  • netbox_librenms_plugin/import_utils/cache.py
  • netbox_librenms_plugin/views/imports/list.py
📚 Learning: 2026-02-15T15:39:11.578Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/frontend.instructions.md:0-0
Timestamp: 2026-02-15T15:39:11.578Z
Learning: Applies to netbox_librenms_plugin/templates/**/*.html : Styling assumes Tabler defaults. Do not add `table-responsive` wrappers as they were deliberately removed to prevent dropdown clipping.

Applied to files:

  • netbox_librenms_plugin/templates/netbox_librenms_plugin/htmx/device_validation_details.html
📚 Learning: 2026-02-15T15:39:03.405Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/background-jobs.instructions.md:0-0
Timestamp: 2026-02-15T15:39:03.405Z
Learning: Applies to **/jobs.py : Background jobs must use NetBox's `JobRunner` base class (`netbox.jobs.JobRunner`) for long-running operations like device filtering with VC detection

Applied to files:

  • netbox_librenms_plugin/jobs.py
  • netbox_librenms_plugin/tests/test_background_jobs.py
  • netbox_librenms_plugin/import_utils/bulk_import.py
📚 Learning: 2026-02-15T15:38:54.189Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-02-15T15:38:54.189Z
Learning: Applies to {navigation.py,urls.py,api/**} : Respect NetBox plugin APIs in `navigation.py`, `urls.py`, and `api/` directories

Applied to files:

  • netbox_librenms_plugin/views/sync/device_fields.py
  • netbox_librenms_plugin/views/imports/actions.py
📚 Learning: 2026-02-15T15:38:54.189Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-02-15T15:38:54.189Z
Learning: Applies to **/*.py : Always call `LibreNMSAPI.get_librenms_id` to retrieve the device/VM LibreNMS mapping via the `librenms_id` custom field instead of touching the field directly

Applied to files:

  • netbox_librenms_plugin/views/sync/device_fields.py
  • netbox_librenms_plugin/views/imports/actions.py
📚 Learning: 2026-02-15T15:38:54.189Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-02-15T15:38:54.189Z
Learning: Follow sync pipeline flow: fetch LibreNMS data via `librenms_api.py`, cache it using `CacheMixin`, build comparison tables in `tables/`, and render HTMX fragments in `templates/netbox_librenms_plugin/htmx/`

Applied to files:

  • netbox_librenms_plugin/views/sync/device_fields.py
  • netbox_librenms_plugin/import_utils/bulk_import.py
📚 Learning: 2026-02-15T15:39:20.744Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/testing.instructions.md:0-0
Timestamp: 2026-02-15T15:39:20.744Z
Learning: Applies to tests/test_background_jobs.py : Patch `cache` where imported: `netbox_librenms_plugin.views.imports.list.cache`, not `django.core.cache.cache`

Applied to files:

  • netbox_librenms_plugin/tests/test_background_jobs.py
  • netbox_librenms_plugin/views/imports/actions.py
  • netbox_librenms_plugin/import_utils/cache.py
  • netbox_librenms_plugin/views/imports/list.py
📚 Learning: 2026-02-15T15:39:20.744Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/testing.instructions.md:0-0
Timestamp: 2026-02-15T15:39:20.744Z
Learning: Applies to tests/test_import_utils.py : Cache key tests must patch `get_validated_device_cache_key` from `import_utils.py`; never hardcode key formats like `job_123_device_1`

Applied to files:

  • netbox_librenms_plugin/tests/test_background_jobs.py
  • netbox_librenms_plugin/import_utils/bulk_import.py
  • netbox_librenms_plugin/import_utils/cache.py
📚 Learning: 2026-02-15T15:39:20.744Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/testing.instructions.md:0-0
Timestamp: 2026-02-15T15:39:20.744Z
Learning: Applies to tests/**/*.py : Test coverage mapping: `librenms_api.py` → `test_librenms_api.py`, `import_utils.py`/`import_validation_helpers.py`/`utils.py` → `test_import_utils.py`/`test_import_validation_helpers.py`/`test_utils.py`, `jobs.py`/`views/imports/list.py` → `test_background_jobs.py`

Applied to files:

  • netbox_librenms_plugin/tests/test_background_jobs.py
📚 Learning: 2026-02-15T15:39:20.744Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/testing.instructions.md:0-0
Timestamp: 2026-02-15T15:39:20.744Z
Learning: Applies to tests/test_background_jobs.py : Instantiate `JobRunner` subclasses using `object.__new__(JobClass)` to bypass `__init__`, then set `job.job = MagicMock()` and `job.logger = MagicMock()`

Applied to files:

  • netbox_librenms_plugin/tests/test_background_jobs.py
📚 Learning: 2026-02-15T15:39:20.744Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/testing.instructions.md:0-0
Timestamp: 2026-02-15T15:39:20.744Z
Learning: Applies to tests/**/*.py : Patch deferred/inline imports at their source module (e.g., `netbox_librenms_plugin.import_utils.process_device_filters`), not the consuming module

Applied to files:

  • netbox_librenms_plugin/tests/test_background_jobs.py
  • netbox_librenms_plugin/import_utils/bulk_import.py
  • netbox_librenms_plugin/import_utils/cache.py
📚 Learning: 2026-02-15T15:39:03.405Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/background-jobs.instructions.md:0-0
Timestamp: 2026-02-15T15:39:03.405Z
Learning: Applies to **/views/imports/** : Check `rq_job.is_stopped` or `rq_job.is_failed` flags in Redis for cancellation detection, not database status

Applied to files:

  • netbox_librenms_plugin/import_utils/bulk_import.py
📚 Learning: 2026-02-15T15:39:03.405Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/background-jobs.instructions.md:0-0
Timestamp: 2026-02-15T15:39:03.405Z
Learning: Applies to **/views/imports/** : `api/views.py::sync_job_status()` must sync database Job status with RQ job status because NetBox worker doesn't always update DB when jobs stop before processing starts

Applied to files:

  • netbox_librenms_plugin/import_utils/bulk_import.py
📚 Learning: 2026-02-15T15:39:03.405Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/background-jobs.instructions.md:0-0
Timestamp: 2026-02-15T15:39:03.405Z
Learning: Applies to **/jobs.py : Jobs must run via Redis Queue (RQ) in Redis, separate from the database Job model, and real-time status must be checked via RQ, not the database

Applied to files:

  • netbox_librenms_plugin/import_utils/bulk_import.py
📚 Learning: 2026-02-15T15:39:03.405Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/background-jobs.instructions.md:0-0
Timestamp: 2026-02-15T15:39:03.405Z
Learning: Applies to **/jobs.py : Database Job status values must be: `pending`, `scheduled`, `running`, `completed`, `failed`, `errored` (NO `cancelled` status exists)

Applied to files:

  • netbox_librenms_plugin/import_utils/bulk_import.py
📚 Learning: 2026-02-15T15:39:03.405Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/background-jobs.instructions.md:0-0
Timestamp: 2026-02-15T15:39:03.405Z
Learning: Applies to **/jobs.py : RQ status values must be: `queued`, `started`, `finished`, `stopped`, `failed` (NOT `completed`)

Applied to files:

  • netbox_librenms_plugin/import_utils/bulk_import.py
📚 Learning: 2026-02-15T15:39:03.405Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/background-jobs.instructions.md:0-0
Timestamp: 2026-02-15T15:39:03.405Z
Learning: Applies to **/views/imports/** : Call plugin's sync endpoint `/api/plugins/librenms_plugin/jobs/{pk}/sync-status/` to update database after stopping RQ job

Applied to files:

  • netbox_librenms_plugin/import_utils/bulk_import.py
🧬 Code graph analysis (1)
netbox_librenms_plugin/views/imports/list.py (3)
netbox_librenms_plugin/views/imports/actions.py (2)
  • get (750-770)
  • get (776-805)
netbox_librenms_plugin/models.py (1)
  • LibreNMSSettings (7-48)
netbox_librenms_plugin/utils.py (1)
  • get_user_pref (258-262)
🔇 Additional comments (4)
netbox_librenms_plugin/tests/test_background_jobs.py (1)

689-723: Solid coverage for naming-aware cache-key loading paths.

These assertions correctly lock in use_sysname/strip_domain propagation through _load_job_results().

Also applies to: 743-761

netbox_librenms_plugin/import_utils/bulk_import.py (1)

530-537: Good cache-key scoping for naming preferences.

Including use_sysname and strip_domain in both validated-device and metadata keys prevents stale cross-mode cache reuse.

Also applies to: 607-613

netbox_librenms_plugin/jobs.py (1)

49-50: Naming-preference propagation is correctly wired through the job lifecycle.

Run signature, processing call, and stored job.data all stay aligned for use_sysname and strip_domain.

Also applies to: 95-97, 116-117

netbox_librenms_plugin/templates/netbox_librenms_plugin/htmx/device_validation_details.html (1)

50-56: Accessibility and status-handling updates look good.

The int/string status normalization and the added aria-label + rel="noopener noreferrer" harden both UX and safety in this modal.

Also applies to: 123-125, 172-174, 185-187, 228-230, 283-285, 308-310, 446-447, 506-507, 544-545, 553-554, 596-607

Comment thread netbox_librenms_plugin/views/sync/device_fields.py
… handling

- actions.py: check both hyphenated and underscore toggle key variants in
  _resolve_naming_preferences (e.g. 'use-sysname-toggle' and 'use_sysname-toggle')
- actions.py: add select_for_update() row lock inside link/update/update_serial
  transaction block; document DB-unique-constraint limitation
- actions.py sync_serial: wrap conflict-check-and-write in transaction.atomic()
  with select_for_update() to prevent concurrent serial assignment race
- device_fields.py RemoveServerMappingView: wrap full_clean()/save() in
  try/except for ValidationError and Exception; call transaction.set_rollback(),
  messages.error(), and redirect on failure instead of bubbling as 500
- bulk_import.py: fix vc_domain dedup key to use sorted member serials instead
  of per-device device_id, preventing duplicate VC creation for stack members
- list.py: add comment explaining why POST toggles are not re-read (they are
  saved via savePref AJAX before filter form submission)
- cache.py: add TODO comment noting location cache key should be server-scoped
  in a future PR
- tests: add TestResolveNamingPreferencesKeys, TestVCDomainStackDedup, and
  TestRemoveServerMappingViewErrorHandling; update DeviceConflictActionView
  tests to mock select_for_update() and transaction.atomic()
Primary IP is informational only and not imported, so the green
check-circle icons were misleading.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 3

♻️ Duplicate comments (3)
netbox_librenms_plugin/import_utils/cache.py (1)

71-76: ⚠️ Potential issue | 🟠 Major

Server-agnostic location choices cache key still risks cross-server label bleed.

Line 75 still uses a global key ("librenms_locations_choices"), so cached location labels can be reused across different servers and render incorrect filter summaries.

🔧 Proposed fix
-    location_cache_key = "librenms_locations_choices"
+    location_cache_key = f"librenms_locations_choices:{server_key}"
     cached_locations = cache.get(location_cache_key)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@netbox_librenms_plugin/import_utils/cache.py` around lines 71 - 76, The cache
key for location choices is currently global (location_cache_key =
"librenms_locations_choices") which allows labels to bleed across servers;
change the key to include the server identifier (e.g., location_cache_key =
f"librenms_locations_choices:{server_key}") wherever location_cache_key is
defined/used (including the cache.get(...) and any cache.set(...) calls) and
ensure server_key is available in that scope before constructing the key so each
server has its own cached location choices.
netbox_librenms_plugin/views/imports/list.py (1)

451-460: ⚠️ Potential issue | 🟠 Major

Naming-preference precedence still drops submitted toggle values when user prefs are unset.

At Line 451 and Line 457, the ternary is reversed: data_source toggle values are only considered when *_pref is not None. When pref is None, form-provided toggles are bypassed and defaults are used.

🔧 Proposed fix
-        use_sysname = (
-            data_source.get("use_sysname_toggle", use_sysname_pref)
-            if use_sysname_pref is not None
-            else (getattr(_settings, "use_sysname_default", True) if _settings else True)
-        )
-        strip_domain = (
-            data_source.get("strip_domain_toggle", strip_domain_pref)
-            if strip_domain_pref is not None
-            else (getattr(_settings, "strip_domain_default", False) if _settings else False)
-        )
+        form_use_sysname = data_source.get("use_sysname_toggle")
+        form_strip_domain = data_source.get("strip_domain_toggle")
+        use_sysname = (
+            form_use_sysname
+            if form_use_sysname is not None
+            else (
+                use_sysname_pref
+                if use_sysname_pref is not None
+                else (getattr(_settings, "use_sysname_default", True) if _settings else True)
+            )
+        )
+        strip_domain = (
+            form_strip_domain
+            if form_strip_domain is not None
+            else (
+                strip_domain_pref
+                if strip_domain_pref is not None
+                else (getattr(_settings, "strip_domain_default", False) if _settings else False)
+            )
+        )
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@netbox_librenms_plugin/views/imports/list.py` around lines 451 - 460, The
ternary conditions for use_sysname and strip_domain are reversed so submitted
toggles in data_source get ignored when user prefs are None; change the logic in
the use_sysname and strip_domain assignments to prefer
data_source.get("use_sysname_toggle"/"strip_domain_toggle") when it exists,
otherwise fall back to use_sysname_pref/strip_domain_pref if not None, and only
then to getattr(_settings, "use_sysname_default"/"strip_domain_default",
<default>). Update the expressions referencing use_sysname_pref,
strip_domain_pref, data_source, and _settings (and the attributes
use_sysname_default/strip_domain_default) accordingly so the precedence is:
submitted toggle → user pref → settings default.
netbox_librenms_plugin/views/imports/actions.py (1)

1177-1186: ⚠️ Potential issue | 🟠 Major

Use the mapping accessor before legacy migration checks.

This branch still reads the librenms_id custom field directly for mapping validation. Please resolve the current mapping via self.librenms_api.get_librenms_id(existing_device) first, and keep raw custom-field access only for legacy-format detection/migration gating.

♻️ Suggested adjustment
+            mapped_id = self.librenms_api.get_librenms_id(existing_device)
+            if mapped_id is None:
+                return HttpResponse("Device has no LibreNMS mapping to migrate.", status=400)
+            if mapped_id != librenms_id:
+                return HttpResponse(
+                    f"Mapped LibreNMS ID ({mapped_id}) does not match active device ID ({librenms_id}).",
+                    status=400,
+                )
             cf_value = existing_device.custom_field_data.get("librenms_id")
             if not isinstance(cf_value, int):
                 return HttpResponse(
                     "Device librenms_id is already in JSON format; no migration needed.",
                     status=400,
                 )

As per coding guidelines: "Always call LibreNMSAPI.get_librenms_id to retrieve the device/VM LibreNMS mapping via the librenms_id custom field instead of touching the field directly".

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@netbox_librenms_plugin/views/imports/actions.py` around lines 1177 - 1186,
Call self.librenms_api.get_librenms_id(existing_device) first and use its
returned value for the mapping validation instead of reading
existing_device.custom_field_data["librenms_id"] directly; keep direct access to
existing_device.custom_field_data only to detect the legacy JSON format (i.e.,
to gate migration when the stored value is not an int) and then perform the
legacy migration to update the mapping; update the checks around cf_value, the
comparison to librenms_id, and the HttpResponse branches to rely on the resolved
mapping from get_librenms_id (use variables like existing_device, cf_value,
librenms_id, and self.librenms_api.get_librenms_id to locate the code).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@netbox_librenms_plugin/import_utils/bulk_import.py`:
- Around line 127-129: The progress log is emitted before the current device is
actually imported (off-by-one); move the job.logger.info(... f"Imported device
{idx} of {total}") so it runs only after a successful import of that device
(i.e., inside the success path of the import loop), and ensure the reported
count matches completed items (use idx+1 if the loop index is zero-based, or
keep idx if it is already 1-based); reference the job.logger call and the loop
variables idx and total when making the change.

In `@netbox_librenms_plugin/tests/test_permissions.py`:
- Around line 961-1038: Add a positive-path assertion to the existing tests to
catch regression where the wrong custom-field key is mutated: in the successful
removal case (use RemoveServerMappingView.post and the mock Device returned by
Device.objects.select_for_update.get), set up a mock_device with
custom_field_data containing the target librenms_id entry, call
view.post(request, pk=...), then assert that
mock_device.custom_field_data["librenms_id"] no longer contains the removed
server_key (or has been updated appropriately) and that mock_device.save() was
called to persist the change; this ensures the view mutates and saves
custom_field_data["librenms_id"] correctly on success.

In `@netbox_librenms_plugin/views/imports/actions.py`:
- Around line 1046-1049: The serial-conflict check around incoming_serial /
conflict_device (the block that queries
Device.objects.filter(serial=incoming_serial).exclude(pk=existing_device.pk).first())
is still race-prone; modify the flow to either (A) enforce a DB-level unique
constraint on the Device.serial column and wrap the create/update in
transaction.atomic while catching IntegrityError to handle duplicate-serial
failures, or (B) serialize attempts on the serial value (e.g., PostgreSQL
advisory lock or a SELECT ... FOR UPDATE on a row representing that serial)
around the checks and the subsequent save so concurrent requests cannot both
pass the check; apply the same change for the other occurrences you noted (the
blocks around lines where incoming_serial is checked at 1072-1074 and 1116-1120)
and ensure you handle and surface IntegrityError or lock-failure cases to the
caller.

---

Duplicate comments:
In `@netbox_librenms_plugin/import_utils/cache.py`:
- Around line 71-76: The cache key for location choices is currently global
(location_cache_key = "librenms_locations_choices") which allows labels to bleed
across servers; change the key to include the server identifier (e.g.,
location_cache_key = f"librenms_locations_choices:{server_key}") wherever
location_cache_key is defined/used (including the cache.get(...) and any
cache.set(...) calls) and ensure server_key is available in that scope before
constructing the key so each server has its own cached location choices.

In `@netbox_librenms_plugin/views/imports/actions.py`:
- Around line 1177-1186: Call self.librenms_api.get_librenms_id(existing_device)
first and use its returned value for the mapping validation instead of reading
existing_device.custom_field_data["librenms_id"] directly; keep direct access to
existing_device.custom_field_data only to detect the legacy JSON format (i.e.,
to gate migration when the stored value is not an int) and then perform the
legacy migration to update the mapping; update the checks around cf_value, the
comparison to librenms_id, and the HttpResponse branches to rely on the resolved
mapping from get_librenms_id (use variables like existing_device, cf_value,
librenms_id, and self.librenms_api.get_librenms_id to locate the code).

In `@netbox_librenms_plugin/views/imports/list.py`:
- Around line 451-460: The ternary conditions for use_sysname and strip_domain
are reversed so submitted toggles in data_source get ignored when user prefs are
None; change the logic in the use_sysname and strip_domain assignments to prefer
data_source.get("use_sysname_toggle"/"strip_domain_toggle") when it exists,
otherwise fall back to use_sysname_pref/strip_domain_pref if not None, and only
then to getattr(_settings, "use_sysname_default"/"strip_domain_default",
<default>). Update the expressions referencing use_sysname_pref,
strip_domain_pref, data_source, and _settings (and the attributes
use_sysname_default/strip_domain_default) accordingly so the precedence is:
submitted toggle → user pref → settings default.

ℹ️ Review info

Configuration used: Repository UI (base), Organization UI (inherited)

Review profile: ASSERTIVE

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 3b5d71d and b64c35a.

📒 Files selected for processing (8)
  • netbox_librenms_plugin/import_utils/bulk_import.py
  • netbox_librenms_plugin/import_utils/cache.py
  • netbox_librenms_plugin/templates/netbox_librenms_plugin/htmx/device_validation_details.html
  • netbox_librenms_plugin/tests/test_import_utils.py
  • netbox_librenms_plugin/tests/test_permissions.py
  • netbox_librenms_plugin/views/imports/actions.py
  • netbox_librenms_plugin/views/imports/list.py
  • netbox_librenms_plugin/views/sync/device_fields.py
📜 Review details
🧰 Additional context used
📓 Path-based instructions (5)
**/*.py

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

**/*.py: Reuse the librenms_api.py LibreNMS client instead of making new requests calls; it handles multi-server configs via LibreNMSSettings model and the servers plugin config, plus caching
Always call LibreNMSAPI.get_librenms_id to retrieve the device/VM LibreNMS mapping via the librenms_id custom field instead of touching the field directly
Matching must be exact-only for site, platform, device type, and role; do not add fuzzy matching; use the functions find_matching_site, match_librenms_hardware_to_device_type, and find_matching_platform from utils.py

Files:

  • netbox_librenms_plugin/views/sync/device_fields.py
  • netbox_librenms_plugin/import_utils/bulk_import.py
  • netbox_librenms_plugin/import_utils/cache.py
  • netbox_librenms_plugin/tests/test_permissions.py
  • netbox_librenms_plugin/views/imports/actions.py
  • netbox_librenms_plugin/views/imports/list.py
netbox_librenms_plugin/templates/**/*.html

📄 CodeRabbit inference engine (.github/instructions/frontend.instructions.md)

netbox_librenms_plugin/templates/**/*.html: HTMX 2.x is the primary async layer. Table row updates should return <tr hx-swap-oob="true">.
Avoid outerHTML swaps in HTMX; use OOB or targeted innerHTML swaps to keep table layout intact.
Modals use Tabler (Bootstrap-like) but without bootstrap.Modal helpers.
Modal buttons should target the htmx-modal-content element. JavaScript in librenms_import.html toggles the wrapper; do not reintroduce data-bs-toggle or duplicate modal IDs.
Keep <select class="device-role-select"> markup stable to preserve JavaScript hook-up with TomSelect decorators.
Styling assumes Tabler defaults. Do not add table-responsive wrappers as they were deliberately removed to prevent dropdown clipping.

Files:

  • netbox_librenms_plugin/templates/netbox_librenms_plugin/htmx/device_validation_details.html
netbox_librenms_plugin/templates/netbox_librenms_plugin/htmx/**/*.html

📄 CodeRabbit inference engine (.github/instructions/frontend.instructions.md)

HTMX fragments should live in templates/netbox_librenms_plugin/htmx/. Keep server responses and HTMX targets in sync.

Files:

  • netbox_librenms_plugin/templates/netbox_librenms_plugin/htmx/device_validation_details.html
netbox_librenms_plugin/templates/netbox_librenms_plugin/**/*.html

📄 CodeRabbit inference engine (.github/instructions/frontend.instructions.md)

Templates should live in templates/netbox_librenms_plugin/. Reusable template includes should be placed under inc/ subdirectory.

Files:

  • netbox_librenms_plugin/templates/netbox_librenms_plugin/htmx/device_validation_details.html
**/views/imports/**

📄 CodeRabbit inference engine (.github/instructions/background-jobs.instructions.md)

**/views/imports/**: Use Job UUID (job.job_id) for RQ API endpoints: /api/core/background-tasks/{uuid}/
Use Job PK (job.pk) for database endpoints and result loading
Check rq_job.is_stopped or rq_job.is_failed flags in Redis for cancellation detection, not database status
Call /api/core/background-tasks/{uuid}/stop/ to stop RQ job in the job cancellation flow
Call plugin's sync endpoint /api/plugins/librenms_plugin/jobs/{pk}/sync-status/ to update database after stopping RQ job
Poll /api/core/background-tasks/{uuid}/ for real-time RQ status instead of polling the database
api/views.py::sync_job_status() must sync database Job status with RQ job status because NetBox worker doesn't always update DB when jobs stop before processing starts

Files:

  • netbox_librenms_plugin/views/imports/actions.py
  • netbox_librenms_plugin/views/imports/list.py
🧠 Learnings (24)
📓 Common learnings
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-02-15T15:38:54.189Z
Learning: Applies to views/imports/**/*.py : REST endpoints for imports live in `views/imports/actions.py` with the list view in `views/imports/list.py`, surface via `urls.py`, and emit HTMX fragments in `templates/netbox_librenms_plugin/htmx/`; keep server responses and HTMX targets in sync
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-02-15T15:38:54.189Z
Learning: Applies to **/*.py : Reuse the `librenms_api.py` LibreNMS client instead of making new `requests` calls; it handles multi-server configs via `LibreNMSSettings` model and the `servers` plugin config, plus caching
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/testing.instructions.md:0-0
Timestamp: 2026-02-15T15:39:20.744Z
Learning: Applies to tests/**/*.py : Patch deferred/inline imports at their source module (e.g., `netbox_librenms_plugin.import_utils.process_device_filters`), not the consuming module
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/testing.instructions.md:0-0
Timestamp: 2026-02-15T15:39:20.744Z
Learning: Applies to tests/**/*.py : Test coverage mapping: `librenms_api.py` → `test_librenms_api.py`, `import_utils.py`/`import_validation_helpers.py`/`utils.py` → `test_import_utils.py`/`test_import_validation_helpers.py`/`test_utils.py`, `jobs.py`/`views/imports/list.py` → `test_background_jobs.py`
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-02-15T15:38:54.189Z
Learning: Applies to **/*.py : Always call `LibreNMSAPI.get_librenms_id` to retrieve the device/VM LibreNMS mapping via the `librenms_id` custom field instead of touching the field directly
📚 Learning: 2026-02-15T15:38:54.189Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-02-15T15:38:54.189Z
Learning: Applies to views/imports/**/*.py : REST endpoints for imports live in `views/imports/actions.py` with the list view in `views/imports/list.py`, surface via `urls.py`, and emit HTMX fragments in `templates/netbox_librenms_plugin/htmx/`; keep server responses and HTMX targets in sync

Applied to files:

  • netbox_librenms_plugin/views/sync/device_fields.py
  • netbox_librenms_plugin/templates/netbox_librenms_plugin/htmx/device_validation_details.html
  • netbox_librenms_plugin/import_utils/bulk_import.py
  • netbox_librenms_plugin/import_utils/cache.py
  • netbox_librenms_plugin/tests/test_permissions.py
  • netbox_librenms_plugin/views/imports/actions.py
  • netbox_librenms_plugin/views/imports/list.py
📚 Learning: 2026-02-15T15:39:11.578Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/frontend.instructions.md:0-0
Timestamp: 2026-02-15T15:39:11.578Z
Learning: Applies to netbox_librenms_plugin/templates/netbox_librenms_plugin/*sync*.html : Sync pages should extend `librenms_sync_base.html`.

Applied to files:

  • netbox_librenms_plugin/views/sync/device_fields.py
  • netbox_librenms_plugin/templates/netbox_librenms_plugin/htmx/device_validation_details.html
  • netbox_librenms_plugin/views/imports/actions.py
📚 Learning: 2026-02-15T15:38:54.189Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-02-15T15:38:54.189Z
Learning: Applies to {navigation.py,urls.py,api/**} : Respect NetBox plugin APIs in `navigation.py`, `urls.py`, and `api/` directories

Applied to files:

  • netbox_librenms_plugin/views/sync/device_fields.py
📚 Learning: 2026-02-15T15:38:54.189Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-02-15T15:38:54.189Z
Learning: Applies to **/*.py : Always call `LibreNMSAPI.get_librenms_id` to retrieve the device/VM LibreNMS mapping via the `librenms_id` custom field instead of touching the field directly

Applied to files:

  • netbox_librenms_plugin/views/sync/device_fields.py
  • netbox_librenms_plugin/views/imports/actions.py
📚 Learning: 2026-02-15T15:38:54.189Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-02-15T15:38:54.189Z
Learning: Follow sync pipeline flow: fetch LibreNMS data via `librenms_api.py`, cache it using `CacheMixin`, build comparison tables in `tables/`, and render HTMX fragments in `templates/netbox_librenms_plugin/htmx/`

Applied to files:

  • netbox_librenms_plugin/views/sync/device_fields.py
  • netbox_librenms_plugin/import_utils/bulk_import.py
📚 Learning: 2026-02-15T15:39:11.578Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/frontend.instructions.md:0-0
Timestamp: 2026-02-15T15:39:11.578Z
Learning: Applies to netbox_librenms_plugin/templates/**/*.html : Avoid `outerHTML` swaps in HTMX; use OOB or targeted `innerHTML` swaps to keep table layout intact.

Applied to files:

  • netbox_librenms_plugin/templates/netbox_librenms_plugin/htmx/device_validation_details.html
📚 Learning: 2026-02-15T15:39:11.578Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/frontend.instructions.md:0-0
Timestamp: 2026-02-15T15:39:11.578Z
Learning: Applies to netbox_librenms_plugin/templates/netbox_librenms_plugin/htmx/**/*.html : HTMX fragments should live in `templates/netbox_librenms_plugin/htmx/`. Keep server responses and HTMX targets in sync.

Applied to files:

  • netbox_librenms_plugin/templates/netbox_librenms_plugin/htmx/device_validation_details.html
📚 Learning: 2026-02-15T15:39:11.578Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/frontend.instructions.md:0-0
Timestamp: 2026-02-15T15:39:11.578Z
Learning: Applies to netbox_librenms_plugin/templates/**/*.html : HTMX 2.x is the primary async layer. Table row updates should return `<tr hx-swap-oob="true">`.

Applied to files:

  • netbox_librenms_plugin/templates/netbox_librenms_plugin/htmx/device_validation_details.html
📚 Learning: 2026-02-15T15:39:11.578Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/frontend.instructions.md:0-0
Timestamp: 2026-02-15T15:39:11.578Z
Learning: Applies to netbox_librenms_plugin/templates/**/*.html : Modal buttons should target the `htmx-modal-content` element. JavaScript in `librenms_import.html` toggles the wrapper; do not reintroduce `data-bs-toggle` or duplicate modal IDs.

Applied to files:

  • netbox_librenms_plugin/templates/netbox_librenms_plugin/htmx/device_validation_details.html
📚 Learning: 2026-02-15T15:39:11.578Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/frontend.instructions.md:0-0
Timestamp: 2026-02-15T15:39:11.578Z
Learning: Applies to netbox_librenms_plugin/templates/**/*.html : Keep `<select class="device-role-select">` markup stable to preserve JavaScript hook-up with TomSelect decorators.

Applied to files:

  • netbox_librenms_plugin/templates/netbox_librenms_plugin/htmx/device_validation_details.html
  • netbox_librenms_plugin/views/imports/actions.py
📚 Learning: 2026-02-15T15:38:54.189Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-02-15T15:38:54.189Z
Learning: Applies to templates/**/*.html : Reuse template includes under `templates/netbox_librenms_plugin/inc/` and ensure sync pages extend `librenms_sync_base.html`

Applied to files:

  • netbox_librenms_plugin/templates/netbox_librenms_plugin/htmx/device_validation_details.html
📚 Learning: 2026-02-15T15:39:11.578Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/frontend.instructions.md:0-0
Timestamp: 2026-02-15T15:39:11.578Z
Learning: Applies to netbox_librenms_plugin/templates/**/*.html : Styling assumes Tabler defaults. Do not add `table-responsive` wrappers as they were deliberately removed to prevent dropdown clipping.

Applied to files:

  • netbox_librenms_plugin/templates/netbox_librenms_plugin/htmx/device_validation_details.html
📚 Learning: 2026-02-15T15:39:20.744Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/testing.instructions.md:0-0
Timestamp: 2026-02-15T15:39:20.744Z
Learning: Applies to tests/**/*.py : Patch deferred/inline imports at their source module (e.g., `netbox_librenms_plugin.import_utils.process_device_filters`), not the consuming module

Applied to files:

  • netbox_librenms_plugin/import_utils/bulk_import.py
  • netbox_librenms_plugin/import_utils/cache.py
  • netbox_librenms_plugin/tests/test_permissions.py
📚 Learning: 2026-02-15T15:39:03.405Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/background-jobs.instructions.md:0-0
Timestamp: 2026-02-15T15:39:03.405Z
Learning: Applies to **/views/imports/** : Check `rq_job.is_stopped` or `rq_job.is_failed` flags in Redis for cancellation detection, not database status

Applied to files:

  • netbox_librenms_plugin/import_utils/bulk_import.py
📚 Learning: 2026-02-15T15:39:03.405Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/background-jobs.instructions.md:0-0
Timestamp: 2026-02-15T15:39:03.405Z
Learning: Applies to **/views/imports/** : `api/views.py::sync_job_status()` must sync database Job status with RQ job status because NetBox worker doesn't always update DB when jobs stop before processing starts

Applied to files:

  • netbox_librenms_plugin/import_utils/bulk_import.py
📚 Learning: 2026-02-15T15:39:03.405Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/background-jobs.instructions.md:0-0
Timestamp: 2026-02-15T15:39:03.405Z
Learning: Applies to **/jobs.py : Jobs must run via Redis Queue (RQ) in Redis, separate from the database Job model, and real-time status must be checked via RQ, not the database

Applied to files:

  • netbox_librenms_plugin/import_utils/bulk_import.py
📚 Learning: 2026-02-15T15:39:03.405Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/background-jobs.instructions.md:0-0
Timestamp: 2026-02-15T15:39:03.405Z
Learning: Applies to **/jobs.py : Database Job status values must be: `pending`, `scheduled`, `running`, `completed`, `failed`, `errored` (NO `cancelled` status exists)

Applied to files:

  • netbox_librenms_plugin/import_utils/bulk_import.py
📚 Learning: 2026-02-15T15:39:03.405Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/background-jobs.instructions.md:0-0
Timestamp: 2026-02-15T15:39:03.405Z
Learning: Applies to **/jobs.py : RQ status values must be: `queued`, `started`, `finished`, `stopped`, `failed` (NOT `completed`)

Applied to files:

  • netbox_librenms_plugin/import_utils/bulk_import.py
📚 Learning: 2026-02-15T15:39:03.405Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/background-jobs.instructions.md:0-0
Timestamp: 2026-02-15T15:39:03.405Z
Learning: Applies to **/jobs.py : Background jobs must use NetBox's `JobRunner` base class (`netbox.jobs.JobRunner`) for long-running operations like device filtering with VC detection

Applied to files:

  • netbox_librenms_plugin/import_utils/bulk_import.py
  • netbox_librenms_plugin/views/imports/list.py
📚 Learning: 2026-02-15T15:39:03.405Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/background-jobs.instructions.md:0-0
Timestamp: 2026-02-15T15:39:03.405Z
Learning: Applies to **/views/imports/** : Call plugin's sync endpoint `/api/plugins/librenms_plugin/jobs/{pk}/sync-status/` to update database after stopping RQ job

Applied to files:

  • netbox_librenms_plugin/import_utils/bulk_import.py
📚 Learning: 2026-02-15T15:39:20.744Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/testing.instructions.md:0-0
Timestamp: 2026-02-15T15:39:20.744Z
Learning: Applies to tests/test_import_utils.py : Cache key tests must patch `get_validated_device_cache_key` from `import_utils.py`; never hardcode key formats like `job_123_device_1`

Applied to files:

  • netbox_librenms_plugin/import_utils/bulk_import.py
  • netbox_librenms_plugin/import_utils/cache.py
  • netbox_librenms_plugin/tests/test_permissions.py
📚 Learning: 2026-02-15T15:39:20.744Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/testing.instructions.md:0-0
Timestamp: 2026-02-15T15:39:20.744Z
Learning: Applies to tests/test_background_jobs.py : Patch `cache` where imported: `netbox_librenms_plugin.views.imports.list.cache`, not `django.core.cache.cache`

Applied to files:

  • netbox_librenms_plugin/import_utils/cache.py
  • netbox_librenms_plugin/tests/test_permissions.py
  • netbox_librenms_plugin/views/imports/actions.py
  • netbox_librenms_plugin/views/imports/list.py
📚 Learning: 2026-02-15T15:39:20.744Z
Learnt from: CR
Repo: marcinpsk/netbox-librenms-plugin PR: 0
File: .github/instructions/testing.instructions.md:0-0
Timestamp: 2026-02-15T15:39:20.744Z
Learning: Applies to tests/**/*.py : Test coverage mapping: `librenms_api.py` → `test_librenms_api.py`, `import_utils.py`/`import_validation_helpers.py`/`utils.py` → `test_import_utils.py`/`test_import_validation_helpers.py`/`test_utils.py`, `jobs.py`/`views/imports/list.py` → `test_background_jobs.py`

Applied to files:

  • netbox_librenms_plugin/tests/test_permissions.py
🧬 Code graph analysis (5)
netbox_librenms_plugin/views/sync/device_fields.py (3)
netbox_librenms_plugin/utils.py (1)
  • match_librenms_hardware_to_device_type (305-353)
netbox_librenms_plugin/views/mixins.py (2)
  • LibreNMSAPIMixin (196-282)
  • librenms_api (217-231)
netbox_librenms_plugin/librenms_api.py (2)
  • get_librenms_id (173-231)
  • get_device_info (314-337)
netbox_librenms_plugin/import_utils/bulk_import.py (5)
netbox_librenms_plugin/utils.py (1)
  • find_by_librenms_id (70-89)
netbox_librenms_plugin/import_utils/cache.py (3)
  • get_cache_metadata_key (12-31)
  • get_import_device_cache_key (169-188)
  • get_validated_device_cache_key (131-166)
netbox_librenms_plugin/import_utils/filters.py (1)
  • get_librenms_devices_for_import (53-234)
netbox_librenms_plugin/import_utils/permissions.py (1)
  • require_permissions (27-44)
netbox_librenms_plugin/import_utils/virtual_chassis.py (3)
  • create_virtual_chassis_with_members (324-474)
  • empty_virtual_chassis_data (15-23)
  • prefetch_vc_data_for_devices (83-117)
netbox_librenms_plugin/import_utils/cache.py (1)
netbox_librenms_plugin/views/imports/list.py (1)
  • get (128-375)
netbox_librenms_plugin/tests/test_permissions.py (1)
netbox_librenms_plugin/views/sync/device_fields.py (8)
  • RemoveServerMappingView (387-446)
  • post (19-56)
  • post (66-110)
  • post (120-171)
  • post (181-238)
  • post (251-308)
  • post (318-384)
  • post (394-446)
netbox_librenms_plugin/views/imports/list.py (3)
netbox_librenms_plugin/views/imports/actions.py (2)
  • get (761-781)
  • get (787-816)
netbox_librenms_plugin/models.py (1)
  • LibreNMSSettings (7-48)
netbox_librenms_plugin/utils.py (1)
  • get_user_pref (258-262)
🔇 Additional comments (7)
netbox_librenms_plugin/import_utils/bulk_import.py (1)

494-531: Good RQ-first cancellation detection with DB fallback.

The termination checks in Line 494 and Line 529 correctly prioritize Redis/RQ state and gracefully fall back to DB status checks.

netbox_librenms_plugin/views/imports/list.py (1)

309-316: Correct UUID/PK separation for background job polling and result loading.

This correctly uses job.job_id for /api/core/background-tasks/{uuid}/ and keeps job.pk for DB-backed result loading.

As per coding guidelines: "Use Job UUID (job.job_id) for RQ API endpoints ... Use Job PK (job.pk) for database endpoints and result loading."

netbox_librenms_plugin/templates/netbox_librenms_plugin/htmx/device_validation_details.html (1)

112-124: Nice hardening on external-link safety and icon-button accessibility.

The rel="noopener noreferrer" additions and explicit aria-label attributes are correctly applied in the modal actions.

Also applies to: 594-605

netbox_librenms_plugin/views/sync/device_fields.py (2)

12-56: Good addition of guarded name-sync flow.

Permission checks, LibreNMS lookup, validation handling, and user-facing redirects/messages are consistently handled here.


424-446: Transaction and failure handling in mapping removal looks solid.

Re-checking under select_for_update() and returning controlled messages on validation/runtime failures prevents partial updates and 500s.

netbox_librenms_plugin/views/imports/actions.py (2)

43-87: Naming preference resolution fallback chain is well-implemented.

Supporting both hyphenated/underscored keys and falling back through POST/GET → user prefs → plugin defaults is a solid hardening step.


845-863: Sync-state boolean logic is now consistent and safer.

platform_synced remains type-stable, and device-type sync correctly flips false when hardware exists but no mapping is found.

Comment thread netbox_librenms_plugin/import_utils/bulk_import.py Outdated
Comment thread netbox_librenms_plugin/tests/test_permissions.py
Comment thread netbox_librenms_plugin/views/imports/actions.py
- cache.py + forms.py: scope location choices cache key by server_key
  (f"librenms_locations_choices:{server_key}") so labels from different
  LibreNMS servers don't bleed into each other's filter summaries
- list.py ~245: include exc_info=True in swallowed cache-check exception log
- list.py ~337: log user.username scalar instead of full user object to avoid
  serialising sensitive/large data in exception handler
- list.py ~451: fix reversed ternary for use_sysname/strip_domain — submitted
  form toggle now correctly takes priority over user pref which takes priority
  over settings default (was: pref check gated the toggle, ignoring toggle when
  pref was None)
- actions.py ~52: add plain "use_sysname" / "strip_domain" canonical keys to
  _USE_SYSNAME_KEYS / _STRIP_DOMAIN_KEYS so preview and import use the same
  resolution logic regardless of which key name the form sends
- actions.py ~1016 / ~1136: wrap select_for_update().get() in try/except
  Device.DoesNotExist and return 409 instead of propagating a 500
- actions.py ~1054 / ~1084: add select_for_update() to serial-conflict filter
  query inside transaction to reduce concurrent-assignment race window
- device_fields.py ~416: also protect "default" server_key when legacy
  single-server librenms_url is configured (was only checking servers dict)
- device_fields.py ~430: wrap select_for_update().get(pk=pk) in try/except
  Device.DoesNotExist and redirect with error message instead of 500
- bulk_import.py ~127: move progress log inside success path after the import
  completes; use idx+1 so the count reflects completed items
- tests: add positive-path removal test; update DeviceConflictActionView mocks
  to cover select_for_update().filter() chain

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 4

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
netbox_librenms_plugin/views/imports/actions.py (2)

584-587: 🧹 Nitpick | 🔵 Trivial

Use lazy string formatting for logger calls.

The f-string is evaluated regardless of whether the log level is enabled. Use %-style formatting for logger efficiency.

♻️ Suggested fix
-                logger.info(
-                    f"Enqueued ImportDevicesJob {job.pk} (UUID: {job.job_id}) for user {request.user} - {total_import_count} devices/VMs"
-                )
+                logger.info(
+                    "Enqueued ImportDevicesJob %s (UUID: %s) for user %s - %d devices/VMs",
+                    job.pk, job.job_id, request.user, total_import_count
+                )
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@netbox_librenms_plugin/views/imports/actions.py` around lines 584 - 587, The
logger call in ImportDevicesJob enqueue uses an f-string which will be evaluated
even if the log level is disabled; change the logger invocation in the block
that logs "Enqueued ImportDevicesJob" (the logger.info call referencing job.pk,
job.job_id, request.user and total_import_count) to use lazy %-style formatting
and pass the values as separate parameters (e.g. logger.info("Enqueued
ImportDevicesJob %s (UUID: %s) for user %s - %d devices/VMs", job.pk,
job.job_id, request.user, total_import_count)) so interpolation happens only
when the message will actually be emitted.

594-606: 🧹 Nitpick | 🔵 Trivial

Consider using reverse() for URL generation.

Hardcoded URLs are fragile and may break if URL patterns change. Django's reverse() provides safer URL generation.

♻️ Suggested fix
+                from django.urls import reverse
+
                 messages.info(
                     request,
                     mark_safe(
                         f"Import job started for {total_import_count} device{'s' if total_import_count != 1 else ''}. "
-                        f'You can monitor progress in the <a href="/core/jobs/{job.pk}/">Jobs interface</a>.'
+                        f'You can monitor progress in the <a href="{reverse("core:job", kwargs={"pk": job.pk})}">Jobs interface</a>.'
                     ),
                 )

                 if request.headers.get("HX-Request"):
-                    # For HTMX requests, redirect to clean import page (no filters)
-                    # This matches the "Clear" button behavior
                     return HttpResponse(
                         "",
-                        headers={"HX-Redirect": "/plugins/librenms_plugin/librenms-import/"},
+                        headers={"HX-Redirect": reverse("plugins:netbox_librenms_plugin:librenms_import")},
                     )
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@netbox_librenms_plugin/views/imports/actions.py` around lines 594 - 606, The
code currently uses hardcoded URLs for the job link and the HTMX redirect (e.g.,
the f'/core/jobs/{job.pk}/' string and
'/plugins/librenms_plugin/librenms-import/') which is fragile; update the view
to build both URLs with Django's reverse() (or reverse_lazy) instead: generate
the job URL with reverse('admin:core_job_change', args=[job.pk]) or the
appropriate named route and use reverse('librenms_plugin:librenms-import') (or
the actual URL names used in your URLconf) for the HX-Redirect, then substitute
those reversed URLs into the mark_safe message and the HttpResponse headers that
check request.headers.get("HX-Request"); ensure imports include
django.urls.reverse and adjust any tests or templates that relied on the
previous hardcoded strings.
♻️ Duplicate comments (1)
netbox_librenms_plugin/import_utils/virtual_chassis.py (1)

309-318: ⚠️ Potential issue | 🟠 Major

Off-by-one error in position handling and 0-based fallback.

Two issues in this loop:

  1. Line 310: Fallback is idx (0-based), inconsistent with detect_virtual_chassis_from_inventory and _clone_virtual_chassis_data which use idx + 1.

  2. Lines 315-316: position = base_position + 1 assumes positions are 0-based, but data from detect_virtual_chassis_from_inventory already uses 1-based positions. This causes position 1 to generate names as if it were position 2.

Trace for a member with position=1:

  • base_position = 1, position = 2 → generates Member-2 instead of Member-1
🐛 Proposed fix
     for idx, member in enumerate(vc_data.get("members", [])):
-        raw_position = member.get("position", idx)
+        raw_position = member.get("position", idx + 1)
         try:
             base_position = int(raw_position)
         except (TypeError, ValueError):
-            base_position = idx
-        position = base_position + 1  # Convert to 1-based position
-        member["position"] = base_position
+            base_position = idx + 1
+        # Positions are already 1-based from detect_virtual_chassis_from_inventory
+        if base_position < 1:
+            base_position = idx + 1
+        member["position"] = base_position
         member["suggested_name"] = _generate_vc_member_name(
-            master_name, position, serial=member.get("serial"), pattern=vc_pattern
+            master_name, base_position, serial=member.get("serial"), pattern=vc_pattern
         )
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@netbox_librenms_plugin/import_utils/virtual_chassis.py` around lines 309 -
318, The loop in import_utils.virtual_chassis.py mis-handles 0/1-based
positions: parse raw_position into an int and treat it as 1-based (if parsed
value is >= 1 use it), otherwise fall back to idx + 1 (not idx), set
member["position"] to that 1-based value (base_position), and pass that same
1-based value to _generate_vc_member_name (do not add +1); this ensures
consistency with detect_virtual_chassis_from_inventory and
_clone_virtual_chassis_data and prevents off-by-one name generation.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@netbox_librenms_plugin/import_utils/filters.py`:
- Around line 41-43: The current filter uses int(d.get("disabled", 0)) which can
raise on None/empty/non-numeric values; change the comprehension to first
normalize/parse the disabled field with a safe conversion (e.g., val =
d.get("disabled"); try: disabled = int(val) except (TypeError, ValueError):
disabled = 0) and then filter with disabled != 1 (replace the existing list
comprehension in the devices filtering block that uses show_disabled). Apply the
same safe-parsing logic to the corresponding devices filtering in bulk_import.py
(the code that currently calls int(d.get("disabled", 0)) around line 446) so
both places use the same normalized disabled value before comparison.

In `@netbox_librenms_plugin/views/imports/actions.py`:
- Line 1051: Replace f-string logger calls with lazy `%`-style logging to avoid
eager evaluation; e.g., change logger.info(f"Linked device
'{existing_device.name}' to LibreNMS ID {librenms_id}") to logger.info("Linked
device '%s' to LibreNMS ID %s", existing_device.name, librenms_id) and apply the
same pattern for the other logger.info/debug/error calls referenced (lines
around the blocks that use variables like existing_device, librenms_id, and any
message templates at 1080-1083, 1110-1113, 1121, 1129, 1162, 1177, 1193,
1228-1231) so all logging defers string interpolation until the message is
emitted.

In `@netbox_librenms_plugin/views/sync/device_fields.py`:
- Around line 46-55: The device name update path currently calls
device.full_clean() and device.save() without wrapping the write inside a
database transaction; wrap the mutation in transaction.atomic() so the revert on
exception is atomic and consistent. Update the view handling the POST (the block
that sets device.name, calls device.full_clean() and device.save()) to perform
the read of selected items via request.POST.getlist('select'), load any cached
data with CacheMixin.get_cache_key(), and then run the full_clean()/save() (and
the device.name rollback on exception) inside a transaction.atomic() context;
ensure the view still uses LibreNMSPermissionMixin and
NetBoxObjectPermissionMixin for permission checks and ends by redirecting to the
sync tab with ?tab=<resource> on error/success.

---

Outside diff comments:
In `@netbox_librenms_plugin/views/imports/actions.py`:
- Around line 584-587: The logger call in ImportDevicesJob enqueue uses an
f-string which will be evaluated even if the log level is disabled; change the
logger invocation in the block that logs "Enqueued ImportDevicesJob" (the
logger.info call referencing job.pk, job.job_id, request.user and
total_import_count) to use lazy %-style formatting and pass the values as
separate parameters (e.g. logger.info("Enqueued ImportDevicesJob %s (UUID: %s)
for user %s - %d devices/VMs", job.pk, job.job_id, request.user,
total_import_count)) so interpolation happens only when the message will
actually be emitted.
- Around line 594-606: The code currently uses hardcoded URLs for the job link
and the HTMX redirect (e.g., the f'/core/jobs/{job.pk}/' string and
'/plugins/librenms_plugin/librenms-import/') which is fragile; update the view
to build both URLs with Django's reverse() (or reverse_lazy) instead: generate
the job URL with reverse('admin:core_job_change', args=[job.pk]) or the
appropriate named route and use reverse('librenms_plugin:librenms-import') (or
the actual URL names used in your URLconf) for the HX-Redirect, then substitute
those reversed URLs into the mark_safe message and the HttpResponse headers that
check request.headers.get("HX-Request"); ensure imports include
django.urls.reverse and adjust any tests or templates that relied on the
previous hardcoded strings.

---

Duplicate comments:
In `@netbox_librenms_plugin/import_utils/virtual_chassis.py`:
- Around line 309-318: The loop in import_utils.virtual_chassis.py mis-handles
0/1-based positions: parse raw_position into an int and treat it as 1-based (if
parsed value is >= 1 use it), otherwise fall back to idx + 1 (not idx), set
member["position"] to that 1-based value (base_position), and pass that same
1-based value to _generate_vc_member_name (do not add +1); this ensures
consistency with detect_virtual_chassis_from_inventory and
_clone_virtual_chassis_data and prevents off-by-one name generation.

ℹ️ Review info

Configuration used: Repository UI (base), Organization UI (inherited)

Review profile: ASSERTIVE

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 3b5d71d and a72e737.

📒 Files selected for processing (11)
  • netbox_librenms_plugin/forms.py
  • netbox_librenms_plugin/import_utils/bulk_import.py
  • netbox_librenms_plugin/import_utils/cache.py
  • netbox_librenms_plugin/import_utils/filters.py
  • netbox_librenms_plugin/import_utils/virtual_chassis.py
  • netbox_librenms_plugin/templates/netbox_librenms_plugin/htmx/device_validation_details.html
  • netbox_librenms_plugin/tests/test_import_utils.py
  • netbox_librenms_plugin/tests/test_permissions.py
  • netbox_librenms_plugin/views/imports/actions.py
  • netbox_librenms_plugin/views/imports/list.py
  • netbox_librenms_plugin/views/sync/device_fields.py

Comment on lines +322 to +338
# Device was deleted since caching — recompute readiness to match
# validate_device_for_import logic.
validation["existing_device"] = None
validation["existing_match_type"] = None
can_import = not bool(validation.get("issues"))
if validation.get("import_as_vm"):
# VMs only require a cluster (site/role not mandatory)
is_ready = can_import and bool(validation.get("cluster", {}).get("found"))
else:
is_ready = (
can_import
and bool(validation.get("site", {}).get("found"))
and bool(validation.get("device_type", {}).get("found"))
and bool(validation.get("device_role", {}).get("found"))
)
validation["can_import"] = can_import
validation["is_ready"] = is_ready

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion | 🟠 Major

Avoid in-place status recomputation here; route through import_validation_helpers.py.

This block manually recalculates can_import/is_ready and mutates validation state inline. That logic should be centralized to prevent behavior drift with the rest of import validation flows.

As per coding guidelines: "Centralize validation state mutation during import using import_validation_helpers.py for role/cluster/rack assignment, issue removal, and status recalculation."

Comment on lines +41 to +43
if not show_disabled:
devices = [d for d in devices if int(d.get("disabled", 0)) != 1]

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Guard disabled-flag parsing to prevent filter crashes.

At Line 42, int(d.get("disabled", 0)) can raise on None, empty strings, or unexpected payloads and abort filtering.

🔧 Proposed fix
+def _is_disabled(value) -> bool:
+    try:
+        return int(str(value).strip()) == 1
+    except (TypeError, ValueError):
+        return False
+
 ...
     if not show_disabled:
-        devices = [d for d in devices if int(d.get("disabled", 0)) != 1]
+        devices = [d for d in devices if not _is_disabled(d.get("disabled", 0))]

Please apply the same normalization in netbox_librenms_plugin/import_utils/bulk_import.py (Line 446) to keep behavior consistent.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@netbox_librenms_plugin/import_utils/filters.py` around lines 41 - 43, The
current filter uses int(d.get("disabled", 0)) which can raise on
None/empty/non-numeric values; change the comprehension to first normalize/parse
the disabled field with a safe conversion (e.g., val = d.get("disabled"); try:
disabled = int(val) except (TypeError, ValueError): disabled = 0) and then
filter with disabled != 1 (replace the existing list comprehension in the
devices filtering block that uses show_disabled). Apply the same safe-parsing
logic to the corresponding devices filtering in bulk_import.py (the code that
currently calls int(d.get("disabled", 0)) around line 446) so both places use
the same normalized disabled value before comparison.

existing_device.device_type = librenms_device_type
if err := _save_device(existing_device):
return err
logger.info(f"Linked device '{existing_device.name}' to LibreNMS ID {librenms_id}")

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial

Consider using lazy string formatting in logger calls.

Multiple logger.info() calls use f-strings which are evaluated regardless of log level. While not a functional issue, using %-style formatting is more efficient.

♻️ Example fix (apply similar pattern to other logger calls)
-                    logger.info(f"Linked device '{existing_device.name}' to LibreNMS ID {librenms_id}")
+                    logger.info("Linked device '%s' to LibreNMS ID %s", existing_device.name, librenms_id)

Also applies to: 1080-1083, 1110-1113, 1121-1121, 1129-1129, 1162-1162, 1177-1177, 1193-1193, 1228-1231

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@netbox_librenms_plugin/views/imports/actions.py` at line 1051, Replace
f-string logger calls with lazy `%`-style logging to avoid eager evaluation;
e.g., change logger.info(f"Linked device '{existing_device.name}' to LibreNMS ID
{librenms_id}") to logger.info("Linked device '%s' to LibreNMS ID %s",
existing_device.name, librenms_id) and apply the same pattern for the other
logger.info/debug/error calls referenced (lines around the blocks that use
variables like existing_device, librenms_id, and any message templates at
1080-1083, 1110-1113, 1121, 1129, 1162, 1177, 1193, 1228-1231) so all logging
defers string interpolation until the message is emitted.

Comment on lines +46 to +55
old_name = device.name
device.name = sys_name
try:
device.full_clean()
device.save()
except (ValidationError, IntegrityError) as e:
device.name = old_name
error_msg = e.message_dict if hasattr(e, "message_dict") else str(e)
messages.error(request, f"Failed to update device name to '{sys_name}': {error_msg}")
return redirect("plugins:netbox_librenms_plugin:device_librenms_sync", pk=pk)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion | 🟠 Major

Wrap name update write path in transaction.atomic().

This sync POST action writes model state but does not wrap the mutation in transaction.atomic().

🔧 Proposed fix
         old_name = device.name
-        device.name = sys_name
         try:
-            device.full_clean()
-            device.save()
+            with transaction.atomic():
+                device.name = sys_name
+                device.full_clean()
+                device.save()
         except (ValidationError, IntegrityError) as e:
             device.name = old_name
             error_msg = e.message_dict if hasattr(e, "message_dict") else str(e)

As per coding guidelines: "Sync action views must follow the pattern: check permissions with LibreNMSPermissionMixin and NetBoxObjectPermissionMixin, read selected items from request.POST.getlist('select'), load cached data using CacheMixin.get_cache_key(), apply changes inside transaction.atomic(), and redirect to the sync tab with ?tab=<resource>."

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@netbox_librenms_plugin/views/sync/device_fields.py` around lines 46 - 55, The
device name update path currently calls device.full_clean() and device.save()
without wrapping the write inside a database transaction; wrap the mutation in
transaction.atomic() so the revert on exception is atomic and consistent. Update
the view handling the POST (the block that sets device.name, calls
device.full_clean() and device.save()) to perform the read of selected items via
request.POST.getlist('select'), load any cached data with
CacheMixin.get_cache_key(), and then run the full_clean()/save() (and the
device.name rollback on exception) inside a transaction.atomic() context; ensure
the view still uses LibreNMSPermissionMixin and NetBoxObjectPermissionMixin for
permission checks and ends by redirecting to the sync tab with ?tab=<resource>
on error/success.

@marcinpsk marcinpsk closed this Mar 3, 2026
marcinpsk added a commit that referenced this pull request Mar 28, 2026
- #12: Pass manufacturer as explicit parameter to _build_row/_build_table_rows;
  remove self._device_manufacturer instance attribute (hidden temporal coupling)
- #13: Eliminate second resolve_module_type loop in _build_table_rows; track
  installable flag inline during first pass via sub_row.get('module_type_id')
- #14: Pre-compute ignore_cache dict once in _build_context; pass to
  _find_transparent_indices and _collect_top_items instead of double evaluation
- #15: Fix convert_speed_to_kbps return type annotation to -> int | None
- #16: Extract _apply_rules() inner helper in apply_normalization_rules to
  eliminate duplicated regex loop between manufacturer/non-manufacturer branches
- #17/#18: Extract BaseSNMPForm with 6 shared fields; rename AddToLIbreSNMPV1V2
  -> AddToLibreSNMPV1V2 and AddToLIbreSNMPV3 -> AddToLibreSNMPV3 (typo fix);
  add backwards-compatible aliases for existing imports
- #19: Rename related_name='librenms_mappings' -> 'librenms_device_type_mappings'
  on DeviceTypeMapping.netbox_device_type and 'librenms_module_type_mappings' on
  ModuleTypeMapping.netbox_module_type to avoid ambiguity; migration 0011
- #20: Add filterset_class to all six API ViewSets (InterfaceTypeMapping,
  DeviceTypeMapping, ModuleTypeMapping, ModuleBayMapping, NormalizationRule,
  InventoryIgnoreRule)
@marcinpsk
marcinpsk deleted the refactor/librenms_id branch May 12, 2026 19:37
marcinpsk added a commit that referenced this pull request Jun 1, 2026
- utils.set_librenms_device_id: use coerce_librenms_id so float-like IDs
  (1.9) and bools are rejected instead of silently truncated (#3).
- migrate.MoveInterfaceToWinnerView: lock + re-read the donor interface row
  inside the txn and use its locked name for the collision check, closing a
  concurrent-rename TOCTOU (the IP-move flow already did this) (#5).
- device_operations: populate existing_librenms_link on the primary-IP match
  path so an already-linked device no longer renders as 'not linked' (#6).
- _oob_interface_select.html: add a label for the new-interface name input (#8).
- ip_addresses_view: resolve the management IP once on the fresh-fetch path and
  cache it; flagging no longer makes a live LibreNMS call on cached renders (#12).
- sync/ip_addresses: rebuild the API client scoped to the POST server_key so the
  management-IP lookup hits the same server the cached rows came from (#13).
- _dt_mapping_form.html: drop the unneeded CSRF header from the read-only GET (#1).
- tests: stale promote_to_host clearing regression (#2); assert same-name
  interface lookup args (#9); distinct locked-row mocks in OOB transfer (#10).

Skipped: #4 (obsolete - auto_create_ipam removed), #7 (hx-include targets the
wrapper span which still includes the -cb checkbox), #11 (no VC-with-OOB config
in practice).
marcinpsk added a commit that referenced this pull request Jun 1, 2026
- utils.set_librenms_device_id: use coerce_librenms_id so float-like IDs
  (1.9) and bools are rejected instead of silently truncated (#3).
- migrate.MoveInterfaceToWinnerView: lock + re-read the donor interface row
  inside the txn and use its locked name for the collision check, closing a
  concurrent-rename TOCTOU (the IP-move flow already did this) (#5).
- device_operations: populate existing_librenms_link on the primary-IP match
  path so an already-linked device no longer renders as 'not linked' (#6).
- _oob_interface_select.html: add a label for the new-interface name input (#8).
- ip_addresses_view: resolve the management IP once on the fresh-fetch path and
  cache it; flagging no longer makes a live LibreNMS call on cached renders (#12).
- sync/ip_addresses: rebuild the API client scoped to the POST server_key so the
  management-IP lookup hits the same server the cached rows came from (#13).
- _dt_mapping_form.html: drop the unneeded CSRF header from the read-only GET (#1).
- tests: stale promote_to_host clearing regression (#2); assert same-name
  interface lookup args (#9); distinct locked-row mocks in OOB transfer (#10).

Skipped: #4 (obsolete - auto_create_ipam removed), #7 (hx-include targets the
wrapper span which still includes the -cb checkbox), #11 (no VC-with-OOB config
in practice).
marcinpsk added a commit that referenced this pull request Jun 1, 2026
- utils.set_librenms_device_id: use coerce_librenms_id so float-like IDs
  (1.9) and bools are rejected instead of silently truncated (#3).
- migrate.MoveInterfaceToWinnerView: lock + re-read the donor interface row
  inside the txn and use its locked name for the collision check, closing a
  concurrent-rename TOCTOU (the IP-move flow already did this) (#5).
- device_operations: populate existing_librenms_link on the primary-IP match
  path so an already-linked device no longer renders as 'not linked' (#6).
- _oob_interface_select.html: add a label for the new-interface name input (#8).
- ip_addresses_view: resolve the management IP once on the fresh-fetch path and
  cache it; flagging no longer makes a live LibreNMS call on cached renders (#12).
- sync/ip_addresses: rebuild the API client scoped to the POST server_key so the
  management-IP lookup hits the same server the cached rows came from (#13).
- _dt_mapping_form.html: drop the unneeded CSRF header from the read-only GET (#1).
- tests: stale promote_to_host clearing regression (#2); assert same-name
  interface lookup args (#9); distinct locked-row mocks in OOB transfer (#10).

Skipped: #4 (obsolete - auto_create_ipam removed), #7 (hx-include targets the
wrapper span which still includes the -cb checkbox), #11 (no VC-with-OOB config
in practice).
marcinpsk added a commit that referenced this pull request Jun 1, 2026
- utils.set_librenms_device_id: use coerce_librenms_id so float-like IDs
  (1.9) and bools are rejected instead of silently truncated (#3).
- migrate.MoveInterfaceToWinnerView: lock + re-read the donor interface row
  inside the txn and use its locked name for the collision check, closing a
  concurrent-rename TOCTOU (the IP-move flow already did this) (#5).
- device_operations: populate existing_librenms_link on the primary-IP match
  path so an already-linked device no longer renders as 'not linked' (#6).
- _oob_interface_select.html: add a label for the new-interface name input (#8).
- ip_addresses_view: resolve the management IP once on the fresh-fetch path and
  cache it; flagging no longer makes a live LibreNMS call on cached renders (#12).
- sync/ip_addresses: rebuild the API client scoped to the POST server_key so the
  management-IP lookup hits the same server the cached rows came from (#13).
- _dt_mapping_form.html: drop the unneeded CSRF header from the read-only GET (#1).
- tests: stale promote_to_host clearing regression (#2); assert same-name
  interface lookup args (#9); distinct locked-row mocks in OOB transfer (#10).

Skipped: #4 (obsolete - auto_create_ipam removed), #7 (hx-include targets the
wrapper span which still includes the -cb checkbox), #11 (no VC-with-OOB config
in practice).
marcinpsk added a commit that referenced this pull request Jun 2, 2026
- utils.set_librenms_device_id: use coerce_librenms_id so float-like IDs
  (1.9) and bools are rejected instead of silently truncated (#3).
- migrate.MoveInterfaceToWinnerView: lock + re-read the donor interface row
  inside the txn and use its locked name for the collision check, closing a
  concurrent-rename TOCTOU (the IP-move flow already did this) (#5).
- device_operations: populate existing_librenms_link on the primary-IP match
  path so an already-linked device no longer renders as 'not linked' (#6).
- _oob_interface_select.html: add a label for the new-interface name input (#8).
- ip_addresses_view: resolve the management IP once on the fresh-fetch path and
  cache it; flagging no longer makes a live LibreNMS call on cached renders (#12).
- sync/ip_addresses: rebuild the API client scoped to the POST server_key so the
  management-IP lookup hits the same server the cached rows came from (#13).
- _dt_mapping_form.html: drop the unneeded CSRF header from the read-only GET (#1).
- tests: stale promote_to_host clearing regression (#2); assert same-name
  interface lookup args (#9); distinct locked-row mocks in OOB transfer (#10).

Skipped: #4 (obsolete - auto_create_ipam removed), #7 (hx-include targets the
wrapper span which still includes the -cb checkbox), #11 (no VC-with-OOB config
in practice).
marcinpsk added a commit that referenced this pull request Jun 2, 2026
- AddAsOOBView: gate Interface/IPAddress perms inside the OOB-IP sub-flow
  (top-level only authorizes change Device); skip IP-set with a warning when
  the perm is missing so the link still commits (#14)
- modules_view: skip interface matching for OOB-sourced inventory rows — only
  the main device's interfaces are indexed, so a name match is a false positive (#13)
- device_operations: describe existing host/OOB linkage in the primary-IP
  collision warning instead of always saying 'not linked to LibreNMS' (#9)
- guard posted server_key: LibreNMSAPI() raises KeyError for an unknown key, so
  a stale/tampered POST turned the IP-sync and OOB-attach/promote actions into a
  500. Add build_librenms_api() helper and surface a user-facing error instead (#7)
- _platform_mapping_form: drop CSRF header from the read-only platform-list GET (#10)
- tests: tie migrate move-update assertion to a dedicated queryset (#6); assert
  find_by_librenms_id call args in the OOB-refresh test (#12); add regression tests
  for the OOB-perm gate, OOB-row match skip, and unknown-server_key guard
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.

1 participant