Stabilize discovery workflow

This commit is contained in:
2026-05-29 17:16:46 +08:00
parent 15772bc17e
commit 3981ffdf5e
40 changed files with 2039 additions and 211 deletions
+83
View File
@@ -0,0 +1,83 @@
<div class="card mb-4">
<div class="card-header">Активные сканирования</div>
<div class="card-body">
<?php if (empty($activeJobs)): ?>
<p class="text-muted mb-0">Нет активных сканирований.</p>
<?php else: ?>
<table class="table table-sm align-middle">
<thead>
<tr>
<th>ID</th>
<th>Тип</th>
<th>Статус</th>
<th>Прогресс</th>
<th>Найдено</th>
<th>Действия</th>
</tr>
</thead>
<tbody>
<?php foreach ($activeJobs as $job): ?>
<?php $progress = json_decode($job->resultJson ?? '{}', true) ?: []; ?>
<?php
$totalIps = (int)($progress['total_ips'] ?? 0);
$nextIpIndex = (int)($progress['next_ip_index'] ?? 0);
$hostsFound = (int)($progress['hosts_found'] ?? 0);
$percent = $totalIps > 0 ? min(100, (int)floor(($nextIpIndex / $totalIps) * 100)) : 0;
$badgeClass = match ($job->status) {
'pending' => 'bg-warning',
'running' => 'bg-info',
'paused' => 'bg-secondary',
default => 'bg-light text-dark',
};
?>
<tr>
<td>#<?= (int)$job->id ?></td>
<td><?= htmlspecialchars($job->type) ?></td>
<td><span class="badge <?= $badgeClass ?>"><?= htmlspecialchars($job->status) ?></span></td>
<td style="min-width: 180px;">
<div class="progress" style="height: 8px;">
<div class="progress-bar" style="width: <?= $percent ?>%"></div>
</div>
<small class="text-muted"><?= $nextIpIndex ?> / <?= $totalIps ?: '?' ?> IP</small>
</td>
<td><?= $hostsFound ?></td>
<td>
<a href="/discoveries?scan_job_id=<?= (int)$job->id ?>&status=all"
class="btn btn-sm btn-outline-primary">
Находки
</a>
<?php if ($job->status === 'running'): ?>
<button type="button"
class="btn btn-sm btn-outline-secondary"
hx-post="/discovery/jobs/<?= (int)$job->id ?>/pause"
hx-target="#active-jobs"
hx-swap="innerHTML">
Пауза
</button>
<?php endif; ?>
<?php if ($job->status === 'paused'): ?>
<button type="button"
class="btn btn-sm btn-outline-success"
hx-post="/discovery/jobs/<?= (int)$job->id ?>/resume"
hx-target="#active-jobs"
hx-swap="innerHTML">
Продолжить
</button>
<?php endif; ?>
<?php if (in_array($job->status, ['pending', 'running', 'paused'], true)): ?>
<button type="button"
class="btn btn-sm btn-outline-danger"
hx-post="/discovery/jobs/<?= (int)$job->id ?>/cancel"
hx-target="#active-jobs"
hx-swap="innerHTML">
Отменить
</button>
<?php endif; ?>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
<?php endif; ?>
</div>
</div>
+59
View File
@@ -0,0 +1,59 @@
<div class="card mb-4">
<div class="card-header">История сканирования</div>
<div class="card-body">
<?php if (empty($scanJobs)): ?>
<p class="text-muted">Нет запусков сканирования.</p>
<?php else: ?>
<table class="table table-sm">
<thead>
<tr>
<th>Тип</th>
<th>Статус</th>
<th>Результат</th>
<th>Запущен</th>
<th>Завершён</th>
<th>Действия</th>
</tr>
</thead>
<tbody>
<?php foreach ($scanJobs as $job): ?>
<tr>
<td><?= htmlspecialchars($job->type) ?></td>
<td>
<?php
$badgeClass = match ($job->status) {
'pending' => 'bg-warning',
'running' => 'bg-info',
'done' => 'bg-success',
'failed' => 'bg-danger',
'paused' => 'bg-secondary',
'cancelled' => 'bg-dark',
default => 'bg-secondary',
};
?>
<span class="badge <?= $badgeClass ?>"><?= htmlspecialchars($job->status) ?></span>
</td>
<td>
<?php if ($job->errorMessage !== null): ?>
<span class="text-danger"><?= htmlspecialchars($job->errorMessage) ?></span>
<?php elseif ($job->resultJson !== null): ?>
<?php $result = json_decode($job->resultJson, true) ?: []; ?>
<?= isset($result['hosts_found']) ? (int)$result['hosts_found'] . ' хост(ов)' : '-' ?>
<?php else: ?>
-
<?php endif; ?>
</td>
<td><?= $job->startedAt?->format('Y-m-d H:i:s') ?? '-' ?></td>
<td><?= $job->finishedAt?->format('Y-m-d H:i:s') ?? '-' ?></td>
<td>
<a href="/discoveries?scan_job_id=<?= (int)$job->id ?>&status=all" class="btn btn-sm btn-outline-primary">
Находки
</a>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
<?php endif; ?>
</div>
</div>
+14 -85
View File
@@ -48,6 +48,9 @@
<?php endif; ?>
</td>
<td>
<a href="/discoveries?range_id=<?= (int)$range->id ?>&status=all" class="btn btn-sm btn-outline-primary">
Находки
</a>
<form method="POST" action="/discovery/ranges/toggle" class="d-inline">
<input type="hidden" name="id" value="<?= $range->id ?>">
<button type="submit" class="btn btn-sm btn-outline-secondary">
@@ -73,93 +76,19 @@
</div>
</div>
<!-- Scan Jobs -->
<div class="card mb-4">
<div class="card-header">История сканирования</div>
<div class="card-body">
<?php if (empty($scanJobs)): ?>
<p class="text-muted">Нет запусков сканирования.</p>
<?php else: ?>
<table class="table table-sm">
<thead>
<tr>
<th>Тип</th>
<th>Статус</th>
<th>Запущен</th>
<th>Завершён</th>
</tr>
</thead>
<tbody>
<?php foreach ($scanJobs as $job): ?>
<tr>
<td><?= htmlspecialchars($job->type) ?></td>
<td>
<?php
$badgeClass = match ($job->status) {
'pending' => 'bg-warning',
'running' => 'bg-info',
'done' => 'bg-success',
'failed' => 'bg-danger',
default => 'bg-secondary',
};
?>
<span class="badge <?= $badgeClass ?>"><?= htmlspecialchars($job->status) ?></span>
</td>
<td><?= $job->startedAt?->format('Y-m-d H:i:s') ?? '-' ?></td>
<td><?= $job->finishedAt?->format('Y-m-d H:i:s') ?? '-' ?></td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
<?php endif; ?>
</div>
<div id="active-jobs"
hx-get="/discovery/jobs/active"
hx-trigger="load, every 2s"
hx-swap="innerHTML">
<?php require dirname(__DIR__) . '/discovery/_active_jobs.php'; ?>
</div>
<!-- New Hosts -->
<div class="card mb-4">
<div class="card-header">Новые находки (<?= count($newHosts) ?>)</div>
<div class="card-body">
<?php if (empty($newHosts)): ?>
<p class="text-muted">Нет новых находок.</p>
<?php else: ?>
<table class="table table-sm">
<thead>
<tr>
<th>IP</th>
<th>Hostname</th>
<th>MAC</th>
<th>Vendor</th>
<th>Порты</th>
<th>Уверенность</th>
<th>Действия</th>
</tr>
</thead>
<tbody>
<?php foreach ($newHosts as $host): ?>
<tr>
<td><?= htmlspecialchars($host->ipAddress) ?></td>
<td><?= htmlspecialchars($host->hostname ?? '-') ?></td>
<td><?= htmlspecialchars($host->macAddress ?? '-') ?></td>
<td><?= htmlspecialchars($host->vendor ?? '-') ?></td>
<td><?= empty($host->openPorts) ? '-' : implode(', ', $host->openPorts) ?></td>
<td><?= $host->confidence ?>%</td>
<td>
<form method="POST" action="/devices/from-host" class="d-inline">
<input type="hidden" name="host_id" value="<?= $host->id ?>">
<input type="hidden" name="name" value="<?= htmlspecialchars($host->hostname ?: $host->ipAddress) ?>">
<button type="submit" class="btn btn-sm btn-outline-primary">Создать устройство</button>
</form>
<form method="POST" action="/discovery/hosts/ignore" class="d-inline">
<input type="hidden" name="host_id" value="<?= $host->id ?>">
<button type="submit" class="btn btn-sm btn-outline-secondary">Игнорировать</button>
</form>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
<?php endif; ?>
</div>
<div id="scan-history"
hx-get="/discovery/jobs/history"
hx-trigger="load, every 2s"
hx-swap="innerHTML">
<?php require dirname(__DIR__) . '/discovery/_scan_history.php'; ?>
</div>
<?php $content = ob_get_clean(); ?>
<?php require dirname(__DIR__) . '/layout.php'; ?>