Files

18 lines
9.4 KiB
JavaScript

window.BackupUI=(()=>{
const esc=v=>String(v??'').replace(/[&<>"']/g,c=>({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'}[c]));
const size=v=>(v/1024**3).toLocaleString('de-DE',{maximumFractionDigits:2})+' GiB';let pending=false,preview=null,jobId=null;
async function request(action,body){const r=await fetch('/api/v1/backup/'+action,{method:'POST',headers:{'Content-Type':'application/json','X-Athena-Deck':'1'},body:JSON.stringify(body)});if(!r.ok){const d=await r.json();throw Error(d.error||'Backup-Anfrage fehlgeschlagen.')}return r;}
function render(){const view=document.querySelector('#view');if(view.querySelector('#backup-page'))return;preview=null;
view.innerHTML=`<div id="backup-page"><div class="kicker">EINSTELLUNGEN / WIEDERHERSTELLUNG</div><h1>Backup &amp; Restore</h1><p>Einstellungen, Profile, Modellquellen, Laufzeitrezepte und zugeordnete Zusatzdienste. Gewichte und Container-Images werden beim Restore erneut geladen; Chats, Medien und Logs sind nicht enthalten.</p><section class="card"><h2>Verschlüsseltes Backup exportieren</h2><p>Enthält Zugangsdaten und gegebenenfalls WireGuard-Schlüssel. Bewahre Datei und Backup-Kennwort sicher auf. Das Kennwort wird nicht gespeichert.</p><form id="backup-export"><label>Backup-Kennwort (mindestens 16 Zeichen)<input type="password" id="backup-export-password" minlength="16" maxlength="256" autocomplete="new-password" required></label><label>Kennwort wiederholen<input type="password" id="backup-export-repeat" minlength="16" maxlength="256" autocomplete="new-password" required></label><label><input type="checkbox" id="backup-history" checked> Dashboard-History mit sichern</label><p><button type="submit">Backup herunterladen</button></p></form></section><section class="card"><h2>Backup importieren</h2><p>Erst prüfen, dann wiederherstellen. Der Restore beendet Deck-eigene Aufträge. Vorhandene Fremddienste und Container werden nicht ersetzt. Währenddessen bleiben Fortschritt und Abbruch verfügbar.</p><form id="backup-import"><label>Backup-Datei<input type="file" id="backup-file" accept=".adbackup" required></label><label>Backup-Kennwort<input type="password" id="backup-import-password" minlength="16" maxlength="256" autocomplete="off" required></label><p><button type="submit">Backup prüfen</button></p></form><div id="backup-preview"></div></section><section class="card" id="backup-progress" aria-live="polite"></section><section class="card" id="backup-checkpoints"></section><p id="backup-message" role="status"></p></div>`;
const message=view.querySelector('#backup-message'),panel=view.querySelector('#backup-preview');
view.querySelector('#backup-export').onsubmit=async e=>{e.preventDefault();if(pending)return;const p=view.querySelector('#backup-export-password').value;if(p!==view.querySelector('#backup-export-repeat').value){message.textContent='Backup-Kennwörter stimmen nicht überein.';return;}pending=true;message.textContent='Konfigurationen werden gesammelt und verschlüsselt …';try{const r=await request('export',{password:p,theme:document.documentElement.dataset.theme||'deck',history:view.querySelector('#backup-history').checked});const blob=await r.blob(),url=URL.createObjectURL(blob),a=document.createElement('a');a.href=url;a.download=(r.headers.get('Content-Disposition')||'').match(/filename="([^"]+)"/)?.[1]||'athena-deck.adbackup';a.click();setTimeout(()=>URL.revokeObjectURL(url),1000);message.textContent='Verschlüsseltes Backup heruntergeladen.';view.querySelector('#backup-export-password').value='';view.querySelector('#backup-export-repeat').value='';}catch(e){message.textContent=e.message;}finally{pending=false;}};
view.querySelector('#backup-import').onsubmit=async e=>{e.preventDefault();if(pending)return;pending=true;message.textContent='Backup entschlüsseln und Quellen prüfen …';try{const file=view.querySelector('#backup-file').files[0];if(!file||file.size>64*1024**2)throw Error('Backup-Datei fehlt oder ist größer als 64 MiB.');const r=await fetch('/api/v1/backup/inspect',{method:'POST',headers:{'Content-Type':'application/octet-stream','X-Athena-Deck':'1','X-Backup-Passphrase':btoa(String.fromCharCode(...new TextEncoder().encode(view.querySelector('#backup-import-password').value)))},body:file});const data=await r.json();if(!r.ok)throw Error(data.error);preview=data;view.querySelector('#backup-import-password').value='';showPreview(panel,data,message);message.textContent='Backup geprüft. Details und Zusatzdienste auswählen.';}catch(e){message.textContent=e.message;}finally{pending=false;}};
poll(view);
}
function showPreview(panel,p,message){panel.innerHTML=`<h3>Wiederherstellungsplan</h3><p>${p.profiles.length} Profile · ${p.models.length} Modelldateien · ${size(p.download_bytes)} fehlende Gewichte · ${p.wireguard?'WireGuard enthalten':'Kein Deck-WireGuard'}</p><details><summary>Modelle und Profile anzeigen</summary><ul>${p.models.map(m=>`<li>${esc(m.name)} · ${size(m.bytes)} · ${m.present?'vorhanden, wird geprüft':'wird heruntergeladen'}</li>`).join('')}</ul><p>${p.profiles.map(esc).join(' · ')}</p></details><h3>Zusatzdienste</h3>${p.services.length?p.services.map(c=>`<label><input type="checkbox" class="backup-service" value="${esc(c.name)}" ${c.restorable?'checked':'disabled'}> ${esc(c.name)} · ${esc(c.registry||c.image)} ${c.build_recipe?'· wird aus festem Rezept gebaut':''}${!c.restorable?'· keine Wiederherstellungsquelle':''}</label>`).join(''):'<p>Keine zugeordneten Container.</p>'}<h3>Laufzeiten</h3><p>llama.cpp: ${esc(p.runtimes.llama?.commit||'nicht eingerichtet')} · ComfyUI: ${p.runtimes.image?'vorhandene wiederverwenden / fehlende installieren':'nicht benötigt'} · TTS: ${p.runtimes.tts?'wiederherstellen':'nicht benötigt'} · LTX Original: ${p.runtimes.ltx_original?'wiederherstellen':'nicht benötigt'}</p>${p.warnings.map(w=>`<p class="note">${esc(w)}</p>`).join('')}${p.blockers.map(w=>`<p role="alert">${esc(w)}</p>`).join('')}<label><input type="checkbox" id="backup-credentials" checked> Oberflächenkennwort und API-Zugang aus Backup wiederherstellen</label><p class="small">Danach gilt das bisherige Kennwort des Backups. Der aktuelle Zugang bleibt erhalten, wenn der Haken entfernt wird.</p><label><input type="checkbox" id="backup-confirm"> Einstellungen ersetzen und ausgewählte Downloads/Builds automatisch ausführen. Lokaler Rückfallstand wird vorher gesichert.</label><p><button id="backup-restore" ${p.blockers.length?'disabled':''}>Wiederherstellung starten</button></p>`;
panel.querySelector('#backup-restore').onclick=async()=>{if(pending)return;if(!panel.querySelector('#backup-confirm').checked){message.textContent='Bitte den angezeigten Wiederherstellungsplan bestätigen.';return;}pending=true;try{await request('restore',{id:p.id,services:[...panel.querySelectorAll('.backup-service:checked')].map(x=>x.value),confirm:true,restore_credentials:panel.querySelector('#backup-credentials').checked});panel.innerHTML='';preview=null;message.textContent='Wiederherstellung gestartet.';}catch(e){message.textContent=e.message;}finally{pending=false;}};
}
async function poll(view){if(!view.querySelector('#backup-page'))return;const panel=view.querySelector('#backup-progress');try{const r=await fetch('/api/v1/backup');if(!r.ok)throw Error('Restore-Status nicht erreichbar.');const {job,preview:p,checkpoints=[]}=await r.json();const saved=view.querySelector('#backup-checkpoints');saved.innerHTML='<h2>Lokale Rückfallstände</h2>'+checkpoints.map(name=>`<p><button data-checkpoint="${esc(name)}">${esc(name)} prüfen</button></p>`).join('');saved.querySelectorAll('[data-checkpoint]').forEach(b=>b.onclick=async()=>{try{const r=await request('checkpoint',{name:b.dataset.checkpoint});const p=await r.json();preview=p;showPreview(view.querySelector('#backup-preview'),p,view.querySelector('#backup-message'));}catch(e){view.querySelector('#backup-message').textContent=e.message;}});if(p&&!preview){preview=p;showPreview(view.querySelector('#backup-preview'),p,view.querySelector('#backup-message'));}if(!job){panel.innerHTML='<h2>Wiederherstellung</h2><p>Noch kein Restore gestartet.</p>';}else{const running=job.state==='running';panel.innerHTML=`<h2>${job.state==='complete'?'Alles wiederhergestellt':job.state==='partial'?'Wiederherstellung mit offenen Punkten':'Wiederherstellung · '+esc(job.state)}</h2><p>${esc(job.phase)}</p><progress max="${job.total}" value="${job.completed}"></progress><p>${job.completed}/${job.total} Schritte</p>${job.current?.total?`<progress max="${job.current.total}" value="${job.current.bytes||0}"></progress><p>${size(job.current.bytes||0)} / ${size(job.current.total)}${job.current.bytes_per_second?' · '+(job.current.bytes_per_second/1024**2).toFixed(1)+' MiB/s':''}${job.current.eta_seconds!=null?' · noch etwa '+Math.ceil(job.current.eta_seconds/60)+' Minuten':''}</p>`:''}${running?'<button id="backup-cancel">Wiederherstellung abbrechen</button>':''}<ul>${job.items.map(x=>`<li><strong>${esc(x.name)}</strong> · ${esc(x.state)}<br>${esc(x.message)}</li>`).join('')}</ul>`;if(running)panel.querySelector('#backup-cancel').onclick=async()=>{try{await request('cancel',{});view.querySelector('#backup-message').textContent='Abbruch angefordert; laufender Container-Schritt wird sicher beendet.';}catch(e){view.querySelector('#backup-message').textContent=e.message;}};if(job.state==='complete'&&jobId!==job.id){jobId=job.id;applyDeckTheme(job.theme);}}}catch(e){view.querySelector('#backup-message').textContent=e.message;}setTimeout(()=>poll(view),1500);}
return {render};
})();