Files
dip-ids/frontend/pages/capture.vue
T
2026-04-12 22:26:26 +04:00

252 lines
8.6 KiB
Vue

<template>
<div class="space-y-4 max-w-4xl">
<!-- Status banner -->
<UAlert
v-if="status.running"
icon="i-heroicons-radio"
color="green"
variant="soft"
:title="`Захват активен — ${status.source} (${modeLabel(status.mode)})`"
:description="`Пакетов: ${status.pkts?.toLocaleString() ?? 0} · Трафик: ${humanBytes(status.bytes ?? 0)}`"
>
<template #actions>
<UButton color="red" variant="soft" size="xs" @click="doStop">Остановить</UButton>
</template>
</UAlert>
<!-- Tabs: Live / PCAP -->
<UCard :ui="cardUi">
<template #header>
<p class="text-sm font-semibold text-gray-200">Источник трафика</p>
</template>
<UTabs :items="tabs" :ui="tabsUi">
<!-- Live capture tab -->
<template #live>
<div class="space-y-4 pt-4">
<div>
<label class="text-xs text-gray-400 mb-1.5 block">Сетевой интерфейс</label>
<div class="flex gap-2">
<USelect
v-model="liveIface"
:options="ifaceOptions"
:disabled="status.running"
class="flex-1"
:ui="selectUi"
/>
<UButton
color="gray"
variant="soft"
size="sm"
icon="i-heroicons-arrow-path"
:loading="loadingIfaces"
@click="loadInterfaces"
/>
</div>
</div>
<UAlert
icon="i-heroicons-information-circle"
color="blue"
variant="soft"
title="Требуются права суперпользователя"
description="Live-захват использует raw sockets. Запустите бэкенд через sudo."
/>
<UButton
:color="status.running ? 'red' : 'green'"
:icon="status.running ? 'i-heroicons-stop-circle' : 'i-heroicons-play-circle'"
:loading="actionLoading"
size="sm"
@click="status.running ? doStop() : doStartLive()"
>
{{ status.running ? 'Остановить захват' : 'Начать захват' }}
</UButton>
</div>
</template>
<!-- PCAP upload tab -->
<template #pcap>
<div class="space-y-4 pt-4">
<div
class="border-2 border-dashed border-gray-700 rounded-xl p-8 text-center transition-colors"
:class="dragging ? 'border-blue-500 bg-blue-500/5' : 'hover:border-gray-600'"
@dragover.prevent="dragging = true"
@dragleave="dragging = false"
@drop.prevent="onDrop"
>
<UIcon name="i-heroicons-document-arrow-up" class="w-10 h-10 text-gray-600 mx-auto mb-3" />
<p class="text-sm text-gray-400 mb-1">
{{ pcapFile ? pcapFile.name : 'Перетащите .pcap файл или выберите' }}
</p>
<p class="text-xs text-gray-600 mb-3">Поддерживается формат libpcap / pcapng</p>
<label class="cursor-pointer">
<UButton color="gray" variant="soft" size="sm" as="span">
Выбрать файл
</UButton>
<input type="file" accept=".pcap,.pcapng" class="hidden" @change="onFileSelect" />
</label>
</div>
<UButton
color="blue"
icon="i-heroicons-arrow-up-tray"
:disabled="!pcapFile || status.running"
:loading="actionLoading"
size="sm"
@click="doUploadPCAP"
>
{{ pcapFile ? `Анализировать ${pcapFile.name}` : 'Выберите файл' }}
</UButton>
</div>
</template>
</UTabs>
</UCard>
<!-- Sessions history -->
<UCard :ui="cardUi">
<template #header>
<p class="text-sm font-semibold text-gray-200">История сессий</p>
</template>
<UTable :rows="sessions" :columns="sessionColumns" :ui="tableUi">
<template #mode-data="{ row }">
<UBadge :color="row.mode === 'live' ? 'blue' : 'purple'" variant="soft" size="xs">
{{ modeLabel(row.mode) }}
</UBadge>
</template>
<template #status-data="{ row }">
<UBadge :color="row.status === 'running' ? 'green' : 'gray'" variant="soft" size="xs">
{{ statusLabel(row.status) }}
</UBadge>
</template>
<template #total_pkts-data="{ row }">
<span class="text-xs font-mono">{{ row.total_pkts.toLocaleString() }}</span>
</template>
<template #total_bytes-data="{ row }">
<span class="text-xs font-mono">{{ humanBytes(row.total_bytes) }}</span>
</template>
<template #created_at-data="{ row }">
<span class="text-xs text-gray-400">{{ formatTime(row.created_at) }}</span>
</template>
</UTable>
</UCard>
</div>
</template>
<script setup lang="ts">
const { post, upload, get } = useApi()
const { status, fetchStatus, stopCapture } = useCaptureStatus()
const liveIface = ref('')
const pcapFile = ref<File | null>(null)
const dragging = ref(false)
const actionLoading = ref(false)
const loadingIfaces = ref(false)
const sessions = ref<any[]>([])
const ifaceOptions = ref<{ label: string, value: string }[]>([])
const tabs = [
{ slot: 'live', label: 'Live захват', icon: 'i-heroicons-radio' },
{ slot: 'pcap', label: 'PCAP файл', icon: 'i-heroicons-document' },
]
const sessionColumns = [
{ key: 'created_at', label: 'Время' },
{ key: 'source', label: 'Источник' },
{ key: 'mode', label: 'Режим' },
{ key: 'status', label: 'Статус' },
{ key: 'total_pkts', label: 'Пакетов' },
{ key: 'total_bytes', label: 'Трафик' },
]
async function loadInterfaces() {
loadingIfaces.value = true
try {
const res = await get<any>('/capture/interfaces')
ifaceOptions.value = (res.interfaces ?? []).map((i: string) => ({ label: i, value: i }))
if (!liveIface.value && ifaceOptions.value.length) {
liveIface.value = ifaceOptions.value[0].value
}
} finally {
loadingIfaces.value = false
}
}
async function loadSessions() {
const res = await get<any>('/capture/sessions')
sessions.value = res.data ?? []
}
async function doStartLive() {
actionLoading.value = true
try {
await post('/capture/start', { interface: liveIface.value })
await fetchStatus()
await loadSessions()
} finally {
actionLoading.value = false
}
}
async function doStop() {
actionLoading.value = true
try {
await stopCapture()
await loadSessions()
} finally {
actionLoading.value = false
}
}
async function doUploadPCAP() {
if (!pcapFile.value) return
actionLoading.value = true
try {
await upload('/capture/upload', pcapFile.value)
await fetchStatus()
await loadSessions()
pcapFile.value = null
} finally {
actionLoading.value = false
}
}
function onFileSelect(e: Event) {
const input = e.target as HTMLInputElement
if (input.files?.[0]) pcapFile.value = input.files[0]
}
function onDrop(e: DragEvent) {
dragging.value = false
const file = e.dataTransfer?.files[0]
if (file) pcapFile.value = file
}
function modeLabel(m?: string) {
return m === 'live' ? 'Live' : m === 'pcap' ? 'PCAP' : m ?? ''
}
function statusLabel(s: string) {
return ({ running: 'Активна', stopped: 'Остановлена', completed: 'Завершена' } as any)[s] ?? s
}
function humanBytes(b: number) {
if (b >= 1 << 30) return `${(b / (1 << 30)).toFixed(2)} GB`
if (b >= 1 << 20) return `${(b / (1 << 20)).toFixed(2)} MB`
if (b >= 1 << 10) return `${(b / (1 << 10)).toFixed(2)} KB`
return `${b} B`
}
function formatTime(iso: string) {
return new Date(iso).toLocaleString('ru-RU', { day: '2-digit', month: '2-digit', year: '2-digit', hour: '2-digit', minute: '2-digit' })
}
onMounted(() => { loadInterfaces(); loadSessions() })
const cardUi = { base: 'bg-gray-900 border border-gray-800', header: { base: 'border-b border-gray-800 px-4 py-3' }, body: { base: 'px-4 py-4' } }
const tableUi = { wrapper: 'overflow-x-auto', td: { base: 'py-2' }, th: { base: 'text-gray-400 text-xs font-medium' } }
const selectUi = { base: 'bg-gray-800 border-gray-700 text-gray-200' }
const tabsUi = { list: { background: 'bg-gray-800', tab: { active: 'bg-gray-700' } } }
</script>