From fd289cc00fef49c71629a0e3106e883f0707dc68 Mon Sep 17 00:00:00 2001 From: Olivier Date: Fri, 22 May 2026 08:52:53 +0200 Subject: [PATCH] feat: auto-detect OS from description; expand OS_LIST to 51 distros MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Remove os field from Device API and form — OS is now detected automatically from name/description via detectOs() in brandIcons.js - Expand OS_LIST from 20 to 51 entries covering all major distros (Debian/Ubuntu flavours, Red Hat, SUSE, Arch, BSD, security distros, Windows/macOS/iOS/Android, generic Linux/BSD catch-alls) - Display detected OS icon in IpAddressing.vue description column - Fix virt_type validator to normalise empty string to null Co-Authored-By: Claude Sonnet 4.6 --- backend/main.py | 14 ++ backend/models.py | 1 + backend/routers/devices.py | 3 +- frontend/package.json | 2 +- frontend/src/App.vue | 9 +- frontend/src/brandIcons.js | 89 ++++++++++- frontend/src/components/DeviceManager.vue | 178 ++-------------------- frontend/src/i18n.js | 18 +++ 8 files changed, 139 insertions(+), 175 deletions(-) diff --git a/backend/main.py b/backend/main.py index ccd7728..5032167 100644 --- a/backend/main.py +++ b/backend/main.py @@ -146,6 +146,19 @@ def _migrate_users(): conn.commit() +def _migrate_device_os(): + """Ajoute la colonne os sur devices si absente.""" + with engine.connect() as conn: + if not conn.execute(text( + "SELECT name FROM sqlite_master WHERE type='table' AND name='devices'" + )).fetchone(): + return + cols = [row[1] for row in conn.execute(text("PRAGMA table_info(devices)")).fetchall()] + if 'os' not in cols: + conn.execute(text("ALTER TABLE devices ADD COLUMN os VARCHAR")) + conn.commit() + + def _migrate_drop_links_table(): """Supprime la table links (fonctionnalité retirée en phase 3). Idempotent.""" with engine.connect() as conn: @@ -162,6 +175,7 @@ def _migrate_drop_links_table(): _migrate_vlan_nullable() _migrate_device_virt_type() _migrate_device_url() +_migrate_device_os() _migrate_users_must_change_password() _migrate_users_token_version() _migrate_force_admin_password_change() diff --git a/backend/models.py b/backend/models.py index 264dd55..86aa1c7 100644 --- a/backend/models.py +++ b/backend/models.py @@ -36,6 +36,7 @@ class Device(Base): is_livebox = Column(Boolean, default=False) virt_type = Column(String, nullable=True) url = Column(String, nullable=True) + os = Column(String, nullable=True) interfaces = relationship( "DeviceInterface", back_populates="device", cascade="all, delete-orphan" diff --git a/backend/routers/devices.py b/backend/routers/devices.py index 7673563..560808e 100644 --- a/backend/routers/devices.py +++ b/backend/routers/devices.py @@ -91,6 +91,8 @@ class DeviceCreate(BaseModel): @field_validator("virt_type") @classmethod def _virt_type(cls, v: Optional[str]) -> Optional[str]: + if not v: + return None if v not in _VALID_VIRT_TYPES: raise ValueError(f"Invalid virt_type: {v!r}. Must be one of: baremetal, lxc, qemu") return v @@ -104,7 +106,6 @@ class DeviceCreate(BaseModel): raise ValueError("url must be a valid http or https URL") return v - class DeviceOut(BaseModel): id: int name: str diff --git a/frontend/package.json b/frontend/package.json index e832067..299d153 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -1,6 +1,6 @@ { "name": "network-topology", - "version": "1.0.1", + "version": "1.1.0", "scripts": { "dev": "vite", "build": "vite build", diff --git a/frontend/src/App.vue b/frontend/src/App.vue index da49c76..3bf2fcc 100644 --- a/frontend/src/App.vue +++ b/frontend/src/App.vue @@ -110,6 +110,7 @@ /> + @@ -124,6 +125,7 @@ import { isAuthenticated, currentUsername, mustChangePassword, setAuth, clearAut import TopologyGraph from './components/TopologyGraph.vue' import VlanManager from './components/VlanManager.vue' import DeviceManager from './components/DeviceManager.vue' +import IpAddressing from './components/IpAddressing.vue' import DiscoveryModal from './components/DiscoveryModal.vue' import LoginPage from './components/LoginPage.vue' import AccountModal from './components/AccountModal.vue' @@ -139,9 +141,10 @@ const showAccount = ref(false) const langs = ['fr', 'en', 'es'] const tabs = computed(() => [ - { id: 'topology', label: t('tabTopology'), icon: '■' }, - { id: 'vlans', label: t('tabNetworks'), icon: '◆' }, - { id: 'devices', label: t('tabDevices'), icon: '▣' }, + { id: 'topology', label: t('tabTopology'), icon: '■' }, + { id: 'vlans', label: t('tabNetworks'), icon: '◆' }, + { id: 'devices', label: t('tabDevices'), icon: '▣' }, + { id: 'addressing', label: t('tabAddressing'), icon: '⊞' }, ]) function onLogin({ token, username, mustChangePassword: mcp }) { diff --git a/frontend/src/brandIcons.js b/frontend/src/brandIcons.js index bdabddc..2e58183 100644 --- a/frontend/src/brandIcons.js +++ b/frontend/src/brandIcons.js @@ -15,6 +15,15 @@ import { siElastic, siKibana, siLogstash, siSplunk, siGraylog, siJaeger, siOpentelemetry, siApple, siDebian, siUbuntu, siFirefox, + siCentos, siRedhat, siFedora, siAlpinelinux, siArchlinux, siOpensuse, siFreebsd, + siAlmalinux, siRockylinux, siNixos, siGentoo, siVoidlinux, siSlackware, + siSuse, siManjaro, siLinuxmint, siZorin, siPopos, siDeepin, siElementary, + siMxlinux, siSolus, siEndeavouros, siArtixlinux, siAsahilinux, + siKubuntu, siLubuntu, siXubuntu, siUbuntumate, + siBsd, siOpenbsd, siNetbsd, + siKalilinux, siParrotsecurity, siTails, siQubesos, siReactos, + siMacos, siIos, siLinux, + siAndroid, siAnsible, siDell, siHp, siRaspberrypi, siArduino, @@ -22,7 +31,7 @@ import { siWordpress, siGhost, siGrav, siJekyll, siHugo, siHexo, siDrupal, siJoomla, siTypo3, siOctobercms, siTextpattern, siMatomo, siPlausibleanalytics, siSamsung, siLg, siSony, siPanasonic, siSharp, siToshiba, siVestel, - siChromecast, siAndroid, siAppletv, siAmazonfiretv, siRoku, siKodi, + siChromecast, siAppletv, siAmazonfiretv, siRoku, siKodi, siJellyfin, siHomeassistant, siPhilipshue, siXiaomi, siRadarr, siSonarr, siTransmission, siExcalidraw, @@ -295,6 +304,84 @@ const BRANDS = [ { kw: ['xiaomi', 'mi home', 'yeelight'], icon: siXiaomi }, ] +export const OS_LIST = [ + // Infrastructure + { value: 'proxmox', label: 'Proxmox VE', kw: ['proxmox', 'pve'], icon: siProxmox }, + { value: 'truenas', label: 'TrueNAS', kw: ['truenas', 'freenas'], icon: siTruenas }, + { value: 'synology', label: 'Synology DSM', kw: ['synology', 'dsm'], icon: siSynology }, + // Réseau / pare-feu + { value: 'openwrt', label: 'OpenWrt', kw: ['openwrt'], icon: siOpenwrt }, + { value: 'pfsense', label: 'pfSense', kw: ['pfsense'], icon: siPfsense }, + { value: 'opnsense', label: 'OPNsense', kw: ['opnsense'], icon: siOpnsense }, + // SBC + { value: 'raspbian', label: 'Raspberry Pi OS', kw: ['raspbian', 'raspberry pi os', 'pi os'], icon: siRaspberrypi }, + // Debian / Ubuntu + { value: 'ubuntu', label: 'Ubuntu', kw: ['ubuntu'], icon: siUbuntu }, + { value: 'kubuntu', label: 'Kubuntu', kw: ['kubuntu'], icon: siKubuntu }, + { value: 'lubuntu', label: 'Lubuntu', kw: ['lubuntu'], icon: siLubuntu }, + { value: 'xubuntu', label: 'Xubuntu', kw: ['xubuntu'], icon: siXubuntu }, + { value: 'ubuntumate', label: 'Ubuntu MATE', kw: ['ubuntu mate', 'ubuntumate'], icon: siUbuntumate }, + { value: 'debian', label: 'Debian', kw: ['debian'], icon: siDebian }, + // Red Hat / CentOS + { value: 'redhat', label: 'Red Hat', kw: ['red hat', 'redhat', 'rhel'], icon: siRedhat }, + { value: 'centos', label: 'CentOS', kw: ['centos'], icon: siCentos }, + { value: 'almalinux', label: 'AlmaLinux', kw: ['almalinux', 'alma linux'], icon: { ...siAlmalinux, hex: '4d4d4d' } }, + { value: 'rockylinux', label: 'Rocky Linux', kw: ['rockylinux', 'rocky linux', 'rocky'], icon: siRockylinux }, + { value: 'fedora', label: 'Fedora', kw: ['fedora'], icon: siFedora }, + // SUSE + { value: 'opensuse', label: 'openSUSE', kw: ['opensuse', 'open suse'], icon: siOpensuse }, + { value: 'suse', label: 'SUSE Linux Enterprise', kw: ['sles', 'suse linux enterprise'], icon: { ...siSuse, hex: '73BA25' } }, + // Arch + { value: 'arch', label: 'Arch Linux', kw: ['arch linux', 'archlinux'], icon: siArchlinux }, + { value: 'manjaro', label: 'Manjaro', kw: ['manjaro'], icon: siManjaro }, + { value: 'endeavouros', label: 'EndeavourOS', kw: ['endeavouros', 'endeavour os'], icon: siEndeavouros }, + { value: 'artixlinux', label: 'Artix Linux', kw: ['artixlinux', 'artix linux', 'artix'], icon: siArtixlinux }, + // Alpine + { value: 'alpine', label: 'Alpine Linux', kw: ['alpine linux', 'alpine'], icon: siAlpinelinux }, + // Divers + { value: 'nixos', label: 'NixOS', kw: ['nixos', 'nix os'], icon: siNixos }, + { value: 'gentoo', label: 'Gentoo', kw: ['gentoo'], icon: siGentoo }, + { value: 'voidlinux', label: 'Void Linux', kw: ['voidlinux', 'void linux'], icon: siVoidlinux }, + { value: 'slackware', label: 'Slackware', kw: ['slackware'], icon: { ...siSlackware, hex: '4d4d4d' } }, + { value: 'asahilinux', label: 'Asahi Linux', kw: ['asahi linux', 'asahi'], icon: siAsahilinux }, + // Bureau + { value: 'linuxmint', label: 'Linux Mint', kw: ['linux mint', 'linuxmint'], icon: siLinuxmint }, + { value: 'popos', label: 'Pop!_OS', kw: ["pop!_os", 'popos', 'pop os'], icon: siPopos }, + { value: 'zorin', label: 'Zorin OS', kw: ['zorin'], icon: siZorin }, + { value: 'elementary', label: 'elementary OS', kw: ['elementary os', 'elementaryos'], icon: siElementary }, + { value: 'deepin', label: 'deepin', kw: ['deepin'], icon: siDeepin }, + { value: 'mxlinux', label: 'MX Linux', kw: ['mx linux', 'mxlinux'], icon: { ...siMxlinux, hex: '4d4d4d' } }, + { value: 'solus', label: 'Solus', kw: ['solus'], icon: siSolus }, + // BSD + { value: 'freebsd', label: 'FreeBSD', kw: ['freebsd', 'free bsd'], icon: siFreebsd }, + { value: 'openbsd', label: 'OpenBSD', kw: ['openbsd'], icon: siOpenbsd }, + { value: 'netbsd', label: 'NetBSD', kw: ['netbsd'], icon: siNetbsd }, + // Sécurité + { value: 'kali', label: 'Kali Linux', kw: ['kali linux', 'kalilinux'], icon: siKalilinux }, + { value: 'parrot', label: 'Parrot Security', kw: ['parrot security', 'parrotsec', 'parrot os'], icon: siParrotsecurity }, + { value: 'tails', label: 'Tails', kw: ['tails os', 'tails linux'], icon: siTails }, + { value: 'qubes', label: 'Qubes OS', kw: ['qubes os', 'qubesos'], icon: siQubesos }, + // Windows / macOS / mobile + { value: 'windows', label: 'Windows', kw: ['windows', 'win10', 'win11', 'winserver', 'windows server'], icon: ICON_WINDOWS }, + { value: 'macos', label: 'macOS', kw: ['macos', 'mac os'], icon: { ...siMacos, hex: '555555' } }, + { value: 'ios', label: 'iOS / iPadOS', kw: ['ios', 'ipados'], icon: { ...siIos, hex: '555555' } }, + { value: 'android', label: 'Android', kw: ['android'], icon: siAndroid }, + // Autres + { value: 'reactos', label: 'ReactOS', kw: ['reactos'], icon: siReactos }, + { value: 'linux', label: 'Linux (générique)', kw: ['linux'], icon: siLinux }, + { value: 'bsd', label: 'BSD (générique)', kw: ['bsd'], icon: siBsd }, +] + +export function osIcon(value) { + return OS_LIST.find(o => o.value === value) ?? null +} + +export function detectOs(name, description) { + const text = ((name || '') + ' ' + (description || '')).toLowerCase() + if (!text.trim()) return null + return OS_LIST.find(o => o.kw.some(kw => text.includes(kw))) ?? null +} + export function detectBrands(name, description) { const text = ((name || '') + ' ' + (description || '')).toLowerCase() if (!text.trim()) return [] diff --git a/frontend/src/components/DeviceManager.vue b/frontend/src/components/DeviceManager.vue index 573f595..989f38d 100644 --- a/frontend/src/components/DeviceManager.vue +++ b/frontend/src/components/DeviceManager.vue @@ -149,90 +149,21 @@ - +