{% extends 'generic/object.html' %} {% load helpers %} {% load i18n %} {% block title %}{{ object }} - {% trans "Scope Reconciliation" %}{% endblock %} {% block extra_controls %}  {% trans "Sync" %} {% endblock extra_controls %} {% block content %}
{% if report_error %} {% trans "Failed" %} {{ report_error }} {% elif report_pending %} {% trans "Not run" %} {% trans "This report queries Forward for every device and endpoint, so it runs in the background rather than on page load." %} {% else %} {% trans "Report ready" %} {# Naming the producer as well as the time: every completed sync #} {# now refreshes this, so "generated" three days ago means the #} {# post-sync job has been failing, not that nobody pressed a #} {# button. Without the source those two look identical. #} {% trans "Generated" %} {{ report_generated_at }}{% if report_source %} {% blocktrans %}by {{ report_source }}{% endblocktrans %}{% endif %} {% endif %}
{% csrf_token %}
{% trans "Device Scope Reconciliation" %}
{% if payload.forward_in_scope_endpoints %} {% endif %} {% if payload.unmanaged.untagged_total %} {% endif %}
{% trans "Include tags" %} {{ payload.include_tags|join:", "|placeholder }}
{% trans "NetBox devices" %} {{ payload.netbox_device_count }}
{% trans "In scope (collected)" %} {{ payload.forward_in_scope_completed }}
{% trans "In scope (SNMP endpoints)" %} {{ payload.forward_in_scope_endpoints }}
{% trans "Tagged but backfilled" %} {{ payload.forward_tagged_backfilled }} {% if payload.netbox_present_backfilled %}  {% blocktrans with count=payload.netbox_present_backfilled %}view {{ count }} tagged{% endblocktrans %}  {% trans "why?" %} {% endif %}
{% trans "Out of scope (orphans)" %} {% if payload.netbox_out_of_scope %} {# Straight to the card carrying the cause badges and the full list. #} {{ payload.netbox_out_of_scope }} {% else %} 0 {% endif %} {% include 'forward_netbox/inc/count_trend.html' with trend=out_of_scope_trend %}
{% trans "Empty orphan sites" %} {% if payload.netbox_empty_orphan_site_count %} {{ payload.netbox_empty_orphan_site_count }} {% else %} 0 {% endif %}
{% trans "In scope, missing from NetBox" %} {{ payload.forward_missing_in_netbox }}
{% trans "Carry no include tag" %} {{ payload.unmanaged.untagged_total }} {% if payload.unmanaged.owned_untagged %} {% blocktrans with count=payload.unmanaged.owned_untagged %}{{ count }} created by this sync{% endblocktrans %} {# The cause of this number, and the cleanup for it, are in a #} {# card further down the page - which is the half a customer #} {# reading only the summary never reached. #} {% trans "why? / clean up" %} {% include 'forward_netbox/inc/count_trend.html' with trend=uncovered_trend %} {% endif %} {% if payload.unmanaged.unclaimed %} {% blocktrans with count=payload.unmanaged.unclaimed %}{{ count }} not claimed by this sync{% endblocktrans %} {% trans "why?" %} {% endif %}
{% trans "Out-of-Scope Sample" %}
{% if payload.out_of_scope_sample %} {% if payload.out_of_scope_absence.available %} {% comment %} Which kind of absence, because all three look identical here and only one of them means the device really left. {% endcomment %}

{% if payload.out_of_scope_absence.absent_from_snapshot %} {% blocktrans count counter=payload.out_of_scope_absence.absent_from_snapshot %}{{ counter }} gone from Forward{% plural %}{{ counter }} gone from Forward{% endblocktrans %} {% endif %} {% if payload.out_of_scope_absence.present_untagged %} {% blocktrans count counter=payload.out_of_scope_absence.present_untagged %}{{ counter }} in Forward but untagged{% plural %}{{ counter }} in Forward but untagged{% endblocktrans %} {% endif %} {% if payload.out_of_scope_absence.vendor_excluded %} {% blocktrans count counter=payload.out_of_scope_absence.vendor_excluded %}{{ counter }} custom-command source{% plural %}{{ counter }} custom-command source{% endblocktrans %} {% endif %}

{% if payload.out_of_scope_absence.present_untagged %}

{% trans "Devices Forward still has but no longer reports under these tags. A Forward-side tag edit and a narrowed query look identical here — confirm in Forward before pruning." %}

{% endif %} {% if payload.out_of_scope_absence.absent_detail %}

{% blocktrans count counter=payload.out_of_scope_absence.absent_still_tagged %}Of those gone from the snapshot, Forward's device configuration still lists {{ counter }} under an include tag:{% plural %}Of those gone from the snapshot, Forward's device configuration still lists {{ counter }} under an include tag:{% endblocktrans %} {% for row in payload.out_of_scope_absence.absent_detail %} {{ row.count }} — {{ row.label }} {% endfor %} {% trans "Forward's UI shows these as tagged; the sync reads the snapshot, which does not carry them. A disabled device stays here until collection is re-enabled or it is pruned." %}

{% endif %} {% endif %} {% comment %} How long the absence has lasted, which is a different question from what kind of absence it is, and the one that decides whether the prune may act yet. {% endcomment %} {% if payload.out_of_scope_quarantine.held %}

{% blocktrans count counter=payload.out_of_scope_quarantine.held %}{{ counter }} in quarantine{% plural %}{{ counter }} in quarantine{% endblocktrans %} {% if payload.out_of_scope_quarantine.prune_eligible %} {% blocktrans count counter=payload.out_of_scope_quarantine.prune_eligible %}{{ counter }} prune-eligible{% plural %}{{ counter }} prune-eligible{% endblocktrans %} {% endif %}

{% blocktrans with runs=payload.out_of_scope_quarantine.required_runs hours=payload.out_of_scope_quarantine.required_hours %}A device disabled in Forward disappears exactly as a removed one does, so an absence must persist for {{ runs }} consecutive syncs and {{ hours }} hours before the prune will delete it. Devices that reappear leave quarantine automatically.{% endblocktrans %}

{% endif %}
    {% for name in payload.out_of_scope_sample %}
  • {{ name }}
  • {% endfor %}
{% if payload.netbox_out_of_scope > payload.out_of_scope_sample|length %}

{% blocktrans with shown=payload.out_of_scope_sample|length total=payload.netbox_out_of_scope %}Showing {{ shown }} of {{ total }}.{% endblocktrans %}

{% endif %} {% if payload.netbox_out_of_scope %} {# The red button below deletes every one of these; the sample is 25. #}

 {% blocktrans with count=payload.netbox_out_of_scope %}List all {{ count }}{% endblocktrans %}

{% endif %} {% else %}

{% trans "No out-of-scope devices." %}

{% endif %}
{% trans "Post-Upgrade Catalog Reconciliation" %}
{% if upgrade_reconciliation.dlm.available %} {% endif %}
{% trans "Mode" %} {% trans "Read only" %}
{% trans "Platforms with manufacturer" %} {{ upgrade_reconciliation.platforms.with_manufacturer }} / {{ upgrade_reconciliation.platforms.total }}
{% trans "Platforms without manufacturer" %} {{ upgrade_reconciliation.platforms.without_manufacturer }}
{% trans "Stale endpoint DeviceTypes" %} {% if upgrade_reconciliation.stale_endpoint_device_types.candidate_count %} {{ upgrade_reconciliation.stale_endpoint_device_types.candidate_count }} {% else %} 0 {% endif %}
{% trans "DLM status" %} {{ upgrade_reconciliation.dlm.status }}
{% trans "Software versions" %} {{ upgrade_reconciliation.dlm.software_versions.total }}
{% trans "Versions without devices" %} {{ upgrade_reconciliation.dlm.software_versions.without_devices }}
{% trans "Protected versions without devices" %} {{ upgrade_reconciliation.dlm.software_versions.protected_without_devices }}
{% trans "Unreferenced version candidates" %} {% if upgrade_reconciliation.dlm.software_versions.unreferenced_without_devices %} {{ upgrade_reconciliation.dlm.software_versions.unreferenced_without_devices }} {% else %} 0 {% endif %}
{% trans "CVEs" %} {{ upgrade_reconciliation.dlm.cves.total }}
{% trans "CVEs with vulnerabilities" %} {{ upgrade_reconciliation.dlm.cves.with_vulnerabilities }}
{% trans "CVEs with affected software" %} {{ upgrade_reconciliation.dlm.cves.with_affected_software }}
{% trans "Unlinked CVEs" %} {{ upgrade_reconciliation.dlm.cves.unlinked }}
{% trans "Review Candidates" %}
{% if upgrade_reconciliation.stale_endpoint_device_types.sample %}
{% trans "Stale endpoint DeviceTypes" %}
    {% for item in upgrade_reconciliation.stale_endpoint_device_types.sample %}
  • {{ item.manufacturer__name }} / {{ item.model }}
  • {% endfor %}
{% endif %} {% if upgrade_reconciliation.dlm.software_versions.unreferenced_sample %}
{% trans "Unreferenced software versions" %}
    {% for item in upgrade_reconciliation.dlm.software_versions.unreferenced_sample %}
  • {{ item.platform__name }} / {{ item.version }}
  • {% endfor %}
{% endif %} {% if upgrade_reconciliation.dlm.software_versions.protected_sample %}
{% trans "Protected software versions without devices" %}
    {% for item in upgrade_reconciliation.dlm.software_versions.protected_sample %}
  • {{ item.platform__name }} / {{ item.version }} ({{ item.protected_by }})
  • {% endfor %}
{% endif %} {% if not upgrade_reconciliation.stale_endpoint_device_types.sample and not upgrade_reconciliation.dlm.software_versions.unreferenced_sample and not upgrade_reconciliation.dlm.software_versions.protected_sample %}

{% trans "No post-upgrade review candidates." %}

{% endif %}
{% trans "Backfill Reason Breakdown" %}
{% if payload.backfilled_reason_breakdown %}

{% trans "Why these in-scope devices were not freshly collected by Forward. This is a Forward collection-health signal, not a sync error — fix collection in Forward (rotate credentials for AUTHENTICATION_FAILED, check reachability for CONNECTION_TIMEOUT, finish onboarding for INCOMPLETE_SETUP)." %}

{% for reason, count in payload.backfilled_reason_breakdown.items %} {% endfor %}
{{ reason }} {{ count }}
{% else %}

{% trans "No backfilled devices in the latest snapshot." %}

{% endif %}
{% trans "Tagged-but-Backfilled Sample" %}
{% if payload.present_backfilled_detail_sample %}

{% trans "In NetBox but not freshly collected in the latest snapshot (serving last-known-good data). Days = how stale the served data is." %}

{% for item in payload.present_backfilled_detail_sample %} {% endfor %}
{% trans "Device" %}{% trans "Reason" %}{% trans "Stale" %}
{{ item.name }} {{ item.reason }} {% if item.stale_days is not None %}{% blocktrans count days=item.stale_days %}{{ days }} day{% plural %}{{ days }} days{% endblocktrans %}{% else %}—{% endif %}
{% if payload.netbox_present_backfilled > payload.present_backfilled_detail_sample|length %}

{% blocktrans with shown=payload.present_backfilled_detail_sample|length total=payload.netbox_present_backfilled %}Showing {{ shown }} of {{ total }}.{% endblocktrans %}

{% endif %} {% if payload.netbox_present_backfilled %}

 {% blocktrans with count=payload.netbox_present_backfilled %}List all {{ count }}{% endblocktrans %}

{% endif %} {% else %}

{% trans "No backfilled devices in NetBox." %}

{% endif %}
{% if catalogue_cleanup.entry_count %}
{% trans "Routing Policy Outside Scope" %}

{% blocktrans with count=catalogue_cleanup.entry_count %}{{ count }} prefix-list, community-list or route-map entries in NetBox belong to definitions that no device inside this sync's scope holds, as of the latest dependency preview. The sync never removes them on its own.{% endblocktrans %}

    {% for model, count in catalogue_cleanup.by_model %}{% if count %}
  • {{ model }}: {{ count }}
  • {% endif %}{% endfor %}

{% trans "Removing them re-reads Forward first, and deletes only entries this sync created, that no sync still asserts, and that no route map outside the cleanup still matches on. Anything else is held and counted in the job result." %}

{% if perms.netbox_routing.delete_prefixlist %}
{% csrf_token %}
{% endif %}
{% endif %} {% if site_relabel_pairs.available and site_relabel_pairs.pair_count or site_relabel_pairs.available and site_relabel_pairs.held_count %}
{% trans "Site-Relabel Duplicates" %}

{% trans "When Forward relabels a device's site, older versions of this plugin created a second device instead of moving the first. The older copy usually keeps the device's primary IP and history. Merging keeps the older device, moves it to the site the sync's device map reports now, and deletes the copy. This repairs existing pairs only - a current sync no longer creates them." %}

{% if site_relabel_pairs.pair_count and perms.dcim.delete_device %}
{% csrf_token %}
{% endif %} {% if site_relabel_pairs.last_repair %}

{% blocktrans with job=site_relabel_pairs.last_repair.job_pk merged=site_relabel_pairs.last_repair.merged_count failed=site_relabel_pairs.last_repair.failed_count %}Last merge (job #{{ job }}): {{ merged }} merged, {{ failed }} refused.{% endblocktrans %}

{% if site_relabel_pairs.last_repair.routing_rows_released %}

{% trans "Routing rows released so the newer copy could be deleted (the next sync rebuilds them on the surviving device):" %} {% for model, count in site_relabel_pairs.last_repair.routing_rows_released %}{{ model }} × {{ count }}{% if not forloop.last %}, {% endif %}{% endfor %}

{% endif %} {% if site_relabel_pairs.last_repair.failed_count %}

{% trans "Refused:" %} {% for reason, count in site_relabel_pairs.last_repair.failed_by_reason %}{{ reason }} × {{ count }}{% if not forloop.last %}, {% endif %}{% endfor %} {% if site_relabel_pairs.last_repair.blocking_models %} — {% trans "blocked by" %} {% for model, count in site_relabel_pairs.last_repair.blocking_models %}{{ model }} × {{ count }}{% if not forloop.last %}, {% endif %}{% endfor %} {% endif %}

{% endif %} {% endif %} {% if site_relabel_pairs.held_count %}

{% blocktrans count counter=site_relabel_pairs.held_count %}{{ counter }} duplicated name is held - it cannot be proven safe to merge yet:{% plural %}{{ counter }} duplicated names are held - they cannot be proven safe to merge yet:{% endblocktrans %}

    {% for entry in site_relabel_pairs.held_by_reason %}
  • {{ entry.count }} - {{ entry.remedy|default:entry.reason }} {% if entry.names %}
    {{ entry.names|join:", " }}{% if entry.more_names %}, +{{ entry.more_names }} {% trans "more" %}{% endif %}
    {% endif %}
  • {% endfor %}
{% endif %}
{% endif %} {% if payload.unmanaged.untagged_total %}
{% trans "Uncovered, Created by This Sync" %}
{% if payload.unmanaged.owned_untagged_sample %}

{% trans "This sync created these devices, and the current Forward result no longer covers them. A device disabled in Forward looks exactly like this, so they are kept, not pruned — which is why this count grows. Use the tag to list all of them, not just this sample." %}

{% if payload.unmanaged.owned_absence.available %}

{% trans "Why:" %} {% if payload.unmanaged.owned_absence.absent_from_snapshot %} {% blocktrans count counter=payload.unmanaged.owned_absence.absent_from_snapshot %}{{ counter }} gone from Forward{% plural %}{{ counter }} gone from Forward{% endblocktrans %} {% endif %} {% if payload.unmanaged.owned_absence.present_untagged %} {% blocktrans count counter=payload.unmanaged.owned_absence.present_untagged %}{{ counter }} in Forward but untagged{% plural %}{{ counter }} in Forward but untagged{% endblocktrans %} {% endif %} {% if payload.unmanaged.owned_absence.vendor_excluded %} {% blocktrans count counter=payload.unmanaged.owned_absence.vendor_excluded %}{{ counter }} custom-command source{% plural %}{{ counter }} custom-command source{% endblocktrans %} {% endif %}

{% if payload.unmanaged.owned_absence.endpoint_detail %}

{% trans "Of those Forward still has, these are SNMP endpoints the endpoint scope excludes:" %} {% for row in payload.unmanaged.owned_absence.endpoint_detail %} {{ row.count }} — {{ row.label }} {% endfor %} {% trans "A console server in that list means its endpoint tags match none of the include tags; a generic endpoint or CIMC controller is excluded by design." %}

{% endif %} {% if payload.unmanaged.owned_absence.absent_detail %} {% comment %} "Tagged in Forward but not in NetBox" is what a customer reads off Forward's Device Tags page, which shows the configuration; the sync reads the snapshot. Name the configuration fact so the gap is not filed as a sync bug. {% endcomment %}

{% blocktrans count counter=payload.unmanaged.owned_absence.absent_still_tagged %}Of those gone from Forward, its device configuration still lists {{ counter }} under an include tag:{% plural %}Of those gone from Forward, its device configuration still lists {{ counter }} under an include tag:{% endblocktrans %} {% for row in payload.unmanaged.owned_absence.absent_detail %} {{ row.count }} — {{ row.label }} {% endfor %} {% trans "Forward's UI shows these as tagged; the snapshot the sync reads does not carry them, so they cannot be tagged or updated in NetBox. Re-enable collection in Forward to cover a disabled device again." %}

{% endif %} {% elif payload.unmanaged.owned_absence %}

{% trans "The Forward census that classifies the cause did not run for this report." %}

{% endif %}

 {% blocktrans with count=payload.unmanaged.owned_untagged %}List all {{ count }}{% endblocktrans %}  {% trans "Filter devices by tag" %}

{# The tag list above is not this sync's count: it is one tag shared #} {# by every sync, so a device another sync uncovered - or one this #} {# sync has since covered again - still carries it and still shows #} {# up there. A customer bulk-deleted from exactly that list reading #} {# "546 uncovered" against a sync whose own card said 107. #}

{% trans "The tag list is shared by every sync and kept from earlier runs; a device another sync uncovered, or one this sync has since covered again, still carries it. List all above is this sync's current count." %}

{% comment %} Only the devices Forward no longer reports at all are eligible, so the button counts `absent_from_snapshot` rather than the card's own total. Offering the larger number would promise a deletion the gates refuse, which is how an operator learns to distrust the panel. {% endcomment %} {% if perms.dcim.delete_device %}
{% csrf_token %} {% if payload.unmanaged.owned_quarantine.held %}
{% endif %} {# Counts what the button would actually delete, not what is of #} {# the right kind. It used to read "Delete 105" while the #} {# quarantine held all 105, so pressing it deleted nothing. #} {% if payload.unmanaged.owned_quarantine.held %} {% blocktrans count counter=payload.unmanaged.owned_quarantine.held %}{{ counter }} more held by the quarantine{% plural %}{{ counter }} more held by the quarantine{% endblocktrans %} {% endif %}
{% endif %} {# The orphan card has carried this sentence since the quarantine #} {# shipped; the uncovered card never did, so an operator watching a #} {# count that would not move had nothing on the page explaining why. #} {% if payload.unmanaged.owned_quarantine.required_runs is not None %}

{% blocktrans with runs=payload.unmanaged.owned_quarantine.required_runs hours=payload.unmanaged.owned_quarantine.required_hours %}A device disabled in Forward disappears exactly as a removed one does, so an absence must persist for {{ runs }} consecutive syncs and {{ hours }} hours before the prune will delete it. Devices that reappear leave quarantine automatically.{% endblocktrans %}

{% endif %}
    {% for name in payload.unmanaged.owned_untagged_sample %}
  • {{ name }}
  • {% endfor %}
{% if payload.unmanaged.owned_untagged > payload.unmanaged.owned_untagged_sample|length %}

{% blocktrans with shown=payload.unmanaged.owned_untagged_sample|length total=payload.unmanaged.owned_untagged %}Showing {{ shown }} of {{ total }}.{% endblocktrans %}

{% endif %} {% else %}

{% trans "Every device this sync created is covered by the current Forward result." %}

{% endif %}
{% trans "Uncovered, Not Claimed by This Sync" %}
{% if payload.unmanaged.unclaimed_sample %}

{% trans "Devices this sync never created: another source, an operator, or an old SNMP endpoint import. Listed so the count above can be accounted for — this sync makes no claim about them, tags none of them, and never prunes them." %}

 {% blocktrans with count=payload.unmanaged.unclaimed %}List all {{ count }}{% endblocktrans %}

    {% for name in payload.unmanaged.unclaimed_sample %}
  • {{ name }}
  • {% endfor %}
{% if payload.unmanaged.unclaimed > payload.unmanaged.unclaimed_sample|length %}

{% blocktrans with shown=payload.unmanaged.unclaimed_sample|length total=payload.unmanaged.unclaimed %}Showing {{ shown }} of {{ total }}.{% endblocktrans %}

{% endif %} {% else %}

{% trans "Every uncovered device was created by this sync." %}

{% endif %}
{% endif %} {% endblock content %}