Compare commits
No commits in common. "main" and "1.0.0" have entirely different histories.
5
.gitignore
vendored
5
.gitignore
vendored
@ -30,7 +30,4 @@ config.json
|
|||||||
|
|
||||||
# Generated documents
|
# Generated documents
|
||||||
KURZANLEITUNG.tex
|
KURZANLEITUNG.tex
|
||||||
KURZANLEITUNG.pdf
|
KURZANLEITUNG.pdf
|
||||||
|
|
||||||
# Internal planning docs
|
|
||||||
docs/
|
|
||||||
@ -159,14 +159,10 @@ API Key: <API key created in the admin interface>
|
|||||||
**Claude Code CLI:**
|
**Claude Code CLI:**
|
||||||
```bash
|
```bash
|
||||||
ANTHROPIC_BASE_URL=http://<host>:8000 \
|
ANTHROPIC_BASE_URL=http://<host>:8000 \
|
||||||
ANTHROPIC_AUTH_TOKEN=<API key created in the admin interface> \
|
ANTHROPIC_AUTH_TOKEN=<API key> \
|
||||||
claude
|
claude
|
||||||
```
|
```
|
||||||
|
|
||||||
## Acknowledgements
|
## Acknowledgements
|
||||||
|
|
||||||
The Anthropic Messages API endpoint (`/v1/messages`) was inspired by [free-claude-code](https://github.com/Alishahryar1/free-claude-code) by Ali Khokhar, which pursues a similar approach for routing Claude Code requests to alternative LLM backends.
|
The Anthropic Messages API endpoint (`/v1/messages`) was inspired by [free-claude-code](https://github.com/Alishahryar1/free-claude-code) by Ali Khokhar, which pursues a similar approach for routing Claude Code requests to alternative LLM backends.
|
||||||
|
|
||||||
## License
|
|
||||||
|
|
||||||
MIT — © 2026 Oliver Hofmann. See [LICENSE](https://git.efi.th-nuernberg.de/gitea/hofmannol/llmproxy/src/branch/main/LICENSE) for details.
|
|
||||||
|
|||||||
@ -162,7 +162,3 @@ ANTHROPIC_BASE_URL=http://<host>:8000 \
|
|||||||
ANTHROPIC_AUTH_TOKEN=<API-Key> \
|
ANTHROPIC_AUTH_TOKEN=<API-Key> \
|
||||||
claude
|
claude
|
||||||
```
|
```
|
||||||
|
|
||||||
## Lizenz
|
|
||||||
|
|
||||||
MIT — © 2026 Oliver Hofmann. Details siehe [LICENSE](https://git.efi.th-nuernberg.de/gitea/hofmannol/llmproxy/src/branch/main/LICENSE).
|
|
||||||
|
|||||||
384
docs/superpowers/plans/2026-05-10-log-viewer-autoreload.md
Normal file
384
docs/superpowers/plans/2026-05-10-log-viewer-autoreload.md
Normal file
@ -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 `</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 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
|
||||||
|
<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"
|
||||||
|
```
|
||||||
@ -0,0 +1,67 @@
|
|||||||
|
# Design: Log-Viewer und Auto-Reload in der Admin-Oberfläche
|
||||||
|
|
||||||
|
## Kontext
|
||||||
|
|
||||||
|
Die Admin-Oberfläche zeigt aktuell nur API-Keys und Einstellungen. Es gibt kein automatisches Aktualisieren und keinen Zugriff auf die Logdateien. Hauptbedürfnis: Das aktuell geladene Ollama-Modell schnell im Blick haben, ohne manuell neu laden zu müssen.
|
||||||
|
|
||||||
|
## Scope
|
||||||
|
|
||||||
|
- Letzten 10 Zeilen von `usage.log` und `error.log` in der Settings-Sektion anzeigen
|
||||||
|
- Alle Daten (Keys, Settings, Logs) alle 5 Minuten automatisch neu laden
|
||||||
|
- Zeitstempel der letzten Aktualisierung anzeigen
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Backend
|
||||||
|
|
||||||
|
### Neuer Endpunkt: `GET /api/logs/{name}`
|
||||||
|
|
||||||
|
**Datei:** `backend/admin.py`
|
||||||
|
|
||||||
|
**Parameter:** `name` — `usage` oder `error` (andere Werte → 400)
|
||||||
|
|
||||||
|
**Verhalten:**
|
||||||
|
- Leitet den Pfad aus `LOG_FILE` (Env-Var, Default: `logs/usage.log`) ab
|
||||||
|
- `error` → selbes Verzeichnis, Dateiname `error.log`
|
||||||
|
- Liest die letzten 10 Zeilen der Datei
|
||||||
|
- Gibt `{"lines": ["...", ...]}` zurück
|
||||||
|
- Datei nicht vorhanden → leeres Array `{"lines": []}`
|
||||||
|
- Geschützt durch `require_admin_auth`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Frontend
|
||||||
|
|
||||||
|
### Log-Anzeige in `SettingsSection`
|
||||||
|
|
||||||
|
**Datei:** `frontend/src/main.jsx`
|
||||||
|
|
||||||
|
- Neuer Unterabschnitt am Ende der Settings-Karte
|
||||||
|
- Zwei `<pre>`-Blöcke: `usage.log` (immer) und `error.log` (nur wenn nicht leer)
|
||||||
|
- Stil konsistent mit bestehenden Settings-Elementen (dunkler Hintergrund, Monospace-Font)
|
||||||
|
- Daten werden beim Mount von `SettingsSection` zusammen mit den übrigen Settings geladen
|
||||||
|
|
||||||
|
### Auto-Reload
|
||||||
|
|
||||||
|
- `setInterval` im `App`-Component, Intervall: **5 Minuten (300 000 ms)**
|
||||||
|
- Ruft dieselben Fetch-Funktionen auf wie beim Login: Keys + Settings + Logs
|
||||||
|
- Timer wird beim Unmount (Logout) via `clearInterval` bereinigt
|
||||||
|
- Oben rechts in der eingeloggten UI: kleiner Hinweis „Zuletzt aktualisiert: HH:MM"
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Nicht im Scope
|
||||||
|
|
||||||
|
- Live-Streaming / WebSocket
|
||||||
|
- Filterung oder Suche in Logs
|
||||||
|
- Konfigurierbare Zeilenzahl (fest: 10)
|
||||||
|
- Automatisches Scrollen
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Tests
|
||||||
|
|
||||||
|
- `GET /api/logs/usage` gibt die letzten 10 Zeilen zurück
|
||||||
|
- `GET /api/logs/error` gibt leeres Array zurück wenn Datei nicht existiert
|
||||||
|
- `GET /api/logs/invalid` gibt 400 zurück
|
||||||
|
- Manueller Test: Admin-UI öffnen, 5 Minuten warten, Timestamp prüfen
|
||||||
8
run_tests.py
Normal file
8
run_tests.py
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Pytest runner for Ollama Proxy tests."""
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
result = subprocess.run([sys.executable, "-m", "pytest"] + sys.argv[1:], cwd="backend")
|
||||||
|
sys.exit(result.returncode)
|
||||||
7
test_api.sh
Normal file
7
test_api.sh
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
curl -X POST http://localhost:8000/api/generate \
|
||||||
|
-H "Authorization: sk-admin-key" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{
|
||||||
|
"model": "llama3",
|
||||||
|
"prompt": "Test"
|
||||||
|
}'
|
||||||
Loading…
x
Reference in New Issue
Block a user