feat(sources): externer Reputations-Layer (IFCN + EUvsDisinfo)

Externe Datenquellen (kostenlos, Open Data) ergaenzen die LLM-geschaetzte
Reliability-Achse mit objektiven Signalen:

- IFCN-Signatories (raw.githubusercontent.com/IFCN/verified-signatories):
  Plain-Text-Liste anerkannter Faktencheck-Organisationen.
- EUvsDisinfo (Zenodo CSV): Pro-Kreml-Desinformations-Datenbank.

Schema-Erweiterung:
- ifcn_signatory, eu_disinfo_listed, eu_disinfo_case_count,
  eu_disinfo_last_seen, external_data_synced_at.

Service src/services/external_reputation.py:
- sync_ifcn_signatories(), sync_eu_disinfo(), apply_reputation_overrides(),
  sync_all() mit Domain-Normalisierung (lowercase, ohne www., ohne Schema).

Reliability-Override-Regeln (laufen nach Approve und manuellem Sync):
- ifcn_signatory=1 -> reliability=sehr_hoch
- eu_disinfo_case_count >= 5 -> reliability=sehr_niedrig
- eu_disinfo_case_count >= 1 -> Reliability eine Stufe runter (max niedrig)

API: POST /api/sources/external-reputation/sync (Admin, BackgroundTask).
Filter: ?ifcn_signatory=true, ?eu_disinfo_listed=true.

UI:
- Filter-Dropdown "Externe Reputation" im Quellen-Modal.
- Badges: gruenes "IFCN" und rotes "EU-Desinfo (n)".
- Tooltip macht Reliability-Quelle transparent: "(IFCN-Faktenchecker)",
  "(EU-Desinfo, n Faelle)" oder "(LLM-Schaetzung)".
- "Externe Daten syncen"-Button im Review-Toolbar (Admin-only).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Dieser Commit ist enthalten in:
Claude Code
2026-05-07 19:40:30 +00:00
Ursprung 48a60d7579
Commit 5fc2467559
9 geänderte Dateien mit 410 neuen und 3 gelöschten Zeilen

Datei anzeigen

@@ -1193,7 +1193,20 @@ const UI = {
}
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>`);
const relLabel = this._reliabilityLabels[rel] || rel;
const relSource = feed.ifcn_signatory ? '(IFCN-Faktenchecker)'
: (feed.eu_disinfo_listed ? `(EU-Desinfo, ${feed.eu_disinfo_case_count || 0} Fälle)`
: '(LLM-Schätzung)');
const relTitle = `Glaubwürdigkeit: ${relLabel} ${relSource}`;
parts.push(`<span class="source-reliability-dot rel-${this.escape(rel)}" title="${this.escape(relTitle)}" aria-label="${this.escape(relTitle)}"></span>`);
}
if (feed.ifcn_signatory) {
parts.push(`<span class="source-ifcn-badge" title="IFCN-zertifizierter Faktenchecker" aria-label="IFCN-Faktenchecker">✓ IFCN</span>`);
}
if (feed.eu_disinfo_listed) {
const cnt = feed.eu_disinfo_case_count || 0;
const title = `EUvsDisinfo: ${cnt} dokumentierte Desinformations-Fälle`;
parts.push(`<span class="source-eu-disinfo-badge" title="${this.escape(title)}" aria-label="${this.escape(title)}">⚠ EU-Desinfo (${cnt})</span>`);
}
if (feed.state_affiliated) {
parts.push(`<span class="source-state-badge" title="Staatsnah/-kontrolliert" aria-label="Staatsnah">⚑</span>`);
@@ -1285,7 +1298,15 @@ const UI = {
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));
const relLabel = this._reliabilityLabels[firstFeed.reliability] || firstFeed.reliability;
const relSrc = firstFeed.ifcn_signatory ? ' (IFCN-Faktenchecker)'
: (firstFeed.eu_disinfo_listed ? ` (EU-Desinfo, ${firstFeed.eu_disinfo_case_count || 0} Fälle)`
: ' (LLM-Schätzung)');
lines.push('Glaubwürdigkeit: ' + relLabel + relSrc);
}
if (firstFeed.ifcn_signatory) lines.push('IFCN-Faktenchecker: ja');
if (firstFeed.eu_disinfo_listed) {
lines.push(`EUvsDisinfo: ${firstFeed.eu_disinfo_case_count || 0} Fälle` + (firstFeed.eu_disinfo_last_seen ? ` (zuletzt ${firstFeed.eu_disinfo_last_seen})` : ''));
}
if (firstFeed.state_affiliated) lines.push('Staatsnah: ja');
if (Array.isArray(firstFeed.alignments) && firstFeed.alignments.length > 0) {