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: '' }) const FALLBACK_MODELS = { generate: ['openai/gpt-5.4', 'openai/gpt-4o', 'openai/gpt-4o-mini', 'anthropic/claude-sonnet-4'], detect: ['openai/gpt-4.1-nano', 'openai/gpt-4o-mini', 'llama3.1'] } function statusLabel(role) { if (role.role === 'detect' && !role.url) return 'nicht konfiguriert, fail-closed' if (role.role === 'detect' && role.url && !role.local && role.ready) { if (role.remote_plaintext_allowed === false) return 'extern konfiguriert, Produktiv blockiert' if (role.remote_plaintext_reason === 'operator_override') { return 'bereit (Übergang: externes Klartext-Detect)' } return 'bereit (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' } function detectBanner(data) { if (!data) return '' if (data.runtime_env === 'production' && data.remote_plaintext_reason === 'operator_override') { return 'Production erlaubt derzeit externes Klartext-Detect (KANSHO_ALLOW_REMOTE_DETECT). Das Gateway bleibt aktiv; Generate sieht nur maskierten Kontext. Ziel bleibt ein lokales Detect-Modell.' } if (data.runtime_env === 'production' && data.remote_plaintext_allowed === false) { return 'Production blockiert externes Klartext-Detect. Maskierung braucht eine lokale URL oder die Operator-Freigabe, sonst entsteht kein Impuls.' } return '' } export default function AdminProvidersPage() { const { session } = useAuth() const [data, setData] = useState(null) const [forms, setForms] = useState({}) const [catalog, setCatalog] = useState({ generate: [], detect: [] }) 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)) const loadCatalog = () => api('/api/admin/providers/models', { token: session.token }) .then((payload) => { setCatalog({ generate: payload.roles?.generate || [], detect: payload.roles?.detect || [] }) }) .catch(() => { setCatalog({ generate: [], detect: [] }) }) useEffect(() => { load() loadCatalog() }, [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.`) loadCatalog() } catch (err) { setError(err.message) } finally { setSaving('') } } const modelOptions = (role) => { const fromCatalog = catalog[role] || [] if (fromCatalog.length) return fromCatalog return (FALLBACK_MODELS[role] || []).map((id) => ({ id, name: id })) } const banner = detectBanner(data) return (

Schnittstellen

Zwei Stufen, zwei eigene Modelle: Maskierung prüft den persönlichen Text, das Sprachmodell schreibt Dialogzug und Journalentwurf. Pro Stufe URL und Modell getrennt wählen. Ohne Detection kein Generate. Der Secret-Key wird nie angezeigt und nie in der Datenbank gespeichert.

{data && (

Laufzeit: {data.runtime_env}

)} {banner &&

{banner}

} {error &&

{error}

} {notice &&

{notice}

} {data && ( <> {data.roles.map((role) => { const form = forms[role.role] || emptyRole() const options = modelOptions(role.role) const listId = `kansho-models-${role.role}` return (
{ e.preventDefault(); save(role.role) }} >

{role.title || role.role}

{role.task &&

{role.task}

}
Status
{statusLabel(role)}
Key
{role.key_present ? (role.key_source === 'generate' ? 'über Sprachmodell' : 'gesetzt') : 'fehlt'}
Modus
{role.local ? 'lokal' : 'extern'} · {role.mode}
) })}

Ohne ZDR und ohne Trainingsverbot bleibt das Sprachmodell fail-closed. Maskierung wählt keine Operation. Gateway lässt sich darüber nicht umgehen.

)}
) }