Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
✨ Features
- 🗂️ Manual inventory — add and manage devices (18 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 |
⚙️ 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. |
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.
🧑💻 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
🏗️ Architecture
See docs/architecture.md for the detailed request flow, Docker setup, and authentication model.
