feat: Netzwerkanalyse-Feature (Wissensgraph)

Neues Feature zur Visualisierung von Entitäten und Beziehungen
aus ausgewählten Lagen als interaktiver d3.js-Netzwerkgraph.

- Haiku extrahiert Entitäten (Person, Organisation, Ort, Ereignis, Militär)
- Opus analysiert Beziehungen und korrigiert Haiku-Fehler
- 6 neue DB-Tabellen (network_analyses, _entities, _relations, etc.)
- REST-API: CRUD + Generierung + Export (JSON/CSV)
- d3.js Force-Directed Graph mit Zoom, Filter, Suche, Export
- WebSocket-Events für Live-Progress während Generierung
- Sidebar-Integration mit Netzwerkanalysen-Sektion

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Dieser Commit ist enthalten in:
Claude Dev
2026-03-16 00:34:26 +01:00
Ursprung d86dae1e86
Commit 9a35973d00
11 geänderte Dateien mit 4047 neuen und 603 gelöschten Zeilen

Datei anzeigen

@@ -16,6 +16,7 @@
<link rel="stylesheet" href="/static/vendor/leaflet.css">
<link rel="stylesheet" href="/static/vendor/MarkerCluster.css">
<link rel="stylesheet" href="/static/vendor/MarkerCluster.Default.css">
<link rel="stylesheet" href="/static/css/network.css?v=20260316a">
<link rel="stylesheet" href="/static/css/style.css?v=20260304h">
</head>
<body>
@@ -93,6 +94,16 @@
</h2>
<div id="archived-incidents" aria-live="polite" style="display:none;"></div>
</div>
<div class="sidebar-section">
<button class="btn btn-primary btn-full btn-small" id="new-network-btn" onclick="App.openNetworkModal()" style="margin-bottom:8px;">+ Neue Netzwerkanalyse</button>
<h2 class="sidebar-section-title collapsible" onclick="App.toggleSidebarSection('network-analyses-list')" role="button" tabindex="0" aria-expanded="true">
<span class="sidebar-chevron" id="chevron-network-analyses-list" aria-hidden="true">&#9662;</span>
Netzwerkanalysen
<span class="sidebar-section-count" id="count-network-analyses"></span>
</h2>
<div id="network-analyses-list" aria-live="polite"></div>
</div>
<div class="sidebar-sources-link">
<button class="btn btn-secondary btn-full btn-small" onclick="App.openSourceManagement()">Quellen verwalten</button>
<button class="btn btn-secondary btn-full btn-small sidebar-feedback-btn" onclick="App.openFeedback()">Feedback senden</button>
@@ -110,6 +121,99 @@
<div class="empty-state-text">Erstelle eine neue Lage oder wähle einen bestehenden Vorfall aus der Seitenleiste.</div>
</div>
<!-- Netzwerkanalyse View (hidden by default) -->
<div id="network-view" style="display:none;">
<!-- Header Strip -->
<div class="network-header-strip">
<div class="network-header-row1">
<div class="network-header-left">
<span class="incident-type-badge type-network">Netzwerk</span>
<h2 class="network-header-title" id="network-title"></h2>
<span class="network-update-badge" id="network-update-badge" style="display:none;" onclick="App.regenerateNetwork()">Aktualisierung verfügbar</span>
</div>
<div class="network-header-actions">
<button class="btn btn-primary btn-small" onclick="App.regenerateNetwork()">Neu generieren</button>
<div class="export-dropdown">
<button class="btn btn-secondary btn-small" onclick="this.nextElementSibling.classList.toggle('show')" aria-haspopup="true">Exportieren &#9662;</button>
<div class="export-dropdown-menu" role="menu">
<button class="export-dropdown-item" role="menuitem" onclick="App.exportNetwork('json')">JSON</button>
<button class="export-dropdown-item" role="menuitem" onclick="App.exportNetwork('csv')">CSV (Kantenliste)</button>
<hr class="export-dropdown-divider" role="separator">
<button class="export-dropdown-item" role="menuitem" onclick="App.exportNetwork('png')">PNG Screenshot</button>
</div>
</div>
<button class="btn btn-danger btn-small" onclick="App.deleteNetworkAnalysis()">Löschen</button>
</div>
</div>
<div class="network-header-meta">
<span id="network-entity-count"></span>
<span id="network-relation-count"></span>
<span id="network-last-generated"></span>
<span id="network-incident-list-text" style="opacity:0.7;"></span>
</div>
</div>
<!-- Progress Bar -->
<div class="network-progress" id="network-progress-bar" style="display:none;">
<div class="network-progress-steps">
<div class="network-progress-step" data-step="entity_extraction">
<div class="network-progress-step-dot"></div>
<span>Entitäten</span>
</div>
<div class="network-progress-connector"></div>
<div class="network-progress-step" data-step="relationship_extraction">
<div class="network-progress-step-dot"></div>
<span>Beziehungen</span>
</div>
<div class="network-progress-connector"></div>
<div class="network-progress-step" data-step="correction">
<div class="network-progress-step-dot"></div>
<span>Korrekturen</span>
</div>
</div>
<div class="network-progress-track">
<div class="network-progress-fill" id="network-progress-fill"></div>
</div>
<div class="network-progress-label" id="network-progress-label">Wird verarbeitet...</div>
</div>
<!-- Graph + Sidebar -->
<div class="network-content">
<div class="network-graph-area" id="network-graph-area">
<div class="network-empty-state">
<div class="network-empty-state-icon">&#9737;</div>
<div class="network-empty-state-text">Graph wird geladen...</div>
</div>
</div>
<div class="network-sidebar">
<div class="network-sidebar-section">
<div class="network-sidebar-section-title">Suche</div>
<input type="text" class="network-search-input" id="network-search" placeholder="Entität suchen...">
</div>
<div class="network-sidebar-section">
<div class="network-sidebar-section-title">Typ-Filter</div>
<div class="network-type-filters" id="network-type-filter-container"></div>
</div>
<div class="network-sidebar-section">
<div class="network-sidebar-section-title">Min. Gewicht: <strong id="network-weight-value">1</strong></div>
<input type="range" class="network-weight-slider" id="network-weight-slider" min="1" max="5" value="1" step="1">
<div class="network-weight-labels"><span>1</span><span>5</span></div>
</div>
<div class="network-sidebar-section" style="border-bottom:none;">
<button class="btn btn-secondary btn-small btn-full" onclick="App.resetNetworkView()" style="margin-bottom:6px;">Filter zurücksetzen</button>
<button class="btn btn-secondary btn-small btn-full" onclick="App.isolateNetworkCluster()">Cluster isolieren</button>
</div>
<div class="network-detail-panel" id="network-detail-panel">
<div class="network-detail-empty">Klicke auf einen Knoten für Details</div>
</div>
</div>
</div>
<!-- Tooltip -->
<div class="network-tooltip" id="network-tooltip"></div>
</div>
<!-- Lagebild (hidden by default) -->
<div id="incident-view" style="display:none;">
<!-- Header Strip -->
@@ -607,9 +711,40 @@
</form>
</div>
<!-- Modal: Neue Netzwerkanalyse -->
<div class="modal-overlay" id="modal-network-new" role="dialog" aria-modal="true" aria-labelledby="modal-network-new-title">
<div class="modal">
<div class="modal-header">
<div class="modal-title" id="modal-network-new-title">Neue Netzwerkanalyse</div>
<button class="modal-close" onclick="closeModal('modal-network-new')" aria-label="Schließen">&times;</button>
</div>
<form onsubmit="App.submitNetworkAnalysis(event); return false;">
<div class="modal-body">
<div class="form-group">
<label for="network-name">Name der Analyse</label>
<input type="text" id="network-name" required placeholder="z.B. Irankonflikt-Netzwerk">
</div>
<div class="form-group">
<label>Lagen auswählen</label>
<div class="network-incident-list">
<input type="text" class="network-incident-search" id="network-incident-search" placeholder="Lagen durchsuchen...">
<div id="network-incident-options"></div>
</div>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" onclick="closeModal('modal-network-new')">Abbrechen</button>
<button type="submit" class="btn btn-primary" id="network-submit-btn">Analyse starten</button>
</div>
</form>
</div>
</div>
<!-- Toast Container -->
<div class="toast-container" id="toast-container" aria-live="polite" aria-atomic="true"></div>
<script src="https://cdn.jsdelivr.net/npm/d3@7/dist/d3.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/gridstack@12/dist/gridstack-all.js"></script>
<script src="/static/vendor/leaflet.js"></script>
<script src="/static/vendor/leaflet.markercluster.js"></script>
@@ -618,6 +753,9 @@
<script src="/static/js/components.js?v=20260304h"></script>
<script src="/static/js/layout.js?v=20260304h"></script>
<script src="/static/js/app.js?v=20260304h"></script>
<script src="/static/js/api_network.js?v=20260316a"></script>
<script src="/static/js/network-graph.js?v=20260316a"></script>
<script src="/static/js/app_network.js?v=20260316a"></script>
<script src="/static/js/chat.js?v=20260315b"></script>
<script>document.addEventListener("DOMContentLoaded",function(){Chat.init()});</script>