Итерация 3: инвентарь устройств

- Миграция devices (name, type, ip, mac, hostname, vendor, os, location, importance, status)
- Device model + DeviceRepository с полным CRUD
- DeviceService для бизнес-логики
- MergeSuggestionService для предложений объединения (MAC→90%, hostname→60%)
- DeviceController: список, создание, редактирование, карточка, удаление, создание из discovered_host
- Шаблоны: devices/index, devices/form, devices/show
- Dashboard с реальными счётчиками (устройства, новые находки)
- Кнопка «Создать устройство» в Discovery для новых хостов
- DeviceRepository + DeviceService + DeviceController в DI
This commit is contained in:
2026-05-26 07:50:37 +08:00
parent 177e44f015
commit 15772bc17e
13 changed files with 760 additions and 28 deletions
+38 -26
View File
@@ -6,15 +6,7 @@
<div class="card text-white bg-primary">
<div class="card-body">
<h5 class="card-title">Устройства</h5>
<p class="card-text display-6">0</p>
</div>
</div>
</div>
<div class="col-md-3">
<div class="card text-white bg-success">
<div class="card-body">
<h5 class="card-title">Сервисы</h5>
<p class="card-text display-6">0</p>
<p class="card-text display-6"><?= $deviceCount ?></p>
</div>
</div>
</div>
@@ -22,6 +14,14 @@
<div class="card text-white bg-info">
<div class="card-body">
<h5 class="card-title">Новые находки</h5>
<p class="card-text display-6"><?= $newDiscoveries ?></p>
</div>
</div>
</div>
<div class="col-md-3">
<div class="card text-white bg-success">
<div class="card-body">
<h5 class="card-title">Сервисы</h5>
<p class="card-text display-6">0</p>
</div>
</div>
@@ -39,33 +39,45 @@
<div class="row">
<div class="col-md-6">
<div class="card mb-4">
<div class="card-header">
Последние найденные хосты
</div>
<div class="card-header">Последние сканирования</div>
<div class="card-body">
<p class="text-muted">Нет данных. Запустите сканирование сети.</p>
<?php if (empty($recentScans)): ?>
<p class="text-muted">Нет запусков сканирования.</p>
<?php else: ?>
<table class="table table-sm">
<thead><tr><th>Тип</th><th>Статус</th><th>Когда</th></tr></thead>
<tbody>
<?php foreach ($recentScans as $scan): ?>
<tr>
<td><?= htmlspecialchars($scan->type) ?></td>
<td>
<?php
$badge = match ($scan->status) {
'pending' => 'bg-warning', 'running' => 'bg-info',
'done' => 'bg-success', 'failed' => 'bg-danger',
default => 'bg-secondary',
};
?>
<span class="badge <?= $badge ?>"><?= htmlspecialchars($scan->status) ?></span>
</td>
<td><?= $scan->createdAt->format('m-d H:i') ?></td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
<?php endif; ?>
</div>
</div>
</div>
<div class="col-md-6">
<div class="card mb-4">
<div class="card-header">
Последние события
</div>
<div class="card-header">Быстрые действия</div>
<div class="card-body">
<p class="text-muted">Нет событий.</p>
<a href="/discovery" class="btn btn-outline-primary mb-2">Сканирование сети</a>
<a href="/devices/create" class="btn btn-outline-secondary mb-2">Добавить устройство</a>
</div>
</div>
</div>
</div>
<div class="card">
<div class="card-header">
Последний скан сети
</div>
<div class="card-body">
<p class="text-muted">Сканирование ещё не запускалось.</p>
</div>
</div>
<?php $content = ob_get_clean(); ?>
<?php require dirname(__DIR__) . '/layout.php'; ?>
+71
View File
@@ -0,0 +1,71 @@
<?php ob_start(); ?>
<h2><?= $device->id === null ? 'Добавить устройство' : 'Редактировать: ' . htmlspecialchars($device->name) ?></h2>
<form method="POST" action="<?= $device->id === null ? '/devices/create' : '/devices/' . $device->id . '/update' ?>" class="row g-3">
<div class="col-md-6">
<label class="form-label">Название *</label>
<input type="text" name="name" class="form-control" value="<?= htmlspecialchars($device->name) ?>" required>
</div>
<div class="col-md-3">
<label class="form-label">Тип</label>
<select name="type" class="form-select">
<?php foreach ($types as $t): ?>
<option value="<?= $t ?>" <?= $device->type === $t ? 'selected' : '' ?>><?= $t ?></option>
<?php endforeach; ?>
</select>
</div>
<div class="col-md-3">
<label class="form-label">Важность</label>
<select name="importance" class="form-select">
<?php foreach ($importances as $imp): ?>
<option value="<?= $imp ?>" <?= $device->importance === $imp ? 'selected' : '' ?>><?= $imp ?></option>
<?php endforeach; ?>
</select>
</div>
<div class="col-md-4">
<label class="form-label">Primary IP</label>
<input type="text" name="primary_ip" class="form-control" value="<?= htmlspecialchars($device->primaryIp ?? '') ?>">
</div>
<div class="col-md-4">
<label class="form-label">MAC адрес</label>
<input type="text" name="mac_address" class="form-control" value="<?= htmlspecialchars($device->macAddress ?? '') ?>" placeholder="aa:bb:cc:dd:ee:ff">
</div>
<div class="col-md-4">
<label class="form-label">Hostname</label>
<input type="text" name="hostname" class="form-control" value="<?= htmlspecialchars($device->hostname ?? '') ?>">
</div>
<div class="col-md-4">
<label class="form-label">Vendor</label>
<input type="text" name="vendor" class="form-control" value="<?= htmlspecialchars($device->vendor ?? '') ?>">
</div>
<div class="col-md-4">
<label class="form-label">OS Name</label>
<input type="text" name="os_name" class="form-control" value="<?= htmlspecialchars($device->osName ?? '') ?>">
</div>
<div class="col-md-4">
<label class="form-label">OS Version</label>
<input type="text" name="os_version" class="form-control" value="<?= htmlspecialchars($device->osVersion ?? '') ?>">
</div>
<div class="col-md-4">
<label class="form-label">Location</label>
<input type="text" name="location" class="form-control" value="<?= htmlspecialchars($device->location ?? '') ?>">
</div>
<div class="col-md-4">
<label class="form-label">Статус</label>
<select name="status" class="form-select">
<option value="active" <?= $device->status === 'active' ? 'selected' : '' ?>>active</option>
<option value="inactive" <?= $device->status === 'inactive' ? 'selected' : '' ?>>inactive</option>
<option value="maintenance" <?= $device->status === 'maintenance' ? 'selected' : '' ?>>maintenance</option>
</select>
</div>
<div class="col-12">
<label class="form-label">Описание</label>
<textarea name="description" class="form-control" rows="2"><?= htmlspecialchars($device->description ?? '') ?></textarea>
</div>
<div class="col-12">
<button type="submit" class="btn btn-primary">Сохранить</button>
<a href="/devices" class="btn btn-secondary">Отмена</a>
</div>
</form>
<?php $content = ob_get_clean(); ?>
<?php require dirname(__DIR__) . '/layout.php'; ?>
+57
View File
@@ -0,0 +1,57 @@
<?php ob_start(); ?>
<h2>Устройства</h2>
<div class="mb-3">
<a href="/devices/create" class="btn btn-primary">Добавить устройство</a>
</div>
<?php if (empty($devices)): ?>
<div class="alert alert-info">Нет устройств. Добавьте первое устройство вручную или создайте из найденного хоста.</div>
<?php else: ?>
<table class="table table-striped">
<thead>
<tr>
<th>Название</th>
<th>Тип</th>
<th>IP</th>
<th>MAC</th>
<th>Vendor</th>
<th>Важность</th>
<th>Статус</th>
<th>Действия</th>
</tr>
</thead>
<tbody>
<?php foreach ($devices as $device): ?>
<tr>
<td><a href="/devices/<?= $device->id ?>"><?= htmlspecialchars($device->name) ?></a></td>
<td><?= htmlspecialchars($device->type) ?></td>
<td><?= htmlspecialchars($device->primaryIp ?? '-') ?></td>
<td><?= htmlspecialchars($device->macAddress ?? '-') ?></td>
<td><?= htmlspecialchars($device->vendor ?? '-') ?></td>
<td>
<?php
$badge = match ($device->importance) {
'critical' => 'bg-danger',
'high' => 'bg-warning',
'low' => 'bg-secondary',
default => 'bg-light text-dark',
};
?>
<span class="badge <?= $badge ?>"><?= htmlspecialchars($device->importance) ?></span>
</td>
<td>
<span class="badge <?= $device->status === 'active' ? 'bg-success' : 'bg-secondary' ?>">
<?= htmlspecialchars($device->status) ?>
</span>
</td>
<td>
<a href="/devices/<?= $device->id ?>/edit" class="btn btn-sm btn-outline-primary">Изменить</a>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
<?php endif; ?>
<?php $content = ob_get_clean(); ?>
<?php require dirname(__DIR__) . '/layout.php'; ?>
+35
View File
@@ -0,0 +1,35 @@
<?php ob_start(); ?>
<div class="d-flex justify-content-between align-items-center">
<h2><?= htmlspecialchars($device->name) ?></h2>
<div>
<a href="/devices/<?= $device->id ?>/edit" class="btn btn-sm btn-outline-primary">Изменить</a>
<form method="POST" action="/devices/<?= $device->id ?>/delete" class="d-inline" onsubmit="return confirm('Удалить устройство?')">
<button type="submit" class="btn btn-sm btn-outline-danger">Удалить</button>
</form>
</div>
</div>
<div class="row">
<div class="col-md-6">
<table class="table table-borderless">
<tr><th>Тип</th><td><?= htmlspecialchars($device->type) ?></td></tr>
<tr><th>IP</th><td><?= htmlspecialchars($device->primaryIp ?? '-') ?></td></tr>
<tr><th>MAC</th><td><?= htmlspecialchars($device->macAddress ?? '-') ?></td></tr>
<tr><th>Hostname</th><td><?= htmlspecialchars($device->hostname ?? '-') ?></td></tr>
<tr><th>Vendor</th><td><?= htmlspecialchars($device->vendor ?? '-') ?></td></tr>
<tr><th>OS</th><td><?= htmlspecialchars(($device->osName ?? '') . ' ' . ($device->osVersion ?? '')) ?></td></tr>
</table>
</div>
<div class="col-md-6">
<table class="table table-borderless">
<tr><th>Важность</th><td><?= htmlspecialchars($device->importance) ?></td></tr>
<tr><th>Статус</th><td><?= htmlspecialchars($device->status) ?></td></tr>
<tr><th>Location</th><td><?= htmlspecialchars($device->location ?? '-') ?></td></tr>
<tr><th>Описание</th><td><?= htmlspecialchars($device->description ?? '-') ?></td></tr>
<tr><th>Создано</th><td><?= $device->createdAt->format('Y-m-d H:i') ?></td></tr>
<tr><th>Обновлено</th><td><?= $device->updatedAt->format('Y-m-d H:i') ?></td></tr>
</table>
</div>
</div>
<?php $content = ob_get_clean(); ?>
<?php require dirname(__DIR__) . '/layout.php'; ?>
+5
View File
@@ -144,6 +144,11 @@
<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>