# netbox-proxbox: LLM Context File

> This file provides comprehensive context for LLMs working with the netbox-proxbox codebase.

## Project Overview

netbox-proxbox is a NetBox plugin that synchronizes Proxmox infrastructure data into NetBox. It keeps DCIM data up-to-date with real Proxmox clusters, nodes, virtual machines, containers (LXC), backups, and snapshots.

### What It Does

SSH secret APIs require the same fresh sensitive-data grant in addition to
API-token authentication, HTTPS, object visibility, and provider permissions.
New ObjectChange snapshots redact settings keys, credential material,
ciphertext, and provider references through a shared registry. Historical
snapshots are unchanged. Proxmox and NetBox endpoint target edits invalidate
`approved_connection_target_fingerprint`; credential payloads require exact
approval before resolution and again before transmission. Review the target
with GET and approve its fingerprint with PUT on the endpoint's
`connection-authority` API action. Approval requires sensitive-data access and
independent view/change access. Migration `0104_security_hardening`
leaves existing approvals blank. FastAPI target changes additionally require
sensitive authority before its existing authentication/adoption flow; disabled
backend target edits remain drafts.


- **Explicit sensitive-data authorization** - endpoint secret exports and runtime
  encryption-key disclosure require an active authenticated superuser or the
  default-off `ProxboxSensitiveDataAccess.can_access_sensitive_data` flag.
  Only active superusers manage grants. Existing object and credential-provider
  permissions remain independent. Grants are checked on every request, safe
  exports omit all usable tokens, and export-specific token creation is removed.
  See `docs/features/endpoint-import-export.md` for the role and API contracts.
- **Clusters and Nodes** — Proxmox cluster and node information
- **Configurable node Device names** — a global template and optional
  per-endpoint override render NetBox Device names from `{node}`, `{cluster}`,
  `{cluster_slug}`, and `{endpoint}`. Proxmox paths and typed sync identity keep
  the short node name. Managed legacy names can be renamed safely, while
  operator-assigned names are preserved. Saving an endpoint override validates
  it against that endpoint's synchronized nodes and clusters; saving the global
  template validates every synchronized node whose endpoint inherits it.
  Invalid DNS output and names longer than 64 characters are rejected with the
  offending inventory context, while empty inventories retain representative-
  sample validation.
- **Virtual Machines** — VM status, resources, and configuration. The NetBox
  cluster Virtual Machines tab exposes selected-row deletion through the core
  VM bulk-delete endpoint; the cluster page's top-level Delete action remains
  parent-cluster deletion and correctly refuses while VMs depend on it.
- **Containers (LXC)** — Container details and settings
- **VM Snapshots** — Point-in-time snapshots for recovery
- **VM Backups** — Backup jobs and restore points
- **Storage** — Datastores and storage content
- **Networking** — VLANs, bridges, and IP assignments
- **Standalone browser console** — permission-gated QEMU noVNC/terminal and LXC terminal sessions through proxbox-api without browser-visible Proxmox credentials
- **Data protection and operations** — backup/snapshot/replication calendars, typed sync-state recovery, soft-deleted VM review, and audited intent/apply records
  Exact replication and backup-routine wall-clock schedules are converted from
  each endpoint's discovered IANA timezone into NetBox's active timezone before
  calendar day bucketing. Unresolved timing remains visibly approximate, and
  combined calendar node selection is capped at 50.
- **Extended inventory** — guest OS interfaces, SDN, firewall, Firecracker, Proxmox metrics, service-monitoring samples, and PBS/PDM endpoint records
- **Endpoint-isolated synchronization** — required SSE stages, firewall, and datacenter passes continue with later Proxmox endpoints after one endpoint fails, then fail the job with persisted per-endpoint evidence. Backend-key and stage retries share bounded delta-seconds and HTTP-date `Retry-After` parsing. Staged work uses one monotonic RQ-timeout deadline with a 120-second persistence reserve, records the current and remaining endpoint failures on exhaustion, and returns through finalization before the hard timeout. Successful UI enqueue redirects directly to the NetBox job detail page.
- **OpenBao endpoint, node, and cloud-init credentials** — exact-policy password, API-token, and SSH material writes with provider-owned transactionality, UUID references, purpose-aware assignments, and shared-credential-safe unlinking. Selected node SSH password/keypair material becomes the primary `login` credential on the linked `dcim.Device`; FastAPI/PBS/PDM tokens use primary `api` assignments and Firecracker agent tokens use a primary `agent` assignment. VM cloud-init password/keypair material is assigned to the parent VM for `login`, with `ssh_pwauth` selecting the primary credential; public SSH keys never enter provider payloads. Required OpenBao reads never downgrade to Fernet, `credential_reference_id`, or an unrelated credential-provider fallback. UI/API mutations begin the provider boundary outside NetBox atomic blocks and carry the request actor; raw/bulk/cascade/downgrade bypasses refuse remaining state. Secret-free readiness and generic assignment selectors support automation while live material remains confined to authenticated consumers.
- **OpenBao structural setup** — `proxbox_openbao_setup --check` and the settings card share a typed, secret-free composed readiness snapshot. Interactive material writes validate only the provider, default engine, and exact policy; RPC and the automation user remain composed diagnostics. Writable setup creates only an explicitly configured default engine and policy, never users or material, and rolls back every write when final readiness fails. Opt-in assignment backfill adds only missing relations, is concurrency-safe and idempotent, and refuses incomplete node ownership, unresolved references, credential-type mismatches, disabled or primary-state-drifted exact rows, and conflicting shared-owner primaries without rewriting operator-owned state or disclosing UUIDs. This structural check does not certify the broader RPC/backend protected-write rollout.

### Requirements

- NetBox 4.5.8 through 4.7.0, including official v4.7.0 GA
- Python 3.12+
- cryptography 50.0.0 or newer
- Proxmox VE 7.x, 8.x, or 9.x
- proxbox-api backend (FastAPI service)

### Proxmox OCI Testing Appliance

`emersonfelipesp/netbox-proxbox:oci` is a testing-only, all-in-one OCI image for
Proxmox VE's OCI-to-LXC workflow. It includes NetBox, the latest stable
`netbox-proxbox` and `proxbox-api` PyPI releases, PostgreSQL, Redis, and an RQ
worker. NetBox listens on port `8080`; proxbox-api listens on loopback port
`8800` unless external binding is explicitly enabled. The plugin is enabled
with its backend URL set to `http://127.0.0.1:8800`.

The image supplies `/sbin/init`, stores persistent state under
`/var/lib/postgresql`, `/var/lib/redis`, `/var/lib/proxbox-api`, and
`/var/lib/proxbox-stack`, and supports direct administrator creation with
`/opt/netbox/netbox/manage.py createsuperuser`. See
`docs/installation/proxmox-oci-appliance.md` for the pull, storage, bootstrap,
and verification procedure. This appliance is not a production deployment
topology.

### Version

- Plugin version: `0.0.29`
- NetBox compatibility: `4.5.8` through `4.7.0`
- Current source pairing: `netbox-proxbox 0.0.29`, `proxbox-api 0.0.23.post3`,
  `proxmox-sdk 0.0.15`, and `netbox-sdk 0.0.13`
- Last documented released runtime pairing: `netbox-proxbox 0.0.27rc4`,
  `proxbox-api 0.0.22.post1`, `proxmox-sdk 0.0.13`, and `netbox-sdk 0.0.13`

---

## Architecture Summary

### High-Level Architecture

```
┌─────────────────┐     ┌─────────────────┐     ┌─────────────────┐
│   Proxmox VE    │────▶│   proxbox-api   │◀────│  netbox-proxbox │
│   (Cluster)     │     │   (FastAPI)     │     │   (NetBox Plugin)│
└─────────────────┘     └─────────────────┘     └─────────────────┘
                               │                       │
                               ▼                       ▼
                        ┌─────────────────────────────────┐
                        │           NetBox                │
                        │   (DCIM/IPAM Database)          │
                        └─────────────────────────────────┘
```

### Data Flow

1. **Endpoint Configuration**: Users create ProxmoxEndpoint, NetBoxEndpoint, and FastAPIEndpoint objects in NetBox UI
2. **Sync Trigger**: User clicks "Full Update" or schedules a sync job
3. **Job Enqueue**: `ProxboxSyncJob` is enqueued to NetBox's RQ worker (default queue)
4. **Backend Call**: Job calls proxbox-api SSE endpoints via `run_sync_stream()`
5. **Data Collection**: proxbox-api fetches data from Proxmox and NetBox
6. **Object Creation**: proxbox-api creates/updates NetBox objects via REST API
7. **Progress Streaming**: SSE events stream back to browser for real-time progress

### Endpoint Enablement

Endpoint rows are both inventory/configuration records and possible connection
targets. Treat `enabled=False` as a hard no-connection gate for every
endpoint-like object that exposes the shared field, including
`ProxmoxEndpoint`, `NetBoxEndpoint`, `FastAPIEndpoint`, `PBSEndpoint`,
`PDMEndpoint`, and companion endpoint rows such as PBS/PDM plugin endpoints.

Disabled endpoint rows must remain visible through the UI and REST API, but
operational code must return before the first proxbox-api, NetBox, Proxmox,
PBS, PDM, or companion-plugin HTTP call. This includes status cards,
keepalive probes, startup/signal registration, backend key registration,
OpenAPI fetches, backend-id resolution, sync scopes, and scheduled/manual jobs.

Use `netbox_proxbox/services/endpoint_enabled.py` for this guard. New live-read
or write paths for PBS/PDM must call `disabled_endpoint_detail()` before
building request contexts or invoking `requests`, SSE, WebSocket, or SDK
clients.

### Home Dashboard Status Loading

The home page uses one four-worker browser request pool for FastAPI badges,
dependent endpoint badges, and Proxmox card hydration. A card failure never
downgrades a badge whose keepalive succeeded. Backend HTTP 429/503 responses
render as a neutral throttled state, and card retries honor `Retry-After` when
the response provides it. Initial requests and at most three coalesced retries
per card use the same persistent scheduler; retry timers never bypass the pool.

FastAPI probes are cached for 30 seconds by endpoint and a hash of its
connection settings; failures use a three-second TTL. Successful Proxmox
endpoint pushes cache a secret-safe canonical payload fingerprint and backend
endpoint ID for five minutes. Endpoint saves invalidate that cache, while the
sync-job preflight retains its explicit batch push behavior. The cached ID is a
hint, not proof of remote existence: every use verifies the single backend row's
immutable identity suffix and resolved host/port. A 404 or identity mismatch
invalidates the hint and immediately retries full discovery and registration.

### Key Components

| Component | Location | Purpose |
|-----------|----------|---------|
| Models | `netbox_proxbox/models/` | Persistent data models |
| Views | `netbox_proxbox/views/` | UI pages and sync actions |
| API | `netbox_proxbox/api/` | REST API endpoints |
| Services | `netbox_proxbox/services/` | Backend proxy, SSE streaming |
| Jobs | `netbox_proxbox/jobs.py` | RQ background jobs |
| Forms | `netbox_proxbox/forms/` | Django forms for models |
| Tables | `netbox_proxbox/tables/` | NetBox table definitions |
| Templates | `netbox_proxbox/templates/` | HTML templates |
| Static | `netbox_proxbox/static/` | JS, CSS assets |

### Current Source Authority

This generated context summary follows the executable source. The authoritative
inventories are `netbox_proxbox/models/`, `netbox_proxbox/urls.py`,
`netbox_proxbox/api/urls.py`, `netbox_proxbox/jobs.py`, `proxbox_cli/`, and the
workflow files. The current schema tip is
`0102_vm_cloudinit_openbao_references`, which adds opaque OpenBao password and
keypair references for VM cloud-init intent after the FastAPI, PBS, PDM, and
Firecracker token references from `0101`. It preserves explicit legacy
credential references and public SSH-key fields without moving material.

---

## Semantic MCP Bridge (bridge v1)

The Proxbox plugin contains a read-only semantic producer descriptor for a
future compatible `netbox-sdk` MCP bridge. It does not embed FastMCP, listen on
a separate MCP port, import the SDK at runtime, or store an MCP-specific
credential. No released SDK identity is currently activated:
`tests/fixtures/netbox_sdk_bridge_activation.json` remains blocked until one
exact version or full commit, exact module origin, and explicitly provisioned
paired CI gate all pass. While blocked, the API root omits `mcp` and the direct
manifest route returns 503. Descriptor source presence alone is not compatibility.

### Ownership and trust boundary

- Proxbox API root: advertises schema version `"1"` and the manifest URL only
  after the checked consumer activation becomes valid.
- Proxbox manifest: describes fixed plugin-local tools and strict JSON Schemas.
- Existing DRF target views: authenticate, require `core.add_job`, validate,
  apply NetBox visibility, and enqueue jobs.
- Activated compatible `netbox-sdk`: owns discovery, manifest validation, plugin-root path
  confinement, the generic `plugin_list_tools` and `plugin_call_tool` MCP
  surface, the existing NetBox credential, input/output validation, and
  disabled-by-default server-wide mutation gating.
- MCP host/agent: preserves operator intent and treats annotations as safety
  metadata, never as authorization.

The descriptor can describe a destructive tool but cannot execute it. The
server-side DRF permission check remains authoritative.
Schema version `"1"` identifies this generic descriptor protocol and supported
schema machinery; it does not freeze every plugin tool payload. The Proxbox
manifest fixture is a producer-owned snapshot, not a fixture authority copied
between repositories. Its manual paired gate trusts no ambient SDK installation:
the caller must supply an explicit SDK root, exact version or full commit, and
exact module origin.

### Discovery

1. Require the checked activation artifact and matching immutable paired-CI
   evidence to name the exact installed SDK. If it remains blocked, stop.
2. Read `GET /api/plugins/proxbox/` with the configured NetBox principal.
3. Require `mcp.schema_version == "1"`.
4. Fetch the URL in `mcp.manifest` (normally
   `/api/plugins/proxbox/mcp/`).
5. Call `plugin_list_tools` with `{"plugin":"proxbox"}`. Let the gated SDK
   validate the descriptors. Never construct another Proxbox MCP server or pass
   a token as a tool argument.

Invoke a descriptor only through `plugin_call_tool` with `plugin`, `tool`,
`arguments`, and optional `dry_run`. `list_sync_jobs` and `schedule_sync` are
descriptor names, not standalone MCP tools.

The root discovery member is:

```json
{
  "mcp": {
    "schema_version": "1",
    "manifest": "/api/plugins/proxbox/mcp/"
  }
}
```

### Tools

| Tool | Existing DRF target | Effect | Behavior |
|---|---|---|---|
| `list_sync_jobs` | `GET sync/schedule/` | read, idempotent, closed-world | Lists visible active, failed, and recurring Proxbox sync jobs. Strict input is `{}`. |
| `schedule_sync` | `POST sync/schedule/` | destructive, non-idempotent, open-world | Queues an immediate, future, or recurring `ProxboxSyncJob`. |

Both tools require `core.add_job`, including the read tool, because they reuse
the existing protected schedule view. The manifest itself follows NetBox's
`LOGIN_REQUIRED` setting and may be anonymously readable only when that setting
allows it.

### `schedule_sync` safety contract

- Treat scheduling as destructive and non-idempotent. Reconciliation may
  remove stale **NetBox inventory** records, and submitting twice can create two
  jobs. The bridge does not delete Proxmox guests or infrastructure.
- Keep the SDK mutation gate disabled unless the operator or host policy accepts
  its complete blast radius. `NETBOX_MCP_ALLOW_MUTATIONS=1` and
  `--allow-mutations` are global MCP-server opt-ins: either enables every
  mutation tool, not only Proxbox `schedule_sync`.
- `sync_stages` is required, nonempty, and unique. Bridge v1 accepts exactly the
  13 concrete stage slugs and does not advertise the legacy `"all"` sentinel.
  A full sync sends the complete canonical stage list, which is normalized to
  the internal `["all"]` identity used by recurring hints and repair debounce.
- Stage selection controls only those 13 SSE stages. Endpoint/configuration
  preflight and scoped cluster/node, firewall, and datacenter CPU reconciliation
  always run first. VM-template reconciliation also runs unless disabled by its
  sync mode. Those passes can mutate NetBox inventory regardless of the subset.
- `job_name` is optional and limited to 200 characters.
- `schedule_at` must be a strict RFC 3339 future time with an explicit timezone
  whose normalized instant is representable; overflowing year-boundary leap
  and timezone-offset normalization are rejected.
  Omit or use `null` for an immediate one-shot call.
- `recurrence` is an object with exactly one positive `minutes`, `hours`,
  `days`, or `weeks` member. Per-unit maxima guarantee that the persisted
  converted interval is at most `2147483647` minutes. If recurrence is present
  and `schedule_at` is omitted, the first run starts at the server's current time.
  Exact maxima are `2147483647` minutes, `35791394` hours, `1491308` days, and
  `213044` weeks; the next integer for any unit is rejected before enqueue.
- Proxmox endpoint IDs are unique signed-64-bit positive NetBox primary keys
  (`1..9223372036854775807`). Integer JSON literals retain the full range.
  Mathematically integral float/Decimal forms such as `7.0` normalize only
  through `9007199254740991`; larger float forms are rejected before ORM lookup
  because parsing may already have rounded their identity. Booleans, strings,
  fractions, non-finite numbers, and signed-64 overflow also reject. The same
  rule protects the unadvertised legacy direct-API endpoint fields.
- Every explicitly requested `proxmox_endpoint_ids` row must exist and be
  enabled. One unknown or disabled ID rejects the entire request; the server
  never filters the list down to a broader or different scope.
- Bridge v1 does not expose `netbox_endpoint_ids`; that legacy REST field has no
  end-to-end bridge scope semantics and is rejected as an additional property.
- Explicit Proxmox endpoint scopes must be nonempty. Omission alone requests the
  scheduler's all-endpoint behavior. Never send `[]` as a placeholder.
- Unknown properties are rejected (`additionalProperties: false`).
- HTTP 400 and 403 failures enqueue nothing. Never retry them with omitted
  scope or weaker validation.
- A timeout, 5xx, invalid response, or transport failure after dispatch is
  ambiguous. `list_sync_jobs` lacks the submitted scope and stable request
  identity, so it cannot prove absence. Never auto-retry; report the ambiguity
  and require operator reconciliation before a deliberate retry.

Minimal immediate request:

```json
{
  "plugin": "proxbox",
  "tool": "schedule_sync",
  "arguments": {
    "sync_stages": [
      "virtual-machines", "storage", "vm-disks", "vm-backups",
      "vm-snapshots", "devices", "network-interfaces", "vm-interfaces",
      "ip-addresses", "sdn", "backup-routines", "replications", "task-history"
    ],
    "job_name": "operator-requested-full-sync"
  },
  "dry_run": false
}
```

Scoped recurring request:

```json
{
  "plugin": "proxbox",
  "tool": "schedule_sync",
  "arguments": {
    "sync_stages": ["devices", "network-interfaces"],
    "job_name": "six-hour-inventory",
    "recurrence": {"hours": 6},
    "proxmox_endpoint_ids": [7]
  },
  "dry_run": false
}
```

Accepted calls return HTTP 201 with `ok`, `job_id`, and `message`. Use
`job_id` as the stable identifier; messages are human-readable.

### Error handling

- 400: do not resubmit automatically or widen endpoint scope. The generic MCP
  error may expose only the HTTP status; require operator inspection instead of
  guessing which field failed.
- 401: repair the normal NetBox SDK authentication; credentials never belong
  in tool input.
- 403: the principal lacks `core.add_job`; do not bypass DRF.
- 404: plugin/route is missing or incompatible; refresh discovery.
- 5xx or timeout after dispatch: treat the write outcome as ambiguous and never
  auto-retry a schedule call.

### Compatibility and maintenance

`tests/fixtures/proxbox_bridge_v1.json` is the Proxbox-owned exact wire snapshot,
not a shared SDK fixture. Keep it aligned with
`netbox_proxbox/api/mcp_bridge.py`, the DRF serializers/views, the named JSON
examples in `docs/api/semantic-mcp-bridge.md`, and the pure plus real-Django MCP
test suites. Keep `tests/fixtures/netbox_sdk_bridge_activation.json` blocked
until an exact compatible released SDK is immutably provisioned in CI and
passes safe-integer plus leap/offset-overflow vectors. Do not silently change a
v1 tool name, path, effect, required field, or meaning. See
`docs/api/semantic-mcp-bridge.md` for the complete
architecture, executable examples, error matrix, verification traceability,
versioning policy, and troubleshooting runbook.

---

## Core Data Models

### ProxmoxEndpoint

Stores Proxmox API connection settings.

```python
# Location: netbox_proxbox/models/proxmox_endpoint.py
class ProxmoxEndpoint(EndpointBase):
    name = models.CharField(max_length=100, unique=True)
    domain = models.CharField(max_length=255, blank=True)
    ip_address = models.ForeignKey(ContentType, on_delete=models.PROTECT)  # ipam.IPAddress
    port = models.PositiveIntegerField(default=8006)
    mode = models.CharField(choices=ProxmoxModeChoices)  # standalone, cluster
    version = models.CharField(blank=True)
    username = models.CharField(max_length=100)  # typically root@pam
    password = models.CharField(max_length=255, blank=True)
    token_name = models.CharField(max_length=100, blank=True)
    token_value = models.CharField(max_length=255, write_only=True)
    verify_ssl = models.BooleanField(default=True)
```

### NetBoxEndpoint

Stores the remote NetBox API target.

```python
# Location: netbox_proxbox/models/netbox_endpoint.py
class NetBoxEndpoint(EndpointBase):
    name = models.CharField(max_length=100, unique=True)
    domain = models.CharField(max_length=255, blank=True)
    ip_address = models.ForeignKey(ContentType, on_delete=models.PROTECT)
    port = models.PositiveIntegerField(default=443)
    token_version = models.CharField(choices=NetBoxTokenVersionChoices)  # v1, v2
    token = models.ForeignKey(users.Token, on_delete=models.PROTECT)
    token_key = models.CharField(max_length=255, blank=True)
    token_secret = models.CharField(max_length=255, blank=True, write_only=True)
    verify_ssl = models.BooleanField(default=True)

    @property
    def effective_token_version(self) -> str: ...
    @property
    def effective_token_value(self) -> str: ...
```

### FastAPIEndpoint

Stores the ProxBox backend HTTP/WebSocket target.

```python
# Location: netbox_proxbox/models/fastapi_endpoint.py
class FastAPIEndpoint(EndpointBase):
    name = models.CharField(max_length=100, unique=True)
    domain = models.CharField(max_length=255, blank=True)
    ip_address = models.ForeignKey(ContentType, on_delete=models.PROTECT)
    port = models.PositiveIntegerField(default=8800)
    verify_ssl = models.BooleanField(default=True)
    token = models.CharField(max_length=255, blank=True)
    websocket_domain = models.CharField(max_length=255, blank=True)
    websocket_port = models.PositiveIntegerField(default=8800)
    use_websocket = models.BooleanField(default=True)
```

### ProxmoxStorage

Stores Proxmox storage inventory synchronized from backend.

```python
# Location: netbox_proxbox/models/storage.py
class ProxmoxStorage(NetBoxModel):
    name = models.CharField(max_length=255)
    storage_id = models.CharField(max_length=255)
    node = models.CharField(max_length=255)
    storage_type = models.CharField(max_length=50)
    status = models.CharField(max_length=50, blank=True)
    content_types = models.CharField(max_length=255, blank=True)
    proxmox_endpoint = models.ForeignKey(ProxmoxEndpoint, on_delete=models.PROTECT)
```

### VMBackup

Stores backup inventory for NetBox virtual machines.

```python
# Location: netbox_proxbox/models/vm_backup.py
class VMBackup(NetBoxModel):
    name = models.CharField(max_length=255)
    backup_id = models.CharField(max_length=255)
    vm = models.ForeignKey(virtualization.VirtualMachine, on_delete=models.CASCADE)
    storage = models.CharField(max_length=255)
    size = models.BigIntegerField()
    ctime = models.DateTimeField()
    status = models.CharField(choices=ProxmoxBackupStatusChoices)
    subtype = models.CharField(choices=ProxmoxBackupSubtypeChoices)  # qemu, lxc
    format = models.CharField(choices=ProxmoxBackupFormatChoices)
    vm_id_on_proxmox = models.PositiveIntegerField()
    proxmox_endpoint = models.ForeignKey(ProxmoxEndpoint, on_delete=models.CASCADE)
```

### VMSnapshot

Stores snapshot inventory for NetBox virtual machines.

```python
# Location: netbox_proxbox/models/vm_snapshot.py
class VMSnapshot(NetBoxModel):
    name = models.CharField(max_length=255)
    description = models.TextField(blank=True)
    vm = models.ForeignKey(virtualization.VirtualMachine, on_delete=models.CASCADE)
    snapshot_id = models.CharField(max_length=255)
    snaptime = models.FloatField()
    parent_snapshot = models.CharField(max_length=255, blank=True)
    status = models.CharField(choices=ProxmoxSnapshotStatusChoices)  # active, stale
    subtype = models.CharField(choices=ProxmoxSnapshotSubtypeChoices)  # qemu, lxc
    vm_id_on_proxmox = models.PositiveIntegerField()
    proxmox_endpoint = models.ForeignKey(ProxmoxEndpoint, on_delete=models.CASCADE)
```

### VMTaskHistory

Stores VM task history records linked to NetBox virtual machines.

```python
# Location: netbox_proxbox/models/vm_task_history.py
class VMTaskHistory(NetBoxModel):
    vm = models.ForeignKey(virtualization.VirtualMachine, on_delete=models.CASCADE)
    task_type = models.CharField(max_length=100)
    start_time = models.DateTimeField(auto_now_add=True)
    end_time = models.DateTimeField(null=True, blank=True)
    status = models.CharField(max_length=50)
    result = models.TextField(blank=True)
    proxmox_endpoint = models.ForeignKey(ProxmoxEndpoint, on_delete=models.CASCADE)
```

### ProxboxPluginSettings

Singleton plugin settings for runtime behavior.

```python
# Location: netbox_proxbox/models/plugin_settings.py
class ProxboxPluginSettings(NetBoxModel):
    use_guest_agent_interface_name = models.BooleanField(default=True)
    vm_interface_sync_strategy = models.CharField(default="guest_os_model", ...)
    proxbox_fetch_max_concurrency = models.PositiveSmallIntegerField(default=8)
    sync_mode_vm_interface = models.CharField(...)  # added in migration 0051
    sync_mode_mac = models.CharField(...)            # added in migration 0051
    interface_batch_size = models.PositiveSmallIntegerField(default=5)
    interface_batch_delay_ms = models.PositiveIntegerField(default=100)
```

### PBSEndpoint / PDMEndpoint / PDMRemote

Companion endpoint models for Proxmox Backup Server and Proxmox Datacenter Manager inventory.
`PBSEndpoint` and `PDMEndpoint` inherit `EndpointBase.enabled`; do not add a
second enabled field or bypass the shared guard when adding PBS/PDM status,
sync, or write surfaces.

```python
# Location: netbox_proxbox/models/pbs_endpoint.py
class PBSEndpoint(EndpointBase): ...

# Location: netbox_proxbox/models/pdm_endpoint.py
class PDMEndpoint(EndpointBase): ...

# Location: netbox_proxbox/models/pdm_remote.py
class PDMRemote(NetBoxModel): ...
```

### NodeSSHCredential

Stores SSH credentials for ProxmoxNode hardware-discovery SSH sessions.

```python
# Location: netbox_proxbox/models/ssh_credential.py
class NodeSSHCredential(NetBoxModel): ...
```

### ProxmoxDatacenterCpuModel

Custom datacenter-level CPU model definitions synced from Proxmox.

```python
# Location: netbox_proxbox/models/datacenter_cpu_model.py
class ProxmoxDatacenterCpuModel(NetBoxModel): ...
```

### ProxmoxVMTemplate / ProxmoxVMCloudInit / CloudImageTemplate

VM template inventory and cloud-init configuration records.

```python
# Location: netbox_proxbox/models/vm_template.py
class ProxmoxVMTemplate(NetBoxModel): ...

# Location: netbox_proxbox/models/vm_cloudinit.py
class ProxmoxVMCloudInit(NetBoxModel): ...

# Location: netbox_proxbox/models/cloud_image_template.py
class CloudImageTemplate(NetBoxModel): ...
```

### ProxmoxApplyJob / DeletionRequest / ProxmoxStorageVirtualDisk

Operational and linkage models.

```python
# Location: netbox_proxbox/models/apply_job.py
class ProxmoxApplyJob(NetBoxModel): ...

# Location: netbox_proxbox/models/deletion_request.py
class DeletionRequest(NetBoxModel): ...

# Location: netbox_proxbox/models/storage.py
class ProxmoxStorageVirtualDisk(NetBoxModel): ...  # join table: ProxmoxStorage ↔ VirtualDisk
```

### Sync State, Intent, Metrics, and Service Monitoring

Current source also persists typed `Proxbox*SyncState` sidecars,
`ProxmoxVMIntent`, `ProxboxBranchIntent`, `ProxmoxMetricsInfluxDB`,
`ProxmoxServiceCollection`, `ProxmoxServiceSample`, and
`ProxmoxServiceStatus`. These support identity-safe reconciliation, audited
intent planning, metrics configuration, and asynchronous systemd collection.

### Firewall Models (6)

Per-datacenter and per-VM Proxmox firewall inventory, read-only synced from proxbox-api.

```python
# Location: netbox_proxbox/models/firewall_security_group.py
class ProxmoxFirewallSecurityGroup(NetBoxModel): ...

# Location: netbox_proxbox/models/firewall_rule.py
class ProxmoxFirewallRule(NetBoxModel): ...

# Location: netbox_proxbox/models/firewall_ipset.py
class ProxmoxFirewallIPSet(NetBoxModel): ...
class ProxmoxFirewallIPSetEntry(NetBoxModel): ...

# Location: netbox_proxbox/models/firewall_alias.py
class ProxmoxFirewallAlias(NetBoxModel): ...

# Location: netbox_proxbox/models/firewall_options.py
class ProxmoxFirewallOptions(NetBoxModel): ...
```

### SDN Models (8)

Proxmox Software-Defined Networking inventory (PVE 9.2+).

```python
# Location: netbox_proxbox/models/sdn_fabric.py
class ProxmoxSdnFabric(NetBoxModel): ...

# Location: netbox_proxbox/models/sdn_route_map.py
class ProxmoxSdnRouteMap(NetBoxModel): ...

# Location: netbox_proxbox/models/sdn_prefix_list.py
class ProxmoxSdnPrefixList(NetBoxModel): ...

# Location: netbox_proxbox/models/sdn_inventory.py
class ProxmoxSdnController(NetBoxModel): ...
class ProxmoxSdnZone(NetBoxModel): ...
class ProxmoxSdnVNet(NetBoxModel): ...
class ProxmoxSdnSubnet(NetBoxModel): ...
class ProxmoxSdnBinding(NetBoxModel): ...
```

### Firecracker Models (4)

Firecracker micro-VM inventory. Firecracker instances are not NetBox core `VirtualMachine` rows; they use `kind="firecracker"` and `instance_ref="firecracker:<id>"`.

```python
# Location: netbox_proxbox/models/firecracker.py
class FirecrackerHostPool(NetBoxModel): ...
class FirecrackerHost(NetBoxModel): ...
class FirecrackerImageTemplate(NetBoxModel): ...
class FirecrackerMicroVM(NetBoxModel): ...
```

---

## Sync Operation Flow

### Sync Types and Paths

The plugin supports multiple sync types, each mapping to a proxbox-api endpoint:

| SyncType | Backend Path | Description |
|----------|--------------|-------------|
| `devices` | `dcim/devices/create/stream` | Sync Proxmox nodes as NetBox devices |
| `storage` | `virtualization/virtual-machines/storage/create/stream` | Sync storage content |
| `virtual-machines` | `virtualization/virtual-machines/create/stream` | Sync VMs and LXC containers |
| `vm-disks` | `virtualization/virtual-machines/virtual-disks/create/stream` | Sync virtual disks |
| `vm-backups` | `virtualization/virtual-machines/backups/all/create/stream` | Sync backup records |
| `vm-snapshots` | `virtualization/virtual-machines/snapshots/all/create/stream` | Sync snapshot records |
| `network-interfaces` | `dcim/devices/interfaces/create/stream` | Sync node interfaces |
| `vm-interfaces` | `virtualization/virtual-machines/interfaces/create/stream` | Sync VM interfaces |
| `ip-addresses` | `virtualization/virtual-machines/interfaces/ip-address/create/stream` | Sync interface addresses |
| `sdn` | `proxmox/sdn/create/stream` | Sync SDN inventory |
| `replications` | `proxmox/replication/stream` | Sync replication jobs |
| `backup-routines` | `proxmox/cluster/backup/stream` | Sync backup routines |
| `task-history` | `virtualization/virtual-machines/task-history/create/stream` | Sync task history |
| `all` | (runs all 13 stages in dependency order) | Full sync |

### Stage Execution Order

When running `all` or multiple sync types, stages execute in dependency order:

```python
_SYNC_STAGE_ORDER = (
    "devices",           # First: create nodes
    "storage",           # Second: create storage
    "virtual-machines",  # Third: create VMs (depends on nodes)
    "task-history",
    "vm-disks",
    "vm-backups",
    "vm-snapshots",
    "network-interfaces",
    "vm-interfaces",
    "ip-addresses",
    "sdn",
    "replications",
    "backup-routines",
)
```

This order describes the selectable backend SSE stages, not the complete job.
Every non-targeted scheduled job first runs endpoint/configuration preflight and
scoped cluster/node, firewall, and datacenter CPU reconciliation. VM-template
reconciliation also runs unless `sync_mode_vm_template=disabled`. These
invariant passes can create, update, or remove stale NetBox inventory regardless
of the selected SSE stage subset. The semantic MCP bridge advertises all 13
concrete stages and translates only the exact complete unique list to the
internal `["all"]` identity used by recurring hints and repair debounce.

### Background Job Flow

```python
# Location: netbox_proxbox/jobs.py

class ProxboxSyncJob(JobRunner):
    """Trigger a ProxBox sync operation against the FastAPI backend."""
    
    Meta.name = "Proxbox Sync"
    
    # Job timeout: UI-backed, 7200 seconds (2 hours) by default
    # Uses NetBox's default RQ queue
```

`ProxmoxServiceMonitoringJob` is separately registered as a one-minute NetBox
system job. It performs per-endpoint due checks and projects completed
`netbox-rpc` executions into service collection, sample, status, and endpoint
heartbeat records.

**Job Lifecycle:**

1. User clicks "Full Update" (UI) or job is scheduled
2. `ProxboxSyncJob.enqueue()` is called with sync types and endpoint IDs
3. Job is stored in NetBox's RQ queue (`default` queue)
4. RQ worker picks up the job and calls `run()`
5. `run()` iterates through sync stages
6. Each stage calls `run_sync_stream()` to connect to proxbox-api
7. SSE frames are parsed and logged to job `log_entries`
8. On completion, job data is saved with results

### SSE Stream Contract

The plugin consumes Server-Sent Events from proxbox-api:

```
event: step
data: {"step": "devices", "status": "syncing", "message": "Creating device pve01"}

event: step
data: {"step": "devices", "status": "completed", "message": "Created 3 devices"}

event: complete
data: {"ok": true, "message": "Sync completed successfully"}
```

**Event Types:**
- `step`: Progress updates during sync
- `complete`: Final completion event (required)
- `error`: Error event with failure details

---

## Backend Integration

### FastAPI Request Context

```python
# Location: netbox_proxbox/services/backend_proxy.py

@dataclass
class BackendRequestContext:
    http_url: str | None
    ip_address_url: str | None
    verify_ssl: bool
    headers: dict[str, str]
```

### HTTP Communication

```python
# Key functions in backend_proxy.py:

def get_fastapi_request_context() -> BackendRequestContext | None:
    """Build auth headers and URLs for the configured FastAPI endpoint."""

def run_sync_stream(
    stream_path: str,
    query_params: dict[str, str] | None = None,
    on_frame: Callable[[str, dict], None] | None = None,
) -> tuple[dict[str, Any], int]:
    """Consume SSE stream from proxbox-api backend."""
```

### Query Parameters Passed to Backend

| Parameter | Description |
|-----------|-------------|
| `use_guest_agent_interface_name` | Use QEMU guest agent for VM interface naming |
| `fetch_max_concurrency` | Max concurrent operations in backend (default: 8) |
| `proxmox_endpoint_ids` | Comma-separated ProxmoxEndpoint IDs |
| `netbox_endpoint_ids` | Comma-separated NetBoxEndpoint IDs |
| `delete_nonexistent_backup` | Delete backup records not found in Proxmox |

For estates with many Proxmox clusters, see [Large multi-cluster deployments](./docs/configuration/large-multi-cluster-deployments.md): most sync stages call proxbox-api one endpoint at a time while backup-routine, replication, firewall, and datacenter operations can fan out across every endpoint in scope, raise `PROXBOX_RATE_LIMIT` on proxbox-api for large estates, keep a single active FastAPI backend API key, and treat `custom_fields_request_delay` as compatibility-only (no runtime effect).

### WebSocket Integration

The plugin includes a WebSocket client for real-time updates:

```python
# Location: netbox_proxbox/websocket_client.py

class WebSocketClient:
    """Long-lived WebSocket client for proxbox-api messages."""
    
    async def connect(self, url: str) -> None: ...
    async def listen(self) -> None: ...
    async def close(self) -> None: ...
```

---

## API Reference

### Plugin API Root

```
/api/plugins/proxbox/
```

### Endpoint CRUD

| Method | Endpoint | Description |
|--------|----------|-------------|
| GET | `/api/plugins/proxbox/` | API root |
| GET/POST | `/api/plugins/proxbox/endpoints/proxmox/` | List/create Proxmox endpoints |
| GET/PUT/PATCH/DELETE | `/api/plugins/proxbox/endpoints/proxmox/{id}/` | CRUD single endpoint subject to safety gates |
| GET/POST | `/api/plugins/proxbox/endpoints/netbox/` | List/create NetBox endpoints |
| GET/PUT/PATCH/DELETE | `/api/plugins/proxbox/endpoints/netbox/{id}/` | CRUD single endpoint |
| GET/POST | `/api/plugins/proxbox/endpoints/fastapi/` | List/create FastAPI endpoints |
| GET/PUT/PATCH/DELETE | `/api/plugins/proxbox/endpoints/fastapi/{id}/` | CRUD single endpoint |

### Storage and VM Objects

| Method | Endpoint | Description |
|--------|----------|-------------|
| GET | `/api/plugins/proxbox/storage/` | List ProxmoxStorage |
| GET | `/api/plugins/proxbox/backups/` | List VMBackup |
| GET | `/api/plugins/proxbox/snapshots/` | List VMSnapshot |
| GET | `/api/plugins/proxbox/task-history/` | List VMTaskHistory |

The router also exposes typed cluster/node, sync-state, guest-interface,
Firecracker, firewall, SDN, service-monitoring, settings, job, and semantic MCP
surfaces. `DeletionRequest` and `ProxmoxApplyJob` remain read-only over REST.

### Serializers

All endpoints use nested serializers from NetBox core:
- `NestedVirtualMachineSerializer` for VM references
- `NestedIPAddressSerializer` for IP references
- `NestedTokenSerializer` for user token references

---

## URL Routes

### Plugin URL Prefix

All plugin routes are under `/plugins/proxbox/`.

### UI Routes

| Pattern | View | Description |
|---------|------|-------------|
| `/` | redirect | Redirect to `/home/` |
| `/home/` | `HomeView` | Plugin home dashboard |
| `/endpoints/proxmox/` | `ProxmoxEndpointListView` | List Proxmox endpoints |
| `/endpoints/proxmox/add/` | `ProxmoxEndpointEditView` | Create Proxmox endpoint |
| `/endpoints/proxmox/{id}/` | `ProxmoxEndpointView` | View Proxmox endpoint |
| `/endpoints/proxmox/{id}/edit/` | `ProxmoxEndpointEditView` | Edit Proxmox endpoint |
| `/endpoints/proxmox/{id}/delete/` | `ProxmoxEndpointDeleteView` | Delete Proxmox endpoint |
| `/endpoints/netbox/` | `NetBoxEndpointListView` | List NetBox endpoints |
| `/endpoints/netbox/add/` | `NetBoxEndpointEditView` | Create NetBox endpoint |
| `/endpoints/netbox/{id}/` | `NetBoxEndpointView` | View NetBox endpoint |
| `/endpoints/netbox/{id}/edit/` | `NetBoxEndpointEditView` | Edit NetBox endpoint |
| `/endpoints/netbox/{id}/delete/` | `NetBoxEndpointDeleteView` | Delete NetBox endpoint |
| `/endpoints/fastapi/` | `FastAPIEndpointListView` | List FastAPI endpoints |
| `/endpoints/fastapi/add/` | `FastAPIEndpointEditView` | Create FastAPI endpoint |
| `/endpoints/fastapi/{id}/` | `FastAPIEndpointView` | View FastAPI endpoint |
| `/endpoints/fastapi/{id}/edit/` | `FastAPIEndpointEditView` | Edit FastAPI endpoint |
| `/endpoints/fastapi/{id}/delete/` | `FastAPIEndpointDeleteView` | Delete FastAPI endpoint |
| `/data-protection/` | `DataProtectionView` | Combined calendar and event list |
| `/virtual-machines/{id}/console/session/` | `ProxboxVMConsoleSessionView` | Create an authorized one-use browser-console session |
| `/storage/` | `ProxmoxStorageListView` | List storage |
| `/vm-backups/` | `VMBackupListView` | List VM backups |
| `/vm-snapshots/` | `VMSnapshotListView` | List VM snapshots |
| `/vm-task-history/` | `VMTaskHistoryListView` | List task history |

### Sync Routes

| Pattern | View | Description |
|---------|------|-------------|
| `/sync/devices/` | `sync_devices` | Sync Proxmox nodes |
| `/sync/storage/` | `sync_storage` | Sync storage |
| `/sync/virtual-machines/` | `sync_virtual_machines` | Sync VMs |
| `/sync/vm-backups/` | `sync_vm_backups` | Sync backups |
| `/sync/vm-snapshots/` | `sync_vm_snapshots` | Sync snapshots |
| `/sync/full-update/` | `sync_full_update` | Full sync |

### Keepalive Routes

| Pattern | View | Description |
|---------|------|-------------|
| `/keepalive/fastapi/` | `keepalive_fastapi` | Check FastAPI backend |
| `/keepalive/proxmox/` | `keepalive_proxmox` | Check Proxmox |
| `/keepalive/netbox/` | `keepalive_netbox` | Check remote NetBox |

### Job Integration Routes

| Pattern | View | Description |
|---------|------|-------------|
| `/job/{id}/run/` | `job_run` | Rerun completed job |
| `/job/{id}/cancel/` | `job_cancel` | Cancel pending/running job |

---

## Key Files by Task

### Adding a New Model

1. Create model in `netbox_proxbox/models/<model_name>.py`
2. Add to `netbox_proxbox/models/__init__.py`
3. Create form in `netbox_proxbox/forms/<model_name>.py`
4. Create table in `netbox_proxbox/tables/<model_name>.py`
5. Add filterset in `netbox_proxbox/filtersets.py`
6. Create view in `netbox_proxbox/views/<model_name>.py`
7. Add to `netbox_proxbox/views/__init__.py`
8. Add URL pattern in `netbox_proxbox/urls.py`
9. Add navigation in `netbox_proxbox/navigation.py`
10. Create API serializer in `netbox_proxbox/api/serializers/<model_name>.py`
11. Add to API viewset in `netbox_proxbox/api/views.py`
12. Add API URL in `netbox_proxbox/api/urls.py`
13. Create migration: `python manage.py makemigrations netbox_proxbox`

### Adding a New Sync Type

1. Add choice to `SyncTypeChoices` in `netbox_proxbox/choices.py`
2. Map sync type to path in `_SYNC_TYPE_PATH` in `netbox_proxbox/jobs.py`
3. Add to `_SYNC_STAGE_ORDER` if needed for dependency order
4. Create sync view in `netbox_proxbox/views/sync.py`
5. Add URL pattern in `netbox_proxbox/urls.py`
6. Add button in home template `netbox_proxbox/templates/netbox_proxbox/home/`

### Changing Backend Communication

1. Update `netbox_proxbox/services/backend_proxy.py`
2. Update SSE parsing in `netbox_proxbox/services/backend_proxy.py` (`_iter_sse_frames`, `_consume_sse_until_complete`)
3. Update job SSE handling in `netbox_proxbox/jobs.py` (`on_frame` callback)
4. Update browser SSE handling in `netbox_proxbox/static/netbox_proxbox/js/sync.js`

### Adding a New View/Page

1. Create view in appropriate `netbox_proxbox/views/` module
2. Add to `netbox_proxbox/views/__init__.py` exports
3. Add URL pattern in `netbox_proxbox/urls.py`
4. Create templates in `netbox_proxbox/templates/netbox_proxbox/`
5. Add navigation button in `netbox_proxbox/navigation.py` if needed

---

## Testing Guide

### Running Tests

```bash
cd /path/to/netbox-proxbox
source .venv/bin/activate  # if using virtual environment

# Run all tests
pytest tests/

# Run specific test file
pytest tests/test_sync.py

# Run with coverage
pytest tests/ --cov=netbox_proxbox --cov-report=html
```

### Test Structure

```
tests/
├── conftest.py                      # Django/NetBox mock fixtures
├── test_sync.py                     # Sync view tests
├── test_run_sync_stream.py          # SSE stream tests
├── test_jobs.py                     # RQ job tests
├── test_frontend_contracts.py       # UI/JS contract tests
├── test_cards.py                    # Dashboard card tests
├── test_keepalive_status.py         # Keepalive tests
├── test_utils.py                    # Utility function tests
├── test_api_source_contracts.py     # API contract tests
├── test_form_and_helper_source_contracts.py  # Form tests
├── test_sse_contracts.py            # SSE parsing tests
├── test_backend_integration.py      # Backend integration tests
└── netbox_test_configuration.py     # NetBox settings stub
```

### Test Patterns

Tests use heavy mocking via `conftest.py` to avoid requiring a live NetBox:

```python
# Example test pattern
def test_sync_enqueues_job(monkeypatch):
    load_plugin_module("netbox_proxbox.views.sync", monkeypatch=monkeypatch)
    # Mock Django/NetBox imports
    # Test behavior
```

### Key Test Fixtures

- `fastapi_endpoint`: Mock FastAPIEndpoint object
- `netbox_endpoint`: Mock NetBoxEndpoint object
- `proxmox_endpoint`: Mock ProxmoxEndpoint object
- `load_plugin_module()`: Helper to load plugin modules with mocked dependencies

---

## Development Workflow

### Pre-commit Checklist

**Before committing ANY change:**

1. Run syntax check:
   ```bash
   python -m compileall netbox_proxbox tests
   ```

2. Run linter:
   ```bash
   ruff check .
   ```

3. Run tests:
   ```bash
   pytest tests/
   ```

### Linting and Formatting

The project uses Ruff for linting:

```toml
[tool.ruff]
target-version = "py312"
line-length = 88

[tool.ruff.lint]
select = ["E", "F", "W"]
ignore = ["F403", "F401", "E501", "W293"]
```

### Dependency Policy

**Prefer this order:**

1. **NetBox plugin idioms** — Patterns from NetBox's plugin framework
2. **NetBox core** — Built-in `utilities.*`, `netbox.*` modules
3. **Django** — Standard Django APIs

**Do NOT introduce new third-party dependencies** for capabilities NetBox/Django already provide.

Existing dependencies in `pyproject.toml`:
- `requests` (HTTP client)
- `websockets` (WebSocket client)
- Optional CLI dependencies (`aiohttp`, `click`, `rich`, `typer`)

---

## Common Patterns

### NetBox Model Pattern

```python
from netbox.models import NetBoxModel
from utilities.choices import ChoiceSet

class MyModel(NetBoxModel):
    name = models.CharField(max_length=100)
    status = models.CharField(choices=MyStatusChoices)
    
    class Meta:
        ordering = ("name",)
    
    def __str__(self):
        return self.name
    
    def get_status_color(self):
        return MyStatusChoices.colors.get(self.status)
```

### NetBox View Pattern

```python
from netbox.views import generic
from netbox_proxbox.models import MyModel
from netbox_proxbox.forms import MyModelForm
from netbox_proxbox.tables import MyModelTable

class MyModelView(generic.ObjectView):
    queryset = MyModel.objects.all()

class MyModelListView(generic.ObjectListView):
    queryset = MyModel.objects.all()
    table = MyModelTable
    filterset = MyModelFilterSet

class MyModelEditView(generic.ObjectEditView):
    queryset = MyModel.objects.all()
    form = MyModelForm

class MyModelDeleteView(generic.ObjectDeleteView):
    queryset = MyModel.objects.all()
```

### NetBox API Pattern

```python
from netbox.api.viewsets import NetBoxModelViewSet
from netbox_proxbox.models import MyModel
from netbox_proxbox.api.serializers import MyModelSerializer

class MyModelViewSet(NetBoxModelViewSet):
    queryset = MyModel.objects.all()
    serializer_class = MyModelSerializer
```

### Permission Pattern

```python
from utilities.views import ContentTypePermissionRequiredMixin

class MyCustomView(ContentTypePermissionRequiredMixin, View):
    def get_required_permission(self):
        return "netbox_proxbox.view_mymodel"
```

### SSE Stream Pattern

```python
from netbox_proxbox.services.backend_proxy import run_sync_stream

def on_frame(event: str, data: dict) -> None:
    if event == "error":
        logger.error(f"Stream error: {data}")
    elif event == "step":
        logger.info(f"[{data['step']}] {data['message']}")

payload, status = run_sync_stream(
    "dcim/devices/create/stream",
    query_params={"use_guest_agent_interface_name": "true"},
    on_frame=on_frame,
)
```

---

## Security & Permissions

### Permission Model

The plugin uses NetBox's permission system:

| Permission | Model | Description |
|------------|-------|-------------|
| `view_proxmoxendpoint` | ProxmoxEndpoint | View Proxmox endpoints |
| `add_proxmoxendpoint` | ProxmoxEndpoint | Create Proxmox endpoints |
| `change_proxmoxendpoint` | ProxmoxEndpoint | Edit Proxmox endpoints |
| `delete_proxmoxendpoint` | ProxmoxEndpoint | Delete Proxmox endpoints |
| (same pattern for NetBoxEndpoint, FastAPIEndpoint) | | |
| `view_proxmoxstorage` | ProxmoxStorage | View storage |
| `view_vmbackup` | VMBackup | View VM backups |
| `view_vmsnapshot` | VMSnapshot | View VM snapshots |
| `view_vmtaskhistory` | VMTaskHistory | View task history |

### Custom Permissions

Located in `netbox_proxbox/views/proxbox_access.py`:

```python
def permission_enqueue_proxbox_sync():
    return "netbox_proxbox.enqueue_proxbox_sync"

def permission_cancel_job():
    return "core.delete_job"  # Requires delete permission on core Job model
```

### View Mixins

Use these mixins for custom views:

- `ConditionalLoginRequiredMixin`: Respects `LOGIN_REQUIRED` setting
- `TokenConditionalLoginRequiredMixin`: Allows REST token auth on browser endpoints
- `ContentTypePermissionRequiredMixin`: Model-based permissions

### Object Visibility

Always use `QuerySet.restrict()` for object-level permissions:

```python
# Correct
devices = Device.objects.restrict(request.user, "view")

# Incorrect - bypasses object permissions
devices = Device.objects.all()
```

---

## Directory Structure

```
netbox-proxbox/
├── netbox_proxbox/
│   ├── __init__.py              # Plugin configuration
│   ├── urls.py                  # URL routing
│   ├── navigation.py            # Menu configuration
│   ├── choices.py               # ChoiceSet definitions
│   ├── fields.py                # Custom model fields
│   ├── filtersets.py            # List view filters
│   ├── utils.py                 # URL/host helpers
│   ├── github.py                # GitHub content fetcher
│   ├── websocket_client.py      # WebSocket client
│   ├── jobs.py                  # RQ background jobs
│   ├── schedule_hints.py        # Schedule hints
│   ├── type_defs.py             # Type definitions
│   ├── template_content.py      # Template content
│   │
│   ├── models/                  # Data models
│   │   ├── __init__.py
│   │   ├── base.py              # EndpointBase, CommonProperties
│   │   ├── proxmox_endpoint.py
│   │   ├── netbox_endpoint.py
│   │   ├── fastapi_endpoint.py
│   │   ├── plugin_settings.py
│   │   ├── storage.py
│   │   ├── vm_backup.py
│   │   ├── vm_snapshot.py
│   │   └── vm_task_history.py
│   │
│   ├── forms/                   # Django forms
│   │   ├── __init__.py
│   │   ├── proxmox.py
│   │   ├── netbox.py
│   │   ├── fastapi.py
│   │   ├── storage.py
│   │   ├── vm_backup.py
│   │   ├── vm_snapshot.py
│   │   ├── vm_task_history.py
│   │   ├── schedule_sync.py
│   │   ├── settings.py
│   │   └── widgets.py
│   │
│   ├── tables/                  # NetBox tables
│   │   ├── __init__.py
│   │   ├── storage.py
│   │   ├── vm_backup.py
│   │   ├── vm_snapshot.py
│   │   └── vm_task_history.py
│   │
│   ├── views/                   # UI views
│   │   ├── __init__.py
│   │   ├── sync.py
│   │   ├── schedule_sync.py
│   │   ├── keepalive_status.py
│   │   ├── cards.py
│   │   ├── storage.py
│   │   ├── vm_backup.py
│   │   ├── vm_snapshot.py
│   │   ├── vm_task_history.py
│   │   ├── vm_config.py
│   │   ├── vm_sync_now.py
│   │   ├── job_run.py
│   │   ├── job_cancel.py
│   │   ├── backend_sync.py
│   │   ├── home_context.py
│   │   ├── external_pages.py
│   │   ├── error_utils.py
│   │   ├── proxbox_access.py
│   │   └── endpoints/
│   │       ├── __init__.py
│   │       ├── proxmox.py
│   │       ├── netbox.py
│   │       └── fastapi.py
│   │
│   ├── api/                     # REST API
│   │   ├── __init__.py
│   │   ├── urls.py
│   │   ├── views.py
│   │   ├── filters.py
│   │   └── serializers/
│   │       ├── __init__.py
│   │       ├── endpoints.py
│   │       ├── storage.py
│   │       ├── vm_backup.py
│   │       ├── vm_snapshot.py
│   │       └── vm_task_history.py
│   │
│   ├── services/                # Business logic
│   │   ├── __init__.py
│   │   ├── backend_proxy.py     # HTTP/SSE to proxbox-api
│   │   └── service_status.py    # Keepalive checks
│   │
│   ├── templates/               # HTML templates
│   │   └── netbox_proxbox/
│   │       ├── base/
│   │       ├── fastapi/
│   │       ├── home/
│   │       ├── partials/
│   │       ├── proxmox/
│   │       ├── storage/
│   │       ├── table/
│   │       ├── test/
│   │       ├── vm_backup/
│   │       ├── vm_snapshot/
│   │       └── vm_task_history/
│   │
│   ├── static/                  # Static assets
│   │   └── netbox_proxbox/
│   │       ├── js/
│   │       │   ├── sync.js
│   │       │   └── proxbox.js
│   │       └── styles/
│   │           └── proxbox.css
│   │
│   └── migrations/              # Database migrations
│       ├── 0001_initial.py
│       ├── ...
│       └── 0010_squashed_plugin_settings_and_storage.py
│
├── tests/                       # Test suite
│   ├── conftest.py
│   ├── test_sync.py
│   ├── test_run_sync_stream.py
│   ├── test_jobs.py
│   ├── test_frontend_contracts.py
│   ├── test_cards.py
│   ├── test_keepalive_status.py
│   ├── test_utils.py
│   ├── test_api_source_contracts.py
│   ├── test_form_and_helper_source_contracts.py
│   ├── test_sse_contracts.py
│   └── test_backend_integration.py
│
├── proxbox_cli/                 # Optional CLI tool
│
├── pyproject.toml               # Project configuration
├── README.md                   # Quick start guide
├── CLAUDE.md                    # Claude Code guide
├── AGENTS.md                    # Agent entry points
├── DEVELOP.md                   # Development guide
├── CONTRIBUTING.md              # Contribution guide
└── llms.txt                     # This file
```

---

## RQ Worker Configuration

The plugin uses NetBox's default RQ queue for background jobs.

### Starting Workers

```bash
# Standard NetBox RQ worker (picks up jobs from 'default' queue)
cd /opt/netbox/netbox
source /opt/netbox/venv/bin/activate
python manage.py rqworker
```

### Job Timeout

Default job timeout: **7200 seconds (2 hours)**. Configure 3600–604800 seconds
under **Proxbox > Settings > Synchronization job timeout (seconds)**. Changes
apply only to newly enqueued jobs.

Override per-job:
```python
job = ProxboxSyncJob.enqueue(sync_types=["all"], job_timeout=10800)
```

### Troubleshooting

| Symptom | Cause | Solution |
|---------|-------|----------|
| Job stuck in "pending" | No RQ worker running | Start `rqworker` |
| Job stuck in "running" | Backend slow or buffered stream | Wait or check proxbox-api logs |
| Job shows "JobTimeoutException" | RQ timeout exceeded | Increase **Synchronization job timeout (seconds)** in Proxbox Settings and enqueue a new job |
| Job shows "cancelled by user" | User canceled | Normal behavior |

---

## NetBox Integration Notes

### Plugin Registration

```python
# netbox_proxbox/__init__.py

class ProxboxConfig(PluginConfig):
    name = "netbox_proxbox"
    verbose_name = "Proxbox"
    description = "NetBox plugin for Proxmox integration"
    version = "0.0.10"
    author = "Emerson Felipe (@emersonfelipesp)"
    author_email = "emersonfelipe.2003@gmail.com"
    min_version = "4.5.0"
    max_version = "4.5.99"
    required_settings = []
    default_settings = {}
    queues = ["netbox_proxbox.sync"]  # Legacy queue (not used for enqueue)
```

### Navigation Menu

```python
# netbox_proxbox/navigation.py

menu_items = (
    PluginMenuButton(
        link="plugins:netbox_proxbox:home",
        permissions=["netbox_proxbox.view_proxmoxendpoint"],
    ),
)

menu_tabs = (
    PluginMenuItem(
        link="plugins:netbox_proxbox:home",
        permissions=["netbox_proxbox.view_proxmoxendpoint"],
    ),
)
```

### Template Inheritance

Templates extend NetBox's base templates:

```html
<!-- templates/netbox_proxbox/home.html -->
{% extends "netbox_proxbox/base/proxbox.html" %}

{% block content %}
  <!-- Plugin-specific content -->
{% endblock %}
```

---

## Backend (proxbox-api) Contract

The plugin expects proxbox-api to implement these endpoints:

### Sync Endpoints (SSE)

| Endpoint | Method | Description |
|----------|--------|-------------|
| `/dcim/devices/create/stream` | GET | Sync Proxmox nodes as devices |
| `/virtualization/virtual-machines/create/stream` | GET | Sync VMs |
| `/virtualization/virtual-machines/storage/create/stream` | GET | Sync storage |
| `/virtualization/virtual-machines/virtual-disks/create/stream` | GET | Sync virtual disks |
| `/virtualization/virtual-machines/backups/all/create/stream` | GET | Sync backups |
| `/virtualization/virtual-machines/snapshots/all/create/stream` | GET | Sync snapshots |
| `/full-update/stream` | GET | Full sync (all stages) |

### Expected Response Format

All sync endpoints return `text/event-stream`:

```
event: step
data: {"step": "<stage>", "status": "<status>", "message": "<message>"}

event: complete
data: {"ok": true, "message": "Sync completed"}
```

### Query Parameters

Backend should accept these query parameters:
- `use_guest_agent_interface_name` (bool)
- `fetch_max_concurrency` (int)
- `proxmox_endpoint_ids` (comma-separated IDs)
- `netbox_endpoint_ids` (comma-separated IDs)
- `delete_nonexistent_backup` (bool)

---

## Frequently Asked Questions

### How do I add a new field to a model?

1. Add field to model in `netbox_proxbox/models/<model>.py`
2. Update form in `netbox_proxbox/forms/<model>.py`
3. Update table in `netbox_proxbox/tables/<model>.py`
4. Update serializer in `netbox_proxbox/api/serializers/<model>.py`
5. Create migration: `python manage.py makemigrations netbox_proxbox`
6. Run migration: `python manage.py migrate`

### How do I change the sync order?

Edit `_SYNC_STAGE_ORDER` in `netbox_proxbox/jobs.py`.

### How do I debug a sync job?

1. Check NetBox job status in UI (Plugins > Proxbox > Jobs)
2. View job `log_entries` field
3. Check RQ worker logs
4. Enable debug logging: `logging.getLogger("netbox_proxbox").setLevel(logging.DEBUG)`

### How do I add a custom sync type?

1. Add choice to `SyncTypeChoices` in `choices.py`
2. Add path mapping in `_SYNC_TYPE_PATH` in `jobs.py`
3. Create view in `views/sync.py`
4. Add URL pattern
5. Add button to home template

### How do I access endpoint credentials in views?

```python
from netbox_proxbox.models import FastAPIEndpoint

fastapi = FastAPIEndpoint.objects.first()
context = get_fastapi_request_context()  # Returns URLs and headers
```

---

## Related Documentation

- **NetBox Documentation**: https://netbox.readthedocs.io/
- **NetBox Plugin Development**: https://netbox.readthedocs.io/en/stable/plugins/development/
- **Proxbox API Documentation**: https://proxbox-api.readthedocs.io/
- **Project README**: ./README.md
- **Development Guide**: ./DEVELOP.md
- **Contributing Guide**: ./CONTRIBUTING.md

---

*This file is designed for LLM context. For human-readable documentation, see README.md and DEVELOP.md.*
