diff --git a/src/config.py b/src/config.py index ba379d7..30b84a4 100644 --- a/src/config.py +++ b/src/config.py @@ -35,3 +35,4 @@ MAGIC_LINK_EXPIRE_MINUTES = 10 CLAUDE_PATH = os.environ.get("CLAUDE_PATH", "/home/claude-dev/.claude/local/claude") CLAUDE_TIMEOUT = 300 MAX_FEEDS_PER_DOMAIN = 3 +CLAUDE_MODEL_FAST = "claude-haiku-4-5-20251001" diff --git a/src/routers/sources.py b/src/routers/sources.py index 3a5a7bd..2ac3e3d 100644 --- a/src/routers/sources.py +++ b/src/routers/sources.py @@ -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, + } diff --git a/src/static/css/style.css b/src/static/css/style.css index 42c4f4f..79d8a2d 100644 --- a/src/static/css/style.css +++ b/src/static/css/style.css @@ -651,3 +651,55 @@ tr:hover td { line-height: 1.6; margin-bottom: 8px; } + + +/* --- 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; +} + +.badge-suggestion-deactivate_source { + background: rgba(245, 158, 11, 0.2); + color: #fcd34d; +} + +.badge-suggestion-remove_source { + background: rgba(239, 68, 68, 0.2); + color: #fca5a5; +} + +.badge-suggestion-fix_url { + background: rgba(168, 85, 247, 0.2); + color: #d8b4fe; +} + +.badge-priority-high { + background: rgba(239, 68, 68, 0.2); + color: #fca5a5; +} + +.badge-priority-medium { + background: rgba(245, 158, 11, 0.2); + color: #fcd34d; +} + +.badge-priority-low { + background: rgba(100, 116, 139, 0.2); + color: #94a3b8; +} diff --git a/src/static/dashboard.html b/src/static/dashboard.html index 8f5e1f2..b68c968 100644 --- a/src/static/dashboard.html +++ b/src/static/dashboard.html @@ -43,7 +43,7 @@