feat(sources): UI fuer Quellen-Klassifikation (Filter, Badges, Edit-Form)

- Quellen-Modal: 4 neue Filter (Politik, Medientyp, Reliability, Alignment).
- Edit-Form: Selects fuer political_orientation/media_type/reliability,
  Multi-Select-Chips fuer alignments, Toggle state_affiliated, Country-Code-Input.
- renderSourceGroup: Politik-Badge mit DACH-Farbskala (rot=L, blau=R),
  Reliability-Punkt (gruen→rot), Alignment-Tags, state-affiliated-Indikator.
  Tooltip um alle 4 Achsen erweitert.
- CSS-Block fuer alle neuen Badge-/Chip-Styles.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Dieser Commit ist enthalten in:
Claude Code
2026-05-07 18:37:09 +00:00
Ursprung f8e2f73bc0
Commit 715af17ac3
4 geänderte Dateien mit 431 neuen und 4 gelöschten Zeilen

Datei anzeigen

@@ -1062,6 +1062,85 @@ const UI = {
'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',
},
_renderClassificationBadges(feed) {
const parts = [];
const pol = feed.political_orientation;
if (pol && pol !== 'na') {
const label = this._politicalLabels[pol] || { short: pol, full: pol };
parts.push(`<span class="source-political-badge pol-${this.escape(pol)}" title="${this.escape(label.full)}">${this.escape(label.short)}</span>`);
}
const rel = feed.reliability;
if (rel && rel !== 'na') {
parts.push(`<span class="source-reliability-dot rel-${this.escape(rel)}" title="Glaubwürdigkeit: ${this.escape(this._reliabilityLabels[rel] || rel)}" aria-label="Glaubwürdigkeit: ${this.escape(this._reliabilityLabels[rel] || rel)}"></span>`);
}
if (feed.state_affiliated) {
parts.push(`<span class="source-state-badge" title="Staatsnah/-kontrolliert" aria-label="Staatsnah">⚑</span>`);
}
const aligns = Array.isArray(feed.alignments) ? feed.alignments : [];
aligns.forEach(a => {
const label = this._alignmentLabels[a] || a;
parts.push(`<span class="source-alignment-chip-badge align-${this.escape(a)}">${this.escape(label)}</span>`);
});
return parts.join('');
},
/**
* Domain-Gruppe rendern (aufklappbar mit Feeds).
*/
@@ -1117,20 +1196,44 @@ const UI = {
? `<span class="source-feed-count">${feedCount} Feed${feedCount !== 1 ? 's' : ''}</span>`
: '';
// Info-Button mit Tooltip (Typ, Sprache, Ausrichtung)
// Info-Button mit Tooltip (Typ, Sprache, Ausrichtung, Klassifikation)
let infoButtonHtml = '';
const firstFeed = feeds[0] || {};
const hasInfo = firstFeed.language || firstFeed.bias;
const hasInfo = firstFeed.language || firstFeed.bias
|| (firstFeed.political_orientation && firstFeed.political_orientation !== 'na')
|| (firstFeed.media_type && firstFeed.media_type !== 'sonstige')
|| (firstFeed.reliability && firstFeed.reliability !== 'na')
|| firstFeed.state_affiliated
|| firstFeed.country_code
|| (Array.isArray(firstFeed.alignments) && firstFeed.alignments.length > 0);
if (hasInfo) {
const typeMap = { rss_feed: 'RSS-Feed', web_source: 'Web-Quelle', telegram_channel: 'Telegram-Kanal' };
const typeMap = { rss_feed: 'RSS-Feed', web_source: 'Web-Quelle', telegram_channel: 'Telegram-Kanal', podcast_feed: 'Podcast' };
const lines = [];
lines.push('Typ: ' + (typeMap[firstFeed.source_type] || firstFeed.source_type || 'Unbekannt'));
if (firstFeed.language) lines.push('Sprache: ' + firstFeed.language);
if (firstFeed.bias) lines.push('Ausrichtung: ' + firstFeed.bias);
if (firstFeed.country_code) lines.push('Land: ' + firstFeed.country_code);
if (firstFeed.media_type && firstFeed.media_type !== 'sonstige') {
lines.push('Medientyp: ' + (this._mediaTypeLabels[firstFeed.media_type] || firstFeed.media_type));
}
if (firstFeed.political_orientation && firstFeed.political_orientation !== 'na') {
const pl = this._politicalLabels[firstFeed.political_orientation];
lines.push('Politisch: ' + (pl ? pl.full : firstFeed.political_orientation));
}
if (firstFeed.reliability && firstFeed.reliability !== 'na') {
lines.push('Glaubwürdigkeit: ' + (this._reliabilityLabels[firstFeed.reliability] || firstFeed.reliability));
}
if (firstFeed.state_affiliated) lines.push('Staatsnah: ja');
if (Array.isArray(firstFeed.alignments) && firstFeed.alignments.length > 0) {
const labels = firstFeed.alignments.map(a => this._alignmentLabels[a] || a);
lines.push('Geopolitische Nähe: ' + labels.join(', '));
}
if (firstFeed.bias) lines.push('Notiz: ' + firstFeed.bias);
const tooltipText = this.escape(lines.join('\n'));
infoButtonHtml = ` <span class="info-icon tooltip-below" data-tooltip="${tooltipText}"><svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" 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"/><path d="M12 16v-4"/><path d="M12 8h.01"/></svg></span>`;
}
const classificationBadges = this._renderClassificationBadges(firstFeed);
return `<div class="source-group">
<div class="source-group-header" ${toggleAttr}>
${toggleIcon}
@@ -1138,6 +1241,7 @@ const UI = {
<span class="source-group-name">${this.escape(displayName)}</span>${infoButtonHtml}
</div>
<span class="source-category-badge cat-${feeds[0]?.category || 'sonstige'}">${catLabel}</span>
${classificationBadges ? `<span class="source-classification-badges">${classificationBadges}</span>` : ''}
${feedCountBadge}
<div class="source-group-actions" onclick="event.stopPropagation()">
${!isGlobal && !hasMultiple && feeds[0]?.id ? `<button class="source-edit-btn" onclick="App.editSource(${feeds[0].id})" title="Bearbeiten" aria-label="Bearbeiten">&#9998;</button>` : ''}