Fix: Progress-Timer zeigte negative Zahlen (-58:-10)
Ursache: Server sendete started_at als Lokalzeit (Europe/Berlin), aber der Client interpretierte es als UTC via parseUTC(). Bei UTC+1 lag die Startzeit dadurch 1 Stunde in der Zukunft. - orchestrator.py: started_at in WebSocket-Nachrichten als echtes UTC (ISO 8601 mit Z-Suffix) senden, DB-Timestamps bleiben Lokalzeit - components.js: elapsed auf min. 0 clampen als Sicherheitsnetz Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Dieser Commit ist enthalten in:
@@ -548,6 +548,7 @@ class AgentOrchestrator:
|
|||||||
|
|
||||||
# Refresh-Log starten
|
# Refresh-Log starten
|
||||||
now = datetime.now(TIMEZONE).strftime('%Y-%m-%d %H:%M:%S')
|
now = datetime.now(TIMEZONE).strftime('%Y-%m-%d %H:%M:%S')
|
||||||
|
now_utc = datetime.utcnow().strftime('%Y-%m-%dT%H:%M:%SZ')
|
||||||
cursor = await db.execute(
|
cursor = await db.execute(
|
||||||
"INSERT INTO refresh_log (incident_id, started_at, status, trigger_type, retry_count, tenant_id) VALUES (?, ?, 'running', ?, ?, ?)",
|
"INSERT INTO refresh_log (incident_id, started_at, status, trigger_type, retry_count, tenant_id) VALUES (?, ?, 'running', ?, ?, ?)",
|
||||||
(incident_id, now, trigger_type, retry_count, tenant_id),
|
(incident_id, now, trigger_type, retry_count, tenant_id),
|
||||||
@@ -562,7 +563,7 @@ class AgentOrchestrator:
|
|||||||
await self._ws_manager.broadcast_for_incident({
|
await self._ws_manager.broadcast_for_incident({
|
||||||
"type": "status_update",
|
"type": "status_update",
|
||||||
"incident_id": incident_id,
|
"incident_id": incident_id,
|
||||||
"data": {"status": research_status, "detail": research_detail, "started_at": now},
|
"data": {"status": research_status, "detail": research_detail, "started_at": now_utc},
|
||||||
}, visibility, created_by, tenant_id)
|
}, visibility, created_by, tenant_id)
|
||||||
|
|
||||||
# Schritt 1+2: RSS-Feeds und Claude-Recherche parallel ausführen
|
# Schritt 1+2: RSS-Feeds und Claude-Recherche parallel ausführen
|
||||||
@@ -840,7 +841,7 @@ class AgentOrchestrator:
|
|||||||
await self._ws_manager.broadcast_for_incident({
|
await self._ws_manager.broadcast_for_incident({
|
||||||
"type": "status_update",
|
"type": "status_update",
|
||||||
"incident_id": incident_id,
|
"incident_id": incident_id,
|
||||||
"data": {"status": "factchecking", "detail": "Prüft Fakten gegen unabhängige Quellen...", "started_at": now},
|
"data": {"status": "factchecking", "detail": "Prüft Fakten gegen unabhängige Quellen...", "started_at": now_utc},
|
||||||
}, visibility, created_by, tenant_id)
|
}, visibility, created_by, tenant_id)
|
||||||
|
|
||||||
# Schritt 4: Faktencheck
|
# Schritt 4: Faktencheck
|
||||||
|
|||||||
@@ -291,7 +291,7 @@ const UI = {
|
|||||||
|
|
||||||
this._progressTimer = setInterval(() => {
|
this._progressTimer = setInterval(() => {
|
||||||
if (!this._progressStartTime) return;
|
if (!this._progressStartTime) return;
|
||||||
const elapsed = Math.floor((Date.now() - this._progressStartTime) / 1000);
|
const elapsed = Math.max(0, Math.floor((Date.now() - this._progressStartTime) / 1000));
|
||||||
const mins = Math.floor(elapsed / 60);
|
const mins = Math.floor(elapsed / 60);
|
||||||
const secs = elapsed % 60;
|
const secs = elapsed % 60;
|
||||||
timerEl.textContent = `${mins}:${String(secs).padStart(2, '0')}`;
|
timerEl.textContent = `${mins}:${String(secs).padStart(2, '0')}`;
|
||||||
@@ -605,6 +605,45 @@ const UI = {
|
|||||||
|
|
||||||
_pendingLocations: null,
|
_pendingLocations: null,
|
||||||
|
|
||||||
|
// Farbige Marker-Icons nach Kategorie (inline SVG, keine externen Ressourcen)
|
||||||
|
_markerIcons: null,
|
||||||
|
_createSvgIcon(fillColor, strokeColor) {
|
||||||
|
const svg = `<svg xmlns="http://www.w3.org/2000/svg" width="28" height="42" viewBox="0 0 28 42">` +
|
||||||
|
`<path d="M14 0C6.27 0 0 6.27 0 14c0 10.5 14 28 14 28s14-17.5 14-28C28 6.27 21.73 0 14 0z" fill="${fillColor}" stroke="${strokeColor}" stroke-width="1.5"/>` +
|
||||||
|
`<circle cx="14" cy="14" r="7" fill="#fff" opacity="0.9"/>` +
|
||||||
|
`<circle cx="14" cy="14" r="4" fill="${fillColor}"/>` +
|
||||||
|
`</svg>`;
|
||||||
|
return L.divIcon({
|
||||||
|
html: svg,
|
||||||
|
className: 'map-marker-svg',
|
||||||
|
iconSize: [28, 42],
|
||||||
|
iconAnchor: [14, 42],
|
||||||
|
popupAnchor: [0, -36],
|
||||||
|
});
|
||||||
|
},
|
||||||
|
_initMarkerIcons() {
|
||||||
|
if (this._markerIcons || typeof L === 'undefined') return;
|
||||||
|
this._markerIcons = {
|
||||||
|
target: this._createSvgIcon('#dc3545', '#a71d2a'),
|
||||||
|
retaliation: this._createSvgIcon('#f39c12', '#c47d0a'),
|
||||||
|
actor: this._createSvgIcon('#2a81cb', '#1a5c8f'),
|
||||||
|
mentioned: this._createSvgIcon('#7b7b7b', '#555555'),
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
_categoryLabels: {
|
||||||
|
target: 'Angegriffene Ziele',
|
||||||
|
retaliation: 'Vergeltung / Eskalation',
|
||||||
|
actor: 'Strategische Akteure',
|
||||||
|
mentioned: 'Erwaehnt',
|
||||||
|
},
|
||||||
|
_categoryColors: {
|
||||||
|
target: '#cb2b3e',
|
||||||
|
retaliation: '#f39c12',
|
||||||
|
actor: '#2a81cb',
|
||||||
|
mentioned: '#7b7b7b',
|
||||||
|
},
|
||||||
|
|
||||||
renderMap(locations) {
|
renderMap(locations) {
|
||||||
const container = document.getElementById('map-container');
|
const container = document.getElementById('map-container');
|
||||||
const emptyEl = document.getElementById('map-empty');
|
const emptyEl = document.getElementById('map-empty');
|
||||||
@@ -683,14 +722,24 @@ const UI = {
|
|||||||
|
|
||||||
// Marker hinzufuegen
|
// Marker hinzufuegen
|
||||||
const bounds = [];
|
const bounds = [];
|
||||||
|
this._initMarkerIcons();
|
||||||
|
const usedCategories = new Set();
|
||||||
|
|
||||||
locations.forEach(loc => {
|
locations.forEach(loc => {
|
||||||
const marker = L.marker([loc.lat, loc.lon]);
|
const cat = loc.category || 'mentioned';
|
||||||
|
usedCategories.add(cat);
|
||||||
|
const icon = (this._markerIcons && this._markerIcons[cat]) ? this._markerIcons[cat] : undefined;
|
||||||
|
const markerOpts = icon ? { icon } : {};
|
||||||
|
const marker = L.marker([loc.lat, loc.lon], markerOpts);
|
||||||
|
|
||||||
// Popup-Inhalt
|
// Popup-Inhalt
|
||||||
|
const catLabel = this._categoryLabels[cat] || cat;
|
||||||
|
const catColor = this._categoryColors[cat] || '#7b7b7b';
|
||||||
let popupHtml = `<div class="map-popup">`;
|
let popupHtml = `<div class="map-popup">`;
|
||||||
popupHtml += `<div class="map-popup-title">${this.escape(loc.location_name)}`;
|
popupHtml += `<div class="map-popup-title">${this.escape(loc.location_name)}`;
|
||||||
if (loc.country_code) popupHtml += ` <span class="map-popup-cc">${this.escape(loc.country_code)}</span>`;
|
if (loc.country_code) popupHtml += ` <span class="map-popup-cc">${this.escape(loc.country_code)}</span>`;
|
||||||
popupHtml += `</div>`;
|
popupHtml += `</div>`;
|
||||||
|
popupHtml += `<div class="map-popup-category" style="font-size:11px;margin-bottom:4px;"><span style="display:inline-block;width:10px;height:10px;border-radius:50%;background:${catColor};margin-right:4px;vertical-align:middle;"></span>${catLabel}</div>`;
|
||||||
popupHtml += `<div class="map-popup-count">${loc.article_count} Artikel</div>`;
|
popupHtml += `<div class="map-popup-count">${loc.article_count} Artikel</div>`;
|
||||||
popupHtml += `<div class="map-popup-articles">`;
|
popupHtml += `<div class="map-popup-articles">`;
|
||||||
const maxShow = 5;
|
const maxShow = 5;
|
||||||
@@ -722,6 +771,29 @@ const UI = {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Legende hinzufuegen
|
||||||
|
if (this._map) {
|
||||||
|
// Alte Legende entfernen
|
||||||
|
this._map.eachLayer(layer => {});
|
||||||
|
const existingLegend = document.querySelector('.map-legend-ctrl');
|
||||||
|
if (existingLegend) existingLegend.remove();
|
||||||
|
|
||||||
|
const legend = L.control({ position: 'bottomright' });
|
||||||
|
const self2 = this;
|
||||||
|
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', '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>`;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
div.innerHTML = html;
|
||||||
|
return div;
|
||||||
|
};
|
||||||
|
legend.addTo(this._map);
|
||||||
|
}
|
||||||
|
|
||||||
// Resize-Fix fuer gridstack (mehrere Versuche, da Container-Hoehe erst spaeter steht)
|
// Resize-Fix fuer gridstack (mehrere Versuche, da Container-Hoehe erst spaeter steht)
|
||||||
const self = this;
|
const self = this;
|
||||||
[100, 300, 800].forEach(delay => {
|
[100, 300, 800].forEach(delay => {
|
||||||
|
|||||||
In neuem Issue referenzieren
Einen Benutzer sperren