From c5f63b1d14115a344cbbe1f8f7e01d10f4e37174 Mon Sep 17 00:00:00 2001 From: claude-dev Date: Sat, 25 Jul 2026 18:48:16 +0000 Subject: [PATCH] fix(sources): Feinschliff nach Konsistenz-Pruefung + UI-Rueckmeldungen Echte Fehler: - esc() maskiert jetzt auch Anfuehrungszeichen. Quellennamen mit Apostroph (z.B. LExpress) brachen sonst die Zeilen-Knoepfe. - formatDateTime parst SQLite-UTC-Zeitstempel jetzt als UTC statt Lokalzeit (Zeiten waren um den Zeitzonen-Offset verschoben). - Audit-Filter Bis-Datum schliesst den gewaehlten Tag jetzt komplett ein. UI-Rueckmeldungen: - Beschreibung in der Vorschlags-Tabelle bricht mehrzeilig um statt abgeschnitten zu werden. - Bestaetigen-Knopf im Dialog ist standardmaessig Gold, Rot nur noch bei destruktiven Aktionen (Org/Nutzer/Quelle loeschen, X-Konto entfernen). - Alle >= 0.85 genehmigen ist ausgegraut, solange keine Klassifikationen ausstehen. Konsistenz: - GET /suggestions deckelt pending nicht mehr auf 50 (Badge und Tabelle liefen auseinander), bearbeitete weiterhin letzte 50. - /stats zaehlt jetzt alle Quellen wie die Liste (nicht nur aktive), Grundquellen/Kundenquellen-Aufteilung wird immer angezeigt. - Health-Spalte sortiert nach Schweregrad statt alphabetisch. - Aufgaben-Badge laedt schon beim Seitenstart, nicht erst beim Reiterklick. - Loesung-suchen-Knopf stellt nach Fehler das Icon wieder her, native confirm()-Dialoge durch showConfirm ersetzt. - Tote Endpoints GET /health (Alt-Liste) und GET /classification/stats entfernt, tote CSS-Bloecke (activity-*, badge-health-*, review-conf-filter) geloescht. - Smoke-Test deckt jetzt alle Klassifikations-/Upload-/Sync-Endpoints ab. Co-Authored-By: Claude Fable 5 --- src/routers/audit.py | 4 + src/routers/sources.py | 155 +++++-------------------------------- src/static/css/style.css | 58 -------------- src/static/dashboard.html | 14 ++-- src/static/js/app.js | 23 +++++- src/static/js/aufgaben.js | 29 +++++-- src/static/js/sources.js | 23 ++++-- src/static/js/x-scraper.js | 3 +- tests/test_api_smoke.py | 10 ++- 9 files changed, 100 insertions(+), 219 deletions(-) diff --git a/src/routers/audit.py b/src/routers/audit.py index e651d7f..b94cc89 100644 --- a/src/routers/audit.py +++ b/src/routers/audit.py @@ -70,6 +70,10 @@ async def list_audit( where.append("ts >= ?") params.append(from_ts) if to_ts: + # Datums-Only-Wert (aus ) soll den gewählten Tag + # komplett EINschließen, sonst fiele er wegen ts <= 'YYYY-MM-DD' raus. + if len(to_ts) == 10: + to_ts = to_ts + " 23:59:59" where.append("ts <= ?") params.append(to_ts) diff --git a/src/routers/sources.py b/src/routers/sources.py index bafcf4b..46da229 100644 --- a/src/routers/sources.py +++ b/src/routers/sources.py @@ -120,7 +120,7 @@ async def tasks_summary( ): """Billiger Zähler für den Aufgaben-Badge im Quellen-Reiter. - classification_pending nutzt DIESELBE Bedingung wie /classification/stats, + classification_pending nutzt DIESELBE Bedingung wie /classification/queue, damit Badge und Review-Karten nie auseinanderlaufen. """ suggestions_pending = 0 @@ -403,13 +403,13 @@ async def get_sources_stats( ): """Aggregierte Stats für die Stats-Bar der vereinten Quellenliste. - Zählt Grund- UND Kundenquellen. by_origin liefert die Aufteilung, - last_check den Zeitpunkt des jüngsten Health-Checks. + Zählt Grund- UND Kundenquellen, aktive wie inaktive (die Liste zeigt + ebenfalls alle). by_origin liefert die Aufteilung, last_check den + Zeitpunkt des jüngsten Health-Checks. """ cur = await db.execute(""" SELECT source_type, COUNT(*) AS count, COALESCE(SUM(article_count), 0) AS articles FROM sources - WHERE status = 'active' GROUP BY source_type """) by_type = {} @@ -425,7 +425,6 @@ async def get_sources_stats( SELECT CASE WHEN tenant_id IS NULL THEN 'global' ELSE 'tenant' END AS origin, COUNT(*) AS cnt FROM sources - WHERE status = 'active' GROUP BY origin """) by_origin = {dict(r)["origin"]: dict(r)["cnt"] for r in await cur.fetchall()} @@ -441,7 +440,6 @@ async def get_sources_stats( SELECT h.status AS hs, COUNT(DISTINCT h.source_id) AS cnt FROM source_health_checks h JOIN sources s ON s.id = h.source_id - WHERE s.status = 'active' GROUP BY h.status """) for r in await cur.fetchall(): @@ -716,107 +714,6 @@ async def add_discovered_sources( # --- Health-Check & Vorschläge --- -@router.get("/health") -async def get_health( - limit: int = 100, - offset: int = 0, - admin: dict = Depends(get_current_admin), - db: aiosqlite.Connection = Depends(db_dependency), -): - """Health-Check-Ergebnisse abrufen. - - Default-Limit 100, sortiert nach Status (errors first, dann warnings, dann ok). - Counters (errors/warnings/ok/total_checks) beziehen sich auf den GESAMTEN - Datenbestand, nicht nur auf die zurückgegebene Page. Damit kann das Frontend - den vollen Status anzeigen, ohne alle Zeilen rendern zu müssen. - has_more zeigt an, ob es weitere Items zum Nachladen gibt. - all_orgs liefert die Liste aller Tenants mit Health-Checks (für Filter-Dropdown). - """ - limit = max(1, min(int(limit or 100), 5000)) - offset = max(0, int(offset or 0)) - - # Prüfen ob Tabelle existiert - cursor = await db.execute( - "SELECT name FROM sqlite_master WHERE type='table' AND name='source_health_checks'" - ) - if not await cursor.fetchone(): - return { - "last_check": None, "total_checks": 0, - "errors": 0, "warnings": 0, "ok": 0, - "checks": [], "all_orgs": [], - "limit": limit, "offset": offset, "has_more": False, - } - - # Aggregate über GESAMTEN Bestand. Eine GROUP-BY-Query nach (check_type, status) - # liefert sowohl die Top-Counters als auch das feine Breakdown für die UI. - cursor = await db.execute( - "SELECT check_type, status, COUNT(*) AS n FROM source_health_checks GROUP BY check_type, status" - ) - breakdown = {} # {check_type: {status: count}} - error_count = 0 - warning_count = 0 - ok_count = 0 - for row in await cursor.fetchall(): - ct = row["check_type"] - st = row["status"] - breakdown.setdefault(ct, {})[st] = row["n"] - if st == "error": - error_count += row["n"] - elif st == "warning": - warning_count += row["n"] - elif st == "ok": - ok_count += row["n"] - total_checks = error_count + warning_count + ok_count - - # Paginierte Daten - cursor = await db.execute(""" - SELECT - h.source_id, s.name, s.domain, s.tenant_id, s.language, - o.name AS org_name, - 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 - ORDER BY - CASE h.status WHEN 'error' THEN 0 WHEN 'warning' THEN 1 ELSE 2 END, - s.name - LIMIT ? OFFSET ? - """, (limit, offset)) - checks = [dict(row) for row in await cursor.fetchall()] - - # Org-Liste (alle Tenants mit Health-Checks, für Frontend-Filter-Dropdown) - cursor = await db.execute(""" - SELECT DISTINCT s.tenant_id AS id, o.name AS name - FROM source_health_checks h - JOIN sources s ON s.id = h.source_id - LEFT JOIN organizations o ON o.id = s.tenant_id - WHERE s.tenant_id IS NOT NULL - ORDER BY o.name - """) - all_orgs = [dict(row) for row in await cursor.fetchall()] - - cursor = await db.execute("SELECT MAX(checked_at) as last_check FROM source_health_checks") - row = await cursor.fetchone() - last_check = row["last_check"] if row else None - - return { - "last_check": last_check, - "total_checks": total_checks, - "errors": error_count, - "warnings": warning_count, - "ok": ok_count, - "breakdown": breakdown, - "checks": checks, - "all_orgs": all_orgs, - "limit": limit, - "offset": offset, - "has_more": (offset + len(checks)) < total_checks, - } - - - - - @router.get("/health/history") async def get_health_history( limit: int = 20, @@ -929,7 +826,11 @@ async def get_suggestions( admin: dict = Depends(get_current_admin), db: aiosqlite.Connection = Depends(db_dependency), ): - """Alle Vorschläge abrufen (pending zuerst, dann letzte 20 bearbeitete).""" + """Alle OFFENEN Vorschläge plus die letzten 50 bearbeiteten. + + Kein Deckel auf pending, sonst liefe der Aufgaben-Badge + (tasks/summary zählt ungedeckelt) der Tabelle davon. + """ cursor = await db.execute( "SELECT name FROM sqlite_master WHERE type='table' AND name='source_suggestions'" ) @@ -938,12 +839,17 @@ async def get_suggestions( cursor = await db.execute(""" SELECT * FROM source_suggestions - ORDER BY - CASE status WHEN 'pending' THEN 0 ELSE 1 END, - created_at DESC + WHERE status = 'pending' + ORDER BY created_at DESC + """) + pending = [dict(row) for row in await cursor.fetchall()] + cursor = await db.execute(""" + SELECT * FROM source_suggestions + WHERE status != 'pending' + ORDER BY created_at DESC LIMIT 50 """) - return [dict(row) for row in await cursor.fetchall()] + return pending + [dict(row) for row in await cursor.fetchall()] class SuggestionAction(BaseModel): @@ -1172,31 +1078,6 @@ Nur das JSON, kein anderer Text.""" # === Klassifikations-Review (LLM-Vorschlaege approve/reject/reclassify) === -@router.get("/classification/stats") -async def classification_stats( - admin: dict = Depends(get_current_admin), - db: aiosqlite.Connection = Depends(db_dependency), -): - """Counts pro classification_source-Wert + Anzahl Pending-Reviews (alle Quellen).""" - cursor = await db.execute( - """SELECT classification_source, COUNT(*) as cnt - FROM sources - WHERE status = 'active' - GROUP BY classification_source""" - ) - by_source = {row["classification_source"] or "legacy": row["cnt"] for row in await cursor.fetchall()} - cursor = await db.execute( - """SELECT COUNT(*) as cnt FROM sources - WHERE status = 'active' AND proposed_political_orientation IS NOT NULL""" - ) - pending = (await cursor.fetchone())["cnt"] - return { - "by_classification_source": by_source, - "pending_review": pending, - "total": sum(by_source.values()), - } - - @router.get("/classification/queue") async def classification_queue( limit: int = 50, diff --git a/src/static/css/style.css b/src/static/css/style.css index ec7a400..a6754aa 100644 --- a/src/static/css/style.css +++ b/src/static/css/style.css @@ -586,36 +586,6 @@ tr:hover td { font-weight: 500; } -/* --- Recent activity --- */ -.activity-item { - display: flex; - align-items: center; - gap: 12px; - padding: 8px 0; - font-size: 13px; -} - -.activity-icon { - width: 28px; - height: 28px; - border-radius: 50%; - display: flex; - align-items: center; - justify-content: center; - font-size: 12px; - flex-shrink: 0; -} - -.activity-icon.org { - background: rgba(59, 130, 246, 0.2); - color: #93c5fd; -} - -.activity-icon.user { - background: rgba(34, 197, 94, 0.2); - color: #86efac; -} - /* --- Org detail panel --- */ .detail-panel { display: none; @@ -702,21 +672,6 @@ tr:hover td { /* --- Health & Suggestion Badges --- */ -.badge-health-error { - background: rgba(239, 68, 68, 0.2); - color: #fca5a5; -} - -.badge-health-warning { - background: rgba(245, 158, 11, 0.2); - color: #fcd34d; -} - -.badge-health-ok { - background: rgba(34, 197, 94, 0.2); - color: #86efac; -} - .badge-suggestion-add_source { background: rgba(59, 130, 246, 0.2); color: #93c5fd; @@ -1133,13 +1088,6 @@ table.show-select .select-col { display: table-cell; } font-size: 13px; color: var(--text-primary); } -.review-conf-filter { - display: inline-flex; - align-items: center; - gap: 6px; - font-size: 12px; - color: var(--text-secondary); -} .review-toolbar-actions { display: flex; gap: 6px; } .review-list { display: flex; flex-direction: column; gap: 8px; } @@ -1440,12 +1388,6 @@ details.adv-section .adv-hint { grid-column: 1 / -1; font-size: 12px; color: var [data-theme="light"] .badge-expired { color: #B91C1C; } [data-theme="light"] .badge-revoked { color: #475569; } [data-theme="light"] .badge-none { color: #475569; } -[data-theme="light"] .activity-icon.org { color: #1D4ED8; } -[data-theme="light"] .activity-icon.user { color: #047857; } -[data-theme="light"] /* --- Health & Suggestion Badges --- */ -.badge-health-error { color: #B91C1C; } -[data-theme="light"] .badge-health-warning { color: #B45309; } -[data-theme="light"] .badge-health-ok { color: #047857; } [data-theme="light"] .badge-suggestion-add_source { color: #1D4ED8; } [data-theme="light"] .badge-suggestion-deactivate_source { color: #B45309; } [data-theme="light"] .badge-suggestion-remove_source { color: #B91C1C; } diff --git a/src/static/dashboard.html b/src/static/dashboard.html index 31febef..6e5faf0 100644 --- a/src/static/dashboard.html +++ b/src/static/dashboard.html @@ -6,7 +6,7 @@ AegisSight Monitor-Verwaltung - +