Quellen-Health UI: Tab, API-Endpunkte, Vorschläge
- Neuer Sub-Tab "Quellen-Health" mit Vorschlägen + Check-Ergebnissen
- API: GET /health, GET /suggestions, PUT /suggestions/{id}, POST /health/run
- Vorschläge annehmen/ablehnen mit Auto-Ausführung
- Badge-Styles für Health-Status und Prioritäten
- Umlaute in Source-Modal und Dashboard korrigiert
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Dieser Commit ist enthalten in:
@@ -1,3 +1,4 @@
|
||||
import os
|
||||
"""Grundquellen-Verwaltung und Kundenquellen-Übersicht."""
|
||||
import sys
|
||||
import logging
|
||||
@@ -12,6 +13,8 @@ from auth import get_current_admin
|
||||
from database import db_dependency
|
||||
import aiosqlite
|
||||
|
||||
sys.path.insert(0, os.path.join('/home/claude-dev/AegisSight-Monitor/src'))
|
||||
|
||||
from source_rules import (
|
||||
discover_source,
|
||||
discover_all_feeds,
|
||||
@@ -312,3 +315,204 @@ async def add_discovered_sources(
|
||||
|
||||
await db.commit()
|
||||
return {"added": added, "skipped": skipped}
|
||||
|
||||
|
||||
|
||||
# --- Health-Check & Vorschläge ---
|
||||
|
||||
@router.get("/health")
|
||||
async def get_health(
|
||||
admin: dict = Depends(get_current_admin),
|
||||
db: aiosqlite.Connection = Depends(db_dependency),
|
||||
):
|
||||
"""Health-Check-Ergebnisse abrufen."""
|
||||
# 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": []}
|
||||
|
||||
cursor = await db.execute("""
|
||||
SELECT
|
||||
h.id, h.source_id, s.name, s.domain, s.url, s.source_type,
|
||||
h.check_type, h.status, h.message, h.details, h.checked_at
|
||||
FROM source_health_checks h
|
||||
JOIN sources s ON s.id = h.source_id
|
||||
ORDER BY
|
||||
CASE h.status WHEN 'error' THEN 0 WHEN 'warning' THEN 1 ELSE 2 END,
|
||||
s.name
|
||||
""")
|
||||
checks = [dict(row) for row in await cursor.fetchall()]
|
||||
|
||||
error_count = sum(1 for c in checks if c["status"] == "error")
|
||||
warning_count = sum(1 for c in checks if c["status"] == "warning")
|
||||
ok_count = sum(1 for c in checks if c["status"] == "ok")
|
||||
|
||||
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": len(checks),
|
||||
"errors": error_count,
|
||||
"warnings": warning_count,
|
||||
"ok": ok_count,
|
||||
"checks": checks,
|
||||
}
|
||||
|
||||
|
||||
@router.get("/suggestions")
|
||||
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)."""
|
||||
cursor = await db.execute(
|
||||
"SELECT name FROM sqlite_master WHERE type='table' AND name='source_suggestions'"
|
||||
)
|
||||
if not await cursor.fetchone():
|
||||
return []
|
||||
|
||||
cursor = await db.execute("""
|
||||
SELECT * FROM source_suggestions
|
||||
ORDER BY
|
||||
CASE status WHEN 'pending' THEN 0 ELSE 1 END,
|
||||
created_at DESC
|
||||
LIMIT 50
|
||||
""")
|
||||
return [dict(row) for row in await cursor.fetchall()]
|
||||
|
||||
|
||||
class SuggestionAction(BaseModel):
|
||||
accept: bool
|
||||
|
||||
|
||||
@router.put("/suggestions/{suggestion_id}")
|
||||
async def update_suggestion(
|
||||
suggestion_id: int,
|
||||
action: SuggestionAction,
|
||||
admin: dict = Depends(get_current_admin),
|
||||
db: aiosqlite.Connection = Depends(db_dependency),
|
||||
):
|
||||
"""Vorschlag annehmen oder ablehnen."""
|
||||
import json as _json
|
||||
|
||||
cursor = await db.execute(
|
||||
"SELECT * FROM source_suggestions WHERE id = ?", (suggestion_id,)
|
||||
)
|
||||
suggestion = await cursor.fetchone()
|
||||
if not suggestion:
|
||||
raise HTTPException(status_code=404, detail="Vorschlag nicht gefunden")
|
||||
|
||||
suggestion = dict(suggestion)
|
||||
if suggestion["status"] != "pending":
|
||||
raise HTTPException(status_code=400, detail=f"Vorschlag bereits {suggestion['status']}")
|
||||
|
||||
new_status = "accepted" if action.accept else "rejected"
|
||||
result_action = None
|
||||
|
||||
if action.accept:
|
||||
stype = suggestion["suggestion_type"]
|
||||
data = _json.loads(suggestion["suggested_data"]) if suggestion["suggested_data"] else {}
|
||||
|
||||
if stype == "add_source":
|
||||
name = data.get("name", "Unbenannt")
|
||||
url = data.get("url")
|
||||
domain = data.get("domain", "")
|
||||
category = data.get("category", "sonstige")
|
||||
source_type = "rss_feed" if url and any(
|
||||
x in (url or "").lower() for x in ("rss", "feed", "xml", "atom")
|
||||
) else "web_source"
|
||||
|
||||
if url:
|
||||
cursor = await db.execute(
|
||||
"SELECT id FROM sources WHERE url = ? AND tenant_id IS NULL", (url,)
|
||||
)
|
||||
if await cursor.fetchone():
|
||||
result_action = "übersprungen (URL bereits vorhanden)"
|
||||
new_status = "rejected"
|
||||
else:
|
||||
await db.execute(
|
||||
"INSERT INTO sources (name, url, domain, source_type, category, status, added_by, tenant_id) "
|
||||
"VALUES (?, ?, ?, ?, ?, 'active', 'haiku-vorschlag', NULL)",
|
||||
(name, url, domain, source_type, category),
|
||||
)
|
||||
result_action = f"Quelle '{name}' angelegt"
|
||||
else:
|
||||
result_action = "übersprungen (keine URL)"
|
||||
new_status = "rejected"
|
||||
|
||||
elif stype == "deactivate_source":
|
||||
source_id = suggestion["source_id"]
|
||||
if source_id:
|
||||
await db.execute("UPDATE sources SET status = 'inactive' WHERE id = ?", (source_id,))
|
||||
result_action = "Quelle deaktiviert"
|
||||
|
||||
elif stype == "remove_source":
|
||||
source_id = suggestion["source_id"]
|
||||
if source_id:
|
||||
await db.execute("DELETE FROM sources WHERE id = ?", (source_id,))
|
||||
result_action = "Quelle gelöscht"
|
||||
|
||||
elif stype == "fix_url":
|
||||
source_id = suggestion["source_id"]
|
||||
new_url = data.get("url")
|
||||
if source_id and new_url:
|
||||
await db.execute("UPDATE sources SET url = ? WHERE id = ?", (new_url, source_id))
|
||||
result_action = f"URL aktualisiert"
|
||||
|
||||
await db.execute(
|
||||
"UPDATE source_suggestions SET status = ?, reviewed_at = CURRENT_TIMESTAMP WHERE id = ?",
|
||||
(new_status, suggestion_id),
|
||||
)
|
||||
await db.commit()
|
||||
return {"status": new_status, "action": result_action}
|
||||
|
||||
|
||||
@router.post("/health/run")
|
||||
async def run_health_check_now(
|
||||
admin: dict = Depends(get_current_admin),
|
||||
db: aiosqlite.Connection = Depends(db_dependency),
|
||||
):
|
||||
"""Health-Check manuell starten."""
|
||||
# Tabellen sicherstellen
|
||||
await db.executescript("""
|
||||
CREATE TABLE IF NOT EXISTS source_health_checks (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
source_id INTEGER NOT NULL REFERENCES sources(id) ON DELETE CASCADE,
|
||||
check_type TEXT NOT NULL,
|
||||
status TEXT NOT NULL,
|
||||
message TEXT,
|
||||
details TEXT,
|
||||
checked_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
CREATE TABLE IF NOT EXISTS source_suggestions (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
suggestion_type TEXT NOT NULL,
|
||||
title TEXT NOT NULL,
|
||||
description TEXT,
|
||||
source_id INTEGER REFERENCES sources(id) ON DELETE SET NULL,
|
||||
suggested_data TEXT,
|
||||
priority TEXT DEFAULT 'medium',
|
||||
status TEXT DEFAULT 'pending',
|
||||
reviewed_at TIMESTAMP,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
""")
|
||||
await db.commit()
|
||||
|
||||
# source_health und source_suggester importieren
|
||||
sys.path.insert(0, "/home/claude-dev/AegisSight-Monitor/src")
|
||||
from services.source_health import run_health_checks
|
||||
from services.source_suggester import generate_suggestions
|
||||
|
||||
result = await run_health_checks(db)
|
||||
suggestion_count = await generate_suggestions(db)
|
||||
|
||||
return {
|
||||
"checked": result["checked"],
|
||||
"issues": result["issues"],
|
||||
"suggestions": suggestion_count,
|
||||
}
|
||||
|
||||
In neuem Issue referenzieren
Einen Benutzer sperren