db = new \PDO( 'mysql:host=localhost;dbname=ki_dev;charset=utf8mb4', 'root', $this->getPassword(), [ \PDO::ATTR_ERRMODE => \PDO::ERRMODE_EXCEPTION, \PDO::ATTR_DEFAULT_FETCH_MODE => \PDO::FETCH_ASSOC, ] ); } private function getPassword(): string { $file = '/var/www/docs/credentials/credentials.md'; $content = file_get_contents($file); foreach (explode("\n", $content) as $line) { if (str_contains($line, 'MariaDB') && str_contains($line, 'root')) { $parts = explode('|', $line); if (count($parts) >= 4) { return trim($parts[3]); } } } return ''; } /** * GET /protokoll * Liste aller Protokoll-Einträge */ public function index(): void { // Statistiken $stats = $this->db->query( 'SELECT COUNT(*) as total, SUM(CASE WHEN status = "completed" THEN 1 ELSE 0 END) as completed, SUM(CASE WHEN status = "error" THEN 1 ELSE 0 END) as errors, COALESCE(SUM(tokens_total), 0) as tokens_total, COALESCE(SUM(duration_ms), 0) as duration_total FROM protokoll' )->fetch(); // Einträge mit Pagination $page = max(1, (int) ($_GET['page'] ?? 1)); $limit = 50; $offset = ($page - 1) * $limit; $search = $_GET['search'] ?? ''; $status = $_GET['status'] ?? ''; $model = $_GET['model'] ?? ''; $sql = 'SELECT id, timestamp, client_name, model_name, status, tokens_total, duration_ms, LEFT(request, 200) as request_preview FROM protokoll WHERE 1=1'; $countSql = 'SELECT COUNT(*) FROM protokoll WHERE 1=1'; $params = []; if ($search !== '') { $sql .= ' AND (request LIKE :search OR response LIKE :search2 OR client_name LIKE :search3)'; $countSql .= ' AND (request LIKE :search OR response LIKE :search2 OR client_name LIKE :search3)'; $params['search'] = '%' . $search . '%'; $params['search2'] = '%' . $search . '%'; $params['search3'] = '%' . $search . '%'; } if ($status !== '') { $sql .= ' AND status = :status'; $countSql .= ' AND status = :status'; $params['status'] = $status; } if ($model !== '') { $sql .= ' AND model_name = :model'; $countSql .= ' AND model_name = :model'; $params['model'] = $model; } // Total count $countStmt = $this->db->prepare($countSql); $countStmt->execute($params); $totalCount = (int) $countStmt->fetchColumn(); // Fetch entries $sql .= ' ORDER BY timestamp DESC LIMIT ' . $limit . ' OFFSET ' . $offset; $stmt = $this->db->prepare($sql); $stmt->execute($params); $entries = $stmt->fetchAll(); // Modelle für Filter $models = $this->db->query( 'SELECT DISTINCT model_name FROM protokoll WHERE model_name IS NOT NULL ORDER BY model_name' )->fetchAll(\PDO::FETCH_COLUMN); $this->view('protokoll.index', [ 'title' => 'KI-Protokoll', 'stats' => $stats, 'entries' => $entries, 'models' => $models, 'currentSearch' => $search, 'currentStatus' => $status, 'currentModel' => $model, 'currentPage' => $page, 'totalCount' => $totalCount, 'totalPages' => ceil($totalCount / $limit), ]); } /** * GET /protokoll/{id} * Detail-Ansicht eines Eintrags */ public function show(string $id): void { $stmt = $this->db->prepare('SELECT * FROM protokoll WHERE id = :id'); $stmt->execute(['id' => (int) $id]); $entry = $stmt->fetch(); if ($entry === false) { http_response_code(404); echo '404 - Protokoll-Eintrag nicht gefunden'; return; } // Request/Response als formatiertes JSON $entry['request_formatted'] = $this->formatJson($entry['request']); $entry['response_formatted'] = $this->formatJson($entry['response']); // Nachbar-Einträge für Navigation $stmt = $this->db->prepare('SELECT id FROM protokoll WHERE id < :id ORDER BY id DESC LIMIT 1'); $stmt->execute(['id' => (int) $id]); $prevEntry = $stmt->fetch(); $stmt = $this->db->prepare('SELECT id FROM protokoll WHERE id > :id ORDER BY id ASC LIMIT 1'); $stmt->execute(['id' => (int) $id]); $nextEntry = $stmt->fetch(); $this->view('protokoll.show', [ 'title' => 'Protokoll #' . $id, 'entry' => $entry, 'prevEntry' => $prevEntry, 'nextEntry' => $nextEntry, ]); } /** * JSON formatieren für Anzeige */ private function formatJson(?string $json): string { if ($json === null || $json === '') { return '-'; } $decoded = json_decode($json, true); if ($decoded === null) { return htmlspecialchars($json); } return json_encode($decoded, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE); } }