# Log-Viewer und Auto-Reload Implementation Plan > **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. **Goal:** Letzten 10 Zeilen von usage.log/error.log in der Settings-Sektion anzeigen; alle Daten alle 5 Minuten automatisch neu laden; Zeitstempel der letzten Aktualisierung im Header. **Architecture:** Neuer Backend-Endpunkt `GET /api/logs/{name}` in `admin.py` liest die letzten 10 Zeilen aus den Logdateien. `SettingsSection` bekommt einen `refreshKey`-Prop, den `App` alle 5 Minuten inkrementiert — das triggert einen neuen `useEffect`-Lauf. `App` hält den `lastUpdated`-Timestamp und zeigt ihn im Header. **Tech Stack:** FastAPI (Python), React 18, axios, CSS --- ### Task 1: Backend-Endpunkt `GET /api/logs/{name}` **Files:** - Modify: `backend/admin.py` - Create: `backend/tests/test_admin_logs.py` - [ ] **Schritt 1: Test schreiben** Datei `backend/tests/test_admin_logs.py` anlegen: ```python import os import pytest from fastapi.testclient import TestClient os.environ.setdefault("ADMIN_PASSWORD", "test-admin-pw") os.environ.setdefault("OLLAMA_URL", "http://127.0.0.1:9999") @pytest.fixture def client(tmp_path): log_file = tmp_path / "usage.log" log_file.write_text("\n".join(f"Zeile {i}" for i in range(1, 16)) + "\n") (tmp_path / "error.log").write_text("Fehler A\nFehler B\n") os.environ["LOG_FILE"] = str(log_file) from database import Base, engine Base.metadata.drop_all(bind=engine) Base.metadata.create_all(bind=engine) from admin import app yield TestClient(app, raise_server_exceptions=False) Base.metadata.drop_all(bind=engine) os.environ.pop("LOG_FILE", None) AUTH = {"Authorization": "Bearer test-admin-pw"} def test_logs_usage_returns_last_10_lines(client): resp = client.get("/api/logs/usage", headers=AUTH) assert resp.status_code == 200 lines = resp.json()["lines"] assert len(lines) == 10 assert lines[-1] == "Zeile 15" assert lines[0] == "Zeile 6" def test_logs_error_returns_content(client): resp = client.get("/api/logs/error", headers=AUTH) assert resp.status_code == 200 assert resp.json()["lines"] == ["Fehler A", "Fehler B"] def test_logs_missing_file_returns_empty(client, tmp_path): os.environ["LOG_FILE"] = str(tmp_path / "nonexistent.log") resp = client.get("/api/logs/usage", headers=AUTH) assert resp.status_code == 200 assert resp.json()["lines"] == [] def test_logs_invalid_name_returns_400(client): resp = client.get("/api/logs/secret", headers=AUTH) assert resp.status_code == 400 def test_logs_requires_auth(client): resp = client.get("/api/logs/usage") assert resp.status_code == 401 ``` - [ ] **Schritt 2: Test zum Scheitern bringen** ```bash cd backend /Users/oliver/Development/Technologien/llm_quota/.venv/bin/python3 -m pytest tests/test_admin_logs.py -v ``` Erwartung: alle Tests FAIL mit 404 oder AttributeError (Endpunkt existiert nicht). - [ ] **Schritt 3: Endpunkt in `admin.py` implementieren** Direkt vor der Static-File-Mount-Zeile (`_dist = ...`) einfügen: ```python @app.get("/api/logs/{name}") async def get_log_lines(name: str, _ = Depends(require_admin_auth)): if name not in ("usage", "error"): raise HTTPException(status_code=400, detail="name must be 'usage' or 'error'") log_file = Path(os.getenv("LOG_FILE", "logs/usage.log")) path = log_file if name == "usage" else log_file.parent / "error.log" try: lines = path.read_text(encoding="utf-8").splitlines() return {"lines": lines[-10:]} except FileNotFoundError: return {"lines": []} ``` - [ ] **Schritt 4: Tests laufen lassen** ```bash cd backend /Users/oliver/Development/Technologien/llm_quota/.venv/bin/python3 -m pytest tests/test_admin_logs.py -v ``` Erwartung: alle 5 Tests PASS. - [ ] **Schritt 5: Gesamte Test-Suite prüfen** ```bash cd backend /Users/oliver/Development/Technologien/llm_quota/.venv/bin/python3 -m pytest tests/ -q ``` Erwartung: alle Tests PASS, keine Regressionen. - [ ] **Schritt 6: Commit** ```bash git add backend/admin.py backend/tests/test_admin_logs.py git commit -m "Add GET /api/logs/{name} endpoint to admin API" ``` --- ### Task 2: Log-Anzeige in `SettingsSection` **Files:** - Modify: `frontend/src/main.jsx` - Modify: `frontend/src/styles.css` - [ ] **Schritt 1: `refreshKey`-Prop und Log-State in `SettingsSection` ergänzen** In `main.jsx`, `SettingsSection`-Signatur und State ändern: ```jsx function SettingsSection({ password, refreshKey }) { // bestehende States beibehalten, neu hinzufügen: const [usageLog, setUsageLog] = useState([]); const [errorLog, setErrorLog] = useState([]); ``` - [ ] **Schritt 2: Log-Fetch in den bestehenden `useEffect` integrieren** Den bestehenden `useEffect` in `SettingsSection` (Zeilen 110–122) ersetzen: ```jsx useEffect(() => { const headers = authHeaders(password); Promise.all([ axios.get('/api/settings', { headers }), axios.get('/api/proxy-info', { headers }), axios.get('/api/logs/usage', { headers }), axios.get('/api/logs/error', { headers }), ]).then(([settingsRes, proxyRes, usageRes, errorRes]) => { const s = settingsRes.data; setSettings(s); setProxyEndpoint(proxyRes.data.endpoint); setAppVersion(proxyRes.data.version); setUsageLog(usageRes.data.lines); setErrorLog(errorRes.data.lines); fetchModels(s.ollama_url, s.force_model); }).catch(() => setError('Einstellungen konnten nicht geladen werden.')); }, [refreshKey]); ``` Wichtig: `[refreshKey]` statt `[]` als Dependency-Array, damit ein neuer `refreshKey` einen Re-Fetch auslöst. - [ ] **Schritt 3: Log-Anzeige am Ende von `SettingsSection` einfügen** Den `return`-Block von `SettingsSection` — direkt vor dem schließenden `` — erweitern: ```jsx
{usageLog.length > 0 ? usageLog.join('\n') : '— keine Einträge —'}
{errorLog.length > 0 && (
<>
{errorLog.join('\n')}
>
)}
{usageLog.length > 0 ? usageLog.join('\n') : '— keine Einträge —'}
{errorLog.length > 0 && (
<>
{errorLog.join('\n')}
>
)}