From 3f88d00b8c836a3d007ba68a15c9e4de647746f4 Mon Sep 17 00:00:00 2001 From: Claude Dev Date: Thu, 9 Apr 2026 20:08:59 +0200 Subject: [PATCH] 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) --- src/agents/orchestrator.py | 3 + src/static/css/style.css | 311 +++++++++++++++--------- src/static/dashboard.html | 71 ++++-- src/static/js/app.js | 48 ++-- src/static/js/components.js | 466 ++++++++++++++++++++++++------------ 5 files changed, 588 insertions(+), 311 deletions(-) diff --git a/src/agents/orchestrator.py b/src/agents/orchestrator.py index cc968e1..b9839e1 100644 --- a/src/agents/orchestrator.py +++ b/src/agents/orchestrator.py @@ -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) diff --git a/src/static/css/style.css b/src/static/css/style.css index 0d4469c..60db7d7 100644 --- a/src/static/css/style.css +++ b/src/static/css/style.css @@ -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 { diff --git a/src/static/dashboard.html b/src/static/dashboard.html index 3b246a6..8d6af31 100644 --- a/src/static/dashboard.html +++ b/src/static/dashboard.html @@ -183,31 +183,11 @@ - - + + + + diff --git a/src/static/js/app.js b/src/static/js/app.js index 510c23d..601880c 100644 --- a/src/static/js/app.js +++ b/src/static/js/app.js @@ -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?'); diff --git a/src/static/js/components.js b/src/static/js/components.js index 2383553..3e477fd 100644 --- a/src/static/js/components.js +++ b/src/static/js/components.js @@ -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 = '
'; + 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'); + } + + if (incidentId === App.currentIncidentId) { + // Remove blur + const grid = document.querySelector('.grid-stack'); + if (grid) grid.classList.remove('blurred'); + + const overlay = document.getElementById('progress-overlay'); + if (overlay) { + overlay.style.display = 'flex'; + overlay.classList.remove('blocking'); + } + + // 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'; + }); + + // 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\u00e4tigt'); + if (data.contradicted_count > 0) parts.push(data.contradicted_count + ' widerlegt'); + const summaryText = parts.length > 0 ? parts.join(', ') : 'Keine neuen Entwicklungen'; + + const summaryEl = document.getElementById('progress-complete-summary'); + if (summaryEl) { + summaryEl.innerHTML = '\u2713 Abgeschlossen: ' + summaryText + + (totalTimeStr ? 'Gesamtzeit: ' + totalTimeStr + '' : ''); + 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); }, - /** - * Abschluss-Animation: Grüner Balken mit Summary-Text. - */ - showProgressComplete(data) { - const bar = document.getElementById('progress-bar'); - if (!bar) return; + showProgressError(errorMsg, willRetry = false, delay = 0, incidentId = null) { + if (!incidentId) incidentId = App.currentIncidentId; + if (incidentId !== App.currentIncidentId) return; - // Timer stoppen - this._stopProgressTimer(); + const overlay = document.getElementById('progress-overlay'); + if (overlay) overlay.style.display = 'flex'; - // Alle Steps auf done - ['step-researching', 'step-analyzing', 'step-factchecking'].forEach(id => { - const el = document.getElementById(id); - if (el) { el.className = 'progress-step done'; } - }); - - // 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 - 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`); - } - 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}`; + // 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 = '' + label + ''; + }, + + _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. */