/* 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 += ``;
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);
}
}