Backup #349
| ID | 349 |
| Dateipfad | /var/www/dev.campus.systemische-tools.de/src/Controller/SystemExplorerController.php |
| Version | 19 |
| Typ |
modified |
| Größe | 16.7 KB |
| Hash | 09a596ecf76d98cace31a265dc47df0b47234345f072dc67ab3acd213a00a323 |
| Datum | 2025-12-22 08:15:01 |
| Geändert von | claude-code-hook |
| Grund | Claude Code Pre-Hook Backup vor Edit-Operation |
| Datei existiert |
Ja
|
Dateiinhalt
<?php
namespace Controller;
use Domain\ValueObject\Pagination;
use Framework\Controller;
use Infrastructure\Config\DatabaseFactory;
use Infrastructure\Docs\HybridSearchService;
use Infrastructure\Persistence\SystemExplorerRepository;
/**
* SystemExplorerController - Interne System-Dokumentation
*
* Zeigt Code-Dokumentation, Seiten und Chunks aus der dokumentation_chunks Pipeline.
* Für Admin/Entwickler - System-Wissen.
*/
class SystemExplorerController extends Controller
{
private \PDO $db;
private SystemExplorerRepository $repository;
public function __construct()
{
$this->repository = new SystemExplorerRepository();
$this->db = DatabaseFactory::dev();
}
/**
* GET /explorer
* Dashboard mit Statistiken
*/
public function index(): void
{
$this->view('system-explorer.index', [
'title' => 'Doc2Vector Explorer',
'dokumenteCount' => $this->repository->countDokumente(),
'seitenCount' => $this->repository->countSeiten(),
'chunkStats' => $this->repository->getChunkStats(),
'taxonomyCategories' => $this->repository->getTopTaxonomyCategories(10),
'dokumente' => $this->repository->getDokumenteWithStats(),
'recentChunks' => $this->repository->getRecentChunks(5),
]);
}
/**
* GET /explorer/dokumente
* Liste aller Hauptbereiche (depth=0)
*/
public function dokumente(): void
{
$dokumente = $this->db->query(
'SELECT d.id, d.title, d.path, d.content, d.created_at, d.updated_at,
(SELECT COUNT(*) FROM dokumentation WHERE parent_id = d.id) as seiten_count,
(SELECT COUNT(*) FROM dokumentation_chunks WHERE dokumentation_id IN
(SELECT id FROM dokumentation WHERE parent_id = d.id OR id = d.id)) as total_chunks,
(SELECT COALESCE(SUM(token_count), 0) FROM dokumentation_chunks WHERE dokumentation_id IN
(SELECT id FROM dokumentation WHERE parent_id = d.id OR id = d.id)) as total_tokens
FROM dokumentation d
WHERE d.depth = 0
ORDER BY d.title'
)->fetchAll();
$this->view('system-explorer.dokumente.index', [
'title' => 'Dokumente',
'dokumente' => $dokumente,
]);
}
/**
* GET /explorer/dokumente/{id}
* Dokument-Details mit Seiten und Chunks
*/
public function dokumentShow(int $id): void
{
$stmt = $this->db->prepare(
'SELECT * FROM dokumentation WHERE id = :id AND depth = 0'
);
$stmt->execute(['id' => $id]);
$dokument = $stmt->fetch();
if ($dokument === false) {
$this->notFound('Dokument nicht gefunden');
}
// Direkte Seiten (depth=1)
$stmt = $this->db->prepare(
'SELECT s.id, s.title, s.path, s.depth,
(SELECT COUNT(*) FROM dokumentation_chunks WHERE dokumentation_id = s.id) as chunks_count,
(SELECT COALESCE(SUM(token_count), 0) FROM dokumentation_chunks WHERE dokumentation_id = s.id) as token_count
FROM dokumentation s
WHERE s.parent_id = :id
ORDER BY s.title'
);
$stmt->execute(['id' => $id]);
$seiten = $stmt->fetchAll();
// Chunks direkt am Dokument
$stmt = $this->db->prepare(
'SELECT c.id, c.chunk_index, c.content, c.token_count, c.taxonomy_category,
c.entities, c.keywords, c.analysis_status, c.qdrant_id
FROM dokumentation_chunks c
WHERE c.dokumentation_id = :id
ORDER BY c.chunk_index'
);
$stmt->execute(['id' => $id]);
$chunks = $stmt->fetchAll();
// Taxonomie-Aggregation für dieses Dokument (inkl. Seiten)
$stmt = $this->db->prepare(
'SELECT taxonomy_category, COUNT(*) as count
FROM dokumentation_chunks
WHERE dokumentation_id IN (
SELECT id FROM dokumentation WHERE id = :id OR parent_id = :id2
)
AND taxonomy_category IS NOT NULL
GROUP BY taxonomy_category
ORDER BY count DESC'
);
$stmt->execute(['id' => $id, 'id2' => $id]);
$taxonomy = $stmt->fetchAll();
$this->view('system-explorer.dokumente.show', [
'title' => $dokument['title'],
'dokument' => $dokument,
'seiten' => $seiten,
'chunks' => $chunks,
'taxonomy' => $taxonomy,
]);
}
/**
* GET /explorer/seiten
* Liste aller Seiten (depth>0)
*/
public function seiten(): void
{
$search = $this->getString('search');
$parentId = $this->getString('parent');
$sql = 'SELECT s.id, s.title, s.path, s.depth, s.parent_id,
p.title as parent_title,
(SELECT COUNT(*) FROM dokumentation_chunks WHERE dokumentation_id = s.id) as chunks_count,
(SELECT COALESCE(SUM(token_count), 0) FROM dokumentation_chunks WHERE dokumentation_id = s.id) as token_count
FROM dokumentation s
LEFT JOIN dokumentation p ON s.parent_id = p.id
WHERE s.depth > 0';
$params = [];
if ($search !== '') {
$sql .= ' AND (s.title LIKE :search OR s.path LIKE :search2)';
$params['search'] = '%' . $search . '%';
$params['search2'] = '%' . $search . '%';
}
if ($parentId !== '') {
$sql .= ' AND s.parent_id = :parent';
$params['parent'] = (int) $parentId;
}
$sql .= ' ORDER BY p.title, s.depth, s.title';
$stmt = $this->db->prepare($sql);
$stmt->execute($params);
$seiten = $stmt->fetchAll();
// Dokumente für Filter
$dokumente = $this->db->query(
'SELECT id, title FROM dokumentation WHERE depth = 0 ORDER BY title'
)->fetchAll();
$this->view('system-explorer.seiten.index', [
'title' => 'Seiten',
'seiten' => $seiten,
'dokumente' => $dokumente,
'currentSearch' => $search,
'currentParent' => $parentId,
]);
}
/**
* GET /explorer/seiten/{id}
* Seiten-Details mit Chunks
*/
public function seiteShow(int $id): void
{
$stmt = $this->db->prepare(
'SELECT s.*, p.title as parent_title, p.path as parent_path
FROM dokumentation s
LEFT JOIN dokumentation p ON s.parent_id = p.id
WHERE s.id = :id'
);
$stmt->execute(['id' => $id]);
$seite = $stmt->fetch();
if ($seite === false) {
$this->notFound('Seite nicht gefunden');
}
// Chunks dieser Seite
$stmt = $this->db->prepare(
'SELECT c.id, c.chunk_index, c.content, c.token_count, c.taxonomy_category,
c.taxonomy_path, c.entities, c.keywords, c.analysis_status, c.qdrant_id,
c.heading_path, c.analyzed_at
FROM dokumentation_chunks c
WHERE c.dokumentation_id = :id
ORDER BY c.chunk_index'
);
$stmt->execute(['id' => $id]);
$chunks = $stmt->fetchAll();
// Unterseiten (falls depth=1)
$stmt = $this->db->prepare(
'SELECT id, title, path, depth FROM dokumentation WHERE parent_id = :id ORDER BY title'
);
$stmt->execute(['id' => $id]);
$unterseiten = $stmt->fetchAll();
// Breadcrumb aufbauen
$breadcrumb = $this->buildBreadcrumb($seite);
$this->view('system-explorer.seiten.show', [
'title' => $seite['title'],
'seite' => $seite,
'chunks' => $chunks,
'unterseiten' => $unterseiten,
'breadcrumb' => $breadcrumb,
]);
}
/**
* GET /explorer/chunks
* Liste aller Chunks mit Filtern
*/
public function chunks(): void
{
$category = $this->getString('category');
$status = $this->getString('status');
$search = $this->getString('search');
$pagination = Pagination::fromRequest(50);
$sql = 'SELECT c.id, c.chunk_index, c.content, c.token_count, c.taxonomy_category,
c.analysis_status, c.qdrant_id, c.created_at,
d.title as dokument_title, d.path as dokument_path
FROM dokumentation_chunks c
JOIN dokumentation d ON c.dokumentation_id = d.id
WHERE 1=1';
$countSql = 'SELECT COUNT(*) FROM dokumentation_chunks c
JOIN dokumentation d ON c.dokumentation_id = d.id
WHERE 1=1';
$params = [];
if ($category !== '') {
$sql .= ' AND c.taxonomy_category = :category';
$countSql .= ' AND c.taxonomy_category = :category';
$params['category'] = $category;
}
if ($status !== '') {
$sql .= ' AND c.analysis_status = :status';
$countSql .= ' AND c.analysis_status = :status';
$params['status'] = $status;
}
if ($search !== '') {
$sql .= ' AND (c.content LIKE :search OR c.keywords LIKE :search2)';
$countSql .= ' AND (c.content LIKE :search OR c.keywords LIKE :search2)';
$params['search'] = '%' . $search . '%';
$params['search2'] = '%' . $search . '%';
}
// Count total
$countStmt = $this->db->prepare($countSql);
$countStmt->execute($params);
$totalCount = (int) $countStmt->fetchColumn();
$pagination = $pagination->withTotal($totalCount);
$sql .= ' ORDER BY c.created_at DESC LIMIT :limit OFFSET :offset';
$stmt = $this->db->prepare($sql);
foreach ($params as $key => $value) {
$stmt->bindValue(':' . $key, $value);
}
$stmt->bindValue(':limit', $pagination->limit, \PDO::PARAM_INT);
$stmt->bindValue(':offset', $pagination->offset, \PDO::PARAM_INT);
$stmt->execute();
$chunks = $stmt->fetchAll();
// Kategorien für Filter
$categories = $this->db->query(
'SELECT DISTINCT taxonomy_category FROM dokumentation_chunks
WHERE taxonomy_category IS NOT NULL ORDER BY taxonomy_category'
)->fetchAll(\PDO::FETCH_COLUMN);
$this->view('system-explorer.chunks.index', [
'title' => 'Chunks',
'chunks' => $chunks,
'categories' => $categories,
'currentCategory' => $category,
'currentStatus' => $status,
'currentSearch' => $search,
'currentPage' => $pagination->page,
'totalCount' => $pagination->totalCount,
'totalPages' => $pagination->totalPages(),
]);
}
/**
* GET /explorer/chunks/{id}
* Chunk-Details
*/
public function chunkShow(int $id): void
{
$stmt = $this->db->prepare(
'SELECT c.*, d.title as dokument_title, d.path as dokument_path, d.id as dokument_id
FROM dokumentation_chunks c
JOIN dokumentation d ON c.dokumentation_id = d.id
WHERE c.id = :id'
);
$stmt->execute(['id' => $id]);
$chunk = $stmt->fetch();
if ($chunk === false) {
$this->notFound('Chunk nicht gefunden');
}
// JSON-Felder dekodieren
$chunk['entities_decoded'] = $this->decodeJson($chunk['entities'] ?? null);
$chunk['keywords_decoded'] = $this->decodeJson($chunk['keywords'] ?? null);
$chunk['taxonomy_path_decoded'] = $this->decodeJson($chunk['taxonomy_path'] ?? null);
$chunk['heading_path_decoded'] = $this->decodeJson($chunk['heading_path'] ?? null);
// Nachbar-Chunks
$stmt = $this->db->prepare(
'SELECT id, chunk_index FROM dokumentation_chunks
WHERE dokumentation_id = :doc_id AND chunk_index = :prev'
);
$stmt->execute(['doc_id' => $chunk['dokumentation_id'], 'prev' => $chunk['chunk_index'] - 1]);
$prevChunk = $stmt->fetch();
$stmt = $this->db->prepare(
'SELECT id, chunk_index FROM dokumentation_chunks
WHERE dokumentation_id = :doc_id AND chunk_index = :next'
);
$stmt->execute(['doc_id' => $chunk['dokumentation_id'], 'next' => $chunk['chunk_index'] + 1]);
$nextChunk = $stmt->fetch();
$this->view('system-explorer.chunks.show', [
'title' => 'Chunk #' . $chunk['id'],
'chunk' => $chunk,
'prevChunk' => $prevChunk,
'nextChunk' => $nextChunk,
]);
}
/**
* GET /explorer/taxonomie
* Taxonomie-Übersicht
*/
public function taxonomie(): void
{
// Kategorien mit Counts
$categories = $this->db->query(
'SELECT taxonomy_category, COUNT(*) as chunk_count,
COALESCE(SUM(token_count), 0) as token_count
FROM dokumentation_chunks
WHERE taxonomy_category IS NOT NULL
GROUP BY taxonomy_category
ORDER BY chunk_count DESC'
)->fetchAll();
// Top Keywords aggregiert
$keywordsRaw = $this->db->query(
'SELECT keywords FROM dokumentation_chunks WHERE keywords IS NOT NULL'
)->fetchAll(\PDO::FETCH_COLUMN);
$keywordCounts = [];
foreach ($keywordsRaw as $json) {
$keywords = $this->decodeJson($json);
foreach ($keywords as $kw) {
$kw = strtolower(trim($kw));
if ($kw !== '') {
$keywordCounts[$kw] = ($keywordCounts[$kw] ?? 0) + 1;
}
}
}
arsort($keywordCounts);
$topKeywords = array_slice($keywordCounts, 0, 30, true);
// Entities aggregiert
$entitiesRaw = $this->db->query(
'SELECT entities FROM dokumentation_chunks WHERE entities IS NOT NULL'
)->fetchAll(\PDO::FETCH_COLUMN);
$entityCounts = [];
foreach ($entitiesRaw as $json) {
$entities = $this->decodeJson($json);
foreach ($entities as $entity) {
$name = $entity['name'] ?? '';
$type = $entity['type'] ?? 'OTHER';
if ($name !== '') {
$key = $name . '|' . $type;
$entityCounts[$key] = ($entityCounts[$key] ?? 0) + 1;
}
}
}
arsort($entityCounts);
$topEntities = array_slice($entityCounts, 0, 30, true);
$this->view('system-explorer.taxonomie', [
'title' => 'Taxonomie & Entities',
'categories' => $categories,
'topKeywords' => $topKeywords,
'topEntities' => $topEntities,
]);
}
/**
* GET /explorer/suche
* Suche-Formular
*/
public function suche(): void
{
$query = $this->getString('q');
$category = $this->getString('category');
$limit = $this->getLimit(20, 10);
$results = [];
$suggestions = [];
if ($query !== '') {
$filters = [];
if ($category !== '') {
$filters['taxonomy_category'] = $category;
}
$search = new HybridSearchService();
$results = $search->search($query, $filters, $limit);
$suggestions = $search->suggestRelatedSearches($results);
}
// Kategorien für Filter
$categories = $this->db->query(
'SELECT DISTINCT taxonomy_category FROM dokumentation_chunks
WHERE taxonomy_category IS NOT NULL ORDER BY taxonomy_category'
)->fetchAll(\PDO::FETCH_COLUMN);
$this->view('system-explorer.suche', [
'title' => 'Dokumentation durchsuchen',
'query' => $query,
'results' => $results,
'suggestions' => $suggestions,
'categories' => $categories,
'currentCategory' => $category,
'limit' => $limit,
]);
}
/**
* Breadcrumb aufbauen
*/
private function buildBreadcrumb(array $seite): array
{
$breadcrumb = [];
$current = $seite;
while ($current !== null && $current !== false) {
array_unshift($breadcrumb, [
'id' => $current['id'],
'title' => $current['title'],
'path' => $current['path'],
'depth' => $current['depth'],
]);
if ($current['parent_id'] === null) {
break;
}
$stmt = $this->db->prepare('SELECT * FROM dokumentation WHERE id = :id');
$stmt->execute(['id' => $current['parent_id']]);
$current = $stmt->fetch();
}
return $breadcrumb;
}
}
Vollständig herunterladen
Aktionen
Andere Versionen dieser Datei
| ID |
Version |
Typ |
Größe |
Datum |
| 1231 |
35 |
modified |
6.8 KB |
2025-12-25 12:32 |
| 697 |
34 |
modified |
6.9 KB |
2025-12-23 07:53 |
| 642 |
33 |
modified |
6.9 KB |
2025-12-23 04:47 |
| 637 |
32 |
modified |
6.9 KB |
2025-12-23 04:46 |
| 636 |
31 |
modified |
6.8 KB |
2025-12-23 04:46 |
| 608 |
30 |
modified |
6.8 KB |
2025-12-23 04:39 |
| 587 |
29 |
modified |
6.8 KB |
2025-12-23 04:24 |
| 392 |
28 |
modified |
7.5 KB |
2025-12-22 08:40 |
| 391 |
27 |
modified |
7.6 KB |
2025-12-22 08:40 |
| 390 |
26 |
modified |
9.4 KB |
2025-12-22 08:39 |
| 389 |
25 |
modified |
10.4 KB |
2025-12-22 08:39 |
| 388 |
24 |
modified |
12.0 KB |
2025-12-22 08:37 |
| 362 |
23 |
modified |
13.0 KB |
2025-12-22 08:22 |
| 361 |
22 |
modified |
13.2 KB |
2025-12-22 08:22 |
| 360 |
21 |
modified |
14.9 KB |
2025-12-22 08:22 |
| 357 |
20 |
modified |
16.0 KB |
2025-12-22 08:20 |
| 349 |
19 |
modified |
16.7 KB |
2025-12-22 08:15 |
| 348 |
18 |
modified |
18.5 KB |
2025-12-22 08:14 |
| 347 |
17 |
modified |
18.4 KB |
2025-12-22 08:14 |
| 346 |
16 |
modified |
18.3 KB |
2025-12-22 08:14 |
| 342 |
15 |
modified |
18.2 KB |
2025-12-22 08:11 |
| 341 |
14 |
modified |
18.3 KB |
2025-12-22 08:11 |
| 340 |
13 |
modified |
18.2 KB |
2025-12-22 08:11 |
| 316 |
12 |
modified |
18.2 KB |
2025-12-22 08:06 |
| 315 |
11 |
modified |
18.2 KB |
2025-12-22 08:06 |
| 314 |
10 |
modified |
18.2 KB |
2025-12-22 08:06 |
| 297 |
9 |
modified |
18.2 KB |
2025-12-22 08:03 |
| 296 |
8 |
modified |
18.2 KB |
2025-12-22 08:03 |
| 290 |
7 |
modified |
18.3 KB |
2025-12-22 08:00 |
| 228 |
6 |
modified |
18.3 KB |
2025-12-22 01:44 |
| 227 |
5 |
modified |
18.4 KB |
2025-12-22 01:44 |
| 226 |
4 |
modified |
18.4 KB |
2025-12-22 01:44 |
| 136 |
3 |
modified |
18.2 KB |
2025-12-20 19:56 |
| 30 |
2 |
modified |
18.9 KB |
2025-12-20 17:23 |
| 25 |
1 |
modified |
18.9 KB |
2025-12-20 17:07 |
← Zurück zur Übersicht