Analysepipeline: Visualisierung der Refresh-Schritte
Neuer Tab "Analysepipeline" zwischen Faktencheck und Quellenuebersicht.
Zeigt 9 Verarbeitungsschritte als n8n-artige Blockkette: Quellen sichten,
Nachrichten sammeln, Doppeltes filtern, Relevanz bewerten, Orte erkennen,
Lagebild verfassen, Fakten pruefen, Qualitaetscheck, Benachrichtigen.
- Backend: refresh_pipeline_steps-Tabelle persistiert pro Refresh+Pass die
Status- und Zahlen-Werte. pipeline_tracker.py kapselt Start/Done/Skip/Error
inkl. WebSocket-Broadcast (Event-Typ pipeline_step). 9 Hooks im Orchestrator
speisen die Anzeige.
- API: GET /api/incidents/{id}/pipeline liefert Definition + letzten Stand
(Zahlen aus letztem Refresh, Multi-Pass-Konsolidierung).
- Frontend: pipeline.js rendert Vollbild-Blockkette mit pulsierendem Glow am
aktiven Block, animierten Pfeilen bei Datenfluss, Haekchen am fertigen Block.
Hover-Tooltip mit Erklaerung in Nutzersprache, Klick oeffnet Detail-Popup.
Bei Research-Lagen leuchtet ein Schleifen-Pfeil pro Mehrfach-Durchlauf auf.
Mini-Variante (nur Icons) im Refresh-Progress-Popup.
- CSS: Light/Dark-Theme-fest, dezenter Circuit-Hintergrund (5% Opacity),
Mobile-vertikale Stapelung unter 900px, prefers-reduced-motion respektiert.
- Uebersprungene Schritte (z.B. Geoparsing ohne neue Artikel) werden
ausgeblendet, brandneue Lagen ohne Refresh zeigen Hinweis.
Tooltips bewusst in normaler Sprache ohne Internas (keine Modellnamen,
keine Toolnamen, keine Phasen-Labels).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Dieser Commit ist enthalten in:
@@ -5638,3 +5638,335 @@ body.tutorial-active .tutorial-cursor {
|
||||
border-color: var(--accent);
|
||||
box-shadow: 0 0 0 2px rgba(var(--accent-rgb, 59, 130, 246), 0.15);
|
||||
}
|
||||
|
||||
/* === Analysepipeline (Visualisierung n8n-Stil) === */
|
||||
.pipeline-card { padding: 0; overflow: hidden; }
|
||||
.pipeline-card .card-header { padding: var(--sp-lg) var(--sp-xl); border-bottom: 1px solid var(--border); }
|
||||
.pipeline-header-meta { font-size: 12px; color: var(--text-secondary); }
|
||||
.pipeline-body {
|
||||
position: relative;
|
||||
padding: var(--sp-3xl) var(--sp-xl);
|
||||
background-color: var(--bg-card);
|
||||
background-image:
|
||||
linear-gradient(var(--pipeline-circuit, rgba(150, 121, 26, 0.045)) 1px, transparent 1px),
|
||||
linear-gradient(90deg, var(--pipeline-circuit, rgba(150, 121, 26, 0.045)) 1px, transparent 1px),
|
||||
radial-gradient(circle at 30px 30px, var(--pipeline-circuit-dot, rgba(150, 121, 26, 0.10)) 1.5px, transparent 2px);
|
||||
background-size: 60px 60px, 60px 60px, 60px 60px;
|
||||
}
|
||||
[data-theme="light"] .pipeline-body {
|
||||
--pipeline-circuit: rgba(31, 51, 89, 0.05);
|
||||
--pipeline-circuit-dot: rgba(31, 51, 89, 0.10);
|
||||
}
|
||||
.pipeline-stage {
|
||||
position: relative;
|
||||
overflow-x: auto;
|
||||
overflow-y: visible;
|
||||
}
|
||||
.pipeline-track {
|
||||
display: flex;
|
||||
align-items: stretch;
|
||||
gap: var(--sp-md);
|
||||
min-width: max-content;
|
||||
padding: var(--sp-md) 0;
|
||||
}
|
||||
.pipeline-empty {
|
||||
text-align: center;
|
||||
color: var(--text-secondary);
|
||||
padding: var(--sp-4xl) var(--sp-xl);
|
||||
font-style: italic;
|
||||
}
|
||||
.pipeline-sidenote {
|
||||
margin-top: var(--sp-xl);
|
||||
padding: var(--sp-lg) var(--sp-xl);
|
||||
border-left: 3px solid var(--accent);
|
||||
background: var(--tint-accent-faint);
|
||||
border-radius: 0 var(--radius-lg) var(--radius-lg) 0;
|
||||
font-size: 13px;
|
||||
color: var(--text-secondary);
|
||||
max-width: 720px;
|
||||
}
|
||||
|
||||
.pipeline-block {
|
||||
position: relative;
|
||||
flex: 0 0 168px;
|
||||
min-height: 132px;
|
||||
padding: var(--sp-lg) var(--sp-md);
|
||||
background: var(--bg-elevated);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius-lg);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
text-align: center;
|
||||
cursor: pointer;
|
||||
transition: transform 0.2s ease, box-shadow 0.2s ease, border-color 0.2s ease;
|
||||
outline: none;
|
||||
}
|
||||
.pipeline-block:hover { transform: translateY(-2px); border-color: var(--accent); }
|
||||
.pipeline-block:focus-visible { box-shadow: 0 0 0 3px var(--tint-accent-strong); }
|
||||
.pipeline-block-icon {
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
color: var(--text-secondary);
|
||||
margin-bottom: var(--sp-sm);
|
||||
transition: color 0.3s ease;
|
||||
}
|
||||
.pipeline-block-icon svg { width: 100%; height: 100%; }
|
||||
.pipeline-block-title {
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
color: var(--text-primary);
|
||||
margin-bottom: var(--sp-xs);
|
||||
line-height: 1.2;
|
||||
}
|
||||
.pipeline-block-count {
|
||||
font-size: 11px;
|
||||
color: var(--text-secondary);
|
||||
line-height: 1.3;
|
||||
}
|
||||
.pipeline-block-count small { display: block; opacity: 0.75; font-size: 10px; }
|
||||
.pipeline-block-count .count-status { font-style: italic; opacity: 0.7; }
|
||||
.pipeline-block-check {
|
||||
position: absolute;
|
||||
top: 6px;
|
||||
right: 6px;
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
color: var(--success);
|
||||
opacity: 0;
|
||||
transform: scale(0.6);
|
||||
transition: opacity 0.3s ease, transform 0.3s ease;
|
||||
}
|
||||
.pipeline-block-check svg { width: 100%; height: 100%; }
|
||||
|
||||
.pipeline-block.status-pending { opacity: 0.55; }
|
||||
.pipeline-block.status-pending .pipeline-block-icon { color: var(--text-tertiary); }
|
||||
|
||||
.pipeline-block.status-active {
|
||||
border-color: var(--accent);
|
||||
box-shadow: var(--glow-accent-strong);
|
||||
animation: pipelinePulse 1.6s ease-in-out infinite;
|
||||
}
|
||||
.pipeline-block.status-active .pipeline-block-icon { color: var(--accent); }
|
||||
@keyframes pipelinePulse {
|
||||
0%, 100% { box-shadow: 0 0 8px rgba(150, 121, 26, 0.35), 0 0 0 1px var(--accent); }
|
||||
50% { box-shadow: 0 0 22px rgba(150, 121, 26, 0.65), 0 0 0 2px var(--accent); }
|
||||
}
|
||||
|
||||
.pipeline-block.status-done {
|
||||
border-color: var(--success);
|
||||
background: linear-gradient(180deg, var(--bg-elevated) 0%, var(--tint-success) 100%);
|
||||
}
|
||||
.pipeline-block.status-done .pipeline-block-icon { color: var(--success); }
|
||||
.pipeline-block.status-done .pipeline-block-check { opacity: 1; transform: scale(1); }
|
||||
|
||||
.pipeline-block.status-error {
|
||||
border-color: var(--error);
|
||||
background: linear-gradient(180deg, var(--bg-elevated) 0%, var(--tint-error) 100%);
|
||||
}
|
||||
.pipeline-block.status-error .pipeline-block-icon { color: var(--error); }
|
||||
|
||||
.pipeline-arrow {
|
||||
flex: 0 0 28px;
|
||||
align-self: center;
|
||||
height: 2px;
|
||||
position: relative;
|
||||
background: var(--border);
|
||||
}
|
||||
.pipeline-arrow::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
right: -4px;
|
||||
top: 50%;
|
||||
width: 0;
|
||||
height: 0;
|
||||
border-top: 4px solid transparent;
|
||||
border-bottom: 4px solid transparent;
|
||||
border-left: 6px solid var(--border);
|
||||
transform: translateY(-50%);
|
||||
}
|
||||
.pipeline-arrow.is-flowing {
|
||||
background: linear-gradient(90deg, var(--accent), var(--accent) 50%, transparent 50%, transparent);
|
||||
background-size: 12px 100%;
|
||||
animation: pipelineFlow 0.8s linear infinite;
|
||||
}
|
||||
.pipeline-arrow.is-flowing::after { border-left-color: var(--accent); }
|
||||
@keyframes pipelineFlow {
|
||||
from { background-position: 0 0; }
|
||||
to { background-position: 12px 0; }
|
||||
}
|
||||
|
||||
.pipeline-loop {
|
||||
position: absolute;
|
||||
bottom: -10px;
|
||||
right: -10px;
|
||||
width: 26px;
|
||||
height: 26px;
|
||||
color: var(--accent);
|
||||
background: var(--bg-card);
|
||||
border-radius: 50%;
|
||||
padding: 4px;
|
||||
border: 1px solid var(--border);
|
||||
opacity: 0.5;
|
||||
transition: opacity 0.3s ease;
|
||||
}
|
||||
.pipeline-loop svg { width: 100%; height: 100%; }
|
||||
.pipeline-stage.is-looping .pipeline-loop {
|
||||
opacity: 1;
|
||||
animation: pipelineLoop 1.2s ease-in-out;
|
||||
}
|
||||
@keyframes pipelineLoop {
|
||||
0% { transform: rotate(0deg) scale(1); }
|
||||
50% { transform: rotate(180deg) scale(1.3); }
|
||||
100% { transform: rotate(360deg) scale(1); }
|
||||
}
|
||||
|
||||
.pipeline-tooltip {
|
||||
position: fixed;
|
||||
background: var(--bg-card);
|
||||
color: var(--text-primary);
|
||||
border: 1px solid var(--accent);
|
||||
padding: var(--sp-md) var(--sp-lg);
|
||||
border-radius: var(--radius);
|
||||
font-size: 12px;
|
||||
line-height: 1.4;
|
||||
width: 280px;
|
||||
box-shadow: var(--shadow-md);
|
||||
pointer-events: none;
|
||||
opacity: 0;
|
||||
transition: opacity 0.15s ease;
|
||||
z-index: 9999;
|
||||
}
|
||||
.pipeline-tooltip.visible { opacity: 1; }
|
||||
|
||||
.pipeline-popup {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
background: var(--backdrop);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 9998;
|
||||
}
|
||||
.pipeline-popup-inner {
|
||||
background: var(--bg-card);
|
||||
border: 1px solid var(--accent);
|
||||
border-radius: var(--radius-lg);
|
||||
padding: var(--sp-3xl);
|
||||
max-width: 480px;
|
||||
width: 90%;
|
||||
box-shadow: var(--shadow-lg);
|
||||
position: relative;
|
||||
}
|
||||
.pipeline-popup-title {
|
||||
font-family: var(--font-title);
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
color: var(--text-primary);
|
||||
margin-bottom: var(--sp-lg);
|
||||
}
|
||||
.pipeline-popup-text { color: var(--text-secondary); line-height: 1.6; font-size: 14px; }
|
||||
.pipeline-popup-close {
|
||||
position: absolute;
|
||||
top: 8px;
|
||||
right: 8px;
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
border: none;
|
||||
background: transparent;
|
||||
color: var(--text-secondary);
|
||||
font-size: 22px;
|
||||
cursor: pointer;
|
||||
border-radius: var(--radius);
|
||||
}
|
||||
.pipeline-popup-close:hover { background: var(--bg-hover); color: var(--text-primary); }
|
||||
|
||||
.pipeline-mini {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-wrap: wrap;
|
||||
gap: var(--sp-xs);
|
||||
padding: var(--sp-md) 0;
|
||||
margin-bottom: var(--sp-md);
|
||||
}
|
||||
.pipeline-mini-block {
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
padding: 5px;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 50%;
|
||||
color: var(--text-tertiary);
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
.pipeline-mini-block svg { width: 100%; height: 100%; }
|
||||
.pipeline-mini-block.status-pending { opacity: 0.4; }
|
||||
.pipeline-mini-block.status-active {
|
||||
color: var(--accent);
|
||||
border-color: var(--accent);
|
||||
box-shadow: var(--glow-accent);
|
||||
animation: pipelinePulse 1.6s ease-in-out infinite;
|
||||
}
|
||||
.pipeline-mini-block.status-done {
|
||||
color: var(--success);
|
||||
border-color: var(--success);
|
||||
background: var(--tint-success);
|
||||
}
|
||||
.pipeline-mini-block.status-error {
|
||||
color: var(--error);
|
||||
border-color: var(--error);
|
||||
background: var(--tint-error);
|
||||
}
|
||||
.pipeline-mini-sep {
|
||||
width: 12px;
|
||||
height: 1px;
|
||||
background: var(--border);
|
||||
}
|
||||
|
||||
@media (max-width: 900px) {
|
||||
.pipeline-track { flex-direction: column; min-width: auto; align-items: stretch; }
|
||||
.pipeline-block { flex: 0 0 auto; width: 100%; min-height: auto; flex-direction: row; padding: var(--sp-md); text-align: left; gap: var(--sp-md); }
|
||||
.pipeline-block-icon { width: 28px; height: 28px; margin-bottom: 0; flex-shrink: 0; }
|
||||
.pipeline-block-title { margin-bottom: 2px; }
|
||||
.pipeline-block-count { font-size: 11px; }
|
||||
.pipeline-arrow {
|
||||
flex: 0 0 18px;
|
||||
width: 2px;
|
||||
height: 18px;
|
||||
margin: 0 auto;
|
||||
align-self: center;
|
||||
background: var(--border);
|
||||
}
|
||||
.pipeline-arrow::after {
|
||||
right: 50%;
|
||||
top: auto;
|
||||
bottom: -4px;
|
||||
border-top: 6px solid var(--border);
|
||||
border-bottom: none;
|
||||
border-left: 4px solid transparent;
|
||||
border-right: 4px solid transparent;
|
||||
transform: translateX(50%);
|
||||
}
|
||||
.pipeline-arrow.is-flowing {
|
||||
background: linear-gradient(180deg, var(--accent), var(--accent) 50%, transparent 50%, transparent);
|
||||
background-size: 100% 12px;
|
||||
animation: pipelineFlowVertical 0.8s linear infinite;
|
||||
}
|
||||
.pipeline-arrow.is-flowing::after { border-top-color: var(--accent); }
|
||||
@keyframes pipelineFlowVertical {
|
||||
from { background-position: 0 0; }
|
||||
to { background-position: 0 12px; }
|
||||
}
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.pipeline-block,
|
||||
.pipeline-mini-block { animation: none !important; }
|
||||
.pipeline-arrow.is-flowing { animation: none !important; }
|
||||
.pipeline-block.status-active { box-shadow: var(--glow-accent); }
|
||||
.pipeline-stage.is-looping .pipeline-loop { animation: none !important; opacity: 1; }
|
||||
}
|
||||
|
||||
In neuem Issue referenzieren
Einen Benutzer sperren