feat: Kontextabhängige Karten-Kategorien

4 feste Farbstufen (primary/secondary/tertiary/mentioned) mit
variablen Labels pro Lage, die von Haiku generiert werden.

- DB: category_labels Spalte in incidents, alte Kategorien migriert
  (target->primary, response/retaliation->secondary, actor->tertiary)
- Geoparsing: generate_category_labels() + neuer Prompt mit neuen Keys
- QC: Kategorieprüfung auf neue Keys umgestellt
- Orchestrator: Tuple-Rückgabe + Labels in DB speichern
- API: category_labels im Locations- und Lagebild-Response
- Frontend: Dynamische Legende aus API-Labels mit Fallback-Defaults
- Migrationsskript für bestehende Lagen

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Dieser Commit ist enthalten in:
Claude Dev
2026-03-15 15:04:02 +01:00
Ursprung 5fd65657c5
Commit 19da099583
9 geänderte Dateien mit 1315 neuen und 1012 gelöschten Zeilen

Datei anzeigen

@@ -698,7 +698,7 @@ const App = {
async loadIncidentDetail(id) {
try {
const [incident, articles, factchecks, snapshots, locations] = await Promise.all([
const [incident, articles, factchecks, snapshots, locationsResponse] = await Promise.all([
API.getIncident(id),
API.getArticles(id),
API.getFactChecks(id),
@@ -706,14 +706,27 @@ const App = {
API.getLocations(id).catch(() => []),
]);
this.renderIncidentDetail(incident, articles, factchecks, snapshots, locations);
// Locations-API gibt jetzt {category_labels, locations} oder Array (Rückwärtskompatibel)
let locations, categoryLabels;
if (Array.isArray(locationsResponse)) {
locations = locationsResponse;
categoryLabels = null;
} else if (locationsResponse && locationsResponse.locations) {
locations = locationsResponse.locations;
categoryLabels = locationsResponse.category_labels || null;
} else {
locations = [];
categoryLabels = null;
}
this.renderIncidentDetail(incident, articles, factchecks, snapshots, locations, categoryLabels);
} catch (err) {
console.error('loadIncidentDetail Fehler:', err);
UI.showToast('Fehler beim Laden: ' + err.message, 'error');
}
},
renderIncidentDetail(incident, articles, factchecks, snapshots, locations) {
renderIncidentDetail(incident, articles, factchecks, snapshots, locations, categoryLabels) {
// Header Strip
{ const _e = document.getElementById('incident-title'); if (_e) _e.textContent = incident.title; }
{ const _e = document.getElementById('incident-description'); if (_e) _e.textContent = incident.description || ''; }
@@ -845,7 +858,7 @@ const App = {
this._resizeTimelineTile();
// Karte rendern
UI.renderMap(locations || []);
UI.renderMap(locations || [], categoryLabels);
},
_collectEntries(filterType, searchTerm, range) {
@@ -1617,8 +1630,12 @@ const App = {
if (btn) { btn.disabled = false; btn.textContent = 'Orte erkennen'; }
if (st.status === 'done' && st.locations > 0) {
UI.showToast(`${st.locations} Orte aus ${st.processed} Artikeln erkannt`, 'success');
const locations = await API.getLocations(incidentId).catch(() => []);
UI.renderMap(locations);
const locResp = await API.getLocations(incidentId).catch(() => []);
let locs, catLabels;
if (Array.isArray(locResp)) { locs = locResp; catLabels = null; }
else if (locResp && locResp.locations) { locs = locResp.locations; catLabels = locResp.category_labels || null; }
else { locs = []; catLabels = null; }
UI.renderMap(locs, catLabels);
} else if (st.status === 'done') {
UI.showToast('Keine neuen Orte gefunden', 'info');
} else if (st.status === 'error') {