Fortschrittsanzeige: Popup mit Checkboxen, Blur, Pro-Lage-Timer

Ladebalken ersetzt durch zentriertes Popup-Fenster mit Checkbox-Checkliste
(Warteschlange, Recherche, Analyse, Faktencheck) und Echtzeit-Timer.

Erster Durchlauf: Popup nicht wegklickbar, Blur-Effekt auf Kacheln.
Aktualisierung: Popup minimierbar zu kompakter Status-Leiste.
Timer laeuft pro Lage im Hintergrund weiter bei Lagenwechsel.
Gesamtzeit wird am Ende im Abschluss-Popup angezeigt.

Sidebar: Animierter Gold-Rand und Fortschrittstext (Recherchiert/
Analysiert/Faktencheck) unter dem Lage-Namen bei laufendem Refresh.

Zusaetzlicher Cancel-Checkpoint im Orchestrator nach Uebersetzung.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Dieser Commit ist enthalten in:
Claude Dev
2026-04-09 20:08:59 +02:00
Ursprung 3356ba1ae5
Commit 3f88d00b8c
5 geänderte Dateien mit 588 neuen und 311 gelöschten Zeilen

Datei anzeigen

@@ -1167,6 +1167,9 @@ class AgentOrchestrator:
# Cancel-Check nach paralleler Verarbeitung
self._check_cancelled(incident_id)
# Cancel-Check nach Analyse+Faktencheck
self._check_cancelled(incident_id)
# --- Faktencheck-Ergebnisse verarbeiten ---
# Pre-Dedup: Duplikate aus LLM-Antwort entfernen
fact_checks = deduplicate_new_facts(fact_checks)

Datei anzeigen

@@ -1900,141 +1900,218 @@ a:hover {
}
/* === Fortschrittsanzeige === */
.progress-bar {
/* === Fortschritts-Popup === */
.progress-overlay {
position: fixed;
inset: 0;
z-index: 9000;
display: flex;
align-items: center;
justify-content: center;
pointer-events: none;
}
.progress-overlay.blocking {
pointer-events: auto;
background: rgba(0,0,0,0.15);
}
.progress-popup {
pointer-events: auto;
background: var(--bg-primary);
border: 1px solid var(--border);
border-radius: 12px;
width: 420px;
max-width: 92vw;
box-shadow: 0 16px 48px rgba(0,0,0,0.5);
overflow: hidden;
animation: popupIn 0.25s ease-out;
}
@keyframes popupIn {
from { opacity: 0; transform: scale(0.95) translateY(10px); }
to { opacity: 1; transform: scale(1) translateY(0); }
}
.progress-popup-header {
display: flex;
align-items: center;
gap: 8px;
padding: 16px 20px 12px;
border-bottom: 1px solid var(--border);
}
.progress-popup-title {
font-size: 14px;
font-weight: 600;
color: var(--text-primary);
flex: 1;
}
.progress-popup-timer {
font-family: var(--font-mono, 'Courier New', monospace);
font-size: 13px;
color: var(--accent);
font-weight: 600;
min-width: 42px;
text-align: right;
}
.progress-popup-minimize {
background: none;
border: 1px solid var(--border);
color: var(--text-secondary);
width: 28px;
height: 28px;
border-radius: 6px;
cursor: pointer;
font-size: 18px;
line-height: 1;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.15s;
}
.progress-popup-minimize:hover {
background: var(--bg-secondary);
color: var(--text-primary);
}
.progress-popup-body { padding: 16px 20px; }
.progress-popup-pass {
font-size: 11px;
color: var(--accent-primary);
font-weight: 600;
letter-spacing: 0.3px;
margin-bottom: 12px;
text-align: center;
}
.progress-checklist { display: flex; flex-direction: column; gap: 6px; }
.progress-check-item {
display: flex;
align-items: center;
gap: 10px;
padding: 8px 10px;
border-radius: 6px;
transition: background 0.2s;
}
.progress-check-item.active { background: rgba(240,180,41,0.08); }
.progress-check-item.done { opacity: 0.55; }
.progress-check-icon {
width: 20px;
height: 20px;
display: flex;
align-items: center;
justify-content: center;
font-size: 14px;
color: var(--text-disabled);
flex-shrink: 0;
}
.progress-check-item.active .progress-check-icon { color: var(--accent); }
.progress-check-item.done .progress-check-icon { color: var(--success); }
.progress-check-item.error .progress-check-icon { color: var(--error); }
.progress-check-icon .spinner {
width: 16px; height: 16px;
border: 2px solid var(--border);
border-top-color: var(--accent);
border-radius: 50%;
animation: spin 0.8s linear infinite;
}
@keyframes spin { to { transform: rotate(360deg); } }
.progress-check-label { font-size: 13px; color: var(--text-secondary); flex: 1; }
.progress-check-item.active .progress-check-label { color: var(--text-primary); font-weight: 500; }
.progress-check-detail { font-size: 11px; color: var(--text-disabled); }
.progress-complete-summary {
margin-top: 12px;
padding: 12px;
background: rgba(34,197,94,0.08);
border-radius: 6px;
font-size: 13px;
color: var(--success);
line-height: 1.5;
}
.progress-complete-summary .total-time {
display: block; margin-top: 6px;
font-family: var(--font-mono, 'Courier New', monospace);
font-size: 12px; color: var(--text-secondary);
}
.progress-popup-footer {
padding: 10px 20px 16px;
display: flex; justify-content: center;
}
.progress-cancel-btn {
background: none; border: none;
color: var(--text-disabled); font-size: 12px;
cursor: pointer; text-decoration: underline;
padding: 4px 8px; transition: color 0.2s;
}
.progress-cancel-btn:hover { color: var(--error); }
/* === Mini Progress Bar === */
.progress-mini {
background: var(--bg-primary);
border: 1px solid var(--border);
border-radius: var(--radius);
padding: var(--sp-xl);
padding: 10px 16px;
margin-bottom: var(--sp-xl);
position: relative;
display: flex; align-items: center; gap: 10px;
cursor: pointer;
transition: border-color 0.2s, background 0.2s;
}
.progress-steps {
display: flex;
justify-content: space-between;
margin-bottom: var(--sp-lg);
}
.progress-step {
display: flex;
align-items: center;
gap: var(--sp-md);
font-size: 12px;
color: var(--text-disabled);
transition: color 0.3s ease;
}
.progress-step.active {
color: var(--accent);
font-weight: 600;
}
.progress-step.done {
color: var(--success);
}
.progress-step-dot {
width: 10px;
height: 10px;
border-radius: 50%;
background: var(--text-disabled);
transition: all 0.3s ease;
.progress-mini:hover { border-color: var(--accent); background: var(--bg-secondary); }
.progress-mini-dot {
width: 8px; height: 8px; border-radius: 50%;
background: var(--accent);
animation: pulse 1.5s ease-in-out infinite;
flex-shrink: 0;
}
.progress-step.active .progress-step-dot {
background: var(--accent);
box-shadow: var(--glow-accent);
animation: pulse 1.5s ease-in-out infinite;
.progress-mini-text { font-size: 12px; color: var(--text-secondary); flex: 1; }
.progress-mini-timer {
font-family: var(--font-mono, 'Courier New', monospace);
font-size: 12px; color: var(--accent); font-weight: 600;
}
.progress-step.done .progress-step-dot {
background: var(--success);
/* === Blur for First Refresh === */
.grid-stack.blurred .grid-stack-item-content {
filter: blur(8px);
pointer-events: none;
user-select: none;
transition: filter 0.4s ease;
}
.progress-track {
height: 4px;
background: var(--bg-secondary);
border-radius: 2px;
overflow: hidden;
margin-bottom: var(--sp-md);
}
.progress-fill {
height: 100%;
background: linear-gradient(90deg, var(--accent), var(--success));
border-radius: 2px;
transition: width 0.5s ease-out;
width: 0%;
}
.progress-label-container {
display: flex;
justify-content: center;
align-items: center;
gap: var(--sp-md);
/* === Sidebar Refreshing Indicator === */
.incident-item.refreshing-item {
border: 1px solid transparent;
background-size: 300% 300%;
animation: sidebarRefreshBorder 3s ease infinite;
border-image: linear-gradient(135deg, var(--accent), transparent, var(--accent)) 1;
border-radius: var(--radius);
position: relative;
}
.progress-label {
font-size: 12px;
color: var(--text-secondary);
}
.progress-timer {
font-family: var(--font-mono, 'Courier New', monospace);
color: var(--text-disabled);
font-size: 12px;
}
.progress-pass-info {
font-size: 11px;
color: var(--accent-primary);
margin-left: 8px;
font-weight: 600;
letter-spacing: 0.3px;
}
.progress-cancel-btn {
.incident-item.refreshing-item::after {
content: '';
position: absolute;
right: var(--sp-xl);
bottom: var(--sp-lg);
background: none;
border: none;
color: var(--text-disabled);
font-size: 11px;
cursor: pointer;
text-decoration: underline;
padding: 2px 4px;
transition: color 0.2s ease;
inset: -1px;
border-radius: var(--radius);
border: 1px solid var(--accent);
opacity: 0.3;
animation: sidebarGlow 2s ease-in-out infinite;
pointer-events: none;
}
.progress-cancel-btn:hover {
color: var(--error);
@keyframes sidebarGlow {
0%, 100% { opacity: 0.15; box-shadow: 0 0 4px var(--accent); }
50% { opacity: 0.4; box-shadow: 0 0 12px var(--accent); }
}
.progress-bar--complete .progress-cancel-btn,
.progress-bar--error .progress-cancel-btn {
display: none;
.incident-refresh-status {
font-size: 10px;
color: var(--accent);
margin-top: 2px;
display: flex;
align-items: center;
gap: 4px;
animation: fadeIn 0.3s ease;
}
.progress-bar--complete .progress-fill {
background: linear-gradient(90deg, var(--success), #34D399);
width: 100% !important;
}
.progress-bar--complete .progress-label {
color: var(--success);
font-weight: 600;
}
.progress-bar--error .progress-fill {
background: linear-gradient(90deg, var(--error), #F87171);
}
.progress-bar--error .progress-label {
color: var(--error);
.incident-refresh-status .mini-spinner {
width: 10px; height: 10px;
border: 1.5px solid var(--border);
border-top-color: var(--accent);
border-radius: 50%;
animation: spin 0.8s linear infinite;
flex-shrink: 0;
}
@keyframes fadeIn { from { opacity: 0; } to { opacity: 1; } }
/* === Briefing === */
.briefing-content {

Datei anzeigen

@@ -183,31 +183,11 @@
</div>
</div>
<!-- Fortschrittsanzeige -->
<div class="progress-bar" id="progress-bar" role="progressbar" aria-valuenow="0" aria-valuemin="0" aria-valuemax="100" aria-label="Verarbeitungsfortschritt" style="display:none;">
<div class="progress-steps">
<div class="progress-step" id="step-researching">
<div class="progress-step-dot"></div>
<span>Recherche</span>
</div>
<div class="progress-step" id="step-analyzing">
<div class="progress-step-dot"></div>
<span>Analyse</span>
</div>
<div class="progress-step" id="step-factchecking">
<div class="progress-step-dot"></div>
<span>Faktencheck</span>
</div>
</div>
<div class="progress-track">
<div class="progress-fill" id="progress-fill"></div>
</div>
<div class="progress-label-container">
<span id="progress-label" class="progress-label">Warte auf Start...</span>
<span id="progress-pass-info" class="progress-pass-info" style="display:none;"></span>
<span id="progress-timer" class="progress-timer"></span>
</div>
<button id="progress-cancel-btn" class="progress-cancel-btn" onclick="App.cancelRefresh()">Abbrechen</button>
<!-- Minimierte Fortschrittsanzeige -->
<div class="progress-mini" id="progress-mini" style="display:none;" onclick="App.openProgressPopup()">
<span class="progress-mini-dot"></span>
<span class="progress-mini-text" id="progress-mini-text">Läuft...</span>
<span class="progress-mini-timer" id="progress-mini-timer"></span>
</div>
<!-- Layout-Toolbar -->
@@ -706,5 +686,46 @@
</div>
</div>
<!-- Fortschritts-Popup -->
<div class="progress-overlay" id="progress-overlay" style="display:none;">
<div class="progress-popup" id="progress-popup">
<div class="progress-popup-header">
<span class="progress-popup-title" id="progress-popup-title">Aktualisierung läuft</span>
<span class="progress-popup-timer" id="progress-popup-timer"></span>
<button class="progress-popup-minimize" id="progress-popup-minimize" style="display:none;" onclick="App.minimizeProgress()" title="Minimieren">&minus;</button>
</div>
<div class="progress-popup-body">
<div class="progress-popup-pass" id="progress-popup-pass" style="display:none;"></div>
<div class="progress-checklist" id="progress-checklist">
<div class="progress-check-item" data-step="queued">
<span class="progress-check-icon"></span>
<span class="progress-check-label">In Warteschlange</span>
<span class="progress-check-detail"></span>
</div>
<div class="progress-check-item" data-step="researching">
<span class="progress-check-icon"></span>
<span class="progress-check-label">Quellen werden durchsucht</span>
<span class="progress-check-detail"></span>
</div>
<div class="progress-check-item" data-step="analyzing">
<span class="progress-check-icon"></span>
<span class="progress-check-label">Meldungen werden analysiert</span>
<span class="progress-check-detail"></span>
</div>
<div class="progress-check-item" data-step="factchecking">
<span class="progress-check-icon"></span>
<span class="progress-check-label">Faktencheck läuft</span>
<span class="progress-check-detail"></span>
</div>
</div>
<div class="progress-complete-summary" id="progress-complete-summary" style="display:none;"></div>
</div>
<div class="progress-popup-footer">
<button class="progress-cancel-btn" id="progress-cancel-btn" onclick="App.cancelRefresh()">Abbrechen</button>
</div>
</div>
</div>
</body>
</html>

Datei anzeigen

@@ -699,10 +699,18 @@ const App = {
// Refresh-Status fuer diese Lage wiederherstellen
const isRefreshing = this._refreshingIncidents.has(id);
this._updateRefreshButton(isRefreshing);
// Hide any popup from previous incident
const prevOverlay = document.getElementById('progress-overlay');
if (prevOverlay) prevOverlay.style.display = 'none';
const prevMini = document.getElementById('progress-mini');
if (prevMini) prevMini.style.display = 'none';
const grid = document.querySelector('.grid-stack');
if (grid) grid.classList.remove('blurred');
if (isRefreshing) {
UI.showProgress('researching');
} else {
UI.hideProgress();
const state = UI._progressState[id];
const step = state ? state.step : 'researching';
const isFirst = state ? state.isFirst : false;
UI.showProgress(step, {}, id, isFirst);
}
// Alte Inhalte sofort leeren um Flackern beim Wechsel zu vermeiden
@@ -1616,7 +1624,7 @@ const App = {
// Sofort ersten Refresh starten
this._refreshingIncidents.add(incident.id);
this._updateRefreshButton(true);
UI.showProgress('queued');
// showProgress called via handleStatusUpdate
await API.refreshIncident(incident.id);
UI.showToast(`Lage "${incident.title}" angelegt. Recherche gestartet.`, 'success');
}
@@ -1676,12 +1684,14 @@ async handleRefresh() {
try {
this._refreshingIncidents.add(this.currentIncidentId);
this._updateRefreshButton(true);
UI.showProgress('queued');
// showProgress called via handleStatusUpdate
const result = await API.refreshIncident(this.currentIncidentId);
if (result && result.status === 'skipped') {
UI.showToast('Aktualisierung ist in der Warteschlange und wird ausgefuehrt, sobald die aktuelle Recherche abgeschlossen ist.', 'info');
} else {
UI.showToast('Aktualisierung gestartet.', 'success');
var _inc2 = this.incidents.find(function(i) { return i.id === this.currentIncidentId; }.bind(this));
UI.showProgress('queued', {}, this.currentIncidentId, _inc2 && !_inc2.summary);
}
} catch (err) {
this._refreshingIncidents.delete(this.currentIncidentId);
@@ -2013,9 +2023,8 @@ async handleRefresh() {
handleStatusUpdate(msg) {
const status = msg.data.status;
if (status === 'retrying') {
// Retry-Status → Fehleranzeige mit Retry-Info
if (msg.incident_id === this.currentIncidentId) {
UI.showProgressError('', true, msg.data.delay || 120);
UI.showProgressError('', true, msg.data.delay || 120, msg.incident_id);
}
return;
}
@@ -2023,8 +2032,11 @@ async handleRefresh() {
this._refreshingIncidents.add(msg.incident_id);
}
this._updateSidebarDot(msg.incident_id);
// Detect first refresh: no summary means first run
const inc = this.incidents.find(i => i.id === msg.incident_id);
const isFirst = inc && !inc.summary;
UI.showProgress(status, msg.data, msg.incident_id, isFirst);
if (msg.incident_id === this.currentIncidentId) {
UI.showProgress(status, msg.data);
this._updateRefreshButton(status !== 'idle');
}
},
@@ -2037,14 +2049,13 @@ async handleRefresh() {
this._updateRefreshButton(false);
await this.loadIncidentDetail(msg.incident_id);
// Progress-Bar nicht sofort ausblenden — auf refresh_summary warten
// Progress-Popup nicht sofort ausblenden — auf refresh_summary warten
this._pendingComplete = msg.incident_id;
// Fallback: Wenn nach 5s kein refresh_summary kommt → direkt ausblenden
if (this._pendingCompleteTimer) clearTimeout(this._pendingCompleteTimer);
this._pendingCompleteTimer = setTimeout(() => {
if (this._pendingComplete === msg.incident_id) {
this._pendingComplete = null;
UI.hideProgress();
UI.hideProgress(msg.incident_id);
}
}, 5000);
}
@@ -2065,8 +2076,7 @@ async handleRefresh() {
this._pendingCompleteTimer = null;
}
this._pendingComplete = null;
UI.showProgressComplete(d);
setTimeout(() => UI.hideProgress(), 4000);
UI.showProgressComplete(d, msg.incident_id);
}
// Toast-Text zusammenbauen
@@ -2145,7 +2155,7 @@ async handleRefresh() {
this._pendingCompleteTimer = null;
}
this._pendingComplete = null;
UI.showProgressError(msg.data.error, false);
UI.showProgressError(msg.data.error, false, 0, msg.incident_id);
}
UI.showToast(`Recherche-Fehler: ${msg.data.error}`, 'error');
},
@@ -2160,11 +2170,19 @@ async handleRefresh() {
this._pendingCompleteTimer = null;
}
this._pendingComplete = null;
UI.hideProgress();
UI.hideProgress(msg.incident_id);
}
UI.showToast('Recherche abgebrochen.', 'info');
},
minimizeProgress() {
UI.minimizeProgress(this.currentIncidentId);
},
openProgressPopup() {
UI.openProgressPopup(this.currentIncidentId);
},
async cancelRefresh() {
if (!this.currentIncidentId) return;
const ok = await confirmDialog('Laufende Recherche abbrechen?');

Datei anzeigen

@@ -232,205 +232,363 @@ const UI = {
/**
* Fortschrittsanzeige einblenden und Status setzen.
*/
showProgress(status, extra = {}) {
const bar = document.getElementById('progress-bar');
if (!bar) return;
bar.style.display = 'block';
bar.classList.remove('progress-bar--complete', 'progress-bar--error');
// === Progress State (per-incident) ===
_progressState: {}, // { incidentId: { step, isFirst, startTime, minimized } }
_progressTimerInterval: null,
const steps = {
queued: { active: 0, label: 'In Warteschlange...' },
researching: { active: 1, label: 'Recherchiert Quellen...' },
deep_researching: { active: 1, label: 'Tiefenrecherche läuft...' },
analyzing: { active: 2, label: 'Analysiert Meldungen...' },
factchecking: { active: 3, label: 'Faktencheck läuft...' },
cancelling: { active: 0, label: 'Wird abgebrochen...' },
_getStepOrder() {
return ['queued', 'researching', 'deep_researching', 'analyzing', 'factchecking'];
},
_getStepLabel(step) {
const map = {
queued: 'In Warteschlange',
researching: 'Recherchiert...',
deep_researching: 'Tiefenrecherche...',
analyzing: 'Analysiert...',
factchecking: 'Faktencheck...',
cancelling: 'Wird abgebrochen...',
};
return map[step] || step;
},
const step = steps[status] || steps.queued;
showProgress(status, extra = {}, incidentId = null, isFirstRefresh = false) {
if (!incidentId) incidentId = App.currentIncidentId;
if (!incidentId) return;
// Queue-Position anzeigen
let labelText = step.label;
if (status === 'queued' && extra.queue_position > 1) {
labelText = `In Warteschlange (Position ${extra.queue_position})...`;
} else if (extra.detail) {
labelText = extra.detail;
// Init state for this incident
if (!this._progressState[incidentId]) {
this._progressState[incidentId] = { step: 'queued', isFirst: isFirstRefresh, startTime: null, minimized: false };
}
const state = this._progressState[incidentId];
state.step = status;
if (isFirstRefresh) state.isFirst = true;
// Start timer on first non-queued status
if (status !== 'queued' && !state.startTime) {
if (extra.started_at) {
const serverStart = typeof parseUTC === 'function' ? parseUTC(extra.started_at) : new Date(extra.started_at);
state.startTime = serverStart ? serverStart.getTime() : Date.now();
} else {
state.startTime = Date.now();
}
}
// Multi-Pass: Durchlauf-Info anzeigen
const passEl = document.getElementById('progress-pass-info');
// Start global timer interval if not running
if (!this._progressTimerInterval) {
this._progressTimerInterval = setInterval(() => this._tickProgressTimers(), 1000);
}
// Only show UI for current incident
if (incidentId !== App.currentIncidentId) return;
// Update sidebar status text
this._updateSidebarRefreshStatus(incidentId, status, extra);
if (state.minimized) {
this._showMiniProgress(status, state);
return;
}
this._showPopupProgress(status, extra, state);
},
_showPopupProgress(status, extra, state) {
const overlay = document.getElementById('progress-overlay');
const popup = document.getElementById('progress-popup');
if (!overlay || !popup) return;
overlay.style.display = 'flex';
// Blocking (no close) for first refresh
if (state.isFirst) {
overlay.classList.add('blocking');
// Apply blur to grid
const grid = document.querySelector('.grid-stack');
if (grid) grid.classList.add('blurred');
} else {
overlay.classList.remove('blocking');
}
// Minimize button: only for updates (not first)
const minBtn = document.getElementById('progress-popup-minimize');
if (minBtn) minBtn.style.display = state.isFirst ? 'none' : '';
// Title
const titleEl = document.getElementById('progress-popup-title');
if (titleEl) titleEl.textContent = state.isFirst ? 'Erste Recherche l\u00e4uft' : 'Aktualisierung l\u00e4uft';
// Multi-pass info
const passEl = document.getElementById('progress-popup-pass');
if (passEl) {
if (extra.research_pass && extra.research_total_passes) {
passEl.textContent = `Durchlauf ${extra.research_pass}/${extra.research_total_passes}`;
passEl.textContent = 'Durchlauf ' + extra.research_pass + '/' + extra.research_total_passes;
passEl.style.display = '';
} else {
passEl.style.display = 'none';
}
}
// Timer starten beim Übergang von queued zu aktivem Status
if (step.active > 0 && !this._progressStartTime) {
if (extra.started_at) {
// Echte Startzeit vom Server verwenden
const serverStart = parseUTC(extra.started_at);
this._progressStartTime = serverStart ? serverStart.getTime() : Date.now();
// Update checklist
const stepOrder = this._getStepOrder();
const currentIdx = stepOrder.indexOf(status === 'deep_researching' ? 'researching' : status);
const items = document.querySelectorAll('.progress-check-item');
// Map checklist items to step indices: queued=0, researching=1, analyzing=3, factchecking=4
const checkStepMap = { queued: 0, researching: 1, analyzing: 3, factchecking: 4 };
items.forEach(item => {
const step = item.dataset.step;
const stepIdx = checkStepMap[step] !== undefined ? checkStepMap[step] : -1;
const icon = item.querySelector('.progress-check-icon');
const detail = item.querySelector('.progress-check-detail');
item.classList.remove('active', 'done', 'error');
if (stepIdx < currentIdx || (step === 'queued' && currentIdx > 0)) {
item.classList.add('done');
if (icon) icon.innerHTML = '\u2713';
} else if (stepIdx === currentIdx || (step === 'researching' && (status === 'researching' || status === 'deep_researching'))) {
item.classList.add('active');
if (icon) icon.innerHTML = '<div class="spinner"></div>';
if (detail && extra.detail) detail.textContent = extra.detail;
else if (detail) detail.textContent = '';
} else {
this._progressStartTime = Date.now();
if (icon) icon.innerHTML = '\u25cb';
if (detail) detail.textContent = '';
}
this._startProgressTimer();
}
const stepIds = ['step-researching', 'step-analyzing', 'step-factchecking'];
stepIds.forEach((id, i) => {
const el = document.getElementById(id);
if (!el) return;
el.className = 'progress-step';
if (i + 1 < step.active) el.classList.add('done');
else if (i + 1 === step.active) el.classList.add('active');
});
const fill = document.getElementById('progress-fill');
const percent = step.active === 0 ? 5 : Math.round((step.active / 3) * 100);
if (fill) {
fill.style.width = percent + '%';
// Cancel button
const cancelBtn = document.getElementById('progress-cancel-btn');
if (cancelBtn) {
cancelBtn.style.display = '';
cancelBtn.textContent = 'Abbrechen';
cancelBtn.disabled = false;
}
// ARIA-Werte auf der Progressbar aktualisieren
bar.setAttribute('aria-valuenow', String(percent));
bar.setAttribute('aria-valuetext', labelText);
// Hide complete summary
const summaryEl = document.getElementById('progress-complete-summary');
if (summaryEl) summaryEl.style.display = 'none';
const label = document.getElementById('progress-label');
if (label) label.textContent = labelText;
// Cancel-Button sichtbar machen
const cancelBtn = document.getElementById('progress-cancel-btn');
if (cancelBtn) cancelBtn.style.display = '';
// Hide mini bar
const mini = document.getElementById('progress-mini');
if (mini) mini.style.display = 'none';
},
/**
* Timer-Intervall starten (1x pro Sekunde).
*/
_startProgressTimer() {
if (this._progressTimer) return;
const timerEl = document.getElementById('progress-timer');
if (!timerEl) return;
_showMiniProgress(status, state) {
const mini = document.getElementById('progress-mini');
if (!mini) return;
mini.style.display = 'flex';
this._progressTimer = setInterval(() => {
if (!this._progressStartTime) return;
const elapsed = Math.max(0, Math.floor((Date.now() - this._progressStartTime) / 1000));
const textEl = document.getElementById('progress-mini-text');
if (textEl) textEl.textContent = this._getStepLabel(status);
// Hide popup
const overlay = document.getElementById('progress-overlay');
if (overlay) overlay.style.display = 'none';
},
minimizeProgress(incidentId) {
if (!incidentId) incidentId = App.currentIncidentId;
const state = this._progressState[incidentId];
if (!state) return;
state.minimized = true;
this._showMiniProgress(state.step, state);
},
openProgressPopup(incidentId) {
if (!incidentId) incidentId = App.currentIncidentId;
const state = this._progressState[incidentId];
if (!state) return;
state.minimized = false;
this._showPopupProgress(state.step, {}, state);
},
showProgressComplete(data, incidentId) {
if (!incidentId) incidentId = App.currentIncidentId;
const state = this._progressState[incidentId];
// Calculate total time
let totalTimeStr = '';
if (state && state.startTime) {
const elapsed = Math.floor((Date.now() - state.startTime) / 1000);
const mins = Math.floor(elapsed / 60);
const secs = elapsed % 60;
timerEl.textContent = `${mins}:${String(secs).padStart(2, '0')}`;
}, 1000);
},
totalTimeStr = mins + ':' + String(secs).padStart(2, '0');
}
/**
* Abschluss-Animation: Grüner Balken mit Summary-Text.
*/
showProgressComplete(data) {
const bar = document.getElementById('progress-bar');
if (!bar) return;
if (incidentId === App.currentIncidentId) {
// Remove blur
const grid = document.querySelector('.grid-stack');
if (grid) grid.classList.remove('blurred');
// Timer stoppen
this._stopProgressTimer();
const overlay = document.getElementById('progress-overlay');
if (overlay) {
overlay.style.display = 'flex';
overlay.classList.remove('blocking');
}
// Alle Steps auf done
['step-researching', 'step-analyzing', 'step-factchecking'].forEach(id => {
const el = document.getElementById(id);
if (el) { el.className = 'progress-step done'; }
// Mark all steps done
document.querySelectorAll('.progress-check-item').forEach(item => {
item.classList.remove('active', 'error');
item.classList.add('done');
const icon = item.querySelector('.progress-check-icon');
if (icon) icon.innerHTML = '\u2713';
});
// Fill auf 100%
const fill = document.getElementById('progress-fill');
if (fill) fill.style.width = '100%';
// Complete-Klasse
bar.classList.remove('progress-bar--error');
bar.classList.add('progress-bar--complete');
// Label mit Summary
// Show summary
const parts = [];
if (data.new_articles > 0) {
parts.push(`${data.new_articles} neue Artikel`);
}
if (data.confirmed_count > 0) {
parts.push(`${data.confirmed_count} Fakten bestätigt`);
}
if (data.contradicted_count > 0) {
parts.push(`${data.contradicted_count} widerlegt`);
}
if (data.new_articles > 0) parts.push(data.new_articles + ' neue Artikel');
if (data.confirmed_count > 0) parts.push(data.confirmed_count + ' Fakten best\u00e4tigt');
if (data.contradicted_count > 0) parts.push(data.contradicted_count + ' widerlegt');
const summaryText = parts.length > 0 ? parts.join(', ') : 'Keine neuen Entwicklungen';
const label = document.getElementById('progress-label');
if (label) label.textContent = `Abgeschlossen: ${summaryText}`;
// Cancel-Button und Pass-Info ausblenden
const cancelBtn = document.getElementById('progress-cancel-btn');
if (cancelBtn) cancelBtn.style.display = 'none';
const passElDone = document.getElementById('progress-pass-info');
if (passElDone) passElDone.style.display = 'none';
bar.setAttribute('aria-valuenow', '100');
bar.setAttribute('aria-valuetext', 'Abgeschlossen');
},
/**
* Fehler-Zustand: Roter Balken mit Fehlermeldung.
*/
showProgressError(errorMsg, willRetry = false, delay = 0) {
const bar = document.getElementById('progress-bar');
if (!bar) return;
bar.style.display = 'block';
// Timer stoppen
this._stopProgressTimer();
// Error-Klasse
bar.classList.remove('progress-bar--complete');
bar.classList.add('progress-bar--error');
const label = document.getElementById('progress-label');
if (label) {
label.textContent = willRetry
? `Fehlgeschlagen \u2014 erneuter Versuch in ${delay}s...`
: `Fehlgeschlagen: ${errorMsg}`;
const summaryEl = document.getElementById('progress-complete-summary');
if (summaryEl) {
summaryEl.innerHTML = '\u2713 Abgeschlossen: ' + summaryText
+ (totalTimeStr ? '<span class="total-time">Gesamtzeit: ' + totalTimeStr + '</span>' : '');
summaryEl.style.display = 'block';
}
// Update title
const titleEl = document.getElementById('progress-popup-title');
if (titleEl) titleEl.textContent = 'Abgeschlossen';
// Hide cancel, show minimize
const cancelBtn = document.getElementById('progress-cancel-btn');
if (cancelBtn) cancelBtn.style.display = 'none';
const minBtn = document.getElementById('progress-popup-minimize');
if (minBtn) minBtn.style.display = '';
// Hide mini bar
const mini = document.getElementById('progress-mini');
if (mini) mini.style.display = 'none';
}
// Remove sidebar refresh status
this._removeSidebarRefreshStatus(incidentId);
// Clean up state after delay
setTimeout(() => {
this.hideProgress(incidentId);
}, 5000);
},
showProgressError(errorMsg, willRetry = false, delay = 0, incidentId = null) {
if (!incidentId) incidentId = App.currentIncidentId;
if (incidentId !== App.currentIncidentId) return;
const overlay = document.getElementById('progress-overlay');
if (overlay) overlay.style.display = 'flex';
// Mark current step as error
const state = this._progressState[incidentId];
if (state) {
const items = document.querySelectorAll('.progress-check-item.active');
items.forEach(item => {
item.classList.remove('active');
item.classList.add('error');
const icon = item.querySelector('.progress-check-icon');
if (icon) icon.innerHTML = '\u2717';
});
}
const titleEl = document.getElementById('progress-popup-title');
if (titleEl) {
titleEl.textContent = willRetry
? 'Fehlgeschlagen \u2014 erneuter Versuch in ' + delay + 's...'
: 'Fehlgeschlagen: ' + errorMsg;
}
// Cancel-Button ausblenden
const cancelBtn = document.getElementById('progress-cancel-btn');
if (cancelBtn) cancelBtn.style.display = 'none';
// Bei finalem Fehler nach 6s ausblenden
if (!willRetry) {
setTimeout(() => this.hideProgress(), 6000);
this._removeSidebarRefreshStatus(incidentId);
setTimeout(() => this.hideProgress(incidentId), 6000);
}
},
/**
* Timer-Intervall stoppen und zurücksetzen.
*/
_stopProgressTimer() {
if (this._progressTimer) {
clearInterval(this._progressTimer);
this._progressTimer = null;
hideProgress(incidentId) {
if (!incidentId) incidentId = App.currentIncidentId;
// Remove blur
const grid = document.querySelector('.grid-stack');
if (grid) grid.classList.remove('blurred');
if (incidentId === App.currentIncidentId) {
const overlay = document.getElementById('progress-overlay');
if (overlay) { overlay.style.display = 'none'; overlay.classList.remove('blocking'); }
const mini = document.getElementById('progress-mini');
if (mini) mini.style.display = 'none';
}
// Remove sidebar status
this._removeSidebarRefreshStatus(incidentId);
// Clean up state
delete this._progressState[incidentId];
// Stop timer if no more active refreshes
if (Object.keys(this._progressState).length === 0 && this._progressTimerInterval) {
clearInterval(this._progressTimerInterval);
this._progressTimerInterval = null;
}
this._progressStartTime = null;
const timerEl = document.getElementById('progress-timer');
if (timerEl) timerEl.textContent = '';
},
/**
* Fortschrittsanzeige ausblenden.
*/
hideProgress() {
const bar = document.getElementById('progress-bar');
if (bar) {
bar.style.display = 'none';
bar.classList.remove('progress-bar--complete', 'progress-bar--error');
_tickProgressTimers() {
for (const [id, state] of Object.entries(this._progressState)) {
if (!state.startTime) continue;
const elapsed = Math.max(0, Math.floor((Date.now() - state.startTime) / 1000));
const mins = Math.floor(elapsed / 60);
const secs = elapsed % 60;
const timeStr = mins + ':' + String(secs).padStart(2, '0');
if (parseInt(id) === App.currentIncidentId) {
// Update popup timer
const timerEl = document.getElementById('progress-popup-timer');
if (timerEl) timerEl.textContent = timeStr;
// Update mini timer
const miniTimer = document.getElementById('progress-mini-timer');
if (miniTimer) miniTimer.textContent = timeStr;
}
// Update sidebar timer for this incident
const sidebarTimer = document.getElementById('sidebar-refresh-timer-' + id);
if (sidebarTimer) sidebarTimer.textContent = timeStr;
}
this._stopProgressTimer();
},
// === Sidebar Refresh Status ===
_updateSidebarRefreshStatus(incidentId, status, extra) {
const item = document.querySelector('.incident-item[data-id="' + incidentId + '"]');
if (!item) return;
// Add refreshing class for animated border
item.classList.add('refreshing-item');
// Add or update status text below meta
let statusEl = document.getElementById('sidebar-refresh-' + incidentId);
if (!statusEl) {
const textCol = item.querySelector('div[style*="flex:1"]');
if (!textCol) return;
statusEl = document.createElement('div');
statusEl.id = 'sidebar-refresh-' + incidentId;
statusEl.className = 'incident-refresh-status';
textCol.appendChild(statusEl);
}
const label = this._getStepLabel(status);
statusEl.innerHTML = '<span class="mini-spinner"></span><span>' + label + '</span><span id="sidebar-refresh-timer-' + incidentId + '" style="margin-left:auto;font-family:var(--font-mono,monospace);font-size:10px;color:var(--text-disabled);"></span>';
},
_removeSidebarRefreshStatus(incidentId) {
const statusEl = document.getElementById('sidebar-refresh-' + incidentId);
if (statusEl) statusEl.remove();
const item = document.querySelector('.incident-item[data-id="' + incidentId + '"]');
if (item) item.classList.remove('refreshing-item');
},
/**
* Zusammenfassung mit Inline-Zitaten und Quellenverzeichnis rendern.
*/