llmproxy/docs/superpowers/plans/2026-05-10-log-viewer-autoreload.md
2026-05-10 10:09:34 +02:00

385 lines
11 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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 110122) 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 `</section>` — erweitern:
```jsx
<div className="log-section">
<h3>Nutzungslog (letzte 10 Einträge)</h3>
<pre className="log-pre">
{usageLog.length > 0 ? usageLog.join('\n') : '— keine Einträge —'}
</pre>
{errorLog.length > 0 && (
<>
<h3>Fehlerlog</h3>
<pre className="log-pre log-pre-error">{errorLog.join('\n')}</pre>
</>
)}
</div>
```
Der vollständige `return` von `SettingsSection` sieht dann so aus:
```jsx
return (
<section>
<h2>Einstellungen</h2>
<form onSubmit={handleSave} className="settings-form">
{/* ... bestehende settings-rows ... */}
{error && <div className="error">{error}</div>}
{saved && <div className="success">Gespeichert.</div>}
<button type="submit">Speichern</button>
</form>
<div className="log-section">
<h3>Nutzungslog (letzte 10 Einträge)</h3>
<pre className="log-pre">
{usageLog.length > 0 ? usageLog.join('\n') : '— keine Einträge —'}
</pre>
{errorLog.length > 0 && (
<>
<h3>Fehlerlog</h3>
<pre className="log-pre log-pre-error">{errorLog.join('\n')}</pre>
</>
)}
</div>
</section>
);
```
- [ ] **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 215221) 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 210213) 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
<SettingsSection password={password} refreshKey={refreshKey} />
```
- [ ] **Schritt 5: Zeitstempel im Header anzeigen**
Den `header`-Block in `App` ersetzen:
```jsx
<div className="header">
<h1>Ollama Proxy Admin</h1>
<div className="header-right">
{lastUpdated && (
<span className="last-updated">
Aktualisiert: {lastUpdated.toLocaleTimeString('de-DE', { hour: '2-digit', minute: '2-digit' })}
</span>
)}
<button onClick={logout}>Abmelden</button>
</div>
</div>
```
- [ ] **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"
```