Итерация 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:
mirivlad 2026-05-26 07:50:37 +08:00
parent 177e44f015
commit 15772bc17e
13 changed files with 760 additions and 28 deletions

View File

@ -9,10 +9,24 @@ use Psr\Http\Message\ServerRequestInterface;
class DashboardController
{
private \Domovoy\Repositories\DeviceRepository $deviceRepository;
private \Domovoy\Repositories\ScanJobRepository $scanJobRepository;
public function __construct(
\Domovoy\Repositories\DeviceRepository $deviceRepository,
\Domovoy\Repositories\ScanJobRepository $scanJobRepository
) {
$this->deviceRepository = $deviceRepository;
$this->scanJobRepository = $scanJobRepository;
}
public function index(ServerRequestInterface $request, ResponseInterface $response): ResponseInterface
{
ob_start();
$username = $_SESSION['username'] ?? 'User';
$deviceCount = $this->deviceRepository->getCount();
$newDiscoveries = $this->deviceRepository->getNewDiscoveriesCount();
$recentScans = $this->scanJobRepository->findRecent(5);
require dirname(__DIR__, 2) . '/templates/dashboard/index.php';
$body = ob_get_clean();
$response->getBody()->write($body);

View File

@ -0,0 +1,182 @@
<?php
declare(strict_types=1);
namespace Domovoy\Controllers;
use Domovoy\Models\Device;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
class DeviceController
{
private \Domovoy\Services\Inventory\DeviceService $deviceService;
private \Domovoy\Repositories\DiscoveredHostRepository $discoveredHostRepository;
public function __construct(
\Domovoy\Services\Inventory\DeviceService $deviceService,
\Domovoy\Repositories\DiscoveredHostRepository $discoveredHostRepository
) {
$this->deviceService = $deviceService;
$this->discoveredHostRepository = $discoveredHostRepository;
}
public function index(ServerRequestInterface $request, ResponseInterface $response): ResponseInterface
{
ob_start();
$username = $_SESSION['username'] ?? 'User';
$devices = $this->deviceService->getAllDevices();
require dirname(__DIR__, 2) . '/templates/devices/index.php';
$body = ob_get_clean();
$response->getBody()->write($body);
return $response;
}
public function createForm(ServerRequestInterface $request, ResponseInterface $response): ResponseInterface
{
ob_start();
$username = $_SESSION['username'] ?? 'User';
$device = new Device();
$types = Device::$types;
$importances = Device::$importances;
require dirname(__DIR__, 2) . '/templates/devices/form.php';
$body = ob_get_clean();
$response->getBody()->write($body);
return $response;
}
public function create(ServerRequestInterface $request, ResponseInterface $response): ResponseInterface
{
$data = $request->getParsedBody();
$device = new Device();
$device->name = trim($data['name'] ?? '');
$device->type = $data['type'] ?? 'unknown';
$device->description = trim($data['description'] ?? '') ?: null;
$device->primaryIp = trim($data['primary_ip'] ?? '') ?: null;
$device->macAddress = trim($data['mac_address'] ?? '') ?: null;
$device->hostname = trim($data['hostname'] ?? '') ?: null;
$device->vendor = trim($data['vendor'] ?? '') ?: null;
$device->osName = trim($data['os_name'] ?? '') ?: null;
$device->osVersion = trim($data['os_version'] ?? '') ?: null;
$device->location = trim($data['location'] ?? '') ?: null;
$device->importance = $data['importance'] ?? 'normal';
$this->deviceService->updateDevice($device);
return $response
->withHeader('Location', '/devices')
->withStatus(302);
}
public function show(ServerRequestInterface $request, ResponseInterface $response, array $args): ResponseInterface
{
$id = (int)$args['id'];
$device = $this->deviceService->getDevice($id);
if ($device === null) {
return $response->withStatus(404)->write('Device not found');
}
ob_start();
$username = $_SESSION['username'] ?? 'User';
$types = Device::$types;
$importances = Device::$importances;
require dirname(__DIR__, 2) . '/templates/devices/show.php';
$body = ob_get_clean();
$response->getBody()->write($body);
return $response;
}
public function editForm(ServerRequestInterface $request, ResponseInterface $response, array $args): ResponseInterface
{
$id = (int)$args['id'];
$device = $this->deviceService->getDevice($id);
if ($device === null) {
return $response->withStatus(404)->write('Device not found');
}
ob_start();
$username = $_SESSION['username'] ?? 'User';
$types = Device::$types;
$importances = Device::$importances;
require dirname(__DIR__, 2) . '/templates/devices/form.php';
$body = ob_get_clean();
$response->getBody()->write($body);
return $response;
}
public function update(ServerRequestInterface $request, ResponseInterface $response, array $args): ResponseInterface
{
$id = (int)$args['id'];
$device = $this->deviceService->getDevice($id);
if ($device === null) {
return $response->withStatus(404)->write('Device not found');
}
$data = $request->getParsedBody();
$device->name = trim($data['name'] ?? '');
$device->type = $data['type'] ?? 'unknown';
$device->description = trim($data['description'] ?? '') ?: null;
$device->primaryIp = trim($data['primary_ip'] ?? '') ?: null;
$device->macAddress = trim($data['mac_address'] ?? '') ?: null;
$device->hostname = trim($data['hostname'] ?? '') ?: null;
$device->vendor = trim($data['vendor'] ?? '') ?: null;
$device->osName = trim($data['os_name'] ?? '') ?: null;
$device->osVersion = trim($data['os_version'] ?? '') ?: null;
$device->location = trim($data['location'] ?? '') ?: null;
$device->importance = $data['importance'] ?? 'normal';
$device->status = $data['status'] ?? 'active';
$this->deviceService->updateDevice($device);
return $response
->withHeader('Location', '/devices/' . $device->id)
->withStatus(302);
}
public function delete(ServerRequestInterface $request, ResponseInterface $response, array $args): ResponseInterface
{
$id = (int)$args['id'];
$this->deviceService->deleteDevice($id);
return $response
->withHeader('Location', '/devices')
->withStatus(302);
}
public function createFromHost(ServerRequestInterface $request, ResponseInterface $response): ResponseInterface
{
$data = $request->getParsedBody();
$hostId = $data['host_id'] ?? null;
$name = trim($data['name'] ?? '');
if ($hostId === null || $name === '') {
return $response->withStatus(400)->write('Host ID and name required');
}
$host = $this->discoveredHostRepository->findById((int)$hostId);
if ($host === null) {
return $response->withStatus(404)->write('Host not found');
}
$device = $this->deviceService->createFromDiscoveredHost(
$name,
$host->ipAddress,
$host->macAddress,
$host->hostname,
$host->vendor
);
// Mark host as accepted
$host->status = 'accepted';
$host->matchedDeviceId = (string)$device->id;
$this->discoveredHostRepository->save($host);
return $response
->withHeader('Location', '/devices/' . $device->id)
->withStatus(302);
}
}

52
app/Models/Device.php Normal file
View File

@ -0,0 +1,52 @@
<?php
declare(strict_types=1);
namespace Domovoy\Models;
class Device
{
public ?int $id = null;
public string $name = '';
public string $type = 'unknown';
public ?string $description = null;
public ?string $primaryIp = null;
public ?string $macAddress = null;
public ?string $hostname = null;
public ?string $vendor = null;
public ?string $osName = null;
public ?string $osVersion = null;
public ?string $location = null;
public string $importance = 'normal';
public string $status = 'active';
public ?\DateTimeImmutable $createdAt = null;
public ?\DateTimeImmutable $updatedAt = null;
public static function fromArray(array $data): self
{
$obj = new self();
$obj->id = (int)$data['id'];
$obj->name = $data['name'];
$obj->type = $data['type'];
$obj->description = $data['description'] ?? null;
$obj->primaryIp = $data['primary_ip'] ?? null;
$obj->macAddress = $data['mac_address'] ?? null;
$obj->hostname = $data['hostname'] ?? null;
$obj->vendor = $data['vendor'] ?? null;
$obj->osName = $data['os_name'] ?? null;
$obj->osVersion = $data['os_version'] ?? null;
$obj->location = $data['location'] ?? null;
$obj->importance = $data['importance'];
$obj->status = $data['status'];
$obj->createdAt = new \DateTimeImmutable($data['created_at']);
$obj->updatedAt = new \DateTimeImmutable($data['updated_at']);
return $obj;
}
public static array $types = [
'server', 'router', 'nas', 'desktop', 'laptop',
'phone', 'printer', 'iot', 'vm', 'container_host', 'unknown',
];
public static array $importances = ['critical', 'high', 'normal', 'low'];
}

View File

@ -0,0 +1,125 @@
<?php
declare(strict_types=1);
namespace Domovoy\Repositories;
use Domovoy\Models\Device;
use PDO;
class DeviceRepository
{
private PDO $pdo;
public function __construct(PDO $pdo)
{
$this->pdo = $pdo;
}
public function findById(int $id): ?Device
{
$stmt = $this->pdo->prepare('SELECT * FROM devices WHERE id = :id');
$stmt->execute(['id' => $id]);
$row = $stmt->fetch();
return $row ? Device::fromArray($row) : null;
}
public function findAll(string $sortBy = 'name'): array
{
$allowed = ['name', 'type', 'status', 'primary_ip', 'created_at'];
$sort = in_array($sortBy, $allowed, true) ? $sortBy : 'name';
$stmt = $this->pdo->query("SELECT * FROM devices ORDER BY {$sort} ASC");
$results = [];
while ($row = $stmt->fetch()) {
$results[] = Device::fromArray($row);
}
return $results;
}
public function findByMac(string $macAddress): ?Device
{
$stmt = $this->pdo->prepare('SELECT * FROM devices WHERE mac_address = :mac');
$stmt->execute(['mac' => strtolower($macAddress)]);
$row = $stmt->fetch();
return $row ? Device::fromArray($row) : null;
}
public function findByName(string $name): ?Device
{
$stmt = $this->pdo->prepare('SELECT * FROM devices WHERE name = :name');
$stmt->execute(['name' => $name]);
$row = $stmt->fetch();
return $row ? Device::fromArray($row) : null;
}
public function getCount(): int
{
return (int)$this->pdo->query('SELECT COUNT(*) FROM devices')->fetchColumn();
}
public function getNewDiscoveriesCount(): int
{
$stmt = $this->pdo->query("SELECT COUNT(*) FROM discovered_hosts WHERE status = 'new'");
return (int)$stmt->fetchColumn();
}
public function save(Device $device): void
{
$now = (new \DateTimeImmutable())->format('Y-m-d H:i:s');
if ($device->id === null) {
$stmt = $this->pdo->prepare(
'INSERT INTO devices (name, type, description, primary_ip, mac_address, hostname,
vendor, os_name, os_version, location, importance, status, created_at, updated_at)
VALUES (:name, :type, :description, :primary_ip, :mac_address, :hostname,
:vendor, :os_name, :os_version, :location, :importance, :status, :created_at, :updated_at)'
);
$stmt->execute([
'name' => $device->name,
'type' => $device->type,
'description' => $device->description,
'primary_ip' => $device->primaryIp,
'mac_address' => $device->macAddress !== null ? strtolower($device->macAddress) : null,
'hostname' => $device->hostname,
'vendor' => $device->vendor,
'os_name' => $device->osName,
'os_version' => $device->osVersion,
'location' => $device->location,
'importance' => $device->importance,
'status' => $device->status,
'created_at' => $now,
'updated_at' => $now,
]);
$device->id = (int)$this->pdo->lastInsertId();
} else {
$stmt = $this->pdo->prepare(
'UPDATE devices SET name = :name, type = :type, description = :description,
primary_ip = :primary_ip, mac_address = :mac_address, hostname = :hostname,
vendor = :vendor, os_name = :os_name, os_version = :os_version,
location = :location, importance = :importance, status = :status,
updated_at = :updated_at WHERE id = :id'
);
$stmt->execute([
'id' => $device->id,
'name' => $device->name,
'type' => $device->type,
'description' => $device->description,
'primary_ip' => $device->primaryIp,
'mac_address' => $device->macAddress !== null ? strtolower($device->macAddress) : null,
'hostname' => $device->hostname,
'vendor' => $device->vendor,
'os_name' => $device->osName,
'os_version' => $device->osVersion,
'location' => $device->location,
'importance' => $device->importance,
'status' => $device->status,
'updated_at' => $now,
]);
}
}
public function delete(int $id): void
{
$stmt = $this->pdo->prepare('DELETE FROM devices WHERE id = :id');
$stmt->execute(['id' => $id]);
}
}

View File

@ -0,0 +1,59 @@
<?php
declare(strict_types=1);
namespace Domovoy\Services\Inventory;
use Domovoy\Models\Device;
use Domovoy\Repositories\DeviceRepository;
class DeviceService
{
private DeviceRepository $deviceRepository;
public function __construct(DeviceRepository $deviceRepository)
{
$this->deviceRepository = $deviceRepository;
}
public function createFromDiscoveredHost(
string $name,
string $ipAddress,
?string $macAddress = null,
?string $hostname = null,
?string $vendor = null
): Device {
$device = new Device();
$device->name = $name;
$device->primaryIp = $ipAddress;
$device->macAddress = $macAddress;
$device->hostname = $hostname;
$device->vendor = $vendor;
$device->type = 'unknown';
$device->status = 'active';
$device->importance = 'normal';
$this->deviceRepository->save($device);
return $device;
}
public function getAllDevices(): array
{
return $this->deviceRepository->findAll();
}
public function getDevice(int $id): ?Device
{
return $this->deviceRepository->findById($id);
}
public function updateDevice(Device $device): void
{
$this->deviceRepository->save($device);
}
public function deleteDevice(int $id): void
{
$this->deviceRepository->delete($id);
}
}

View File

@ -0,0 +1,58 @@
<?php
declare(strict_types=1);
namespace Domovoy\Services\Inventory;
use Domovoy\Models\DiscoveredHost;
use Domovoy\Repositories\DeviceRepository;
class MergeSuggestionService
{
private DeviceRepository $deviceRepository;
public function __construct(DeviceRepository $deviceRepository)
{
$this->deviceRepository = $deviceRepository;
}
/**
* Find merge suggestions for a discovered host.
* Returns array of ['device' => Device, 'confidence' => int, 'reason' => string]
*/
public function findSuggestions(DiscoveredHost $host): array
{
$suggestions = [];
// High confidence: MAC address match
if ($host->macAddress !== null) {
$device = $this->deviceRepository->findByMac($host->macAddress);
if ($device !== null) {
$suggestions[] = [
'device' => $device,
'confidence' => 90,
'reason' => 'MAC адрес совпадает',
];
}
}
// Medium confidence: hostname match
if ($host->hostname !== null) {
$device = $this->deviceRepository->findByName($host->hostname);
if ($device !== null) {
$suggestions[] = [
'device' => $device,
'confidence' => 60,
'reason' => 'Hostname совпадает',
];
}
}
// Low confidence: IP match
if ($host->primaryIp !== null) {
// Would need findByIp in repository — skip for now
}
return $suggestions;
}
}

View File

@ -0,0 +1,33 @@
<?php
declare(strict_types=1);
use Phinx\Migration\AbstractMigration;
final class CreateDevices extends AbstractMigration
{
public function change(): void
{
$table = $this->table('devices');
$table
->addColumn('name', 'string', ['limit' => 255, 'null' => false])
->addColumn('type', 'string', ['limit' => 30, 'null' => false, 'default' => 'unknown'])
->addColumn('description', 'text', ['null' => true])
->addColumn('primary_ip', 'string', ['limit' => 45, 'null' => true])
->addColumn('mac_address', 'string', ['limit' => 17, 'null' => true])
->addColumn('hostname', 'string', ['limit' => 255, 'null' => true])
->addColumn('vendor', 'string', ['limit' => 255, 'null' => true])
->addColumn('os_name', 'string', ['limit' => 255, 'null' => true])
->addColumn('os_version', 'string', ['limit' => 255, 'null' => true])
->addColumn('location', 'string', ['limit' => 255, 'null' => true])
->addColumn('importance', 'string', ['limit' => 20, 'null' => false, 'default' => 'normal'])
->addColumn('status', 'string', ['limit' => 20, 'null' => false, 'default' => 'active'])
->addColumn('created_at', 'datetime', ['null' => false, 'default' => 'CURRENT_TIMESTAMP'])
->addColumn('updated_at', 'datetime', ['null' => false, 'default' => 'CURRENT_TIMESTAMP'])
->addIndex(['primary_ip'])
->addIndex(['mac_address'], ['unique' => true])
->addIndex(['type'])
->addIndex(['status'])
->create();
}
}

View File

@ -114,8 +114,11 @@ $containerBuilder->addDefinitions([
\Domovoy\Controllers\SetupController::class => function ($c) {
return new \Domovoy\Controllers\SetupController($c->get(\Domovoy\Services\AuthService::class));
},
\Domovoy\Controllers\DashboardController::class => function () {
return new \Domovoy\Controllers\DashboardController();
\Domovoy\Controllers\DashboardController::class => function ($c) {
return new \Domovoy\Controllers\DashboardController(
$c->get(\Domovoy\Repositories\DeviceRepository::class),
$c->get(\Domovoy\Repositories\ScanJobRepository::class)
);
},
\Domovoy\Controllers\DiscoveryController::class => function ($c) {
return new \Domovoy\Controllers\DiscoveryController(
@ -129,6 +132,22 @@ $containerBuilder->addDefinitions([
$c->get(\Domovoy\Repositories\NetworkRangeRepository::class)
);
},
// Inventory
\Domovoy\Repositories\DeviceRepository::class => function ($c) {
return new \Domovoy\Repositories\DeviceRepository($c->get(PDO::class));
},
\Domovoy\Services\Inventory\DeviceService::class => function ($c) {
return new \Domovoy\Services\Inventory\DeviceService($c->get(\Domovoy\Repositories\DeviceRepository::class));
},
\Domovoy\Services\Inventory\MergeSuggestionService::class => function ($c) {
return new \Domovoy\Services\Inventory\MergeSuggestionService($c->get(\Domovoy\Repositories\DeviceRepository::class));
},
\Domovoy\Controllers\DeviceController::class => function ($c) {
return new \Domovoy\Controllers\DeviceController(
$c->get(\Domovoy\Services\Inventory\DeviceService::class),
$c->get(\Domovoy\Repositories\DiscoveredHostRepository::class)
);
},
]);
$container = $containerBuilder->build();
@ -161,6 +180,16 @@ $app->group('', function (\Slim\Routing\RouteCollectorProxy $group) {
$group->post('/discovery/ranges/create', [\Domovoy\Controllers\NetworkRangeController::class, 'create'])->setName('discovery.ranges.create');
$group->post('/discovery/ranges/toggle', [\Domovoy\Controllers\NetworkRangeController::class, 'toggle'])->setName('discovery.ranges.toggle');
$group->post('/discovery/ranges/delete', [\Domovoy\Controllers\NetworkRangeController::class, 'delete'])->setName('discovery.ranges.delete');
// Devices CRUD
$group->get('/devices', [\Domovoy\Controllers\DeviceController::class, 'index'])->setName('devices');
$group->get('/devices/create', [\Domovoy\Controllers\DeviceController::class, 'createForm'])->setName('devices.create');
$group->post('/devices/create', [\Domovoy\Controllers\DeviceController::class, 'create'])->setName('devices.create.post');
$group->get('/devices/{id}', [\Domovoy\Controllers\DeviceController::class, 'show'])->setName('devices.show');
$group->get('/devices/{id}/edit', [\Domovoy\Controllers\DeviceController::class, 'editForm'])->setName('devices.edit');
$group->post('/devices/{id}/update', [\Domovoy\Controllers\DeviceController::class, 'update'])->setName('devices.update');
$group->post('/devices/{id}/delete', [\Domovoy\Controllers\DeviceController::class, 'delete'])->setName('devices.delete');
$group->post('/devices/from-host', [\Domovoy\Controllers\DeviceController::class, 'createFromHost'])->setName('devices.from_host');
});
$app->run();

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'; ?>

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'; ?>

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'; ?>

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'; ?>

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>