update_vlan now checks for vlan_id conflicts (excluding the current record) before committing, matching the behaviour of create_vlan and preventing an unhandled IntegrityError 500. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
✨ Features
- 🗂️ Manual inventory — add and manage devices (21 types) with IPs, VLANs, descriptions and optional web links
- 🗺️ Topology view — card-based layout per network (LAN / VLAN 802.1Q), with WAN and gateway sections
- 📡 ICMP ping sweep — check reachability of all known hosts in one click
- 🔍 Auto-discovery — ping sweep + PTR DNS lookup on a subnet to import new hosts
- 🏷️ Brand logos — automatic detection and display of vendor logos (Proxmox, Cisco, Synology, Docker, 30+ more)
- 🔐 Authentication — JWT-based login with forced password change on first use
- 🌙 Dark mode — light / dark theme toggle
- 🌍 i18n — French, English, Spanish
🛠️ Stack
| Layer | Technology |
|---|---|
| Backend | FastAPI + SQLAlchemy + SQLite (Python 3.11) |
| Frontend | Vue 3 + Vite, served by Nginx |
| Auth | JWT HS256, 24-hour expiry |
| Runtime | Docker Compose |
🚀 Quick start
# 1. Clone and enter the project
git clone https://git.raspot.in/olivier/stupid-simple-network-inventory.git
cd stupid-simple-network-inventory
# 2. Create the data directory owned by the current user
mkdir -p db_data
# 3. Configure environment (required for correct bind-mount ownership)
cp .env.example .env
# Edit .env:
# DOCKER_UID / DOCKER_GID → output of: id -u && id -g
# INITIAL_ADMIN_PASSWORD → set to avoid the default admin/admin bootstrap
# 4. Build and start
docker compose --env-file .env up --build -d
# 5. Open http://localhost:8080 in your browser
First login
| Case | Credentials | Behaviour |
|---|---|---|
INITIAL_ADMIN_PASSWORD set |
admin / <your password> |
Normal login |
INITIAL_ADMIN_PASSWORD unset |
admin / admin |
Forced password change before accessing the app |
🔄 Updating
Data is stored in ./db_data/ (bind-mount), which is never touched by container rebuilds. Updating is safe:
git pull
docker compose up --build -d
Database migrations run automatically on startup — no manual steps required.
⚙️ Configuration
All configuration is via environment variables. See .env.example for the full list with descriptions.
| Variable | Default | Description |
|---|---|---|
SECRET_KEY |
auto-generated | JWT signing key. Set explicitly in production. |
INITIAL_ADMIN_PASSWORD |
(empty) | Bootstrap admin password. If unset, admin/admin is used with forced change. |
ALLOWED_ORIGINS |
* |
CORS allowed origins (comma-separated). Set to your domain in production. |
BIND_ADDRESS |
0.0.0.0 |
IP address to listen on. Set to the interface facing the reverse proxy. |
DNS_SERVER |
(empty) | DNS server for PTR lookups during auto-discovery. Pre-fills the field in the UI (overridable per scan). Leave empty to disable reverse DNS. |
DOCKER_UID / DOCKER_GID |
1000 |
UID/GID for the backend process. Must match the host user owning ./db_data/. |
cp .env.example .env
# Edit .env — at minimum set DOCKER_UID, DOCKER_GID, INITIAL_ADMIN_PASSWORD
docker compose --env-file .env up --build -d
🔒 Security
Secret management
Two options depending on your security requirements.
Option A — Auto-generated secret (recommended for single-node)
Leave SECRET_KEY unset (or empty) in .env. On first start the backend generates a random 64-character hex key, writes it to db_data/secret_key.txt with permissions 0600, and reuses it on every subsequent restart. The secret never appears in an environment variable, a compose file, or a log.
# .env — leave the line empty or remove it
SECRET_KEY=
The only requirement is that db_data/ is backed up (it already contains the database).
Option B — Docker Compose file secret
Stores the secret in a file on the host, outside version control, and mounts it into the container. The value never appears in an environment variable.
# Generate and store the secret outside the project directory
mkdir -p ~/.secrets
python3 -c "import secrets; print(secrets.token_hex(32))" > ~/.secrets/topologie_secret_key
chmod 600 ~/.secrets/topologie_secret_key
Then uncomment the secrets: blocks in docker-compose.yml (see comments in that file) and remove SECRET_KEY from .env. Docker Compose merges the override automatically:
docker compose up -d
Key rotation
To rotate the JWT secret (invalidates all active sessions):
# Option A — environment variable (recommended)
# Set a new SECRET_KEY in your deployment config and restart
# Option B — file rotation
docker compose stop backend
rm db_data/secret_key.txt
docker compose start backend
# All users will need to log in again
HTTPS
This application does not terminate TLS. For production use, place it behind a reverse proxy that handles HTTPS.
Nginx
server {
listen 443 ssl;
server_name inventory.example.com;
ssl_certificate /etc/letsencrypt/live/inventory.example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/inventory.example.com/privkey.pem;
location / {
proxy_pass http://127.0.0.1:8080;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}
Traefik — Docker labels
Add these labels to the frontend service and connect it to the network shared with Traefik:
# docker-compose.override.yml
services:
frontend:
labels:
- "traefik.enable=true"
- "traefik.http.routers.inventory.rule=Host(`inventory.example.com`)"
- "traefik.http.routers.inventory.entrypoints=websecure"
- "traefik.http.routers.inventory.tls.certresolver=letsencrypt"
- "traefik.http.services.inventory.loadbalancer.server.port=8080"
networks:
- internal
- traefik_public # network shared with your Traefik instance
networks:
traefik_public:
external: true
Traefik — Dynamic configuration (file provider)
If Traefik is not running in Docker (or you prefer the file provider), drop a file in your dynamic config directory:
# /etc/traefik/dynamic/inventory.yml
http:
routers:
inventory:
rule: "Host(`inventory.example.com`)"
entryPoints:
- websecure
tls:
certResolver: letsencrypt
service: inventory-svc
services:
inventory-svc:
loadBalancer:
servers:
- url: "http://127.0.0.1:8080"
Traefik picks up the file automatically — no restart required.
For local-only use, bind to loopback to prevent accidental LAN exposure:
# docker-compose.override.yml
services:
frontend:
ports:
- "127.0.0.1:8080:8080"
Container hardening
| Measure | Backend | Frontend |
|---|---|---|
| Non-root user | DOCKER_UID:DOCKER_GID (host user) |
nginx (UID 101) |
cap_drop: ALL |
✓ | ✓ |
cap_add: NET_RAW |
✓ (ping) | — |
no-new-privileges |
— ¹ | ✓ |
| Healthcheck | ✓ | ✓ |
¹ Omitted on the backend: ping uses file capabilities (cap_net_raw=ep); no-new-privileges suppresses the file effective bit and would prevent the subprocess from acquiring CAP_NET_RAW in its effective set even though the parent holds it in its permitted set.
💾 Data persistence
All data is stored in ./db_data/:
| File | Description |
|---|---|
topology.db |
SQLite database |
secret_key.txt |
Auto-generated JWT secret (0600 permissions) |
Backup: cp -r db_data/ db_data.bak/
Restore: stop the stack, replace db_data/, restart.
🏷️ Brand logo detection
Logo detection runs automatically against the device name and description fields (case-insensitive keyword matching). No manual configuration needed — just include a recognisable keyword in the name or description.
Logos are displayed in two places:
- Topology view — small inline SVGs next to the device name on each chip
- Device list — coloured badge(s) in the device card
Multiple logos can appear simultaneously if several keywords match.
| Category | Brand | Trigger keywords |
|---|---|---|
| Virtualisation | Proxmox | proxmox, pve |
| Virtualisation | Docker | docker |
| NAS | Synology | synology, dsm |
| NAS | TrueNAS | truenas, freenas |
| UPS | Schneider Electric / APC | apc, schneider electric, symmetra, smart-ups, easy ups, galaxy ups |
| UPS | Eaton | eaton, powerware, eaton ups |
| UPS | Riello | riello, riello ups |
| UPS | Vertiv | vertiv, liebert, avocent, geist |
| French ISP | Orange | orange, sosh, livebox |
| French ISP | OVH | ovh, ovhcloud, kimsufi, soyoustart |
| French ISP | Free | freebox, free mobile, free telecom, iliad |
| French ISP | Bouygues Telecom | bouygues, bbox |
| French ISP | SFR | sfr, red by sfr, sfr box |
| Network | Ubiquiti / UniFi | ubiquiti, unifi, usg, udm |
| Network | MikroTik | mikrotik, routeros |
| Network | Cisco | cisco |
| Network | TP-Link | tp-link, tplink, tp link |
| Network | ASUS | asus |
| Network | Netgear | netgear |
| Network | pfSense | pfsense |
| Network | OPNsense | opnsense |
| Network | OpenWrt | openwrt |
| Network security | Fortinet / FortiGate | fortinet, fortigate, fortios, fortimanager, fortiauthenticator |
| Web / proxy | Apache | apache, apache2, httpd |
| Web / proxy | Nginx | nginx |
| Web / proxy | Traefik | traefik |
| Web / proxy | Apache Guacamole | guacamole |
| Bastion | Bastion / jump host | bastion, jumphost, jump host, jump server, teleport, bastillion |
| Auth / SSO | Authelia | authelia |
| Auth / SSO | Keycloak | keycloak |
| Auth / SSO | Authentik | authentik |
| Auth / SSO | Okta | okta |
| Auth / SSO | Auth0 | auth0 |
| Password vault | Vaultwarden | vaultwarden |
| Password vault | Bitwarden | bitwarden |
| Password vault | 1Password | 1password, onepassword |
| Password vault | KeePassXC | keepass, keepassxc |
| Password vault | HashiCorp Vault | hashicorp vault, hashicorp |
| Archiving | Archive server | archive, archiver, archivage, archivar, archivebox |
| Mail server | mail, smtp, imap, postfix, dovecot, mailcow, mailu, roundcube |
|
| Database | MariaDB | mariadb, maria db |
| Orchestration | Kubernetes | kubernetes, k8s, kubectl, k3s |
| Monitoring | Zabbix | zabbix |
| Monitoring | Centreon | centreon |
| Monitoring | Nagios | nagios, nagiosxi, nagios xi |
| Monitoring | PRTG | prtg, paessler |
| Monitoring | Prometheus | prometheus |
| Monitoring | Grafana | grafana |
| Monitoring | Datadog | datadog |
| Monitoring | Netdata | netdata |
| Monitoring | Checkmk | checkmk, check_mk |
| Monitoring | Icinga | icinga, icinga2 |
| Monitoring | InfluxDB | influxdb, influx db |
| Monitoring | VictoriaMetrics | victoriametrics, victoria metrics |
| Alerting | Opsgenie | opsgenie |
| Alerting | PagerDuty | pagerduty, pager duty |
| Logs / Traces | Elastic / ELK | elasticsearch, elastic stack, elk |
| Logs / Traces | Kibana | kibana |
| Logs / Traces | Logstash | logstash |
| Logs / Traces | Splunk | splunk |
| Logs / Traces | Graylog | graylog |
| Logs / Traces | Jaeger | jaeger |
| Logs / Traces | OpenTelemetry | opentelemetry, otel |
| Apple ecosystem | Apple | apple, iphone, ipad, ipados, macbook, imac, mac mini, mac pro, mac studio, macos, mac os, ios, icloud, airpods, airdrop |
| OS | Windows | windows, win10, win11, winserver, windows server |
| OS | Debian | debian |
| OS | Ubuntu | ubuntu |
| Automation | Ansible | ansible |
| Servers | Dell | dell, idrac, poweredge |
| Servers | HP | proliant, ilo, hewlett |
| SBC / DIY | Raspberry Pi | raspberry, raspberrypi, rpi, raspi |
| SBC / DIY | Arduino | arduino |
| Browser | Firefox | firefox |
| Desktop | KDE / Plasma | kde, plasma, kde desktop |
| Tools | Excalidraw | excalidraw |
| Self-hosted | Nextcloud | nextcloud |
| Self-hosted | Paperless-NGX | paperless, paperless-ng, paperless-ngx |
| Self-hosted | Uptime Kuma | uptime-kuma, uptimekuma, uptime kuma |
| Self-hosted | MkDocs | mkdocs, material for mkdocs |
| CMS / Blog | WordPress | wordpress |
| CMS / Blog | Ghost | ghost |
| CMS / Blog | Grav | grav |
| CMS / Blog | Jekyll | jekyll |
| CMS / Blog | Hugo | hugo |
| CMS / Blog | Hexo | hexo |
| CMS / Blog | Drupal | drupal |
| CMS / Blog | Joomla | joomla |
| CMS / Blog | TYPO3 | typo3 |
| CMS / Blog | OctoberCMS | octobercms, october cms |
| CMS / Blog | Textpattern | textpattern |
| Analytics | Matomo | matomo |
| Analytics | Plausible | plausible |
| Smart TV | Samsung | samsung, tizen, samsung tv |
| Smart TV | LG | lg, webos, lg tv |
| Smart TV | Sony | sony, bravia |
| Smart TV | Panasonic | panasonic |
| Smart TV | Sharp | sharp |
| Smart TV | Toshiba | toshiba |
| Smart TV | Vestel | vestel |
| TV Box | Chromecast / Google TV | chromecast, google tv |
| TV Box | Android TV | android tv, androidtv |
| TV Box | Apple TV | apple tv, appletv |
| TV Box | Amazon Fire TV | fire tv, firetv, amazon fire |
| TV Box | Roku | roku |
| TV Box | Kodi | kodi |
| Media / torrent | Radarr | radarr |
| Media / torrent | Sonarr | sonarr |
| Media / torrent | Transmission | transmission |
| Media / home automation | Jellyfin | jellyfin |
| Media / home automation | Home Assistant | homeassistant, home assistant, hassio, hass |
| Media / home automation | Philips Hue | philips hue, hue bridge, hue hub |
| Media / home automation | Xiaomi | xiaomi, mi home, yeelight |
🧑💻 Development
Backend tests
cd backend
pip install -r requirements.txt -r requirements-test.txt
pytest tests/ -v
Local dev (without Docker)
# Backend
cd backend
pip install -r requirements.txt
uvicorn main:app --reload
# Frontend (separate terminal)
cd frontend
npm install
npm run dev # Vite dev server on :5173, proxies /api/ to :8000
🔧 Troubleshooting
Scan returns hundreds of false positives (proxy-ARP / UniFi)
Symptom — Auto-discovery reports as many hosts as there are addresses in the subnet (e.g. 254 for a /24), while only a few machines are actually present.
Cause — Some network equipment (notably UniFi Security Gateway, Dream Machine, and similar devices) enables proxy-ARP and responds to ICMP pings for every IP in the subnet, spoofing the source IP of the reply. The built-in source-IP check in the scanner cannot filter these false positives.
Fix — Enable the "TCP check (anti proxy-ARP)" option in the scan configuration screen. This option uses TCP instead of ICMP to detect live hosts (ports 22, 80, 443, 8080, 8443):
- A real host replies with RST (port closed) or accepts the connection → marked alive.
- A ghost IP: the gateway silently drops the SYN without replying → timeout → discarded.
Note
: a device whose firewall silently drops (DROP, without RST) all probed ports will not be discovered automatically and must be added manually.
Some devices are missing from the scan (ICMP rate-limiting)
Symptom — A few hosts (APs, switches, IoT devices) respond to a direct ping but are not found during a full subnet scan.
Cause — When 100 concurrent ICMP workers flood a /24, some devices or managed switches rate-limit ICMP responses. The device drops the probe during the scan even though a single ping works fine.
Fix — Two options:
- Enable "Soft scan (slow ICMP)": reduces concurrency from 100 to 10 workers. The scan takes longer but ICMP probes are spread out, avoiding rate-limiting. Best for subnets without proxy-ARP.
- Enable "TCP check (anti proxy-ARP)": bypasses ICMP entirely. TCP probes are not subject to the same rate-limiting. Best when both proxy-ARP and rate-limiting are present.
🏗️ Architecture
See docs/architecture.md for the detailed request flow, Docker setup, and authentication model.
🤖 Built with AI assistance
This application was developed entirely with the help of AI coding assistants:
- Claude Code (Anthropic) — used throughout the project for architecture design, backend API (FastAPI, SQLAlchemy, authentication, ICMP discovery), frontend components (Vue 3, CSS topology layout, i18n, dark mode), Docker & Nginx configuration, and documentation.
- Codex (OpenAI) — also used during development for code generation and suggestions across the entire codebase.
📄 License
This project is licensed under the GNU General Public License v3.0.
You are free to use, modify and distribute this software under the terms of the GPL v3. Any derivative work must be distributed under the same license.
