window.EndpointUI=(()=>{ const e=v=>String(v??'—').replace(/[&<>"']/g,c=>({'&':'&','<':'<','>':'>','"':'"',"'":'''}[c])); const stateName={running:'Läuft',stopped:'Gestoppt',stopping:'Wird gestoppt · laufende Aufträge werden beendet',loading:'Modell lädt',ready:'Modell bereit',failed:'Fehlgeschlagen'}; let pending=false,sequence=0; async function api(path='',data){const r=await fetch('/api/v1/endpoint'+path,data!==undefined?{method:'POST',headers:{'Content-Type':'application/json','X-Athena-Deck':'1'},body:JSON.stringify(data)}:{});const v=await r.json();if(!r.ok)throw Error(v.error||'Endpunkt nicht erreichbar');return v;} function put(el,html){if(el._deckHtml!==html){el.innerHTML=html;el._deckHtml=html;}} function summary(s){return `

OpenAI-kompatibler Endpunkt

${e(stateName[s.state])}
Erreichbarkeit${s.reachable?'Listener bereit':'Nicht erreichbar'}
Lokaler API-Port${s.port}
Laufende / wartende Aufträge${s.scheduler.active_requests} / ${s.scheduler.waiting_requests}

${e(s.base_url)} · Port und Profile einstellen →

Geladen: ${e(s.worker.profile_name||'Kein Sprachmodell')} · ${e(stateName[s.worker.state]||s.worker.state)}

${s.worker.error||s.error?`

${e(s.worker.error||s.error)}

`:''}

Aktivierte Profile werden angeboten. Erst eine Anfrage lädt das Modell. Wechsel warten auf laufende Antworten; Bildaufträge entladen vorher das eigene Sprachmodell.

${[['llm','Sprachmodelle'],['image','Bildgenerierung'],['tts','Text → Sprache (TTS)'],['stt','Sprache → Text (STT)']].map(([key,label])=>{const c=s.counts[key];return `

${label}

${c.supported?`${c.enabled} Profile aktiviert · ${c.available} ausführbar`:'Noch keine eigene Laufzeit eingerichtet'}

${c.loaded?'AKTIV IM SPEICHER':c.available?'BEI ANFRAGE LADEN':'NICHT AKTIV'}
`}).join('')}
`;} async function render(settings=false){ const view=document.querySelector('#view');let root=view.querySelector('#endpoint-page'); if(!root||root.dataset.settings!==String(settings)){ sequence++;view.innerHTML=`
${settings?'EINSTELLUNGEN / API':'ATHENA DECK / ROUTER'}

${settings?'Endpunkt & Profile':'Dein Modell-Endpunkt'}

Eigener Router für Deck-Profile. Bestehende Athena-Dienste bleiben unabhängig.

${settings?'

Lokaler API-Port

Der Zugriff verwendet den API-Token aus Zugang & API. Verwaltungsoberfläche und API haben getrennte Ports.

Am Endpunkt angebotene Profile

Nur explizit aktivierte, ausführbare Profile erscheinen in GET /v1/models. Aktivieren lädt noch kein Modell.

API-Routen

POST /v1/chat/completions · Text und Streaming
POST /v1/images/generations · Qwen-Image-Rezept, ein Bild als b64_json
POST /v1/audio/speech und POST /v1/audio/transcriptions · noch nicht eingerichtet (501)

Alle Routen nutzen denselben Port und Bearer-Token. Bei Bildaufträgen bestimmt das Profil Auflösung, Schritte, Guidance und Seed. Noch keine Bildbearbeitung oder Vision-Eingaben.

':''}
`; root=view.querySelector('#endpoint-page'); for(const action of ['start','stop'])root.querySelector('#endpoint-'+action).onclick=()=>mutate('/'+action,{}); root.querySelector('#endpoint-port-form')?.addEventListener('submit',event=>{event.preventDefault();mutate('/config',{port:Number(root.querySelector('#endpoint-port').value)});}); } if(pending)return;const id=sequence; try{const s=await api();if(!root.isConnected||id!==sequence)return; put(root.querySelector('#endpoint-summary'),summary(s)); if(s.video)VideoUI.mode(root.querySelector('#video-mode-panel'),s.video,s.profiles,s.port); root.querySelector('#endpoint-start').disabled=s.state!=='stopped';root.querySelector('#endpoint-stop').disabled=s.state!=='running'; if(settings){const port=root.querySelector('#endpoint-port');if(document.activeElement!==port)port.value=s.port;port.disabled=s.state!=='stopped';root.querySelector('#endpoint-port-save').disabled=s.state!=='stopped'; root.querySelector('#endpoint-port-help').textContent=s.allowed_ports.length?'Docker-Testinstallation: veröffentlichte Loopback-Ports '+s.allowed_ports.join(', ')+'. Portwechsel nur bei gestopptem Endpunkt. Der SSH-Tunnel muss denselben Port weiterleiten.':'Nativer Dienst: freie Ports zwischen 1024 und 65535, nur bei gestopptem Endpunkt.'; put(root.querySelector('#endpoint-profiles'),s.profiles.map(p=>`
${e(p.name)} · ${e(p.kind)} · ${p.runnable?'ausführbar':e(p.blockers.join(' '))}
`).join('')||'

Noch keine Profile angelegt. Erstelle sie in der jeweiligen Modellkategorie.

'); root.querySelectorAll('[data-enable]').forEach(b=>{const p=s.profiles.find(p=>p.id===b.dataset.enable);b.disabled=!p.runnable&&!p.enabled;b.onclick=()=>mutate('/profile',{id:b.dataset.enable,enabled:b.dataset.value==='true'});}); } }catch(error){if(root.isConnected)root.querySelector('#endpoint-message').textContent=error.message;} } async function mutate(path,data){if(pending)return;pending=true;const root=document.querySelector('#endpoint-page');root.querySelectorAll('button').forEach(b=>b.disabled=true);try{await api(path,data);root.querySelector('#endpoint-message').textContent=path==='/stop'?'Neue Anfragen werden abgewiesen; laufende Aufträge werden beendet.':'Einstellung übernommen.';}catch(error){if(root.isConnected)root.querySelector('#endpoint-message').textContent=error.message;}finally{pending=false;if(root.isConnected)render(root.dataset.settings==='true');}} const runningHTML=()=>'

Laufzeitstatus wird geladen …

'; async function bindRunning(){const root=document.querySelector('#endpoint-running');if(!root)return;try{const s=await api();if(root.isConnected)root.innerHTML=`

${e(s.worker.profile_name||'Kein Sprachmodell geladen')}

${e(stateName[s.worker.state]||s.worker.state)} · ${s.scheduler.active_requests} laufende Anfragen · ${s.scheduler.waiting_requests} wartend

${e(s.worker.error||'Aktivierte Profile werden bei einer API-Anfrage automatisch geladen.')}

Endpunkt und Profile verwalten →
`;}catch(error){if(root.isConnected)root.textContent=error.message;}if(root.isConnected)setTimeout(()=>{if(root.isConnected)bindRunning();},3000);} return {render,runningHTML,bindRunning}; })();