/* Grundquellen & Kundenquellen Management */ "use strict"; let globalSourcesCache = []; let tenantSourcesCache = []; let editingSourceId = null; let globalSortField = "category"; let globalSortAsc = true; const CATEGORY_LABELS = { nachrichtenagentur: "Nachrichtenagentur", "oeffentlich-rechtlich": "Öffentlich-Rechtlich", qualitaetszeitung: "Qualitätszeitung", behoerde: "Behörde", fachmedien: "Fachmedien", "think-tank": "Think-Tank", international: "International", regional: "Regional", boulevard: "Boulevard", sonstige: "Sonstige", "cybercrime": "Cybercrime / Hacktivismus", "cybercrime-leaks": "Cybercrime / Leaks", "ukraine-russland-krieg": "Ukraine-Russland-Krieg", "irankonflikt": "Irankonflikt", "osint-international": "OSINT International", "extremismus-deutschland": "Extremismus Deutschland", "russische-staatspropaganda": "Russische Staatspropaganda", "russische-opposition": "Russische Opposition / Exilmedien", "syrien-nahost": "Syrien / Nahost", }; const TYPE_LABELS = { rss_feed: "RSS-Feed", web_source: "Webquelle", telegram_channel: "Telegram-Kanal", excluded: "Ausgeschlossen", }; // --- Init --- document.addEventListener("DOMContentLoaded", () => { setupSourceSubTabs(); setupSourceForms(); // Beim Tab-Wechsel auf "Quellen" laden document.querySelectorAll('.nav-tab[data-section="sources"]').forEach((tab) => { tab.addEventListener("click", () => loadGlobalSources()); }); }); function setupSourceSubTabs() { document.querySelectorAll("#sourceSubTabs .nav-tab").forEach((tab) => { tab.addEventListener("click", () => { const subtab = tab.dataset.subtab; document.querySelectorAll("#sourceSubTabs .nav-tab").forEach((t) => t.classList.remove("active")); tab.classList.add("active"); document.querySelectorAll("#sec-sources > .section").forEach((s) => s.classList.remove("active")); document.getElementById("sub-" + subtab).classList.add("active"); if (subtab === "global-sources") loadGlobalSources(); else if (subtab === "tenant-sources") loadTenantSources(); else if (subtab === "source-health") loadHealthData(); }); }); } // --- Grundquellen --- async function loadGlobalSources() { try { globalSourcesCache = await API.get("/api/sources/global"); renderGlobalSources(globalSourcesCache); } catch (err) { console.error("Grundquellen laden fehlgeschlagen:", err); } } function renderGlobalSources(sources) { const tbody = document.getElementById("globalSourceTable"); const cols = 7; if (sources.length === 0) { tbody.innerHTML = `Keine Grundquellen`; return; } // Nach Kategorie gruppieren (Reihenfolge beibehalten) const grouped = {}; const order = []; sources.forEach((s) => { const cat = s.category || "sonstige"; if (!grouped[cat]) { grouped[cat] = []; order.push(cat); } grouped[cat].push(s); }); let html = ""; order.forEach((cat) => { const label = CATEGORY_LABELS[cat] || cat; const count = grouped[cat].length; html += `${esc(label)}${count}`; grouped[cat].forEach((s) => { const hasNotes = s.notes && s.notes.trim(); const infoBtn = hasNotes ? `` : ''; const notesRow = hasNotes ? `${esc(s.notes)}` : ''; html += ` ${infoBtn} ${esc(s.name)} ${esc(s.url || "-")} ${esc(s.domain || "-")} ${TYPE_LABELS[s.source_type] || s.source_type} ${s.article_count || 0} ${s.status === "active" ? "Aktiv" : "Inaktiv"} ${notesRow}`; }); }); tbody.innerHTML = html; document.getElementById("globalSourceCount").textContent = `${sources.length} Grundquellen`; // Sort-Icons aktualisieren document.querySelectorAll("th.sortable .sort-icon").forEach(el => el.textContent = ""); const activeHeader = document.querySelector(`th.sortable[data-sort="${globalSortField}"] .sort-icon`); if (activeHeader) activeHeader.textContent = globalSortAsc ? " ▲" : " ▼"; } // Filter + Sortierung document.addEventListener("DOMContentLoaded", () => { const el = document.getElementById("globalSourceSearch"); if (el) { el.addEventListener("input", () => filterGlobalSources()); } }); function filterGlobalSources() { const q = (document.getElementById("globalSourceSearch")?.value || "").toLowerCase(); const typeFilter = document.getElementById("globalFilterType")?.value || ""; const catFilter = document.getElementById("globalFilterCategory")?.value || ""; const statusFilter = document.getElementById("globalFilterStatus")?.value || ""; let filtered = globalSourcesCache.filter((s) => { if (q && !(s.name.toLowerCase().includes(q) || (s.domain || "").toLowerCase().includes(q) || (s.url || "").toLowerCase().includes(q))) return false; if (typeFilter && s.source_type !== typeFilter) return false; if (catFilter && s.category !== catFilter) return false; if (statusFilter && s.status !== statusFilter) return false; return true; }); // Sortierung anwenden filtered.sort((a, b) => { let va = a[globalSortField] ?? ""; let vb = b[globalSortField] ?? ""; if (globalSortField === "article_count") { va = va || 0; vb = vb || 0; return globalSortAsc ? va - vb : vb - va; } va = String(va).toLowerCase(); vb = String(vb).toLowerCase(); const cmp = va.localeCompare(vb, "de"); return globalSortAsc ? cmp : -cmp; }); renderGlobalSources(filtered); } function sortGlobalSources(field) { if (globalSortField === field) { globalSortAsc = !globalSortAsc; } else { globalSortField = field; globalSortAsc = true; } filterGlobalSources(); } // --- Grundquelle erstellen/bearbeiten --- function openNewGlobalSource() { editingSourceId = null; document.getElementById("sourceModalTitle").textContent = "Neue Grundquelle"; document.getElementById("sourceForm").reset(); openModal("modalSource"); } function editGlobalSource(id) { const s = globalSourcesCache.find((x) => x.id === id); if (!s) return; editingSourceId = id; document.getElementById("sourceModalTitle").textContent = "Grundquelle bearbeiten"; document.getElementById("sourceName").value = s.name; document.getElementById("sourceUrl").value = s.url || ""; document.getElementById("sourceDomain").value = s.domain || ""; document.getElementById("sourceType").value = s.source_type; document.getElementById("sourceCategory").value = s.category; document.getElementById("sourceStatus").value = s.status; document.getElementById("sourceNotes").value = s.notes || ""; openModal("modalSource"); } function setupSourceForms() { document.getElementById("newGlobalSourceBtn").addEventListener("click", openNewGlobalSource); document.getElementById("discoverSourceBtn").addEventListener("click", () => { document.getElementById("discoverUrl").value = ""; document.getElementById("discoverStatus").style.display = "none"; document.getElementById("discoverResults").style.display = "none"; openModal("modalDiscover"); }); document.getElementById("sourceForm").addEventListener("submit", async (e) => { e.preventDefault(); const errEl = document.getElementById("sourceError"); errEl.style.display = "none"; const body = { name: document.getElementById("sourceName").value, url: document.getElementById("sourceUrl").value || null, domain: document.getElementById("sourceDomain").value || null, source_type: document.getElementById("sourceType").value, category: document.getElementById("sourceCategory").value, status: document.getElementById("sourceStatus").value, notes: document.getElementById("sourceNotes").value || null, }; try { if (editingSourceId) { await API.put("/api/sources/global/" + editingSourceId, body); } else { await API.post("/api/sources/global", body); } closeModal("modalSource"); loadGlobalSources(); } catch (err) { errEl.textContent = err.message; errEl.style.display = "block"; } }); // Domain aus URL ableiten document.getElementById("sourceUrl").addEventListener("blur", (e) => { const domainField = document.getElementById("sourceDomain"); if (domainField.value) return; try { const url = new URL(e.target.value); domainField.value = url.hostname.replace(/^www\./, ""); } catch (_) {} }); } function confirmDeleteGlobalSource(id, name) { showConfirm( "Grundquelle löschen", `Soll die Grundquelle "${name}" endgültig gelöscht werden? Sie wird für alle Monitore entfernt.`, async () => { try { await API.del("/api/sources/global/" + id); loadGlobalSources(); } catch (err) { alert(err.message); } } ); } // --- Kundenquellen --- async function loadTenantSources() { try { tenantSourcesCache = await API.get("/api/sources/tenant"); renderTenantSources(tenantSourcesCache); } catch (err) { console.error("Kundenquellen laden fehlgeschlagen:", err); } } function renderTenantSources(sources) { const tbody = document.getElementById("tenantSourceTable"); if (sources.length === 0) { tbody.innerHTML = 'Keine Kundenquellen'; return; } tbody.innerHTML = sources.map((s) => ` ${esc(s.name)} ${esc(s.domain || "-")} ${TYPE_LABELS[s.source_type] || s.source_type} ${CATEGORY_LABELS[s.category] || s.category} ${esc(s.org_name || "-")} ${esc(s.added_by || "-")} `).join(""); document.getElementById("tenantSourceCount").textContent = `${sources.length} Kundenquellen`; } // Suche Kundenquellen document.addEventListener("DOMContentLoaded", () => { const el = document.getElementById("tenantSourceSearch"); if (el) { el.addEventListener("input", () => { const q = el.value.toLowerCase(); const filtered = tenantSourcesCache.filter((s) => s.name.toLowerCase().includes(q) || (s.domain || "").toLowerCase().includes(q) || (s.org_name || "").toLowerCase().includes(q) ); renderTenantSources(filtered); }); } }); function promoteSource(id, name) { showConfirm( "Zur Grundquelle machen", `Soll "${name}" als Grundquelle übernommen werden? Sie wird dann für alle Monitore verfügbar.`, async () => { try { await API.post("/api/sources/tenant/" + id + "/promote"); loadTenantSources(); } catch (err) { alert(err.message); } } ); } // --- Discovery --- let discoveredFeeds = []; async function runDiscover() { const url = document.getElementById("discoverUrl").value.trim(); if (!url) return; const btn = document.getElementById("discoverBtn"); const statusEl = document.getElementById("discoverStatus"); const resultsEl = document.getElementById("discoverResults"); btn.disabled = true; btn.textContent = "Suche..."; statusEl.style.display = "block"; statusEl.textContent = "Analysiere Website und suche RSS-Feeds..."; resultsEl.style.display = "none"; try { const data = await API.post("/api/sources/discover?url=" + encodeURIComponent(url)); discoveredFeeds = data.feeds || []; if (discoveredFeeds.length === 0 && (!data.existing || data.existing.length === 0)) { statusEl.textContent = data.message || "Keine RSS-Feeds gefunden für " + data.domain; return; } statusEl.style.display = "none"; resultsEl.style.display = "block"; // Bereits vorhandene anzeigen const existingEl = document.getElementById("discoverExisting"); if (data.existing && data.existing.length > 0) { existingEl.style.display = "block"; existingEl.innerHTML = '
Bereits als Grundquelle vorhanden:
' + data.existing.map(f => '
✓ ' + esc(f.name) + '
').join(""); } else { existingEl.style.display = "none"; } // Neue Feeds mit Checkboxen const feedsEl = document.getElementById("discoverFeeds"); if (discoveredFeeds.length > 0) { feedsEl.innerHTML = '
Neue Feeds gefunden (' + data.domain + ', ' + (CATEGORY_LABELS[data.category] || data.category) + '):
' + discoveredFeeds.map((f, i) => ` `).join(""); document.getElementById("addDiscoveredBtn").style.display = ""; } else { feedsEl.innerHTML = '
Alle Feeds dieser Domain sind bereits als Grundquellen vorhanden.
'; document.getElementById("addDiscoveredBtn").style.display = "none"; } } catch (err) { statusEl.textContent = "Fehler: " + err.message; } finally { btn.disabled = false; btn.textContent = "Erkennen"; } } async function addDiscoveredFeeds() { const checkboxes = document.querySelectorAll("#discoverFeeds input[type=checkbox]:checked"); const selected = []; checkboxes.forEach(cb => { const idx = parseInt(cb.dataset.idx); if (discoveredFeeds[idx]) selected.push(discoveredFeeds[idx]); }); if (selected.length === 0) { alert("Keine Feeds ausgewählt"); return; } const btn = document.getElementById("addDiscoveredBtn"); btn.disabled = true; btn.textContent = "Wird hinzugefügt..."; try { const result = await API.post("/api/sources/discover/add", selected); closeModal("modalDiscover"); loadGlobalSources(); alert(result.added + " Grundquelle(n) hinzugefügt" + (result.skipped ? ", " + result.skipped + " übersprungen" : "")); } catch (err) { alert("Fehler: " + err.message); } finally { btn.disabled = false; btn.textContent = "Ausgewählte hinzufügen"; } } function toggleSourceInfo(id) { const row = document.getElementById("notes-" + id); if (!row) return; const isVisible = row.style.display !== "none"; row.style.display = isVisible ? "none" : "table-row"; const mainRow = row.previousElementSibling; if (mainRow) { const btn = mainRow.querySelector(".src-info-toggle"); if (btn) btn.classList.toggle("active", !isVisible); } }