first commit
This commit is contained in:
@@ -5,107 +5,108 @@
|
||||
{% block body %}
|
||||
<div class="container-fluid">
|
||||
|
||||
<!-- ===================================================== -->
|
||||
<!-- HEADER -->
|
||||
<!-- ===================================================== -->
|
||||
{% set chunkStatus = vectorHealth.status|default('UNKNOWN') %}
|
||||
{% set chunkBadgeClass =
|
||||
chunkStatus starts with 'OK'
|
||||
? 'bg-success'
|
||||
: (chunkStatus == 'INCONSISTENT_MISSING_VECTOR'
|
||||
? 'bg-warning text-dark'
|
||||
: 'bg-danger')
|
||||
%}
|
||||
|
||||
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||
<h1 class="h3 mb-0"><i class="bi bi-hdd-rack"></i> Systemübersicht</h1>
|
||||
<span class="badge bg-secondary">RAG Enterprise</span>
|
||||
{% set tagStatus = tagVectorHealth.status|default('UNKNOWN') %}
|
||||
{% set tagBadgeClass =
|
||||
tagStatus starts with 'OK'
|
||||
? 'bg-success'
|
||||
: (tagStatus == 'INCONSISTENT_MISSING_VECTOR'
|
||||
? 'bg-warning text-dark'
|
||||
: 'bg-danger')
|
||||
%}
|
||||
|
||||
{% set percent = chunkLimit > 0 ? (chunkCount / chunkLimit * 100)|round(3) : 0 %}
|
||||
{% set percentClass =
|
||||
percent >= 95
|
||||
? 'bg-danger'
|
||||
: (percent >= 85 ? 'bg-warning text-dark' : 'bg-success')
|
||||
%}
|
||||
|
||||
{% set chunkHealthy = chunkStatus in ['OK', 'OK_EMPTY'] %}
|
||||
{% set tagHealthy = tagStatus in ['OK', 'OK_EMPTY'] %}
|
||||
{% set anyHealthIssue = not chunkHealthy or not tagHealthy %}
|
||||
|
||||
<div class="d-flex justify-content-between align-items-center mb-4 flex-wrap gap-2">
|
||||
<h1 class="h3 mb-0">
|
||||
<i class="bi bi-hdd-rack"></i> Systemübersicht
|
||||
</h1>
|
||||
<span class="badge bg-secondary">RetrieX Admin</span>
|
||||
</div>
|
||||
|
||||
<!-- ===================================================== -->
|
||||
<!-- KPI ROW (NUR STATUS-AMPELN) -->
|
||||
<!-- ===================================================== -->
|
||||
{% if anyHealthIssue %}
|
||||
<div class="alert alert-warning shadow-sm mb-4">
|
||||
<strong>Achtung:</strong>
|
||||
Mindestens ein Index-Zustand ist nicht konsistent.
|
||||
Prüfe die Detailkarten unten und führe bei Bedarf einen Global Reindex aus.
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<div class="row g-4 mb-4">
|
||||
|
||||
{# ================= CHUNK VECTOR STATUS ================= #}
|
||||
{% if vectorHealth is defined %}
|
||||
{% set status = vectorHealth.status %}
|
||||
{% set badgeClass =
|
||||
status starts with 'OK'
|
||||
? 'bg-success'
|
||||
: (status == 'INCONSISTENT_MISSING_VECTOR'
|
||||
? 'bg-warning text-dark'
|
||||
: 'bg-danger') %}
|
||||
{% endif %}
|
||||
|
||||
<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 shadow-sm">
|
||||
<div class="card-body">
|
||||
<div class="small text-light mb-2"><i class="bi bi-files"></i> Chunk-Vektor</div>
|
||||
<div class="small text-light mb-2">
|
||||
<i class="bi bi-files"></i> Chunk-Vektor
|
||||
</div>
|
||||
|
||||
{% if vectorHealth is defined %}
|
||||
<h4 class="mb-0">
|
||||
<span class="badge {{ badgeClass }}">
|
||||
{{ vectorHealth.status }}
|
||||
<h4 class="mb-2">
|
||||
<span class="badge {{ chunkBadgeClass }}">
|
||||
{{ chunkStatus }}
|
||||
</span>
|
||||
</h4>
|
||||
{% else %}
|
||||
<div class="small text-light">
|
||||
Keine Daten verfügbar.
|
||||
</div>
|
||||
{% endif %}
|
||||
</h4>
|
||||
|
||||
<div class="small text-muted">
|
||||
Keyword-/Chunk-Retrieval-Grundlage des Systems
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{# ================= TAG VECTOR STATUS ================= #}
|
||||
{% 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-3">
|
||||
<div class="card bg-black border-secondary text-light h-100">
|
||||
<div class="card bg-black border-secondary text-light h-100 shadow-sm">
|
||||
<div class="card-body">
|
||||
<div class="small text-light mb-2"><i class="bi bi-tags"></i> Tag-Vektor</div>
|
||||
<div class="small text-light mb-2">
|
||||
<i class="bi bi-tags"></i> Tag-Vektor
|
||||
</div>
|
||||
|
||||
{% if tagVectorHealth is defined %}
|
||||
<h4 class="mb-0">
|
||||
<h4 class="mb-2">
|
||||
<span class="badge {{ tagBadgeClass }}">
|
||||
{{ tagVectorHealth.status }}
|
||||
{{ tagStatus }}
|
||||
</span>
|
||||
</h4>
|
||||
{% else %}
|
||||
<div class="small text-light">
|
||||
Keine Daten verfügbar.
|
||||
</div>
|
||||
{% endif %}
|
||||
</h4>
|
||||
|
||||
<div class="small text-muted">
|
||||
Semantisches Tag-Routing für Dokumenträume und Entity-Erkennung
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{# ================= KNOWLEDGE CAPACITY ================= #}
|
||||
{% set percent = chunkLimit > 0 ? (chunkCount / chunkLimit * 100)|round(3) : 0 %}
|
||||
|
||||
<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 shadow-sm">
|
||||
<div class="card-body">
|
||||
<div class="small text-light mb-2"><i class="bi bi-robot"></i> Wissenskapazität</div>
|
||||
<div class="small text-light mb-2">
|
||||
<i class="bi bi-robot"></i> Wissenskapazität
|
||||
</div>
|
||||
|
||||
<h4 class="mb-2">
|
||||
{{ chunkCount|number_format(0, ',', '.') }}
|
||||
<span class="text-secondary small">
|
||||
/ {{ chunkLimit|number_format(0, ',', '.') }} Chunks
|
||||
</span>
|
||||
/ {{ chunkLimit|number_format(0, ',', '.') }} Chunks
|
||||
</span>
|
||||
</h4>
|
||||
|
||||
<div class="progress bg-dark mb-2" style="height: 14px;">
|
||||
<div class="progress-bar
|
||||
{% if percent >= 95 %}
|
||||
bg-danger
|
||||
{% elseif percent >= 85 %}
|
||||
bg-warning text-dark
|
||||
{% else %}
|
||||
bg-success
|
||||
{% endif %}"
|
||||
<div class="progress-bar {{ percentClass }}"
|
||||
style="width: {{ percent }}%;">
|
||||
</div>
|
||||
</div>
|
||||
@@ -117,20 +118,21 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{# ================= GOVERNANCE ================= #}
|
||||
<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 shadow-sm">
|
||||
<div class="card-body">
|
||||
<div class="small text-light mb-2"><i class="bi bi-shield-check"></i> System-Governance</div>
|
||||
<div class="small text-light mb-2">
|
||||
<i class="bi bi-shield-check"></i> System-Governance
|
||||
</div>
|
||||
|
||||
<div class="small">
|
||||
<strong>Benutzer</strong><br>
|
||||
{{ app.user.userIdentifier }}
|
||||
{{ app.user ? app.user.userIdentifier : '-' }}
|
||||
</div>
|
||||
|
||||
<div class="small mt-3">
|
||||
<strong>Rollen</strong><br>
|
||||
{{ app.user.roles|join(', ') }}
|
||||
{{ app.user ? app.user.roles|join(', ') : '-' }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -138,65 +140,94 @@
|
||||
|
||||
</div>
|
||||
|
||||
<!-- ===================================================== -->
|
||||
<!-- DETAIL ROW (HIER SIND DIE ZAHLEN) -->
|
||||
<!-- ===================================================== -->
|
||||
|
||||
<div class="row g-4">
|
||||
|
||||
{% if vectorHealth 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"><i class="bi bi-files"></i> Chunk-Vektor-Details</h5>
|
||||
|
||||
<div class="small text-info">NDJSON-Chunks</div>
|
||||
<div class="h5 mb-3">
|
||||
{{ vectorHealth.ndjson_chunk_count|number_format(0, ',', '.') }}
|
||||
</div>
|
||||
|
||||
<div class="small text-info">Vektor-Index-Chunks</div>
|
||||
<div class="h5">
|
||||
{{ vectorHealth.vector_chunk_count|number_format(0, ',', '.') }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% 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"><i class="bi bi-tags"></i> Tag-Vektor-Details</h5>
|
||||
|
||||
<div class="small text-info">NDJSON-Tags</div>
|
||||
<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 bg-black border-secondary text-light h-100 shadow-sm">
|
||||
<div class="card-body">
|
||||
<h5 class="text-info mb-3"><i class="bi bi-search"></i> Indexierung (Ingest Jobs)</h5>
|
||||
<h5 class="text-info mb-3">
|
||||
<i class="bi bi-files"></i> Chunk-Vektor-Details
|
||||
</h5>
|
||||
|
||||
<div class="small text-info">NDJSON-Chunks</div>
|
||||
<div class="h5 mb-3">
|
||||
{{ vectorHealth.ndjson_chunk_count|default(0)|number_format(0, ',', '.') }}
|
||||
</div>
|
||||
|
||||
<div class="small text-info">Vektor-Index-Chunks</div>
|
||||
<div class="h5 mb-3">
|
||||
{{ vectorHealth.vector_chunk_count|default(0)|number_format(0, ',', '.') }}
|
||||
</div>
|
||||
|
||||
<div class="d-flex flex-wrap gap-2 mt-3">
|
||||
<span class="badge {{ vectorHealth.ndjson_exists|default(false) ? 'text-bg-success' : 'text-bg-danger' }}">
|
||||
NDJSON {{ vectorHealth.ndjson_exists|default(false) ? 'vorhanden' : 'fehlt' }}
|
||||
</span>
|
||||
<span class="badge {{ vectorHealth.vector_exists|default(false) ? 'text-bg-success' : 'text-bg-danger' }}">
|
||||
Index {{ vectorHealth.vector_exists|default(false) ? 'vorhanden' : 'fehlt' }}
|
||||
</span>
|
||||
<span class="badge {{ vectorHealth.meta_exists|default(false) ? 'text-bg-success' : 'text-bg-danger' }}">
|
||||
Meta {{ vectorHealth.meta_exists|default(false) ? 'vorhanden' : 'fehlt' }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-lg-4">
|
||||
<div class="card bg-black border-secondary text-light h-100 shadow-sm">
|
||||
<div class="card-body">
|
||||
<h5 class="text-info mb-3">
|
||||
<i class="bi bi-tags"></i> Tag-Vektor-Details
|
||||
</h5>
|
||||
|
||||
<div class="small text-info">Exportierte Tags (NDJSON)</div>
|
||||
<div class="h5 mb-3">
|
||||
{{ tagVectorHealth.tags_ndjson_count|default(0)|number_format(0, ',', '.') }}
|
||||
</div>
|
||||
|
||||
<div class="small text-info">Vektor-Index-Tags</div>
|
||||
<div class="h5 mb-3">
|
||||
{{ tagVectorHealth.vector_tag_count|default(0)|number_format(0, ',', '.') }}
|
||||
</div>
|
||||
|
||||
<div class="small text-info">Tags mit aktiven Dokumenten</div>
|
||||
<div class="h5 mb-3">
|
||||
{{ tagVectorHealth.tags_with_active_document_ids|default(0)|number_format(0, ',', '.') }}
|
||||
</div>
|
||||
|
||||
<div class="d-flex flex-wrap gap-2 mt-3">
|
||||
<span class="badge {{ tagVectorHealth.tags_ndjson_exists|default(false) ? 'text-bg-success' : 'text-bg-danger' }}">
|
||||
NDJSON {{ tagVectorHealth.tags_ndjson_exists|default(false) ? 'vorhanden' : 'fehlt' }}
|
||||
</span>
|
||||
<span class="badge {{ tagVectorHealth.vector_exists|default(false) ? 'text-bg-success' : 'text-bg-danger' }}">
|
||||
Index {{ tagVectorHealth.vector_exists|default(false) ? 'vorhanden' : 'fehlt' }}
|
||||
</span>
|
||||
<span class="badge {{ tagVectorHealth.meta_exists|default(false) ? 'text-bg-success' : 'text-bg-danger' }}">
|
||||
Meta {{ tagVectorHealth.meta_exists|default(false) ? 'vorhanden' : 'fehlt' }}
|
||||
</span>
|
||||
<span class="badge {{ tagVectorHealth.meta_valid|default(false) ? 'text-bg-success' : 'text-bg-danger' }}">
|
||||
Meta {{ tagVectorHealth.meta_valid|default(false) ? 'gültig' : 'ungültig' }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-lg-4">
|
||||
<div class="card bg-black border-secondary text-light h-100 shadow-sm">
|
||||
<div class="card-body">
|
||||
<h5 class="text-info mb-3">
|
||||
<i class="bi bi-search"></i> Indexierung (Ingest Jobs)
|
||||
</h5>
|
||||
|
||||
<div class="text-muted small mb-3">
|
||||
Erstellt den kompletten Wissensindex neu.
|
||||
Kann je nach Datenmenge mehrere Minuten dauern.
|
||||
Erstellt den kompletten Wissensindex neu und zieht dabei auch die
|
||||
physischen Retrieval-Artefakte wieder gerade.
|
||||
</div>
|
||||
|
||||
<form method="post"
|
||||
action="/admin/jobs/global-reindex"
|
||||
action="{{ path('admin_global_reindex') }}"
|
||||
onsubmit="return confirm('Global Reindex starten? Dies kann mehrere Minuten dauern.');">
|
||||
|
||||
<input type="hidden"
|
||||
@@ -208,15 +239,23 @@
|
||||
Global Reindex starten
|
||||
</button>
|
||||
</form>
|
||||
|
||||
{% if anyHealthIssue %}
|
||||
<div class="alert alert-dark border border-warning text-light small mt-3 mb-0">
|
||||
Empfohlen bei inkonsistentem Chunk- oder Tag-Zustand.
|
||||
</div>
|
||||
{% endif %}
|
||||
</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 shadow-sm">
|
||||
<div class="card-body">
|
||||
<h5 class="text-danger mb-3"><i class="bi bi-sign-stop-fill"></i> Kritische Systemoperationen</h5>
|
||||
<h5 class="text-danger mb-3">
|
||||
<i class="bi bi-sign-stop-fill"></i> Kritische Systemoperationen
|
||||
</h5>
|
||||
|
||||
<div class="small mb-3 text-secondary">
|
||||
Entfernt alle Dokumente, Versionen, Indizes und Jobs.
|
||||
|
||||
@@ -4,8 +4,15 @@
|
||||
|
||||
{% block body %}
|
||||
|
||||
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||
<h1 class="h3 mb-0"><i class="bi bi-card-list"></i> Dokumente</h1>
|
||||
<div class="d-flex justify-content-between align-items-center mb-4 flex-wrap gap-2">
|
||||
<div>
|
||||
<h1 class="h3 mb-1">
|
||||
<i class="bi bi-card-list"></i> Dokumente
|
||||
</h1>
|
||||
<div class="small text-muted">
|
||||
Übersicht über Dokumente, aktive Versionen, Ingest-Zustände und Tag-Zuordnungen.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<a href="{{ path('admin_document_new') }}"
|
||||
class="btn btn-sm btn-outline-info">
|
||||
@@ -13,154 +20,222 @@
|
||||
</a>
|
||||
</div>
|
||||
|
||||
{% for message in app.flashes('success') %}
|
||||
<div class="alert alert-success shadow-sm">
|
||||
{{ message }}
|
||||
</div>
|
||||
{% endfor %}
|
||||
|
||||
{% for message in app.flashes('danger') %}
|
||||
<div class="alert alert-danger shadow-sm">
|
||||
{{ message }}
|
||||
</div>
|
||||
{% endfor %}
|
||||
|
||||
{% for message in app.flashes('info') %}
|
||||
<div class="alert alert-info shadow-sm">
|
||||
{{ message }}
|
||||
</div>
|
||||
{% endfor %}
|
||||
|
||||
<div class="card bg-dark border-secondary text-light mb-4 shadow-sm">
|
||||
<div class="card-body row g-4">
|
||||
<div class="col-lg-7">
|
||||
<h5 class="text-info mb-3">Worauf achten?</h5>
|
||||
<ul class="small mb-0">
|
||||
<li><strong>INDEXED</strong> bedeutet: aktive Version ist sauber im Wissensindex.</li>
|
||||
<li><strong>PENDING</strong> oder <strong>FAILED</strong> bedeuten: Dokument prüfen und ggf. Ingest erneut anstoßen.</li>
|
||||
<li><strong>Tags</strong> sollten fachlich präzise sein und nicht nur generische Oberbegriffe abbilden.</li>
|
||||
<li>Die aktive Version ist die fachlich führende Version des Dokuments.</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="col-lg-5">
|
||||
<h5 class="text-info mb-3">Schnellzugriff</h5>
|
||||
<div class="small text-light">
|
||||
Über <strong>Tags</strong> gelangst du direkt in die Tag-Pflege des Dokuments.
|
||||
Über <strong>Details</strong> steuerst du Versionen, Aktivierung, Re-Ingest und Löschung.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% if documents is empty %}
|
||||
|
||||
<div class="alert alert-secondary">
|
||||
<div class="alert alert-secondary shadow-sm">
|
||||
Keine Dokumente vorhanden.
|
||||
</div>
|
||||
|
||||
{% else %}
|
||||
|
||||
<div class="card bg-black border-secondary">
|
||||
<div class="card bg-black border-secondary shadow-sm">
|
||||
<div class="card-body p-0">
|
||||
|
||||
<table class="table table-dark table-striped table-hover align-middle mb-0">
|
||||
<thead class="table-secondary text-dark">
|
||||
<tr>
|
||||
<th>Titel</th>
|
||||
<th>ID</th>
|
||||
<th>Typ</th>
|
||||
<th>Status</th>
|
||||
<th>Indexierung</th>
|
||||
<th>Versionen</th>
|
||||
<th>Aktive Version</th>
|
||||
<th>Erstellt</th>
|
||||
<th class="text-end">Aktionen</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<div class="d-flex justify-content-between align-items-center px-3 py-3 border-bottom border-secondary flex-wrap gap-2">
|
||||
<div>
|
||||
<strong class="text-info">Vorhandene Dokumente</strong>
|
||||
<span class="small text-muted ms-2">{{ documents|length }} Einträge</span>
|
||||
</div>
|
||||
|
||||
{% for document in documents %}
|
||||
<div class="small text-muted">
|
||||
Neueste Dokumente stehen oben.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="table-responsive">
|
||||
<table class="table table-dark table-striped table-hover align-middle mb-0">
|
||||
<thead class="table-secondary text-dark">
|
||||
<tr>
|
||||
<th style="width: 20%">Titel</th>
|
||||
<th style="width: 14%">ID</th>
|
||||
<th style="width: 8%">Typ</th>
|
||||
<th style="width: 8%">Status</th>
|
||||
<th style="width: 10%">Indexierung</th>
|
||||
<th style="width: 7%">Versionen</th>
|
||||
<th style="width: 8%">Aktive Version</th>
|
||||
<th style="width: 7%">Tags</th>
|
||||
<th style="width: 8%">Erstellt</th>
|
||||
<th class="text-end" style="width: 10%">Aktionen</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
|
||||
{# Titel #}
|
||||
<td>
|
||||
<a href="{{ path('admin_document_show', {id: document.id}) }}"
|
||||
class="text-light text-decoration-none">
|
||||
{{ document.title }}
|
||||
</a>
|
||||
</td>
|
||||
{% for document in documents %}
|
||||
<tr>
|
||||
<td>
|
||||
<div class="fw-semibold">
|
||||
<a href="{{ path('admin_document_show', {id: document.id}) }}"
|
||||
class="text-light text-decoration-none">
|
||||
{{ document.title }}
|
||||
</a>
|
||||
</div>
|
||||
|
||||
{# ID #}
|
||||
<td class="small text-info">
|
||||
{{ document.id }}
|
||||
</td>
|
||||
{% if document.currentVersion and document.currentVersion.filePath %}
|
||||
<div class="small text-muted mt-1">
|
||||
Aktive Datei vorhanden
|
||||
</div>
|
||||
{% endif %}
|
||||
</td>
|
||||
|
||||
{# Typ #}
|
||||
<td>
|
||||
{% if document.currentVersion %}
|
||||
<span class="badge bg-secondary">
|
||||
{{ document.currentVersion.fileTypeLabel }}
|
||||
</span>
|
||||
{% else %}
|
||||
<span class="badge bg-dark border border-secondary">
|
||||
-
|
||||
</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td class="small text-info">
|
||||
<code>{{ document.id }}</code>
|
||||
</td>
|
||||
|
||||
{# Dokument Status #}
|
||||
<td>
|
||||
{% if document.status == 'ACTIVE' %}
|
||||
<span class="badge bg-success">Aktiv</span>
|
||||
{% else %}
|
||||
<span class="badge bg-secondary">Archiviert</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
|
||||
{# Ingest Status #}
|
||||
<td>
|
||||
{% if document.currentVersion %}
|
||||
{% if document.currentVersion.ingestStatus == 'INDEXED' %}
|
||||
<span class="badge bg-success">INDEXED</span>
|
||||
{% elseif document.currentVersion.ingestStatus == 'PENDING' %}
|
||||
<span class="badge bg-warning text-dark">PENDING</span>
|
||||
{% elseif document.currentVersion.ingestStatus == 'FAILED' %}
|
||||
<span class="badge bg-danger">FAILED</span>
|
||||
<td>
|
||||
{% if document.currentVersion %}
|
||||
<span class="badge bg-secondary">
|
||||
{{ document.currentVersion.fileTypeLabel }}
|
||||
</span>
|
||||
{% else %}
|
||||
<span class="badge bg-dark border border-secondary">
|
||||
{{ document.currentVersion.ingestStatus }}
|
||||
</span>
|
||||
-
|
||||
</span>
|
||||
{% endif %}
|
||||
{% else %}
|
||||
<span class="badge bg-dark border border-secondary">-</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
</td>
|
||||
|
||||
{# Version Count #}
|
||||
<td>
|
||||
{{ document.versions|length }}
|
||||
</td>
|
||||
<td>
|
||||
{% if document.status == 'ACTIVE' %}
|
||||
<span class="badge bg-success">Aktiv</span>
|
||||
{% else %}
|
||||
<span class="badge bg-secondary">Archiviert</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
|
||||
{# Aktive Version #}
|
||||
<td>
|
||||
{% if document.currentVersion %}
|
||||
v{{ document.currentVersion.versionNumber }}
|
||||
{% else %}
|
||||
-
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
{% if document.currentVersion %}
|
||||
{% if document.currentVersion.ingestStatus == 'INDEXED' %}
|
||||
<span class="badge bg-success">INDEXED</span>
|
||||
{% elseif document.currentVersion.ingestStatus == 'PENDING' %}
|
||||
<span class="badge bg-warning text-dark">PENDING</span>
|
||||
{% elseif document.currentVersion.ingestStatus == 'RUNNING' %}
|
||||
<span class="badge bg-warning text-dark">RUNNING</span>
|
||||
{% elseif document.currentVersion.ingestStatus == 'FAILED' %}
|
||||
<span class="badge bg-danger">FAILED</span>
|
||||
{% else %}
|
||||
<span class="badge bg-dark border border-secondary">
|
||||
{{ document.currentVersion.ingestStatus ?: '-' }}
|
||||
</span>
|
||||
{% endif %}
|
||||
{% else %}
|
||||
<span class="badge bg-dark border border-secondary">-</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
|
||||
{# Created At #}
|
||||
<td class="small">
|
||||
{{ document.createdAt|date('d.m.Y H:i') }}
|
||||
</td>
|
||||
<td>
|
||||
<span class="badge text-bg-dark border border-secondary">
|
||||
{{ document.versions|length }}
|
||||
</span>
|
||||
</td>
|
||||
|
||||
{# Aktionen #}
|
||||
<td class="text-end">
|
||||
<td>
|
||||
{% if document.currentVersion %}
|
||||
<span class="badge bg-info text-dark">
|
||||
v{{ document.currentVersion.versionNumber }}
|
||||
</span>
|
||||
{% else %}
|
||||
-
|
||||
{% endif %}
|
||||
</td>
|
||||
|
||||
<a class="btn btn-sm btn-outline-info me-2"
|
||||
href="{{ path('admin_document_tags_edit', {id: document.id}) }}">
|
||||
Tags
|
||||
</a>
|
||||
<td>
|
||||
<span class="badge text-bg-dark border border-secondary">
|
||||
{{ document.tags|length }}
|
||||
</span>
|
||||
</td>
|
||||
|
||||
<a class="btn btn-sm btn-outline-light me-2"
|
||||
href="{{ path('admin_document_show', {id: document.id}) }}">
|
||||
Details
|
||||
</a>
|
||||
<td class="small">
|
||||
{{ document.createdAt|date('d.m.Y H:i') }}
|
||||
</td>
|
||||
|
||||
{% if is_granted('ROLE_SUPER_ADMIN') %}
|
||||
<form method="post"
|
||||
action="{{ path('admin_document_delete', {id: document.id}) }}"
|
||||
class="d-inline"
|
||||
onsubmit="return confirm('Dokument wirklich endgültig löschen? Diese Aktion entfernt Dokument, Versionen und Index-Daten.');">
|
||||
<td class="text-end">
|
||||
<div class="d-flex justify-content-end flex-wrap gap-2">
|
||||
<a class="btn btn-sm btn-outline-info"
|
||||
href="{{ path('admin_document_tags_edit', {id: document.id}) }}">
|
||||
Tags
|
||||
</a>
|
||||
|
||||
<input type="hidden"
|
||||
name="_token"
|
||||
value="{{ csrf_token('delete_document_' ~ document.id) }}">
|
||||
<a class="btn btn-sm btn-outline-light"
|
||||
href="{{ path('admin_document_show', {id: document.id}) }}">
|
||||
Details
|
||||
</a>
|
||||
|
||||
<button class="btn btn-sm btn-outline-danger">
|
||||
Löschen
|
||||
</button>
|
||||
</form>
|
||||
{% endif %}
|
||||
{% if is_granted('ROLE_SUPER_ADMIN') %}
|
||||
<form method="post"
|
||||
action="{{ path('admin_document_delete', {id: document.id}) }}"
|
||||
class="d-inline"
|
||||
onsubmit="return confirm('Dokument wirklich löschen? Der Inhalt wird per Delete-Job aus dem Index entfernt.');">
|
||||
<input type="hidden"
|
||||
name="_token"
|
||||
value="{{ csrf_token('delete_document_' ~ document.id) }}">
|
||||
|
||||
</td>
|
||||
<button class="btn btn-sm btn-outline-danger">
|
||||
Löschen
|
||||
</button>
|
||||
</form>
|
||||
{% endif %}
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
|
||||
</tr>
|
||||
{% endfor %}
|
||||
|
||||
</tbody>
|
||||
</table>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% endif %}
|
||||
|
||||
<div class="mt-4 small text-secondary">
|
||||
Hinweis: Das Löschen eines Dokuments entfernt alle Versionen und
|
||||
erfordert eine Aktualisierung des NDJSON-Indexes.
|
||||
<div class="card bg-dark border-secondary text-light mt-4 shadow-sm">
|
||||
<div class="card-body">
|
||||
<h5 class="text-info mb-3">Hinweis zum Dokument-Lifecycle</h5>
|
||||
<div class="small text-light">
|
||||
Änderungen an aktiven Versionen und Löschvorgänge wirken sich direkt auf den Wissensindex aus.
|
||||
Zugewiesene Tags beeinflussen zusätzlich die semantische Routing-Ebene des Systems.
|
||||
Dokumente mit schwachen oder fehlenden Tags sind oft ein guter Kandidat für fachliche Nachpflege.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% endblock %}
|
||||
@@ -4,8 +4,13 @@
|
||||
|
||||
{% block body %}
|
||||
|
||||
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||
<h1 class="h3">Neues Dokument</h1>
|
||||
<div class="d-flex justify-content-between align-items-center mb-4 flex-wrap gap-2">
|
||||
<div>
|
||||
<h1 class="h3 mb-1">Neues Dokument</h1>
|
||||
<div class="small text-muted">
|
||||
Neuer Upload mit initialer Version und anschließendem asynchronen Ingest.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<a href="{{ path('admin_documents') }}"
|
||||
class="btn btn-sm btn-outline-secondary">
|
||||
@@ -13,7 +18,49 @@
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="card bg-black border-secondary text-light">
|
||||
{% for message in app.flashes('success') %}
|
||||
<div class="alert alert-success shadow-sm">
|
||||
{{ message }}
|
||||
</div>
|
||||
{% endfor %}
|
||||
|
||||
{% for message in app.flashes('danger') %}
|
||||
<div class="alert alert-danger shadow-sm">
|
||||
{{ message }}
|
||||
</div>
|
||||
{% endfor %}
|
||||
|
||||
{% for message in app.flashes('info') %}
|
||||
<div class="alert alert-info shadow-sm">
|
||||
{{ message }}
|
||||
</div>
|
||||
{% endfor %}
|
||||
|
||||
<div class="card bg-dark border-secondary text-light mb-4 shadow-sm">
|
||||
<div class="card-body row g-4">
|
||||
<div class="col-lg-7">
|
||||
<h5 class="text-info mb-3">Warum ist der Titel wichtig?</h5>
|
||||
|
||||
<ul class="small mb-0">
|
||||
<li>Der Titel wird später Teil des fachlichen Kontexts des Dokuments.</li>
|
||||
<li>Ein präziser Titel verbessert Retrieval, Chunk-Einordnung und spätere Tag-Pflege.</li>
|
||||
<li>Generische Titel wie <code>Dokument 1</code> oder nur Dateinamen sind deutlich schwächer.</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="col-lg-5">
|
||||
<h5 class="text-info mb-3">Gute Beispiele</h5>
|
||||
|
||||
<ul class="small mb-0">
|
||||
<li><code>Testomat 808 – Technisches Datenblatt</code></li>
|
||||
<li><code>Resthärte-Messung – Produktübersicht</code></li>
|
||||
<li><code>Indikator 300 – Anwendung und Dosierung</code></li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card bg-black border-secondary text-light shadow-sm">
|
||||
<div class="card-body">
|
||||
|
||||
<form method="post" enctype="multipart/form-data">
|
||||
@@ -22,31 +69,24 @@
|
||||
name="_token"
|
||||
value="{{ csrf_token('create_document') }}">
|
||||
|
||||
{# ============================= #}
|
||||
{# Titel #}
|
||||
{# ============================= #}
|
||||
|
||||
<div class="mb-4">
|
||||
<label class="form-label">Titel</label>
|
||||
|
||||
<div class="alert alert-secondary small">
|
||||
<strong>Hinweis zur Qualität:</strong><br>
|
||||
Der Titel ist entscheidend für die semantische Einordnung
|
||||
der erzeugten Chunks. Jeder Chunk erhält den Titel als Kontext,
|
||||
wodurch Retrieval und Antwortqualität signifikant verbessert werden.<br><br>
|
||||
|
||||
Wird kein Titel angegeben, wird automatisch der Dateiname
|
||||
verwendet (nicht empfohlen).
|
||||
Verwende einen fachlich präzisen Titel, der Produkt, Thema oder Dokumenttyp klar beschreibt.
|
||||
Wenn kein Titel angegeben wird, wird automatisch der Dateiname verwendet.
|
||||
</div>
|
||||
|
||||
<input class="form-control bg-dark text-light border-secondary"
|
||||
name="title"
|
||||
placeholder="z. B. Sicherheitsdatenblatt – Produkt XY">
|
||||
</div>
|
||||
value="{{ app.request.get('title') }}"
|
||||
placeholder="z. B. Testomat 808 – Technisches Datenblatt">
|
||||
|
||||
{# ============================= #}
|
||||
{# Datei Upload #}
|
||||
{# ============================= #}
|
||||
<div class="form-text text-secondary">
|
||||
Der Titel muss nicht lang sein, aber fachlich eindeutig.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-4">
|
||||
<label class="form-label">Datei</label>
|
||||
@@ -58,14 +98,22 @@
|
||||
|
||||
<div class="form-text text-secondary">
|
||||
Unterstützte Formate: PDF, DOCX, TXT, MD.
|
||||
Das Dokument wird versioniert gespeichert und anschließend
|
||||
indexiert.
|
||||
Nach dem Upload wird automatisch Version 1 erstellt und ein Ingest-Job gestartet.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{# ============================= #}
|
||||
{# Submit #}
|
||||
{# ============================= #}
|
||||
<div class="card bg-dark border-secondary mb-4">
|
||||
<div class="card-body">
|
||||
<h6 class="text-info mb-3">Was passiert nach dem Speichern?</h6>
|
||||
|
||||
<ul class="small mb-0">
|
||||
<li>Das Dokument wird versioniert gespeichert.</li>
|
||||
<li>Die erste Version wird als aktuelle Version gesetzt.</li>
|
||||
<li>Ein asynchroner Ingest-Job verarbeitet das Dokument für den Wissensindex.</li>
|
||||
<li>Später können dem Dokument gezielt Tags zugewiesen werden.</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="d-flex justify-content-end">
|
||||
<button class="btn btn-outline-info">
|
||||
@@ -79,8 +127,7 @@
|
||||
</div>
|
||||
|
||||
<div class="mt-4 small text-secondary">
|
||||
Hinweis: Nach dem Upload wird automatisch eine neue Dokumentversion erstellt.
|
||||
Die Indexierung erfolgt asynchron über einen Ingest-Job.
|
||||
Hinweis: Ein sauber benanntes Dokument ist die beste Grundlage für gutes Retrieval und späteres präzises Tagging.
|
||||
</div>
|
||||
|
||||
{% endblock %}
|
||||
{% endblock %}
|
||||
@@ -4,10 +4,13 @@
|
||||
|
||||
{% block body %}
|
||||
|
||||
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||
<h1 class="h3 mb-0">
|
||||
Neue Version
|
||||
</h1>
|
||||
<div class="d-flex justify-content-between align-items-center mb-4 flex-wrap gap-2">
|
||||
<div>
|
||||
<h1 class="h3 mb-1">Neue Version</h1>
|
||||
<div class="small text-muted">
|
||||
Neue unveränderliche Version für ein bestehendes Dokument hochladen.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<a href="{{ path('admin_document_show', {id: document.id}) }}"
|
||||
class="btn btn-sm btn-outline-secondary">
|
||||
@@ -15,36 +18,99 @@
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="card bg-dark border-secondary mb-4 text-light">
|
||||
<div class="card-body">
|
||||
{% for message in app.flashes('success') %}
|
||||
<div class="alert alert-success shadow-sm">
|
||||
{{ message }}
|
||||
</div>
|
||||
{% endfor %}
|
||||
|
||||
<div class="mb-3">
|
||||
<strong>Dokument:</strong>
|
||||
<span class="text-light">{{ document.title }}</span>
|
||||
{% for message in app.flashes('danger') %}
|
||||
<div class="alert alert-danger shadow-sm">
|
||||
{{ message }}
|
||||
</div>
|
||||
{% endfor %}
|
||||
|
||||
{% for message in app.flashes('info') %}
|
||||
<div class="alert alert-info shadow-sm">
|
||||
{{ message }}
|
||||
</div>
|
||||
{% endfor %}
|
||||
|
||||
<div class="card bg-dark border-secondary text-light mb-4 shadow-sm">
|
||||
<div class="card-body row g-4">
|
||||
<div class="col-lg-7">
|
||||
<h5 class="text-info mb-3">Dokumentkontext</h5>
|
||||
|
||||
<div class="mb-2">
|
||||
<strong>Dokument:</strong>
|
||||
<span class="text-light">{{ document.title }}</span>
|
||||
</div>
|
||||
|
||||
<div class="small text-secondary">
|
||||
Eine neue Version erzeugt eine zusätzliche, unveränderliche Dokumentversion.
|
||||
Die bestehende aktive Version bleibt zunächst unverändert.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="small text-secondary">
|
||||
Das Hochladen einer neuen Version erzeugt eine zusätzliche
|
||||
unveränderliche Dokumentversion. Die Aktivierung erfolgt separat
|
||||
und löst einen deterministischen Re-Ingest aus.
|
||||
</div>
|
||||
<div class="col-lg-5">
|
||||
<h5 class="text-info mb-3">Aktueller Stand</h5>
|
||||
|
||||
<div class="small mb-2">
|
||||
<strong>Aktive Version:</strong>
|
||||
{% if document.currentVersion %}
|
||||
<span class="badge bg-info text-dark">
|
||||
v{{ document.currentVersion.versionNumber }}
|
||||
</span>
|
||||
{% else %}
|
||||
-
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<div class="small mb-2">
|
||||
<strong>Vorhandene Versionen:</strong>
|
||||
{{ document.versions|length }}
|
||||
</div>
|
||||
|
||||
<div class="small">
|
||||
<strong>Zugewiesene Tags:</strong>
|
||||
{{ document.tags|length }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card bg-black border-secondary text-light">
|
||||
<div class="card bg-dark border-secondary text-light mb-4 shadow-sm">
|
||||
<div class="card-body row g-4">
|
||||
<div class="col-lg-7">
|
||||
<h5 class="text-info mb-3">Wichtig für den Lifecycle</h5>
|
||||
|
||||
<ul class="small mb-0">
|
||||
<li>Der Upload erzeugt nur eine <strong>neue Version</strong>, aber aktiviert sie nicht automatisch.</li>
|
||||
<li>Erst die spätere <strong>Aktivierung</strong> löst den deterministischen Re-Ingest aus.</li>
|
||||
<li>Tags bleiben auf <strong>Dokumentebene</strong> bestehen und gelten weiterhin für das Dokument als Ganzes.</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="col-lg-5">
|
||||
<h5 class="text-info mb-3">Gute Praxis</h5>
|
||||
|
||||
<ul class="small mb-0">
|
||||
<li>Nur fachlich wirklich passende Nachfolgeversionen hochladen.</li>
|
||||
<li>Kein anderes Thema oder anderes Produkt in dieselbe Dokumentlinie mischen.</li>
|
||||
<li>Bei stark verändertem Fachinhalt später Tagging mitprüfen.</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card bg-black border-secondary text-light shadow-sm">
|
||||
<div class="card-body">
|
||||
|
||||
<form method="post" enctype="multipart/form-data">
|
||||
|
||||
<input type="hidden"
|
||||
name="_token"
|
||||
value="{{ csrf_token('create_document_version_' ~ document.id) }}">
|
||||
|
||||
{# ============================= #}
|
||||
{# Datei Upload #}
|
||||
{# ============================= #}
|
||||
|
||||
<div class="mb-4">
|
||||
<label class="form-label">Datei auswählen</label>
|
||||
|
||||
@@ -54,15 +120,23 @@
|
||||
required>
|
||||
|
||||
<div class="form-text text-secondary">
|
||||
Unterstützte Formate: PDF, DOCX, TXT, MD.<br>
|
||||
Die Datei wird versioniert gespeichert und mit einer
|
||||
eindeutigen Checksum versehen.
|
||||
Unterstützte Formate: PDF, DOCX, TXT, MD.
|
||||
Die Datei wird versioniert gespeichert und mit einer eindeutigen Checksum versehen.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{# ============================= #}
|
||||
{# Submit #}
|
||||
{# ============================= #}
|
||||
<div class="card bg-dark border-secondary mb-4">
|
||||
<div class="card-body">
|
||||
<h6 class="text-info mb-3">Was passiert nach dem Upload?</h6>
|
||||
|
||||
<ul class="small mb-0">
|
||||
<li>Es wird eine neue, unveränderliche Dokumentversion angelegt.</li>
|
||||
<li>Die aktive Version bleibt zunächst unverändert.</li>
|
||||
<li>Ein Re-Ingest erfolgt erst nach späterer Aktivierung dieser Version.</li>
|
||||
<li>Danach wird der Wissensindex deterministisch neu aufgebaut.</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% if is_granted('ROLE_SUPER_ADMIN') %}
|
||||
<div class="d-flex justify-content-end">
|
||||
@@ -71,16 +145,14 @@
|
||||
</button>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
</form>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-4 small text-secondary">
|
||||
Hinweis: Eine neue Version ersetzt nicht automatisch die aktive Version.
|
||||
Erst nach Aktivierung wird ein Re-Ingest durchgeführt und der Index
|
||||
neu aufgebaut.
|
||||
Hinweis: Eine neue Version verbessert den Dokument-Lifecycle nur dann sauber, wenn sie fachlich wirklich zu diesem Dokument gehört.
|
||||
Bei stark verändertem Inhalt sollten nach der späteren Aktivierung auch die Tags geprüft werden.
|
||||
</div>
|
||||
|
||||
{% endblock %}
|
||||
{% endblock %}
|
||||
@@ -4,116 +4,225 @@
|
||||
|
||||
{% block body %}
|
||||
|
||||
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||
<h1 class="h3 mb-0">{{ document.title ?? 'Ein Fehler trat auf' }}</h1>
|
||||
<div class="d-flex justify-content-between align-items-center mb-4 flex-wrap gap-2">
|
||||
<div>
|
||||
<h1 class="h3 mb-1">{{ document.title }}</h1>
|
||||
<div class="small text-muted">
|
||||
Detailansicht für Dokument, Versionen und Tag-Zuordnung.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<a href="{{ path('admin_documents') }}"
|
||||
class="btn btn-sm btn-outline-secondary">
|
||||
Zurück zur Übersicht
|
||||
</a>
|
||||
<div class="d-flex flex-wrap gap-2">
|
||||
<a href="{{ path('admin_document_tags_edit', {id: document.id}) }}"
|
||||
class="btn btn-sm btn-outline-info">
|
||||
Tags bearbeiten
|
||||
</a>
|
||||
|
||||
<a href="{{ path('admin_documents') }}"
|
||||
class="btn btn-sm btn-outline-secondary">
|
||||
Zurück zur Übersicht
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% if document %}
|
||||
|
||||
{# ============================= #}
|
||||
{# Dokument-Meta #}
|
||||
{# ============================= #}
|
||||
|
||||
<div class="card bg-dark border-secondary mb-5 text-light">
|
||||
<div class="card-body">
|
||||
|
||||
<div class="mb-2">
|
||||
<strong>Status:</strong>
|
||||
{% if document.status == 'ACTIVE' %}
|
||||
<span class="badge bg-success">Aktiv</span>
|
||||
{% else %}
|
||||
<span class="badge bg-secondary">Archiviert</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<div class="mb-2">
|
||||
<strong>Erstellt von:</strong>
|
||||
{{ document.createdBy ? document.createdBy.email : '-' }}
|
||||
</div>
|
||||
|
||||
<div class="mb-2">
|
||||
<strong>Erstellt am:</strong>
|
||||
{{ document.createdAt|date('d.m.Y H:i') }}
|
||||
</div>
|
||||
|
||||
<div class="mb-2">
|
||||
<strong>Aktive Version:</strong>
|
||||
{% if document.currentVersion %}
|
||||
<span class="badge bg-info text-dark">
|
||||
v{{ document.currentVersion.versionNumber }}
|
||||
</span>
|
||||
{% else %}
|
||||
-
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
</div>
|
||||
{% for message in app.flashes('success') %}
|
||||
<div class="alert alert-success shadow-sm">
|
||||
{{ message }}
|
||||
</div>
|
||||
{% endfor %}
|
||||
|
||||
{# ============================= #}
|
||||
{# Versionen #}
|
||||
{# ============================= #}
|
||||
|
||||
<div class="d-flex justify-content-between align-items-center mb-3">
|
||||
<h2 class="h5 mb-0">Versionen</h2>
|
||||
|
||||
{% if is_granted('ROLE_SUPER_ADMIN') %}
|
||||
<a href="{{ path('admin_document_version_new', {id: document.id}) }}"
|
||||
class="btn btn-sm btn-outline-info">
|
||||
Neue Version
|
||||
</a>
|
||||
{% endif %}
|
||||
{% for message in app.flashes('danger') %}
|
||||
<div class="alert alert-danger shadow-sm">
|
||||
{{ message }}
|
||||
</div>
|
||||
{% endfor %}
|
||||
|
||||
{% if document.versions is empty %}
|
||||
{% for message in app.flashes('info') %}
|
||||
<div class="alert alert-info shadow-sm">
|
||||
{{ message }}
|
||||
</div>
|
||||
{% endfor %}
|
||||
|
||||
<div class="alert alert-secondary">
|
||||
Keine Versionen vorhanden.
|
||||
</div>
|
||||
<div class="row g-4 mb-4">
|
||||
|
||||
{% else %}
|
||||
|
||||
<div class="card bg-black border-secondary">
|
||||
<div class="col-lg-7">
|
||||
<div class="card bg-dark border-secondary text-light h-100 shadow-sm">
|
||||
<div class="card-body">
|
||||
<h5 class="text-info mb-3">Dokument-Metadaten</h5>
|
||||
|
||||
<div class="row g-3">
|
||||
<div class="col-md-6">
|
||||
<div class="small text-muted mb-1">Status</div>
|
||||
<div>
|
||||
{% if document.status == 'ACTIVE' %}
|
||||
<span class="badge bg-success">Aktiv</span>
|
||||
{% else %}
|
||||
<span class="badge bg-secondary">Archiviert</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-md-6">
|
||||
<div class="small text-muted mb-1">Aktive Version</div>
|
||||
<div>
|
||||
{% if document.currentVersion %}
|
||||
<span class="badge bg-info text-dark">
|
||||
v{{ document.currentVersion.versionNumber }}
|
||||
</span>
|
||||
{% else %}
|
||||
-
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-md-6">
|
||||
<div class="small text-muted mb-1">Erstellt von</div>
|
||||
<div>{{ document.createdBy ? document.createdBy.email : '-' }}</div>
|
||||
</div>
|
||||
|
||||
<div class="col-md-6">
|
||||
<div class="small text-muted mb-1">Erstellt am</div>
|
||||
<div>{{ document.createdAt|date('d.m.Y H:i:s') }}</div>
|
||||
</div>
|
||||
|
||||
<div class="col-md-6">
|
||||
<div class="small text-muted mb-1">Anzahl Versionen</div>
|
||||
<div>{{ document.versions|length }}</div>
|
||||
</div>
|
||||
|
||||
<div class="col-md-6">
|
||||
<div class="small text-muted mb-1">Zugewiesene Tags</div>
|
||||
<div>{{ document.tags|length }}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% if is_granted('ROLE_SUPER_ADMIN') %}
|
||||
<hr class="border-secondary">
|
||||
|
||||
<div class="d-flex flex-wrap gap-2">
|
||||
<a href="{{ path('admin_document_version_new', {id: document.id}) }}"
|
||||
class="btn btn-sm btn-outline-info">
|
||||
Neue Version
|
||||
</a>
|
||||
|
||||
<form method="post"
|
||||
action="{{ path('admin_document_delete', {id: document.id}) }}"
|
||||
class="d-inline"
|
||||
onsubmit="return confirm('Dokument wirklich löschen? Der Inhalt wird per Delete-Job aus dem Index entfernt.');">
|
||||
<input type="hidden"
|
||||
name="_token"
|
||||
value="{{ csrf_token('delete_document_' ~ document.id) }}">
|
||||
<button class="btn btn-sm btn-outline-danger">
|
||||
Dokument löschen
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-lg-5">
|
||||
<div class="card bg-dark border-secondary text-light h-100 shadow-sm">
|
||||
<div class="card-body">
|
||||
<div class="d-flex justify-content-between align-items-center mb-3">
|
||||
<h5 class="text-info mb-0">Tags</h5>
|
||||
|
||||
<a href="{{ path('admin_document_tags_edit', {id: document.id}) }}"
|
||||
class="btn btn-sm btn-outline-light">
|
||||
Bearbeiten
|
||||
</a>
|
||||
</div>
|
||||
|
||||
{% if document.tags is empty %}
|
||||
<div class="alert alert-secondary mb-0">
|
||||
Diesem Dokument sind noch keine Tags zugewiesen.
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="d-flex flex-wrap gap-2">
|
||||
{% for tag in document.tags %}
|
||||
<span class="badge px-3 py-2
|
||||
{% if tag.type == 'catalog_entity' %}
|
||||
text-bg-info
|
||||
{% elseif tag.type == 'sales_signal' %}
|
||||
text-bg-warning
|
||||
{% else %}
|
||||
text-bg-secondary
|
||||
{% endif %}">
|
||||
{{ tag.label }}
|
||||
</span>
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
<div class="small text-muted mt-3">
|
||||
Tags steuern die semantische Routing-Ebene. Weise nur fachlich wirklich passende Tags zu.
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<div class="d-flex justify-content-between align-items-center mb-3 flex-wrap gap-2">
|
||||
<div>
|
||||
<h2 class="h5 mb-1">Versionen</h2>
|
||||
<div class="small text-muted">
|
||||
Beim Aktivieren einer Version wird automatisch ein Re-Ingest ausgelöst.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% if is_granted('ROLE_SUPER_ADMIN') %}
|
||||
<a href="{{ path('admin_document_version_new', {id: document.id}) }}"
|
||||
class="btn btn-sm btn-outline-info">
|
||||
Neue Version
|
||||
</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
{% if document.versions is empty %}
|
||||
|
||||
<div class="alert alert-secondary shadow-sm">
|
||||
Keine Versionen vorhanden.
|
||||
</div>
|
||||
|
||||
{% else %}
|
||||
|
||||
<div class="card bg-black border-secondary shadow-sm">
|
||||
<div class="card-body p-0">
|
||||
|
||||
<div class="table-responsive">
|
||||
<table class="table table-dark table-striped table-hover align-middle mb-0">
|
||||
<thead class="table-secondary text-dark">
|
||||
<tr>
|
||||
<th>Version</th>
|
||||
<th>Status</th>
|
||||
<th>Ingest</th>
|
||||
<th>Checksum</th>
|
||||
<th>Erstellt von</th>
|
||||
<th>Datum</th>
|
||||
<th class="text-end">Aktionen</th>
|
||||
<th style="width: 10%">Version</th>
|
||||
<th style="width: 10%">Aktiv</th>
|
||||
<th style="width: 14%">Ingest</th>
|
||||
<th style="width: 18%">Checksum</th>
|
||||
<th style="width: 16%">Erstellt von</th>
|
||||
<th style="width: 14%">Datum</th>
|
||||
<th class="text-end" style="width: 18%">Aktionen</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
|
||||
{% for version in document.versions %}
|
||||
|
||||
<tr>
|
||||
<td>
|
||||
<strong>v{{ version.versionNumber }}</strong>
|
||||
{% if document.currentVersion and version.id == document.currentVersion.id %}
|
||||
<div class="small text-info mt-1">Current</div>
|
||||
{% endif %}
|
||||
</td>
|
||||
|
||||
{# Aktivstatus #}
|
||||
<td>
|
||||
{% if version.isActive %}
|
||||
<span class="badge bg-success">Aktiv</span>
|
||||
{% else %}
|
||||
<span class="badge bg-dark border border-secondary">
|
||||
Inaktiv
|
||||
</span>
|
||||
<span class="badge bg-dark border border-secondary">Inaktiv</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
|
||||
{# Ingest Status #}
|
||||
<td>
|
||||
{% if version.ingestStatus == 'INDEXED' %}
|
||||
<span class="badge bg-success">INDEXED</span>
|
||||
@@ -125,99 +234,85 @@
|
||||
<span class="badge bg-secondary">PENDING</span>
|
||||
{% else %}
|
||||
<span class="badge bg-dark border border-secondary">
|
||||
{{ version.ingestStatus }}
|
||||
</span>
|
||||
{{ version.ingestStatus ?: '-' }}
|
||||
</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
|
||||
{# Checksum #}
|
||||
<td class="small text-secondary">
|
||||
{{ version.checksum ? version.checksum[:10] ~ '…' : '-' }}
|
||||
{% if version.checksum %}
|
||||
<code>{{ version.checksum[:12] ~ '…' }}</code>
|
||||
{% else %}
|
||||
-
|
||||
{% endif %}
|
||||
</td>
|
||||
|
||||
{# Created by #}
|
||||
<td>
|
||||
{{ version.createdBy ? version.createdBy.email : '-' }}
|
||||
</td>
|
||||
|
||||
{# Date #}
|
||||
<td class="small">
|
||||
{{ version.createdAt|date('d.m.Y H:i') }}
|
||||
{{ version.createdAt|date('d.m.Y H:i:s') }}
|
||||
</td>
|
||||
|
||||
{# Aktionen #}
|
||||
<td class="text-end">
|
||||
|
||||
{% if version.isActive %}
|
||||
|
||||
{% if version.ingestStatus in ['PENDING', 'FAILED'] and is_granted('ROLE_SUPER_ADMIN') %}
|
||||
|
||||
<form method="post"
|
||||
action="{{ path('admin_document_version_ingest', {versionId: version.id}) }}"
|
||||
class="d-inline"
|
||||
onsubmit="return confirm('Ingest erneut starten?');">
|
||||
|
||||
<input type="hidden"
|
||||
name="_token"
|
||||
value="{{ csrf_token('ingest_version_' ~ version.id) }}">
|
||||
|
||||
<button class="btn btn-sm btn-outline-info">
|
||||
Ingest starten
|
||||
</button>
|
||||
</form>
|
||||
|
||||
<div class="d-flex justify-content-end flex-wrap gap-2">
|
||||
{% if version.isActive %}
|
||||
{% if version.ingestStatus in ['PENDING', 'FAILED'] and is_granted('ROLE_SUPER_ADMIN') %}
|
||||
<form method="post"
|
||||
action="{{ path('admin_document_version_ingest', {versionId: version.id}) }}"
|
||||
class="d-inline"
|
||||
onsubmit="return confirm('Ingest erneut starten?');">
|
||||
<input type="hidden"
|
||||
name="_token"
|
||||
value="{{ csrf_token('ingest_version_' ~ version.id) }}">
|
||||
<button class="btn btn-sm btn-outline-info">
|
||||
Ingest starten
|
||||
</button>
|
||||
</form>
|
||||
{% else %}
|
||||
<span class="small text-success align-self-center">
|
||||
Keine Aktion nötig
|
||||
</span>
|
||||
{% endif %}
|
||||
{% else %}
|
||||
<span class="text-success small">
|
||||
Bereits indexiert
|
||||
</span>
|
||||
{% if is_granted('ROLE_SUPER_ADMIN') %}
|
||||
<form method="post"
|
||||
action="{{ path('admin_document_version_activate', {versionId: version.id}) }}"
|
||||
class="d-inline"
|
||||
onsubmit="return confirm('Diese Version aktivieren? Es wird ein Re-Ingest ausgelöst.');">
|
||||
<input type="hidden"
|
||||
name="_token"
|
||||
value="{{ csrf_token('activate_version_' ~ version.id) }}">
|
||||
<button class="btn btn-sm btn-outline-light">
|
||||
Aktivieren
|
||||
</button>
|
||||
</form>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
|
||||
{% else %}
|
||||
|
||||
{% if is_granted('ROLE_SUPER_ADMIN') %}
|
||||
<form method="post"
|
||||
action="{{ path('admin_document_version_activate', {versionId: version.id}) }}"
|
||||
class="d-inline"
|
||||
onsubmit="return confirm('Diese Version aktivieren? Es wird ein Re-Ingest ausgelöst.');">
|
||||
|
||||
<input type="hidden"
|
||||
name="_token"
|
||||
value="{{ csrf_token('activate_version_' ~ version.id) }}">
|
||||
|
||||
<button class="btn btn-sm btn-outline-light">
|
||||
Aktivieren
|
||||
</button>
|
||||
</form>
|
||||
{% endif %}
|
||||
|
||||
{% endif %}
|
||||
|
||||
</div>
|
||||
</td>
|
||||
|
||||
</tr>
|
||||
|
||||
{% endfor %}
|
||||
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
{% endif %}
|
||||
|
||||
<div class="mt-4 small text-secondary">
|
||||
Hinweis: Beim Aktivieren einer Version wird automatisch ein Re-Ingest
|
||||
durchgeführt. Der NDJSON-Index und der FAISS-Index werden deterministisch
|
||||
neu aufgebaut.
|
||||
</div>
|
||||
|
||||
{% else %}
|
||||
|
||||
<div class="alert alert-danger">
|
||||
Dokument nicht gefunden.
|
||||
</div>
|
||||
|
||||
{% endif %}
|
||||
|
||||
{% endblock %}
|
||||
<div class="card bg-dark border-secondary text-light mt-4 shadow-sm">
|
||||
<div class="card-body">
|
||||
<h5 class="text-info mb-3">Hinweis zum Lifecycle</h5>
|
||||
<div class="small text-light">
|
||||
Beim Aktivieren einer Version wird automatisch ein Re-Ingest durchgeführt.
|
||||
Der NDJSON-Bestand und der Vektorindex werden deterministisch neu aufgebaut.
|
||||
Wenn Tags zugewiesen sind, beeinflusst dieses Dokument zusätzlich die semantische Routing-Ebene.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% endblock %}
|
||||
@@ -4,81 +4,87 @@
|
||||
|
||||
{% block body %}
|
||||
|
||||
{# ============================================= #}
|
||||
{# Tag-Rebuild Status (Echte Live-Anzeige) #}
|
||||
{# ============================================= #}
|
||||
|
||||
<div id="rebuild-status" class="mb-5" style="min-height:54px"></div>
|
||||
<div id="rebuild-status" class="mb-4">
|
||||
{% if latestJob %}
|
||||
<div class="alert alert-secondary shadow-sm mb-0">
|
||||
Status wird geladen…
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<script>
|
||||
let polling = null;
|
||||
const statusBox = document.getElementById('rebuild-status');
|
||||
const source = new EventSource("{{ path('admin_tags_rebuild_stream') }}");
|
||||
|
||||
function renderStatus(status) {
|
||||
const el = document.getElementById('rebuild-status');
|
||||
source.onmessage = function (event) {
|
||||
const data = JSON.parse(event.data);
|
||||
let html = '';
|
||||
|
||||
if (!status) {
|
||||
el.innerHTML = '';
|
||||
return;
|
||||
}
|
||||
|
||||
if (status === 'RUNNING') {
|
||||
el.innerHTML = `
|
||||
<div class="alert alert-info d-flex justify-content-between align-items-center">
|
||||
<div><strong>Dokument-Tag-Rebuild läuft…</strong></div>
|
||||
if (data.status === '{{ statusRunning }}') {
|
||||
html = `
|
||||
<div class="alert alert-info shadow-sm d-flex justify-content-between align-items-center mb-0">
|
||||
<div>
|
||||
<strong>Dokument-Tag-Rebuild läuft</strong><br>
|
||||
${data.startedAt ? 'Gestartet: ' + new Date(data.startedAt).toLocaleString() : ''}
|
||||
</div>
|
||||
<div class="spinner-border spinner-border-sm"></div>
|
||||
</div>
|
||||
`;
|
||||
} else if (status === 'QUEUED') {
|
||||
el.innerHTML = `
|
||||
<div class="alert alert-secondary">
|
||||
Dokument-Tag-Rebuild in Warteschlange…
|
||||
} else if (data.status === '{{ statusQueued }}') {
|
||||
html = `
|
||||
<div class="alert alert-secondary shadow-sm mb-0">
|
||||
<strong>Dokument-Tag-Rebuild in Warteschlange</strong>
|
||||
</div>
|
||||
`;
|
||||
} else if (status === 'COMPLETED') {
|
||||
el.innerHTML = `
|
||||
<div class="alert alert-success fw-bold">
|
||||
Dokument-Tag-Rebuild erfolgreich abgeschlossen.
|
||||
} else if (data.status === '{{ statusCompleted }}') {
|
||||
html = `
|
||||
<div class="alert alert-success shadow-sm mb-0">
|
||||
<i class="bi bi-check-lg"></i> Dokument-Tag-Rebuild erfolgreich abgeschlossen
|
||||
</div>
|
||||
`;
|
||||
stopPolling();
|
||||
} else if (status === 'FAILED') {
|
||||
el.innerHTML = `
|
||||
<div class="alert alert-danger">
|
||||
Dokument-Tag-Rebuild fehlgeschlagen.
|
||||
} else if (data.status === '{{ statusFailed }}') {
|
||||
html = `
|
||||
<div class="alert alert-danger shadow-sm mb-0">
|
||||
<strong>Dokument-Tag-Rebuild fehlgeschlagen</strong><br>
|
||||
${data.error ? '<code>' + data.error + '</code>' : ''}
|
||||
</div>
|
||||
`;
|
||||
stopPolling();
|
||||
}
|
||||
}
|
||||
|
||||
function checkStatus() {
|
||||
fetch('{{ path('admin_tags_status') }}')
|
||||
.then(r => r.json())
|
||||
.then(data => renderStatus(data.status))
|
||||
.catch(() => stopPolling());
|
||||
}
|
||||
statusBox.innerHTML = html;
|
||||
};
|
||||
|
||||
function startPolling() {
|
||||
polling = setInterval(checkStatus, 2000);
|
||||
}
|
||||
source.onerror = function () {
|
||||
console.warn('SSE Verbindung verloren');
|
||||
};
|
||||
|
||||
function stopPolling() {
|
||||
if (polling) {
|
||||
clearInterval(polling);
|
||||
polling = null;
|
||||
}
|
||||
}
|
||||
|
||||
// Start polling sofort
|
||||
checkStatus();
|
||||
startPolling();
|
||||
window.addEventListener('beforeunload', function () {
|
||||
source.close();
|
||||
});
|
||||
</script>
|
||||
|
||||
{% for message in app.flashes('success') %}
|
||||
<div class="alert alert-success shadow-sm">
|
||||
{{ message }}
|
||||
</div>
|
||||
{% endfor %}
|
||||
|
||||
{% for message in app.flashes('danger') %}
|
||||
<div class="alert alert-danger shadow-sm">
|
||||
{{ message }}
|
||||
</div>
|
||||
{% endfor %}
|
||||
|
||||
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||
<h1 class="h3 mb-0">
|
||||
Tags für Dokument
|
||||
<span class="text-info">{{ document.title }}</span>
|
||||
</h1>
|
||||
<div>
|
||||
<h1 class="h3 mb-1">
|
||||
Tags für Dokument
|
||||
<span class="text-info">{{ document.title }}</span>
|
||||
</h1>
|
||||
<div class="small text-muted">
|
||||
Weise nur Tags zu, die den fachlichen Kern des Dokuments wirklich beschreiben.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<a href="{{ path('admin_documents') }}"
|
||||
class="btn btn-sm btn-outline-light">
|
||||
@@ -86,14 +92,40 @@
|
||||
</a>
|
||||
</div>
|
||||
|
||||
{# ============================================= #}
|
||||
{# Bereits zugewiesene Tags #}
|
||||
{# ============================================= #}
|
||||
<div class="card bg-dark border-secondary text-light mb-4 shadow-sm">
|
||||
<div class="card-body row g-4">
|
||||
<div class="col-lg-7">
|
||||
<h5 class="text-info mb-3">Hinweis für gutes Tagging</h5>
|
||||
|
||||
<div class="card bg-dark border-secondary mb-4">
|
||||
<ul class="small mb-0">
|
||||
<li><strong>Präzise statt breit:</strong> lieber produkt- oder themenscharfe Tags als allgemeine Oberbegriffe.</li>
|
||||
<li><strong>Catalog Entity</strong> nur bei echten Produktfamilien, Katalogbegriffen oder klaren Entitäten.</li>
|
||||
<li><strong>Generic</strong> nur als unterstützende Zusatzsemantik.</li>
|
||||
<li><strong>Sales Signal</strong> sparsam und bewusst einsetzen, nicht als Ersatz für Fach-Tags.</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="col-lg-5">
|
||||
<h5 class="text-info mb-3">Aktueller Stand</h5>
|
||||
|
||||
<div class="d-flex flex-wrap gap-2">
|
||||
<span class="badge text-bg-dark border border-secondary">
|
||||
Zugewiesen: {{ document.tags|length }}
|
||||
</span>
|
||||
<span class="badge text-bg-dark border border-secondary">
|
||||
Verfügbar: {{ allTags|length }}
|
||||
</span>
|
||||
<span class="badge text-bg-dark border border-secondary">
|
||||
Nicht zugewiesen: {{ allTags|length - document.tags|length }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card bg-dark border-secondary mb-4 shadow-sm">
|
||||
<div class="card-body">
|
||||
|
||||
<h5 class="mb-3">Zugewiesene Tags für: <span class="text-info ">{{ document.title }}</span></h5>
|
||||
<h5 class="mb-3">Bereits zugewiesene Tags</h5>
|
||||
|
||||
{% if document.tags is empty %}
|
||||
<div class="alert alert-secondary mb-0">
|
||||
@@ -101,22 +133,26 @@
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="d-flex flex-wrap gap-2">
|
||||
{% for tag in document.tags %}
|
||||
<span class="badge bg-info text-dark px-3 py-2">
|
||||
{{ tag.label }}
|
||||
</span>
|
||||
{% for tag in allTags %}
|
||||
{% if tag in document.tags %}
|
||||
<span class="badge px-3 py-2
|
||||
{% if tag.type == 'catalog_entity' %}
|
||||
text-bg-info
|
||||
{% elseif tag.type == 'sales_signal' %}
|
||||
text-bg-warning
|
||||
{% else %}
|
||||
text-bg-secondary
|
||||
{% endif %}">
|
||||
{{ tag.label }}
|
||||
</span>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{# ============================================= #}
|
||||
{# Tag-Zuweisung Formular #}
|
||||
{# ============================================= #}
|
||||
|
||||
<div class="card bg-black border-secondary">
|
||||
<div class="card bg-black border-secondary shadow-sm">
|
||||
<div class="card-body">
|
||||
|
||||
<h5 class="text-info mb-3">Tags zuweisen</h5>
|
||||
@@ -128,38 +164,125 @@
|
||||
name="_token"
|
||||
value="{{ csrf_token('admin_document_tags_save_' ~ document.id) }}">
|
||||
|
||||
<div class="row">
|
||||
{% for tag in allTags %}
|
||||
<div class="col-md-2 mb-2">
|
||||
|
||||
<div class="form-check">
|
||||
|
||||
<input
|
||||
class="form-check-input"
|
||||
type="checkbox"
|
||||
name="tag_ids[]"
|
||||
value="{{ tag.id }}"
|
||||
id="tag_{{ tag.id }}"
|
||||
{% if tag in document.tags %}checked{% endif %}
|
||||
>
|
||||
|
||||
<label class="form-check-label bg-info text-black badge"{% if tag not in document.tags %} style="opacity: .5;"{% endif %}
|
||||
for="tag_{{ tag.id }}">
|
||||
{{ tag.label }}
|
||||
</label>
|
||||
|
||||
<div class="row g-4">
|
||||
<div class="col-lg-6">
|
||||
<div class="card bg-dark border-secondary h-100">
|
||||
<div class="card-header bg-secondary-subtle text-dark fw-semibold">
|
||||
Zugewiesene Tags
|
||||
</div>
|
||||
|
||||
<div class="card-body">
|
||||
<div class="row">
|
||||
{% set hasAssigned = false %}
|
||||
{% for tag in allTags %}
|
||||
{% if tag in document.tags %}
|
||||
{% set hasAssigned = true %}
|
||||
<div class="col-md-6 mb-3">
|
||||
<div class="form-check">
|
||||
<input
|
||||
class="form-check-input"
|
||||
type="checkbox"
|
||||
name="tag_ids[]"
|
||||
value="{{ tag.id }}"
|
||||
id="tag_{{ tag.id }}"
|
||||
checked
|
||||
>
|
||||
<label class="form-check-label w-100" for="tag_{{ tag.id }}">
|
||||
<span class="badge
|
||||
{% if tag.type == 'catalog_entity' %}
|
||||
text-bg-info
|
||||
{% elseif tag.type == 'sales_signal' %}
|
||||
text-bg-warning
|
||||
{% else %}
|
||||
text-bg-secondary
|
||||
{% endif %}">
|
||||
{{ tag.type }}
|
||||
</span>
|
||||
<span class="ms-2 fw-semibold">{{ tag.label }}</span>
|
||||
{% if tag.description %}
|
||||
<div class="small text-muted mt-1">{{ tag.description }}</div>
|
||||
{% endif %}
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
|
||||
{% if not hasAssigned %}
|
||||
<div class="col-12">
|
||||
<div class="text-muted">
|
||||
Noch keine Tags zugewiesen.
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
<div class="col-lg-6">
|
||||
<div class="card bg-dark border-secondary h-100">
|
||||
<div class="card-header bg-secondary-subtle text-dark fw-semibold">
|
||||
Verfügbare Tags
|
||||
</div>
|
||||
|
||||
<div class="card-body">
|
||||
<div class="row">
|
||||
{% set hasAvailable = false %}
|
||||
{% for tag in allTags %}
|
||||
{% if tag not in document.tags %}
|
||||
{% set hasAvailable = true %}
|
||||
<div class="col-md-6 mb-3">
|
||||
<div class="form-check">
|
||||
<input
|
||||
class="form-check-input"
|
||||
type="checkbox"
|
||||
name="tag_ids[]"
|
||||
value="{{ tag.id }}"
|
||||
id="tag_{{ tag.id }}"
|
||||
>
|
||||
<label class="form-check-label w-100" for="tag_{{ tag.id }}">
|
||||
<span class="badge
|
||||
{% if tag.type == 'catalog_entity' %}
|
||||
text-bg-info
|
||||
{% elseif tag.type == 'sales_signal' %}
|
||||
text-bg-warning
|
||||
{% else %}
|
||||
text-bg-secondary
|
||||
{% endif %}">
|
||||
{{ tag.type }}
|
||||
</span>
|
||||
<span class="ms-2">{{ tag.label }}</span>
|
||||
{% if tag.description %}
|
||||
<div class="small text-muted mt-1">{{ tag.description }}</div>
|
||||
{% endif %}
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
|
||||
{% if not hasAvailable %}
|
||||
<div class="col-12">
|
||||
<div class="text-muted">
|
||||
Keine weiteren Tags verfügbar.
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<hr class="border-secondary">
|
||||
|
||||
<button type="submit"
|
||||
class="btn btn-sm btn-outline-info">
|
||||
Speichern
|
||||
</button>
|
||||
<div class="d-flex justify-content-end">
|
||||
<button type="submit"
|
||||
class="btn btn-sm btn-outline-info">
|
||||
Speichern
|
||||
</button>
|
||||
</div>
|
||||
|
||||
</form>
|
||||
|
||||
|
||||
@@ -4,8 +4,17 @@
|
||||
|
||||
{% block body %}
|
||||
|
||||
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||
<h1 class="h3"><i class="bi bi-terminal"></i> Indexierung (Ingest Jobs)</h1>
|
||||
{% set latestJob = jobs is not empty ? jobs|first : null %}
|
||||
|
||||
<div class="d-flex justify-content-between align-items-center mb-4 flex-wrap gap-2">
|
||||
<div>
|
||||
<h1 class="h3 mb-1">
|
||||
<i class="bi bi-terminal"></i> Indexierung (Ingest Jobs)
|
||||
</h1>
|
||||
<div class="small text-muted">
|
||||
Übersicht über Reindex-, Dokument- und Aktivierungsjobs des Systems.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% if is_granted('ROLE_SUPER_ADMIN') %}
|
||||
<form method="post"
|
||||
@@ -25,102 +34,215 @@
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<div class="card bg-dark border-secondary text-light mb-4 shadow-sm">
|
||||
<div class="card-body row g-4">
|
||||
<div class="col-lg-7">
|
||||
<h5 class="text-info mb-3">Was sieht man hier?</h5>
|
||||
<ul class="small mb-0">
|
||||
<li><strong>DOCUMENT</strong> verarbeitet ein einzelnes Dokument neu.</li>
|
||||
<li><strong>DOCUMENT_VERSION_ACTIVATE</strong> zieht eine aktivierte Version deterministisch neu in den Index.</li>
|
||||
<li><strong>DOCUMENT_DELETE</strong> entfernt Dokumentinhalt wieder sauber aus den Index-Artefakten.</li>
|
||||
<li><strong>GLOBAL_REINDEX</strong> baut den Wissensindex vollständig neu auf und ist der stärkste Reparaturpfad.</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="col-lg-5">
|
||||
<h5 class="text-info mb-3">Worauf achten?</h5>
|
||||
<ul class="small mb-0">
|
||||
<li><strong>RUNNING</strong> und <strong>QUEUED</strong> bedeuten: keine unnötigen parallelen Rebuilds starten.</li>
|
||||
<li><strong>FAILED</strong> oder <strong>ABORTED</strong> direkt prüfen.</li>
|
||||
<li>Bei inkonsistentem Indexzustand ist meist ein <strong>Global Reindex</strong> der richtige Reparaturschritt.</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% if latestJob %}
|
||||
<div class="card bg-black border-secondary text-light mb-4 shadow-sm">
|
||||
<div class="card-body">
|
||||
<div class="d-flex justify-content-between align-items-start flex-wrap gap-3">
|
||||
<div>
|
||||
<div class="small text-muted mb-1">Letzter Job</div>
|
||||
<div class="fw-semibold">
|
||||
<a href="{{ path('admin_job_show', {id: latestJob.id}) }}"
|
||||
class="text-light text-decoration-none">
|
||||
{{ latestJob.id }}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div class="small text-muted mb-1">Typ</div>
|
||||
<span class="badge bg-info text-dark">{{ latestJob.type }}</span>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div class="small text-muted mb-1">Status</div>
|
||||
{% if latestJob.status == 'COMPLETED' %}
|
||||
<span class="badge bg-success">COMPLETED</span>
|
||||
{% elseif latestJob.status == 'QUEUED' %}
|
||||
<span class="badge bg-secondary">QUEUED</span>
|
||||
{% elseif latestJob.status == 'RUNNING' %}
|
||||
<span class="badge bg-warning text-dark">RUNNING</span>
|
||||
{% elseif latestJob.status == 'FAILED' %}
|
||||
<span class="badge bg-danger">FAILED</span>
|
||||
{% elseif latestJob.status == 'ABORTED' %}
|
||||
<span class="badge bg-dark border border-danger text-danger">ABORTED</span>
|
||||
{% else %}
|
||||
<span class="badge bg-dark border border-secondary">{{ latestJob.status }}</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div class="small text-muted mb-1">Gestartet</div>
|
||||
<div class="small">
|
||||
{{ latestJob.startedAt ? latestJob.startedAt|date('d.m.Y H:i:s') : '-' }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div class="small text-muted mb-1">Beendet</div>
|
||||
<div class="small">
|
||||
{{ latestJob.finishedAt ? latestJob.finishedAt|date('d.m.Y H:i:s') : 'läuft noch / offen' }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% if latestJob.errorMessage %}
|
||||
<div class="alert alert-danger small mt-3 mb-0">
|
||||
<strong>Fehler:</strong>
|
||||
{{ latestJob.errorMessage|slice(0, 250) }}{% if latestJob.errorMessage|length > 250 %}…{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if jobs is empty %}
|
||||
|
||||
<div class="alert alert-secondary">
|
||||
<div class="alert alert-secondary shadow-sm">
|
||||
Keine Ingest Jobs vorhanden.
|
||||
</div>
|
||||
|
||||
{% else %}
|
||||
|
||||
<div class="card bg-black border-secondary">
|
||||
<div class="card bg-black border-secondary shadow-sm">
|
||||
<div class="card-body p-0">
|
||||
|
||||
<table class="table table-dark table-striped table-hover align-middle mb-0">
|
||||
<thead class="table-secondary text-dark">
|
||||
<tr>
|
||||
<th>Job-ID</th>
|
||||
<th>Typ</th>
|
||||
<th>Status</th>
|
||||
<th>Dokument</th>
|
||||
<th>Version</th>
|
||||
<th>Gestartet</th>
|
||||
<th>Beendet</th>
|
||||
<th>Benutzer</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<div class="d-flex justify-content-between align-items-center px-3 py-3 border-bottom border-secondary flex-wrap gap-2">
|
||||
<div>
|
||||
<strong class="text-info">Vorhandene Jobs</strong>
|
||||
<span class="small text-muted ms-2">{{ jobs|length }} Einträge</span>
|
||||
</div>
|
||||
|
||||
{% for job in jobs %}
|
||||
<div class="small text-muted">
|
||||
Neueste Jobs stehen oben.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="table-responsive">
|
||||
<table class="table table-dark table-striped table-hover align-middle mb-0">
|
||||
<thead class="table-secondary text-dark">
|
||||
<tr>
|
||||
|
||||
<td class="small">
|
||||
<a href="{{ path('admin_job_show', {id: job.id}) }}"
|
||||
class="text-light text-decoration-none">
|
||||
{{ job.id }}
|
||||
</a>
|
||||
</td>
|
||||
|
||||
<td>
|
||||
<span class="badge bg-info text-dark">
|
||||
{{ job.type }}
|
||||
</span>
|
||||
</td>
|
||||
|
||||
<td>
|
||||
{% if job.status == 'COMPLETED' %}
|
||||
<span class="badge bg-success">COMPLETED</span>
|
||||
{% elseif job.status == 'QUEUED' %}
|
||||
<span class="badge bg-secondary">QUEUED</span>
|
||||
{% elseif job.status == 'RUNNING' %}
|
||||
<span class="badge bg-warning text-dark">RUNNING</span>
|
||||
{% elseif job.status == 'FAILED' %}
|
||||
<span class="badge bg-danger">FAILED</span>
|
||||
{% else %}
|
||||
<span class="badge bg-dark border border-secondary">
|
||||
{{ job.status }}
|
||||
</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
|
||||
<td>
|
||||
{% if job.documentId %}
|
||||
<a href="{{ path('admin_document_show', {id: job.documentId}) }}"
|
||||
class="text-light text-decoration-none">
|
||||
{{ job.documentId }}
|
||||
</a>
|
||||
{% else %}
|
||||
-
|
||||
{% endif %}
|
||||
</td>
|
||||
|
||||
<td>
|
||||
{{ job.documentVersionId ?? '-' }}
|
||||
</td>
|
||||
|
||||
<td class="small">
|
||||
{{ job.startedAt ? job.startedAt|date('d.m.Y H:i:s') : '-' }}
|
||||
</td>
|
||||
|
||||
<td class="small">
|
||||
{{ job.finishedAt ? job.finishedAt|date('d.m.Y H:i:s') : '-' }}
|
||||
</td>
|
||||
|
||||
<td class="small">
|
||||
{{ job.startedBy ? job.startedBy.email : '-' }}
|
||||
</td>
|
||||
|
||||
<th style="width: 18%">Job</th>
|
||||
<th style="width: 14%">Typ</th>
|
||||
<th style="width: 12%">Status</th>
|
||||
<th style="width: 18%">Bezug</th>
|
||||
<th style="width: 12%">Gestartet</th>
|
||||
<th style="width: 12%">Beendet</th>
|
||||
<th style="width: 14%">Benutzer</th>
|
||||
</tr>
|
||||
{% else %}
|
||||
<tr>
|
||||
<td colspan="8" class="text-center text-secondary py-4">
|
||||
Keine Jobs gefunden.
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</thead>
|
||||
<tbody>
|
||||
|
||||
</tbody>
|
||||
</table>
|
||||
{% for job in jobs %}
|
||||
<tr>
|
||||
<td class="small">
|
||||
<div class="fw-semibold">
|
||||
<a href="{{ path('admin_job_show', {id: job.id}) }}"
|
||||
class="text-light text-decoration-none">
|
||||
{{ job.id }}
|
||||
</a>
|
||||
</div>
|
||||
|
||||
{% if job.errorMessage %}
|
||||
<div class="text-danger small mt-1"
|
||||
title="{{ job.errorMessage }}">
|
||||
{{ job.errorMessage|slice(0, 120) }}{% if job.errorMessage|length > 120 %}…{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
</td>
|
||||
|
||||
<td>
|
||||
<span class="badge bg-info text-dark">
|
||||
{{ job.type }}
|
||||
</span>
|
||||
</td>
|
||||
|
||||
<td>
|
||||
{% if job.status == 'COMPLETED' %}
|
||||
<span class="badge bg-success">COMPLETED</span>
|
||||
{% elseif job.status == 'QUEUED' %}
|
||||
<span class="badge bg-secondary">QUEUED</span>
|
||||
{% elseif job.status == 'RUNNING' %}
|
||||
<span class="badge bg-warning text-dark">RUNNING</span>
|
||||
{% elseif job.status == 'FAILED' %}
|
||||
<span class="badge bg-danger">FAILED</span>
|
||||
{% elseif job.status == 'ABORTED' %}
|
||||
<span class="badge bg-dark border border-danger text-danger">ABORTED</span>
|
||||
{% else %}
|
||||
<span class="badge bg-dark border border-secondary">
|
||||
{{ job.status }}
|
||||
</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
|
||||
<td class="small">
|
||||
{% if job.documentId %}
|
||||
<div>
|
||||
<span class="text-muted">Dokument:</span>
|
||||
<a href="{{ path('admin_document_show', {id: job.documentId}) }}"
|
||||
class="text-light text-decoration-none">
|
||||
{{ job.documentId }}
|
||||
</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if job.documentVersionId %}
|
||||
<div class="mt-1">
|
||||
<span class="text-muted">Version:</span>
|
||||
{{ job.documentVersionId }}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if not job.documentId and not job.documentVersionId %}
|
||||
-
|
||||
{% endif %}
|
||||
</td>
|
||||
|
||||
<td class="small">
|
||||
{{ job.startedAt ? job.startedAt|date('d.m.Y H:i:s') : '-' }}
|
||||
</td>
|
||||
|
||||
<td class="small">
|
||||
{{ job.finishedAt ? job.finishedAt|date('d.m.Y H:i:s') : 'offen' }}
|
||||
</td>
|
||||
|
||||
<td class="small">
|
||||
{{ job.startedBy ? job.startedBy.email : '-' }}
|
||||
</td>
|
||||
</tr>
|
||||
{% else %}
|
||||
<tr>
|
||||
<td colspan="7" class="text-center text-secondary py-4">
|
||||
Keine Jobs gefunden.
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
@@ -128,8 +250,8 @@
|
||||
{% endif %}
|
||||
|
||||
<div class="mt-4 small text-secondary">
|
||||
Hinweis: Während laufender Jobs (Status RUNNING) sollten keine
|
||||
parallelen Reindex-Prozesse gestartet werden.
|
||||
Hinweis: Während laufender Jobs (Status <strong>RUNNING</strong>) oder wartender Jobs (<strong>QUEUED</strong>)
|
||||
sollten keine unnötigen parallelen Reindex-Prozesse gestartet werden.
|
||||
</div>
|
||||
|
||||
{% endblock %}
|
||||
{% endblock %}
|
||||
@@ -4,8 +4,18 @@
|
||||
|
||||
{% block body %}
|
||||
|
||||
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||
<h1 class="h3 mb-0">Ingest Job</h1>
|
||||
{% set jobStatus = job.status|upper %}
|
||||
{% set isActiveJob = jobStatus in ['QUEUED', 'RUNNING'] %}
|
||||
|
||||
<div class="d-flex justify-content-between align-items-center mb-4 flex-wrap gap-2">
|
||||
<div>
|
||||
<h1 class="h3 mb-1">
|
||||
<i class="bi bi-terminal"></i> Ingest Job
|
||||
</h1>
|
||||
<div class="small text-muted">
|
||||
Detailansicht für einen einzelnen Indexierungs- oder Rebuild-Job.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<a href="{{ path('admin_jobs') }}"
|
||||
class="btn btn-sm btn-outline-secondary">
|
||||
@@ -13,61 +23,134 @@
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="card bg-black border-secondary text-light">
|
||||
<div class="card bg-dark border-secondary text-light mb-4 shadow-sm">
|
||||
<div class="card-body row g-4">
|
||||
<div class="col-lg-7">
|
||||
<h5 class="text-info mb-3">Einordnung</h5>
|
||||
<ul class="small mb-0">
|
||||
<li><strong>DOCUMENT</strong> verarbeitet ein einzelnes Dokument neu.</li>
|
||||
<li><strong>DOCUMENT_VERSION_ACTIVATE</strong> aktiviert eine Version und zieht sie deterministisch neu in den Index.</li>
|
||||
<li><strong>DOCUMENT_DELETE</strong> entfernt Dokumentinhalt wieder sauber aus dem Wissensbestand.</li>
|
||||
<li><strong>GLOBAL_REINDEX</strong> baut den Gesamtindex vollständig neu auf.</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="col-lg-5">
|
||||
<h5 class="text-info mb-3">Aktueller Zustand</h5>
|
||||
<div class="d-flex flex-wrap gap-2">
|
||||
{% if jobStatus == 'COMPLETED' %}
|
||||
<span class="badge bg-success">COMPLETED</span>
|
||||
{% elseif jobStatus == 'QUEUED' %}
|
||||
<span class="badge bg-secondary">QUEUED</span>
|
||||
{% elseif jobStatus == 'RUNNING' %}
|
||||
<span class="badge bg-warning text-dark">RUNNING</span>
|
||||
{% elseif jobStatus == 'FAILED' %}
|
||||
<span class="badge bg-danger">FAILED</span>
|
||||
{% elseif jobStatus == 'ABORTED' %}
|
||||
<span class="badge bg-dark border border-danger text-danger">ABORTED</span>
|
||||
{% else %}
|
||||
<span class="badge bg-dark border border-secondary">{{ jobStatus }}</span>
|
||||
{% endif %}
|
||||
|
||||
{% if isActiveJob %}
|
||||
<span class="badge text-bg-info">Polling aktiv</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card bg-black border-secondary text-light shadow-sm">
|
||||
<div class="card-body">
|
||||
|
||||
<div class="mb-2">
|
||||
<strong>ID:</strong>
|
||||
<span class="small text-light">{{ job.id }}</span>
|
||||
<div class="row g-4">
|
||||
<div class="col-lg-6">
|
||||
<div class="mb-3">
|
||||
<div class="small text-muted mb-1">Job-ID</div>
|
||||
<div class="fw-semibold small text-light">{{ job.id }}</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<div class="small text-muted mb-1">Typ</div>
|
||||
<div>
|
||||
<span class="badge bg-info text-dark">{{ job.type }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<div class="small text-muted mb-1">Status</div>
|
||||
<div id="job-status-badge">
|
||||
{% if jobStatus == 'COMPLETED' %}
|
||||
<span class="badge bg-success">COMPLETED</span>
|
||||
{% elseif jobStatus == 'QUEUED' %}
|
||||
<span class="badge bg-secondary">QUEUED</span>
|
||||
{% elseif jobStatus == 'RUNNING' %}
|
||||
<span class="badge bg-warning text-dark">RUNNING</span>
|
||||
{% elseif jobStatus == 'FAILED' %}
|
||||
<span class="badge bg-danger">FAILED</span>
|
||||
{% elseif jobStatus == 'ABORTED' %}
|
||||
<span class="badge bg-dark border border-danger text-danger">ABORTED</span>
|
||||
{% else %}
|
||||
<span class="badge bg-dark border border-secondary">{{ jobStatus }}</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<div class="small text-muted mb-1">Dokument</div>
|
||||
<div>
|
||||
{% if job.documentId %}
|
||||
<a href="{{ path('admin_document_show', {id: job.documentId}) }}"
|
||||
class="text-light text-decoration-none">
|
||||
{{ job.documentId }}
|
||||
</a>
|
||||
{% else %}
|
||||
-
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-0">
|
||||
<div class="small text-muted mb-1">Dokumentversion</div>
|
||||
<div>{{ job.documentVersionId ?? '-' }}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-lg-6">
|
||||
<div class="mb-3">
|
||||
<div class="small text-muted mb-1">Gestartet</div>
|
||||
<div>
|
||||
{{ job.startedAt ? job.startedAt|date('d.m.Y H:i:s') : '-' }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<div class="small text-muted mb-1">Beendet</div>
|
||||
<div id="job-finished-at">
|
||||
{{ job.finishedAt ? job.finishedAt|date('d.m.Y H:i:s') : '-' }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<div class="small text-muted mb-1">Gestartet von</div>
|
||||
<div>{{ job.startedBy ? job.startedBy.email : '-' }}</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-0">
|
||||
<div class="small text-muted mb-1">Polling</div>
|
||||
<div class="small text-light">
|
||||
{% if isActiveJob %}
|
||||
Status wird automatisch aktualisiert.
|
||||
{% else %}
|
||||
Kein Live-Polling nötig.
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-2">
|
||||
<strong>Typ:</strong>
|
||||
<span class="badge bg-info text-dark">{{ job.type }}</span>
|
||||
</div>
|
||||
|
||||
<div class="mb-2">
|
||||
<strong>Status:</strong>
|
||||
<span id="job-status-badge"></span>
|
||||
</div>
|
||||
|
||||
<div class="mb-2">
|
||||
<strong>Dokument:</strong>
|
||||
{% if job.documentId %}
|
||||
<a href="{{ path('admin_document_show', {id: job.documentId}) }}"
|
||||
class="text-light text-decoration-none">
|
||||
{{ job.documentId }}
|
||||
</a>
|
||||
{% else %}
|
||||
-
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<div class="mb-2">
|
||||
<strong>Version:</strong>
|
||||
{{ job.documentVersionId ?? '-' }}
|
||||
</div>
|
||||
|
||||
<div class="mb-2">
|
||||
<strong>Gestartet:</strong>
|
||||
{{ job.startedAt|date('d.m.Y H:i:s') }}
|
||||
</div>
|
||||
|
||||
<div class="mb-2">
|
||||
<strong>Beendet:</strong>
|
||||
<span id="job-finished-at">
|
||||
{{ job.finishedAt ? job.finishedAt|date('d.m.Y H:i:s') : '-' }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="mb-2">
|
||||
<strong>Gestartet von:</strong>
|
||||
{{ job.startedBy ? job.startedBy.email : '-' }}
|
||||
</div>
|
||||
|
||||
{# Loader #}
|
||||
<div id="job-loader"
|
||||
class="mt-3 d-none">
|
||||
class="mt-4 {% if not isActiveJob %}d-none{% endif %}">
|
||||
<div class="d-flex align-items-center gap-2">
|
||||
<div class="spinner-border spinner-border-sm text-info" role="status"></div>
|
||||
<div>
|
||||
@@ -79,10 +162,10 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{# Fehlerbereich #}
|
||||
<div id="job-error"
|
||||
class="alert alert-danger mt-3 {% if not job.errorMessage %}d-none{% endif %}">
|
||||
class="alert alert-danger mt-4 {% if not job.errorMessage %}d-none{% endif %}">
|
||||
{% if job.errorMessage %}
|
||||
<strong>Fehler:</strong><br>
|
||||
{{ job.errorMessage }}
|
||||
{% endif %}
|
||||
</div>
|
||||
@@ -91,13 +174,13 @@
|
||||
</div>
|
||||
|
||||
<div class="mt-4 small text-secondary">
|
||||
Hinweis: Bei DOCUMENT_VERSION_ACTIVATE-Jobs wird ein vollständiger
|
||||
NDJSON-Rebuild und FAISS-Reindex durchgeführt.
|
||||
Hinweis: Bei <strong>DOCUMENT_VERSION_ACTIVATE</strong>-Jobs wird ein vollständiger
|
||||
NDJSON-Rebuild und FAISS-Reindex durchgeführt. Bei <strong>GLOBAL_REINDEX</strong>
|
||||
wird der gesamte Wissensindex neu aufgebaut.
|
||||
</div>
|
||||
|
||||
<script>
|
||||
(function () {
|
||||
|
||||
const statusUrl = {{ path('admin_job_status', {id: job.id})|json_encode|raw }};
|
||||
const badgeWrap = document.getElementById('job-status-badge');
|
||||
const finishedAtEl = document.getElementById('job-finished-at');
|
||||
@@ -106,18 +189,26 @@
|
||||
|
||||
let timer = null;
|
||||
|
||||
function escapeHtml(value) {
|
||||
return String(value)
|
||||
.replaceAll('&', '&')
|
||||
.replaceAll('<', '<')
|
||||
.replaceAll('>', '>')
|
||||
.replaceAll('"', '"')
|
||||
.replaceAll("'", ''');
|
||||
}
|
||||
|
||||
function renderBadge(status) {
|
||||
const map = {
|
||||
COMPLETED: 'bg-success',
|
||||
QUEUED: 'bg-secondary',
|
||||
RUNNING: 'bg-warning text-dark',
|
||||
FAILED: 'bg-danger',
|
||||
ABORTED: 'bg-dark'
|
||||
ABORTED: 'bg-dark border border-danger text-danger'
|
||||
};
|
||||
|
||||
const css = map[status] || 'bg-secondary';
|
||||
badgeWrap.innerHTML =
|
||||
`<span class="badge ${css}">${status}</span>`;
|
||||
const css = map[status] || 'bg-dark border border-secondary';
|
||||
badgeWrap.innerHTML = `<span class="badge ${css}">${escapeHtml(status || 'UNKNOWN')}</span>`;
|
||||
}
|
||||
|
||||
function stopPolling() {
|
||||
@@ -127,20 +218,39 @@
|
||||
}
|
||||
}
|
||||
|
||||
function renderError(message) {
|
||||
if (!message) {
|
||||
errorEl.classList.add('d-none');
|
||||
errorEl.innerHTML = '';
|
||||
return;
|
||||
}
|
||||
|
||||
errorEl.classList.remove('d-none');
|
||||
errorEl.innerHTML = `<strong>Fehler:</strong><br>${escapeHtml(message)}`;
|
||||
}
|
||||
|
||||
async function poll() {
|
||||
try {
|
||||
const res = await fetch(statusUrl);
|
||||
if (!res.ok) return;
|
||||
const res = await fetch(statusUrl, {
|
||||
headers: {
|
||||
'Accept': 'application/json'
|
||||
},
|
||||
cache: 'no-store'
|
||||
});
|
||||
|
||||
if (!res.ok) {
|
||||
stopPolling();
|
||||
return;
|
||||
}
|
||||
|
||||
const data = await res.json();
|
||||
const status = (data.status || '').toUpperCase();
|
||||
const status = String(data.status || '').toUpperCase();
|
||||
|
||||
renderBadge(status);
|
||||
|
||||
finishedAtEl.textContent =
|
||||
data.finishedAt
|
||||
? new Date(data.finishedAt).toLocaleString('de-DE')
|
||||
: '-';
|
||||
finishedAtEl.textContent = data.finishedAt
|
||||
? new Date(data.finishedAt).toLocaleString('de-DE')
|
||||
: '-';
|
||||
|
||||
if (status === 'QUEUED' || status === 'RUNNING') {
|
||||
loaderEl.classList.remove('d-none');
|
||||
@@ -149,26 +259,22 @@
|
||||
stopPolling();
|
||||
}
|
||||
|
||||
if (status === 'FAILED' && data.errorMessage) {
|
||||
errorEl.classList.remove('d-none');
|
||||
errorEl.innerHTML =
|
||||
`<strong>Fehler:</strong><br>${data.errorMessage}`;
|
||||
if (status === 'FAILED' || status === 'ABORTED') {
|
||||
renderError(data.errorMessage || '');
|
||||
} else {
|
||||
renderError('');
|
||||
}
|
||||
|
||||
} catch (e) {
|
||||
stopPolling();
|
||||
}
|
||||
}
|
||||
|
||||
// Initial render from server state
|
||||
renderBadge("{{ job.status|upper }}");
|
||||
renderBadge({{ jobStatus|json_encode|raw }});
|
||||
|
||||
if (["QUEUED", "RUNNING"].includes("{{ job.status|upper }}")) {
|
||||
loaderEl.classList.remove('d-none');
|
||||
if ({{ isActiveJob ? 'true' : 'false' }}) {
|
||||
timer = setInterval(poll, 2000);
|
||||
}
|
||||
|
||||
})();
|
||||
</script>
|
||||
|
||||
{% endblock %}
|
||||
{% endblock %}
|
||||
@@ -4,19 +4,24 @@
|
||||
|
||||
{% block body %}
|
||||
|
||||
{# ========================================================= #}
|
||||
{# LIVE REBUILD STATUS (SSE) #}
|
||||
{# ========================================================= #}
|
||||
|
||||
<div id="rebuild-status" class="mb-5">
|
||||
<div class="alert alert-secondary shadow-sm">
|
||||
Status wird geladen…
|
||||
</div>
|
||||
<div id="rebuild-status" class="mb-4">
|
||||
{% if latestJob %}
|
||||
<div class="alert alert-secondary shadow-sm mb-0">
|
||||
Status wird geladen…
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||
<h1 class="h3 mb-0">
|
||||
<i class="bi bi-tag-fill"></i> Tag: {{ tag.label }}
|
||||
</h1>
|
||||
<div>
|
||||
<h1 class="h3 mb-1">
|
||||
<i class="bi bi-tag-fill"></i> Tag: {{ tag.label }}
|
||||
</h1>
|
||||
|
||||
<div class="small text-muted">
|
||||
Slug: <code>{{ tag.slug }}</code>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<a href="{{ path('admin_tags_index') }}"
|
||||
class="btn btn-sm btn-outline-secondary">
|
||||
@@ -24,7 +29,6 @@
|
||||
</a>
|
||||
</div>
|
||||
|
||||
|
||||
<script>
|
||||
const statusBox = document.getElementById('rebuild-status');
|
||||
const source = new EventSource("{{ path('admin_tags_rebuild_stream') }}");
|
||||
@@ -35,9 +39,9 @@
|
||||
|
||||
if (data.status === '{{ statusRunning }}') {
|
||||
html = `
|
||||
<div class="alert alert-info shadow-sm d-flex justify-content-between align-items-center">
|
||||
<div class="alert alert-info shadow-sm d-flex justify-content-between align-items-center mb-0">
|
||||
<div>
|
||||
Tag-Rebuild läuft<br>
|
||||
<strong>Tag-Rebuild läuft</strong><br>
|
||||
${data.startedAt ? 'Gestartet: ' + new Date(data.startedAt).toLocaleString() : ''}
|
||||
</div>
|
||||
<div class="spinner-border spinner-border-sm"></div>
|
||||
@@ -45,20 +49,20 @@
|
||||
`;
|
||||
} else if (data.status === '{{ statusQueued }}') {
|
||||
html = `
|
||||
<div class="alert alert-secondary shadow-sm">
|
||||
Tag-Rebuild in Warteschlange
|
||||
<div class="alert alert-secondary shadow-sm mb-0">
|
||||
<strong>Tag-Rebuild in Warteschlange</strong>
|
||||
</div>
|
||||
`;
|
||||
} else if (data.status === '{{ statusCompleted }}') {
|
||||
html = `
|
||||
<div class="alert alert-success shadow-sm">
|
||||
<div class="alert alert-success shadow-sm mb-0">
|
||||
<i class="bi bi-check-lg"></i> Tag-Rebuild erfolgreich abgeschlossen
|
||||
</div>
|
||||
`;
|
||||
} else if (data.status === '{{ statusFailed }}') {
|
||||
html = `
|
||||
<div class="alert alert-danger shadow-sm">
|
||||
Tag-Rebuild fehlgeschlagen<br>
|
||||
<div class="alert alert-danger shadow-sm mb-0">
|
||||
<strong>Tag-Rebuild fehlgeschlagen</strong><br>
|
||||
${data.error ? '<code>' + data.error + '</code>' : ''}
|
||||
</div>
|
||||
`;
|
||||
@@ -70,100 +74,179 @@
|
||||
source.onerror = function () {
|
||||
console.warn('SSE Verbindung verloren');
|
||||
};
|
||||
|
||||
window.addEventListener('beforeunload', function () {
|
||||
source.close();
|
||||
});
|
||||
</script>
|
||||
|
||||
{# ============================= #}
|
||||
{# Flash Messages #}
|
||||
{# ============================= #}
|
||||
|
||||
{% for message in app.flashes('success') %}
|
||||
<div class="alert alert-success">
|
||||
<div class="alert alert-success shadow-sm">
|
||||
{{ message }}
|
||||
</div>
|
||||
{% endfor %}
|
||||
|
||||
{% for message in app.flashes('danger') %}
|
||||
<div class="alert alert-danger">
|
||||
<div class="alert alert-danger shadow-sm">
|
||||
{{ message }}
|
||||
</div>
|
||||
{% endfor %}
|
||||
|
||||
{# ============================= #}
|
||||
{# Tag → Dokumente #}
|
||||
{# ============================= #}
|
||||
<div class="card bg-dark border-secondary text-light mb-4 shadow-sm">
|
||||
<div class="card-body row g-4">
|
||||
<div class="col-lg-7">
|
||||
<h5 class="text-info mb-3">Einordnung des Tags</h5>
|
||||
|
||||
<div class="mb-2">
|
||||
{% if tag.type == 'catalog_entity' %}
|
||||
<span class="badge text-bg-info">Catalog Entity</span>
|
||||
{% elseif tag.type == 'sales_signal' %}
|
||||
<span class="badge text-bg-warning">Sales Signal</span>
|
||||
{% else %}
|
||||
<span class="badge text-bg-secondary">Generic</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<p class="small mb-2">
|
||||
{{ tag.description ?: 'Keine Beschreibung hinterlegt.' }}
|
||||
</p>
|
||||
|
||||
<p class="small text-muted mb-0">
|
||||
Weise diesen Tag nur Dokumenten zu, die fachlich wirklich denselben Gegenstand,
|
||||
dieselbe Produktfamilie oder denselben Anwendungsfall abbilden.
|
||||
Zu breite Zuweisungen machen das Routing weicher.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="col-lg-5">
|
||||
<h5 class="text-info mb-3">Aktueller Stand</h5>
|
||||
|
||||
<div class="d-flex flex-wrap gap-2">
|
||||
<span class="badge text-bg-dark border border-secondary">
|
||||
Zugewiesen: {{ assignedDocIds|length }}
|
||||
</span>
|
||||
<span class="badge text-bg-dark border border-secondary">
|
||||
Verfügbar: {{ documents|length }}
|
||||
</span>
|
||||
<span class="badge text-bg-dark border border-secondary">
|
||||
Nicht zugewiesen: {{ documents|length - assignedDocIds|length }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<form method="post">
|
||||
|
||||
<input type="hidden"
|
||||
name="_token"
|
||||
value="{{ csrf_token('assign_tag_' ~ tag.id) }}">
|
||||
|
||||
<div class="card bg-black border-secondary">
|
||||
<div class="card-body p-0 row">
|
||||
<div class=" col-lg-6">
|
||||
<table class="table table-dark table-striped table-hover mb-0 align-middle">
|
||||
<thead class="table-secondary text-dark">
|
||||
<tr>
|
||||
<th style="width:60px;"><i class="bi bi-three-dots"></i></th>
|
||||
<th>Zugewiesene Dokumente</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<div class="row g-4">
|
||||
<div class="col-lg-6">
|
||||
<div class="card bg-black border-secondary shadow-sm h-100">
|
||||
<div class="card-header bg-secondary-subtle text-dark fw-semibold">
|
||||
Zugewiesene Dokumente
|
||||
</div>
|
||||
|
||||
<tbody>
|
||||
{% for doc in documents %}
|
||||
{% if doc.id in assignedDocIds %}
|
||||
<div class="card-body p-0">
|
||||
<div class="table-responsive">
|
||||
<table class="table table-dark table-striped table-hover mb-0 align-middle">
|
||||
<thead class="table-secondary text-dark">
|
||||
<tr>
|
||||
<td>
|
||||
<input type="checkbox"
|
||||
name="documents[]"
|
||||
value="{{ doc.id }}"
|
||||
checked>
|
||||
</td>
|
||||
<td>
|
||||
{{ doc.title }}
|
||||
</td>
|
||||
<th style="width: 60px;">
|
||||
<i class="bi bi-check2-square"></i>
|
||||
</th>
|
||||
<th>Dokument</th>
|
||||
</tr>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</thead>
|
||||
<tbody>
|
||||
{% set hasAssigned = false %}
|
||||
{% for doc in documents %}
|
||||
{% if doc.id in assignedDocIds %}
|
||||
{% set hasAssigned = true %}
|
||||
<tr>
|
||||
<td>
|
||||
<input type="checkbox"
|
||||
name="documents[]"
|
||||
value="{{ doc.id }}"
|
||||
checked>
|
||||
</td>
|
||||
<td class="fw-semibold">
|
||||
{{ doc.title }}
|
||||
</td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
|
||||
</tbody>
|
||||
</table>
|
||||
{% if not hasAssigned %}
|
||||
<tr>
|
||||
<td colspan="2" class="text-center text-muted p-4">
|
||||
Noch keine Dokumente zugewiesen.
|
||||
</td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class=" col-lg-6">
|
||||
<table class="table table-dark table-striped table-hover mb-0 align-middle col-lg-6">
|
||||
<thead class="table-secondary text-dark">
|
||||
<tr>
|
||||
<th style="width:60px;"><i class="bi bi-three-dots"></i></th>
|
||||
<th>Nicht zugewiesene Dokumente</th>
|
||||
</tr>
|
||||
</thead>
|
||||
</div>
|
||||
|
||||
<tbody>
|
||||
{% for doc in documents %}
|
||||
{% if doc.id not in assignedDocIds %}
|
||||
<div class="col-lg-6">
|
||||
<div class="card bg-black border-secondary shadow-sm h-100">
|
||||
<div class="card-header bg-secondary-subtle text-dark fw-semibold">
|
||||
Verfügbare Dokumente
|
||||
</div>
|
||||
|
||||
<div class="card-body p-0">
|
||||
<div class="table-responsive">
|
||||
<table class="table table-dark table-striped table-hover mb-0 align-middle">
|
||||
<thead class="table-secondary text-dark">
|
||||
<tr>
|
||||
<td>
|
||||
<input type="checkbox"
|
||||
name="documents[]"
|
||||
value="{{ doc.id }}"
|
||||
>
|
||||
</td>
|
||||
<td class="opacity-50">
|
||||
{{ doc.title }}
|
||||
</td>
|
||||
<th style="width: 60px;">
|
||||
<i class="bi bi-square"></i>
|
||||
</th>
|
||||
<th>Dokument</th>
|
||||
</tr>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% set hasUnassigned = false %}
|
||||
{% for doc in documents %}
|
||||
{% if doc.id not in assignedDocIds %}
|
||||
{% set hasUnassigned = true %}
|
||||
<tr>
|
||||
<td>
|
||||
<input type="checkbox"
|
||||
name="documents[]"
|
||||
value="{{ doc.id }}">
|
||||
</td>
|
||||
<td class="opacity-75">
|
||||
{{ doc.title }}
|
||||
</td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
|
||||
{% if not hasUnassigned %}
|
||||
<tr>
|
||||
<td colspan="2" class="text-center text-muted p-4">
|
||||
Keine weiteren aktiven Dokumente verfügbar.
|
||||
</td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button class="btn btn-primary mt-3">
|
||||
Speichern
|
||||
</button>
|
||||
|
||||
<div class="d-flex justify-content-end mt-4">
|
||||
<button class="btn btn-primary">
|
||||
Zuweisungen speichern
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
{% endblock %}
|
||||
@@ -4,77 +4,52 @@
|
||||
|
||||
{% block body %}
|
||||
|
||||
{# ========================================================= #}
|
||||
{# LIVE REBUILD STATUS (SSE) #}
|
||||
{# ========================================================= #}
|
||||
|
||||
<div id="rebuild-status" class="mb-5">
|
||||
<div id="rebuild-status" class="mb-4">
|
||||
{% if latestJob %}
|
||||
<div class="alert alert-secondary shadow-sm">
|
||||
<div class="alert alert-secondary shadow-sm mb-0">
|
||||
Status wird geladen…
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||
<h1 class="h3 mb-0"><i class="bi bi-tag-fill"></i> Tag-Management</h1>
|
||||
<h1 class="h3 mb-0">
|
||||
<i class="bi bi-tag-fill"></i> Tag-Management
|
||||
</h1>
|
||||
</div>
|
||||
|
||||
{# ========================================================= #}
|
||||
{# TAG SYSTEM DESCRIPTION #}
|
||||
{# ========================================================= #}
|
||||
<div class="card bg-dark border-secondary text-light mb-4 shadow-sm">
|
||||
<div class="card-body row">
|
||||
<div class="card-body row g-4">
|
||||
<div class="col-lg-6">
|
||||
<h5 class="text-info mb-3">Was machen Tags im System?</h5>
|
||||
|
||||
<p class="small text-light mb-2">
|
||||
Tags dienen als semantische Routing-Ebene innerhalb des RAG-Systems.
|
||||
Sie strukturieren Dokumente thematisch und beeinflussen,
|
||||
welche Inhalte bei einer Nutzeranfrage priorisiert werden.
|
||||
Tags sind die semantische Routing-Ebene innerhalb des Systems.
|
||||
Sie helfen dabei, thematisch passende Dokumenträume schneller zu erkennen
|
||||
und gute Retrieval-Kandidaten zu priorisieren.
|
||||
</p>
|
||||
|
||||
<ul class="small text-light mb-3">
|
||||
<li>
|
||||
Tags werden Dokumenten manuell zugewiesen.
|
||||
</li>
|
||||
<li>
|
||||
Beim Rebuild wird aus allen Tags eine eigene
|
||||
<code>tags.ndjson</code> erzeugt.
|
||||
</li>
|
||||
<li>
|
||||
Zusätzlich wird ein separater Vektorindex
|
||||
(<code>vector_tags.index</code>) aufgebaut.
|
||||
</li>
|
||||
<li>
|
||||
Bei einer Anfrage erfolgt zunächst ein Tag-Matching,
|
||||
danach wird das Chunk-Retrieval entsprechend gewichtet.
|
||||
</li>
|
||||
<ul class="small text-light mb-0">
|
||||
<li>Tags werden Dokumenten manuell zugewiesen.</li>
|
||||
<li>Beim Rebuild wird aus den aktiven Tag-Zuordnungen eine <code>tags.ndjson</code> erzeugt.</li>
|
||||
<li>Zusätzlich wird ein eigener Tag-Vektorindex (<code>vector_tags.index</code>) gebaut.</li>
|
||||
<li>Bei Anfragen erfolgt zunächst ein semantisches Tag-Matching, danach das eigentliche Chunk-Retrieval.</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="col-lg-6">
|
||||
<h6 class="text-info mt-3">Wie werden Tags bewertet?</h6>
|
||||
<h5 class="text-info mb-3">Was ist gutes Tagging?</h5>
|
||||
|
||||
<p class="small text-light mb-2">
|
||||
Die Bewertung erfolgt über einen eigenen Vektor-Similarity-Score
|
||||
im Tag-Index. Das System berechnet:
|
||||
</p>
|
||||
|
||||
<ul class="small text-light">
|
||||
<li>
|
||||
Ähnlichkeit zwischen Nutzeranfrage und Tag-Embedding
|
||||
</li>
|
||||
<li>
|
||||
Top-K Treffer im Tag-Index
|
||||
</li>
|
||||
<li>
|
||||
Gewichtete Übergabe an das Chunk-Retrieval
|
||||
</li>
|
||||
<ul class="small text-light mb-3">
|
||||
<li><strong>Präzise statt generisch:</strong> lieber <code>Produktnamen</code> als <code>Gerät</code>.</li>
|
||||
<li><strong>Fachlich sauber:</strong> Tags sollen echte Produktfamilien, Anwendungsfälle oder Entitäten abbilden.</li>
|
||||
<li><strong>Wenig Überschneidung:</strong> keine unnötig breiten oder doppeldeutigen Tags.</li>
|
||||
<li><strong>Bewusst typisieren:</strong> <code>catalog_entity</code> für echte Katalog-/Entity-Tags, <code>generic</code> nur für allgemeine Zusatzsemantik.</li>
|
||||
</ul>
|
||||
|
||||
<p class="small text-light mt-2 mb-0">
|
||||
Tags wirken somit als semantischer Verstärker.
|
||||
Sie ersetzen kein Chunk-Retrieval, sondern steuern dessen Priorisierung.
|
||||
<p class="small text-warning mb-0">
|
||||
Zu breite Tags wie „Produkt“, „System“ oder „Gerät“ machen das Routing weicher
|
||||
und bringen meist weniger Nutzen als präzise fachliche Tags.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -90,9 +65,9 @@
|
||||
|
||||
if (data.status === '{{ statusRunning }}') {
|
||||
html = `
|
||||
<div class="alert alert-info shadow-sm d-flex justify-content-between align-items-center">
|
||||
<div class="alert alert-info shadow-sm d-flex justify-content-between align-items-center mb-0">
|
||||
<div>
|
||||
Tag-Rebuild läuft<br>
|
||||
<strong>Tag-Rebuild läuft</strong><br>
|
||||
${data.startedAt ? 'Gestartet: ' + new Date(data.startedAt).toLocaleString() : ''}
|
||||
</div>
|
||||
<div class="spinner-border spinner-border-sm"></div>
|
||||
@@ -100,20 +75,20 @@
|
||||
`;
|
||||
} else if (data.status === '{{ statusQueued }}') {
|
||||
html = `
|
||||
<div class="alert alert-secondary shadow-sm">
|
||||
Tag-Rebuild in Warteschlange
|
||||
<div class="alert alert-secondary shadow-sm mb-0">
|
||||
<strong>Tag-Rebuild in Warteschlange</strong>
|
||||
</div>
|
||||
`;
|
||||
} else if (data.status === '{{ statusCompleted }}') {
|
||||
html = `
|
||||
<div class="alert alert-success shadow-sm">
|
||||
<div class="alert alert-success shadow-sm mb-0">
|
||||
<i class="bi bi-check-lg"></i> Tag-Rebuild erfolgreich abgeschlossen
|
||||
</div>
|
||||
`;
|
||||
} else if (data.status === '{{ statusFailed }}') {
|
||||
html = `
|
||||
<div class="alert alert-danger shadow-sm">
|
||||
Tag-Rebuild fehlgeschlagen<br>
|
||||
<div class="alert alert-danger shadow-sm mb-0">
|
||||
<strong>Tag-Rebuild fehlgeschlagen</strong><br>
|
||||
${data.error ? '<code>' + data.error + '</code>' : ''}
|
||||
</div>
|
||||
`;
|
||||
@@ -125,11 +100,12 @@
|
||||
source.onerror = function () {
|
||||
console.warn('SSE Verbindung verloren');
|
||||
};
|
||||
|
||||
window.addEventListener('beforeunload', function () {
|
||||
source.close();
|
||||
});
|
||||
</script>
|
||||
|
||||
{# ========================================================= #}
|
||||
{# Create Tag Card #}
|
||||
{# ========================================================= #}
|
||||
<div class="card bg-black border-secondary text-light mb-4 shadow-sm">
|
||||
<div class="card-body">
|
||||
<h5 class="text-info mb-3">Neuen Tag hinzufügen</h5>
|
||||
@@ -153,24 +129,26 @@
|
||||
required/>
|
||||
</div>
|
||||
|
||||
<div class="col-md-4">
|
||||
<label class="form-label small text-muted">Beschreibung</label>
|
||||
<input class="form-control form-control-sm"
|
||||
name="description"
|
||||
placeholder="Semantische Beschreibung des Tags"
|
||||
required/>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Type</label>
|
||||
<select name="type" class="form-select">
|
||||
<option value="generic">Generic</option>
|
||||
<option value="catalog_entity">Catalog Entity</option>
|
||||
<option value="sales_signal">Sales Signal</option>
|
||||
<div class="col-md-3">
|
||||
<label class="form-label small text-muted">Typ</label>
|
||||
<select name="type" class="form-select form-select-sm">
|
||||
{% for choiceLabel, choiceValue in tagTypeChoices %}
|
||||
<option value="{{ choiceValue }}"
|
||||
{% if choiceValue == 'generic' %}selected{% endif %}>
|
||||
{{ choiceLabel }}
|
||||
</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="col-md-2 d-grid align-items-end">
|
||||
<div class="col-md-3">
|
||||
<label class="form-label small text-muted">Beschreibung</label>
|
||||
<input class="form-control form-control-sm"
|
||||
name="description"
|
||||
placeholder="Optional: fachlicher Kontext des Tags"/>
|
||||
</div>
|
||||
|
||||
<div class="col-12 d-grid d-md-flex justify-content-md-end">
|
||||
<button class="btn btn-sm btn-outline-info">
|
||||
Anlegen
|
||||
</button>
|
||||
@@ -179,66 +157,85 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{# ========================================================= #}
|
||||
{# Tag Table #}
|
||||
{# ========================================================= #}
|
||||
<div class="card bg-black border-secondary text-light shadow-sm">
|
||||
<div class="card-body">
|
||||
|
||||
<div class="mb-3">
|
||||
<strong class="text-info">Vorhandene Tags:</strong>
|
||||
<span class="text-muted small ms-2">
|
||||
{{ tags|length }} Einträge
|
||||
</span>
|
||||
<div class="mb-3 d-flex justify-content-between align-items-center flex-wrap gap-2">
|
||||
<div>
|
||||
<strong class="text-info">Vorhandene Tags:</strong>
|
||||
<span class="text-muted small ms-2">
|
||||
{{ tags|length }} Einträge
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="small text-muted">
|
||||
Dokumentanzahl bezieht sich auf aktive Dokumente.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<table class="table table-dark table-striped table-hover mb-0 align-middle">
|
||||
<thead class="table-secondary text-dark">
|
||||
<tr>
|
||||
<th style="width: 25%">Label</th>
|
||||
<th style="width: 25%">Slug</th>
|
||||
<th style="width: 35%">Beschreibung</th>
|
||||
<th class="text-end" style="width: 15%">Aktion</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for tag in tags %}
|
||||
<div class="table-responsive">
|
||||
<table class="table table-dark table-striped table-hover mb-0 align-middle">
|
||||
<thead class="table-secondary text-dark">
|
||||
<tr>
|
||||
<td class="fw-semibold">{{ tag.label }}</td>
|
||||
<td><code>{{ tag.slug }}</code></td>
|
||||
<td>{{ tag.description ?: '-' }}</td>
|
||||
<td class="text-end">
|
||||
|
||||
<a href="{{ path('admin_tags_assign', { id: tag.id }) }}"
|
||||
class="btn btn-sm btn-outline-info me-2">
|
||||
Zuweisen
|
||||
</a>
|
||||
|
||||
<form method="post"
|
||||
action="{{ path('admin_tags_delete', {id: tag.id}) }}"
|
||||
style="display:inline-block;">
|
||||
|
||||
<input type="hidden"
|
||||
name="_token"
|
||||
value="{{ csrf_token('admin_tag_delete_' ~ tag.id) }}"/>
|
||||
|
||||
<button class="btn btn-sm btn-outline-danger"
|
||||
onclick="return confirm('Tag wirklich löschen? Zuweisungen werden entfernt.')">
|
||||
Löschen
|
||||
</button>
|
||||
</form>
|
||||
|
||||
</td>
|
||||
<th style="width: 18%">Label</th>
|
||||
<th style="width: 18%">Slug</th>
|
||||
<th style="width: 14%">Typ</th>
|
||||
<th style="width: 10%">Aktive Dokumente</th>
|
||||
<th style="width: 25%">Beschreibung</th>
|
||||
<th class="text-end" style="width: 15%">Aktion</th>
|
||||
</tr>
|
||||
{% else %}
|
||||
<tr>
|
||||
<td colspan="4" class="p-4 text-center text-muted">
|
||||
Noch keine Tags vorhanden.
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for tag in tags %}
|
||||
{% set activeDocumentCount = documentCountByTagId[tag.id.toRfc4122] ?? 0 %}
|
||||
<tr>
|
||||
<td class="fw-semibold">{{ tag.label }}</td>
|
||||
<td><code>{{ tag.slug }}</code></td>
|
||||
<td>
|
||||
{% if tag.type == 'catalog_entity' %}
|
||||
<span class="badge text-bg-info">Catalog Entity</span>
|
||||
{% elseif tag.type == 'sales_signal' %}
|
||||
<span class="badge text-bg-warning">Sales Signal</span>
|
||||
{% else %}
|
||||
<span class="badge text-bg-secondary">Generic</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
<span class="badge text-bg-dark border border-secondary">
|
||||
{{ activeDocumentCount }}
|
||||
</span>
|
||||
</td>
|
||||
<td>{{ tag.description ?: '-' }}</td>
|
||||
<td class="text-end">
|
||||
<a href="{{ path('admin_tags_assign', { id: tag.id }) }}"
|
||||
class="btn btn-sm btn-outline-info me-2">
|
||||
Zuweisen
|
||||
</a>
|
||||
|
||||
<form method="post"
|
||||
action="{{ path('admin_tags_delete', {id: tag.id}) }}"
|
||||
style="display:inline-block;">
|
||||
<input type="hidden"
|
||||
name="_token"
|
||||
value="{{ csrf_token('admin_tag_delete_' ~ tag.id) }}"/>
|
||||
|
||||
<button class="btn btn-sm btn-outline-danger"
|
||||
onclick="return confirm('Tag wirklich löschen? Zuweisungen werden entfernt.')">
|
||||
Löschen
|
||||
</button>
|
||||
</form>
|
||||
</td>
|
||||
</tr>
|
||||
{% else %}
|
||||
<tr>
|
||||
<td colspan="6" class="p-4 text-center text-muted">
|
||||
Noch keine Tags vorhanden.
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user