';
} else {
var md = (this.currentView && this.currentView.summary) || '';
var zf = this.extractZusammenfassung(md);
if (!zf) {
el.innerHTML = '
Keine Zusammenfassung verfügbar.
';
return;
}
var body = this.mdToHtml(this.fixUmlauts(zf));
var srcMap = {};
var sources = (this.currentView && this.currentView.sources_json) || [];
for (var i = 0; i < sources.length; i++) srcMap[String(sources[i].nr)] = sources[i];
var self = this;
body = body.replace(/\[(\d+[a-z]?)\]/g, function(match, nr) {
var src = srcMap[nr];
if (src && src.url) {
return '[' + nr + ']';
}
return '[' + nr + ']';
});
el.innerHTML = body;
}
},
extractZusammenfassung: function(md) {
if (!md) return '';
var sections = md.split(/^## /m);
for (var i = 0; i < sections.length; i++) {
var s = sections[i];
if (/^zusammenfassung/i.test(s.trim())) {
var next = s.split(/\n## /)[0];
return next.trim();
}
}
return '';
},
renderLatestDevelopmentsHtml: function(text, sources) {
if (!text) return '';
sources = Array.isArray(sources) ? sources : [];
var self = this;
var lines = text.split('\n').map(function(l){return l.trim();})
.filter(function(l){return l && (l.charAt(0) === '-' || l.charAt(0) === '[');});
if (!lines.length) return '';
var bulletRe = /^(?:-\s*)?\[\s*(\d{1,2})\.(\d{1,2})\.?(?:\d{2,4})?\s+(\d{1,2}:\d{2})\s*\]\s*(.+?)\s*$/;
var trailingRe = /\s*\{([^{}]+)\}\s*\.?\s*$/;
var citationRe = /\[(\d+[a-z]?)\]/g;
var junkRe = /^(unbekannt|unknown|n\/?a|keine|keine quelle|tba)$/i;
function normName(s) { return String(s||'').toLowerCase().replace(/^(der|die|das)\s+/,'').replace(/\s+/g,' ').trim(); }
function lookupByName(name) {
var n = normName(name); if (!n) return null;
var exact = sources.find(function(s){ return normName(s.name) === n; });
if (exact) return exact;
return sources.find(function(s){ var sn=normName(s.name); return sn && (sn.indexOf(n)!==-1 || n.indexOf(sn)!==-1); }) || null;
}
function buildPill(src, name) {
var disp = (src && src.name) || name;
var url = (src && src.url) || '';
var tgMatch = url.match(/^https?:\/\/t\.me\/([^\/?#]+)/i);
var label = tgMatch ? disp + ' (t.me/' + tgMatch[1] + ')' : disp;
var e = self.esc(label);
var titleEsc = self.esc(disp);
if (src && src.url) return ''+e+'';
return ''+e+'';
}
var cards = [];
for (var i = 0; i < lines.length; i++) {
var m = bulletRe.exec(lines[i]); if (!m) continue;
var date = String(m[1]).padStart(2,'0') + '.' + String(m[2]).padStart(2,'0') + '.';
var time = m[3]; var body = m[4];
var pills = '';
var t = trailingRe.exec(body);
if (t) {
body = body.replace(trailingRe, '').trim();
var names = t[1].split(',').map(function(n){return n.trim();}).filter(Boolean);
var seen = {};
pills = names.map(function(n){
if (junkRe.test(n)) return '';
var k = normName(n); if (seen[k]) return ''; seen[k] = true;
return buildPill(lookupByName(n), n);
}).filter(Boolean).join('');
}
if (!pills) {
var nums = []; var cm;
while ((cm = citationRe.exec(body)) !== null) { if (nums.indexOf(cm[1]) === -1) nums.push(cm[1]); }
citationRe.lastIndex = 0;
if (nums.length) {
body = body.replace(citationRe, '').replace(/\s+/g, ' ').trim();
pills = nums.map(function(num){
var src = sources.find(function(s){ return String(s.nr) === num || Number(s.nr) === Number(num); });
return src ? buildPill(src, src.name) : '';
}).filter(Boolean).join('');
}
}
var head = '
';
},
/* ===== HERO ===== */
renderHero: function() {
var d = this.data;
document.getElementById('incident-title').innerHTML =
this.esc(this.fixUmlauts(d.incident.title)) +
' \u2013 ' + this.t('standPrefix') + this.fmtDateOnly(d.generated_at) + ', ' + this.fmtTimeOnly(d.generated_at) + this.t('standSuffix') + '';
// Stat Cards (3: Artikel, Quellen, Faktenchecks)
var statsHtml = '';
statsHtml += this.statCard(this.icons.fileText, '0', this.t("statArticles"));
statsHtml += this.statCard(this.icons.globe, '0', this.t("statSources"));
statsHtml += this.statCard(this.icons.shieldCheck, '0', this.t("statFactchecks"));
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 '
' +
'
' + icon + '
' +
'
' +
'' + value + '' +
'' + label + '' +
'
';
},
/* ===== 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(Lagebild.getLocale());
if (progress < 1) {
requestAnimationFrame(update);
}
}
requestAnimationFrame(update);
},
/* ===== TIMELINE STRIP ===== */
renderTimeline: function() {
var snaps = this.data.available_snapshots || [];
var lagebildUpdated = (this.data.current_lagebild.updated_at || '').replace(' ', 'T');
var newestSnap = snaps.length > 0 ? snaps[0] : null;
var newestSnapTime = newestSnap ? newestSnap.created_at.replace(' ', 'T') : '';
var all;
if (!newestSnap || lagebildUpdated !== newestSnapTime) {
// Neues Lagebild seit letztem Snapshot – eigenen current-Eintrag zeigen
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
};
all = [current].concat(snaps);
} else {
// Lagebild identisch mit neuestem Snapshot – kein Geister-Eintrag,
// stattdessen Snapshot als "current" mit aktuellen Live-Counts markieren
var merged = {};
for (var key in newestSnap) {
if (newestSnap.hasOwnProperty(key)) merged[key] = newestSnap[key];
}
merged.id = 'current';
merged.article_count = this.data.incident.article_count;
merged.fact_check_count = this.data.incident.factcheck_count;
all = [merged].concat(snaps.slice(1));
}
// 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 isActive = (j === dates.length - 1);
var defaultSnap = isActive ? daySnaps[0] : daySnaps[daySnaps.length - 1];
var d = new Date(date + 'T12:00:00Z');
h += '';
}
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');
self.showTimelineDropdown(dateKey, snapId);
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,
article_count: self.data.incident.article_count,
fact_check_count: self.data.incident.factcheck_count
};
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('.h-timeline-point');
if (!item) return;
var items = dropdown.querySelectorAll('.h-timeline-point');
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,
article_count: self.data.incident.article_count,
fact_check_count: self.data.incident.factcheck_count
};
self.renderCurrentView();
} else {
self.loadSnapshot(parseInt(snapId));
}
});
// Show dropdown for newest day by default
var newestDate = dates[dates.length - 1];
if (newestDate) {
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 === 0) {
dropdown.classList.remove('open');
dropdown.innerHTML = '';
return;
}
// Oldest left, newest right
var ordered = snaps.slice().reverse();
var h = '
';
for (var i = 0; i < ordered.length; i++) {
var snap = ordered[i];
var isActive = (String(snap.id) === String(activeSnapId));
h += '';
}
h += '
';
dropdown.innerHTML = h;
dropdown.classList.add('open');
// Scroll to active point
var activePoint = dropdown.querySelector('.h-timeline-point.active');
if (activePoint) {
setTimeout(function() {
activePoint.scrollIntoView({ behavior: 'smooth', inline: 'center', block: 'nearest' });
}, 50);
}
},
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.filterArticlesAtTime(sd.created_at),
fact_checks: this.getFactChecksAtTime(sd.created_at),
article_count: sd.article_count,
fact_check_count: sd.fact_check_count
};
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();
}
// Counts aktualisieren (Hero + Badges)
var articles = this.currentView.articles || [];
var artCount = this.currentView.article_count || articles.length;
var srcCount = new Set(articles.map(function(a) { return a.source; })).size;
var fcCount = this.currentView.fact_check_count || (this.currentView.fact_checks || []).length;
var heroArt = document.getElementById('hero-art-count');
if (heroArt) heroArt.textContent = artCount.toLocaleString(this.getLocale());
var heroSrc = document.getElementById('hero-src-count');
if (heroSrc) heroSrc.textContent = srcCount.toLocaleString(this.getLocale());
var heroFc = document.getElementById('hero-fc-count');
if (heroFc) heroFc.textContent = fcCount.toLocaleString(this.getLocale());
var fcBadge = document.getElementById('tab-badge-faktenchecks');
if (fcBadge) fcBadge.textContent = fcCount;
var quellenBadge = document.getElementById('tab-badge-quellen');
if (quellenBadge) quellenBadge.textContent = srcCount;
},
/* ===== 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[String(sources[i].nr)] = sources[i];
}
var self = this;
html = html.replace(/\[(\d+[a-z]?)\]/g, function(match, nr) {
var src = srcMap[nr];
if (src && src.url) {
return '[' + nr + ']';
}
return '[' + nr + ']';
});
document.getElementById('summary-content').innerHTML = html;
},
renderInlineSources: function() {
document.getElementById('inline-sources').innerHTML = '';
},
/* ===== TAB: QUELLEN (Tile Grid) ===== */
renderSourcesTab: function() {
var articles = this.currentView.articles || [];
var container = document.getElementById('sources-grid-container');
if (!container) return;
// Aggregate by source
var sourceMap = {};
for (var i = 0; i < articles.length; i++) {
var a = articles[i];
var name = a.source || 'Unknown';
if (!sourceMap[name]) sourceMap[name] = { count: 0, articles: [], languages: {}, domain: null };
sourceMap[name].count++;
sourceMap[name].articles.push(a);
var lang = (a.language || '').toUpperCase();
if (lang) sourceMap[name].languages[lang] = (sourceMap[name].languages[lang] || 0) + 1;
if (!sourceMap[name].domain && a.source_url) sourceMap[name].domain = this.extractDomain(a.source_url);
}
// Sort by count desc
var sources = [];
for (var name in sourceMap) {
sources.push({ name: name, data: sourceMap[name] });
}
sources.sort(function(a, b) { return b.data.count - a.data.count; });
// Language totals
var langTotals = {};
for (var i = 0; i < articles.length; i++) {
var lang = (articles[i].language || '').toUpperCase();
if (lang) langTotals[lang] = (langTotals[lang] || 0) + 1;
}
var h = '';
// Header
h += '
';
h += '' + this.t('srcArticlesFrom').replace('{count}', articles.length).replace('{sources}', sources.length) + '';
h += '
';
var langKeys = Object.keys(langTotals).sort(function(a, b) { return langTotals[b] - langTotals[a]; });
for (var i = 0; i < langKeys.length; i++) {
h += '' + langKeys[i] + ' ' + langTotals[langKeys[i]] + '';
}
h += '
';
// Grid
h += '
';
for (var i = 0; i < sources.length; i++) {
var s = sources[i];
var langBadge = Object.keys(s.data.languages).join('/');
h += '
';
h += '
';
if (s.data.domain) {
h += '';
}
h += '' + this.esc(s.name) + '';
h += '
';
h += '
';
h += '' + langBadge + '';
h += '' + s.data.count + '';
h += '
';
h += '
';
}
h += '
';
container.innerHTML = h;
this._sourceTiles = sources;
// Click handler
var self = this;
document.getElementById('sources-grid').addEventListener('click', function(e) {
var tile = e.target.closest('.source-tile');
if (!tile) return;
self.toggleSourceDetail(tile);
});
},
toggleSourceDetail: function(tile) {
var grid = document.getElementById('sources-grid');
var idx = parseInt(tile.getAttribute('data-source-index'));
var existingPanel = grid.querySelector('.source-detail-panel');
var wasActive = tile.classList.contains('active');
// Remove active from all tiles
var allTiles = grid.querySelectorAll('.source-tile');
for (var k = 0; k < allTiles.length; k++) allTiles[k].classList.remove('active');
// Remove existing panel
if (existingPanel) existingPanel.remove();
// If same tile was active, just close
if (wasActive) return;
tile.classList.add('active');
// Find last tile in same visual row (by offsetTop)
var clickedTop = tile.offsetTop;
var lastInRow = tile;
for (var k = 0; k < allTiles.length; k++) {
if (allTiles[k].offsetTop === clickedTop) {
lastInRow = allTiles[k];
}
}
// Build detail panel
var src = this._sourceTiles[idx];
var arts = src.data.articles.slice().sort(function(a, b) {
var da = new Date(Lagebild.toUTC(a.published_at || a.collected_at || ''));
var db = new Date(Lagebild.toUTC(b.published_at || b.collected_at || ''));
return db - da;
});
var h = '
';
h += '
';
h += '';
if (src.data.domain) {
h += ' ';
}
h += this.esc(src.name) + '';
h += '' + src.data.count + ' ' + Lagebild.t('srcArticle') + '';
h += '';
h += '
';
h += '
';
for (var j = 0; j < arts.length; j++) {
var a = arts[j];
var dt = a.published_at || a.collected_at || '';
var dObj = dt ? new Date(this.toUTC(dt)) : null;
var hl = this.fixUmlauts(Lagebild.getHeadline(a));
h += '
';
if (a.source_url) {
h += '' + this.esc(hl) + ' ' + this.icons.externalLink + '';
} else {
h += '' + this.esc(hl) + '';
}
if (dObj && !isNaN(dObj.getTime())) {
h += '' + dObj.toLocaleDateString(Lagebild.getLocale(), { day: '2-digit', month: '2-digit', year: 'numeric', timeZone: TIMEZONE }) + '';
}
h += '
';
return;
}
// Count stats
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 confirmedTotal = stats.confirmed + stats.established;
var openTotal = stats.unconfirmed + stats.developing;
var contradictedTotal = stats.contradicted + stats.disputed;
// Stat cards (clickable filters)
var h = '
';
h += '';
h += '';
h += '';
if (contradictedTotal > 0)
h += '';
h += '
';
// Hinweis bei unvollständiger Liste
var storedFcCount = this.currentView.fact_check_count || 0;
if (storedFcCount > 0 && checks.length < storedFcCount) {
h += '
';
h += this.t('fcCleaned').replace('{count}', checks.length).replace('{total}', storedFcCount);
h += '
';
}
// Sort: status_history first, then by sources_count
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);
});
// Compact accordion list
h += '
';
for (var i = 0; i < checks.length; i++) {
var fc = checks[i];
var status = fc.status || 'developing';
var filterGroup = 'all';
if (status === 'confirmed' || status === 'established') filterGroup = 'confirmed';
else if (status === 'unconfirmed' || status === 'developing') filterGroup = 'unconfirmed';
else if (status === 'contradicted' || status === 'disputed') filterGroup = 'contradicted';
var hasProg = fc.status_history && fc.status_history.length > 1;
var icon = this.fcIcons[status] || '?';
var label = this.stLabel(status);
h += '
';
h += '
';
h += '' + icon + '';
h += '' + this.esc(this.fixUmlauts(fc.claim || '')) + '';
h += '' + (fc.sources_count || 0) + '';
if (hasProg) h += '';
h += '▸';
h += '