/** * Store Pinia pour la gestion des IPs */ import { defineStore } from 'pinia' import { ref, computed } from 'vue' import axios from 'axios' export const useIPStore = defineStore('ip', () => { // État const ips = ref([]) const selectedIP = ref(null) const loading = ref(false) const error = ref(null) const stats = ref({ total: 0, online: 0, offline: 0, known: 0, unknown: 0 }) const searchQuery = ref('') const invertSearch = ref(false) const lastScanDate = ref(null) const scanProgress = ref({ current: 0, total: 0, currentIP: null }) const scanLogs = ref([]) const isScanning = ref(false) const uiConfig = ref({ cell_size: 30, architecture_title_font_size: 18 }) const configReloadTick = ref(0) // WebSocket const ws = ref(null) const wsConnected = ref(false) // Computed const filteredIPs = computed(() => { const { tokens, flags } = parseSearchQuery(searchQuery.value) return ips.value.filter(ip => { if (flags.requireOnline && ip.last_status !== 'online') return false if (flags.requireOffline && ip.last_status !== 'offline') return false if (flags.requireFree && ip.last_status) return false if (flags.requireKnown && !ip.known) return false if (flags.requireUnknown && ip.known) return false if (flags.requireTracked && !ip.tracked) return false if (flags.requireVm && !ip.vm) return false if (flags.requireHardwareBench && !ip.hardware_bench) return false let matches = true if (tokens.length === 0) { matches = true } else { const haystack = normalizeText([ ip.ip, ip.name, ip.hostname, ip.host, ip.location, ip.mac, ip.vendor, ip.link, ip.last_status, ip.tracked ? 'suivie' : '', ip.vm ? 'vm' : '', ip.hardware_bench ? 'hardware' : '', (ip.open_ports || []).join(' ') ].filter(Boolean).join(' ')) // Match si au moins un mot est présent matches = tokens.some(token => haystack.includes(token)) } return invertSearch.value ? !matches : matches }) }) // Actions async function fetchUIConfig() { try { const response = await axios.get('/api/config/ui') uiConfig.value = response.data // Appliquer la taille des cellules, de la police et de l'espacement via variables CSS document.documentElement.style.setProperty('--cell-size', `${response.data.cell_size}px`) document.documentElement.style.setProperty('--font-size', `${response.data.font_size}px`) document.documentElement.style.setProperty('--cell-gap', `${response.data.cell_gap}px`) document.documentElement.style.setProperty( '--arch-title-size', `${response.data.architecture_title_font_size}px` ) } catch (err) { console.error('Erreur chargement config UI:', err) } } async function reloadConfig() { try { const response = await axios.post('/api/config/reload') if (response.data.success) { // Appliquer la nouvelle config UI uiConfig.value = response.data.ui document.documentElement.style.setProperty('--cell-size', `${response.data.ui.cell_size}px`) document.documentElement.style.setProperty('--font-size', `${response.data.ui.font_size}px`) document.documentElement.style.setProperty('--cell-gap', `${response.data.ui.cell_gap}px`) document.documentElement.style.setProperty( '--arch-title-size', `${response.data.ui.architecture_title_font_size}px` ) return response.data.message } } catch (err) { console.error('Erreur rechargement config:', err) throw err } } function bumpConfigReload() { configReloadTick.value += 1 } async function fetchIPs() { loading.value = true error.value = null try { const response = await axios.get('/api/ips/') ips.value = response.data await fetchStats() } catch (err) { error.value = err.message console.error('Erreur chargement IPs:', err) } finally { loading.value = false } } async function fetchStats() { try { const response = await axios.get('/api/ips/stats/summary') stats.value = response.data } catch (err) { console.error('Erreur chargement stats:', err) } } async function updateIP(ipAddress, data) { try { const response = await axios.put(`/api/ips/${ipAddress}`, data) // Mettre à jour dans le store const index = ips.value.findIndex(ip => ip.ip === ipAddress) if (index !== -1) { ips.value[index] = response.data } if (selectedIP.value?.ip === ipAddress) { selectedIP.value = response.data } return response.data } catch (err) { error.value = err.message throw err } } async function deleteIP(ipAddress) { try { await axios.delete(`/api/ips/${ipAddress}`) // Retirer du store const index = ips.value.findIndex(ip => ip.ip === ipAddress) if (index !== -1) { ips.value.splice(index, 1) } if (selectedIP.value?.ip === ipAddress) { selectedIP.value = null } await fetchStats() } catch (err) { error.value = err.message throw err } } async function getIPHistory(ipAddress, hours = 24) { try { const response = await axios.get(`/api/ips/${ipAddress}/history?hours=${hours}`) return response.data } catch (err) { console.error('Erreur chargement historique:', err) throw err } } async function startScan() { try { await axios.post('/api/scan/start') } catch (err) { error.value = err.message throw err } } function selectIP(ip) { selectedIP.value = ip } function clearSelection() { selectedIP.value = null } // WebSocket function connectWebSocket() { const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:' const wsUrl = `${protocol}//${window.location.host}/ws` ws.value = new WebSocket(wsUrl) ws.value.onopen = () => { console.log('WebSocket connecté') wsConnected.value = true // Heartbeat toutes les 30s setInterval(() => { if (ws.value?.readyState === WebSocket.OPEN) { ws.value.send('ping') } }, 30000) } ws.value.onmessage = (event) => { // Ignorer les messages ping/pong (texte brut) if (typeof event.data === 'string' && (event.data === 'ping' || event.data === 'pong')) { return } try { const message = JSON.parse(event.data) handleWebSocketMessage(message) } catch (err) { // Ignorer silencieusement les erreurs de parsing pour les messages non-JSON } } ws.value.onerror = (error) => { // Erreur WebSocket - ne pas logger si c'est juste une déconnexion normale wsConnected.value = false } ws.value.onclose = (event) => { wsConnected.value = false // Ne logger que si c'est une fermeture anormale if (!event.wasClean) { console.log('WebSocket déconnecté - reconnexion dans 5s...') } // Reconnexion après 5s setTimeout(() => { // Ne reconnecter que si on n'est pas déjà connecté if (!ws.value || ws.value.readyState === WebSocket.CLOSED) { connectWebSocket() } }, 5000) } } function handleWebSocketMessage(message) { // Logger uniquement les messages importants (pas scan_progress pour éviter le spam) if (message.type !== 'scan_progress') { console.log('WebSocket:', message.type) } switch (message.type) { case 'scan_start': // Notification début de scan isScanning.value = true scanLogs.value = [] scanProgress.value = { current: 0, total: message.total || 0, currentIP: null } break case 'scan_progress': // Progression du scan if (message.current) scanProgress.value.current = message.current if (message.total) scanProgress.value.total = message.total if (message.ip) scanProgress.value.currentIP = message.ip break case 'scan_complete': // Rafraîchir les données après scan isScanning.value = false lastScanDate.value = new Date() scanProgress.value = { current: 0, total: 0, currentIP: null } fetchIPs() if (message.stats) stats.value = message.stats break case 'ip_update': // Mise à jour d'une IP const updatedIP = ips.value.find(ip => ip.ip === message.data.ip) if (updatedIP) { Object.assign(updatedIP, message.data) } break case 'new_ip': // Nouvelle IP détectée fetchIPs() // Recharger pour être sûr break case 'scan_log': if (message.message) { scanLogs.value.push(message.message) if (scanLogs.value.length > 200) { scanLogs.value.splice(0, scanLogs.value.length - 200) } } break } } function disconnectWebSocket() { if (ws.value) { ws.value.close() ws.value = null wsConnected.value = false } } return { // État ips, selectedIP, loading, error, stats, searchQuery, invertSearch, wsConnected, lastScanDate, scanProgress, scanLogs, isScanning, uiConfig, configReloadTick, // Computed filteredIPs, // Actions fetchUIConfig, reloadConfig, bumpConfigReload, fetchIPs, fetchStats, updateIP, deleteIP, getIPHistory, startScan, selectIP, clearSelection, connectWebSocket, disconnectWebSocket } }) function normalizeText(value) { return String(value ?? '') .normalize('NFD') .replace(/[\u0300-\u036f]/g, '') .toLowerCase() .trim() } function parseSearchQuery(query) { let normalized = normalizeText(query) const flags = { requireOnline: false, requireOffline: false, requireKnown: false, requireUnknown: false, requireFree: false, requireTracked: false, requireVm: false, requireHardwareBench: false } const onlinePattern = /\ben\s+ligne\b/g const offlinePattern = /\bhors\s+ligne\b/g if (onlinePattern.test(normalized)) flags.requireOnline = true if (offlinePattern.test(normalized)) flags.requireOffline = true normalized = normalized .replace(onlinePattern, ' ') .replace(offlinePattern, ' ') let tokens = normalized.split(/\s+/).filter(Boolean) tokens = tokens.filter(token => { if (token === 'connue') { flags.requireKnown = true return false } if (token === 'inconnue') { flags.requireUnknown = true return false } if (token === 'libre') { flags.requireFree = true return false } if (token === 'suivie' || token === 'suivi') { flags.requireTracked = true return false } if (token === 'vm') { flags.requireVm = true return false } if (token === 'hardware' || token === 'bench' || token === 'hardware_bench') { flags.requireHardwareBench = true return false } if (token === 'enligne' || token === 'en-ligne') { flags.requireOnline = true return false } if (token === 'horsligne' || token === 'hors-ligne') { flags.requireOffline = true return false } return true }) return { tokens, flags } }