Dateien
Website/lagebild/lagebild.js
Claude Code 8d432e5ec9 Lagebild: Zitate verlinken auf Quell-URL, Quellenverzeichnis kompakter, Em-Dashes entfernt
- Zitat-Refs zeigen Quellenname im Tooltip, oeffnen URL in neuem Tab
- Quellenverzeichnis: kompaktes 2-Spalten-Layout, nur Name als Link
- Alle Em-Dashes durch Bindestriche ersetzt
- Stand im Hero zeigt jetzt auch Uhrzeit

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-07 15:08:39 +01:00

724 Zeilen
35 KiB
JavaScript

/**
* AegisSight Lagebild Page - Dark Theme Design Refresh
* Count-Up, Timeline mit Dropdown, Scroll-Reveal, Smooth-Scroll, Favicons
*/
/** Feste Zeitzone fuer alle Anzeigen - NIEMALS aendern. */
var TIMEZONE = 'Europe/Berlin';
var Lagebild = {
data: null,
allSnapshots: {},
currentView: null,
map: null,
timelineGroups: null,
/* ===== Inline SVG Icons ===== */
icons: {
clock: '<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><polyline points="12 6 12 12 16 14"/></svg>',
fileText: '<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><polyline points="14 2 14 8 20 8"/><line x1="16" y1="13" x2="8" y2="13"/><line x1="16" y1="17" x2="8" y2="17"/></svg>',
globe: '<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><line x1="2" y1="12" x2="22" y2="12"/><path d="M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10 15.3 15.3 0 0 1 4-10z"/></svg>',
shieldCheck: '<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"/><polyline points="9 12 11 14 15 10"/></svg>',
externalLink: '<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6"/><polyline points="15 3 21 3 21 9"/><line x1="10" y1="14" x2="21" y2="3"/></svg>'
},
async init() {
if (typeof initTranslations === 'function') {
try { initTranslations(); } catch(e) {}
}
try {
var resp = await fetch('data/current.json?t=' + Date.now());
if (!resp.ok) throw new Error('HTTP ' + resp.status);
this.data = await resp.json();
this.currentView = {
summary: this.data.current_lagebild.summary_markdown,
sources_json: this.data.current_lagebild.sources_json,
updated_at: this.data.current_lagebild.updated_at || this.data.generated_at,
articles: this.data.articles,
fact_checks: this.data.fact_checks
};
this.render();
this.initTabs();
this.initLangToggle();
this.initScrollReveal();
} catch (e) {
console.error('Lagebild laden fehlgeschlagen:', e);
this.showError();
}
},
render: function() {
this.renderHero();
this.renderTimeline();
this.renderTabBadges();
this.renderCurrentView();
},
/* ===== HERO ===== */
renderHero: function() {
var d = this.data;
document.getElementById('incident-title').innerHTML =
this.esc(this.fixUmlauts(d.incident.title)) +
' <span class="hero-date-info">- Stand: ' + this.fmtDateOnly(d.generated_at) + ', ' + this.fmtTimeOnly(d.generated_at) + ' Uhr</span>';
// Stat Cards (3: Artikel, Quellen, Faktenchecks)
var statsHtml = '';
statsHtml += this.statCard(this.icons.fileText, '<span class="count-up" data-target="' + d.incident.article_count + '">0</span>', 'Artikel');
statsHtml += this.statCard(this.icons.globe, '<span class="count-up" data-target="' + d.incident.source_count + '">0</span>', 'Quellen');
statsHtml += this.statCard(this.icons.shieldCheck, '<span class="count-up" data-target="' + d.incident.factcheck_count + '">0</span>', 'Faktenchecks');
document.getElementById('hero-stats').innerHTML = statsHtml;
// Start count-up animations
var self = this;
requestAnimationFrame(function() {
var els = document.querySelectorAll('.count-up');
for (var i = 0; i < els.length; i++) {
self.animateCount(els[i], parseInt(els[i].getAttribute('data-target')), 800);
}
});
},
statCard: function(icon, value, label) {
return '<div class="stat-card">' +
'<div class="stat-card-icon">' + icon + '</div>' +
'<div class="stat-card-content">' +
'<span class="stat-card-value">' + value + '</span>' +
'<span class="stat-card-label">' + label + '</span>' +
'</div></div>';
},
/* ===== COUNT-UP ANIMATION ===== */
animateCount: function(element, target, duration) {
var start = performance.now();
function update(now) {
var elapsed = now - start;
var progress = Math.min(elapsed / duration, 1);
var eased = 1 - Math.pow(1 - progress, 3); // easeOutCubic
var current = Math.round(target * eased);
element.textContent = current.toLocaleString('de-DE');
if (progress < 1) {
requestAnimationFrame(update);
}
}
requestAnimationFrame(update);
},
/* ===== TIMELINE STRIP ===== */
renderTimeline: function() {
var snaps = this.data.available_snapshots || [];
var current = {
id: 'current',
article_count: this.data.incident.article_count,
fact_check_count: this.data.incident.factcheck_count,
created_at: this.data.generated_at
};
var all = [current].concat(snaps);
// Group by date
var groups = {};
for (var i = 0; i < all.length; i++) {
var s = all[i];
var dateKey = this.toDateKey(s.created_at);
if (!groups[dateKey]) groups[dateKey] = [];
groups[dateKey].push(s);
}
// Sort each group descending (newest first)
for (var dk in groups) {
groups[dk].sort(function(a, b) {
return new Date(Lagebild.toUTC(b.created_at)) - new Date(Lagebild.toUTC(a.created_at));
});
}
this.timelineGroups = groups;
var dates = Object.keys(groups).sort();
var strip = document.getElementById('timeline-strip');
var h = '';
for (var j = 0; j < dates.length; j++) {
var date = dates[j];
var daySnaps = groups[date];
var latest = daySnaps[0];
var isActive = (j === dates.length - 1);
var d = new Date(date + 'T12:00:00Z');
h += '<button class="timeline-day' + (isActive ? ' active' : '') + '"';
h += ' data-date="' + date + '"';
h += ' data-snapshot-id="' + latest.id + '"';
if (daySnaps.length > 1) {
h += ' title="' + daySnaps.length + ' Updates an diesem Tag"';
}
h += '>';
if (isActive) h += '<span class="timeline-dot"></span>';
h += '<span class="timeline-day-num">' + d.getUTCDate() + '</span>';
h += '<span class="timeline-day-month">' + d.toLocaleDateString('de-DE', { month: 'short', timeZone: 'UTC' }) + '</span>';
h += '<span class="timeline-day-count">' + latest.article_count + '</span>';
if (daySnaps.length > 1) {
h += '<span class="timeline-day-updates">' + daySnaps.length + 'x</span>';
}
if (isActive) {
h += '<span class="timeline-day-label">Aktuell</span>';
}
h += '</button>';
}
strip.innerHTML = h;
// Scroll to active day
var active = strip.querySelector('.timeline-day.active');
if (active) {
setTimeout(function() {
active.scrollIntoView({ behavior: 'smooth', inline: 'center', block: 'nearest' });
}, 150);
}
// Click handler for day buttons
var self = this;
strip.addEventListener('click', function(e) {
var btn = e.target.closest('.timeline-day');
if (!btn) return;
var allDays = strip.querySelectorAll('.timeline-day');
for (var k = 0; k < allDays.length; k++) allDays[k].classList.remove('active');
btn.classList.add('active');
var dateKey = btn.getAttribute('data-date');
var snapId = btn.getAttribute('data-snapshot-id');
// Show dropdown for this day
self.showTimelineDropdown(dateKey, snapId);
// Load latest snapshot
if (snapId === 'current') {
self.currentView = {
summary: self.data.current_lagebild.summary_markdown,
sources_json: self.data.current_lagebild.sources_json,
updated_at: self.data.current_lagebild.updated_at || self.data.generated_at,
articles: self.data.articles,
fact_checks: self.data.fact_checks
};
self.renderCurrentView();
} else {
self.loadSnapshot(parseInt(snapId));
}
});
// Click handler for dropdown snapshot items (delegated, set up once)
var dropdown = document.getElementById('timeline-dropdown');
dropdown.addEventListener('click', function(e) {
var item = e.target.closest('.timeline-snap-item');
if (!item) return;
var items = dropdown.querySelectorAll('.timeline-snap-item');
for (var k = 0; k < items.length; k++) items[k].classList.remove('active');
item.classList.add('active');
var snapId = item.getAttribute('data-snapshot-id');
if (snapId === 'current') {
self.currentView = {
summary: self.data.current_lagebild.summary_markdown,
sources_json: self.data.current_lagebild.sources_json,
updated_at: self.data.current_lagebild.updated_at || self.data.generated_at,
articles: self.data.articles,
fact_checks: self.data.fact_checks
};
self.renderCurrentView();
} else {
self.loadSnapshot(parseInt(snapId));
}
});
// Show dropdown for newest day by default
var newestDate = dates[dates.length - 1];
if (newestDate && groups[newestDate].length > 1) {
this.showTimelineDropdown(newestDate, groups[newestDate][0].id);
}
},
showTimelineDropdown: function(dateKey, activeSnapId) {
var dropdown = document.getElementById('timeline-dropdown');
var snaps = this.timelineGroups[dateKey];
if (!snaps || snaps.length <= 1) {
dropdown.classList.remove('open');
dropdown.innerHTML = '';
return;
}
var d = new Date(dateKey + 'T12:00:00Z');
var dateLabel = d.toLocaleDateString('de-DE', { day: 'numeric', month: 'long', year: 'numeric', timeZone: 'UTC' });
var h = '<div class="timeline-dropdown-header">' + dateLabel + ' - ' + snaps.length + ' Updates</div>';
h += '<div class="timeline-snap-list">';
for (var i = 0; i < snaps.length; i++) {
var snap = snaps[i];
var isActive = (String(snap.id) === String(activeSnapId));
h += '<button class="timeline-snap-item' + (isActive ? ' active' : '') + '"';
h += ' data-snapshot-id="' + snap.id + '">';
h += '<span class="timeline-snap-time">' + this.fmtTimeOnly(snap.created_at) + ' Uhr</span>';
h += '<span class="timeline-snap-meta">' + snap.article_count + ' Artikel, ' + snap.fact_check_count + ' FC</span>';
h += '</button>';
}
h += '</div>';
dropdown.innerHTML = h;
dropdown.classList.add('open');
},
toDateKey: function(iso) {
if (!iso) return '';
var d = new Date(this.toUTC(iso));
return d.toLocaleDateString('en-CA', { timeZone: TIMEZONE });
},
/* ===== TAB BADGES ===== */
renderTabBadges: function() {
var quellenBadge = document.getElementById('tab-badge-quellen');
var fcBadge = document.getElementById('tab-badge-faktenchecks');
if (quellenBadge) quellenBadge.textContent = this.data.incident.source_count;
if (fcBadge) fcBadge.textContent = this.data.incident.factcheck_count;
},
/* ===== SNAPSHOT LOADING ===== */
loadSnapshot: async function(id) {
if (this.allSnapshots[id]) {
this.currentView = this.allSnapshots[id];
this.renderCurrentView();
return;
}
try {
var resp = await fetch('data/snapshot-' + id + '.json');
if (!resp.ok) throw new Error('HTTP ' + resp.status);
var sd = await resp.json();
var sj = sd.sources_json;
if (typeof sj === 'string') { try { sj = JSON.parse(sj); } catch(e) { sj = []; } }
this.currentView = {
summary: sd.summary,
sources_json: sj || [],
updated_at: sd.created_at,
articles: this.data.articles,
fact_checks: this.data.fact_checks
};
this.allSnapshots[id] = this.currentView;
this.renderCurrentView();
} catch (e) { console.error('Snapshot Fehler:', e); }
},
renderCurrentView: function() {
this.renderSummary();
this.renderInlineSources();
this.renderSourcesTab();
this.renderArticlesTab();
this.renderFactChecksTab();
if (document.getElementById('panel-karte').classList.contains('active')) {
this.renderMap();
}
},
/* ===== TAB: LAGEBILD ===== */
renderSummary: function() {
var v = this.currentView;
document.getElementById('lagebild-timestamp').textContent = this.fmtDT(v.updated_at);
var md = this.fixUmlauts(v.summary || '');
var html = this.mdToHtml(md);
// Build source lookup for citation links
var srcMap = {};
var sources = v.sources_json || [];
for (var i = 0; i < sources.length; i++) {
srcMap[sources[i].nr] = sources[i];
}
var self = this;
html = html.replace(/\[(\d+)\]/g, function(match, nr) {
var src = srcMap[nr];
if (src && src.url) {
return '<a class="citation-ref" href="' + self.esc(src.url) + '" target="_blank" rel="noopener" title="' + self.esc(src.name || '') + '">[' + nr + ']</a>';
}
return '<a class="citation-ref" title="Quelle ' + nr + '">[' + nr + ']</a>';
});
document.getElementById('summary-content').innerHTML = html;
},
renderInlineSources: function() {
var sources = this.currentView.sources_json || [];
if (!sources.length) { document.getElementById('inline-sources').innerHTML = ''; return; }
var h = '<h3 class="inline-sources-title">Quellenverzeichnis</h3>';
h += '<ul class="inline-sources-list">';
for (var i = 0; i < sources.length; i++) {
var s = sources[i];
h += '<li id="src-' + s.nr + '"><span class="src-nr">[' + s.nr + ']</span>';
if (s.url) {
h += '<a href="' + this.esc(s.url) + '" target="_blank" rel="noopener">' + this.esc(s.name || '') + '</a>';
} else {
h += '<span>' + this.esc(s.name || '') + '</span>';
}
h += '</li>';
}
h += '</ul>';
document.getElementById('inline-sources').innerHTML = h;
},
/* ===== TAB: QUELLEN ===== */
renderSourcesTab: function() {
var sources = this.currentView.sources_json || [];
var articles = this.currentView.articles || [];
var h = '';
h += '<div class="sources-section"><h3>Im Lagebild zitierte Quellen (' + sources.length + ')</h3>';
if (sources.length) {
h += '<ol class="sources-list">';
for (var i = 0; i < sources.length; i++) {
var s = sources[i];
if (s.url) {
h += '<li><a href="' + this.esc(s.url) + '" target="_blank" rel="noopener">' + this.esc(s.name || '') + '</a></li>';
} else {
h += '<li><span class="source-name">' + this.esc(s.name || '') + '</span></li>';
}
}
h += '</ol>';
}
h += '</div>';
document.getElementById('sources-cited').innerHTML = h;
document.getElementById('articles-heading').textContent = 'Alle gesammelten Artikel (' + articles.length + ')';
var ah = '';
for (var i = 0; i < articles.length; i++) {
var a = articles[i];
var dt = a.published_at || a.collected_at || '';
var dObj = dt ? new Date(this.toUTC(dt)) : null;
var hl = this.fixUmlauts(a.headline_de || a.headline || '');
var domain = this.extractDomain(a.source_url);
ah += '<div class="article-card">';
ah += '<div class="article-date">';
if (dObj && !isNaN(dObj.getTime())) {
ah += '<span class="day">' + dObj.toLocaleDateString('de-DE', { day: 'numeric', timeZone: TIMEZONE }) + '</span>';
ah += '<span class="month">' + dObj.toLocaleDateString('de-DE', { month: 'short', timeZone: TIMEZONE }) + '</span>';
}
ah += '</div><div class="article-info">';
ah += '<p class="article-headline">';
if (a.source_url) ah += '<a href="' + this.esc(a.source_url) + '" target="_blank" rel="noopener">';
ah += this.esc(hl);
if (a.source_url) ah += '</a>';
ah += '</p><div class="article-meta">';
ah += '<span class="article-source">';
if (domain) ah += '<img class="article-favicon" src="https://www.google.com/s2/favicons?domain=' + encodeURIComponent(domain) + '&sz=16" width="16" height="16" alt="" loading="lazy"> ';
ah += this.esc(a.source || '') + '</span>';
if (a.language) ah += '<span class="article-lang">' + a.language.toUpperCase() + '</span>';
if (dObj && !isNaN(dObj.getTime())) {
ah += '<span>' + dObj.toLocaleDateString('de-DE', { day: '2-digit', month: '2-digit', year: 'numeric', timeZone: TIMEZONE }) + ', ' + dObj.toLocaleTimeString('de-DE', { hour: '2-digit', minute: '2-digit', timeZone: TIMEZONE }) + ' Uhr</span>';
}
if (a.source_url) ah += '<a class="article-link" href="' + this.esc(a.source_url) + '" target="_blank" rel="noopener">Artikel lesen ' + this.icons.externalLink + '</a>';
ah += '</div></div></div>';
}
document.getElementById('articles-content').innerHTML = ah;
},
renderArticlesTab: function() {},
/* ===== TAB: KARTE ===== */
renderMap: function() {
if (this.map) { this.map.remove(); this.map = null; }
this.map = L.map('map-container').setView([33.0, 48.0], 5);
// Dark map tiles (CartoDB Dark Matter)
L.tileLayer('https://{s}.basemaps.cartocdn.com/dark_all/{z}/{x}/{y}{r}.png', {
attribution: '&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> &copy; <a href="https://carto.com/">CARTO</a>',
maxZoom: 19,
subdomains: 'abcd'
}).addTo(this.map);
var redIcon = L.icon({
iconUrl: 'https://raw.githubusercontent.com/pointhi/leaflet-color-markers/master/img/marker-icon-2x-red.png',
shadowUrl: 'https://cdnjs.cloudflare.com/ajax/libs/leaflet/1.9.4/images/marker-shadow.png',
iconSize: [25, 41], iconAnchor: [12, 41], popupAnchor: [1, -34], shadowSize: [41, 41]
});
var blueIcon = L.icon({
iconUrl: 'https://raw.githubusercontent.com/pointhi/leaflet-color-markers/master/img/marker-icon-2x-blue.png',
shadowUrl: 'https://cdnjs.cloudflare.com/ajax/libs/leaflet/1.9.4/images/marker-shadow.png',
iconSize: [25, 41], iconAnchor: [12, 41], popupAnchor: [1, -34], shadowSize: [41, 41]
});
var orangeIcon = L.icon({
iconUrl: 'https://raw.githubusercontent.com/pointhi/leaflet-color-markers/master/img/marker-icon-2x-orange.png',
shadowUrl: 'https://cdnjs.cloudflare.com/ajax/libs/leaflet/1.9.4/images/marker-shadow.png',
iconSize: [25, 41], iconAnchor: [12, 41], popupAnchor: [1, -34], shadowSize: [41, 41]
});
var locs = [
{n:'Teheran, Iran',lat:35.6892,lng:51.3890,d:'Hauptziel der US-israelischen Luftschläge. Über 1.000 Tote nach fünf Tagen Krieg.',ic:redIcon},
{n:'Beirut, Libanon',lat:33.8938,lng:35.5018,d:'Gleichzeitige US-israelische Luftschläge auf Beirut und Teheran [6].',ic:redIcon},
{n:'Juffair, Bahrain',lat:26.2235,lng:50.6001,d:'US-Marinebasis - Ziel iranischer Vergeltungsraketen [3].',ic:orangeIcon},
{n:'Al Udeid, Katar',lat:25.1173,lng:51.3150,d:'US-Luftwaffenstützpunkt - Ziel iranischer Gegenangriffe.',ic:orangeIcon},
{n:'Tel Aviv, Israel',lat:32.0853,lng:34.7818,d:'Operationsbasis für israelische Angriffe auf den Iran.',ic:blueIcon},
{n:'Ankara, Türkei',lat:39.9334,lng:32.8597,d:'NATO vermutet iranischen Raketenbeschuss auf Türkei [10]. Keine NATO-Beteiligung geplant.',ic:orangeIcon},
{n:'Bagdad, Irak',lat:33.3152,lng:44.3661,d:'Lage im Irak als Faktor im Kriegsverlauf [2].',ic:blueIcon},
{n:'Persischer Golf',lat:27.0,lng:51.5,d:'Iran greift Energieinfrastruktur und diplomatische Einrichtungen in der Golfregion an.',ic:orangeIcon},
{n:'Dubai, VAE',lat:25.2048,lng:55.2708,d:'US-Botschaft in Dubai durch iranischen Angriff getroffen.',ic:redIcon},
{n:'Washington D.C., USA',lat:38.9072,lng:-77.0369,d:'War-Powers-Abstimmung im Senat gescheitert (47:53). Trump verteidigt Iran-Krieg vor Kongress.',ic:blueIcon}
];
for (var i = 0; i < locs.length; i++) {
var l = locs[i];
L.marker([l.lat, l.lng], { icon: l.ic })
.addTo(this.map)
.bindPopup('<strong style="color:#E8ECF4;">' + l.n + '</strong><br><span style="font-size:0.85rem;color:#8896AB;">' + l.d + '</span>');
}
// Dark legend
var legend = L.control({ position: 'bottomright' });
legend.onAdd = function() {
var div = L.DomUtil.create('div', 'map-legend');
div.style.cssText = 'background:#151D2E;padding:10px 14px;border-radius:4px;border:1px solid #1E2D45;box-shadow:0 2px 8px rgba(0,0,0,0.3);font-size:0.8rem;line-height:1.8;color:#E8ECF4;';
div.innerHTML = '<strong style="color:#C8A851;">Legende</strong><br>'
+ '<span style="color:#cb2b3e;">&#9679;</span> Angegriffene Ziele<br>'
+ '<span style="color:#f39c12;">&#9679;</span> Vergeltung / Eskalation<br>'
+ '<span style="color:#2a81cb;">&#9679;</span> Strategische Akteure';
return div;
};
legend.addTo(this.map);
// Dark popup styling
if (!document.getElementById('leaflet-dark-style')) {
var style = document.createElement('style');
style.id = 'leaflet-dark-style';
style.textContent = '.lagebild-page .leaflet-popup-content-wrapper{background:#151D2E;color:#E8ECF4;border:1px solid #1E2D45;border-radius:4px;box-shadow:0 4px 16px rgba(0,0,0,0.4);}.lagebild-page .leaflet-popup-tip{background:#151D2E;}';
document.head.appendChild(style);
}
setTimeout(function() { if (Lagebild.map) Lagebild.map.invalidateSize(); }, 300);
},
/* ===== TAB: FAKTENCHECKS ===== */
renderFactChecksTab: function() {
var checks = this.currentView.fact_checks || [];
if (!checks.length) {
document.getElementById('factchecks-content').innerHTML = '<p style="color:#8896AB">Keine Faktenchecks verfügbar.</p>';
return;
}
var stats = { confirmed: 0, unconfirmed: 0, contradicted: 0, developing: 0, established: 0, disputed: 0 };
for (var k = 0; k < checks.length; k++) {
var st = checks[k].status || 'developing';
if (stats[st] !== undefined) stats[st]++;
}
var h = '<div class="fc-stats">';
h += '<div class="fc-stat"><span class="fc-stat-num">' + checks.length + '</span><span class="fc-stat-label">Gesamt</span></div>';
h += '<div class="fc-stat confirmed"><span class="fc-stat-num">' + (stats.confirmed + stats.established) + '</span><span class="fc-stat-label">Bestätigt</span></div>';
h += '<div class="fc-stat unconfirmed"><span class="fc-stat-num">' + (stats.unconfirmed + stats.developing) + '</span><span class="fc-stat-label">Offen</span></div>';
if (stats.contradicted + stats.disputed > 0)
h += '<div class="fc-stat contradicted"><span class="fc-stat-num">' + (stats.contradicted + stats.disputed) + '</span><span class="fc-stat-label">Widerlegt</span></div>';
h += '</div>';
checks = checks.slice().sort(function(a, b) {
var aH = (a.status_history || []).length;
var bH = (b.status_history || []).length;
if (bH !== aH) return bH - aH;
return (b.sources_count || 0) - (a.sources_count || 0);
});
for (var i = 0; i < checks.length; i++) {
var fc = checks[i];
var status = fc.status || 'developing';
var hasProg = fc.status_history && fc.status_history.length > 1;
h += '<div class="factcheck-card' + (hasProg ? ' highlight' : '') + '" data-status="' + status + '">';
h += '<div class="factcheck-header">';
h += '<span class="status-badge ' + status + '">' + this.stLabel(status) + '</span>';
h += '<span class="factcheck-sources">' + (fc.sources_count || 0) + ' unabhängige Quellen</span>';
h += '</div>';
h += '<p class="factcheck-claim">' + this.esc(this.fixUmlauts(fc.claim || '')) + '</p>';
if (fc.evidence) {
var ev = this.fixUmlauts(fc.evidence);
ev = this.esc(ev).replace(/(https?:\/\/[^\s,)]+)/g, '<a href="$1" target="_blank" rel="noopener">$1</a>');
h += '<div class="factcheck-evidence"><strong>Evidenz:</strong> ' + ev + '</div>';
}
if (hasProg) {
h += '<div class="status-progression">';
h += '<span class="progression-label">Verlauf:</span>';
for (var j = 0; j < fc.status_history.length; j++) {
var step = fc.status_history[j];
if (j > 0) h += '<span class="progression-arrow">&rarr;</span>';
h += '<span class="progression-step">';
h += '<span class="status-badge ' + step.status + '">' + this.stLabel(step.status) + '</span>';
if (step.at) h += '<span class="progression-time">' + this.fmtShort(step.at) + '</span>';
h += '</span>';
}
h += '</div>';
}
h += '</div>';
}
document.getElementById('factchecks-content').innerHTML = h;
},
/* ===== TABS ===== */
initTabs: function() {
var btns = document.querySelectorAll('.tab-btn');
var self = this;
for (var i = 0; i < btns.length; i++) {
btns[i].addEventListener('click', function() {
var tab = this.getAttribute('data-tab');
for (var j = 0; j < btns.length; j++) btns[j].classList.remove('active');
this.classList.add('active');
var panels = document.querySelectorAll('.tab-panel');
for (var j = 0; j < panels.length; j++) panels[j].classList.remove('active');
var activePanel = document.getElementById('panel-' + tab);
activePanel.classList.add('active');
// Trigger reveal for cards in newly active panel
var revealCards = activePanel.querySelectorAll('.reveal:not(.revealed)');
for (var k = 0; k < revealCards.length; k++) {
revealCards[k].classList.add('revealed');
}
if (tab === 'karte') self.renderMap();
});
}
},
initLangToggle: function() {
var btn = document.querySelector('.lang-toggle');
if (!btn) return;
btn.addEventListener('click', function(e) {
e.preventDefault();
if (typeof switchLanguage === 'function') {
var cur = (typeof getCurrentLanguage === 'function') ? getCurrentLanguage() : 'de';
switchLanguage(cur === 'de' ? 'en' : 'de');
}
});
},
/* ===== SCROLL REVEAL ===== */
initScrollReveal: function() {
var cards = document.querySelectorAll('.content-card, .lagebild-cta');
if (!('IntersectionObserver' in window)) {
for (var i = 0; i < cards.length; i++) cards[i].classList.add('revealed');
return;
}
var observer = new IntersectionObserver(function(entries) {
entries.forEach(function(entry) {
if (entry.isIntersecting) {
entry.target.classList.add('revealed');
observer.unobserve(entry.target);
}
});
}, { threshold: 0.1 });
for (var i = 0; i < cards.length; i++) {
cards[i].classList.add('reveal');
// Immediately reveal cards in the active (visible) tab panel
var panel = cards[i].closest('.tab-panel');
if (!panel || panel.classList.contains('active')) {
cards[i].classList.add('revealed');
} else {
observer.observe(cards[i]);
}
}
},
/* ===== HILFSFUNKTIONEN ===== */
extractDomain: function(url) {
if (!url) return null;
try { return new URL(url).hostname; } catch(e) { return null; }
},
fixUmlauts: function(text) {
if (!text) return text;
var skip = ['Israel','Israelis','Jazeera','Euronews','Reuters','Februar',
'Juffair','abgefeuert','Feindseligkeiten','Gegenschlag','neuesten',
'auszuweiten','befeuert','feuerte','Feuer','feuer','neue','neuen',
'neuer','neues','Neue','Aero','aero','Manoeuvre','Dauerfeuer'];
var ph = []; var c = 0;
for (var i = 0; i < skip.length; i++) {
var re = new RegExp('\\b' + skip[i] + '\\b', 'g');
text = text.replace(re, function(m) { ph.push(m); return '##S' + (c++) + '##'; });
}
text = text.replace(/ae/g, '\u00e4').replace(/Ae/g, '\u00c4');
text = text.replace(/oe/g, '\u00f6').replace(/Oe/g, '\u00d6');
text = text.replace(/ue/g, '\u00fc').replace(/Ue/g, '\u00dc');
text = text.replace(/##S(\d+)##/g, function(m, idx) { return ph[parseInt(idx)]; });
return text;
},
stLabel: function(s) {
return { confirmed: 'Bestätigt', unconfirmed: 'Unbestätigt', established: 'Gesichert',
unverified: 'Nicht verifiziert', contradicted: 'Widerlegt', disputed: 'Umstritten',
developing: 'In Entwicklung', 'false': 'Falsch' }[s] || s;
},
mdToHtml: function(md) {
if (!md) return '';
var lines = md.split('\n'), html = '', inList = false;
for (var i = 0; i < lines.length; i++) {
var l = lines[i];
if (/^### (.+)$/.test(l)) { if (inList) { html += '</ul>'; inList = false; } html += '<h3>' + l.replace(/^### /, '') + '</h3>'; continue; }
if (/^## (.+)$/.test(l)) { if (inList) { html += '</ul>'; inList = false; } html += '<h2>' + l.replace(/^## /, '') + '</h2>'; continue; }
if (/^[-*] (.+)$/.test(l)) { if (!inList) { html += '<ul>'; inList = true; } html += '<li>' + l.replace(/^[-*] /, '') + '</li>'; continue; }
if (inList) { html += '</ul>'; inList = false; }
if (l.trim() === '') continue;
html += '<p>' + l + '</p>';
}
if (inList) html += '</ul>';
html = html.replace(/\*\*(.+?)\*\*/g, '<strong>$1</strong>');
html = html.replace(/\*(.+?)\*/g, '<em>$1</em>');
return html;
},
esc: function(s) { if (!s) return ''; var d = document.createElement('div'); d.textContent = s; return d.innerHTML; },
toUTC: function(s) {
if (!s) return s;
s = String(s).trim();
if (/[Zz]$/.test(s) || /[+-]\d{2}:?\d{2}$/.test(s)) return s;
return s.replace(' ', 'T') + 'Z';
},
fmtDT: function(iso) {
if (!iso) return '';
try {
var d = new Date(this.toUTC(iso));
if (isNaN(d.getTime())) return iso;
var opts = { timeZone: TIMEZONE, weekday: 'long', day: 'numeric', month: 'long', year: 'numeric', hour: '2-digit', minute: '2-digit', hour12: false };
var parts = new Intl.DateTimeFormat('de-DE', opts).formatToParts(d);
var p = {};
parts.forEach(function(x) { p[x.type] = x.value; });
return p.weekday + ', ' + p.day + '. ' + p.month + ' ' + p.year
+ ' um ' + p.hour + ':' + p.minute + ' Uhr';
} catch(e) { return iso; }
},
fmtDateOnly: function(iso) {
if (!iso) return '';
try {
var d = new Date(this.toUTC(iso));
if (isNaN(d.getTime())) return iso;
return d.toLocaleDateString('de-DE', { day: 'numeric', month: 'short', year: 'numeric', timeZone: TIMEZONE });
} catch(e) { return iso; }
},
fmtTimeOnly: function(iso) {
if (!iso) return '';
try {
var d = new Date(this.toUTC(iso));
if (isNaN(d.getTime())) return iso;
return d.toLocaleTimeString('de-DE', { hour: '2-digit', minute: '2-digit', timeZone: TIMEZONE });
} catch(e) { return iso; }
},
fmtShort: function(iso) {
if (!iso) return '';
try { return new Date(this.toUTC(iso)).toLocaleDateString('de-DE', { day: 'numeric', month: 'short', hour: '2-digit', minute: '2-digit', timeZone: TIMEZONE }); }
catch(e) { return iso; }
},
showError: function() {
document.getElementById('summary-content').innerHTML =
'<div class="lagebild-error"><p>Das Lagebild konnte nicht geladen werden. Bitte versuchen Sie es später erneut.</p></div>';
}
};
document.addEventListener('DOMContentLoaded', function() { Lagebild.init(); });