optimize dashboard

This commit is contained in:
team2
2026-03-01 16:52:50 +01:00
parent eb9ab2ec48
commit 6d1ab87f75
7 changed files with 206 additions and 94 deletions

View File

@@ -190,6 +190,12 @@ services:
App\Tag\TagRoutingService: ~ App\Tag\TagRoutingService: ~
App\Tag\TagVectorIndexHealthService:
arguments:
$tagsNdjsonPath: '%mto.knowledge.tags_ndjson%'
$vectorTagsIndexPath: '%mto.knowledge.vector_tags_index%'
$vectorTagsMetaPath: '%mto.knowledge.vector_tags_index_meta%'
# ------------------------------------------------------------ # ------------------------------------------------------------
# Tag Rebuild Jobs # Tag Rebuild Jobs
# ------------------------------------------------------------ # ------------------------------------------------------------

View File

@@ -5,6 +5,7 @@ namespace App\Controller\Admin;
use App\Index\IndexMetaManager; use App\Index\IndexMetaManager;
use App\Ingest\IngestFlow; use App\Ingest\IngestFlow;
use App\Tag\TagVectorIndexHealthService;
use App\Vector\VectorIndexHealthService; use App\Vector\VectorIndexHealthService;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\RedirectResponse; use Symfony\Component\HttpFoundation\RedirectResponse;
@@ -23,7 +24,7 @@ final class DashboardController extends AbstractController
#[Route('/admin/dashboard', name: 'admin_dashboard')] #[Route('/admin/dashboard', name: 'admin_dashboard')]
public function dashboard(IndexMetaManager $metaManager,VectorIndexHealthService $health): Response public function dashboard(IndexMetaManager $metaManager,VectorIndexHealthService $health,TagVectorIndexHealthService $tagHealth): Response
{ {
$chunkCount = $metaManager->getRuntimeChunkCount(); $chunkCount = $metaManager->getRuntimeChunkCount();
$limit = IngestFlow::CHUNK_LIMIT_HARD; $limit = IngestFlow::CHUNK_LIMIT_HARD;
@@ -32,6 +33,9 @@ final class DashboardController extends AbstractController
'chunkCount' => $chunkCount, 'chunkCount' => $chunkCount,
'chunkLimit' => $limit, 'chunkLimit' => $limit,
'vectorHealth' => $health->check(), 'vectorHealth' => $health->check(),
'tagVectorHealth' => $tagHealth->check(),
]); ]);
} }
} }

View File

@@ -0,0 +1,68 @@
<?php
declare(strict_types=1);
namespace App\Tag;
final readonly class TagVectorIndexHealthService
{
public function __construct(
private string $tagsNdjsonPath,
private string $vectorTagsIndexPath,
private string $vectorTagsMetaPath
) {}
public function check(): array
{
$ndjsonExists = is_file($this->tagsNdjsonPath);
$vectorExists = is_file($this->vectorTagsIndexPath);
$metaExists = is_file($this->vectorTagsMetaPath);
$ndjsonTagCount = 0;
if ($ndjsonExists) {
$h = @fopen($this->tagsNdjsonPath, 'r');
if ($h !== false) {
while (($line = fgets($h)) !== false) {
$line = trim($line);
if ($line === '') continue;
$data = json_decode($line, true);
if (is_array($data) && !empty($data['tag_id']) && !empty($data['text'])) {
$ndjsonTagCount++;
}
}
fclose($h);
}
}
$vectorTagCount = 0;
if ($metaExists) {
$meta = json_decode((string) file_get_contents($this->vectorTagsMetaPath), true);
if (is_array($meta)) {
$vectorTagCount = count($meta);
}
}
$status = $this->determineStatus($ndjsonTagCount, $vectorExists, $metaExists, $vectorTagCount);
return [
'tags_ndjson_exists' => $ndjsonExists,
'tags_ndjson_count' => $ndjsonTagCount,
'vector_exists' => $vectorExists,
'meta_exists' => $metaExists,
'vector_tag_count' => $vectorTagCount,
'status' => $status,
];
}
private function determineStatus(int $ndjsonTagCount, bool $vectorExists, bool $metaExists, int $vectorTagCount): string
{
if ($ndjsonTagCount === 0 && !$vectorExists && !$metaExists) return 'OK_EMPTY';
if ($ndjsonTagCount > 0 && $vectorExists && $metaExists && $vectorTagCount === $ndjsonTagCount) return 'OK';
if ($ndjsonTagCount === 0 && ($vectorExists || $metaExists)) return 'INCONSISTENT_STALE_VECTOR';
if ($ndjsonTagCount > 0 && (!$vectorExists || !$metaExists)) return 'INCONSISTENT_MISSING_VECTOR';
if ($ndjsonTagCount !== $vectorTagCount) return 'INCONSISTENT_COUNT_MISMATCH';
return 'UNKNOWN';
}
}

View File

@@ -4,7 +4,7 @@ declare(strict_types=1);
namespace App\Vector; namespace App\Vector;
final class VectorIndexHealthService final readonly class VectorIndexHealthService
{ {
public function __construct( public function __construct(
private string $indexNdjsonPath, private string $indexNdjsonPath,

View File

@@ -6,7 +6,9 @@
<title>{% block title %}Admin{% endblock %}</title> <title>{% block title %}Admin{% endblock %}</title>
{% block stylesheets %} {% block stylesheets %}
<link href="/assets/styles/bootstrap.min.css" rel="stylesheet"/> <link href="/assets/styles/bootstrap.min.css" rel="stylesheet"/>
<link href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.css" rel="stylesheet">
<link rel="stylesheet" href="/assets/styles/base.css"> <link rel="stylesheet" href="/assets/styles/base.css">
<link rel="stylesheet" href="{{ asset('/assets/styles/admin-markdown.css') }}"> <link rel="stylesheet" href="{{ asset('/assets/styles/admin-markdown.css') }}">
{% endblock %} {% endblock %}
</head> </head>
@@ -51,11 +53,11 @@
{% set route = app.request.attributes.get('_route') %} {% set route = app.request.attributes.get('_route') %}
<nav class="nav flex-column small"> <nav class="nav flex-column">
<a class="nav-link text-light {% if route starts with 'admin_dashboard' %}active fw-bold{% endif %}" <a class="nav-link text-light {% if route starts with 'admin_dashboard' %}active fw-bold{% endif %}"
href="{{ path('admin_dashboard') }}"> href="{{ path('admin_dashboard') }}">
Dashboard <i class="bi bi-hdd-rack"></i> Dashboard
</a> </a>
<hr class="border-secondary"> <hr class="border-secondary">
@@ -66,7 +68,7 @@
<a class="nav-link text-light {% if route starts with 'admin_document' %}active fw-bold{% endif %}" <a class="nav-link text-light {% if route starts with 'admin_document' %}active fw-bold{% endif %}"
href="{{ path('admin_documents') }}"> href="{{ path('admin_documents') }}">
Dokumente <i class="bi bi-card-list"></i> Dokumente
</a> </a>
{# ------------------------- #} {# ------------------------- #}
@@ -74,12 +76,12 @@
{# ------------------------- #} {# ------------------------- #}
<a class="nav-link text-light {% if route starts with 'admin_tags' %}active fw-bold{% endif %}" <a class="nav-link text-light {% if route starts with 'admin_tags' %}active fw-bold{% endif %}"
href="{{ path('admin_tags_index') }}"> href="{{ path('admin_tags_index') }}">
Tags <i class="bi bi-tag-fill"></i> Tags
</a> </a>
<a class="nav-link text-light {% if route starts with 'admin_system_agent' %}active fw-bold{% endif %}" <a class="nav-link text-light {% if route starts with 'admin_system_agent' %}active fw-bold{% endif %}"
href="{{ path('admin_system_agent') }}"> href="{{ path('admin_system_agent') }}">
Retrieval Wissensbasis (Chunk-Index) <i class="bi bi-body-text"></i> Wissensbasis (Chunk-Index)
</a> </a>
<hr class="border-secondary"> <hr class="border-secondary">
@@ -90,12 +92,12 @@
<a class="nav-link text-light {% if route starts with 'admin_system_prompt' %}active fw-bold{% endif %}" <a class="nav-link text-light {% if route starts with 'admin_system_prompt' %}active fw-bold{% endif %}"
href="{{ path('admin_system_prompt') }}"> href="{{ path('admin_system_prompt') }}">
System Prompt <i class="bi bi-chat-right-dots-fill"></i> System Prompt
</a> </a>
<a class="nav-link text-light {% if route starts with 'admin_ingest_profile' %}active fw-bold{% endif %}" <a class="nav-link text-light {% if route starts with 'admin_ingest_profile' %}active fw-bold{% endif %}"
href="{{ path('admin_ingest_profile_list') }}"> href="{{ path('admin_ingest_profile_list') }}">
Indexierungsprofile (Ingest Profiles) <i class="bi bi-diagram-3-fill"></i> Indexierungsprofile (Ingest Profiles)
</a> </a>
<hr class="border-secondary"> <hr class="border-secondary">
@@ -106,7 +108,7 @@
<a class="nav-link text-light {% if route starts with 'admin_model_config' %}active fw-bold{% endif %}" <a class="nav-link text-light {% if route starts with 'admin_model_config' %}active fw-bold{% endif %}"
href="{{ path('admin_model_config_list') }}"> href="{{ path('admin_model_config_list') }}">
Modell-Generierung (Matrix Parameter) <i class="bi bi-motherboard-fill"></i> Modell-Generierung (Matrix Parameter)
</a> </a>
<hr class="border-secondary"> <hr class="border-secondary">
<div class="text-info text-uppercase small mb-2"> <div class="text-info text-uppercase small mb-2">
@@ -115,7 +117,7 @@
<a class="nav-link text-light {% if route starts with 'admin_model_config' %}active fw-bold{% endif %}" <a class="nav-link text-light {% if route starts with 'admin_model_config' %}active fw-bold{% endif %}"
href="{{ path('admin_guides_index') }}"> href="{{ path('admin_guides_index') }}">
How-To & Leitfäden <i class="bi bi-collection"></i> How-To & Leitfäden
</a> </a>
<hr class="border-secondary"> <hr class="border-secondary">
<div class="text-info text-uppercase small mb-2"> <div class="text-info text-uppercase small mb-2">
@@ -123,15 +125,15 @@
</div> </div>
<a class="nav-link text-light {% if route starts with 'admin_job' %}active fw-bold{% endif %}" <a class="nav-link text-light {% if route starts with 'admin_job' %}active fw-bold{% endif %}"
href="{{ path('admin_jobs') }}"> href="{{ path('admin_jobs') }}">
Indexierungs-Log (Ingest Jobs) <i class="bi bi-binoculars-fill"></i> Indexierungs-Log (Ingest Jobs)
</a> </a>
<a class="nav-link text-light {% if route starts with 'admin_job' %}active fw-bold{% endif %}" <a class="nav-link text-light {% if route starts with 'admin_job' %}active fw-bold{% endif %}"
href="{{ path('admin_vector_log') }}"> href="{{ path('admin_vector_log') }}">
Vector-Log Python <i class="bi bi-binoculars-fill"></i> Vector-Log Python
</a> </a>
<a class="nav-link text-light {% if route starts with 'admin_job' %}active fw-bold{% endif %}" <a class="nav-link text-light {% if route starts with 'admin_job' %}active fw-bold{% endif %}"
href="{{ path('admin_system_logs_index') }}"> href="{{ path('admin_system_logs_index') }}">
System-Logs <i class="bi bi-binoculars-fill"></i> System-Logs
</a> </a>
</nav> </nav>

View File

@@ -15,11 +15,12 @@
</div> </div>
<!-- ===================================================== --> <!-- ===================================================== -->
<!-- KPI ROW --> <!-- KPI ROW (NUR STATUS-AMPELN) -->
<!-- ===================================================== --> <!-- ===================================================== -->
<div class="row g-4 mb-4"> <div class="row g-4 mb-4">
{# ================= CHUNK VECTOR STATUS ================= #}
{% if vectorHealth is defined %} {% if vectorHealth is defined %}
{% set status = vectorHealth.status %} {% set status = vectorHealth.status %}
{% set badgeClass = {% set badgeClass =
@@ -27,47 +28,67 @@
? 'bg-success' ? 'bg-success'
: (status == 'INCONSISTENT_MISSING_VECTOR' : (status == 'INCONSISTENT_MISSING_VECTOR'
? 'bg-warning text-dark' ? 'bg-warning text-dark'
: 'bg-danger') : 'bg-danger') %}
%}
{% endif %} {% endif %}
<!-- Vector Status --> <div class="col-lg-6 col-xl-3">
<div class="col-lg-6 col-xl-4">
<div class="card bg-black border-secondary text-light h-100"> <div class="card bg-black border-secondary text-light h-100">
<div class="card-body"> <div class="card-body">
<div class="small text-light mb-2">Chunk-Vektor</div>
<div class="small text-secondary mb-2">Vektor-Infrastruktur</div>
{% if vectorHealth is defined %} {% if vectorHealth is defined %}
<h4 class="mb-2"> <h4 class="mb-0">
<span class="badge {{ badgeClass }}"> <span class="badge {{ badgeClass }}">
{{ vectorHealth.status }} {{ vectorHealth.status }}
</span> </span>
</h4> </h4>
<div class="small text-secondary">
NDJSON-Chunks: {{ vectorHealth.ndjson_chunk_count|number_format(0, ',', '.') }}
<br>
Vektor-Index-Chunks: {{ vectorHealth.vector_chunk_count|number_format(0, ',', '.') }}
</div>
{% else %} {% else %}
<div class="text-secondary small"> <div class="small text-light">
Keine Infrastrukturdaten verfügbar. Keine Daten verfügbar.
</div> </div>
{% endif %} {% endif %}
</div> </div>
</div> </div>
</div> </div>
<!-- Knowledge Capacity --> {# ================= TAG VECTOR STATUS ================= #}
{% set percent = chunkLimit > 0 ? (chunkCount / chunkLimit * 100)|round(1) : 0 %} {% if tagVectorHealth is defined %}
{% set tagStatus = tagVectorHealth.status %}
{% set tagBadgeClass =
tagStatus starts with 'OK'
? 'bg-success'
: (tagStatus == 'INCONSISTENT_MISSING_VECTOR'
? 'bg-warning text-dark'
: 'bg-danger') %}
{% endif %}
<div class="col-lg-6 col-xl-4"> <div class="col-lg-6 col-xl-3">
<div class="card bg-black border-secondary text-light h-100"> <div class="card bg-black border-secondary text-light h-100">
<div class="card-body"> <div class="card-body">
<div class="small text-light mb-2">Tag-Vektor</div>
<div class="small text-secondary mb-2">Wissenskapazität</div> {% if tagVectorHealth is defined %}
<h4 class="mb-0">
<span class="badge {{ tagBadgeClass }}">
{{ tagVectorHealth.status }}
</span>
</h4>
{% else %}
<div class="small text-light">
Keine Daten verfügbar.
</div>
{% endif %}
</div>
</div>
</div>
{# ================= KNOWLEDGE CAPACITY ================= #}
{% set percent = chunkLimit > 0 ? (chunkCount / chunkLimit * 100)|round(1) : 0 %}
<div class="col-lg-6 col-xl-3">
<div class="card bg-black border-secondary text-light h-100">
<div class="card-body">
<div class="small text-light mb-2">Wissenskapazität</div>
<h4 class="mb-2"> <h4 class="mb-2">
{{ chunkCount|number_format(0, ',', '.') }} {{ chunkCount|number_format(0, ',', '.') }}
@@ -77,8 +98,7 @@
</h4> </h4>
<div class="progress bg-dark mb-2" style="height: 14px;"> <div class="progress bg-dark mb-2" style="height: 14px;">
<div <div class="progress-bar
class="progress-bar
{% if percent >= 95 %} {% if percent >= 95 %}
bg-danger bg-danger
{% elseif percent >= 85 %} {% elseif percent >= 85 %}
@@ -90,20 +110,18 @@
</div> </div>
</div> </div>
<div class="small text-secondary"> <div class="small text-light">
{{ percent }} % ausgelastet {{ percent }} % ausgelastet
</div> </div>
</div> </div>
</div> </div>
</div> </div>
<!-- Governance Snapshot --> {# ================= GOVERNANCE ================= #}
<div class="col-lg-6 col-xl-4"> <div class="col-lg-6 col-xl-3">
<div class="card bg-black border-secondary text-light h-100"> <div class="card bg-black border-secondary text-light h-100">
<div class="card-body"> <div class="card-body">
<div class="small text-light mb-2">System-Governance</div>
<div class="small text-secondary mb-2">System-Governance</div>
<div class="small"> <div class="small">
<strong>Benutzer</strong><br> <strong>Benutzer</strong><br>
@@ -114,7 +132,6 @@
<strong>Rollen</strong><br> <strong>Rollen</strong><br>
{{ app.user.roles|join(', ') }} {{ app.user.roles|join(', ') }}
</div> </div>
</div> </div>
</div> </div>
</div> </div>
@@ -122,75 +139,93 @@
</div> </div>
<!-- ===================================================== --> <!-- ===================================================== -->
<!-- DETAIL ROW --> <!-- DETAIL ROW (HIER SIND DIE ZAHLEN) -->
<!-- ===================================================== --> <!-- ===================================================== -->
<div class="row g-4"> <div class="row g-4">
{% if vectorHealth is defined %} {% if vectorHealth is defined %}
<div class="col-lg-4">
<!-- Vector Detail -->
<div class="col-lg-6">
<div class="card bg-black border-secondary text-light h-100"> <div class="card bg-black border-secondary text-light h-100">
<div class="card-body"> <div class="card-body">
<h5 class="text-info mb-3">Chunk-Vektor-Details</h5>
<h5 class="text-info mb-3">Details zur Vektor-Infrastruktur</h5> <div class="small text-info">NDJSON-Chunks</div>
<div class="h5 mb-3">
<div class="row">
<div class="col-6">
<div class="small text-secondary">NDJSON-Chunks</div>
<div class="h5">
{{ vectorHealth.ndjson_chunk_count|number_format(0, ',', '.') }} {{ vectorHealth.ndjson_chunk_count|number_format(0, ',', '.') }}
</div> </div>
</div>
<div class="col-6"> <div class="small text-info">Vektor-Index-Chunks</div>
<div class="small text-secondary">Vektor-Index-Chunks</div>
<div class="h5"> <div class="h5">
{{ vectorHealth.vector_chunk_count|number_format(0, ',', '.') }} {{ vectorHealth.vector_chunk_count|number_format(0, ',', '.') }}
</div> </div>
</div> </div>
</div> </div>
</div> </div>
</div>
</div>
{% endif %} {% endif %}
{% if is_granted('ROLE_SUPER_ADMIN') %} {% if tagVectorHealth is defined %}
<div class="col-lg-4">
<div class="card bg-black border-secondary text-light h-100">
<div class="card-body">
<h5 class="text-info mb-3">Tag-Vektor-Details</h5>
<!-- Critical Operations --> <div class="small text-info">NDJSON-Tags</div>
<div class="col-lg-6"> <div class="h5 mb-3">
{{ tagVectorHealth.tags_ndjson_count|number_format(0, ',', '.') }}
</div>
<div class="small text-info">Vektor-Index-Tags</div>
<div class="h5">
{{ tagVectorHealth.vector_tag_count|number_format(0, ',', '.') }}
</div>
</div>
</div>
</div>
{% endif %}
<!-- INDEXIERUNG -->
<div class="col-lg-4">
<div class="card bg-black border-secondary text-light h-100">
<div class="card-body">
<h5 class="text-info mb-3">Indexierung (Ingest Jobs)</h5>
<div class="text-muted small mb-3">
Erstellt den kompletten Wissensindex neu.
Kann je nach Datenmenge mehrere Minuten dauern.
</div>
<form method="post"
action="/admin/jobs/global-reindex"
onsubmit="return confirm('Global Reindex starten? Dies kann mehrere Minuten dauern.');">
<input type="hidden"
name="_token"
value="{{ csrf_token('global_reindex') }}">
<button type="submit"
class="btn btn-sm btn-outline-info">
Global Reindex starten
</button>
</form>
</div>
</div>
</div>
{% if is_granted('ROLE_SUPER_ADMIN') %}
<div class="col-lg-4">
<div class="card bg-black border-danger text-light h-100"> <div class="card bg-black border-danger text-light h-100">
<div class="card-body"> <div class="card-body">
<h5 class="text-danger mb-3">Kritische Systemoperationen</h5> <h5 class="text-danger mb-3">Kritische Systemoperationen</h5>
<div class="small mb-3 text-secondary"> <div class="small mb-3 text-secondary">
Ein vollständiger System-Reset entfernt: Entfernt alle Dokumente, Versionen, Indizes und Jobs.
<ul> <br><strong>Nicht rückgängig zu machen.</strong>
<li>Alle Dokumente und Versionen</li>
<li>Den gesamten var/knowledge Inhalt</li>
<li>Den FAISS-Vektorindex für Chunks und Tags</li>
<li>Alle Ingest-Jobs</li>
<li>Alle Tags-Jobs</li>
<li>Alle Tags / Dokument Tags</li>
</ul>
<strong>Diese Aktion ist nicht rückgängig zu machen.</strong>
</div> </div>
{% for label, messages in app.flashes %}
{% for message in messages %}
<div class="alert alert-{{ label }}">
{{ message }}
</div>
{% endfor %}
{% endfor %}
<form method="post" <form method="post"
action="{{ path('admin_document_reset') }}" action="{{ path('admin_document_reset') }}"
onsubmit="return confirm('System vollständig zurücksetzen? Diese Aktion kann nicht rückgängig gemacht werden.');"> onsubmit="return confirm('System vollständig zurücksetzen?');">
<input type="hidden" <input type="hidden"
name="_token" name="_token"
@@ -200,13 +235,10 @@
class="btn btn-outline-danger"> class="btn btn-outline-danger">
Vollständigen System-Reset ausführen Vollständigen System-Reset ausführen
</button> </button>
</form> </form>
</div> </div>
</div> </div>
</div> </div>
{% endif %} {% endif %}
</div> </div>

View File

@@ -18,7 +18,7 @@
value="{{ csrf_token('global_reindex') }}"> value="{{ csrf_token('global_reindex') }}">
<button type="submit" <button type="submit"
class="btn btn-sm btn-outline-danger"> class="btn btn-sm btn-outline-info">
Global Reindex starten Global Reindex starten
</button> </button>
</form> </form>