diff --git a/docker-ui.js b/docker-ui.js index d298bad..825aadd 100644 --- a/docker-ui.js +++ b/docker-ui.js @@ -6,11 +6,11 @@ window.DockerUI=(()=>{ 'ltx-deskweb':{title:'LTX DeskWEB',description:'LTX-Webstudio für Videoerzeugung über Decks originale LTX-Laufzeit.',port:8118,hint:'Videoprofil freigeben und in der Steuerung Video starten.'}, 'athena-swarm-ui':{title:'SwarmUI',description:'API By URL · Oberfläche für Decks ComfyUI-Laufzeit.',port:8125,hint:'In der Steuerung Video mit ComfyUI starten.'} }; - function appInfo(c){const a=apps[c.name];return a?`

${esc(a.description)}

${esc(a.title)} öffnen →

${esc(a.hint)} SSH-Tunnel: Port ${a.port}.

`:'';} + function appInfo(c){const a=apps[c.name];return a?`

${esc(a.description)}

${esc(a.title)} öffnen →

${esc(a.hint)} Port ${a.port}.

`:'';} async function api(action='',data){const r=await fetch('/api/v1/docker'+action,data?{method:'POST',headers:{'Content-Type':'application/json','X-Athena-Deck':'1'},body:JSON.stringify(data)}:{});if(r.status===401){location.reload();throw Error('Bitte erneut anmelden.');}const value=await r.json();if(!r.ok)throw Error(value.error||'Docker-Anfrage fehlgeschlagen');return value;} function html(services=false){return `
${services?'ANWENDUNGEN':'EINSTELLUNGEN / LAUFZEITEN'}

${services?'Weitere Dienste':'Docker'}

${services?'Zusätzliche Oberflächen werden getrennt von Modellen und Laufzeiten verwaltet. Hier erscheinen ausschließlich ausdrücklich für Deck markierte Anwendungscontainer.':'Optionale Umgebung für zusätzliche Studios. Native Modelllaufzeiten bleiben unabhängig davon.'}

`;} function bind(services=false){const panel=document.querySelector('#docker-panel'),message=document.querySelector('#docker-message');let signature='',pending=false,timer; - async function refresh(){clearTimeout(timer);try{if(pending)return;const s=await api();if(!panel.isConnected)return;const next=JSON.stringify(s);if(signature!==next){signature=next;const running=s.job?.state==='running'; + async function refresh(){clearTimeout(timer);try{if(pending)return;const [s]=await Promise.all([api(),DeckLinks.refresh()]);if(!panel.isConnected)return;const next=JSON.stringify(s);if(signature!==next){signature=next;const running=s.job?.state==='running'; if(services){panel.innerHTML=`

Explizite Zuordnung

Beide Container-Labels müssen gesetzt sein:

io.athena-deck.managed=true\nio.athena-deck.role=application

Keine automatische Übernahme vorhandener Container. Labels werden beim Erstellen in Docker Compose gesetzt. Starten und Stoppen betrifft nur markierte Container. Löschen entfernt den Container einschließlich seiner beschreibbaren Schicht; Images und Volumes bleiben erhalten.

Docker-Verbindung prüfen →
${!s.helper_available||!s.reachable?'

Docker nicht verbunden

'+esc(s.message)+'

':s.services.length?'
'+s.services.map(c=>`
DOCKER · DECK-ANWENDUNG

${esc(c.name)}

${esc(c.image)}

${esc(c.state)} · ${esc(c.status)}

${appInfo(c)}
`).join('')+'
' :'

Noch keine Deck-Anwendungen

Docker ist erreichbar. Kein Container trägt beide erforderlichen Labels. Bestehende Router-, WireGuard- und andere Container bleiben ausgeblendet.

'}`;} else {panel.innerHTML=`
${s.installed?'INSTALLIERT':s.installed===false?'NICHT INSTALLIERT':'NICHT ERMITTELBAR'}

Docker Engine ${esc(s.version||'')}

${s.reachable?'Daemon erreichbar · Verbindung über begrenzten Systemhelfer':s.helper_available?'Systemhelfer erreichbar · Docker-Daemon nicht erreichbar':'Systemhelfer nicht verbunden'}

${esc(s.message)}

${s.job?`

${esc(s.job.state)} · ${esc(s.job.phase)}

`:''}${!s.installed&&s.install_supported&&!running?'

':``}

Keine automatische Aktualisierung vorhandener Docker-Installationen. NVIDIA Container Toolkit und Treiber sind separate Voraussetzungen für GPU-Container. Die Paketinstallation wird nicht mitten im Vorgang abgebrochen.

Weitere Dienste öffnen →
`; const button=panel.querySelector('#docker-install'),ack=panel.querySelector('#docker-install-ack');if(button){ack.onchange=()=>button.disabled=pending||!ack.checked;button.onclick=async()=>{pending=true;button.disabled=true;try{await api('/install',{confirm:true});message.textContent='Docker-Erstinstallation gestartet.';}catch(error){message.textContent=error.message;}finally{pending=false;signature='';}};} diff --git a/endpoint-ui.js b/endpoint-ui.js index 07f30fb..e80aebb 100644 --- a/endpoint-ui.js +++ b/endpoint-ui.js @@ -13,12 +13,12 @@ window.EndpointUI=(()=>{ const current=selected===active,available=selected==='video'?Boolean(v.service?.id):selected==='separator'?Boolean(v.separator?.installed&&v.separator.models?.some(m=>m.installed)):Boolean(s.counts[selected]?.available); const ready=current&&(selected==='separator'||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==='separator'?[['Trennauftrag',location.protocol+'//'+location.hostname+':8108/api/v1/separator-jobs/start'],['Status',location.protocol+'//'+location.hostname+':8108/api/v1/separator-tests']]:selected==='music'?[['Musik erzeugen',address+'/v1/audio/music'],['Musikmodelle',address+'/v1/audio/music/models']]:[['Video-API',address+'/']]; + const address=DeckLinks.url(s.port).replace(/\/$/,''); + const routes=selected==='llm'?[['Chat',address+'/v1/chat/completions'],['Modelle',address+'/v1/models']]:selected==='separator'?[['Trennauftrag',DeckLinks.url(8108,'/api/v1/separator-jobs/start')],['Status',DeckLinks.url(8108,'/api/v1/separator-tests')]]: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.':selected==='separator'?'Installiere zuerst die Laufzeit und ein Trennmodellpaket.':'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||active==='separator')?`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 ${selected==='separator'?8108:s.port}

${selected==='separator'?`

${v.separator?.models?.filter(m=>m.installed).length||0} Modellpakete bereit · Gewichte laden beim Trennauftrag und werden anschließend entladen.

Audio-Trennoberfläche öffnen →

Modelle und Aufträge →

`: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

${available?`LadyPoly YuE2 öffnen →${!ready?'

Vor dem Generieren Musik starten.

':''}`:''}`:`

${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

`); + put(root.querySelector('#endpoint-summary'),`

${label}

${e(state)}

${active==='switching'?`Wechsel zu ${e(modeLabels[v.target_mode]||v.target_mode)}`:(s.reachable||active==='separator')?`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 ${selected==='separator'?8108:s.port}

${selected==='separator'?`

${v.separator?.models?.filter(m=>m.installed).length||0} Modellpakete bereit · Gewichte laden beim Trennauftrag und werden anschließend entladen.

Audio-Trennoberfläche öffnen →

Modelle und Aufträge →

`: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

${available?`LadyPoly YuE2 öffnen →${!ready?'

Vor dem Generieren Musik starten.

':''}`:''}`:`

${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); @@ -53,7 +53,7 @@ window.EndpointUI=(()=>{ 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; + try{const [s]=await Promise.all([api(),DeckLinks.refresh()]);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'; diff --git a/network-ui.js b/network-ui.js index f7b3450..0cce90f 100644 --- a/network-ui.js +++ b/network-ui.js @@ -54,3 +54,17 @@ async function networkPage(){ networkButtons(); }catch(e){if(document.querySelector('#network-message'))document.querySelector('#network-message').textContent=e.message;} } + +/* Application links follow confirmed access policy, including SSH management views. */ +window.DeckLinks=(()=>{ + let status=null,checked=0; + async function refresh(){ + if(Date.now()-checked<5000)return; + try{status=await networkRequest('');checked=Date.now();}catch{} + } + function url(port,path='/'){ + const base=status?.enabled&&status?.mode==='tunnel'&&status?.tunnel_url?new URL(status.tunnel_url):new URL(location.href); + base.port=String(port);base.pathname=path;base.search='';base.hash='';return base.href; + } + return {refresh,url}; +})();