Kansho/frontend/src/pages/AdminProvidersPage.jsx

172 lines
6.1 KiB
React
Raw Normal View History

2026-08-25 13:57:23 +02:00
import { useEffect, useState } from 'react'
import { api } from '../api.js'
import { useAuth } from '../context/AuthContext.jsx'
const emptyRole = () => ({
role: '',
name: '',
url: '',
model: '',
zdr: true,
no_train: true,
key: ''
})
function statusLabel(role) {
if (role.role === 'detect' && !role.url) return 'nicht konfiguriert, fail-closed'
2026-08-25 13:57:23 +02:00
if (role.role === 'detect' && role.url && !role.local && role.ready) {
return role.remote_plaintext_allowed === false
? 'extern konfiguriert, Produktiv blockiert'
: 'bereit (nur Development/Test, extern)'
}
if (role.ready) return 'bereit'
if (!role.key_present && !role.local) return 'Key fehlt'
if (!role.url) return 'URL fehlt'
return 'nicht bereit'
}
export default function AdminProvidersPage() {
const { session } = useAuth()
const [data, setData] = useState(null)
const [forms, setForms] = useState({})
const [error, setError] = useState('')
const [notice, setNotice] = useState('')
const [saving, setSaving] = useState('')
const load = () =>
api('/api/admin/providers', { token: session.token })
.then((payload) => {
setData(payload)
const next = {}
for (const role of payload.roles) {
next[role.role] = {
role: role.role,
name: role.name || '',
url: role.url || '',
model: role.model || '',
zdr: Boolean(role.zdr),
no_train: Boolean(role.no_train),
key: ''
}
}
setForms(next)
})
.catch((e) => setError(e.message))
useEffect(() => { load() }, [session.token])
const update = (role, field, value) => {
setForms((prev) => ({ ...prev, [role]: { ...(prev[role] || emptyRole()), [field]: value } }))
}
const save = async (role) => {
const form = forms[role]
if (!form) return
setError('')
setNotice('')
setSaving(role)
try {
const body = {
name: form.name,
url: form.url,
model: form.model,
zdr: form.zdr,
no_train: form.no_train
}
if (form.key.trim()) body.key = form.key.trim()
const payload = await api(`/api/admin/providers/${role}`, {
token: session.token,
method: 'PUT',
body
})
setData(payload)
setForms((prev) => ({ ...prev, [role]: { ...prev[role], key: '' } }))
setNotice(form.key.trim() ? `${role}: gespeichert, Key in .env geschrieben.` : `${role}: gespeichert.`)
} catch (err) {
setError(err.message)
} finally {
setSaving('')
}
}
return (
<section className="card">
<h1>Schnittstellen</h1>
<p className="muted">
Zwei eigene Einträge: Maskierung (vollständige semantische Detection) und Sprachmodell (Dialogzug und Journalentwurf).
Externes Klartext-Detect ist nur in Development/Test zulässig. Produktiv ist ein lokales Detect-Modell nötig. Ohne Detection kein Generate.
2026-08-25 13:57:23 +02:00
Der Secret-Key wird nie angezeigt und nie in der Datenbank gespeichert.
</p>
{error && <p className="error">{error}</p>}
{notice && <p>{notice}</p>}
{data && (
<>
{data.roles.map((role) => {
const form = forms[role.role] || emptyRole()
return (
<form
key={role.role}
className="stack"
onSubmit={(e) => { e.preventDefault(); save(role.role) }}
>
<h2>{role.title || role.role}</h2>
{role.task && <p className="muted">{role.task}</p>}
<dl className="meta">
<dt>Status</dt>
<dd>{statusLabel(role)}</dd>
<dt>Key</dt>
<dd>{role.key_present ? (role.key_source === 'generate' ? 'über Sprachmodell' : 'gesetzt') : 'fehlt'}</dd>
<dt>Modus</dt>
<dd>{role.local ? 'lokal' : 'extern'} · {role.mode}</dd>
</dl>
<label>
Name
<input value={form.name} onChange={(e) => update(role.role, 'name', e.target.value)} />
</label>
<label>
URL
<input
value={form.url}
onChange={(e) => update(role.role, 'url', e.target.value)}
placeholder={role.role === 'detect' ? 'http://127.0.0.1:11434/v1/chat/completions' : ''}
/>
</label>
<label>
Modell
<input value={form.model} onChange={(e) => update(role.role, 'model', e.target.value)} />
</label>
<label className="check">
<input type="checkbox" checked={form.zdr} onChange={(e) => update(role.role, 'zdr', e.target.checked)} />
Zero Data Retention
</label>
<label className="check">
<input
type="checkbox"
checked={form.no_train}
onChange={(e) => update(role.role, 'no_train', e.target.checked)}
/>
kein Provider-Training
</label>
<label>
API-Key {role.key_present ? '(leer lassen, um den bestehenden zu behalten)' : '(pflichtig für externe URL)'}
<input
type="password"
autoComplete="off"
value={form.key}
onChange={(e) => update(role.role, 'key', e.target.value)}
placeholder={role.role === 'detect' ? 'leer = Key des Sprachmodells' : ''}
/>
</label>
<button type="submit" disabled={saving === role.role}>
{saving === role.role ? 'Speichert …' : 'Speichern'}
</button>
</form>
)
})}
<p className="muted">Ohne ZDR und ohne Trainingsverbot bleibt das Sprachmodell fail-closed. Maskierung wählt keine Operation. Gateway lässt sich darüber nicht umgehen.</p>
</>
)}
</section>
)
}