Skip to content

Browser Frontend

The netbox-proxbox plugin delivers its UI through standard Django templates bundled with the plugin, augmented by JavaScript for real-time SSE streaming, keepalive polling, WebSocket updates, and dashboard card hydration.


Template Hierarchy

All plugin templates live under netbox_proxbox/templates/netbox_proxbox/. They extend NetBox's base templates to inherit the standard NetBox chrome (navigation, header, breadcrumbs).

templates/netbox_proxbox/
├── base/               Base plugin page template (extends NetBox layout)
├── home/               Plugin dashboard / home page
│   ├── index.html      Main plugin home
│   └── partials/       Card and widget fragments
├── fastapi/            FastAPI endpoint detail and test pages
├── proxmox/            ProxmoxEndpoint detail and cluster/node pages
├── table/              Shared table fragment templates
├── test/               WebSocket and SSE test/debug pages
└── partials/           Reusable fragment templates (status badges, etc.)

Template Extensions

netbox_proxbox/template_content.py injects custom panels and buttons into standard NetBox pages outside the plugin:

Extension class Target model What it adds
ProxboxJobButtons core.Job "Cancel job" / "Run now" buttons on Job detail pages for Proxbox Sync jobs
ProxboxVMPanel virtualization.VirtualMachine ProxBox data panel (snapshots, backups, task history) on VM detail pages

Static Assets

static/netbox_proxbox/
├── css/                Compiled CSS for specific plugin pages
├── js/                 Browser-side JavaScript (12 files)
└── styles/             SCSS sources and compiled theme assets

JavaScript Files

File Purpose
common.js Shared helpers: badge state, tooltip setup, CSRF token retrieval
home.js Dashboard initialization: WebSocket setup, keepalive badge refresh, Proxmox card hydration
sync.js SSE consumer: EventSource management, frame parsing, DOM progress updates
job_log_view.js Renders streamed Proxbox job log payloads on Job detail pages
job_live_panel.js Live job progress panel on the home page (polls job status, updates summary panel)
websocket.js WebSocket integration: onSyncEnd(listener) and notifySyncEnd(obj) hooks
endpoint-status.js Periodic keepalive/status badge refresh for dashboard and list pages
polling.js Generic repeated-polling helpers
logs.js Backend logs page behavior
device.js Client behavior for device/node sync views
virtual_machine.js Client behavior for VM sync views
table.js Table-specific dynamic behavior

Browser Page Lifecycle

flowchart TD
    A["Page Load"] --> B["Django renders template\n(initial HTML)"]
    B --> C["DOMContentLoaded"]
    C --> D["home.js: init WebSocket\n(if WS endpoint configured)"]
    C --> E["endpoint-status.js: start\nkeepalive polling (badge refresh)"]
    C --> F["home.js: hydrate Proxmox\ncards via /proxbox/card-data/"]
    D --> G["websocket.js: connect to\nws://.../plugins/proxbox/ws/"]
    G --> H{Sync triggered?}
    F --> I["Cards show cluster\nmetadata + warnings"]
    H -- "User clicks Sync" --> J["Form POST to sync URL"]
    J --> K["Job enqueued in RQ"]
    K --> L["Browser redirected to Job\ndetail page or SSE stream page"]
    L --> M["sync.js: open EventSource\nfor SSE stream"]
    M --> N["Parse SSE frames:\ndiscovery, step, item_progress"]
    N --> O["Update DOM progress elements\n(stage indicators, counts)"]
    O --> P{complete event?}
    P -- "ok=true" --> Q["Show success state\nnotifySyncEnd()"]
    P -- "ok=false" --> R["Show error state\nShow error_detail message"]
    Q --> S["websocket.js:\nbroadcast onSyncEnd to listeners"]
    R --> S
    S --> T["home.js: refresh cards\nand job live panel"]

Dashboard Hydration

The plugin home page inlines home_inline.js, loads with skeleton card elements, and then hydrates them asynchronously:

  1. One worker pool, with a default concurrency of four, processes FastAPI badges, dependent endpoint badges, and Proxmox cards without an endpoint-count-sized request burst.
  2. The card task calls the card-data route for each configured Proxmox endpoint.
  3. The Django view returns a JSON payload with cluster metadata and any warnings.
  4. The script updates the DOM card with the received data.

The home-page keepalive badges are refreshed by home_inline.js; endpoint list pages use endpoint-status.js. A successful keepalive badge is authoritative and card hydration never downgrades it. Backend HTTP 429/503 responses render as a neutral Throttled state. Card hydration honors the server-provided Retry-After delay, coalesces each card to one pending timer, and permits at most three retries. Timers enqueue work into the same persistent four-worker scheduler as initial badge and card requests; they never call the network path directly. The keepalive JSON views are protected by the normal login requirement and use restricted querysets.

The poller is deliberately bounded, because every keepalive request can land on a freshly spawned WSGI worker thread (Granian in netbox-docker spawns blocking threads on demand and reaps them after 30 s idle) and Django keeps one persistent database connection per thread for CONN_MAX_AGE seconds. An unbounded poller therefore turns into a PostgreSQL connection leak on any long-lived endpoint page:

  • One self-rescheduling loop per page (a setTimeout armed only after the previous round settles), never a fixed setInterval, so a slow backend cannot stack overlapping rounds.
  • Badges are refreshed sequentially, one request at a time, instead of fanning out one request per badge.
  • Polling pauses while the tab is hidden (document.hidden via the Page Visibility API) and resumes with one immediate refresh when it becomes visible again.
  • The keepalive view (views/keepalive_status.py) retires the request thread's Django database connection once the JSON payload is built: it moves the connection's close_at deadline to now so Django's own request_finished handler (close_old_connections) closes it after response middleware has run. It never closes inside an atomic block, so ATOMIC_REQUESTS deployments keep their transaction intact. A reaped worker thread therefore cannot leave an idle connection behind.
  • The NetBox endpoint push and the Proxmox mode detection that piggyback on keepalive are throttled through the shared Django cache with an atomic cache.add claim (5 minutes), keyed per FastAPI endpoint or per Proxmox endpoint so every configured backend still gets its own push, and fall back to a process-local throttle only when no cache is available.
  • FastAPI probe results are cached for 30 seconds by endpoint and a hash of its connection settings. Failures use a three-second TTL. A successful Proxmox endpoint push caches a secret-safe payload fingerprint and backend endpoint ID for five minutes. The cache is only a discovery hint: each use verifies that single backend row still has the endpoint's immutable identity suffix and resolved host/port. A missing or reassigned ID invalidates the hint and immediately falls back to full discovery and registration. Endpoint saves also invalidate the optimization; sync-job preflight retains its explicit batch push behavior.

SSE Rendering

sync.js is the SSE consumer for both the direct streaming views and the background job stream:

static/netbox_proxbox/js/sync.js (pattern)
function connectToSyncStream(streamUrl, onFrame, onComplete) {
    const evtSource = new EventSource(streamUrl);

    evtSource.addEventListener("step", (e) => {
        const data = JSON.parse(e.data);
        updateStageIndicator(data.step, data.status, data.result);
    });

    evtSource.addEventListener("item_progress", (e) => {
        const data = JSON.parse(e.data);
        incrementProgressCounter(data.type, data.status);
    });

    evtSource.addEventListener("complete", (e) => {
        const data = JSON.parse(e.data);
        evtSource.close();
        onComplete(data);
        notifySyncEnd(data);
    });

    evtSource.addEventListener("error", (e) => {
        const data = JSON.parse(e.data);
        showErrorBanner(data.error, data.detail);
    });
}

Job Log Viewer

When an operator opens a NetBox Job detail page for a Proxbox Sync job, job_log_view.js is loaded by the template extension. It renders the stored SSE log payload — which was captured by the on_frame callback during the job's run() execution — into a structured UI with per-stage indicators, object counts, and error messages.


WebSocket Integration

websocket.js provides a thin integration layer on top of the browser WebSocket API:

static/netbox_proxbox/js/websocket.js
// Register a callback that fires when any sync operation completes
onSyncEnd(function(syncData) {
    refreshDashboardCards();
    updateJobLivePanel(syncData);
});

// Called by sync.js when the complete event arrives
notifySyncEnd(completePayload);

The WebSocket URL is injected into the page template from FastAPIEndpoint.websocket_url. If no WebSocket endpoint is configured, websocket.js degrades gracefully and the dashboard falls back to polling-only mode.

The server-side polling bridge returns both a process-local message cursor and, when supported by the backend, a run identifier from its CSRF-protected start request. polling.js carries both values: the cursor prevents replay of older buffer entries, while the run identifier prevents a stale or unrelated terminal from completing the current page's run. Correlated full updates wait for the full-update aggregate terminal and report its failed status through the error callback. Backends without the ws_run_id capability continue to use the legacy bare command and per-stage terminal behavior. Legacy polling excludes all frames that carry a run identifier. If dispatch-time capability validation or an invalid-envelope response downgrades a queued command, a run-matched control record switches that page to legacy matching without attributing other workers' correlated broadcasts to it. If endpoint trust changes before a command or compatibility retry is transmitted, the bridge closes the connection and publishes a failed sync-run record; the poller reports that record through the error callback instead of waiting for its idle timeout.


Forms and Sync Actions

Sync action buttons are implemented as native HTML forms with data-sync-url attributes rather than fetch/AJAX calls. The form submission navigates to the sync URL, which either:

  • Returns a StreamingHttpResponse (SSE stream) for browser-facing streaming views
  • Redirects to the NetBox Job detail page after enqueuing a background job

This means sync actions work correctly without JavaScript — JS only adds the real-time progress overlay.