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>
263 lines
9.3 KiB
Markdown
263 lines
9.3 KiB
Markdown
# 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.
|
|
|
|
```javascript
|
|
// 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).
|
|
|
|
```javascript
|
|
// 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.
|
|
|
|
```javascript
|
|
// 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`
|
|
|
|
```javascript
|
|
// 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`
|
|
|
|
```javascript
|
|
// 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**:
|
|
```css
|
|
/* 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; }
|
|
```
|