import React, { useState, useEffect } from 'react'; import ReactDOM from 'react-dom/client'; import axios from 'axios'; import './styles.css'; const displayKey = (prefix) => prefix ? `${prefix}••••••••` : '••••••••••••'; function authHeaders(token) { return { Authorization: `Bearer ${token}` }; } const fmtK = (n) => { const k = n / 1000; return k % 1 === 0 ? `${k}k` : `${k.toFixed(1)}k`; }; function QuotaBar({ used, limit, isToken = false, since = null }) { const fmt = isToken ? fmtK : (n) => n.toLocaleString('de-DE'); const sinceLabel = since ? new Date(since).toLocaleDateString('de-DE', { day: '2-digit', month: '2-digit' }) : null; if (limit == null) return (
{sinceLabel && seit {sinceLabel}}
); const pct = Math.min(100, (used / limit) * 100); const color = pct >= 90 ? '#e74c3c' : pct >= 70 ? '#e67e22' : '#27ae60'; return (
{fmt(used)} / {fmt(limit)}
{sinceLabel && seit {sinceLabel}}
); } function Login({ onLogin }) { const [password, setPassword] = useState(''); const [error, setError] = useState(null); const handleSubmit = async (e) => { e.preventDefault(); setError(null); try { await axios.get('/api/api-keys', { headers: authHeaders(password) }); sessionStorage.setItem('admin_password', password); onLogin(password); } catch { setError('Ungültiges Passwort.'); } }; return (

Ollama Proxy Admin

setPassword(e.target.value)} placeholder="Admin-Passwort eingeben" autoFocus /> {error &&
{error}
}
); } const EMPTY_KEY_FORM = { name: '', expires_at: '', daily_tokens: '', monthly_tokens: '', daily_requests: '', monthly_requests: '', }; function SettingsSection({ password }) { const [settings, setSettings] = useState(null); const [availableModels, setAvailableModels] = useState([]); const [modelsLoading, setModelsLoading] = useState(false); const [ollamaReachable, setOllamaReachable] = useState(true); const [proxyEndpoint, setProxyEndpoint] = useState(null); const [saved, setSaved] = useState(false); const [error, setError] = useState(null); const fetchModels = async (url, currentModel) => { setModelsLoading(true); try { const res = await axios.get('/api/ollama-models', { headers: authHeaders(password), params: url ? { url } : {}, }); const { models, reachable } = res.data; setOllamaReachable(reachable); setAvailableModels(models); if (models.length > 0 && !models.includes(currentModel)) { setSettings(s => ({ ...s, default_model: models[0] })); } } catch { setOllamaReachable(false); setAvailableModels([]); } finally { setModelsLoading(false); } }; useEffect(() => { const headers = authHeaders(password); Promise.all([ axios.get('/api/settings', { headers }), axios.get('/api/proxy-info', { headers }), ]).then(([settingsRes, proxyRes]) => { const s = settingsRes.data; setSettings(s); setProxyEndpoint(proxyRes.data.endpoint); fetchModels(s.ollama_url, s.default_model); }).catch(() => setError('Einstellungen konnten nicht geladen werden.')); }, []); const handleSave = async (e) => { e.preventDefault(); setError(null); setSaved(false); try { await axios.put('/api/settings', settings, { headers: authHeaders(password) }); setSaved(true); setTimeout(() => setSaved(false), 3000); } catch { setError('Fehler beim Speichern.'); } }; if (!settings) return
Laden...
; return (

Einstellungen

{proxyEndpoint ?? '…'} (Änderung erfordert Neustart)
setSettings({ ...settings, ollama_url: e.target.value })} onBlur={(e) => fetchModels(e.target.value, settings.default_model)} placeholder="http://localhost:11434" required /> {!ollamaReachable && !modelsLoading && (
⚠ Ollama nicht erreichbar unter {settings.ollama_url}
)}
{modelsLoading ? ( Lade Modelle… ) : availableModels.length > 0 ? ( ) : ( setSettings({ ...settings, default_model: e.target.value })} placeholder="llama3" required /> )}
{error &&
{error}
} {saved &&
Gespeichert.
}
); } function App() { const [password, setPassword] = useState(() => sessionStorage.getItem('admin_password')); const [apiKeys, setApiKeys] = useState([]); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); const [newKey, setNewKey] = useState(null); const [form, setForm] = useState(EMPTY_KEY_FORM); const [creating, setCreating] = useState(false); const [editKey, setEditKey] = useState(null); const [editForm, setEditForm] = useState({}); useEffect(() => { if (!password) { setLoading(false); return; } fetchApiKeys().finally(() => setLoading(false)); }, [password]); const fetchApiKeys = async () => { try { const res = await axios.get('/api/api-keys', { headers: authHeaders(password) }); setApiKeys(res.data); } catch { setError('API-Keys konnten nicht geladen werden.'); } }; const handleCreate = async (e) => { e.preventDefault(); setCreating(true); try { const payload = { name: form.name }; if (form.expires_at) payload.expires_at = new Date(form.expires_at).toISOString(); if (form.daily_tokens) payload.daily_tokens = Number(form.daily_tokens) * 1000; if (form.monthly_tokens) payload.monthly_tokens = Number(form.monthly_tokens) * 1000; if (form.daily_requests) payload.daily_requests = Number(form.daily_requests); if (form.monthly_requests) payload.monthly_requests = Number(form.monthly_requests); const res = await axios.post('/api/api-keys', payload, { headers: authHeaders(password) }); setNewKey(res.data.plaintext_key); setForm(EMPTY_KEY_FORM); await fetchApiKeys(); } catch { setError('Fehler beim Erstellen des API-Keys.'); } finally { setCreating(false); } }; const handleDeactivate = async (id) => { try { await axios.put(`/api/api-keys/${id}/deactivate`, {}, { headers: authHeaders(password) }); await fetchApiKeys(); } catch { setError('Fehler beim Deaktivieren.'); } }; const handleActivate = async (id) => { try { await axios.put(`/api/api-keys/${id}/activate`, {}, { headers: authHeaders(password) }); await fetchApiKeys(); } catch { setError('Fehler beim Aktivieren.'); } }; const handleDelete = async (id, name) => { if (!window.confirm(`API-Key "${name}" wirklich löschen?`)) return; try { await axios.delete(`/api/api-keys/${id}`, { headers: authHeaders(password) }); if (editKey?.id === id) setEditKey(null); await fetchApiKeys(); } catch { setError('Fehler beim Löschen.'); } }; const handleEdit = (key) => { setEditKey(key); setEditForm({ daily_tokens: key.daily_tokens != null ? key.daily_tokens / 1000 : '', monthly_tokens: key.monthly_tokens != null ? key.monthly_tokens / 1000 : '', daily_requests: key.daily_requests ?? '', monthly_requests: key.monthly_requests ?? '', }); }; const handleSaveEdit = async (e) => { e.preventDefault(); try { const payload = { daily_tokens: editForm.daily_tokens !== '' ? Number(editForm.daily_tokens) * 1000 : null, monthly_tokens: editForm.monthly_tokens !== '' ? Number(editForm.monthly_tokens) * 1000 : null, daily_requests: editForm.daily_requests !== '' ? Number(editForm.daily_requests) : null, monthly_requests: editForm.monthly_requests !== '' ? Number(editForm.monthly_requests) : null, }; await axios.patch(`/api/api-keys/${editKey.id}/quota`, payload, { headers: authHeaders(password) }); setEditKey(null); await fetchApiKeys(); } catch { setError('Fehler beim Speichern der Limits.'); } }; const logout = () => { sessionStorage.removeItem('admin_password'); setPassword(null); }; if (!password) return ; if (loading) return
Laden...
; if (error) return
{error}
; return (

Ollama Proxy Admin

Neuer API-Key

{newKey && (
Neuer Key (nur einmal sichtbar): {newKey}
)}

API Keys

{apiKeys.map(key => ( ))}
Name Key Läuft ab Tokens/Tag Tokens/Monat Req/Tag Req/Monat
{key.name} {displayKey(key.key_prefix)} {key.expires_at ? new Date(key.expires_at).toLocaleDateString('de-DE', { timeZone: 'Europe/Berlin' }) : '∞'} {key.is_active ? ( ) : ( )}
{editKey && (

Limits bearbeiten: {editKey.name}

)}
); } ReactDOM.createRoot(document.getElementById('root')).render( );