perf(sources): Quellen-Health Tab schneller (Payload-Slim + 60s-Cache)

Tab "Quellen Health" lädt deutlich schneller:

1. /api/sources/health: SELECT reduziert auf nur die im Frontend wirklich
   gerenderten Felder. Weg sind: h.id, s.url, s.source_type, s.category,
   s.bias, h.details, h.checked_at. Response-Größe sinkt damit von ~198 KB
   auf grob die Hälfte (bei 519 Health-Checks) ohne UI-Verlust.

2. source-health.js: 60-Sekunden In-Memory-Cache fürs loadHealthData.
   Tab hin und her klicken ist damit instant statt jedes Mal voller
   Reload + Render der 519 Tabellen-Zeilen.
   Bei Mutationen (Vorschlag annehmen/ablehnen, run-stream beendet,
   search-fix) wird mit loadHealthData(true) der Cache umgangen,
   damit frische Daten gezeigt werden.

3. dashboard.html: Cache-Buster für source-health.js auf 20260509e gebumpt.
Dieser Commit ist enthalten in:
claude-dev
2026-05-09 12:33:30 +00:00
Ursprung 1b25d8ba12
Commit f6af21e6cb
3 geänderte Dateien mit 21 neuen und 8 gelöschten Zeilen

Datei anzeigen

@@ -578,10 +578,9 @@ async def get_health(
cursor = await db.execute("""
SELECT
h.id, h.source_id, s.name, s.domain, s.url, s.source_type,
s.tenant_id, s.category, s.language, s.bias,
h.source_id, s.name, s.domain, s.tenant_id, s.language,
o.name AS org_name,
h.check_type, h.status, h.message, h.details, h.checked_at
h.check_type, h.status, h.message
FROM source_health_checks h
JOIN sources s ON s.id = h.source_id
LEFT JOIN organizations o ON o.id = s.tenant_id

Datei anzeigen

@@ -708,7 +708,7 @@
<script src="/static/js/app.js?v=20260509d"></script>
<script src="/static/js/sources.js?v=20260509d"></script>
<script src="/static/js/source-health.js?v=20260509d"></script>
<script src="/static/js/source-health.js?v=20260509e"></script>
<script src="/static/js/audit.js?v=20260509d"></script>
<div id="toastContainer" class="toast-container" aria-live="polite" aria-atomic="true"></div>
</body>

Datei anzeigen

@@ -6,6 +6,11 @@ let suggestionsCache = [];
let healthFilters = { status: "", check_type: "", org: "all" };
let healthHistoryCache = [];
// 60-Sekunden-Cache, damit Tab-Wechsel nicht jedes Mal die volle Antwort neu lädt.
// Bei Mutationen (Vorschlag annehmen/ablehnen, run-stream, search-fix) wird mit force=true neu geladen.
let healthDataCache = { health: null, suggestions: null, history: null, ts: 0 };
const HEALTH_CACHE_TTL_MS = 60000;
const CHECK_TYPE_LABELS = {
reachability: "Erreichbarkeit",
@@ -38,7 +43,15 @@ function setupHealthTab() {
document.addEventListener("DOMContentLoaded", setupHealthTab);
// --- Health-Daten laden ---
async function loadHealthData() {
async function loadHealthData(force = false) {
const now = Date.now();
if (!force && healthDataCache.health && (now - healthDataCache.ts) < HEALTH_CACHE_TTL_MS) {
healthData = healthDataCache.health;
suggestionsCache = healthDataCache.suggestions;
healthHistoryCache = healthDataCache.history;
renderHealthDashboard();
return;
}
try {
const [health, suggestions, history] = await Promise.all([
API.get("/api/sources/health"),
@@ -48,6 +61,7 @@ async function loadHealthData() {
healthData = health;
suggestionsCache = suggestions;
healthHistoryCache = history || [];
healthDataCache = { health, suggestions, history: history || [], ts: Date.now() };
renderHealthDashboard();
} catch (err) {
console.error("Health-Daten laden fehlgeschlagen:", err);
@@ -289,7 +303,7 @@ async function handleSuggestion(id, accept) {
if (result.action) {
showToast("Ergebnis: " + result.action, "success");
}
loadHealthData();
loadHealthData(true);
// Grundquellen-Liste auch aktualisieren
if (typeof loadGlobalSources === "function") loadGlobalSources();
} catch (err) {
@@ -371,7 +385,7 @@ async function runHealthCheck() {
}
}
loadHealthData();
loadHealthData(true);
} catch (err) {
progressEl.innerHTML = '<span class="text-danger">Fehler: ' + esc(err.message) + '</span>';
} finally {
@@ -419,7 +433,7 @@ async function searchFix(btn) {
msg += `\n\nKosten: $${result.cost_usd.toFixed(2)}`;
}
showToast(msg, "info");
loadHealthData();
loadHealthData(true);
} catch (err) {
showToast("Fehler: " + err.message, "error");
} finally {