feat: Discovery-Funktion in Grundquellen-Verwaltung integriert
- POST /api/sources/discover: URL analysieren, RSS-Feeds erkennen, Duplikate prüfen - POST /api/sources/discover/add: Erkannte Feeds als Grundquellen anlegen (inkl. Web-Source) - Erkennen-Button und Modal im Dashboard mit Feed-Auswahl per Checkbox - Duplikat-Erkennung zeigt bereits vorhandene Grundquellen an - source_rules aus Monitor importiert für Feed-Discovery und Claude-Bewertung - config.py um Discovery-Konfiguration erweitert Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Dieser Commit ist enthalten in:
@@ -1,242 +1,342 @@
|
||||
/* Grundquellen & Kundenquellen Management */
|
||||
"use strict";
|
||||
|
||||
let globalSourcesCache = [];
|
||||
let tenantSourcesCache = [];
|
||||
let editingSourceId = null;
|
||||
|
||||
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",
|
||||
};
|
||||
|
||||
const TYPE_LABELS = {
|
||||
rss_feed: "RSS-Feed",
|
||||
web_source: "Webquelle",
|
||||
excluded: "Gesperrt",
|
||||
};
|
||||
|
||||
// --- 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();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// --- 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");
|
||||
if (sources.length === 0) {
|
||||
tbody.innerHTML = '<tr><td colspan="7" class="text-muted">Keine Grundquellen</td></tr>';
|
||||
return;
|
||||
}
|
||||
tbody.innerHTML = sources.map((s) => `
|
||||
<tr>
|
||||
<td>${esc(s.name)}</td>
|
||||
<td class="text-secondary" style="max-width:200px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;" title="${esc(s.url || '')}">${esc(s.url || "-")}</td>
|
||||
<td>${esc(s.domain || "-")}</td>
|
||||
<td>${TYPE_LABELS[s.source_type] || s.source_type}</td>
|
||||
<td>${CATEGORY_LABELS[s.category] || s.category}</td>
|
||||
<td><span class="badge badge-${s.status === "active" ? "active" : "inactive"}">${s.status === "active" ? "Aktiv" : "Inaktiv"}</span></td>
|
||||
<td>
|
||||
<button class="btn btn-secondary btn-small" onclick="editGlobalSource(${s.id})">Bearbeiten</button>
|
||||
<button class="btn btn-danger btn-small" onclick="confirmDeleteGlobalSource(${s.id}, '${esc(s.name)}')">Loeschen</button>
|
||||
</td>
|
||||
</tr>
|
||||
`).join("");
|
||||
|
||||
document.getElementById("globalSourceCount").textContent = `${sources.length} Grundquellen`;
|
||||
}
|
||||
|
||||
// Suche
|
||||
document.addEventListener("DOMContentLoaded", () => {
|
||||
const el = document.getElementById("globalSourceSearch");
|
||||
if (el) {
|
||||
el.addEventListener("input", () => {
|
||||
const q = el.value.toLowerCase();
|
||||
const filtered = globalSourcesCache.filter((s) =>
|
||||
s.name.toLowerCase().includes(q) || (s.domain || "").toLowerCase().includes(q) || (s.category || "").toLowerCase().includes(q)
|
||||
);
|
||||
renderGlobalSources(filtered);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// --- 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("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 loeschen",
|
||||
`Soll die Grundquelle "${name}" endgueltig geloescht werden? Sie wird fuer 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 = '<tr><td colspan="7" class="text-muted">Keine Kundenquellen</td></tr>';
|
||||
return;
|
||||
}
|
||||
tbody.innerHTML = sources.map((s) => `
|
||||
<tr>
|
||||
<td>${esc(s.name)}</td>
|
||||
<td class="text-secondary" style="max-width:180px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;" title="${esc(s.url || '')}">${esc(s.domain || "-")}</td>
|
||||
<td>${TYPE_LABELS[s.source_type] || s.source_type}</td>
|
||||
<td>${CATEGORY_LABELS[s.category] || s.category}</td>
|
||||
<td>${esc(s.org_name || "-")}</td>
|
||||
<td>${esc(s.added_by || "-")}</td>
|
||||
<td>
|
||||
<button class="btn btn-primary btn-small" onclick="promoteSource(${s.id}, '${esc(s.name)}')">Uebernehmen</button>
|
||||
</td>
|
||||
</tr>
|
||||
`).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 uebernommen werden? Sie wird dann fuer alle Monitore verfuegbar.`,
|
||||
async () => {
|
||||
try {
|
||||
await API.post("/api/sources/tenant/" + id + "/promote");
|
||||
loadTenantSources();
|
||||
} catch (err) {
|
||||
alert(err.message);
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
/* Grundquellen & Kundenquellen Management */
|
||||
"use strict";
|
||||
|
||||
let globalSourcesCache = [];
|
||||
let tenantSourcesCache = [];
|
||||
let editingSourceId = null;
|
||||
|
||||
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",
|
||||
};
|
||||
|
||||
const TYPE_LABELS = {
|
||||
rss_feed: "RSS-Feed",
|
||||
web_source: "Webquelle",
|
||||
excluded: "Gesperrt",
|
||||
};
|
||||
|
||||
// --- 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();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// --- 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");
|
||||
if (sources.length === 0) {
|
||||
tbody.innerHTML = '<tr><td colspan="7" class="text-muted">Keine Grundquellen</td></tr>';
|
||||
return;
|
||||
}
|
||||
tbody.innerHTML = sources.map((s) => `
|
||||
<tr>
|
||||
<td>${esc(s.name)}</td>
|
||||
<td class="text-secondary" style="max-width:200px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;" title="${esc(s.url || '')}">${esc(s.url || "-")}</td>
|
||||
<td>${esc(s.domain || "-")}</td>
|
||||
<td>${TYPE_LABELS[s.source_type] || s.source_type}</td>
|
||||
<td>${CATEGORY_LABELS[s.category] || s.category}</td>
|
||||
<td><span class="badge badge-${s.status === "active" ? "active" : "inactive"}">${s.status === "active" ? "Aktiv" : "Inaktiv"}</span></td>
|
||||
<td>
|
||||
<button class="btn btn-secondary btn-small" onclick="editGlobalSource(${s.id})">Bearbeiten</button>
|
||||
<button class="btn btn-danger btn-small" onclick="confirmDeleteGlobalSource(${s.id}, '${esc(s.name)}')">Loeschen</button>
|
||||
</td>
|
||||
</tr>
|
||||
`).join("");
|
||||
|
||||
document.getElementById("globalSourceCount").textContent = `${sources.length} Grundquellen`;
|
||||
}
|
||||
|
||||
// Suche
|
||||
document.addEventListener("DOMContentLoaded", () => {
|
||||
const el = document.getElementById("globalSourceSearch");
|
||||
if (el) {
|
||||
el.addEventListener("input", () => {
|
||||
const q = el.value.toLowerCase();
|
||||
const filtered = globalSourcesCache.filter((s) =>
|
||||
s.name.toLowerCase().includes(q) || (s.domain || "").toLowerCase().includes(q) || (s.category || "").toLowerCase().includes(q)
|
||||
);
|
||||
renderGlobalSources(filtered);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// --- 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 loeschen",
|
||||
`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 = '<tr><td colspan="7" class="text-muted">Keine Kundenquellen</td></tr>';
|
||||
return;
|
||||
}
|
||||
tbody.innerHTML = sources.map((s) => `
|
||||
<tr>
|
||||
<td>${esc(s.name)}</td>
|
||||
<td class="text-secondary" style="max-width:180px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;" title="${esc(s.url || '')}">${esc(s.domain || "-")}</td>
|
||||
<td>${TYPE_LABELS[s.source_type] || s.source_type}</td>
|
||||
<td>${CATEGORY_LABELS[s.category] || s.category}</td>
|
||||
<td>${esc(s.org_name || "-")}</td>
|
||||
<td>${esc(s.added_by || "-")}</td>
|
||||
<td>
|
||||
<button class="btn btn-primary btn-small" onclick="promoteSource(${s.id}, '${esc(s.name)}')">Uebernehmen</button>
|
||||
</td>
|
||||
</tr>
|
||||
`).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 = '<div class="text-secondary" style="font-size:12px;margin-bottom:8px;">Bereits als Grundquelle vorhanden:</div>' +
|
||||
data.existing.map(f => '<div style="padding:4px 0;font-size:13px;color:var(--text-tertiary);">✓ ' + esc(f.name) + '</div>').join("");
|
||||
} else {
|
||||
existingEl.style.display = "none";
|
||||
}
|
||||
|
||||
// Neue Feeds mit Checkboxen
|
||||
const feedsEl = document.getElementById("discoverFeeds");
|
||||
if (discoveredFeeds.length > 0) {
|
||||
feedsEl.innerHTML = '<div class="text-secondary" style="font-size:12px;margin-bottom:8px;">Neue Feeds gefunden (' + data.domain + ', ' + (CATEGORY_LABELS[data.category] || data.category) + '):</div>' +
|
||||
discoveredFeeds.map((f, i) => `
|
||||
<label style="display:flex;align-items:center;gap:8px;padding:6px 0;font-size:13px;cursor:pointer;">
|
||||
<input type="checkbox" checked data-idx="${i}">
|
||||
<span>${esc(f.name)}</span>
|
||||
<span class="text-secondary" style="font-size:11px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;max-width:250px;" title="${esc(f.url)}">${esc(f.url)}</span>
|
||||
</label>
|
||||
`).join("");
|
||||
document.getElementById("addDiscoveredBtn").style.display = "";
|
||||
} else {
|
||||
feedsEl.innerHTML = '<div class="text-muted" style="font-size:13px;">Alle Feeds dieser Domain sind bereits als Grundquellen vorhanden.</div>';
|
||||
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 ausgewaehlt");
|
||||
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";
|
||||
}
|
||||
}
|
||||
|
||||
In neuem Issue referenzieren
Einen Benutzer sperren