Phase 3b: Kategorien/Typen aus Backend (/api/sources/meta)

- src/source_meta.py NEU: SOURCE_CATEGORIES + SOURCE_TYPES als
  Single Source of Truth (Liste mit {key, label}). category_label/type_label
  Lookup-Funktionen, get_meta() liefert das gesamte Set.
- src/routers/sources.py: GET /api/sources/meta ergänzt (admin-auth,
  liefert Kategorien + Typen)
- src/static/js/app.js: window.META + loadMeta() + categoryLabel/typeLabel +
  populateSelect Helper. Beim DOMContentLoaded wird Meta geladen, befüllt
  globale CATEGORY_LABELS und TYPE_LABELS.
- src/static/js/sources.js: hardcoded const CATEGORY_LABELS und TYPE_LABELS
  entfernt - werden jetzt aus app.js loadMeta() global gesetzt.
  loadGlobalSources() ruft populateSelect() für die Filter-Dropdowns auf.
- src/static/js/source-health.js: gleiche hardcoded Listen entfernt.
- src/static/dashboard.html: <option>-Listen für globalFilterCategory und
  globalFilterType entfernt (nur noch default Alle). JS befüllt sie dynamisch.

Ergebnis: Bei einer neuen Kategorie nur source_meta.py anpassen,
keine 3-fach-Pflege mehr in HTML+sources.js+source-health.js.
Dieser Commit ist enthalten in:
claude-dev
2026-05-09 03:05:16 +00:00
Ursprung 5a87168416
Commit eda60f9299
5 geänderte Dateien mit 138 neuen und 53 gelöschten Zeilen

Datei anzeigen

@@ -833,3 +833,48 @@ document.addEventListener('DOMContentLoaded', function() {
});
}
});
// === Source-Meta (Kategorien + Typen aus dem Backend) ===
window.META = { categories: [], types: [] };
window.CATEGORY_LABELS = {};
window.TYPE_LABELS = {};
async function loadMeta() {
try {
const data = await API.get("/api/sources/meta");
window.META = data;
window.CATEGORY_LABELS = Object.fromEntries((data.categories || []).map(c => [c.key, c.label]));
window.TYPE_LABELS = Object.fromEntries((data.types || []).map(t => [t.key, t.label]));
return data;
} catch (err) {
console.warn("loadMeta:", err);
return null;
}
}
function categoryLabel(key) {
return window.CATEGORY_LABELS[key] || key || "";
}
function typeLabel(key) {
return window.TYPE_LABELS[key] || key || "";
}
function populateSelect(el, items, allLabel) {
if (!el) return;
const current = el.value;
el.innerHTML = '<option value="">' + (allLabel || "Alle") + '</option>';
items.forEach(it => {
const opt = document.createElement("option");
opt.value = it.key;
opt.textContent = it.label;
el.appendChild(opt);
});
if (current && items.some(it => it.key === current)) el.value = current;
}
document.addEventListener("DOMContentLoaded", () => {
// Beim Page-Load Meta einmalig laden (asynchron, blockiert nicht)
if (window.API && (localStorage.getItem("token") || window.location.pathname === "/dashboard")) {
loadMeta();
}
});