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,selectedMode=null,controlTimer=null; 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)'],['music','Musik']].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('')}
`;} const modeLabels={llm:'LLM',video:'Video',music:'Musik'}; function control(s,root){ const v=s.video||{},active=v.mode||'llm'; if(!selectedMode)selectedMode=active==='switching'?(v.target_mode||'llm'):active; const selected=selectedMode,label=modeLabels[selected],switching=v.state==='switching',busy=pending||switching||s.state==='stopping'; const current=selected===active,available=selected==='video'?Boolean(v.service?.id):Boolean(s.counts[selected]?.available); const ready=current&&s.reachable&&!switching&&(selected!=='video'||v.service?.ready); const loading=current&&selected==='llm'&&s.worker.state==='loading'; const address=location.protocol+'//'+location.hostname+':'+s.port; const routes=selected==='llm'?[['Chat',address+'/v1/chat/completions'],['Modelle',address+'/v1/models']]:selected==='music'?[['Musik erzeugen',address+'/v1/audio/music'],['Musikmodelle',address+'/v1/audio/music/models']]:[['Video-API',address+'/']]; const reason=available?'':selected==='video'?'Wähle zuerst ein eingerichtetes Videomodell.':'Gib zuerst ein ausführbares Profil am API-Endpunkt frei.'; const state=switching?'Wechsel läuft':loading?'Modell wird geladen':ready?'Bereit':current&&s.state==='stopping'?'Wird gestoppt':current&&s.reachable?'Startet':current?'Gestoppt':'Nicht aktiv'; put(root.querySelector('#control-modes'),`
${Object.entries(modeLabels).map(([id,name])=>``).join('')}
`); put(root.querySelector('#endpoint-summary'),`

${label}

${e(state)}

${active==='switching'?`Wechsel zu ${e(modeLabels[v.target_mode]||v.target_mode)}`:s.reachable?`Aktiver Betrieb: ${e(modeLabels[active]||active)}`:'Betrieb gestoppt'}

${switching||loading||pending||s.state==='stopping'?`
${e(switching?v.switch_phase:loading?s.worker.phase||'Modell wird geladen':s.state==='stopping'?'Laufende Aufträge werden beendet':'Start wird angefordert')}
`:''}${reason?`

${e(reason)} Einrichten →

`:''}${(current&&(v.error||s.error||selected==='llm'&&s.worker.error))?``:''}

API-Adresse

${routes.map(([name,url])=>`
${name}${e(url)}
`).join('')}

${ready?'Erreichbar':'Erreichbar, sobald dieser Betrieb bereit ist'} · Port ${s.port}

${selected==='llm'?`

${s.counts.llm.available} Chatprofile freigegeben · ${e(s.worker.profile_name||'Gewichte laden bei der ersten Anfrage')}

`:selected==='music'?`

${e(v.music_profiles?.map(p=>p.name).join(', ')||'Kein Musikprofil freigegeben')} · Gewichte laden bei Anfrage

`:`

${e(v.service?.name||'Kein Videomodell ausgewählt')} · ${v.runtime_kind==='original'?'LTX Original':'ComfyUI'} · Gewichte laden bei Generierung

${ready?`${v.runtime_kind==='original'?'LTX DeskWEB':'ComfyUI'} öffnen →`:''}`}
Weitere Dienste & Einstellungen

Im LLM-Betrieb stehen auch freigegebene Bild-, TTS- und STT-Profile bereit.

Endpunkt und Port einstellen →

${s.scheduler.active_requests} laufende · ${s.scheduler.waiting_requests} wartende Aufträge

`); root.querySelectorAll('[data-control-mode]').forEach(b=>b.onclick=()=>{selectedMode=b.dataset.controlMode;control(s,root);}); root.querySelector('[data-control-start]').onclick=()=>controlAction('start',selected); root.querySelector('[data-control-stop]').onclick=()=>controlAction('stop',selected); } async function switchMode(mode){ const r=await fetch('/api/v1/video/mode',{method:'POST',headers:{'Content-Type':'application/json','X-Athena-Deck':'1'},body:JSON.stringify({mode})}); const result=await r.json();if(!r.ok)throw Error(result.error||'Wechsel fehlgeschlagen');return result; } async function controlAction(action,mode){ if(pending)return;pending=true;const root=document.querySelector('#endpoint-page'); root.querySelectorAll('button').forEach(b=>b.disabled=true); root.querySelector('#endpoint-message').innerHTML=`
${action==='start'?e(modeLabels[mode])+' wird gestartet':'Betrieb wird gestoppt'}
`; try{ const s=await api(); if(action==='start'){ if((s.video?.mode||'llm')!==mode||(mode==='video'&&!s.video?.service?.ready))await switchMode(mode); if(s.state==='stopped')await api('/start',{}); }else{ await api('/stop',{}); if(s.video&&s.video.mode!=='llm')await switchMode('llm'); } if(root.isConnected)root.querySelector('#endpoint-message').textContent=''; }catch(error){if(root.isConnected)root.querySelector('#endpoint-message').textContent=error.message;} finally{pending=false;if(root.isConnected){root.querySelector('#control-modes')._deckHtml=null;root.querySelector('#endpoint-summary')._deckHtml=null;render(false);}} } async function render(settings=false){ const view=document.querySelector('#view');let root=view.querySelector('#endpoint-page'); if(!root||root.dataset.settings!==String(settings)){ sequence++;clearTimeout(controlTimer);selectedMode=null;view.innerHTML=`
${settings?'EINSTELLUNGEN / API':'ATHENA DECK / ROUTER'}

${settings?'Endpunkt':'Steuerung'}

${settings?'

API-Port und freigegebene Profile verwalten.

':'

Betriebsart wählen und starten.

'}

${settings?'

Lokaler API-Port

Der Zugriff verwendet den API-Token aus Zugang & API-Token. 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'); if(settings)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; if(!settings){control(s,root);clearTimeout(controlTimer);if(s.video?.state==='switching'||s.worker.state==='loading'||s.state==='stopping')controlTimer=setTimeout(()=>{if(root.isConnected)render(false);},1000);return;} put(root.querySelector('#endpoint-summary'),summary(s)); 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}; })();