Skip to content

Conventions

Decisions that hold across the codebase. If you're adding something and you're not sure how, the answer is probably here.

Identifiers

  • Every model has a UUID PK. Period.
  • Slugs are used for human URLs (org, tenant, tag). Generated via slugify on save.

Tenant scoping

  • Every domain model has tenant = FK(Tenant, on_delete=CASCADE).
  • Every view starts with tenant = _get_active_tenant(request).
  • Every queryset starts with .filter(tenant=tenant).
  • Never trust a UUID in a URL to belong to the current tenant. Use get_object_or_404(Model, pk=pk, tenant=tenant).

VRF handling

  • Prefix.vrf and IPAddress.vrf are nullable (NULL = Global).
  • Uniqueness uses nulls_distinct=False so NULL behaves like a value.
  • Children inherit parent's VRF on create (form sets initial).

Naming

Thing Convention Example
Models PascalCase singular Prefix, IPAddress
ViewSets / functions snake_case prefix_create, prefixes_list
URLs kebab-case /prefixes/<uuid>/edit/
URL names <resource> or <resource>-<action> api:prefix-detail, api:prefixes-export-csv
Templates <app>/<resource>.html or <app>/<resource>_<action>.html api/prefixes.html, api/prefix_detail.html
Partial templates leading _ api/_shell.html, api/_prefix_row.html
Internal helpers leading _ _get_active_tenant, _autospawn_gateway

Forms

  • ModelForms inherit a tenant= kwarg in __init__ to scope select querysets.
  • All validation in clean_<field> or clean() — never in the view.
  • Show clean errors that suggest what to do next: not just "duplicate", but "A prefix with CIDR X already exists in VRF 'production'. Pick a different VRF or CIDR."

Linked objects on detail pages

Detail pages show their related objects as inline tables, not "View X →" links that navigate away. Reuse the embedded-table components (EmbeddedDeviceTable, EmbeddedDeviceTypeTable, and EmbeddedIpTable / EmbeddedRackTable / EmbeddedClusterTable in embedded-tables.tsx): each takes a filter object of /api/ query params and renders a DataTable in a tab. The list endpoints carry the matching filters (device device_type/ role/platform/manufacturer/site/location; ip role/status/vrf; rack role/location; cluster type/group). A count-stat-only page gains a drill-in tab; a "View X" link becomes the table itself.

Picking objects (SPA)

Never hand-roll a FormCombobox + ?picker=1 options block for a domain object. Use the generic <ObjectPicker spec …/> (frontend/src/components/object-picker.tsx) or, better, one of its presets — DevicePicker, RackPicker, VlanPicker, PrefixPicker, IpPicker (the last two prove the no-name case — optionLabel renders CIDR / address · DNS). Each is a searchable combobox (shared compact-picker cache) plus a sliders button opening an advanced-search dialog: free text + server-side filter selects + a paginated result table (250/page), all driven by an ObjectPickerSpec (endpoints, filters, columns, optional row/option ghosting). The Field contract matches FormCombobox, so call sites swap 1:1; excludeIds hides rows, quickAdd forwards a "+" control. New picker = ~60-line spec file; the DevicePicker extras (VC ghosting via ?with_vc=1) show how type-specific behavior rides on optionState/rowState.

/api/devices/ supports these filter params (all tenant/RBAC-scoped): search, site, location, device_type, role, status, rack, platform, manufacturer (via the device's type), region (includes descendant regions — Region is a plain adjacency-list tree, walked in Python since there's no MPTT), and tag (repeatable, AND semantics). Add ?picker=1 for the compact {id, name} shape; omit it for the full list serializer the advanced table needs. A generic <ObjectPicker> can later be extracted from DevicePicker for other object types.

Faceplate rendering (SPA)

The front-panel drawing is millimetre-true. All physical constants live in frontend/src/lib/faceplate-geometry.ts (CONNECTOR_MM per connector family, PANEL_MM EIA-310 numbers, familyForType(slug) mapping the NetBox-style type taxonomy → connector family, renderTemplateName mirroring the backend's {position} resolver; {module} resolves to the module bay's position at install time — see render_module_name). Layout logic lives in frontend/src/lib/faceplate-layout.ts: one JSON doc schema (FaceplateDoc v1: front/rear group arrays → slots referencing component-template names, never pixels) is both computed by autoLayout() (front only) and saved by the Faceplate builder to DeviceType.faceplate (JSONB, null = auto; validated in DeviceTypeSerializer.validate_faceplate). resolveLayout(doc, side, …) matches one side against a device's components; unmatched slots render as ghosts, uncovered interfaces append as auto groups on the front; a port lives on exactly one side. Groups carry an optional u (1-based rack-unit lane; multi-U types get one builder lane + "+" zone per U and render stacked lanes). Never hardcode port pixel sizes — extend CONNECTOR_MM/familyForType instead. The eighth component kind, Aux port (AuxPort/AuxPortTemplate, aux_port_types taxonomy), models USB/video/SD/grounding connectors.

Templates

  • Pages extend api/_shell.html (sidebar + topbar shell).
  • Repeated row markup → extract to _<thing>_row.html partial.
  • Inline Lucide-style SVGs at 16×16 (h-4 w-4), stroke 2, currentColor.
  • Class strings on UI components match /CLAUDE.md snippets exactly.

Mockup ↔ template

  • Mockups in design/*.html are the design source of truth.
  • When a mockup is approved, port to a Django template at api/templates/api/.
  • Class strings should match the mockup verbatim — if they diverge, update the mockup or open a design issue.

Comments

  • Lean: don't write comments that re-state code.
  • Do write the why when it isn't obvious — a workaround, a constraint, a deliberate departure from a common convention. The "deliberate departure from all-or-nothing CSV import" comment in _import_rows is the model.

Migrations

  • Generate via makemigrations core api.
  • For schema changes that aren't compatible with existing rows (new non-null FK):
    1. Drop the dev DB (DROP DATABASE danbyte; CREATE DATABASE danbyte OWNER danbyte;)
    2. Remove the affected migrations
    3. Re-makemigrations + migrate
    4. seed_demo
  • This is a dev shortcut. Production migrations need data migrations + nullable-first-then-non-null patterns.

Seed data

  • seed_demo is opt-in (python manage.py seed_demo), idempotent, and uses get_or_create.
  • Don't put seed data in migrations.
  • Don't ship seed data with prod.

Tests

  • Currently sparse. Add focused tests when fixing a bug; broader test coverage is a deferred quality-of-life round.