';
data.sources.forEach(s => {
const langs = (s.languages || ['de']).map(l => (l || 'de').toUpperCase()).join('/');
const sourceName = this.escape(s.source || 'Unbekannt');
html += `
${sourceName}
${langs}
${s.article_count}
`;
});
html += '
';
return html;
},
renderSourceOverview(articles) {
if (!articles || articles.length === 0) return '';
// Nach Quelle aggregieren
const sourceMap = {};
articles.forEach(a => {
const name = a.source || 'Unbekannt';
if (!sourceMap[name]) {
sourceMap[name] = { count: 0, languages: new Set(), urls: [] };
}
sourceMap[name].count++;
sourceMap[name].languages.add(a.language || 'de');
if (a.source_url) sourceMap[name].urls.push(a.source_url);
});
const sources = Object.entries(sourceMap)
.sort((a, b) => b[1].count - a[1].count);
// Sprach-Statistik
const langCount = {};
articles.forEach(a => {
const lang = (a.language || 'de').toUpperCase();
langCount[lang] = (langCount[lang] || 0) + 1;
});
const langChips = Object.entries(langCount)
.sort((a, b) => b[1] - a[1])
.map(([lang, count]) => `';
sources.forEach(([name, data]) => {
const langs = [...data.languages].map(l => l.toUpperCase()).join('/');
html += `
${this.escape(name)}
${langs}
${data.count}
`;
});
html += '
';
return html;
},
/**
* Kategorie-Labels.
*/
_categoryLabels: {
'nachrichtenagentur': 'Agentur',
'oeffentlich-rechtlich': 'ÖR',
'qualitaetszeitung': 'Qualität',
'behoerde': 'Behörde',
'fachmedien': 'Fach',
'think-tank': 'Think Tank',
'international': 'Intl.',
'regional': 'Regional',
'boulevard': 'Boulevard',
'telegram': 'Telegram',
'sonstige': 'Sonstige',
},
_politicalLabels: {
links_extrem: { short: 'L+', full: 'Links (extrem)' },
links: { short: 'L', full: 'Links' },
mitte_links: { short: 'ML', full: 'Mitte-Links' },
liberal: { short: 'LIB', full: 'Liberal' },
mitte: { short: 'M', full: 'Mitte' },
konservativ: { short: 'KON', full: 'Konservativ' },
mitte_rechts: { short: 'MR', full: 'Mitte-Rechts' },
rechts: { short: 'R', full: 'Rechts' },
rechts_extrem: { short: 'R+', full: 'Rechts (extrem)' },
na: { short: '?', full: 'Nicht eingeordnet' },
},
_reliabilityLabels: {
sehr_hoch: 'Sehr hoch',
hoch: 'Hoch',
gemischt: 'Gemischt',
niedrig: 'Niedrig',
sehr_niedrig: 'Sehr niedrig',
na: 'Nicht eingeordnet',
},
_mediaTypeLabels: {
tageszeitung: 'Tageszeitung',
wochenzeitung: 'Wochenzeitung',
magazin: 'Magazin',
tv_sender: 'TV-Sender',
radio: 'Radio',
oeffentlich_rechtlich: 'Öffentlich-Rechtlich',
nachrichtenagentur: 'Nachrichtenagentur',
online_only: 'Online-only',
blog: 'Blog',
telegram_kanal: 'Telegram-Kanal',
telegram_bot: 'Telegram-Bot',
podcast: 'Podcast',
social_media: 'Social Media',
imageboard: 'Imageboard',
think_tank: 'Think Tank',
ngo: 'NGO',
behoerde: 'Behörde',
staatsmedium: 'Staatsmedium',
fachmedium: 'Fachmedium',
sonstige: 'Sonstige',
},
_alignmentLabels: {
prorussisch: 'prorussisch',
proiranisch: 'proiranisch',
prowestlich: 'prowestlich',
proukrainisch: 'proukrainisch',
prochinesisch: 'prochinesisch',
projapanisch: 'projapanisch',
proisraelisch: 'proisraelisch',
propalaestinensisch: 'propalästinensisch',
protuerkisch: 'protürkisch',
panarabisch: 'panarabisch',
neutral: 'neutral',
sonstige: 'sonstige',
},
/**
* Eintrag in der Klassifikations-Review-Queue.
* Zeigt Diff zwischen aktuellem Wert und LLM-Vorschlag.
*/
renderClassificationQueueItem(item) {
const cur = item.current || {};
const prop = item.proposed || {};
const conf = prop.confidence || 0;
const confPct = Math.round(conf * 100);
const confClass = conf >= 0.85 ? 'high' : (conf >= 0.7 ? 'medium' : 'low');
const diffRow = (label, currentVal, proposedVal, formatter) => {
const fmt = formatter || (v => v == null || v === '' ? '–' : String(v));
const c = fmt(currentVal);
const p = fmt(proposedVal);
const changed = c !== p;
return `
${diffRow('Politik', cur.political_orientation, prop.political_orientation, polFmt)}
${diffRow('Medientyp', cur.media_type, prop.media_type, mtFmt)}
${diffRow('Glaubwürdigkeit', cur.reliability, prop.reliability, relFmt)}
${diffRow('Staatsnah', cur.state_affiliated, prop.state_affiliated, stateFmt)}
${diffRow('Land', cur.country_code, prop.country_code, ccFmt)}
${diffRow('Geopol. Nähe', cur.alignments, prop.alignments, alignFmt)}
${reasoning ? `
Begründung: ${reasoning}
` : ''}
`;
},
_renderClassificationBadges(feed) {
const parts = [];
const pol = feed.political_orientation;
if (pol && pol !== 'na') {
const label = this._politicalLabels[pol] || { short: pol, full: pol };
parts.push(``;
realFeeds.forEach((feed, i) => {
const isLast = i === realFeeds.length - 1;
const connector = isLast ? '\u2514\u2500' : '\u251C\u2500';
const typeLabel = feed.source_type === 'rss_feed' ? 'RSS' : 'Web';
const urlDisplay = feed.url ? this._shortenUrl(feed.url) : '';
feedRows += `
${connector}
${this.escape(feed.name)}
${typeLabel}
${this.escape(urlDisplay)}
${!feed.is_global ? `
` : 'Grundquelle'}
`;
});
feedRows += '
';
}
const feedCountBadge = feedCount > 0
? `