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>
131 lines
4.2 KiB
PHP
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;
|
|
}
|
|
}
|