fix: Lagen im Netzwerk-Modal sortiert — Live alphabetisch, dann Analyse alphabetisch

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Dieser Commit ist enthalten in:
Claude Dev
2026-03-16 00:42:56 +01:00
Ursprung 432147de4b
Commit 5018dddad5

Datei anzeigen

@@ -1,447 +1,454 @@
/**
* Netzwerkanalyse-Erweiterungen für App-Objekt.
* Wird nach app.js geladen und erweitert App um Netzwerk-Funktionalität.
*/
// State-Erweiterung
App.networkAnalyses = [];
App.currentNetworkId = null;
App._networkGenerating = new Set();
/**
* Netzwerkanalysen laden und Sidebar rendern.
*/
App.loadNetworkAnalyses = async function() {
try {
this.networkAnalyses = await API.listNetworkAnalyses();
} catch (e) {
console.warn('Netzwerkanalysen laden fehlgeschlagen:', e);
this.networkAnalyses = [];
}
this.renderNetworkSidebar();
};
/**
* Netzwerkanalysen-Sektion in der Sidebar rendern.
*/
App.renderNetworkSidebar = function() {
var container = document.getElementById('network-analyses-list');
if (!container) return;
var countEl = document.getElementById('count-network-analyses');
if (countEl) countEl.textContent = '(' + this.networkAnalyses.length + ')';
if (this.networkAnalyses.length === 0) {
container.innerHTML = '<div style="padding:8px 12px;font-size:12px;color:var(--text-tertiary);">Keine Netzwerkanalysen</div>';
return;
}
var self = this;
container.innerHTML = this.networkAnalyses.map(function(na) {
var isActive = na.id === self.currentNetworkId;
var statusClass = na.status === 'generating' ? 'generating' : (na.status === 'error' ? 'error' : 'ready');
var countText = na.status === 'ready' ? (na.entity_count + ' / ' + na.relation_count) : na.status === 'generating' ? '...' : '';
return '<div class="sidebar-network-item' + (isActive ? ' active' : '') + '" onclick="App.selectNetworkAnalysis(' + na.id + ')">' +
'<svg class="network-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="5" r="3"/><circle cx="5" cy="19" r="3"/><circle cx="19" cy="19" r="3"/><line x1="12" y1="8" x2="5" y2="16"/><line x1="12" y1="8" x2="19" y2="16"/></svg>' +
'<span class="network-item-name" title="' + _escHtml(na.name) + '">' + _escHtml(na.name) + '</span>' +
'<span class="network-item-count">' + countText + '</span>' +
'<span class="network-status-dot ' + statusClass + '"></span>' +
'</div>';
}).join('');
};
/**
* Netzwerkanalyse auswählen und anzeigen.
*/
App.selectNetworkAnalysis = async function(id) {
this.currentNetworkId = id;
this.currentIncidentId = null;
localStorage.removeItem('selectedIncidentId');
localStorage.setItem('selectedNetworkId', id);
// Views umschalten
document.getElementById('empty-state').style.display = 'none';
document.getElementById('incident-view').style.display = 'none';
document.getElementById('network-view').style.display = 'flex';
// Sidebar aktualisieren
this.renderSidebar();
this.renderNetworkSidebar();
// Analyse laden
try {
var analysis = await API.getNetworkAnalysis(id);
this._renderNetworkHeader(analysis);
if (analysis.status === 'ready') {
this._hideNetworkProgress();
var graphData = await API.getNetworkGraph(id);
NetworkGraph.init('network-graph-area', graphData);
this._setupNetworkFilters(graphData);
// Update-Check
try {
var updateCheck = await API.checkNetworkUpdate(id);
var badge = document.getElementById('network-update-badge');
if (badge) badge.style.display = updateCheck.has_update ? 'inline-flex' : 'none';
} catch (e) { /* ignorieren */ }
} else if (analysis.status === 'generating') {
this._showNetworkProgress('entity_extraction', 0);
} else if (analysis.status === 'error') {
this._hideNetworkProgress();
var graphArea = document.getElementById('network-graph-area');
if (graphArea) graphArea.innerHTML = '<div class="network-empty-state"><div class="network-empty-state-icon">&#9888;</div><div class="network-empty-state-text">Fehler bei der Generierung. Versuche es erneut.</div></div>';
}
} catch (err) {
UI.showToast('Fehler beim Laden der Netzwerkanalyse: ' + err.message, 'error');
}
};
/**
* Netzwerkanalyse-Header rendern.
*/
App._renderNetworkHeader = function(analysis) {
var el;
el = document.getElementById('network-title');
if (el) el.textContent = analysis.name;
el = document.getElementById('network-entity-count');
if (el) el.textContent = analysis.entity_count + ' Entitäten';
el = document.getElementById('network-relation-count');
if (el) el.textContent = analysis.relation_count + ' Beziehungen';
el = document.getElementById('network-incident-list-text');
if (el) el.textContent = (analysis.incident_titles || []).join(', ') || '-';
el = document.getElementById('network-last-generated');
if (el) {
if (analysis.last_generated_at) {
var d = parseUTC(analysis.last_generated_at) || new Date(analysis.last_generated_at);
el.textContent = 'Generiert: ' + d.toLocaleDateString('de-DE', { day: '2-digit', month: '2-digit', year: 'numeric', timeZone: TIMEZONE }) + ' ' +
d.toLocaleTimeString('de-DE', { hour: '2-digit', minute: '2-digit', timeZone: TIMEZONE });
} else {
el.textContent = '';
}
}
};
/**
* Filter-Controls in der Netzwerk-Sidebar aufsetzen.
*/
App._setupNetworkFilters = function(graphData) {
// Typ-Filter-Buttons aktivieren
var types = new Set();
(graphData.entities || []).forEach(function(e) { types.add(e.entity_type); });
var filterContainer = document.getElementById('network-type-filter-container');
if (filterContainer) {
var allTypes = ['person', 'organisation', 'location', 'event', 'military'];
var typeLabels = { person: 'Person', organisation: 'Organisation', location: 'Ort', event: 'Ereignis', military: 'Militär' };
filterContainer.innerHTML = allTypes.map(function(t) {
var hasEntities = types.has(t);
return '<button class="network-type-filter active" data-type="' + t + '" onclick="App.toggleNetworkTypeFilter(this)" ' +
(hasEntities ? '' : 'disabled style="opacity:0.3"') + '>' +
'<span class="type-dot"></span><span>' + typeLabels[t] + '</span></button>';
}).join('');
}
// Gewicht-Slider
var slider = document.getElementById('network-weight-slider');
if (slider) {
slider.value = 1;
slider.oninput = function() {
var label = document.getElementById('network-weight-value');
if (label) label.textContent = this.value;
NetworkGraph.filterByWeight(parseInt(this.value));
};
}
// Suche
var searchInput = document.getElementById('network-search');
if (searchInput) {
searchInput.value = '';
var timer = null;
searchInput.oninput = function() {
clearTimeout(timer);
var val = this.value;
timer = setTimeout(function() {
NetworkGraph.search(val);
}, 250);
};
}
};
/**
* Typ-Filter toggle.
*/
App.toggleNetworkTypeFilter = function(btn) {
btn.classList.toggle('active');
var activeTypes = [];
document.querySelectorAll('.network-type-filter.active').forEach(function(b) {
activeTypes.push(b.dataset.type);
});
NetworkGraph.filterByType(new Set(activeTypes));
};
/**
* Progress-Bar anzeigen.
*/
App._showNetworkProgress = function(phase, progress) {
var bar = document.getElementById('network-progress-bar');
if (bar) bar.style.display = 'block';
var steps = ['entity_extraction', 'relationship_extraction', 'correction'];
var stepEls = document.querySelectorAll('.network-progress-step');
var connectorEls = document.querySelectorAll('.network-progress-connector');
var phaseIndex = steps.indexOf(phase);
stepEls.forEach(function(el, i) {
el.classList.remove('active', 'done');
if (i < phaseIndex) el.classList.add('done');
else if (i === phaseIndex) el.classList.add('active');
});
connectorEls.forEach(function(el, i) {
el.classList.remove('done');
if (i < phaseIndex) el.classList.add('done');
});
var fill = document.getElementById('network-progress-fill');
if (fill) {
var pct = ((phaseIndex / steps.length) * 100) + (progress || 0) * (100 / steps.length) / 100;
fill.style.width = Math.min(100, pct) + '%';
}
var label = document.getElementById('network-progress-label');
if (label) {
var labels = { entity_extraction: 'Entitäten werden extrahiert...', relationship_extraction: 'Beziehungen werden analysiert...', correction: 'Korrekturen werden angewendet...' };
label.textContent = labels[phase] || 'Wird verarbeitet...';
}
};
App._hideNetworkProgress = function() {
var bar = document.getElementById('network-progress-bar');
if (bar) bar.style.display = 'none';
};
/**
* Modal: Neue Netzwerkanalyse öffnen.
*/
App.openNetworkModal = async function() {
var list = document.getElementById('network-incident-options');
if (list) list.innerHTML = '<div style="padding:12px;color:var(--text-disabled);font-size:12px;">Lade Lagen...</div>';
openModal('modal-network-new');
// Lagen laden
try {
var incidents = await API.listIncidents();
if (list) {
list.innerHTML = incidents.map(function(inc) {
var typeLabel = inc.type === 'research' ? 'Analyse' : 'Live';
return '<label class="network-incident-option">' +
'<input type="checkbox" value="' + inc.id + '" class="network-incident-cb">' +
'<span>' + _escHtml(inc.title) + '</span>' +
'<span class="incident-option-type">' + typeLabel + '</span>' +
'</label>';
}).join('');
}
} catch (e) {
if (list) list.innerHTML = '<div style="padding:12px;color:var(--error);font-size:12px;">Fehler beim Laden der Lagen</div>';
}
// Name-Feld leeren
var nameField = document.getElementById('network-name');
if (nameField) nameField.value = '';
// Suchfeld leeren
var searchField = document.getElementById('network-incident-search');
if (searchField) {
searchField.value = '';
searchField.oninput = function() {
var term = this.value.toLowerCase();
document.querySelectorAll('.network-incident-option').forEach(function(opt) {
var text = opt.textContent.toLowerCase();
opt.style.display = text.includes(term) ? '' : 'none';
});
};
}
};
/**
* Netzwerkanalyse erstellen.
*/
App.submitNetworkAnalysis = async function(e) {
if (e) e.preventDefault();
var name = (document.getElementById('network-name').value || '').trim();
if (!name) {
UI.showToast('Bitte einen Namen eingeben.', 'warning');
return;
}
var incidentIds = [];
document.querySelectorAll('.network-incident-cb:checked').forEach(function(cb) {
incidentIds.push(parseInt(cb.value));
});
if (incidentIds.length === 0) {
UI.showToast('Bitte mindestens eine Lage auswählen.', 'warning');
return;
}
var btn = document.getElementById('network-submit-btn');
if (btn) btn.disabled = true;
try {
var result = await API.createNetworkAnalysis({ name: name, incident_ids: incidentIds });
closeModal('modal-network-new');
await this.loadNetworkAnalyses();
await this.selectNetworkAnalysis(result.id);
UI.showToast('Netzwerkanalyse gestartet.', 'success');
} catch (err) {
UI.showToast('Fehler: ' + err.message, 'error');
} finally {
if (btn) btn.disabled = false;
}
};
/**
* Netzwerkanalyse neu generieren.
*/
App.regenerateNetwork = async function() {
if (!this.currentNetworkId) return;
if (!await confirmDialog('Netzwerkanalyse neu generieren? Bestehende Daten werden überschrieben.')) return;
try {
await API.regenerateNetwork(this.currentNetworkId);
this._showNetworkProgress('entity_extraction', 0);
await this.loadNetworkAnalyses();
UI.showToast('Neugenerierung gestartet.', 'success');
} catch (err) {
UI.showToast('Fehler: ' + err.message, 'error');
}
};
/**
* Netzwerkanalyse löschen.
*/
App.deleteNetworkAnalysis = async function() {
if (!this.currentNetworkId) return;
if (!await confirmDialog('Netzwerkanalyse wirklich löschen? Alle Daten gehen verloren.')) return;
try {
await API.deleteNetworkAnalysis(this.currentNetworkId);
this.currentNetworkId = null;
localStorage.removeItem('selectedNetworkId');
NetworkGraph.destroy();
document.getElementById('network-view').style.display = 'none';
document.getElementById('empty-state').style.display = 'flex';
await this.loadNetworkAnalyses();
UI.showToast('Netzwerkanalyse gelöscht.', 'success');
} catch (err) {
UI.showToast('Fehler: ' + err.message, 'error');
}
};
/**
* Netzwerkanalyse exportieren.
*/
App.exportNetwork = async function(format) {
if (!this.currentNetworkId) return;
if (format === 'png') {
NetworkGraph.exportPNG();
return;
}
try {
var resp = await API.exportNetworkAnalysis(this.currentNetworkId, format);
if (!resp.ok) throw new Error('Export fehlgeschlagen');
var blob = await resp.blob();
var url = URL.createObjectURL(blob);
var a = document.createElement('a');
a.href = url;
a.download = 'netzwerk-' + this.currentNetworkId + '.' + format;
a.click();
URL.revokeObjectURL(url);
} catch (err) {
UI.showToast('Export fehlgeschlagen: ' + err.message, 'error');
}
};
/**
* WebSocket-Handler für Netzwerk-Events.
*/
App._handleNetworkStatus = function(msg) {
if (msg.analysis_id === this.currentNetworkId) {
this._showNetworkProgress(msg.phase, msg.progress || 0);
}
};
App._handleNetworkComplete = async function(msg) {
this._networkGenerating.delete(msg.analysis_id);
if (msg.analysis_id === this.currentNetworkId) {
this._hideNetworkProgress();
// Graph neu laden
try {
var graphData = await API.getNetworkGraph(msg.analysis_id);
NetworkGraph.init('network-graph-area', graphData);
this._setupNetworkFilters(graphData);
var analysis = await API.getNetworkAnalysis(msg.analysis_id);
this._renderNetworkHeader(analysis);
} catch (e) {
console.error('Graph nach Generierung laden fehlgeschlagen:', e);
}
UI.showToast('Netzwerkanalyse fertig: ' + (msg.entity_count || 0) + ' Entitäten, ' + (msg.relation_count || 0) + ' Beziehungen', 'success');
}
await this.loadNetworkAnalyses();
};
App._handleNetworkError = function(msg) {
this._networkGenerating.delete(msg.analysis_id);
if (msg.analysis_id === this.currentNetworkId) {
this._hideNetworkProgress();
var graphArea = document.getElementById('network-graph-area');
if (graphArea) graphArea.innerHTML = '<div class="network-empty-state"><div class="network-empty-state-icon">&#9888;</div><div class="network-empty-state-text">Fehler: ' + _escHtml(msg.error || 'Unbekannter Fehler') + '</div></div>';
}
UI.showToast('Netzwerkanalyse fehlgeschlagen: ' + (msg.error || 'Unbekannter Fehler'), 'error');
this.loadNetworkAnalyses();
};
/**
* Cluster isolieren (nur verbundene Knoten zeigen).
*/
App.isolateNetworkCluster = function() {
if (NetworkGraph._selectedNode) {
NetworkGraph.isolateCluster(NetworkGraph._selectedNode.id);
}
};
/**
* Graph-Ansicht zurücksetzen.
*/
App.resetNetworkView = function() {
NetworkGraph.resetView();
// Typ-Filter zurücksetzen
document.querySelectorAll('.network-type-filter').forEach(function(btn) {
if (!btn.disabled) btn.classList.add('active');
});
var slider = document.getElementById('network-weight-slider');
if (slider) { slider.value = 1; var lbl = document.getElementById('network-weight-value'); if (lbl) lbl.textContent = '1'; }
var search = document.getElementById('network-search');
if (search) search.value = '';
};
// HTML-Escape Hilfsfunktion (falls nicht global verfügbar)
function _escHtml(text) {
if (typeof UI !== 'undefined' && UI.escape) return UI.escape(text);
var d = document.createElement('div');
d.textContent = text || '';
return d.innerHTML;
}
/**
* Netzwerkanalyse-Erweiterungen für App-Objekt.
* Wird nach app.js geladen und erweitert App um Netzwerk-Funktionalität.
*/
// State-Erweiterung
App.networkAnalyses = [];
App.currentNetworkId = null;
App._networkGenerating = new Set();
/**
* Netzwerkanalysen laden und Sidebar rendern.
*/
App.loadNetworkAnalyses = async function() {
try {
this.networkAnalyses = await API.listNetworkAnalyses();
} catch (e) {
console.warn('Netzwerkanalysen laden fehlgeschlagen:', e);
this.networkAnalyses = [];
}
this.renderNetworkSidebar();
};
/**
* Netzwerkanalysen-Sektion in der Sidebar rendern.
*/
App.renderNetworkSidebar = function() {
var container = document.getElementById('network-analyses-list');
if (!container) return;
var countEl = document.getElementById('count-network-analyses');
if (countEl) countEl.textContent = '(' + this.networkAnalyses.length + ')';
if (this.networkAnalyses.length === 0) {
container.innerHTML = '<div style="padding:8px 12px;font-size:12px;color:var(--text-tertiary);">Keine Netzwerkanalysen</div>';
return;
}
var self = this;
container.innerHTML = this.networkAnalyses.map(function(na) {
var isActive = na.id === self.currentNetworkId;
var statusClass = na.status === 'generating' ? 'generating' : (na.status === 'error' ? 'error' : 'ready');
var countText = na.status === 'ready' ? (na.entity_count + ' / ' + na.relation_count) : na.status === 'generating' ? '...' : '';
return '<div class="sidebar-network-item' + (isActive ? ' active' : '') + '" onclick="App.selectNetworkAnalysis(' + na.id + ')">' +
'<svg class="network-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="5" r="3"/><circle cx="5" cy="19" r="3"/><circle cx="19" cy="19" r="3"/><line x1="12" y1="8" x2="5" y2="16"/><line x1="12" y1="8" x2="19" y2="16"/></svg>' +
'<span class="network-item-name" title="' + _escHtml(na.name) + '">' + _escHtml(na.name) + '</span>' +
'<span class="network-item-count">' + countText + '</span>' +
'<span class="network-status-dot ' + statusClass + '"></span>' +
'</div>';
}).join('');
};
/**
* Netzwerkanalyse auswählen und anzeigen.
*/
App.selectNetworkAnalysis = async function(id) {
this.currentNetworkId = id;
this.currentIncidentId = null;
localStorage.removeItem('selectedIncidentId');
localStorage.setItem('selectedNetworkId', id);
// Views umschalten
document.getElementById('empty-state').style.display = 'none';
document.getElementById('incident-view').style.display = 'none';
document.getElementById('network-view').style.display = 'flex';
// Sidebar aktualisieren
this.renderSidebar();
this.renderNetworkSidebar();
// Analyse laden
try {
var analysis = await API.getNetworkAnalysis(id);
this._renderNetworkHeader(analysis);
if (analysis.status === 'ready') {
this._hideNetworkProgress();
var graphData = await API.getNetworkGraph(id);
NetworkGraph.init('network-graph-area', graphData);
this._setupNetworkFilters(graphData);
// Update-Check
try {
var updateCheck = await API.checkNetworkUpdate(id);
var badge = document.getElementById('network-update-badge');
if (badge) badge.style.display = updateCheck.has_update ? 'inline-flex' : 'none';
} catch (e) { /* ignorieren */ }
} else if (analysis.status === 'generating') {
this._showNetworkProgress('entity_extraction', 0);
} else if (analysis.status === 'error') {
this._hideNetworkProgress();
var graphArea = document.getElementById('network-graph-area');
if (graphArea) graphArea.innerHTML = '<div class="network-empty-state"><div class="network-empty-state-icon">&#9888;</div><div class="network-empty-state-text">Fehler bei der Generierung. Versuche es erneut.</div></div>';
}
} catch (err) {
UI.showToast('Fehler beim Laden der Netzwerkanalyse: ' + err.message, 'error');
}
};
/**
* Netzwerkanalyse-Header rendern.
*/
App._renderNetworkHeader = function(analysis) {
var el;
el = document.getElementById('network-title');
if (el) el.textContent = analysis.name;
el = document.getElementById('network-entity-count');
if (el) el.textContent = analysis.entity_count + ' Entitäten';
el = document.getElementById('network-relation-count');
if (el) el.textContent = analysis.relation_count + ' Beziehungen';
el = document.getElementById('network-incident-list-text');
if (el) el.textContent = (analysis.incident_titles || []).join(', ') || '-';
el = document.getElementById('network-last-generated');
if (el) {
if (analysis.last_generated_at) {
var d = parseUTC(analysis.last_generated_at) || new Date(analysis.last_generated_at);
el.textContent = 'Generiert: ' + d.toLocaleDateString('de-DE', { day: '2-digit', month: '2-digit', year: 'numeric', timeZone: TIMEZONE }) + ' ' +
d.toLocaleTimeString('de-DE', { hour: '2-digit', minute: '2-digit', timeZone: TIMEZONE });
} else {
el.textContent = '';
}
}
};
/**
* Filter-Controls in der Netzwerk-Sidebar aufsetzen.
*/
App._setupNetworkFilters = function(graphData) {
// Typ-Filter-Buttons aktivieren
var types = new Set();
(graphData.entities || []).forEach(function(e) { types.add(e.entity_type); });
var filterContainer = document.getElementById('network-type-filter-container');
if (filterContainer) {
var allTypes = ['person', 'organisation', 'location', 'event', 'military'];
var typeLabels = { person: 'Person', organisation: 'Organisation', location: 'Ort', event: 'Ereignis', military: 'Militär' };
filterContainer.innerHTML = allTypes.map(function(t) {
var hasEntities = types.has(t);
return '<button class="network-type-filter active" data-type="' + t + '" onclick="App.toggleNetworkTypeFilter(this)" ' +
(hasEntities ? '' : 'disabled style="opacity:0.3"') + '>' +
'<span class="type-dot"></span><span>' + typeLabels[t] + '</span></button>';
}).join('');
}
// Gewicht-Slider
var slider = document.getElementById('network-weight-slider');
if (slider) {
slider.value = 1;
slider.oninput = function() {
var label = document.getElementById('network-weight-value');
if (label) label.textContent = this.value;
NetworkGraph.filterByWeight(parseInt(this.value));
};
}
// Suche
var searchInput = document.getElementById('network-search');
if (searchInput) {
searchInput.value = '';
var timer = null;
searchInput.oninput = function() {
clearTimeout(timer);
var val = this.value;
timer = setTimeout(function() {
NetworkGraph.search(val);
}, 250);
};
}
};
/**
* Typ-Filter toggle.
*/
App.toggleNetworkTypeFilter = function(btn) {
btn.classList.toggle('active');
var activeTypes = [];
document.querySelectorAll('.network-type-filter.active').forEach(function(b) {
activeTypes.push(b.dataset.type);
});
NetworkGraph.filterByType(new Set(activeTypes));
};
/**
* Progress-Bar anzeigen.
*/
App._showNetworkProgress = function(phase, progress) {
var bar = document.getElementById('network-progress-bar');
if (bar) bar.style.display = 'block';
var steps = ['entity_extraction', 'relationship_extraction', 'correction'];
var stepEls = document.querySelectorAll('.network-progress-step');
var connectorEls = document.querySelectorAll('.network-progress-connector');
var phaseIndex = steps.indexOf(phase);
stepEls.forEach(function(el, i) {
el.classList.remove('active', 'done');
if (i < phaseIndex) el.classList.add('done');
else if (i === phaseIndex) el.classList.add('active');
});
connectorEls.forEach(function(el, i) {
el.classList.remove('done');
if (i < phaseIndex) el.classList.add('done');
});
var fill = document.getElementById('network-progress-fill');
if (fill) {
var pct = ((phaseIndex / steps.length) * 100) + (progress || 0) * (100 / steps.length) / 100;
fill.style.width = Math.min(100, pct) + '%';
}
var label = document.getElementById('network-progress-label');
if (label) {
var labels = { entity_extraction: 'Entitäten werden extrahiert...', relationship_extraction: 'Beziehungen werden analysiert...', correction: 'Korrekturen werden angewendet...' };
label.textContent = labels[phase] || 'Wird verarbeitet...';
}
};
App._hideNetworkProgress = function() {
var bar = document.getElementById('network-progress-bar');
if (bar) bar.style.display = 'none';
};
/**
* Modal: Neue Netzwerkanalyse öffnen.
*/
App.openNetworkModal = async function() {
var list = document.getElementById('network-incident-options');
if (list) list.innerHTML = '<div style="padding:12px;color:var(--text-disabled);font-size:12px;">Lade Lagen...</div>';
openModal('modal-network-new');
// Lagen laden
try {
var incidents = await API.listIncidents();
// Sortierung: zuerst Live (adhoc) alphabetisch, dann Analyse (research) alphabetisch
incidents.sort(function(a, b) {
var typeA = (a.type === 'research') ? 1 : 0;
var typeB = (b.type === 'research') ? 1 : 0;
if (typeA !== typeB) return typeA - typeB;
return (a.title || '').localeCompare(b.title || '', 'de');
});
if (list) {
list.innerHTML = incidents.map(function(inc) {
var typeLabel = inc.type === 'research' ? 'Analyse' : 'Live';
return '<label class="network-incident-option">' +
'<input type="checkbox" value="' + inc.id + '" class="network-incident-cb">' +
'<span>' + _escHtml(inc.title) + '</span>' +
'<span class="incident-option-type">' + typeLabel + '</span>' +
'</label>';
}).join('');
}
} catch (e) {
if (list) list.innerHTML = '<div style="padding:12px;color:var(--error);font-size:12px;">Fehler beim Laden der Lagen</div>';
}
// Name-Feld leeren
var nameField = document.getElementById('network-name');
if (nameField) nameField.value = '';
// Suchfeld leeren
var searchField = document.getElementById('network-incident-search');
if (searchField) {
searchField.value = '';
searchField.oninput = function() {
var term = this.value.toLowerCase();
document.querySelectorAll('.network-incident-option').forEach(function(opt) {
var text = opt.textContent.toLowerCase();
opt.style.display = text.includes(term) ? '' : 'none';
});
};
}
};
/**
* Netzwerkanalyse erstellen.
*/
App.submitNetworkAnalysis = async function(e) {
if (e) e.preventDefault();
var name = (document.getElementById('network-name').value || '').trim();
if (!name) {
UI.showToast('Bitte einen Namen eingeben.', 'warning');
return;
}
var incidentIds = [];
document.querySelectorAll('.network-incident-cb:checked').forEach(function(cb) {
incidentIds.push(parseInt(cb.value));
});
if (incidentIds.length === 0) {
UI.showToast('Bitte mindestens eine Lage auswählen.', 'warning');
return;
}
var btn = document.getElementById('network-submit-btn');
if (btn) btn.disabled = true;
try {
var result = await API.createNetworkAnalysis({ name: name, incident_ids: incidentIds });
closeModal('modal-network-new');
await this.loadNetworkAnalyses();
await this.selectNetworkAnalysis(result.id);
UI.showToast('Netzwerkanalyse gestartet.', 'success');
} catch (err) {
UI.showToast('Fehler: ' + err.message, 'error');
} finally {
if (btn) btn.disabled = false;
}
};
/**
* Netzwerkanalyse neu generieren.
*/
App.regenerateNetwork = async function() {
if (!this.currentNetworkId) return;
if (!await confirmDialog('Netzwerkanalyse neu generieren? Bestehende Daten werden überschrieben.')) return;
try {
await API.regenerateNetwork(this.currentNetworkId);
this._showNetworkProgress('entity_extraction', 0);
await this.loadNetworkAnalyses();
UI.showToast('Neugenerierung gestartet.', 'success');
} catch (err) {
UI.showToast('Fehler: ' + err.message, 'error');
}
};
/**
* Netzwerkanalyse löschen.
*/
App.deleteNetworkAnalysis = async function() {
if (!this.currentNetworkId) return;
if (!await confirmDialog('Netzwerkanalyse wirklich löschen? Alle Daten gehen verloren.')) return;
try {
await API.deleteNetworkAnalysis(this.currentNetworkId);
this.currentNetworkId = null;
localStorage.removeItem('selectedNetworkId');
NetworkGraph.destroy();
document.getElementById('network-view').style.display = 'none';
document.getElementById('empty-state').style.display = 'flex';
await this.loadNetworkAnalyses();
UI.showToast('Netzwerkanalyse gelöscht.', 'success');
} catch (err) {
UI.showToast('Fehler: ' + err.message, 'error');
}
};
/**
* Netzwerkanalyse exportieren.
*/
App.exportNetwork = async function(format) {
if (!this.currentNetworkId) return;
if (format === 'png') {
NetworkGraph.exportPNG();
return;
}
try {
var resp = await API.exportNetworkAnalysis(this.currentNetworkId, format);
if (!resp.ok) throw new Error('Export fehlgeschlagen');
var blob = await resp.blob();
var url = URL.createObjectURL(blob);
var a = document.createElement('a');
a.href = url;
a.download = 'netzwerk-' + this.currentNetworkId + '.' + format;
a.click();
URL.revokeObjectURL(url);
} catch (err) {
UI.showToast('Export fehlgeschlagen: ' + err.message, 'error');
}
};
/**
* WebSocket-Handler für Netzwerk-Events.
*/
App._handleNetworkStatus = function(msg) {
if (msg.analysis_id === this.currentNetworkId) {
this._showNetworkProgress(msg.phase, msg.progress || 0);
}
};
App._handleNetworkComplete = async function(msg) {
this._networkGenerating.delete(msg.analysis_id);
if (msg.analysis_id === this.currentNetworkId) {
this._hideNetworkProgress();
// Graph neu laden
try {
var graphData = await API.getNetworkGraph(msg.analysis_id);
NetworkGraph.init('network-graph-area', graphData);
this._setupNetworkFilters(graphData);
var analysis = await API.getNetworkAnalysis(msg.analysis_id);
this._renderNetworkHeader(analysis);
} catch (e) {
console.error('Graph nach Generierung laden fehlgeschlagen:', e);
}
UI.showToast('Netzwerkanalyse fertig: ' + (msg.entity_count || 0) + ' Entitäten, ' + (msg.relation_count || 0) + ' Beziehungen', 'success');
}
await this.loadNetworkAnalyses();
};
App._handleNetworkError = function(msg) {
this._networkGenerating.delete(msg.analysis_id);
if (msg.analysis_id === this.currentNetworkId) {
this._hideNetworkProgress();
var graphArea = document.getElementById('network-graph-area');
if (graphArea) graphArea.innerHTML = '<div class="network-empty-state"><div class="network-empty-state-icon">&#9888;</div><div class="network-empty-state-text">Fehler: ' + _escHtml(msg.error || 'Unbekannter Fehler') + '</div></div>';
}
UI.showToast('Netzwerkanalyse fehlgeschlagen: ' + (msg.error || 'Unbekannter Fehler'), 'error');
this.loadNetworkAnalyses();
};
/**
* Cluster isolieren (nur verbundene Knoten zeigen).
*/
App.isolateNetworkCluster = function() {
if (NetworkGraph._selectedNode) {
NetworkGraph.isolateCluster(NetworkGraph._selectedNode.id);
}
};
/**
* Graph-Ansicht zurücksetzen.
*/
App.resetNetworkView = function() {
NetworkGraph.resetView();
// Typ-Filter zurücksetzen
document.querySelectorAll('.network-type-filter').forEach(function(btn) {
if (!btn.disabled) btn.classList.add('active');
});
var slider = document.getElementById('network-weight-slider');
if (slider) { slider.value = 1; var lbl = document.getElementById('network-weight-value'); if (lbl) lbl.textContent = '1'; }
var search = document.getElementById('network-search');
if (search) search.value = '';
};
// HTML-Escape Hilfsfunktion (falls nicht global verfügbar)
function _escHtml(text) {
if (typeof UI !== 'undefined' && UI.escape) return UI.escape(text);
var d = document.createElement('div');
d.textContent = text || '';
return d.innerHTML;
}