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 : ''
} )
2026-09-08 10:02:28 +02:00
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' ]
}
2026-08-25 13:57:23 +02:00
function statusLabel ( role ) {
2026-08-27 10:03:05 +02:00
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 ) {
2026-09-08 10:02:28 +02:00
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)'
2026-08-25 13:57:23 +02:00
}
if ( role . ready ) return 'bereit'
if ( ! role . key _present && ! role . local ) return 'Key fehlt'
if ( ! role . url ) return 'URL fehlt'
return 'nicht bereit'
}
2026-09-08 10:02:28 +02:00
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 ''
}
2026-08-25 13:57:23 +02:00
export default function AdminProvidersPage ( ) {
const { session } = useAuth ( )
const [ data , setData ] = useState ( null )
const [ forms , setForms ] = useState ( { } )
2026-09-08 10:02:28 +02:00
const [ catalog , setCatalog ] = useState ( { generate : [ ] , detect : [ ] } )
2026-08-25 13:57:23 +02:00
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 ) )
2026-09-08 10:02:28 +02:00
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 ] )
2026-08-25 13:57:23 +02:00
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. ` )
2026-09-08 10:02:28 +02:00
loadCatalog ( )
2026-08-25 13:57:23 +02:00
} catch ( err ) {
setError ( err . message )
} finally {
setSaving ( '' )
}
}
2026-09-08 10:02:28 +02:00
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 )
2026-08-25 13:57:23 +02:00
return (
< section className = "card" >
< h1 > Schnittstellen < / h1 >
< p className = "muted" >
2026-09-08 10:02:28 +02:00
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 .
2026-08-25 13:57:23 +02:00
< / p >
2026-09-08 10:02:28 +02:00
{ data && (
< p className = "muted" > Laufzeit : { data . runtime _env } < / p >
) }
{ banner && < p > { banner } < / p > }
2026-08-25 13:57:23 +02:00
{ error && < p className = "error" > { error } < / p > }
{ notice && < p > { notice } < / p > }
{ data && (
< >
{ data . roles . map ( ( role ) => {
const form = forms [ role . role ] || emptyRole ( )
2026-09-08 10:02:28 +02:00
const options = modelOptions ( role . role )
const listId = ` kansho-models- ${ role . role } `
2026-08-25 13:57:23 +02:00
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 >
2026-09-08 10:02:28 +02:00
Modell für diese Stufe
< input
list = { listId }
value = { form . model }
onChange = { ( e ) => update ( role . role , 'model' , e . target . value ) }
placeholder = { role . role === 'detect' ? 'openai/gpt-4.1-nano oder lokales Ollama-Modell' : 'openai/gpt-5.4' }
/ >
< datalist id = { listId } >
{ options . map ( ( item ) => (
< option key = { item . id } value = { item . id } >
{ item . name && item . name !== item . id ? item . name : item . id }
< / option >
) ) }
< / datalist >
2026-08-25 13:57:23 +02:00
< / 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 >
)
}