{# SPDX-License-Identifier: Apache-2.0 #} {# Copyright (C) 2025 Marcin Zieba #} {% extends 'generic/object.html' %} {% load helpers %} {% load static %} {% block content %} {# Vendored diff2html (MIT) — renders the apply-preview's NSO tree diff; no CDN. #} {# Vendored Tabulator (MIT) — the category grids; no CDN. #} {# Shared grid harness — badge vocabulary, per-cell Accept, inline-edit-owns, state #} {# pills. Must load before any lazy category fragment runs its column definitions. #} {# Interfaces panel column definitions — static (testable); the fragment only mounts. #} {# Compact LACP bundle/member parameters with coordinated ownership editing. #} {# BFD compact/editable column definitions — loaded before its lazy fragment mounts. #} {# Compact OSPF process/binding columns with safe inline routing knobs. #} {# Compact IS-IS process/interface columns with safe non-secret editors. #} {# Compact BGP peers/templates with safe peer session editing. #} {# Compact static-route identity and native policy editing. #} {# Compact redistribution identity/policy columns with safe policy editing. #} {# Route-policy compact object links and safe route-map rename. #} {% if mgmt %} {# ── Adapter link/sync failure — managing this device couldn't reach the adapter ─────── #} {% if mgmt.adapter_link_error %} {% endif %} {# ── Adapter error banner ──────────────────────────────────────────────── #} {% if adapter_error %} {% endif %} {# ── Adapter intent drift (split-brain) — renders ONLY when there is stale intent ── #} {% if intent_drift %} {% endif %} {# ── Device capability gaps — matrix unsupported/skipped on this NED/sw; renders only if any ── #} {% if device_capability %} {% endif %} {# ── What "Accept" means (read this before clicking) ───────────────────── #} {# ── Management summary ────────────────────────────────────────────────── #}
NSO Management Edit
{% if failover %} {# ── Mgmt-IP failover status (adapter-owned; not persisted on the mgmt row) ── #} {% endif %}
NSO Instance{{ mgmt.nso_instance }}
NSO Device Name{{ mgmt.nso_device_name }}
Managed Scopes{{ mgmt.managed_scopes|join:", "|default:"None" }}
Interface Attributes{{ mgmt.managed_attributes|join:", "|default:"None" }}
Auto Apply {% if mgmt.auto_apply %}Enabled {% else %}Disabled{% endif %}
Last device sync {{ mgmt.last_sync_at|date:"Y-m-d H:i"|default:"Never" }}
Last Sync Status {% if mgmt.last_sync_status == "succeeded" %}{{ mgmt.last_sync_status }} {% elif mgmt.last_sync_status == "failed" %}{{ mgmt.last_sync_status }} {% elif mgmt.last_sync_status == "partial" %}partial {% if mgmt.degraded_surfaces %}
Stale (NSO read failed): {% for surface in mgmt.degraded_surfaces %}{{ surface }}{% endfor %}
{% endif %} {% elif mgmt.last_sync_status %}{{ mgmt.last_sync_status }} {% else %}—{% endif %}
Failover Address {% if failover.active_address == "oob" %} OOB (failed over) {{ failover.oob_ip|default:"" }} {% else %} Primary {{ failover.primary_ip|default:"" }} {% endif %} {% if failover.manual_override %} Manual override {% endif %}
Last Probe {% if failover.last_probe_result == "ok" %}OK {% elif failover.last_probe_result == "timeout" %}Timed out {% elif failover.last_probe_result == "unreachable" %}Unreachable {% elif failover.last_probe_result == "error" %}Probe error {% elif failover.last_probe_result %}Unknown {% else %}—{% endif %} {% if failover.last_probe_target %}{{ failover.last_probe_target|upper }} target{% endif %} {% if failover.last_probe_at %}{{ failover.last_probe_at|date:"Y-m-d H:i" }}{% endif %} {% if failover.last_probe_detail %}
{{ failover.last_probe_detail|truncatechars:160 }}
{% endif %}
OOB Fallback Health {% if failover.oob_health_result == "ok" %}Healthy {% elif failover.oob_health_result == "timeout" %}Timed out {% elif failover.oob_health_result == "unreachable" %}Unreachable {% elif failover.oob_health_result == "error" %}Probe error {% elif failover.oob_health_result %}Unknown {% elif failover.oob_healthy is True %}Healthy {% elif failover.oob_healthy is False %}Unknown {% else %}{% endif %} {% if failover.oob_health_checked_at %}{{ failover.oob_health_checked_at|date:"Y-m-d H:i" }}{% endif %} {% if failover.oob_health_detail %}
{{ failover.oob_health_detail|truncatechars:160 }}
{% endif %}
Last Failover Switch {{ failover.last_switch_at|date:"Y-m-d H:i"|default:"Never" }}
{# ── Action buttons ────────────────────────────────────────────────────── #}
Actions
{% csrf_token %}
{% csrf_token %}
{% csrf_token %}
{% csrf_token %}
{% csrf_token %}
{% csrf_token %}
{# Bulk Accept — rendered via JS by the interfaces fragment once loaded #}
{# ── Blocked removals (collateral guard) — filled by the jobs poll below ── #} {# A removal the adapter refused because the PUT-replace would flush service rows #} {# nobody removed (orphaned intent). Persists until the operator re-accepts the #} {# rows or forces the removal — unlike the job strip, it is not displaced by later #} {# jobs. Capability transparency: while blocked, the retraction is NOT on the device. #} {# ── #104-A: succeeded removals that left residue on the device (JS-populated) ── #}
{# ── Job activity (loaded on tab open; shows any running job + last finished) ── #}
{# ── Job status (populated by JS polling; hidden until an action fires) ── #}
{# ── Apply confirmation modal (self-contained overlay; no Bootstrap-JS dep) ── #}
Apply intent to the device
{# ── NSO configuration: counts-first, rows loaded lazily on expand ──────── #} {% if nso_categories %}

NSO Configuration

{# READSEM S4 (R11/R12): device-wide reset banner — always in the DOM so the #} {# counts poll can toggle it; honored by every render path incl. adapter-down. #}
Refresh pending — the adapter's data store was reset. Showing last-known state until the next reconcile adopts the new dataset.
{% if families_version_mismatch %}
Adapter read-state vocabulary v{{ families_version_mismatch.served }} does not match this plugin (v{{ families_version_mismatch.expected }}) — update the older side; read chips may be incomplete or unknown.
{% endif %}

Counts reflect the last sync. Click a category to load its rows.

{% for cat in nso_categories %}
Loading…
{% endfor %} {# READSEM S4 (D8/R4-4): families the adapter reports that this plugin does not #} {# know — rendered generically from the LIVE response (no fetcher/gate exists). #} {% if read_state_unknown %}
Other families reported by the adapter but unknown to this plugin version
{% for u in read_state_unknown %} {{ u.family }}: {{ u.read_state.outcome|default:"?" }} {% endfor %}
{% endif %} {% elif not adapter_error %}
No managed scopes for this device. Edit scope.
{% endif %} {% else %} {% endif %} {% endblock content %}