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

11 KiB
Raw Blame History

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:

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
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:

@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
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
cd backend
/Users/oliver/Development/Technologien/llm_quota/.venv/bin/python3 -m pytest tests/ -q

Erwartung: alle Tests PASS, keine Regressionen.

  • Schritt 6: Commit
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:

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:

  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:

        <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:

  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:

.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
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:

  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:

  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:

  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:

      <SettingsSection password={password} refreshKey={refreshKey} />
  • Schritt 5: Zeitstempel im Header anzeigen

Den header-Block in App ersetzen:

      <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:

.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
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"