88cf6458d0
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>
208 lines
11 KiB
Markdown
208 lines
11 KiB
Markdown
# 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 `<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`.
|