From ac3291f6086fcceed8da83a1b1a999439afaa36f Mon Sep 17 00:00:00 2001 From: claude-dev Date: Sat, 7 Mar 2026 10:31:21 +0100 Subject: [PATCH] fix: Alle Zeitanzeigen fest auf Europe/Berlin Zeitzone Alle toLocaleTimeString/toLocaleDateString-Aufrufe und getHours/getMinutes-Zugriffe verwenden jetzt fix die Zeitzone Europe/Berlin statt der Browser-Zeitzone. Co-Authored-By: Claude Opus 4.6 --- src/static/js/app.js | 95 ++++++++++++++++++++++++++------------------ 1 file changed, 57 insertions(+), 38 deletions(-) diff --git a/src/static/js/app.js b/src/static/js/app.js index ec0a29c..7de4f5f 100644 --- a/src/static/js/app.js +++ b/src/static/js/app.js @@ -2,6 +2,20 @@ * OSINT Lagemonitor - Hauptanwendungslogik. */ +/** Feste Zeitzone fuer alle Anzeigen — NIEMALS aendern. */ +const TIMEZONE = 'Europe/Berlin'; + +/** Gibt Jahr/Monat(0-basiert)/Tag/Stunde/Minute in Berliner Zeit zurueck. */ +function _tz(d) { + const s = d.toLocaleString('en-CA', { + timeZone: TIMEZONE, year: 'numeric', month: '2-digit', day: '2-digit', + hour: '2-digit', minute: '2-digit', second: '2-digit', hour12: false + }); + const m = s.match(/(\d{4})-(\d{2})-(\d{2}),?\s*(\d{2}):(\d{2}):(\d{2})/); + if (!m) return { year: d.getFullYear(), month: d.getMonth(), date: d.getDate(), hours: d.getHours(), minutes: d.getMinutes() }; + return { year: +m[1], month: +m[2] - 1, date: +m[3], hours: +m[4], minutes: +m[5] }; +} + /** * Theme Manager: Dark/Light Theme Toggle mit localStorage-Persistenz. */ @@ -317,7 +331,7 @@ const NotificationCenter = { list.innerHTML = this._notifications.map(n => { const time = new Date(n.timestamp); - const timeStr = time.toLocaleTimeString('de-DE', { hour: '2-digit', minute: '2-digit' }); + const timeStr = time.toLocaleTimeString('de-DE', { hour: '2-digit', minute: '2-digit', timeZone: TIMEZONE }); const unreadClass = n.read ? '' : ' unread'; const icon = n.icon || 'info'; return `
@@ -692,10 +706,10 @@ const App = { } // Meta (im Header-Strip) — relative Zeitangabe mit vollem Datum als Tooltip - const updated = incident.updated_at ? new Date(incident.updated_at) : null; + const updated = incident.updated_at ? parseUTC(incident.updated_at) : null; const metaUpdated = document.getElementById('meta-updated'); if (updated) { - const fullDate = `${updated.toLocaleDateString('de-DE')} ${updated.toLocaleTimeString('de-DE', { hour: '2-digit', minute: '2-digit' })}`; + const fullDate = `${updated.toLocaleDateString('de-DE', { timeZone: TIMEZONE })} ${updated.toLocaleTimeString('de-DE', { hour: '2-digit', minute: '2-digit', timeZone: TIMEZONE })}`; metaUpdated.textContent = `Stand: ${App._timeAgo(updated)}`; metaUpdated.title = fullDate; } else { @@ -707,7 +721,7 @@ const App = { const lagebildTs = document.getElementById('lagebild-timestamp'); if (lagebildTs) { lagebildTs.textContent = updated - ? `Stand: ${updated.toLocaleDateString('de-DE', { day: '2-digit', month: '2-digit', year: 'numeric' })} ${updated.toLocaleTimeString('de-DE', { hour: '2-digit', minute: '2-digit' })} Uhr` + ? `Stand: ${updated.toLocaleDateString('de-DE', { day: '2-digit', month: '2-digit', year: 'numeric', timeZone: TIMEZONE })} ${updated.toLocaleTimeString('de-DE', { hour: '2-digit', minute: '2-digit', timeZone: TIMEZONE })} Uhr` : ''; } @@ -945,15 +959,16 @@ const App = { const bucketMap = {}; entries.forEach(e => { const d = new Date(e.timestamp || 0); + const b = _tz(d); let key, label, ts; if (granularity === 'hour') { - key = `${d.getFullYear()}-${d.getMonth() + 1}-${d.getDate()}-${d.getHours()}`; - label = d.toLocaleDateString('de-DE', { day: '2-digit', month: 'short' }) + ', ' + d.getHours().toString().padStart(2, '0') + ':00'; - ts = new Date(d.getFullYear(), d.getMonth(), d.getDate(), d.getHours()).getTime(); + key = `${b.year}-${b.month + 1}-${b.date}-${b.hours}`; + label = d.toLocaleDateString('de-DE', { day: '2-digit', month: 'short', timeZone: TIMEZONE }) + ', ' + b.hours.toString().padStart(2, '0') + ':00'; + ts = new Date(b.year, b.month, b.date, b.hours).getTime(); } else { - key = `${d.getFullYear()}-${d.getMonth() + 1}-${d.getDate()}`; - label = d.toLocaleDateString('de-DE', { weekday: 'short', day: '2-digit', month: 'short' }); - ts = new Date(d.getFullYear(), d.getMonth(), d.getDate(), 12).getTime(); + key = `${b.year}-${b.month + 1}-${b.date}`; + label = d.toLocaleDateString('de-DE', { weekday: 'short', day: '2-digit', month: 'short', timeZone: TIMEZONE }); + ts = new Date(b.year, b.month, b.date, 12).getTime(); } if (!bucketMap[key]) { bucketMap[key] = { key, label, timestamp: ts, entries: [] }; @@ -1019,7 +1034,8 @@ const App = { const ts = (granularity === 'day' && b.entries && b.entries.length > 0) ? new Date(b.entries[0].timestamp || b.timestamp) : new Date(b.timestamp); - return ts.getHours().toString().padStart(2, '0') + ':' + ts.getMinutes().toString().padStart(2, '0'); + const tp = _tz(ts); + return tp.hours.toString().padStart(2, '0') + ':' + tp.minutes.toString().padStart(2, '0'); } return b.label; }; @@ -1056,21 +1072,22 @@ const App = { const markers = []; buckets.forEach(b => { const d = new Date(b.timestamp); - const dayKey = `${d.getFullYear()}-${d.getMonth()}-${d.getDate()}`; + const bp = _tz(d); + const dayKey = `${bp.year}-${bp.month}-${bp.date}`; if (!seen[dayKey]) { seen[dayKey] = true; - const now = new Date(); - const today = new Date(now.getFullYear(), now.getMonth(), now.getDate()); - const yesterday = new Date(today.getTime() - 86400000); - const bucketDay = new Date(d.getFullYear(), d.getMonth(), d.getDate()); + const np = _tz(new Date()); + const todayKey = `${np.year}-${np.month}-${np.date}`; + const yp = _tz(new Date(Date.now() - 86400000)); + const yesterdayKey = `${yp.year}-${yp.month}-${yp.date}`; let label; - const dateStr = d.toLocaleDateString('de-DE', { day: '2-digit', month: 'short' }); - if (bucketDay.getTime() === today.getTime()) { + const dateStr = d.toLocaleDateString('de-DE', { day: '2-digit', month: 'short', timeZone: TIMEZONE }); + if (dayKey === todayKey) { label = 'Heute, ' + dateStr; - } else if (bucketDay.getTime() === yesterday.getTime()) { + } else if (dayKey === yesterdayKey) { label = 'Gestern, ' + dateStr; } else { - label = d.toLocaleDateString('de-DE', { weekday: 'short', day: '2-digit', month: 'short' }); + label = d.toLocaleDateString('de-DE', { weekday: 'short', day: '2-digit', month: 'short', timeZone: TIMEZONE }); } const pos = this._bucketPositionPercent(b, rangeStart, rangeEnd, buckets.length); markers.push({ text: label, pos }); @@ -1202,9 +1219,10 @@ const App = { * Einträge nach Zeitperiode gruppieren. */ _groupByTimePeriod(entries, granularity) { - const now = new Date(); - const today = new Date(now.getFullYear(), now.getMonth(), now.getDate()); - const yesterday = new Date(today.getTime() - 86400000); + const np = _tz(new Date()); + const todayKey = `${np.year}-${np.month}-${np.date}`; + const yp = _tz(new Date(Date.now() - 86400000)); + const yesterdayKey = `${yp.year}-${yp.month}-${yp.date}`; const groups = []; let currentGroup = null; @@ -1217,18 +1235,18 @@ const App = { key = 'unknown'; label = 'Unbekannt'; } else if (granularity === 'hour') { - const h = d.getHours(); - key = `${d.toDateString()}-${h}`; - label = `${h.toString().padStart(2, '0')}:00 Uhr`; + const ep = _tz(d); + key = `${ep.year}-${ep.month}-${ep.date}-${ep.hours}`; + label = `${ep.hours.toString().padStart(2, '0')}:00 Uhr`; } else { - const entryDate = new Date(d.getFullYear(), d.getMonth(), d.getDate()); - key = entryDate.toDateString(); - if (entryDate.getTime() === today.getTime()) { + const ep = _tz(d); + key = `${ep.year}-${ep.month}-${ep.date}`; + if (key === todayKey) { label = 'Heute'; - } else if (entryDate.getTime() === yesterday.getTime()) { + } else if (key === yesterdayKey) { label = 'Gestern'; } else { - label = d.toLocaleDateString('de-DE', { weekday: 'short', day: 'numeric', month: 'short' }); + label = d.toLocaleDateString('de-DE', { weekday: 'short', day: 'numeric', month: 'short', timeZone: TIMEZONE }); } } @@ -1280,7 +1298,7 @@ const App = { const dateField = (type === 'research' && article.published_at) ? article.published_at : article.collected_at; const time = dateField - ? new Date(dateField).toLocaleTimeString('de-DE', { hour: '2-digit', minute: '2-digit' }) + ? (parseUTC(dateField) || new Date(dateField)).toLocaleTimeString('de-DE', { hour: '2-digit', minute: '2-digit', timeZone: TIMEZONE }) : '--:--'; const headline = article.headline_de || article.headline; @@ -1322,7 +1340,7 @@ const App = { */ _renderSnapshotEntry(snapshot) { const time = snapshot.created_at - ? new Date(snapshot.created_at).toLocaleTimeString('de-DE', { hour: '2-digit', minute: '2-digit' }) + ? (parseUTC(snapshot.created_at) || new Date(snapshot.created_at)).toLocaleTimeString('de-DE', { hour: '2-digit', minute: '2-digit', timeZone: TIMEZONE }) : '--:--'; const stats = []; @@ -1379,7 +1397,8 @@ const App = { _getMinuteKey(timestamp) { if (!timestamp) return 'none'; const d = new Date(timestamp); - return `${d.getFullYear()}-${d.getMonth()}-${d.getDate()}-${d.getHours()}-${d.getMinutes()}`; + const p = _tz(d); + return `${p.year}-${p.month}-${p.date}-${p.hours}-${p.minutes}`; }, // === Event Handlers === @@ -1659,9 +1678,9 @@ const App = { } list.innerHTML = logs.map(log => { - const started = new Date(log.started_at); - const timeStr = started.toLocaleDateString('de-DE', { day: '2-digit', month: '2-digit' }) + ' ' + - started.toLocaleTimeString('de-DE', { hour: '2-digit', minute: '2-digit' }); + const started = parseUTC(log.started_at) || new Date(log.started_at); + const timeStr = started.toLocaleDateString('de-DE', { day: '2-digit', month: '2-digit', timeZone: TIMEZONE }) + ' ' + + started.toLocaleTimeString('de-DE', { hour: '2-digit', minute: '2-digit', timeZone: TIMEZONE }); let detail = ''; if (log.status === 'completed') { @@ -2967,7 +2986,7 @@ function buildDetailedSourceOverview() { data.articles.forEach(a => { const headline = UI.escape(a.headline_de || a.headline || 'Ohne Titel'); const time = a.collected_at - ? new Date(a.collected_at).toLocaleString('de-DE', { day: '2-digit', month: '2-digit', hour: '2-digit', minute: '2-digit' }) + ? (parseUTC(a.collected_at) || new Date(a.collected_at)).toLocaleString('de-DE', { day: '2-digit', month: '2-digit', hour: '2-digit', minute: '2-digit', timeZone: TIMEZONE }) : ''; const langBadge = a.language && a.language !== 'de' ? `${a.language.toUpperCase()}` : '';