Files
stupid-simple-network-inven…/AGENTS.md
T
olivier 88cf6458d0 Initial commit — Stupid Simple Network Inventory
Application web d'inventaire réseau manuel avec FastAPI, Vue 3 et Docker.
Inclut l'authentification JWT, la découverte ICMP, et la topologie en cards CSS.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-17 09:19:19 +02:00

11 KiB

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

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 <AccountModal :forced="true"> 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<LoginPage>
  • mustChangePassword<AccountModal :forced="true">
  • else → full app

Template root is <div class="app-root"> 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 <a>, 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<Name> 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 <style>:

  • :root — light theme
  • html.dark — dark theme

Available variables: --bg-page, --bg-card, --bg-card-hover, --bg-input, --bg-thead, --bg-chip, --bg-chip-hover, --border, --border-strong, --text-primary, --text-secondary, --text-muted, --text-faint, --chip-ip-bg, --chip-ip-color, --modal-overlay, --shadow-card, --shadow-modal, --shadow-drop.

Rule: all background/text/border colors in components must use these variables, not hardcoded hex. Exception: semantic fixed colors (type-specific chip icon colors, VLAN badge colors) stay as hex.

Dark-mode overrides for scoped components must use :global(html.dark .selector) — the full selector must be inside the parentheses. :global(html.dark) .selector does NOT work (Vue's scoped CSS transformer does not follow descendant selectors after :global()).

Toggle button (☾/☀) in sidebar footer.

i18n (i18n.js)

i18n.js exports locale (ref), t(key), tFmt(key, ...args), setLocale(lang). Persisted in localStorage('locale'), default 'fr'.

Supported languages: fr, en, es.

Rule: all visible UI strings use t('key') — never hardcoded text in templates. deviceTypes in DeviceManager is a computed (not a const) so type labels react to locale changes.

Language switcher pills (fr / en / es) in sidebar footer.

Favicon

frontend/public/favicon.svg — SVG network graph icon (indigo #6366f1), same shape as sidebar logo. Linked in index.html as <link rel="icon" type="image/svg+xml" href="/favicon.svg" />.

Action Buttons (btn-icon)

.btn-icon uses background: var(--bg-chip) and color: var(--text-secondary) for adequate contrast in both themes. Hover: var(--border-strong) / var(--text-primary). Danger hover: rgba(239,68,68,0.15) (transparent red, theme-neutral).

Persistence

Docker volume db_data/app/data/topology.db and /app/data/secret_key.txt.