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

@@ -3503,6 +3503,117 @@ a.dev-source-pill:hover {
color: var(--info); color: var(--info);
} }
/* Klassifikations-Badges (politisch / reliability / alignments / state) */
.source-classification-badges {
display: inline-flex;
align-items: center;
gap: 4px;
flex-wrap: wrap;
}
.source-political-badge {
display: inline-flex;
align-items: center;
justify-content: center;
min-width: 22px;
padding: 2px 6px;
border-radius: var(--radius);
font-size: 10px;
font-weight: 700;
letter-spacing: 0.4px;
color: #fff;
background: #9e9e9e;
}
.source-political-badge.pol-links_extrem { background: #b71c1c; }
.source-political-badge.pol-links { background: #e53935; }
.source-political-badge.pol-mitte_links { background: #ef9a9a; color: #4a0d0d; }
.source-political-badge.pol-liberal { background: #fdd835; color: #4a3700; }
.source-political-badge.pol-mitte { background: #9e9e9e; }
.source-political-badge.pol-konservativ { background: #90caf9; color: #0d2740; }
.source-political-badge.pol-mitte_rechts { background: #5c6bc0; }
.source-political-badge.pol-rechts { background: #1976d2; }
.source-political-badge.pol-rechts_extrem { background: #0d47a1; }
.source-reliability-dot {
display: inline-block;
width: 10px;
height: 10px;
border-radius: 50%;
background: #9e9e9e;
border: 1px solid rgba(0, 0, 0, 0.15);
}
.source-reliability-dot.rel-sehr_hoch { background: #2e7d32; }
.source-reliability-dot.rel-hoch { background: #66bb6a; }
.source-reliability-dot.rel-gemischt { background: #fbc02d; }
.source-reliability-dot.rel-niedrig { background: #ef6c00; }
.source-reliability-dot.rel-sehr_niedrig { background: #c62828; }
.source-state-badge {
display: inline-flex;
align-items: center;
justify-content: center;
width: 18px;
height: 18px;
border-radius: 50%;
background: #4a148c;
color: #fff;
font-size: 11px;
line-height: 1;
}
.source-alignment-chip-badge {
display: inline-flex;
align-items: center;
padding: 1px 6px;
border-radius: 999px;
font-size: 10px;
font-weight: 500;
background: var(--cat-sonstige-bg, #eef);
color: var(--text-secondary, #555);
border: 1px solid rgba(0, 0, 0, 0.08);
}
/* Edit-Form: Klassifikations-Sektion */
.sources-classification-section {
margin-top: 12px;
padding-top: 12px;
border-top: 1px solid var(--border-color, rgba(0,0,0,0.08));
}
.sources-classification-header {
font-size: 12px;
font-weight: 600;
color: var(--text-secondary, #555);
margin-bottom: 8px;
letter-spacing: 0.3px;
text-transform: uppercase;
}
.alignment-chips {
display: flex;
flex-wrap: wrap;
gap: 6px;
}
.alignment-chip {
display: inline-flex;
align-items: center;
padding: 4px 10px;
border-radius: 999px;
font-size: 11px;
font-weight: 500;
background: transparent;
color: var(--text-secondary, #555);
border: 1px solid var(--border-color, rgba(0,0,0,0.15));
cursor: pointer;
transition: all 0.12s ease;
}
.alignment-chip:hover {
background: var(--cat-sonstige-bg, #eef);
}
.alignment-chip.active {
background: var(--primary, #2a81cb);
color: #fff;
border-color: var(--primary, #2a81cb);
}
/* Typ-Badges */ /* Typ-Badges */
.source-type-badge { .source-type-badge {
display: inline-flex; display: inline-flex;

Datei anzeigen

@@ -481,6 +481,70 @@
<option value="boulevard">Boulevard</option> <option value="boulevard">Boulevard</option>
<option value="sonstige">Sonstige</option> <option value="sonstige">Sonstige</option>
</select> </select>
<label for="sources-filter-political" class="sr-only">Politische Ausrichtung filtern</label>
<select id="sources-filter-political" class="timeline-filter-select" onchange="App.filterSources()">
<option value="">Alle Ausrichtungen</option>
<option value="links_extrem">Links (extrem)</option>
<option value="links">Links</option>
<option value="mitte_links">Mitte-Links</option>
<option value="liberal">Liberal</option>
<option value="mitte">Mitte</option>
<option value="konservativ">Konservativ</option>
<option value="mitte_rechts">Mitte-Rechts</option>
<option value="rechts">Rechts</option>
<option value="rechts_extrem">Rechts (extrem)</option>
<option value="na">Nicht eingeordnet</option>
</select>
<label for="sources-filter-mediatype" class="sr-only">Medientyp filtern</label>
<select id="sources-filter-mediatype" class="timeline-filter-select" onchange="App.filterSources()">
<option value="">Alle Medientypen</option>
<option value="tageszeitung">Tageszeitung</option>
<option value="wochenzeitung">Wochenzeitung</option>
<option value="magazin">Magazin</option>
<option value="tv_sender">TV-Sender</option>
<option value="radio">Radio</option>
<option value="oeffentlich_rechtlich">Öffentlich-Rechtlich</option>
<option value="nachrichtenagentur">Nachrichtenagentur</option>
<option value="online_only">Online-only</option>
<option value="blog">Blog</option>
<option value="telegram_kanal">Telegram-Kanal</option>
<option value="telegram_bot">Telegram-Bot</option>
<option value="podcast">Podcast</option>
<option value="social_media">Social Media</option>
<option value="imageboard">Imageboard</option>
<option value="think_tank">Think Tank</option>
<option value="ngo">NGO</option>
<option value="behoerde">Behörde</option>
<option value="staatsmedium">Staatsmedium</option>
<option value="fachmedium">Fachmedium</option>
<option value="sonstige">Sonstige</option>
</select>
<label for="sources-filter-reliability" class="sr-only">Glaubwürdigkeit filtern</label>
<select id="sources-filter-reliability" class="timeline-filter-select" onchange="App.filterSources()">
<option value="">Alle Glaubwürdigkeiten</option>
<option value="sehr_hoch">Sehr hoch</option>
<option value="hoch">Hoch</option>
<option value="gemischt">Gemischt</option>
<option value="niedrig">Niedrig</option>
<option value="sehr_niedrig">Sehr niedrig</option>
<option value="na">Nicht eingeordnet</option>
</select>
<label for="sources-filter-alignment" class="sr-only">Geopolitische Nähe filtern</label>
<select id="sources-filter-alignment" class="timeline-filter-select" onchange="App.filterSources()">
<option value="">Alle Nähen</option>
<option value="prorussisch">Prorussisch</option>
<option value="proiranisch">Proiranisch</option>
<option value="prowestlich">Prowestlich</option>
<option value="proukrainisch">Proukrainisch</option>
<option value="prochinesisch">Prochinesisch</option>
<option value="projapanisch">Projapanisch</option>
<option value="proisraelisch">Proisraelisch</option>
<option value="propalaestinensisch">Propalästinensisch</option>
<option value="protuerkisch">Protürkisch</option>
<option value="panarabisch">Panarabisch</option>
<option value="neutral">Neutral</option>
<option value="sonstige">Sonstige</option>
</select>
<label for="sources-search" class="sr-only">Quellen durchsuchen</label> <label for="sources-search" class="sr-only">Quellen durchsuchen</label>
<input type="text" id="sources-search" class="timeline-filter-input sources-search-input" placeholder="Suche..." oninput="App.filterSources()"> <input type="text" id="sources-search" class="timeline-filter-input sources-search-input" placeholder="Suche..." oninput="App.filterSources()">
</div> </div>
@@ -548,6 +612,89 @@
<input type="text" id="src-notes" placeholder="Optional"> <input type="text" id="src-notes" placeholder="Optional">
</div> </div>
</div> </div>
<div class="sources-classification-section">
<div class="sources-classification-header">Einordnung</div>
<div class="sources-add-form-grid">
<div class="form-group">
<label for="src-political">Politische Ausrichtung</label>
<select id="src-political">
<option value="na">Nicht eingeordnet</option>
<option value="links_extrem">Links (extrem)</option>
<option value="links">Links</option>
<option value="mitte_links">Mitte-Links</option>
<option value="liberal">Liberal</option>
<option value="mitte">Mitte</option>
<option value="konservativ">Konservativ</option>
<option value="mitte_rechts">Mitte-Rechts</option>
<option value="rechts">Rechts</option>
<option value="rechts_extrem">Rechts (extrem)</option>
</select>
</div>
<div class="form-group">
<label for="src-mediatype">Medientyp</label>
<select id="src-mediatype">
<option value="sonstige">Sonstige</option>
<option value="tageszeitung">Tageszeitung</option>
<option value="wochenzeitung">Wochenzeitung</option>
<option value="magazin">Magazin</option>
<option value="tv_sender">TV-Sender</option>
<option value="radio">Radio</option>
<option value="oeffentlich_rechtlich">Öffentlich-Rechtlich</option>
<option value="nachrichtenagentur">Nachrichtenagentur</option>
<option value="online_only">Online-only</option>
<option value="blog">Blog</option>
<option value="telegram_kanal">Telegram-Kanal</option>
<option value="telegram_bot">Telegram-Bot</option>
<option value="podcast">Podcast</option>
<option value="social_media">Social Media</option>
<option value="imageboard">Imageboard</option>
<option value="think_tank">Think Tank</option>
<option value="ngo">NGO</option>
<option value="behoerde">Behörde</option>
<option value="staatsmedium">Staatsmedium</option>
<option value="fachmedium">Fachmedium</option>
</select>
</div>
<div class="form-group">
<label for="src-reliability">Glaubwürdigkeit</label>
<select id="src-reliability">
<option value="na">Nicht eingeordnet</option>
<option value="sehr_hoch">Sehr hoch</option>
<option value="hoch">Hoch</option>
<option value="gemischt">Gemischt</option>
<option value="niedrig">Niedrig</option>
<option value="sehr_niedrig">Sehr niedrig</option>
</select>
</div>
<div class="form-group">
<label for="src-country">Land (ISO 3166)</label>
<input type="text" id="src-country" maxlength="2" placeholder="z.B. DE, RU, US" style="text-transform:uppercase;">
</div>
<div class="form-group">
<label class="checkbox-label" style="display:flex;align-items:center;gap:8px;">
<input type="checkbox" id="src-state-affiliated">
<span>Staatsnah/-kontrolliert</span>
</label>
</div>
</div>
<div class="form-group" style="margin-top:8px;">
<label>Geopolitische Nähe (Mehrfachauswahl)</label>
<div id="src-alignments-chips" class="alignment-chips" onclick="App.handleAlignmentChipClick(event)">
<button type="button" class="alignment-chip" data-alignment="prorussisch">prorussisch</button>
<button type="button" class="alignment-chip" data-alignment="proiranisch">proiranisch</button>
<button type="button" class="alignment-chip" data-alignment="prowestlich">prowestlich</button>
<button type="button" class="alignment-chip" data-alignment="proukrainisch">proukrainisch</button>
<button type="button" class="alignment-chip" data-alignment="prochinesisch">prochinesisch</button>
<button type="button" class="alignment-chip" data-alignment="projapanisch">projapanisch</button>
<button type="button" class="alignment-chip" data-alignment="proisraelisch">proisraelisch</button>
<button type="button" class="alignment-chip" data-alignment="propalaestinensisch">propalästinensisch</button>
<button type="button" class="alignment-chip" data-alignment="protuerkisch">protürkisch</button>
<button type="button" class="alignment-chip" data-alignment="panarabisch">panarabisch</button>
<button type="button" class="alignment-chip" data-alignment="neutral">neutral</button>
<button type="button" class="alignment-chip" data-alignment="sonstige">sonstige</button>
</div>
</div>
</div>
<div class="sources-discovery-actions"> <div class="sources-discovery-actions">
<button class="btn btn-primary btn-small" onclick="App.saveSource()">Speichern</button> <button class="btn btn-primary btn-small" onclick="App.saveSource()">Speichern</button>
<button class="btn btn-secondary btn-small" onclick="App.toggleSourceForm(false)">Abbrechen</button> <button class="btn btn-secondary btn-small" onclick="App.toggleSourceForm(false)">Abbrechen</button>

Datei anzeigen

@@ -2750,6 +2750,10 @@ async handleRefresh() {
// Filter anwenden // Filter anwenden
const typeFilter = document.getElementById('sources-filter-type')?.value || ''; const typeFilter = document.getElementById('sources-filter-type')?.value || '';
const catFilter = document.getElementById('sources-filter-category')?.value || ''; const catFilter = document.getElementById('sources-filter-category')?.value || '';
const politicalFilter = document.getElementById('sources-filter-political')?.value || '';
const mediaTypeFilter = document.getElementById('sources-filter-mediatype')?.value || '';
const reliabilityFilter = document.getElementById('sources-filter-reliability')?.value || '';
const alignmentFilter = document.getElementById('sources-filter-alignment')?.value || '';
const search = (document.getElementById('sources-search')?.value || '').toLowerCase(); const search = (document.getElementById('sources-search')?.value || '').toLowerCase();
// Alle Quellen nach Domain gruppieren // Alle Quellen nach Domain gruppieren
@@ -2800,6 +2804,20 @@ async handleRefresh() {
if (!hasMatchingCat) continue; if (!hasMatchingCat) continue;
} }
// Klassifikations-Filter
if (politicalFilter) {
if (!feeds.some(f => (f.political_orientation || 'na') === politicalFilter)) continue;
}
if (mediaTypeFilter) {
if (!feeds.some(f => (f.media_type || 'sonstige') === mediaTypeFilter)) continue;
}
if (reliabilityFilter) {
if (!feeds.some(f => (f.reliability || 'na') === reliabilityFilter)) continue;
}
if (alignmentFilter) {
if (!feeds.some(f => Array.isArray(f.alignments) && f.alignments.includes(alignmentFilter))) continue;
}
// Suche // Suche
if (search) { if (search) {
const groupText = feeds.map(f => const groupText = feeds.map(f =>
@@ -3054,6 +3072,13 @@ async handleRefresh() {
document.getElementById('src-discover-btn').disabled = false; document.getElementById('src-discover-btn').disabled = false;
document.getElementById('src-discover-btn').textContent = 'Erkennen'; document.getElementById('src-discover-btn').textContent = 'Erkennen';
document.getElementById('src-type-select').value = 'rss_feed'; document.getElementById('src-type-select').value = 'rss_feed';
// Klassifikations-Felder auf Default zurücksetzen
const polEl = document.getElementById('src-political'); if (polEl) polEl.value = 'na';
const mtEl = document.getElementById('src-mediatype'); if (mtEl) mtEl.value = 'sonstige';
const relEl = document.getElementById('src-reliability'); if (relEl) relEl.value = 'na';
const ccEl = document.getElementById('src-country'); if (ccEl) ccEl.value = '';
const saEl = document.getElementById('src-state-affiliated'); if (saEl) saEl.checked = false;
this._setAlignmentChips([]);
// Save-Button Text zurücksetzen // Save-Button Text zurücksetzen
const saveBtn = document.querySelector('#src-discovery-result .sources-discovery-actions .btn-primary'); const saveBtn = document.querySelector('#src-discovery-result .sources-discovery-actions .btn-primary');
if (saveBtn) saveBtn.textContent = 'Speichern'; if (saveBtn) saveBtn.textContent = 'Speichern';
@@ -3235,6 +3260,19 @@ async handleRefresh() {
rss_url: source.url, rss_url: source.url,
}; };
// Klassifikations-Felder setzen
const polEl = document.getElementById('src-political');
if (polEl) polEl.value = source.political_orientation || 'na';
const mtEl = document.getElementById('src-mediatype');
if (mtEl) mtEl.value = source.media_type || 'sonstige';
const relEl = document.getElementById('src-reliability');
if (relEl) relEl.value = source.reliability || 'na';
const ccEl = document.getElementById('src-country');
if (ccEl) ccEl.value = source.country_code || '';
const saEl = document.getElementById('src-state-affiliated');
if (saEl) saEl.checked = !!source.state_affiliated;
this._setAlignmentChips(source.alignments || []);
// Submit-Button-Text ändern // Submit-Button-Text ändern
const saveBtn = document.querySelector('#src-discovery-result .sources-discovery-actions .btn-primary'); const saveBtn = document.querySelector('#src-discovery-result .sources-discovery-actions .btn-primary');
if (saveBtn) saveBtn.textContent = 'Quelle speichern'; if (saveBtn) saveBtn.textContent = 'Quelle speichern';
@@ -3243,6 +3281,27 @@ async handleRefresh() {
if (form) form.scrollIntoView({ behavior: 'smooth', block: 'start' }); if (form) form.scrollIntoView({ behavior: 'smooth', block: 'start' });
}, },
_setAlignmentChips(active) {
const chips = document.querySelectorAll('#src-alignments-chips .alignment-chip');
const set = new Set((active || []).map(a => (a || '').toLowerCase()));
chips.forEach(chip => {
if (set.has(chip.dataset.alignment)) chip.classList.add('active');
else chip.classList.remove('active');
});
},
_getAlignmentChips() {
return Array.from(document.querySelectorAll('#src-alignments-chips .alignment-chip.active'))
.map(chip => chip.dataset.alignment);
},
handleAlignmentChipClick(e) {
const chip = e.target.closest('.alignment-chip');
if (!chip) return;
e.preventDefault();
chip.classList.toggle('active');
},
async saveSource() { async saveSource() {
const name = document.getElementById('src-name').value.trim(); const name = document.getElementById('src-name').value.trim();
if (!name) { if (!name) {
@@ -3258,6 +3317,12 @@ async handleRefresh() {
url: discovered.rss_url || (discovered.source_type === 'telegram_channel' ? (document.getElementById('src-domain').value || null) : null), url: discovered.rss_url || (discovered.source_type === 'telegram_channel' ? (document.getElementById('src-domain').value || null) : null),
domain: document.getElementById('src-domain').value.trim() || discovered.domain || null, domain: document.getElementById('src-domain').value.trim() || discovered.domain || null,
notes: document.getElementById('src-notes').value.trim() || null, notes: document.getElementById('src-notes').value.trim() || null,
political_orientation: document.getElementById('src-political')?.value || 'na',
media_type: document.getElementById('src-mediatype')?.value || 'sonstige',
reliability: document.getElementById('src-reliability')?.value || 'na',
country_code: (document.getElementById('src-country')?.value || '').trim().toUpperCase() || null,
state_affiliated: !!document.getElementById('src-state-affiliated')?.checked,
alignments: this._getAlignmentChips(),
}; };
if (!data.domain && discovered.domain) { if (!data.domain && discovered.domain) {

Datei anzeigen

@@ -1062,6 +1062,85 @@ const UI = {
'sonstige': 'Sonstige', '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). * Domain-Gruppe rendern (aufklappbar mit Feeds).
*/ */
@@ -1117,20 +1196,44 @@ const UI = {
? `<span class="source-feed-count">${feedCount} Feed${feedCount !== 1 ? 's' : ''}</span>` ? `<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 = ''; let infoButtonHtml = '';
const firstFeed = feeds[0] || {}; 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) { 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 = []; const lines = [];
lines.push('Typ: ' + (typeMap[firstFeed.source_type] || firstFeed.source_type || 'Unbekannt')); lines.push('Typ: ' + (typeMap[firstFeed.source_type] || firstFeed.source_type || 'Unbekannt'));
if (firstFeed.language) lines.push('Sprache: ' + firstFeed.language); 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')); 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>`; 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"> return `<div class="source-group">
<div class="source-group-header" ${toggleAttr}> <div class="source-group-header" ${toggleAttr}>
${toggleIcon} ${toggleIcon}
@@ -1138,6 +1241,7 @@ const UI = {
<span class="source-group-name">${this.escape(displayName)}</span>${infoButtonHtml} <span class="source-group-name">${this.escape(displayName)}</span>${infoButtonHtml}
</div> </div>
<span class="source-category-badge cat-${feeds[0]?.category || 'sonstige'}">${catLabel}</span> <span class="source-category-badge cat-${feeds[0]?.category || 'sonstige'}">${catLabel}</span>
${classificationBadges ? `<span class="source-classification-badges">${classificationBadges}</span>` : ''}
${feedCountBadge} ${feedCountBadge}
<div class="source-group-actions" onclick="event.stopPropagation()"> <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>` : ''} ${!isGlobal && !hasMultiple && feeds[0]?.id ? `<button class="source-edit-btn" onclick="App.editSource(${feeds[0].id})" title="Bearbeiten" aria-label="Bearbeiten">&#9998;</button>` : ''}