# Stupid Simple Network Inventory — Agent Notes ## Project Overview Web application for manual server/network inventory and logical network topology display. ## Stack - Backend: FastAPI, Python 3.11, SQLAlchemy, SQLite - Frontend: Vue 3 + Vite - Topology view: CSS cards, no Cytoscape - Icons: `lucide-vue-next` for generic UI, `simple-icons` for brand logos - Reverse proxy: Nginx serves the frontend and proxies `/api/` to the backend - Runtime: Docker Compose ## Run ```bash docker compose up --build -d ``` Application URL: http://localhost:8080 ## Architecture - `backend/main.py`: FastAPI entry point + startup SQLite migrations (run before `create_all`) - `backend/database.py`: SQLAlchemy engine and sessions - `backend/models.py`: ORM models (User, Vlan, Device, DeviceInterface) - `backend/routers/auth.py`: JWT login, account update, token refresh — exports `get_current_user` for main.py - `backend/routers/vlans.py`: network CRUD - `backend/routers/devices.py`: device CRUD — assign fields explicitly, do not use `model_dump()` on Device - `backend/routers/discovery.py`: ICMP ping sweep + DNS PTR (`/scan`) + parallel ping (`/ping`) - `frontend/src/App.vue`: global layout and state + auth guard (shows LoginPage if not authenticated) - `frontend/src/api.js`: axios API calls (`vlansApi`, `devicesApi`, `discoveryApi`, `authApi`) - `frontend/src/auth.js`: reactive auth state (`isAuthenticated`, `currentUsername`, `setAuth`, `clearAuth`, `getToken`) — persisted in localStorage - `frontend/src/i18n.js`: i18n — `locale` (ref), `t(key)`, `tFmt(key, ...args)`, `setLocale(lang)` - `frontend/src/theme.js`: theme — `theme` (ref), `toggleTheme()`, applies/removes `html.dark` class - `frontend/src/brandIcons.js`: BRANDS array + `detectBrands(name, description)` — shared utility - `frontend/src/components/TopologyGraph.vue`: topology card layout, ping feature - `frontend/src/components/DeviceIcon.vue`: Lucide type icon (prop `typeOnly`) or brand logos - `frontend/src/components/DeviceManager.vue`: device CRUD + filter bar + search - `frontend/src/components/VlanManager.vue`: network CRUD (LANs + VLANs) - `frontend/src/components/DiscoveryModal.vue`: automatic discovery UI - `frontend/src/components/LoginPage.vue`: full-screen login form, emits `@login` with `{ token, username }` - `frontend/src/components/AccountModal.vue`: change username/password, emits `@updated` with new token ## Data Model - `User`: `id`, `username` (unique), `hashed_password` (bcrypt), `must_change_password` (bool, default 0), `token_version` (int, default 1) - `Vlan`: `id`, `vlan_id` (nullable int — null = plain LAN), `name`, `cidr`, `color` - `Device`: `id`, `name`, `type`, `description`, `is_gateway`, `is_livebox`, `virt_type` (nullable), `url` (nullable) - `DeviceInterface`: `id`, `device_id`, `vlan_id` (nullable FK), `ip_address`, `name`, `is_upstream` ## Authentication JWT HS256, **24-hour expiry**. Payload: `{ sub: username, ver: token_version, exp: ... }`. Secret key persisted in `data/secret_key.txt` (0600 permissions) or `SECRET_KEY` env var. Default credentials: `admin` / `admin` — seeded with `must_change_password=1` by `_migrate_users()` if the `users` table is empty. Set `INITIAL_ADMIN_PASSWORD` env var to skip this bootstrap. **Token invalidation**: password change increments `User.token_version`. `get_current_user` rejects tokens whose `ver` doesn't match → old sessions expire immediately. **must_change_password**: when `True`, business routers return 403 `"Password change required"`. The frontend shows a forced `` that blocks the app. `_migrate_force_admin_password_change()` sets this flag at startup if admin's password is still the default. **bcrypt compatibility**: `passlib 1.7.4` is incompatible with `bcrypt >= 4.0`. `requirements.txt` pins `bcrypt==3.2.2`. Do not upgrade `bcrypt` without also upgrading `passlib`. Business route groups protected via `dependencies=[Depends(require_password_changed)]` (wraps `get_current_user`). Auth router has no protection. Frontend auth guard in `App.vue`: - `!isAuthenticated` → `` - `mustChangePassword` → `` - else → full app Template root is `
` with `display: contents` — prevents Firefox autofill overlay `NotFoundError` on fragment comment nodes. ## Device Types (18) `server`, `switch`, `router`, `nas`, `gateway`, `livebox`, `access_point`, `camera`, `temperature`, `sensor`, `hub`, `smart_plug`, `alarm`, `light`, `doorbell`, `desktop`, `laptop`, `other` ## Device Fields ### virt_type Form label: "Type d'environnement d'exécution". Values: `null`, `baremetal`, `lxc`, `qemu`. ### url Optional web UI URL. Hidden in the form for `desktop` and `laptop` types. Displayed as a clickable link in the device card, and as a green "Link" tag on topology chips. When adding a new Device field: update `models.py`, add a startup migration in `main.py`, update `DeviceCreate` and `DeviceOut` in `routers/devices.py`, assign explicitly in `create_device` and `update_device`. ## Networks (VlanManager) `vlan_id` is optional. Omitting it creates a plain LAN ("LAN" badge). Networks ordered: LANs first (NULL vlan_id), then VLANs by ascending ID. ## Topology View `TopologyGraph.vue` uses CSS cards only. - Toolbar: Ping button with animated dot indicator + up/down count - WAN card: red, `is_livebox` devices - Gateway card: yellow, `is_gateway` devices - Network list: one full-width card per network, stacked vertically (flex-col); a device appears in every card matching its interfaces - Unassigned card: devices with no interface, not livebox, not gateway - Devices sorted by IP ascending; no-IP devices go last ### Card layout All network cards are full width, stacked vertically. Inside each card, chips are arranged in a horizontal grid (`grid auto-fill, minmax(210px, 1fr)`), wrapping as needed. Card height grows with the number of chips. Single column below 600px. ### Device chip structure ``` [chip-icon (type, Lucide)] [chip-body] [chip-tags] chip-name chip-sub: IP + iface + brand SVGs (11px) chip-tags: Link | GW | LXC | VM + ping dot ``` - `chip-icon`: always the Lucide type icon (`type-only` prop) - Brand logos: small colored SVGs in `chip-sub`, no text label, `title` tooltip on hover - `tag-link`: green, clickable ``, only if `device.url` is set - Ping dot: green (`ping-up`) or red (`ping-down`), shown after a ping run ## DeviceIcon.vue Prop `typeOnly` (bool, default false): - `false`: show brand logos if detected, else Lucide — used in TopologyGraph chips - `true`: always show Lucide type icon — used in DeviceManager cards ## Brand Icons (brandIcons.js) Central `detectBrands(name, description)` returns all matching brand icon objects from `simple-icons`. To check if a brand exists: `node -e "const si = require('./node_modules/simple-icons'); console.log(Object.keys(si).filter(k => k.toLowerCase().includes('name')))"` To add a brand: import `si` in `brandIcons.js`, add entry to `BRANDS` array. Supported brands: Proxmox, Docker, Synology, TrueNAS, Ubiquiti/UniFi, MikroTik, Cisco, TP-Link, ASUS, Netgear, pfSense, OPNsense, OpenWrt, Apache/Apache2, Traefik, MariaDB, Kubernetes/k3s, Debian, Ubuntu, Ansible, Dell, HP, Raspberry Pi, Arduino, KDE/Plasma, Excalidraw, Nextcloud, Paperless-NGX, Uptime Kuma, MkDocs, Jellyfin, Home Assistant, Philips Hue, Xiaomi. ## DeviceManager Filter Bar Compact 28px bar combining: - Search input (name, description, IP — real-time, Escape to clear) - Type dropdown (multi-select, only present types) - Network dropdown (with color swatch) - Brand dropdown (with SVG icon, sorted alphabetically) - Virt dropdown (only if at least one device has a virt_type) All filters combine (AND between categories, OR within). "Clear" button + "X / total" counter when active. ## Ping (discovery.py) `POST /api/discovery/ping` — parallel ICMP ping, 50 workers. - Body: `{ ips: ["1.2.3.4", ...] }` - Returns: `[{ ip, alive }, ...]` Device status: "up" if any of its IPs responds, "down" if all fail, no dot if no IPs. ## SQLite Migrations (main.py) Idempotent startup migrations run before `create_all`: - `_migrate_vlan_nullable()`: recreates `vlans` to allow NULL `vlan_id` - `_migrate_device_virt_type()`: adds `virt_type VARCHAR` to `devices` - `_migrate_device_url()`: adds `url VARCHAR` to `devices` - `_migrate_users()`: creates the `users` table and seeds `admin`/`admin` if count is 0 Pattern for new nullable column: check if column exists via `PRAGMA table_info`, run `ALTER TABLE … ADD COLUMN` if missing. ## Discovery `POST /api/discovery/scan`: ICMP ping sweep + DNS PTR lookup. Max 1024 hosts/network, 100 workers. Needs `cap_add: NET_RAW` (already configured). ## Theme (theme.js) `theme.js` exports `theme` (ref, `'light'`/`'dark'`) and `toggleTheme()`. Persisted in `localStorage('theme')`. Applies/removes `html.dark` class on `document.documentElement`. CSS variables are defined in `App.vue` global `