monitor/api/src/Repository/AgentRepository.php
fds2610-bot 857fc88834 Phase 1 der Neuausrichtung: REST-API fuer Tickets/Events/Systeme/KPIs/Agenten
Setzt Phase 1 aus Visualization.md um: Vanilla-PHP-REST-API mit SQLite-Persistenz,
statischen API-Keys pro Agent, Codeberg-Sync fuer Backlog/Done, PHPUnit-Tests und
vollstaendiger API.md-Dokumentation - Vorgaben aus Detailantworten.md (F1-F16)
umgesetzt. Entwicklung/Build/Test laufen komplett in Docker-Compose-Containern
(PHP 8.2 + SQLite + Composer + PHPUnit/pcov), analog zum NC-Gantt-Projekt ist auf
dem Host kein PHP/Composer noetig.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-27 19:38:52 +02:00

131 lines
4.2 KiB
PHP

<?php
declare(strict_types=1);
namespace Monitor\Api\Repository;
use InvalidArgumentException;
use Monitor\Api\Auth\ApiKeyGenerator;
use Monitor\Api\Db\Database;
use Monitor\Api\Http\NotFoundException;
final class AgentRepository
{
/** @var string[] */
public const ROLES = ['entwickler', 'product_owner', 'qa', 'user'];
public function __construct(private readonly Database $db, private readonly int $deskCount = 8)
{
}
/**
* Registriert einen neuen Agenten und weist ihm bei der Registrierung fest einen
* Schreibtisch zu (F1/F2: "Agenten erhalten bei der Registrierung eine feste Zuordnung").
* Die Rolle "user" bekommt nie einen Schreibtisch (kein fester Sitzplatz, F2). Sind alle
* Schreibtische belegt, registriert sich der Agent trotzdem, aber ohne Schreibtisch
* ("kommen durch die Tuer, machen ihre Aufgaben und gehen wieder").
*
* @return array{agent: array<string, mixed>, api_key: string}
*/
public function register(string $displayName, string $role): array
{
if (!in_array($role, self::ROLES, true)) {
throw new InvalidArgumentException('Unbekannte Rolle: ' . $role);
}
$deskIndex = $role === 'user' ? null : $this->findFreeDesk();
$apiKey = ApiKeyGenerator::generate();
$now = $this->db->now();
$stmt = $this->db->pdo()->prepare(
'INSERT INTO agents (display_name, role, api_key_hash, desk_index, state, created_at, last_seen_at)
VALUES (:display_name, :role, :api_key_hash, :desk_index, :state, :created_at, :created_at)'
);
$stmt->execute([
'display_name' => $displayName,
'role' => $role,
'api_key_hash' => ApiKeyGenerator::hash($apiKey),
'desk_index' => $deskIndex,
'state' => 'idle',
'created_at' => $now,
]);
$agent = $this->findById((int) $this->db->pdo()->lastInsertId());
return ['agent' => $agent, 'api_key' => $apiKey];
}
private function findFreeDesk(): ?int
{
$stmt = $this->db->pdo()->query(
'SELECT desk_index FROM agents WHERE desk_index IS NOT NULL'
);
$occupied = array_map('intval', array_column($stmt->fetchAll(), 'desk_index'));
for ($i = 0; $i < $this->deskCount; $i++) {
if (!in_array($i, $occupied, true)) {
return $i;
}
}
return null;
}
/** @return array<string, mixed>|null */
public function findById(int $id): ?array
{
$stmt = $this->db->pdo()->prepare('SELECT * FROM agents WHERE id = :id');
$stmt->execute(['id' => $id]);
$row = $stmt->fetch();
return $row === false ? null : $this->hidingKey($row);
}
/** @return array<string, mixed>|null */
public function findByApiKey(string $apiKey): ?array
{
$stmt = $this->db->pdo()->prepare('SELECT * FROM agents WHERE api_key_hash = :hash');
$stmt->execute(['hash' => ApiKeyGenerator::hash($apiKey)]);
$row = $stmt->fetch();
return $row === false ? null : $this->hidingKey($row);
}
/** @return array<int, array<string, mixed>> */
public function all(): array
{
$stmt = $this->db->pdo()->query('SELECT * FROM agents ORDER BY id ASC');
return array_map(fn (array $row) => $this->hidingKey($row), $stmt->fetchAll());
}
/** @return array<string, mixed> */
public function updateActivity(int $id, string $state, ?int $currentTicketId): array
{
$stmt = $this->db->pdo()->prepare(
'UPDATE agents SET state = :state, current_ticket_id = :ticket_id, last_seen_at = :now WHERE id = :id'
);
$stmt->execute([
'state' => $state,
'ticket_id' => $currentTicketId,
'now' => $this->db->now(),
'id' => $id,
]);
$agent = $this->findById($id);
if ($agent === null) {
throw new NotFoundException('Agent nicht gefunden: ' . $id);
}
return $agent;
}
/** @param array<string, mixed> $row @return array<string, mixed> */
private function hidingKey(array $row): array
{
unset($row['api_key_hash']);
return $row;
}
}