Quellenverwaltung: Filter, Sortierung, Artikelzähler, Umlaute

- Filter-Dropdowns für Typ, Kategorie und Status
- Sortierbare Spalten (Name, Domain, Typ, Kategorie, Artikel, Status)
- Artikel-Spalte zeigt article_count an
- Umlaute korrigiert (Löschen, Übernehmen, Läuft ab, Hinzugefügt von)
- 53 kaputte/doppelte Quellen bereinigt (Bashinho, Netzpolitik, Facebook)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Dieser Commit ist enthalten in:
claude-dev
2026-03-08 14:51:31 +01:00
Ursprung 801944a7ea
Commit dbd5568296
3 geänderte Dateien mit 118 neuen und 22 gelöschten Zeilen

Datei anzeigen

@@ -4,6 +4,8 @@
let globalSourcesCache = [];
let tenantSourcesCache = [];
let editingSourceId = null;
let globalSortField = "name";
let globalSortAsc = true;
const CATEGORY_LABELS = {
nachrichtenagentur: "Nachrichtenagentur",
@@ -21,7 +23,7 @@ const CATEGORY_LABELS = {
const TYPE_LABELS = {
rss_feed: "RSS-Feed",
web_source: "Webquelle",
excluded: "Gesperrt",
excluded: "Ausgeschlossen",
};
// --- Init ---
@@ -63,7 +65,7 @@ async function loadGlobalSources() {
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>';
tbody.innerHTML = '<tr><td colspan="8" class="text-muted">Keine Grundquellen</td></tr>';
return;
}
tbody.innerHTML = sources.map((s) => `
@@ -73,31 +75,72 @@ function renderGlobalSources(sources) {
<td>${esc(s.domain || "-")}</td>
<td>${TYPE_LABELS[s.source_type] || s.source_type}</td>
<td>${CATEGORY_LABELS[s.category] || s.category}</td>
<td class="text-right">${s.article_count || 0}</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>
<button class="btn btn-danger btn-small" onclick="confirmDeleteGlobalSource(${s.id}, '${esc(s.name)}')">Löschen</button>
</td>
</tr>
`).join("");
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 ? " ▲" : " ▼";
}
// Suche
// Filter + Sortierung
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);
});
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;
@@ -172,7 +215,7 @@ function setupSourceForms() {
function confirmDeleteGlobalSource(id, name) {
showConfirm(
"Grundquelle loeschen",
"Grundquelle löschen",
`Soll die Grundquelle "${name}" endgültig gelöscht werden? Sie wird für alle Monitore entfernt.`,
async () => {
try {
@@ -210,7 +253,7 @@ function renderTenantSources(sources) {
<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>
<button class="btn btn-primary btn-small" onclick="promoteSource(${s.id}, '${esc(s.name)}')">Übernehmen</button>
</td>
</tr>
`).join("");
@@ -320,7 +363,7 @@ async function addDiscoveredFeeds() {
});
if (selected.length === 0) {
alert("Keine Feeds ausgewaehlt");
alert("Keine Feeds ausgewählt");
return;
}