Files
stupid-simple-network-inven…/CLAUDE.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

14 KiB

Stupid Simple Network Inventory

Application web d'inventaire manuel de serveurs et de génération de topologie réseau logique.

Stack technique

  • Backend : FastAPI (Python 3.11) + SQLAlchemy + SQLite
  • Frontend : Vue 3 + Vite (pas de Cytoscape — topologie en cards CSS)
  • Icônes : lucide-vue-next (UI générique) + simple-icons (logos de marques)
  • Reverse proxy : Nginx (sert le frontend et proxifie /api/ vers le backend)
  • Conteneurisation : Docker Compose

Lancement

docker compose up --build -d

L'application est accessible sur http://localhost:8080

Architecture

topologie/
├── docker-compose.yml
├── backend/
│   ├── Dockerfile           # Python 3.11-slim + iputils-ping (pour ICMP)
│   ├── requirements.txt     # fastapi, sqlalchemy, dnspython, ...
│   ├── main.py              # Point d'entrée FastAPI + migrations SQLite au démarrage
│   ├── database.py          # SQLAlchemy engine + session
│   ├── models.py            # Modèles ORM (Vlan, Device, DeviceInterface)
│   └── routers/
│       ├── vlans.py
│       ├── devices.py
│       └── discovery.py     # POST /api/discovery/scan + POST /api/discovery/ping
└── frontend/
    ├── Dockerfile           # Multi-stage: build Vite → Nginx
    ├── nginx.conf           # Proxy /api/ → backend:8000
    ├── index.html
    ├── package.json
    ├── vite.config.js
    └── src/
        ├── main.js
        ├── App.vue           # Layout + état global (vlans, devices) + garde auth
        ├── api.js            # Appels axios (vlansApi, devicesApi, discoveryApi, authApi)
        ├── auth.js           # État auth : isAuthenticated, currentUsername, setAuth(), clearAuth()
        ├── i18n.js           # Internationalisation : locale (ref), t(key), tFmt(key, ...args), setLocale()
        ├── theme.js          # Thème : theme (ref), toggleTheme() — classe html.dark
        ├── brandIcons.js     # Tableau BRANDS + fonction detectBrands() — partagé entre composants
        └── components/
            ├── TopologyGraph.vue   # Vue cards par réseau + ping statut
            ├── DeviceIcon.vue      # Icône Lucide par type (prop typeOnly) ou logos marques
            ├── DeviceManager.vue   # CRUD équipements + barre filtres/recherche
            ├── VlanManager.vue     # CRUD réseaux (VLANs + LANs classiques)
            ├── DiscoveryModal.vue  # Découverte auto : ping sweep + DNS PTR
            ├── LoginPage.vue       # Page de connexion plein écran
            └── AccountModal.vue    # Modale changement nom d'utilisateur / mot de passe

Modèle de données

  • User : id, username (unique), hashed_password (bcrypt)
  • Vlan : id, vlan_id (int, nullable — null = LAN classique sans tag 802.1Q), name, cidr, color
  • Device : id, name, type, description, is_gateway, is_livebox, virt_type (nullable), url (nullable)
  • DeviceInterface : id, device_id, vlan_id (nullable), ip_address, name, is_upstream

Types d'équipements (18)

server, switch, router, nas, gateway, livebox, access_point, camera, temperature, sensor, hub, smart_plug, alarm, light, doorbell, desktop, laptop, other

Champ virt_type (Device)

Libellé dans le formulaire : "Type d'environnement d'exécution". Valeurs :

  • null — non précisé (défaut)
  • baremetal — serveur physique
  • lxc — conteneur LXC (tag bleu "LXC" dans la topologie)
  • qemu — VM QEMU/KVM (tag violet "VM" dans la topologie)

Champ url (Device)

URL optionnelle d'accès à l'interface web de l'équipement. Non applicable aux types desktop et laptop (masqué dans le formulaire). Affiché comme lien cliquable dans la card équipement et comme tag "Link" vert sur les chips de topologie.

Réseaux (VlanManager.vue)

vlan_id est optionnel. Si absent → réseau LAN classique (badge "LAN"). Si présent → VLAN 802.1Q (badge "VLAN X"). Les réseaux sont triés : LANs en premier (vlan_id NULL), puis VLANs par ID croissant.

Topologie (TopologyGraph.vue)

Pas de graphe Cytoscape. Layout en cards CSS :

  • Barre d'outils en haut : bouton Ping avec indicateur animé + compteur up/down
  • Carte WAN (rouge) — devices is_livebox
  • Carte Passerelle (jaune) — devices is_gateway
  • Grille réseaux — une card par réseau (LAN ou VLAN), chaque device apparaît dans toutes les cards de ses réseaux (multi-réseau natif)
  • Zone non assigné — devices sans interface réseau, ni livebox, ni gateway
  • Tri : devices triés par IP croissante dans chaque card

Layout des cards

  • Cards pleine largeur, empilées verticalement (flex-col) — toutes de la même largeur, hauteur variable selon le nombre de devices
  • Barre colorée en haut de chaque card (3px, couleur du réseau), bordures latérales neutres
  • Header : badge VLAN/LAN (pill colorée) + nom + CIDR inline + compteur de devices (pill)
  • Chips en grille horizontale (grid auto-fill, minmax(210px, 1fr)) à l'intérieur de chaque card — s'adaptent automatiquement au nombre de colonnes disponibles
  • Responsive : sous 600px, une seule colonne de chips

Device chip

  • Icône de type (Lucide) toujours dans le carré arrondi (28px) avec fond coloré
  • Nom (bold 12px) + IP (pill monospace) + nom d'interface (grisé) + logos de marques (SVG colorés 11px)
  • Tags à droite en colonne : Link (vert, cliquable — tag URL de l'équipement) / GW / LXC / VM
  • Dot de ping : vert (joignable) ou rouge (injoignable), affiché après un ping

Icônes (DeviceIcon.vue + brandIcons.js)

La détection de marque est centralisée dans brandIcons.js (detectBrands(name, description)).

DeviceIcon.vue accepte une prop typeOnly (booléen) :

  • false (défaut) : affiche les logos de marques si détectés, sinon icône Lucide — utilisé dans TopologyGraph
  • true : toujours l'icône Lucide par type — utilisé dans DeviceManager

Dans DeviceManager, les logos de marques apparaissent comme badges colorés séparés (couleur officielle de la marque) dans les meta de la card.

Dans TopologyGraph, les logos de marques apparaissent comme petits SVG 11px colorés dans chip-sub.

Marques supportées via Simple Icons : 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

Pour ajouter une marque : vérifier avec node -e "const si = require('./node_modules/simple-icons'); console.log(Object.keys(si).filter(k => k.toLowerCase().includes('nom')))", importer si<Name> dans brandIcons.js, ajouter une entrée dans BRANDS.

Fallback : Lucide (icône par type d'équipement).

Vue Équipements (DeviceManager.vue)

Barre de filtres + recherche

Barre compacte (28px) combinant :

  • Champ de recherche : filtre par nom, description ou IP en temps réel (Échap pour vider)
  • Filtre Type : dropdown multi-sélection, seuls les types présents dans les données
  • Filtre Réseau : dropdown avec pastille colorée par VLAN/LAN
  • Filtre Marque : dropdown avec icône SVG, trié alphabétiquement
  • Filtre Virt : dropdown, seulement si au moins un équipement a un virt_type

Tous les filtres sont combinables (ET entre catégories, OU à l'intérieur). Bouton "Effacer" + compteur X / total si filtre actif.

Ping (discovery.py + TopologyGraph.vue)

POST /api/discovery/ping — ping ICMP parallèle (50 workers).

  • Body : { ips: ["1.2.3.4", ...] }
  • Retourne : [{ ip, alive }, ...]

Dans la topologie : bouton "Ping" déclenche le ping de toutes les IPs connues. Un équipement est "up" si au moins une de ses IPs répond. Dot vert/rouge sur chaque chip.

Authentification (auth.py + auth.js)

JWT HS256, expiration 24h. Payload : { sub: username, ver: token_version, exp: ... }. Clé secrète dans data/secret_key.txt (permissions 0600, auto-générée) ou SECRET_KEY env var.

Compte par défaut : admin / admin avec must_change_password=1 — créé si la table users est vide. Définir INITIAL_ADMIN_PASSWORD pour un mot de passe personnalisé (sans changement forcé). La migration _migrate_force_admin_password_change() remet must_change_password=1 si l'admin utilise encore le mot de passe bootstrap sur une base existante.

Invalidation de token : le changement de mot de passe incrémente User.token_version. get_current_user rejette tout token dont ver ne correspond pas → invalidation immédiate des sessions précédentes.

Compatibilité bcrypt : passlib 1.7.4 est incompatible avec bcrypt >= 4.0. Le fichier requirements.txt épingle bcrypt==3.2.2. Ne pas mettre à jour bcrypt sans mettre à jour passlib.

Endpoints auth

Méthode Chemin Auth requise Description
POST /api/auth/login Non Form → { access_token, token_type, username, must_change_password } — 429 si rate-limited
PUT /api/auth/account Oui (même si must_change) { current_password, new_username?, new_password? } → nouveau token + must_change_password
GET /api/auth/me Oui { username, must_change_password }

Routeurs métier protégés via dependencies=[Depends(require_password_changed)] (= get_current_user + rejet 403 si must_change_password=True).

Côté frontend

auth.js expose isAuthenticated, currentUsername, mustChangePassword (refs) et setAuth(token, username, mustChange), clearAuth(), getToken(). Persisté dans localStorage.

App.vue guard :

  • !isAuthenticated<LoginPage>
  • mustChangePassword<AccountModal :forced="true"> (bloque l'application)
  • sinon → application complète

Template enveloppé dans <div class="app-root"> (un seul nœud racine) pour éviter le bug Firefox avec l'overlay d'autofill sur les fragments Vue (plusieurs nœuds racines). .app-root utilise display: flex; flex-direction: column; min-height: 100vh — ne pas revenir à display: contents (bug Firefox : les clics ne sont plus dispatché aux enfants après un remplacement du DOM à l'intérieur d'un élément display: contents).

Migrations SQLite (main.py)

main.py exécute des migrations idempotentes au démarrage avant create_all :

  • _migrate_vlan_nullable() — recrée vlans sans contrainte NOT NULL sur vlan_id
  • _migrate_device_virt_type() — ajoute virt_type si absente
  • _migrate_device_url() — ajoute url si absente
  • _migrate_users_must_change_password() — ajoute must_change_password si absente
  • _migrate_users_token_version() — ajoute token_version si absente
  • _migrate_force_admin_password_change() — rattrapage : force must_change si admin utilise le mot de passe bootstrap
  • _migrate_drop_links_table() — supprime la table links (fonctionnalité retirée en phase 3)
  • _migrate_users() — crée users + compte admin si vide

Pour toute nouvelle colonne nullable sur une table existante, ajouter une fonction de migration du même type.

Quand on ajoute un champ à Device : mettre à jour models.py, ajouter la migration dans main.py, ajouter le champ dans DeviceCreate et DeviceOut dans routers/devices.py, l'assigner explicitement dans create_device et update_device.

Découverte automatique (DiscoveryModal.vue)

POST /api/discovery/scan — ping sweep ICMP + lookup PTR DNS contre un serveur DNS configurable.

  • Max 1024 hôtes par VLAN, 100 workers concurrents
  • Retourne : ip, hostname, vlan_id, durée
  • Nécessite cap_add: NET_RAW sur le service backend (déjà configuré)

Thème (theme.js + App.vue)

frontend/src/theme.js exporte theme (ref) et toggleTheme().

  • Valeurs : 'light' (défaut) / 'dark'
  • Persisté dans localStorage('theme')
  • Applique / retire la classe html.dark via watch

Les variables CSS sont définies dans le <style> global de App.vue :

  • :root — thème clair (--bg-page: #F1F5F9, --bg-card: #ffffff, --text-primary: #0F172A, ...)
  • html.dark — thème sombre (--bg-page: #0F172A, --bg-card: #1E293B, --text-primary: #F1F5F9, ...)

Variables disponibles : --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.

Bouton toggle (☾/☀) dans le footer de la sidebar.

Règle : toutes les couleurs de fond/texte/bordure dans les composants doivent utiliser ces variables, pas des valeurs hex hardcodées.

Exception : les couleurs sémantiques fixes (couleur des icônes par type d'équipement dans les chips, couleurs des badges VLAN) restent en hex car elles ne dépendent pas du thème.

Internationalisation (i18n.js)

frontend/src/i18n.js exporte :

  • locale (ref) — langue active, persistée dans localStorage('locale'), défaut 'fr'
  • setLocale(lang) — change la langue
  • t(key) — retourne la traduction, fallback 'fr' puis la clé brute
  • tFmt(key, ...args) — idem avec interpolation {0}, {1}, ...

Langues supportées : français (fr), anglais (en), espagnol (es).

Sélecteur (pills fr / en / es) dans le footer de la sidebar.

Règle : toutes les chaînes visibles dans l'UI passent par t('key') — jamais de texte hardcodé dans les templates. deviceTypes dans DeviceManager est un computed (pas une constante) pour réagir aux changements de locale.

Favicon

frontend/public/favicon.svg — SVG indigo (#6366f1), même forme que le logo sidebar (graphe de nœuds réseau). Référencé dans frontend/index.html : <link rel="icon" type="image/svg+xml" href="/favicon.svg" />.

Données persistées

Volume Docker db_data monté sur /app/data/topology.db (SQLite)