# Data Model ## Schema Overview ``` users id PK username UNIQUE NOT NULL hashed_password NOT NULL vlans id PK vlan_id UNIQUE NULL ← NULL = plain LAN (no 802.1Q tag) name NOT NULL cidr NULL color NULL devices id PK name NOT NULL type description is_gateway BOOL is_livebox BOOL virt_type NULL ← null | baremetal | lxc | qemu url NULL device_interfaces id PK device_id FK → devices.id NOT NULL vlan_id FK → vlans.id NULL ← NULL = no network assignment ip_address NULL name is_upstream BOOL links id PK source_device_id FK → devices.id NOT NULL target_device_id FK → devices.id NOT NULL link_type description ``` ## Relationships - `Device` 1 → N `DeviceInterface` (cascade delete-orphan) - `Vlan` 1 → N `DeviceInterface` (nullable FK — deleting a VLAN sets `vlan_id` to NULL on its interfaces) - `Link` references `Device` twice (source, target) — links are deleted explicitly in `delete_device` before removing the device ## Key Design Decisions ### `vlan_id` is nullable A network with `vlan_id = NULL` is a plain LAN without 802.1Q tagging. This allows modelling both tagged and untagged networks in the same table. The UI distinguishes them with "LAN" vs "VLAN X" badges. The `vlans` table was originally created with `vlan_id NOT NULL`. The migration `_migrate_vlan_nullable()` recreates it (SQLite cannot `ALTER COLUMN`). ### Interfaces as a separate table A device can belong to multiple networks simultaneously. The `device_interfaces` table is a join table with extra attributes (IP, name, is_upstream). A device appears in every topology card that corresponds to one of its interfaces. ### `is_gateway` and `is_livebox` These boolean flags drive the special WAN and Gateway cards in the topology view. They are independent of network membership — a gateway device can also have interfaces in various VLANs. ### No foreign key enforcement at the SQLite level SQLAlchemy's `ForeignKey` declarations define the schema, but SQLite does not enforce FK constraints unless `PRAGMA foreign_keys=ON` is set per-connection. The application does not enable this pragma, so referential integrity is maintained by application logic (explicit deletes in the routers). ## SQLite Migrations Migrations are idempotent Python functions that run at startup before `Base.metadata.create_all`. They check for the presence of columns or tables before acting, so they are safe to run on every container start. Pattern for adding a nullable column: ```python def _migrate_my_column(): with engine.connect() as conn: if not conn.execute(text( "SELECT name FROM sqlite_master WHERE type='table' AND name='devices'" )).fetchone(): return cols = [row[1] for row in conn.execute(text("PRAGMA table_info(devices)")).fetchall()] if 'my_column' not in cols: conn.execute(text("ALTER TABLE devices ADD COLUMN my_column VARCHAR")) conn.commit() ``` ## Device Types 18 valid values for `devices.type`: | Value | Description | |-------|-------------| | `server` | Physical or virtual server | | `switch` | Network switch | | `router` | Network router | | `nas` | Network-attached storage | | `gateway` | Internet gateway / firewall | | `livebox` | ISP-provided box (WAN) | | `access_point` | Wi-Fi access point | | `camera` | IP camera | | `temperature` | Temperature sensor | | `sensor` | Generic sensor | | `hub` | Network hub | | `smart_plug` | Smart power plug | | `alarm` | Alarm system | | `light` | Smart light | | `doorbell` | Smart doorbell | | `desktop` | Desktop computer | | `laptop` | Laptop computer | | `other` | Anything else | `url` is hidden in the device form for `desktop` and `laptop` — these types have no web UI. ## Virtualisation Types `devices.virt_type` valid values: | Value | Display | Badge | |-------|---------|-------| | `null` | (not shown) | — | | `baremetal` | Bare metal | — | | `lxc` | LXC container | blue "LXC" | | `qemu` | QEMU/KVM VM | purple "VM" |