From 7ce4d3a8958861b4f2bc66e4fc93d7f8a4735323 Mon Sep 17 00:00:00 2001 From: Oliver Hofmann Date: Sun, 10 May 2026 10:09:34 +0200 Subject: [PATCH] Add implementation plan: log viewer and auto-reload --- .../plans/2026-05-10-log-viewer-autoreload.md | 384 ++++++++++++++++++ 1 file changed, 384 insertions(+) create mode 100644 docs/superpowers/plans/2026-05-10-log-viewer-autoreload.md diff --git a/docs/superpowers/plans/2026-05-10-log-viewer-autoreload.md b/docs/superpowers/plans/2026-05-10-log-viewer-autoreload.md new file mode 100644 index 0000000..0d446b4 --- /dev/null +++ b/docs/superpowers/plans/2026-05-10-log-viewer-autoreload.md @@ -0,0 +1,384 @@ +# 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 +
+

Nutzungslog (letzte 10 Einträge)

+
+            {usageLog.length > 0 ? usageLog.join('\n') : '— keine Einträge —'}
+          
+ {errorLog.length > 0 && ( + <> +

Fehlerlog

+
{errorLog.join('\n')}
+ + )} +
+``` + +Der vollständige `return` von `SettingsSection` sieht dann so aus: + +```jsx + return ( +
+

Einstellungen

+
+ {/* ... bestehende settings-rows ... */} + {error &&
{error}
} + {saved &&
Gespeichert.
} + +
+
+

Nutzungslog (letzte 10 Einträge)

+
+          {usageLog.length > 0 ? usageLog.join('\n') : '— keine Einträge —'}
+        
+ {errorLog.length > 0 && ( + <> +

Fehlerlog

+
{errorLog.join('\n')}
+ + )} +
+
+ ); +``` + +- [ ] **Schritt 4: CSS ergänzen** + +Am Ende von `frontend/src/styles.css` anhängen: + +```css +.log-section { + margin-top: 24px; + border-top: 1px solid #eee; + padding-top: 16px; +} + +.log-section h3 { + font-size: 13px; + font-weight: 600; + color: #555; + margin: 0 0 6px; + text-transform: uppercase; + letter-spacing: 0.04em; +} + +.log-pre { + background: #1e2a35; + color: #c8d6df; + font-family: 'Menlo', 'Consolas', monospace; + font-size: 11px; + line-height: 1.6; + padding: 10px 14px; + border-radius: 4px; + margin: 0 0 14px; + overflow-x: auto; + white-space: pre; +} + +.log-pre-error { + background: #2d1b1b; + color: #f5a0a0; +} +``` + +- [ ] **Schritt 5: Manueller Browser-Test** + +Dev-Server starten (`./start.sh`), Admin-UI öffnen (`http://localhost:5173`), anmelden. Unter den Einstellungen müssen die letzten 10 Zeilen von `usage.log` erscheinen. Wenn `error.log` leer ist, kein Fehlerlog-Block sichtbar. + +- [ ] **Schritt 6: Commit** + +```bash +git add frontend/src/main.jsx frontend/src/styles.css +git commit -m "Show last 10 log lines in settings section" +``` + +--- + +### Task 3: Auto-Reload alle 5 Minuten + Zeitstempel + +**Files:** +- Modify: `frontend/src/main.jsx` +- Modify: `frontend/src/styles.css` + +- [ ] **Schritt 1: `refreshKey` und `lastUpdated` State in `App` ergänzen** + +Im `App`-Component direkt nach den bestehenden `useState`-Deklarationen: + +```jsx + const [refreshKey, setRefreshKey] = useState(0); + const [lastUpdated, setLastUpdated] = useState(null); +``` + +- [ ] **Schritt 2: `fetchApiKeys` so anpassen, dass `lastUpdated` gesetzt wird** + +Die bestehende `fetchApiKeys`-Funktion in `App` (Zeilen 215–221) ersetzen: + +```jsx + const fetchApiKeys = async () => { + try { + const res = await axios.get('/api/api-keys', { headers: authHeaders(password) }); + setApiKeys(res.data); + setLastUpdated(new Date()); + } catch { + setError('API-Keys konnten nicht geladen werden.'); + } + }; +``` + +- [ ] **Schritt 3: `setInterval` in `App` einbauen** + +Den bestehenden `useEffect` in `App` (Zeilen 210–213) ersetzen: + +```jsx + useEffect(() => { + if (!password) { setLoading(false); return; } + fetchApiKeys().finally(() => setLoading(false)); + + const timer = setInterval(() => { + fetchApiKeys(); + setRefreshKey(k => k + 1); + }, 5 * 60 * 1000); + + return () => clearInterval(timer); + }, [password]); +``` + +- [ ] **Schritt 4: `refreshKey` an `SettingsSection` übergeben** + +In der `return`-Anweisung von `App` den `SettingsSection`-Aufruf anpassen: + +```jsx + +``` + +- [ ] **Schritt 5: Zeitstempel im Header anzeigen** + +Den `header`-Block in `App` ersetzen: + +```jsx +
+

Ollama Proxy Admin

+
+ {lastUpdated && ( + + Aktualisiert: {lastUpdated.toLocaleTimeString('de-DE', { hour: '2-digit', minute: '2-digit' })} + + )} + +
+
+``` + +- [ ] **Schritt 6: CSS für Header-Right und Zeitstempel** + +Am Ende von `frontend/src/styles.css` anhängen: + +```css +.header-right { + display: flex; + align-items: center; + gap: 16px; +} + +.last-updated { + font-size: 12px; + color: #95a5a6; +} +``` + +- [ ] **Schritt 7: Manueller Browser-Test** + +Admin-UI öffnen. Nach dem Login: Zeitstempel erscheint oben rechts (HH:MM). Nach 5 Minuten müssen Keys und Logs automatisch aktualisiert werden — Timestamp springt auf neue Uhrzeit. + +Zum Schnelltest: Intervall testweise auf `5000` (5 Sekunden) setzen, prüfen, dann wieder auf `5 * 60 * 1000` zurücksetzen. + +- [ ] **Schritt 8: Commit** + +```bash +git add frontend/src/main.jsx frontend/src/styles.css +git commit -m "Add 5-minute auto-reload and last-updated timestamp to admin UI" +```