Итерация 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
+14
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);
+182
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
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'];
}
+125
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]);
}
}
+59
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);
}
}
@@ -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;
}
}