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

@@ -639,30 +639,29 @@ const UI = {
_initMarkerIcons() {
if (this._markerIcons || typeof L === 'undefined') return;
this._markerIcons = {
target: this._createSvgIcon('#dc3545', '#a71d2a'),
retaliation: this._createSvgIcon('#f39c12', '#c47d0a'),
response: this._createSvgIcon('#f39c12', '#c47d0a'),
actor: this._createSvgIcon('#2a81cb', '#1a5c8f'),
primary: this._createSvgIcon('#dc3545', '#a71d2a'),
secondary: this._createSvgIcon('#f39c12', '#c47d0a'),
tertiary: this._createSvgIcon('#2a81cb', '#1a5c8f'),
mentioned: this._createSvgIcon('#7b7b7b', '#555555'),
};
},
_categoryLabels: {
target: 'Angegriffene Ziele',
retaliation: 'Vergeltung / Eskalation',
response: 'Reaktion / Gegenmassnahmen',
actor: 'Strategische Akteure',
_defaultCategoryLabels: {
primary: 'Hauptgeschehen',
secondary: 'Reaktionen',
tertiary: 'Beteiligte',
mentioned: 'Erwaehnt',
},
_categoryColors: {
target: '#cb2b3e',
retaliation: '#f39c12',
response: '#f39c12',
actor: '#2a81cb',
primary: '#cb2b3e',
secondary: '#f39c12',
tertiary: '#2a81cb',
mentioned: '#7b7b7b',
},
renderMap(locations) {
_activeCategoryLabels: null,
renderMap(locations, categoryLabels) {
const container = document.getElementById('map-container');
const emptyEl = document.getElementById('map-empty');
const statsEl = document.getElementById('map-stats');
@@ -741,6 +740,9 @@ const UI = {
// Marker hinzufuegen
const bounds = [];
this._initMarkerIcons();
// Dynamische Labels verwenden (API > Default)
const catLabels = categoryLabels || this._activeCategoryLabels || this._defaultCategoryLabels;
this._activeCategoryLabels = catLabels;
const usedCategories = new Set();
locations.forEach(loc => {
@@ -751,7 +753,7 @@ const UI = {
const marker = L.marker([loc.lat, loc.lon], markerOpts);
// Popup-Inhalt
const catLabel = this._categoryLabels[cat] || cat;
const catLabel = catLabels[cat] || this._defaultCategoryLabels[cat] || cat;
const catColor = this._categoryColors[cat] || '#7b7b7b';
let popupHtml = `<div class="map-popup">`;
popupHtml += `<div class="map-popup-title">${this.escape(loc.location_name)}`;
@@ -798,12 +800,13 @@ const UI = {
const legend = L.control({ position: 'bottomright' });
const self2 = this;
const legendLabels = catLabels;
legend.onAdd = function() {
const div = L.DomUtil.create('div', 'map-legend-ctrl');
let html = '<strong style="display:block;margin-bottom:6px;">Legende</strong>';
['target', 'retaliation', 'response', 'actor', 'mentioned'].forEach(cat => {
if (usedCategories.has(cat)) {
html += `<div style="display:flex;align-items:center;gap:6px;margin:3px 0;"><span style="width:10px;height:10px;border-radius:50%;background:${self2._categoryColors[cat]};flex-shrink:0;"></span><span>${self2._categoryLabels[cat]}</span></div>`;
['primary', 'secondary', 'tertiary', 'mentioned'].forEach(cat => {
if (usedCategories.has(cat) && legendLabels[cat]) {
html += `<div style="display:flex;align-items:center;gap:6px;margin:3px 0;"><span style="width:10px;height:10px;border-radius:50%;background:${self2._categoryColors[cat]};flex-shrink:0;"></span><span>${legendLabels[cat]}</span></div>`;
}
});
div.innerHTML = html;
@@ -853,7 +856,7 @@ const UI = {
if (this._pendingLocations && typeof L !== 'undefined') {
const locs = this._pendingLocations;
this._pendingLocations = null;
this.renderMap(locs);
this.renderMap(locs, this._activeCategoryLabels);
}
},