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>
76 Zeilen
2.9 KiB
JavaScript
76 Zeilen
2.9 KiB
JavaScript
/**
|
|
* LayoutManager: Tab-Navigation fuer das Monitor-Dashboard.
|
|
* Nur ein Tab-Panel gleichzeitig sichtbar, pro Lage gemerkt in localStorage.
|
|
*/
|
|
const LayoutManager = {
|
|
TAB_ORDER: ['zusammenfassung', 'lagebild', 'timeline', 'karte', 'faktencheck', 'pipeline', 'quellen'],
|
|
_currentIncidentId: null,
|
|
_initialized: false,
|
|
|
|
init() {
|
|
if (this._initialized) return;
|
|
const nav = document.getElementById('tab-nav');
|
|
if (!nav) return;
|
|
|
|
nav.querySelectorAll('.tab-btn').forEach(btn => {
|
|
btn.addEventListener('click', () => {
|
|
const tab = btn.getAttribute('data-tab');
|
|
if (tab) this.switchTab(tab);
|
|
});
|
|
});
|
|
|
|
nav.style.display = '';
|
|
this._initialized = true;
|
|
},
|
|
|
|
switchTab(tabId, save = true) {
|
|
if (!this.TAB_ORDER.includes(tabId)) tabId = 'zusammenfassung';
|
|
|
|
document.querySelectorAll('#tab-nav .tab-btn').forEach(b => {
|
|
b.classList.toggle('active', b.getAttribute('data-tab') === tabId);
|
|
});
|
|
document.querySelectorAll('.tab-panel').forEach(p => {
|
|
p.classList.toggle('active', p.id === 'panel-' + tabId);
|
|
});
|
|
|
|
// Leaflet-Karte: invalidateSize nach Panel-Wechsel, damit Tiles korrekt rendern
|
|
if (tabId === 'karte' && typeof UI !== 'undefined' && UI._map) {
|
|
setTimeout(() => { try { UI._map.invalidateSize(); } catch (e) { /* ignore */ } }, 50);
|
|
}
|
|
|
|
if (save && this._currentIncidentId != null) {
|
|
try {
|
|
localStorage.setItem('osint_tab_' + this._currentIncidentId, tabId);
|
|
} catch (e) { /* quota */ }
|
|
}
|
|
},
|
|
|
|
restoreTabFor(incidentId) {
|
|
this._currentIncidentId = incidentId;
|
|
let target = 'zusammenfassung';
|
|
try {
|
|
const saved = localStorage.getItem('osint_tab_' + incidentId);
|
|
if (saved && this.TAB_ORDER.includes(saved)) target = saved;
|
|
} catch (e) { /* ignore */ }
|
|
this.switchTab(target, false);
|
|
},
|
|
|
|
/** Tab-Labels je Incident-Typ anpassen (adhoc vs. research). */
|
|
applyTypeLabels(incidentType) {
|
|
const isResearch = incidentType === 'research';
|
|
const zf = document.querySelector('#tab-nav .tab-btn[data-tab="zusammenfassung"]');
|
|
const lb = document.querySelector('#tab-nav .tab-btn[data-tab="lagebild"]');
|
|
if (zf) zf.textContent = isResearch ? 'Zusammenfassung' : 'Neueste Entwicklungen';
|
|
if (lb) lb.textContent = isResearch ? 'Recherchebericht' : 'Lagebild';
|
|
},
|
|
|
|
// Legacy-API-Stubs: falls alte Aufrufe im Code liegen, stumm schlucken statt crashen.
|
|
toggleTile() { /* legacy no-op */ },
|
|
reset() { /* legacy no-op */ },
|
|
save() { /* legacy no-op */ },
|
|
resizeTileToContent() { /* legacy no-op */ },
|
|
destroy() { /* legacy no-op */ },
|
|
};
|
|
|
|
document.addEventListener('DOMContentLoaded', () => LayoutManager.init());
|