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:
@@ -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?');
|
||||
|
||||
@@ -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');
|
||||
}
|
||||
|
||||
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 ? '<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);
|
||||
},
|
||||
|
||||
/**
|
||||
* 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 = '<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.
|
||||
*/
|
||||
|
||||
In neuem Issue referenzieren
Einen Benutzer sperren