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>
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-nextfor generic UI,simple-iconsfor 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 beforecreate_all)backend/database.py: SQLAlchemy engine and sessionsbackend/models.py: ORM models (User, Vlan, Device, DeviceInterface)backend/routers/auth.py: JWT login, account update, token refresh — exportsget_current_userfor main.pybackend/routers/vlans.py: network CRUDbackend/routers/devices.py: device CRUD — assign fields explicitly, do not usemodel_dump()on Devicebackend/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 localStoragefrontend/src/i18n.js: i18n —locale(ref),t(key),tFmt(key, ...args),setLocale(lang)frontend/src/theme.js: theme —theme(ref),toggleTheme(), applies/removeshtml.darkclassfrontend/src/brandIcons.js: BRANDS array +detectBrands(name, description)— shared utilityfrontend/src/components/TopologyGraph.vue: topology card layout, ping featurefrontend/src/components/DeviceIcon.vue: Lucide type icon (proptypeOnly) or brand logosfrontend/src/components/DeviceManager.vue: device CRUD + filter bar + searchfrontend/src/components/VlanManager.vue: network CRUD (LANs + VLANs)frontend/src/components/DiscoveryModal.vue: automatic discovery UIfrontend/src/components/LoginPage.vue: full-screen login form, emits@loginwith{ token, username }frontend/src/components/AccountModal.vue: change username/password, emits@updatedwith 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,colorDevice: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_liveboxdevices - Gateway card: yellow,
is_gatewaydevices - 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-onlyprop)- Brand logos: small colored SVGs in
chip-sub, no text label,titletooltip on hover tag-link: green, clickable<a>, only ifdevice.urlis 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 chipstrue: 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(): recreatesvlansto allow NULLvlan_id_migrate_device_virt_type(): addsvirt_type VARCHARtodevices_migrate_device_url(): addsurl VARCHARtodevices_migrate_users(): creates theuserstable and seedsadmin/adminif 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 themehtml.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.