Use WireGuard access address for application and API links

This commit is contained in:
Mikei386 committed 2026-10-01 23:17:13 +02:00
1 parent 11f16ef4a3
commit b451b52a57
3 files changed
+20 -6

No files matched your search

+2 -2
View File
@@ -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?`<p>${esc(a.description)}</p><a class="link" href="${esc(location.protocol+'//'+location.hostname+':'+a.port+'/')}" target="_blank" rel="noopener">${esc(a.title)} öffnen →</a><p class="small">${esc(a.hint)} SSH-Tunnel: Port ${a.port}.</p>`:'';}
function appInfo(c){const a=apps[c.name];return a?`<p>${esc(a.description)}</p><a class="link" href="${esc(DeckLinks.url(a.port))}" target="_blank" rel="noopener">${esc(a.title)} öffnen →</a><p class="small">${esc(a.hint)} Port ${a.port}.</p>`:'';}
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 `<div class="kicker">${services?'ANWENDUNGEN':'EINSTELLUNGEN / LAUFZEITEN'}</div><h1>${services?'Weitere Dienste':'Docker'}</h1><p>${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.'}</p><section id="docker-panel"></section><p id="docker-message" role="status"></p>`;}
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=`<section class="card"><h2>Explizite Zuordnung</h2><p>Beide Container-Labels müssen gesetzt sein:</p><pre><code>io.athena-deck.managed=true\nio.athena-deck.role=application</code></pre><p class="small">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.</p><a class="link" href="#docker-runtime">Docker-Verbindung prüfen →</a></section>${!s.helper_available||!s.reachable?'<section class="empty"><h2>Docker nicht verbunden</h2><p>'+esc(s.message)+'</p></section>':s.services.length?'<div class="grid">'+s.services.map(c=>`<section class="card"><span class="pill">DOCKER · DECK-ANWENDUNG</span><h2>${esc(c.name)}</h2><p>${esc(c.image)}</p><p><strong>${esc(c.state)}</strong> · ${esc(c.status)}</p>${appInfo(c)}<div class="card-actions"><button data-container-action="start" data-container-id="${esc(c.id)}" ${c.state==='running'?'disabled':''}>Starten</button><button class="secondary" data-container-action="stop" data-container-id="${esc(c.id)}" ${c.state!=='running'?'disabled':''}>Stoppen</button><button class="secondary danger" data-container-action="delete" data-container-id="${esc(c.id)}" ${!['exited','created','dead'].includes(c.state)?'disabled':''}>Löschen</button></div></section>`).join('')+'</div>' :'<section class="empty"><h2>Noch keine Deck-Anwendungen</h2><p>Docker ist erreichbar. Kein Container trägt beide erforderlichen Labels. Bestehende Router-, WireGuard- und andere Container bleiben ausgeblendet.</p></section>'}`;}
else {panel.innerHTML=`<section class="card"><span class="pill">${s.installed?'INSTALLIERT':s.installed===false?'NICHT INSTALLIERT':'NICHT ERMITTELBAR'}</span><h2>Docker Engine ${esc(s.version||'')}</h2><p>${s.reachable?'Daemon erreichbar · Verbindung über begrenzten Systemhelfer':s.helper_available?'Systemhelfer erreichbar · Docker-Daemon nicht erreichbar':'Systemhelfer nicht verbunden'}</p><p>${esc(s.message)}</p>${s.job?`<p role="status">${esc(s.job.state)} · ${esc(s.job.phase)}</p>`:''}${!s.installed&&s.install_supported&&!running?'<label><input type="checkbox" id="docker-install-ack"> Docker Engine, Compose und Buildx auf diesem Server installieren. Docker richtet einen Dienst und Netzwerk-/Firewallregeln ein.</label><p><button id="docker-install" disabled>Docker installieren</button></p>':`<button disabled>${running?'Installation läuft …':s.installed?'Docker bereits vorhanden':'Installation hier nicht verfügbar'}</button>`}<p class="small">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.</p><a class="link" href="#services">Weitere Dienste öffnen →</a></section>`;
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='';}};}
+4 -4
View File
@@ -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'),`<div class="control-modes" role="tablist" aria-label="Betriebsart">${Object.entries(modeLabels).map(([id,name])=>`<button role="tab" aria-selected="${selected===id}" data-control-mode="${id}" class="${selected===id?'chosen':'secondary'}" ${busy?'disabled':''}>${name}</button>`).join('')}</div>`);
put(root.querySelector('#endpoint-summary'),`<section class="card control-card" role="tabpanel"><div class="control-heading"><h2>${label}</h2><span class="pill">${e(state)}</span></div><p>${active==='switching'?`Wechsel zu ${e(modeLabels[v.target_mode]||v.target_mode)}`:(s.reachable||active==='separator')?`Aktiver Betrieb: ${e(modeLabels[active]||active)}`:'Betrieb gestoppt'}</p><div class="card-actions"><button data-control-start ${busy||!available||ready?'disabled':''}>${label} starten</button><button data-control-stop class="secondary" ${busy||!current||(s.state==='stopped'&&active==='llm')?'disabled':''}>Stoppen</button></div>${switching||loading||pending||s.state==='stopping'?`<div class="video-activity" role="status" aria-live="polite"><strong>${e(switching?v.switch_phase:loading?s.worker.phase||'Modell wird geladen':s.state==='stopping'?'Laufende Aufträge werden beendet':'Start wird angefordert')}</strong><progress aria-label="Fortschritt"></progress></div>`:''}${reason?`<p class="warning">${e(reason)} <a class="link" href="#${selected==='video'?'video':selected==='music'?'music':selected==='separator'?'separation':'chat'}">Einrichten →</a></p>`:''}${(current&&(v.error||s.error||selected==='llm'&&s.worker.error))?`<p class="video-error" role="alert">${e(v.error||s.error||s.worker.error)}</p>`:''}<div class="control-api"><h3>API-Adresse</h3>${routes.map(([name,url])=>`<div><span>${name}</span><code>${e(url)}</code></div>`).join('')}<p class="small">${ready?'Erreichbar':'Erreichbar, sobald dieser Betrieb bereit ist'} · Port ${selected==='separator'?8108:s.port}</p></div>${selected==='separator'?`<p class="small">${v.separator?.models?.filter(m=>m.installed).length||0} Modellpakete bereit · Gewichte laden beim Trennauftrag und werden anschließend entladen.</p><a class="link" href="${e(location.protocol+'//'+location.hostname+':8128/')}" target="_blank" rel="noopener">Audio-Trennoberfläche öffnen →</a><p><a class="link" href="#separation">Modelle und Aufträge →</a></p>`:selected==='llm'?`<p class="small">${s.counts.llm.available} Chatprofile freigegeben · ${e(s.worker.profile_name||'Gewichte laden bei der ersten Anfrage')}</p>`:selected==='music'?`<p class="small">${e(v.music_profiles?.map(p=>p.name).join(', ')||'Kein Musikprofil freigegeben')} · Gewichte laden bei Anfrage</p>${available?`<a class="link" href="${e(location.protocol+'//'+location.hostname+':8127/')}" target="_blank" rel="noopener">LadyPoly YuE2 öffnen →</a>${!ready?'<p class="small">Vor dem Generieren Musik starten.</p>':''}`:''}`:`<p class="small">${e(v.service?.name||'Kein Videomodell ausgewählt')} · ${v.runtime_kind==='original'?'LTX Original':'ComfyUI'} · Gewichte laden bei Generierung</p>${ready?`<a class="link" href="${e(v.runtime_kind==='original'?location.protocol+'//'+location.hostname+':8118/':address+'/')}" target="_blank" rel="noopener">${v.runtime_kind==='original'?'LTX DeskWEB':'ComfyUI'} öffnen →</a>`:''}`}<details class="control-details"><summary>Weitere Dienste & Einstellungen</summary><p>Im LLM-Betrieb stehen auch freigegebene Bild-, TTS- und STT-Profile bereit.</p><p><a class="link" href="#endpoint">Endpunkt und Port einstellen →</a></p><p>${s.scheduler.active_requests} laufende · ${s.scheduler.waiting_requests} wartende Aufträge</p></details></section>`);
put(root.querySelector('#endpoint-summary'),`<section class="card control-card" role="tabpanel"><div class="control-heading"><h2>${label}</h2><span class="pill">${e(state)}</span></div><p>${active==='switching'?`Wechsel zu ${e(modeLabels[v.target_mode]||v.target_mode)}`:(s.reachable||active==='separator')?`Aktiver Betrieb: ${e(modeLabels[active]||active)}`:'Betrieb gestoppt'}</p><div class="card-actions"><button data-control-start ${busy||!available||ready?'disabled':''}>${label} starten</button><button data-control-stop class="secondary" ${busy||!current||(s.state==='stopped'&&active==='llm')?'disabled':''}>Stoppen</button></div>${switching||loading||pending||s.state==='stopping'?`<div class="video-activity" role="status" aria-live="polite"><strong>${e(switching?v.switch_phase:loading?s.worker.phase||'Modell wird geladen':s.state==='stopping'?'Laufende Aufträge werden beendet':'Start wird angefordert')}</strong><progress aria-label="Fortschritt"></progress></div>`:''}${reason?`<p class="warning">${e(reason)} <a class="link" href="#${selected==='video'?'video':selected==='music'?'music':selected==='separator'?'separation':'chat'}">Einrichten →</a></p>`:''}${(current&&(v.error||s.error||selected==='llm'&&s.worker.error))?`<p class="video-error" role="alert">${e(v.error||s.error||s.worker.error)}</p>`:''}<div class="control-api"><h3>API-Adresse</h3>${routes.map(([name,url])=>`<div><span>${name}</span><code>${e(url)}</code></div>`).join('')}<p class="small">${ready?'Erreichbar':'Erreichbar, sobald dieser Betrieb bereit ist'} · Port ${selected==='separator'?8108:s.port}</p></div>${selected==='separator'?`<p class="small">${v.separator?.models?.filter(m=>m.installed).length||0} Modellpakete bereit · Gewichte laden beim Trennauftrag und werden anschließend entladen.</p><a class="link" href="${e(DeckLinks.url(8128,'/'))}" target="_blank" rel="noopener">Audio-Trennoberfläche öffnen →</a><p><a class="link" href="#separation">Modelle und Aufträge →</a></p>`:selected==='llm'?`<p class="small">${s.counts.llm.available} Chatprofile freigegeben · ${e(s.worker.profile_name||'Gewichte laden bei der ersten Anfrage')}</p>`:selected==='music'?`<p class="small">${e(v.music_profiles?.map(p=>p.name).join(', ')||'Kein Musikprofil freigegeben')} · Gewichte laden bei Anfrage</p>${available?`<a class="link" href="${e(DeckLinks.url(8127,'/'))}" target="_blank" rel="noopener">LadyPoly YuE2 öffnen →</a>${!ready?'<p class="small">Vor dem Generieren Musik starten.</p>':''}`:''}`:`<p class="small">${e(v.service?.name||'Kein Videomodell ausgewählt')} · ${v.runtime_kind==='original'?'LTX Original':'ComfyUI'} · Gewichte laden bei Generierung</p>${ready?`<a class="link" href="${e(v.runtime_kind==='original'?DeckLinks.url(8118,'/'):address+'/')}" target="_blank" rel="noopener">${v.runtime_kind==='original'?'LTX DeskWEB':'ComfyUI'} öffnen →</a>`:''}`}<details class="control-details"><summary>Weitere Dienste & Einstellungen</summary><p>Im LLM-Betrieb stehen auch freigegebene Bild-, TTS- und STT-Profile bereit.</p><p><a class="link" href="#endpoint">Endpunkt und Port einstellen →</a></p><p>${s.scheduler.active_requests} laufende · ${s.scheduler.waiting_requests} wartende Aufträge</p></details></section>`);
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';
+14
View File
@@ -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};
})();