Files
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

9.3 KiB

Frontend Reference

Stack

  • Vue 3 (Composition API, <script setup>)
  • Vite 5 (build + dev server)
  • Axios (HTTP client)
  • lucide-vue-next (generic UI icons)
  • simple-icons (brand logos as inline SVG)

No Pinia, no Vue Router (single page, tab switching via reactive state in App.vue).


Module Inventory

src/main.js

Minimal entry point: creates the Vue app and mounts it to #app.


src/auth.js

Reactive authentication state. All consuming components import from here — there is no Vuex/Pinia store.

// Exports
isAuthenticated  // computed ref<boolean> — true if token is present
currentUsername  // computed ref<string | null>

setAuth(token, username)   // stores in reactive refs + localStorage
clearAuth()                // clears refs + localStorage
getToken()                 // returns raw token string (used by api.js interceptor)

State is initialised from localStorage at module load, so auth survives page refreshes.


src/api.js

Thin axios wrapper. All requests go to /api (relative URL, proxied by Nginx).

Request interceptor: injects Authorization: Bearer <token> if getToken() is non-null.

Response interceptor: on 401, calls clearAuth() and reloads the page — but only if a token was present (avoids an infinite reload loop if the login endpoint itself returns 401).

// Named exports
authApi     // login, updateAccount, me
vlansApi    // list, create, update, remove
devicesApi  // list, create, update, remove
linksApi    // list, create, remove
discoveryApi // scan, ping

authApi.login sends application/x-www-form-urlencoded (required by FastAPI's OAuth2PasswordRequestForm). All other calls use JSON.


src/i18n.js

Simple key-based i18n, no external library.

// Exports
locale      // ref<'fr'|'en'|'es'> — persisted in localStorage, default 'fr'
setLocale(lang)
t(key)      // returns translation, fallback to 'fr', then raw key
tFmt(key, ...args)  // t() with {0}, {1}, ... interpolation

Translations are plain objects keyed by locale code. To add a key: add it to all three locale objects. To add a language: add a new locale object and add the code to the switcher pills in App.vue.

Rule: every visible string in a template must use t('key'). Never hardcode text.


src/theme.js

// Exports
theme        // ref<'light'|'dark'> — persisted in localStorage, default 'light'
toggleTheme()

Applies/removes the html.dark class on document.documentElement. All dark-mode styling is driven by CSS custom properties defined in App.vue's global <style>.


src/brandIcons.js

// Exports
BRANDS            // array of { id, name, keywords[], hex, path }
detectBrands(name, description)  // returns BRANDS entries matching the device

detectBrands is a pure function — it takes name and description strings and returns all matching brand objects. It is called in both DeviceIcon.vue and DeviceManager.vue.

To add a brand:

  1. Check the simple-icons export name: node -e "const si = require('./node_modules/simple-icons'); console.log(Object.keys(si).filter(k => k.toLowerCase().includes('yourterm')))"
  2. Import si<Name> from simple-icons
  3. Add an entry to BRANDS: { id: 'yourbrand', name: 'YourBrand', keywords: ['keyword1', 'keyword2'], hex: si<Name>.hex, path: si<Name>.path }

Component Inventory

App.vue

Global layout and state. Single-file, no sub-layouts.

Auth guard: v-if="!isAuthenticated" renders <LoginPage>, v-else renders the main layout. The template is wrapped in <div class="app-root"> (display: flex; flex-direction: column; min-height: 100vh) — single root node to prevent a Firefox autofill overlay bug on Vue fragment comment nodes. Do not use display: contents here: Firefox fails to update click hit-testing after a child replacement inside a display: contents element, causing buttons to be unclickable after login.

Global state (reactive refs at the top of <script setup>):

  • vlans — array loaded from GET /api/vlans/
  • devices — array loaded from GET /api/devices/
  • activeTab'topology' | 'devices' | 'vlans'

Auth functions: onLogin({ token, username }), onAccountUpdated({ token, username }), logout() — all call setAuth/clearAuth from auth.js.

CSS variables are defined here in the global (non-scoped) <style> block:

  • :root — light theme
  • html.dark — dark theme overrides

LoginPage.vue

Full-screen centered login card. No router navigation — it is shown/hidden by App.vue's auth guard.

Props: none
Emits: login with payload { token: string, username: string }

Calls authApi.login(username, password). On success, emits the token. On failure, shows a localised error message.


AccountModal.vue

Modal for changing username and/or password.

Props: none (reads currentUsername directly from auth.js)
Emits: close, updated with payload { token: string, username: string }

Fields: new username (optional), new password (optional), confirm password (shown only when new password is typed), current password (always required).

Calls authApi.updateAccount({ current_password, new_username?, new_password? }). On success, emits the new token so App.vue can refresh auth state.


TopologyGraph.vue

Topology view. CSS cards only — no SVG, no Cytoscape.

Props: vlans[], devices[]
Emits: none (read-only view)

Layout:

toolbar (Ping button + up/down counter)
├── WAN card (is_livebox devices)
├── Gateway card (is_gateway devices)
├── [one card per network, sorted: LANs first then VLANs ascending]
└── Unassigned card (no interface, not livebox, not gateway)

Inside each card, devices are sorted by IP address ascending. Devices with no IP go last. A device appears in every card that matches one of its interfaces.

Each device is rendered as a chip — a compact tile with:

  • Left: type icon (Lucide, always, typeOnly prop)
  • Center: name + IP pill + interface name + brand SVG logos (11px)
  • Right tags column: Link (green, <a>) | GW | LXC | VM | ping dot

Ping: POST /api/discovery/ping with all known IPs. Populates pingResults reactive map { ip → alive }. A device is "up" if any of its IPs maps to alive: true.

Dark mode CSS: scoped styles use :global(html.dark .special-wan) etc. The full selector must be inside :global() — Vue's scoped CSS transformer does not resolve descendant selectors after the closing parenthesis.


DeviceIcon.vue

Props: device (object with name, description, type), typeOnly (boolean, default false)

  • typeOnly: false — shows brand SVG logo(s) if detectBrands() finds a match, else falls back to the Lucide type icon. Used in topology chips.
  • typeOnly: true — always shows the Lucide type icon. Used in DeviceManager cards.

Lucide icon is selected by a type → component map covering all 18 device types.


DeviceManager.vue

CRUD view for devices. Presented as a card list.

Props: vlans[], devices[]
Emits: refresh (triggers parent reload of both vlans and devices)

Filter bar (28px height):

  • Free-text search (name, description, IP) — cleared by Escape
  • Type multi-select (only types present in data)
  • Network dropdown (color swatch + name)
  • Brand dropdown (SVG icon + name, alphabetical)
  • Virt dropdown (only shown if at least one device has a virt_type)

Filters combine with AND across categories, OR within. Active filter shows X / total counter and a "Clear" button.

deviceTypes is a computed (not a constant) so labels re-render on locale change.

Form: inline panel (not a modal). Supports add and edit. Interfaces are managed as a sub-list within the same form.


VlanManager.vue

CRUD view for networks (VLANs and plain LANs).

Props: vlans[]
Emits: refresh

vlan_id is optional. If omitted, the network is a plain LAN (badge "LAN"). If provided, it is a VLAN (badge "VLAN X"). Color picker included. CIDR is a free-text field with no validation.


DiscoveryModal.vue

Auto-discovery modal: ping sweep + DNS PTR lookup.

Props: vlans[]
Emits: close, import(hosts[]) — parent handles inserting discovered hosts

Lets the user select VLANs to scan and configure the DNS server. Shows progress and results. Discovered hosts can be selectively imported.


CSS Conventions

All background, text, and border colors use CSS custom properties from App.vue:

--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

Exceptions (hardcoded hex is acceptable):

  • Device type icon colors in chips (semantic, theme-independent)
  • VLAN badge background colors
  • Auth feedback colors (#EF4444 red, #15803D green) with dark overrides via :global(html.dark .class)

Dark-mode scoped override syntax:

/* CORRECT */
:global(html.dark .my-component .child) { color: red; }

/* WRONG — Vue does not resolve descendants after :global() closing paren */
:global(html.dark) .my-component .child { color: red; }