/* Vereinte Quellenliste (Grund- + Kundenquellen) */ "use strict"; // Hält Grund- UND Kundenquellen (GET /api/sources?scope=all) let globalSourcesCache = []; // Auswahl für Bulk-Promote (nur bei Herkunfts-Filter Kundenquellen sichtbar) let tenantSelected = new Set(); // Aktiver Herkunfts-Filter ("" = alle, "global", "tenant") let originFilter = ""; // Lazy-Cache für die Health-Ausklapp-Details je Quelle const sourceHealthDetailCache = new Map(); let editingSourceId = null; let globalSortField = "category"; let globalSortAsc = true; const CHECK_TYPE_LABELS = { reachability: "Erreichbarkeit", feed_validity: "Feed-Validität", stale: "Aktualität", duplicate: "Duplikat", }; // CATEGORY_LABELS jetzt global (aus app.js loadMeta) // TYPE_LABELS jetzt global (aus app.js loadMeta) // META muss geladen sein, bevor Dropdowns befüllt werden (loadMeta läuft async beim Start) async function ensureMeta() { if (!window.META || !window.META.categories || !window.META.categories.length) { if (typeof loadMeta === "function") await loadMeta(); } } // Modal-Selects ohne Leer-Option aus META befüllen function fillSelect(el, items, opts = {}) { if (!el) return; el.innerHTML = ""; (items || []).forEach((it) => { const o = document.createElement("option"); o.value = it.key; o.textContent = it.label + (opts.disabledKeys && opts.disabledKeys.includes(it.key) && opts.disabledHint ? " " + opts.disabledHint : ""); if (opts.disabledKeys && opts.disabledKeys.includes(it.key)) o.disabled = true; el.appendChild(o); }); if (opts.value !== undefined) el.value = opts.value; } function fillSourceModalSelects() { fillSelect(document.getElementById("sourceType"), (window.META && window.META.types) || [], { disabledKeys: ["pdf_document"], disabledHint: "(nur Upload)" }); fillSelect(document.getElementById("sourceCategory"), (window.META && window.META.categories) || []); } // --- Init --- document.addEventListener("DOMContentLoaded", () => { setupSourceSubTabs(); setupSourceForms(); // Beim Tab-Wechsel auf "Quellen" laden (+ Aufgaben-Badge aktualisieren) document.querySelectorAll('.nav-tab[data-section="sources"]').forEach((tab) => { tab.addEventListener("click", () => { loadUnifiedSources(); if (typeof refreshTasksBadge === "function") refreshTasksBadge(); }); }); }); 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") loadUnifiedSources(); else if (subtab === "aufgaben") loadAufgaben(); else if (subtab === "x-scraper") loadXScraperAccounts(); }); }); } // --- Vereinte Quellenliste --- async function loadUnifiedSources() { try { await ensureMeta(); // Kategorien/Typen-Dropdowns aus META befüllen (idempotent) if (window.META && window.META.categories && window.META.categories.length) { populateSelect(document.getElementById("globalFilterCategory"), window.META.categories, "Alle Kategorien"); populateSelect(document.getElementById("globalFilterType"), window.META.types || [], "Alle Typen"); } const [list, stats, languages] = await Promise.all([ API.get("/api/sources?scope=all"), API.get("/api/sources/stats"), API.get("/api/sources/languages").catch(() => []), ]); globalSourcesCache = list; sourceHealthDetailCache.clear(); populateSelect( document.getElementById("globalFilterLanguage"), (languages || []).map(l => ({ key: l, label: l })), "Alle Sprachen", ); // datalist fuer Edit-Modal const dl = document.getElementById("languageSuggestions"); if (dl) { dl.innerHTML = ""; (languages || []).forEach(l => { const o = document.createElement("option"); o.value = l; dl.appendChild(o); }); } renderUnifiedStats(stats); filterUnifiedSources(); checkHealthRunOnLoad(); } catch (err) { console.error("Quellenliste laden fehlgeschlagen:", err); } } async function showSourceAudit(sourceId, sourceName) { document.getElementById("auditTitle").textContent = `Audit-Spur: ${sourceName}`; document.getElementById("auditContent").innerHTML = '
Lade...
'; openModal("modalAudit"); try { const res = await API.get(`/api/audit-log?resource_type=source&resource_id=${sourceId}&limit=50`); renderAuditEntries(res.items || []); } catch (err) { document.getElementById("auditContent").innerHTML = `
Audit konnte nicht geladen werden: ${esc(err.message || String(err))}
`; } } function renderAuditEntries(items) { const c = document.getElementById("auditContent"); if (!items.length) { c.innerHTML = '
Keine Audit-Einträge für diese Quelle.
'; return; } c.innerHTML = items.map(e => { const meta = `${formatDateTime(e.ts)} · ${esc(e.admin_username || "-")} · ${esc(e.ip || "-")}`; const hasDiff = (e.before && Object.keys(e.before).length) || (e.after && Object.keys(e.after).length); const diffPayload = JSON.stringify({ before: e.before, after: e.after }, null, 2); return `
${esc(e.action)}
${hasDiff ? `
Diff anzeigen
${esc(diffPayload)}
` : ""}
`; }).join(""); } function formatDateTime(iso) { if (!iso) return "-"; try { const d = new Date(iso); return d.toLocaleString("de-DE", { day: "2-digit", month: "2-digit", year: "numeric", hour: "2-digit", minute: "2-digit", }); } catch { return iso; } } function renderUnifiedStats(stats) { const bar = document.getElementById("globalStatsBar"); if (!bar) return; if (!stats || !stats.by_type) { bar.innerHTML = ""; return; } const types = window.META && window.META.types ? window.META.types : []; const parts = []; parts.push(`${stats.total || 0} Quellen gesamt`); const bo = stats.by_origin || {}; if (bo.tenant) { parts.push(`${bo.global || 0} Grundquellen`); parts.push(`${bo.tenant} Kundenquellen`); } for (const t of types) { const v = stats.by_type[t.key] || { count: 0, articles: 0 }; parts.push(`${v.count} ${esc(t.label)}`); } parts.push(`${stats.total_articles || 0} Artikel`); const h = stats.health || { errors: 0, warnings: 0, ok: 0 }; if (h.errors) parts.push(`${h.errors} Fehler`); if (h.warnings) parts.push(`${h.warnings} Warnungen`); if (h.ok) parts.push(`${h.ok} OK`); const lastCheck = stats.last_check ? formatDateTime(stats.last_check) : "noch nie"; parts.push(`Zuletzt geprüft ${lastCheck}`); bar.innerHTML = parts.join(""); if (healthRunPollTimer) setHealthRunButton(true); } // --- Manueller Health-Run (Knopf in der Stats-Leiste) --- let healthRunPollTimer = null; async function startHealthRun() { try { await API.post("/api/sources/health/run", {}); setHealthRunButton(true); beginHealthRunPolling(); showToast("Prüfung gestartet. Das kann einige Minuten dauern.", "info"); } catch (err) { if (String(err.message || "").includes("bereits")) { showToast("Es läuft bereits eine Prüfung.", "warning"); setHealthRunButton(true); beginHealthRunPolling(); } else { showToast("Start fehlgeschlagen. " + err.message, "error"); } } } function beginHealthRunPolling() { if (healthRunPollTimer) return; healthRunPollTimer = setInterval(pollHealthRunStatus, 4000); } async function pollHealthRunStatus() { try { const st = await API.get("/api/sources/health/run-status"); if (st.running) { setHealthRunButton(true); return; } // Lauf beendet if (healthRunPollTimer) { clearInterval(healthRunPollTimer); healthRunPollTimer = null; } setHealthRunButton(false); if (st.result) { const r = st.result; showToast(`Health-Check abgeschlossen. ${r.checked} geprüft, ${r.issues} Probleme, ${r.suggestions} neue Vorschläge.`, "success"); } else if (st.error) { showToast("Health-Check fehlgeschlagen. " + st.error, "error"); } else { showToast("Prüfung beendet, Ergebnis unbekannt (Server wurde neu gestartet).", "info"); } loadUnifiedSources(); if (typeof refreshTasksBadge === "function") refreshTasksBadge(); } catch (_) { /* nächster Tick versucht es erneut */ } } function setHealthRunButton(running) { const btn = document.getElementById("runHealthCheckBtn"); if (!btn) return; btn.disabled = running; btn.textContent = running ? "Prüfung läuft…" : "Jetzt prüfen"; } // Beim Öffnen des Quellen-Reiters einmal nachsehen, ob (z.B. aus einer anderen // Session) gerade ein Lauf aktiv ist, und dann das Polling aufnehmen. async function checkHealthRunOnLoad() { try { const st = await API.get("/api/sources/health/run-status"); if (st.running) { setHealthRunButton(true); beginHealthRunPolling(); } } catch (_) { /* unkritisch */ } } function renderUnifiedSources(sources) { const tbody = document.getElementById("globalSourceTable"); const cols = 14; if (sources.length === 0) { tbody.innerHTML = `Keine Quellen`; updateBulkButton(); 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 isTenant = s.tenant_id != null; const hasNotes = s.notes && s.notes.trim(); const infoBtn = hasNotes ? `` : ''; // EINE gemeinsame Detailzeile je Quelle: Notizen + Health-Details. const hasDetail = hasNotes || s.health_status; const detailRow = hasDetail ? `${hasNotes ? `
${esc(s.notes)}
` : ""}${s.health_status ? `
` : ""}` : ''; const lastSeen = s.last_seen_at ? formatDate(s.last_seen_at) : "-"; const hs = s.health_status || "unknown"; const hsLabel = { error: "Fehler", warning: "Warnung", ok: "OK", unknown: "—" }[hs]; const hsClass = "health-badge-" + (hs === "unknown" ? "unknown" : hs); const healthCell = s.health_status ? `${hsLabel}` : `${hsLabel}`; const selectCell = isTenant ? `` : ``; // Kundenquellen sind bewusst nicht editierbar: Workflow ist erst // "Übernehmen" (Promote zur Grundquelle), dann als Grundquelle bearbeiten. const actions = isTenant ? ` ` : ` `; html += ` ${selectCell} ${infoBtn} ${esc(s.name)} ${esc(s.url || "-")} ${esc(s.domain || "-")} ${typeLabel(s.source_type)} ${s.article_count || 0} ${s.articles_7d || 0} / ${s.articles_30d || 0} ${esc(s.language || "-")} ${esc(s.bias || "-")} ${lastSeen} ${healthCell} ${s.status === "active" ? "Aktiv" : "Inaktiv"} ${isTenant ? esc(s.org_name || ("Org " + s.tenant_id)) : ''} ${actions} ${detailRow}`; }); }); tbody.innerHTML = html; const originLabel = originFilter === "global" ? "Grundquellen" : originFilter === "tenant" ? "Kundenquellen" : "Quellen"; document.getElementById("globalSourceCount").textContent = `${sources.length} ${originLabel}`; updateBulkButton(); // Sort-Icons aktualisieren document.querySelectorAll("#unifiedTable th.sortable .sort-icon").forEach(el => el.textContent = ""); const activeHeader = document.querySelector(`#unifiedTable 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", () => filterUnifiedSources()); } }); function filterUnifiedSources() { 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 || ""; const langFilter = document.getElementById("globalFilterLanguage")?.value || ""; const newOrigin = document.getElementById("filterOrigin")?.value || ""; if (newOrigin !== originFilter) { originFilter = newOrigin; tenantSelected.clear(); } updateOriginControls(); let filtered = globalSourcesCache.filter((s) => { if (originFilter === "global" && s.tenant_id != null) return false; if (originFilter === "tenant" && s.tenant_id == null) return false; if (q && !(s.name.toLowerCase().includes(q) || (s.domain || "").toLowerCase().includes(q) || (s.url || "").toLowerCase().includes(q) || (s.bias || "").toLowerCase().includes(q) || (s.org_name || "").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; if (langFilter && s.language !== langFilter) return false; return true; }); // Sortierung anwenden filtered.sort((a, b) => { let va = a[globalSortField] ?? ""; let vb = b[globalSortField] ?? ""; const NUMERIC_FIELDS = ["article_count", "articles_7d", "articles_30d"]; if (NUMERIC_FIELDS.includes(globalSortField)) { va = parseInt(va) || 0; vb = parseInt(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; }); renderUnifiedSources(filtered); } function sortUnifiedSources(field) { if (globalSortField === field) { globalSortAsc = !globalSortAsc; } else { globalSortField = field; globalSortAsc = true; } filterUnifiedSources(); } // Checkbox-Spalte + Bulk-Knopf nur bei Herkunfts-Filter "Kundenquellen" function updateOriginControls() { const showSelect = originFilter === "tenant"; const table = document.getElementById("unifiedTable"); if (table) table.classList.toggle("show-select", showSelect); const btn = document.getElementById("tenantBulkPromoteBtn"); if (btn) btn.style.display = showSelect ? "" : "none"; } // --- Grundquelle erstellen/bearbeiten --- async function openNewGlobalSource() { editingSourceId = null; document.getElementById("sourceModalTitle").textContent = "Neue Grundquelle"; document.getElementById("sourceForm").reset(); await ensureMeta(); fillSourceModalSelects(); document.getElementById("sourceType").value = "rss_feed"; document.getElementById("sourceCategory").value = "sonstige"; openModal("modalSource"); } async function editGlobalSource(id) { const s = globalSourcesCache.find((x) => x.id === id); if (!s) return; editingSourceId = id; await ensureMeta(); fillSourceModalSelects(); 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 || ""; document.getElementById("sourceLanguage").value = s.language || ""; document.getElementById("sourceBias").value = s.bias || ""; document.getElementById("sourceFetchStrategy").value = s.fetch_strategy || "default"; document.getElementById("sourcePolitical").value = s.political_orientation || ""; document.getElementById("sourceMediaType").value = s.media_type || ""; document.getElementById("sourceReliability").value = s.reliability || ""; document.getElementById("sourceCountryCode").value = s.country_code || ""; document.getElementById("sourceStateAffiliated").checked = !!s.state_affiliated; openModal("modalSource"); } function setupSourceForms() { document.getElementById("newGlobalSourceBtn").addEventListener("click", openNewGlobalSource); document.getElementById("newPdfSourceBtn")?.addEventListener("click", openPdfUploadModal); setupPdfUploadForm(); 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, status: document.getElementById("sourceStatus").value, notes: document.getElementById("sourceNotes").value || null, language: document.getElementById("sourceLanguage").value || null, bias: document.getElementById("sourceBias").value || null, fetch_strategy: document.getElementById("sourceFetchStrategy").value || "default", }; // Leere Werte NIE mitsenden. Ein leerer Select-Wert (z.B. META noch nicht // geladen) würde sonst die Kategorie serverseitig still überschreiben. const st = document.getElementById("sourceType").value; if (st) body.source_type = st; const cat = document.getElementById("sourceCategory").value; if (cat) body.category = cat; const pol = document.getElementById("sourcePolitical")?.value; if (pol) body.political_orientation = pol; const mt = document.getElementById("sourceMediaType")?.value; if (mt) body.media_type = mt; const rel = document.getElementById("sourceReliability")?.value; if (rel) body.reliability = rel; const cc = (document.getElementById("sourceCountryCode")?.value || "").trim().toUpperCase(); if (cc) body.country_code = cc; if (editingSourceId) { body.state_affiliated = !!document.getElementById("sourceStateAffiliated")?.checked; } try { if (editingSourceId) { await API.put("/api/sources/global/" + editingSourceId, body); } else { await API.post("/api/sources/global", body); } closeModal("modalSource"); loadUnifiedSources(); } 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); loadUnifiedSources(); } catch (err) { showToast(err.message, "error"); } } ); } // --- Bulk-Promote (Kundenquellen in der vereinten Liste) --- function toggleTenantSelectAll(checked) { document.querySelectorAll("#globalSourceTable input.tenant-select").forEach(cb => { cb.checked = checked; const id = parseInt(cb.dataset.id); if (checked) tenantSelected.add(id); else tenantSelected.delete(id); }); updateBulkButton(); } function toggleTenantSelect(id, checked) { id = parseInt(id); if (checked) tenantSelected.add(id); else tenantSelected.delete(id); updateBulkButton(); // Header-Checkbox anpassen const visible = document.querySelectorAll("#globalSourceTable input.tenant-select").length; const checkedVisible = document.querySelectorAll("#globalSourceTable input.tenant-select:checked").length; const all = document.getElementById("unifiedSelectAll"); if (all) all.checked = visible > 0 && visible === checkedVisible; } function updateBulkButton() { const btn = document.getElementById("tenantBulkPromoteBtn"); if (!btn) return; const n = tenantSelected.size; btn.disabled = n === 0; btn.textContent = `Ausgewählte übernehmen (${n})`; } async function bulkPromoteSelected() { if (tenantSelected.size === 0) return; const ids = Array.from(tenantSelected); const ok = await showConfirm( "Ausgewählte als Grundquelle übernehmen", `Sollen ${ids.length} Kundenquelle(n) als Grundquelle übernommen werden? Sie werden dann für alle Monitore verfügbar.`, ); if (!ok) return; try { const result = await API.post("/api/sources/tenant/bulk-promote", { source_ids: ids }); let msg = `${result.promoted} übernommen`; if (result.skipped && result.skipped.length) msg += `, ${result.skipped.length} übersprungen`; if (result.failed && result.failed.length) msg += `, ${result.failed.length} Fehler`; showToast(msg, result.failed && result.failed.length ? "warning" : "success"); tenantSelected.clear(); await loadUnifiedSources(); } catch (err) { showToast("Bulk-Promote fehlgeschlagen: " + err.message, "error"); } } 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"); tenantSelected.delete(id); loadUnifiedSources(); } catch (err) { showToast(err.message, "error"); } } ); } // --- Health-Details in der Ausklapp-Zeile --- async function toggleHealthDetail(id) { const row = document.getElementById("detail-" + id); if (!row) return; const isVisible = row.style.display !== "none"; toggleSourceInfo(id); if (isVisible) return; // wurde gerade zugeklappt const box = document.getElementById("health-detail-" + id); if (!box) return; if (sourceHealthDetailCache.has(id)) { renderHealthDetail(id, sourceHealthDetailCache.get(id)); return; } box.innerHTML = '
Lade Health-Details…
'; try { const res = await API.get("/api/sources/" + id + "/health"); sourceHealthDetailCache.set(id, res.checks || []); renderHealthDetail(id, res.checks || []); } catch (err) { box.innerHTML = `
Health-Details konnten nicht geladen werden. ${esc(err.message || "")}
`; } } function renderHealthDetail(id, checks) { const box = document.getElementById("health-detail-" + id); if (!box) return; const s = globalSourcesCache.find((x) => x.id === id); const deactivateBtn = s && s.status === "active" ? `` : ""; if (!checks.length) { box.innerHTML = `
Keine Health-Checks für diese Quelle vorhanden.${deactivateBtn}
`; return; } const statusLabel = (st) => st === "error" ? "Fehler" : st === "warning" ? "Warnung" : "OK"; const rows = checks.map((c) => `
${statusLabel(c.status)} ${CHECK_TYPE_LABELS[c.check_type] || esc(c.check_type)} ${esc(c.message || "")} ${formatDateTime(c.checked_at)}
`).join(""); box.innerHTML = `
Health-Checks${deactivateBtn}
${rows}`; } function deactivateSource(id, name) { showConfirm( "Quelle deaktivieren", `Soll die Quelle "${name}" deaktiviert werden? Sie wird dann nicht mehr abgerufen.`, async () => { try { await API.put("/api/sources/global/" + id, { status: "inactive" }); showToast("Quelle wurde deaktiviert.", "success"); sourceHealthDetailCache.delete(id); loadUnifiedSources(); } catch (err) { showToast(err.message, "error"); } } ); } // --- 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) { showToast("Keine Feeds ausgewählt", "warning"); 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"); loadUnifiedSources(); showToast(result.added + " Grundquelle(n) hinzugefügt" + (result.skipped ? ", " + result.skipped + " übersprungen" : ""), "success"); } catch (err) { showToast("Fehler: " + err.message, "error"); } finally { btn.disabled = false; btn.textContent = "Ausgewählte hinzufügen"; } } function toggleSourceInfo(id) { const row = document.getElementById("detail-" + 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); } } // --- PDF-Quellen-Upload --- async function openPdfUploadModal() { const form = document.getElementById("pdfUploadForm"); if (form) form.reset(); await ensureMeta(); fillSelect(document.getElementById("pdfCategory"), (window.META && window.META.categories) || [], { value: "sonstige" }); const err = document.getElementById("pdfUploadError"); if (err) { err.style.display = "none"; err.textContent = ""; } const prog = document.getElementById("pdfUploadProgress"); if (prog) prog.style.display = "none"; openModal("modalPdfUpload"); } function setupPdfUploadForm() { const form = document.getElementById("pdfUploadForm"); if (!form || form.dataset.bound === "1") return; form.dataset.bound = "1"; form.addEventListener("submit", async (e) => { e.preventDefault(); const errEl = document.getElementById("pdfUploadError"); const progEl = document.getElementById("pdfUploadProgress"); const submitBtn = document.getElementById("pdfUploadSubmitBtn"); errEl.style.display = "none"; const fileInput = document.getElementById("pdfFile"); const f = fileInput?.files?.[0]; if (!f) { errEl.textContent = "Bitte eine PDF-Datei auswaehlen."; errEl.style.display = "block"; return; } if (f.size > 50 * 1024 * 1024) { errEl.textContent = "Datei ueberschreitet 50 MB."; errEl.style.display = "block"; return; } const fd = new FormData(); fd.append("file", f); const nm = document.getElementById("pdfName").value.trim(); if (nm) fd.append("name", nm); fd.append("category", document.getElementById("pdfCategory").value || "sonstige"); const lng = document.getElementById("pdfLanguage").value.trim(); if (lng) fd.append("language", lng); const nt = document.getElementById("pdfNotes").value.trim(); if (nt) fd.append("notes", nt); submitBtn.disabled = true; progEl.style.display = "block"; try { await API.upload("/api/sources/global/upload-pdf", fd); closeModal("modalPdfUpload"); if (typeof showToast === "function") { showToast("PDF hochgeladen -- Verarbeitung laeuft im Hintergrund", "success"); } loadUnifiedSources(); } catch (err) { errEl.textContent = err.message || "Upload fehlgeschlagen"; errEl.style.display = "block"; } finally { submitBtn.disabled = false; progEl.style.display = "none"; } }); }