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:
@@ -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') {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
In neuem Issue referenzieren
Einen Benutzer sperren