scan port
This commit is contained in:
@@ -69,6 +69,15 @@
|
||||
<span class="mdi mdi-star"></span> Suivi
|
||||
</button>
|
||||
|
||||
<!-- Bouton Ports & Services -->
|
||||
<button
|
||||
@click="goToServices"
|
||||
class="px-3 py-1.5 rounded bg-monokai-cyan/80 text-monokai-bg text-sm font-bold hover:bg-monokai-cyan transition-colors"
|
||||
title="Scanner les services réseau"
|
||||
>
|
||||
<span class="mdi mdi-lan-check"></span> Ports & Services
|
||||
</button>
|
||||
|
||||
<!-- Bouton Architecture -->
|
||||
<button
|
||||
@click="goToArchitecture"
|
||||
@@ -158,6 +167,10 @@ function goToTracking() {
|
||||
router.push('/tracking')
|
||||
}
|
||||
|
||||
function goToServices() {
|
||||
router.push('/services')
|
||||
}
|
||||
|
||||
function goToArchitecture() {
|
||||
router.push('/architecture')
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ import MainView from '@/views/MainView.vue'
|
||||
import TrackingView from '@/views/TrackingView.vue'
|
||||
import ArchitectureView from '@/views/ArchitectureView.vue'
|
||||
import TestView from '@/views/TestView.vue'
|
||||
import ServicesView from '@/views/ServicesView.vue'
|
||||
|
||||
const routes = [
|
||||
{
|
||||
@@ -33,6 +34,14 @@ const routes = [
|
||||
title: 'IPWatch - Architecture réseau'
|
||||
}
|
||||
},
|
||||
{
|
||||
path: '/services',
|
||||
name: 'services',
|
||||
component: ServicesView,
|
||||
meta: {
|
||||
title: 'IPWatch - Ports & Services'
|
||||
}
|
||||
},
|
||||
{
|
||||
path: '/test',
|
||||
name: 'test',
|
||||
|
||||
@@ -348,6 +348,7 @@ export const useIPStore = defineStore('ip', () => {
|
||||
stats,
|
||||
searchQuery,
|
||||
invertSearch,
|
||||
ws,
|
||||
wsConnected,
|
||||
lastScanDate,
|
||||
scanProgress,
|
||||
|
||||
567
frontend/src/views/ServicesView.vue
Normal file
567
frontend/src/views/ServicesView.vue
Normal file
@@ -0,0 +1,567 @@
|
||||
<template>
|
||||
<div class="h-screen flex flex-col bg-monokai-bg">
|
||||
<!-- Header -->
|
||||
<header class="bg-monokai-bg border-b-2 border-monokai-comment p-4">
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="flex items-center gap-4">
|
||||
<h1 class="text-3xl font-bold text-monokai-green">IPWatch</h1>
|
||||
<div class="flex flex-col">
|
||||
<span class="text-monokai-comment">Ports & Services</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center gap-6">
|
||||
<SystemStats />
|
||||
|
||||
<div class="h-8 w-px bg-monokai-comment"></div>
|
||||
|
||||
<button
|
||||
@click="goToDashboard"
|
||||
class="px-4 py-2 rounded bg-monokai-yellow text-monokai-bg font-bold hover:bg-monokai-orange transition-colors"
|
||||
title="Retour au dashboard"
|
||||
>
|
||||
<span class="mdi mdi-arrow-left"></span> Dashboard
|
||||
</button>
|
||||
|
||||
<button
|
||||
@click="openSettings"
|
||||
class="px-4 py-2 rounded bg-monokai-purple text-monokai-bg text-sm hover:bg-monokai-pink transition-colors"
|
||||
title="Ouvrir les paramètres"
|
||||
>
|
||||
<span class="mdi mdi-cog"></span> Paramètres
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<!-- Layout 3 colonnes -->
|
||||
<div class="flex-1 flex overflow-hidden">
|
||||
|
||||
<!-- Volet gauche : Liste des services -->
|
||||
<div class="w-80 flex-shrink-0 border-r border-monokai-comment flex flex-col">
|
||||
<div class="p-4 border-b border-monokai-comment">
|
||||
<h2 class="text-lg font-bold text-monokai-cyan flex items-center gap-2">
|
||||
<span class="mdi mdi-server-network"></span>
|
||||
Services
|
||||
<span class="text-xs text-monokai-comment font-normal">({{ selectedCount }}/{{ services.length }})</span>
|
||||
</h2>
|
||||
</div>
|
||||
|
||||
<!-- Liste scrollable des services -->
|
||||
<div class="flex-1 overflow-auto p-2">
|
||||
<div
|
||||
v-for="service in services"
|
||||
:key="service.name + service.port"
|
||||
class="flex items-center gap-2 px-3 py-1.5 rounded hover:bg-monokai-comment/20 cursor-pointer transition-colors"
|
||||
@click="toggleService(service)"
|
||||
>
|
||||
<input
|
||||
type="checkbox"
|
||||
:checked="isSelected(service)"
|
||||
class="form-checkbox accent-monokai-cyan"
|
||||
@click.stop
|
||||
@change="toggleService(service)"
|
||||
/>
|
||||
<span class="flex-1 text-sm text-monokai-text truncate">{{ service.name }}</span>
|
||||
<span class="text-xs px-1.5 py-0.5 rounded bg-monokai-comment/30 text-monokai-comment font-mono">
|
||||
{{ service.port }}
|
||||
</span>
|
||||
<span v-if="service.protocol" class="text-xs text-monokai-purple">
|
||||
{{ service.protocol }}
|
||||
</span>
|
||||
</div>
|
||||
<div v-if="services.length === 0" class="text-center text-monokai-comment text-sm p-4">
|
||||
Aucun service configuré dans config.yaml
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Boutons de sélection -->
|
||||
<div class="p-3 border-t border-monokai-comment flex gap-2">
|
||||
<button
|
||||
@click="selectAll"
|
||||
class="flex-1 px-2 py-1.5 rounded text-xs font-bold bg-monokai-comment/30 text-monokai-text hover:bg-monokai-comment/50 transition-colors"
|
||||
>
|
||||
Tout cocher
|
||||
</button>
|
||||
<button
|
||||
@click="selectNone"
|
||||
class="flex-1 px-2 py-1.5 rounded text-xs font-bold bg-monokai-comment/30 text-monokai-text hover:bg-monokai-comment/50 transition-colors"
|
||||
>
|
||||
Tout décocher
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Zone centrale : Scan et résultats -->
|
||||
<div class="flex-1 flex flex-col overflow-hidden">
|
||||
|
||||
<!-- Barre d'action -->
|
||||
<div class="p-4 border-b border-monokai-comment">
|
||||
<div class="flex items-center gap-4">
|
||||
<button
|
||||
@click="launchScan"
|
||||
:disabled="selectedCount === 0 || isScanning"
|
||||
class="px-4 py-2 rounded font-bold transition-colors"
|
||||
:class="selectedCount === 0 || isScanning
|
||||
? 'bg-monokai-comment/30 text-monokai-comment cursor-not-allowed'
|
||||
: 'bg-monokai-cyan text-monokai-bg hover:bg-monokai-green'"
|
||||
>
|
||||
<span class="mdi mdi-magnify-scan"></span>
|
||||
{{ isScanning ? 'Scan en cours...' : 'Lancer le scan' }}
|
||||
</button>
|
||||
|
||||
<!-- Progression -->
|
||||
<div v-if="isScanning" class="flex items-center gap-3 flex-1">
|
||||
<div class="flex-1 h-2 bg-monokai-comment/30 rounded-full overflow-hidden">
|
||||
<div
|
||||
class="h-full bg-monokai-cyan transition-all duration-300 rounded-full"
|
||||
:style="{ width: progressPercent + '%' }"
|
||||
></div>
|
||||
</div>
|
||||
<span class="text-xs text-monokai-cyan whitespace-nowrap">
|
||||
{{ scanProgress.current }} / {{ scanProgress.total }}
|
||||
</span>
|
||||
<span v-if="scanProgress.ip" class="text-xs text-monokai-comment">
|
||||
{{ scanProgress.ip }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<!-- Stats résultat -->
|
||||
<div v-if="!isScanning && results.length > 0" class="text-sm text-monokai-comment">
|
||||
{{ filteredResults.length }} service(s) détecté(s)
|
||||
<span v-if="filterText" class="text-monokai-purple">(filtré)</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Message informatif sous le scan -->
|
||||
<div v-if="isScanning" class="mt-2 text-xs text-monokai-comment flex items-center gap-2">
|
||||
<span class="mdi mdi-information-outline text-monokai-cyan"></span>
|
||||
Scan de {{ selectedCount }} service(s) sur l'ensemble des IPs connues du réseau.
|
||||
Chaque IP est testée avec un timeout de {{ scanTimeout }}s.
|
||||
<span v-if="scanProgress.total > 0" class="text-monokai-yellow">
|
||||
Temps estimé : ~{{ estimatedTime }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Tableau de résultats -->
|
||||
<div v-if="results.length > 0 || isScanning" class="flex-1 overflow-auto">
|
||||
<!-- Filtre texte -->
|
||||
<div class="px-4 py-2 sticky top-0 bg-monokai-bg border-b border-monokai-comment/50 z-10">
|
||||
<input
|
||||
v-model="filterText"
|
||||
type="text"
|
||||
placeholder="Filtrer les résultats (IP, nom, service...)"
|
||||
class="w-full px-3 py-1.5 rounded bg-monokai-comment/20 text-monokai-text text-sm border border-monokai-comment/30 focus:border-monokai-cyan focus:outline-none"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<table class="w-full text-sm">
|
||||
<thead class="sticky top-[41px] bg-monokai-bg z-10">
|
||||
<tr class="text-monokai-comment border-b border-monokai-comment">
|
||||
<th class="text-left px-4 py-2 cursor-pointer hover:text-monokai-text" @click="sortBy('ip')">
|
||||
IP {{ sortIcon('ip') }}
|
||||
</th>
|
||||
<th class="text-left px-4 py-2 cursor-pointer hover:text-monokai-text" @click="sortBy('name')">
|
||||
Nom {{ sortIcon('name') }}
|
||||
</th>
|
||||
<th class="text-left px-4 py-2 cursor-pointer hover:text-monokai-text" @click="sortBy('service_name')">
|
||||
Service {{ sortIcon('service_name') }}
|
||||
</th>
|
||||
<th class="text-left px-4 py-2 cursor-pointer hover:text-monokai-text" @click="sortBy('port')">
|
||||
Port {{ sortIcon('port') }}
|
||||
</th>
|
||||
<th class="text-left px-4 py-2">Lien</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr
|
||||
v-for="(row, index) in filteredResults"
|
||||
:key="row.ip + '-' + row.port"
|
||||
class="border-b border-monokai-comment/20 hover:bg-monokai-comment/10 transition-colors"
|
||||
:class="index % 2 === 0 ? 'bg-monokai-bg' : 'bg-monokai-comment/5'"
|
||||
>
|
||||
<td class="px-4 py-2 font-mono text-monokai-cyan">{{ row.ip }}</td>
|
||||
<td class="px-4 py-2 text-monokai-text">{{ row.name || row.hostname || '-' }}</td>
|
||||
<td class="px-4 py-2">
|
||||
<span class="px-2 py-0.5 rounded text-xs font-bold"
|
||||
:class="serviceColor(row.protocol)">
|
||||
{{ row.service_name }}
|
||||
</span>
|
||||
</td>
|
||||
<td class="px-4 py-2 font-mono text-monokai-purple">{{ row.port }}</td>
|
||||
<td class="px-4 py-2">
|
||||
<a
|
||||
v-if="row.url"
|
||||
:href="row.url"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
class="text-monokai-green hover:text-monokai-yellow underline flex items-center gap-1"
|
||||
>
|
||||
<span class="mdi mdi-open-in-new text-xs"></span>
|
||||
{{ row.url }}
|
||||
</a>
|
||||
<span v-else-if="row.protocol" class="text-monokai-comment text-xs">
|
||||
{{ row.protocol }}://{{ row.ip }}:{{ row.port }}
|
||||
</span>
|
||||
<span v-else class="text-monokai-comment text-xs">{{ row.ip }}:{{ row.port }}</span>
|
||||
</td>
|
||||
</tr>
|
||||
<!-- Message pendant le scan si pas encore de résultats -->
|
||||
<tr v-if="filteredResults.length === 0 && isScanning">
|
||||
<td colspan="5" class="px-4 py-8 text-center text-monokai-comment">
|
||||
<span class="mdi mdi-magnify-scan text-2xl block mb-2 animate-pulse"></span>
|
||||
Scan en cours, en attente de résultats...
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<!-- État vide -->
|
||||
<div v-else-if="!isScanning" class="flex-1 flex items-center justify-center">
|
||||
<div class="text-center text-monokai-comment">
|
||||
<span class="mdi mdi-lan-check text-6xl block mb-4 opacity-30"></span>
|
||||
<div class="text-lg">Sélectionnez des services et lancez un scan</div>
|
||||
<div class="text-sm mt-2 opacity-60">
|
||||
Les résultats afficheront les services actifs sur chaque IP du réseau
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Volet droit : Logs -->
|
||||
<div
|
||||
class="relative flex-shrink-0 border-l border-monokai-comment"
|
||||
:style="{ width: `${rightPanelWidth}px` }"
|
||||
>
|
||||
<!-- Poignée de redimensionnement -->
|
||||
<div
|
||||
class="absolute left-0 top-0 bottom-0 w-2 cursor-ew-resize"
|
||||
@mousedown.prevent="startResize"
|
||||
></div>
|
||||
<div class="absolute left-1 top-1/2 -translate-y-1/2 flex flex-col gap-1 opacity-70 pointer-events-none">
|
||||
<span class="w-1.5 h-1.5 rounded-full bg-monokai-comment/70"></span>
|
||||
<span class="w-1.5 h-1.5 rounded-full bg-monokai-comment/70"></span>
|
||||
<span class="w-1.5 h-1.5 rounded-full bg-monokai-comment/70"></span>
|
||||
<span class="w-1.5 h-1.5 rounded-full bg-monokai-comment/70"></span>
|
||||
</div>
|
||||
|
||||
<div class="p-4 h-full flex flex-col">
|
||||
<h2 class="text-lg font-bold text-monokai-pink flex items-center gap-2">
|
||||
<span class="mdi mdi-text-box-outline"></span>
|
||||
Logs du scan
|
||||
</h2>
|
||||
<div class="mt-3 bg-monokai-bg border border-monokai-comment rounded flex-1 min-h-0 overflow-auto">
|
||||
<pre class="text-xs text-monokai-text font-mono p-3 whitespace-pre-wrap">{{ logs.length ? logs.join('\n') : 'Aucun log pour le moment.' }}</pre>
|
||||
</div>
|
||||
<button
|
||||
v-if="logs.length > 0"
|
||||
@click="clearLogs"
|
||||
class="mt-2 px-3 py-1 rounded text-xs bg-monokai-comment/30 text-monokai-comment hover:bg-monokai-comment/50 transition-colors"
|
||||
>
|
||||
Effacer les logs
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Modal Paramètres -->
|
||||
<SettingsModal
|
||||
:isOpen="showSettings"
|
||||
@close="showSettings = false"
|
||||
@configReloaded="handleConfigReloaded"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed, onMounted, onBeforeUnmount } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { useIPStore } from '@/stores/ipStore'
|
||||
import SystemStats from '@/components/SystemStats.vue'
|
||||
import SettingsModal from '@/components/SettingsModal.vue'
|
||||
import axios from 'axios'
|
||||
|
||||
const router = useRouter()
|
||||
const ipStore = useIPStore()
|
||||
const showSettings = ref(false)
|
||||
|
||||
// --- Données ---
|
||||
const services = ref([])
|
||||
const selectedPorts = ref(new Set())
|
||||
const results = ref([])
|
||||
const isScanning = ref(false)
|
||||
const scanProgress = ref({ current: 0, total: 0, ip: '' })
|
||||
const logs = ref([])
|
||||
const filterText = ref('')
|
||||
const sortColumn = ref('ip')
|
||||
const sortAsc = ref(true)
|
||||
|
||||
// --- Panneau droit redimensionnable ---
|
||||
const rightPanelWidth = ref(384)
|
||||
const minPanelWidth = 288
|
||||
const maxPanelWidth = 576
|
||||
let isResizing = false
|
||||
let startX = 0
|
||||
let startWidth = 0
|
||||
|
||||
// --- Computed ---
|
||||
const selectedCount = computed(() => selectedPorts.value.size)
|
||||
|
||||
const progressPercent = computed(() => {
|
||||
if (scanProgress.value.total === 0) return 0
|
||||
return Math.round((scanProgress.value.current / scanProgress.value.total) * 100)
|
||||
})
|
||||
|
||||
const scanTimeout = ref(1.0)
|
||||
|
||||
const estimatedTime = computed(() => {
|
||||
const total = scanProgress.value.total
|
||||
const remaining = total - scanProgress.value.current
|
||||
const seconds = Math.ceil(remaining * scanTimeout.value)
|
||||
if (seconds < 60) return `${seconds}s`
|
||||
const min = Math.floor(seconds / 60)
|
||||
const sec = seconds % 60
|
||||
return sec > 0 ? `${min}min ${sec}s` : `${min}min`
|
||||
})
|
||||
|
||||
const filteredResults = computed(() => {
|
||||
let data = [...results.value]
|
||||
|
||||
// Filtre texte
|
||||
if (filterText.value) {
|
||||
const search = filterText.value.toLowerCase()
|
||||
data = data.filter(r =>
|
||||
r.ip.includes(search) ||
|
||||
(r.name || '').toLowerCase().includes(search) ||
|
||||
(r.hostname || '').toLowerCase().includes(search) ||
|
||||
r.service_name.toLowerCase().includes(search) ||
|
||||
String(r.port).includes(search) ||
|
||||
(r.protocol || '').toLowerCase().includes(search)
|
||||
)
|
||||
}
|
||||
|
||||
// Tri
|
||||
data.sort((a, b) => {
|
||||
let va = a[sortColumn.value]
|
||||
let vb = b[sortColumn.value]
|
||||
|
||||
// Tri numérique pour IP
|
||||
if (sortColumn.value === 'ip') {
|
||||
const toNum = ip => ip.split('.').reduce((acc, oct) => acc * 256 + parseInt(oct), 0)
|
||||
va = toNum(va)
|
||||
vb = toNum(vb)
|
||||
}
|
||||
|
||||
// Tri numérique pour port
|
||||
if (sortColumn.value === 'port') {
|
||||
va = Number(va)
|
||||
vb = Number(vb)
|
||||
}
|
||||
|
||||
if (va < vb) return sortAsc.value ? -1 : 1
|
||||
if (va > vb) return sortAsc.value ? 1 : -1
|
||||
|
||||
// Tri secondaire : par port si même IP, par IP si même colonne
|
||||
if (sortColumn.value === 'ip') return a.port - b.port
|
||||
const toNum = ip => ip.split('.').reduce((acc, oct) => acc * 256 + parseInt(oct), 0)
|
||||
return toNum(a.ip) - toNum(b.ip)
|
||||
})
|
||||
|
||||
return data
|
||||
})
|
||||
|
||||
// --- Méthodes ---
|
||||
function isSelected(service) {
|
||||
return selectedPorts.value.has(service.port)
|
||||
}
|
||||
|
||||
function toggleService(service) {
|
||||
const ports = new Set(selectedPorts.value)
|
||||
if (ports.has(service.port)) {
|
||||
ports.delete(service.port)
|
||||
} else {
|
||||
ports.add(service.port)
|
||||
}
|
||||
selectedPorts.value = ports
|
||||
}
|
||||
|
||||
function selectAll() {
|
||||
selectedPorts.value = new Set(services.value.map(s => s.port))
|
||||
}
|
||||
|
||||
function selectNone() {
|
||||
selectedPorts.value = new Set()
|
||||
}
|
||||
|
||||
function sortBy(column) {
|
||||
if (sortColumn.value === column) {
|
||||
sortAsc.value = !sortAsc.value
|
||||
} else {
|
||||
sortColumn.value = column
|
||||
sortAsc.value = true
|
||||
}
|
||||
}
|
||||
|
||||
function sortIcon(column) {
|
||||
if (sortColumn.value !== column) return ''
|
||||
return sortAsc.value ? '▲' : '▼'
|
||||
}
|
||||
|
||||
function serviceColor(protocol) {
|
||||
if (!protocol) return 'bg-monokai-comment/30 text-monokai-comment'
|
||||
switch (protocol) {
|
||||
case 'http':
|
||||
case 'https':
|
||||
return 'bg-monokai-green/20 text-monokai-green'
|
||||
case 'ssh':
|
||||
return 'bg-monokai-cyan/20 text-monokai-cyan'
|
||||
case 'rdp':
|
||||
return 'bg-monokai-orange/20 text-monokai-orange'
|
||||
case 'smb':
|
||||
case 'ftp':
|
||||
return 'bg-monokai-yellow/20 text-monokai-yellow'
|
||||
default:
|
||||
return 'bg-monokai-purple/20 text-monokai-purple'
|
||||
}
|
||||
}
|
||||
|
||||
async function loadServices() {
|
||||
try {
|
||||
const response = await axios.get('/api/services/list')
|
||||
services.value = response.data.services || []
|
||||
} catch (error) {
|
||||
console.error('Erreur chargement services:', error)
|
||||
addLog('Erreur chargement de la liste des services')
|
||||
}
|
||||
}
|
||||
|
||||
function addLog(message) {
|
||||
const now = new Date().toLocaleTimeString('fr-FR')
|
||||
logs.value.push(`[${now}] ${message}`)
|
||||
}
|
||||
|
||||
function clearLogs() {
|
||||
logs.value = []
|
||||
}
|
||||
|
||||
async function launchScan() {
|
||||
if (selectedCount.value === 0 || isScanning.value) return
|
||||
|
||||
isScanning.value = true
|
||||
results.value = []
|
||||
scanProgress.value = { current: 0, total: 0, ip: '' }
|
||||
|
||||
const ports = Array.from(selectedPorts.value)
|
||||
const serviceNames = ports.map(p => {
|
||||
const svc = services.value.find(s => s.port === p)
|
||||
return svc ? svc.name : `Port ${p}`
|
||||
})
|
||||
addLog(`Scan lancé pour ${ports.length} service(s) : ${serviceNames.join(', ')}`)
|
||||
|
||||
try {
|
||||
const response = await axios.post('/api/services/scan', { ports })
|
||||
if (response.data.status === 'error') {
|
||||
addLog(`Erreur : ${response.data.message}`)
|
||||
isScanning.value = false
|
||||
}
|
||||
// Les résultats arrivent via WebSocket (service_scan_complete)
|
||||
} catch (error) {
|
||||
console.error('Erreur scan services:', error)
|
||||
addLog(`Erreur : ${error.response?.data?.detail || error.message}`)
|
||||
isScanning.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// --- WebSocket : écouter la progression du scan ---
|
||||
function handleWsMessage(event) {
|
||||
try {
|
||||
const data = JSON.parse(event.data)
|
||||
if (data.type === 'service_scan_progress') {
|
||||
scanProgress.value = {
|
||||
current: data.current || 0,
|
||||
total: data.total || 0,
|
||||
ip: data.ip || ''
|
||||
}
|
||||
} else if (data.type === 'service_scan_start') {
|
||||
addLog(data.message || 'Scan de services démarré')
|
||||
} else if (data.type === 'service_scan_log') {
|
||||
addLog(data.message || '')
|
||||
} else if (data.type === 'service_scan_result') {
|
||||
// Résultat individuel en temps réel
|
||||
if (data.result) {
|
||||
results.value.push(data.result)
|
||||
}
|
||||
} else if (data.type === 'service_scan_complete') {
|
||||
addLog(data.message || 'Scan de services terminé')
|
||||
// Mettre à jour avec les résultats finaux (triés)
|
||||
if (data.results) {
|
||||
results.value = data.results
|
||||
}
|
||||
isScanning.value = false
|
||||
scanProgress.value = { current: 0, total: 0, ip: '' }
|
||||
}
|
||||
} catch {
|
||||
// Message non-JSON, ignorer
|
||||
}
|
||||
}
|
||||
|
||||
// --- Navigation ---
|
||||
function goToDashboard() {
|
||||
router.push('/')
|
||||
}
|
||||
|
||||
function openSettings() {
|
||||
showSettings.value = true
|
||||
}
|
||||
|
||||
async function handleConfigReloaded() {
|
||||
await ipStore.fetchIPs()
|
||||
ipStore.bumpConfigReload()
|
||||
await loadServices()
|
||||
}
|
||||
|
||||
// --- Redimensionnement panneau droit ---
|
||||
function clampWidth(value) {
|
||||
return Math.max(minPanelWidth, Math.min(maxPanelWidth, value))
|
||||
}
|
||||
|
||||
function onMouseMove(event) {
|
||||
if (!isResizing) return
|
||||
const delta = startX - event.clientX
|
||||
rightPanelWidth.value = clampWidth(startWidth + delta)
|
||||
}
|
||||
|
||||
function stopResize() {
|
||||
if (!isResizing) return
|
||||
isResizing = false
|
||||
window.removeEventListener('mousemove', onMouseMove)
|
||||
window.removeEventListener('mouseup', stopResize)
|
||||
}
|
||||
|
||||
function startResize(event) {
|
||||
isResizing = true
|
||||
startX = event.clientX
|
||||
startWidth = rightPanelWidth.value
|
||||
window.addEventListener('mousemove', onMouseMove)
|
||||
window.addEventListener('mouseup', stopResize)
|
||||
}
|
||||
|
||||
// --- Lifecycle ---
|
||||
onMounted(async () => {
|
||||
await loadServices()
|
||||
|
||||
// Écouter les messages WebSocket pour la progression
|
||||
if (ipStore.ws) {
|
||||
ipStore.ws.addEventListener('message', handleWsMessage)
|
||||
}
|
||||
})
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
stopResize()
|
||||
if (ipStore.ws) {
|
||||
ipStore.ws.removeEventListener('message', handleWsMessage)
|
||||
}
|
||||
})
|
||||
</script>
|
||||
Reference in New Issue
Block a user