Итерация 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:
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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'];
|
||||
}
|
||||
@@ -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]);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user