The escrow wizard sends the typed-back words to the box; the new package reaches the hub only when they match (R-1151 controller half, unreleased; works with every agent)
gates / gates (push) Successful in 47s

This commit is contained in:
2026-10-19 03:19:02 +02:00
parent 8ae9178138
commit ba4a6f060b
12 changed files with 434 additions and 35 deletions
@@ -98,6 +98,7 @@
<div id="verify-error" class="alert alert-error" style="display:none;margin-top:.5rem">{{T "backups_escrow.a_megadott_szavak_nem_egyeznek"}}</div>
</div>
<button class="btn btn-sm" id="verify-btn" onclick="verifyWords()">{{T "backups_escrow.szavak_ellenorzese"}}</button>
<a href="/backup/escrow" class="btn btn-sm btn-primary" id="verify-restart" style="display:none">{{T "backups_escrow.ujrainditas"}}</a>
<div id="finish-block" style="display:none;margin-top:1rem">
<h3>{{T "backups_escrow.7_befejezes"}}</h3>
{{/* v0.138.0: the "what happens next" note — the wizard finishes to /backups/remote,
@@ -239,6 +240,9 @@ function pollStatus(){
// finishWizard() clears both. It never leaves the page's JS scope.
var claimedR='';
var verifyIdx=[0,0];
// R-1151: an agent ≥ 0.160.0 keeps the new package until the two words come back through /api/escrow/confirm, and
// names the two positions itself; an older agent stored it at the claim, and the page picks the positions.
var confirmRequired=false;
async function claimCode(){
var btn=document.getElementById('reveal-btn'); btn.disabled=true;
@@ -253,9 +257,13 @@ async function claimCode(){
// distinct 1-based word positions of the dash-separated code.
var words=claimedR.split('-');
var n=words.length;
var a=1+Math.floor(Math.random()*n), b=1+Math.floor(Math.random()*n);
while(b===a){ b=1+Math.floor(Math.random()*n); }
verifyIdx=[Math.min(a,b),Math.max(a,b)];
confirmRequired=!!j.data.confirm_required;
if(confirmRequired){ verifyIdx=[j.data.verify_positions[0], j.data.verify_positions[1]]; }
else {
var a=1+Math.floor(Math.random()*n), b=1+Math.floor(Math.random()*n);
while(b===a){ b=1+Math.floor(Math.random()*n); }
verifyIdx=[Math.min(a,b),Math.max(a,b)];
}
// Span-per-word DOM build — createElement + textContent + createTextNode ONLY. R never flows
// through innerHTML/insertAdjacentHTML: with no HTML parse context there is no injection
// surface, the same guarantee the old single textContent assignment gave. The two words the
@@ -288,13 +296,31 @@ function setCodeBlur(on){
}
function toggleCodeBlur(){ setCodeBlur(!codeBlurred); }
function verifyWords(){
async function verifyWords(){
var words=claimedR.split('-');
var w1=(document.getElementById('verify-w1').value||'').trim().toLowerCase();
var w2=(document.getElementById('verify-w2').value||'').trim().toLowerCase();
var ok = w1===words[verifyIdx[0]-1] && w2===words[verifyIdx[1]-1];
document.getElementById('verify-error').style.display=ok?'none':'block';
if(ok){ document.getElementById('finish-block').style.display='block'; }
var errEl=document.getElementById('verify-error');
errEl.textContent='{{T "backups_escrow.a_megadott_szavak_nem_egyeznek"}}';
errEl.style.display=ok?'none':'block';
if(!ok){ return; }
if(confirmRequired){
var btn=document.getElementById('verify-btn'); btn.disabled=true;
try{
var r=await fetch('/api/escrow/confirm',{method:'POST',
headers:Object.assign({'Content-Type':'application/json'},csrfHeaders()),body:JSON.stringify({words:[w1,w2]})});
var j=await r.json();
if(!j.ok){
errEl.textContent=j.error||'{{T "backups_escrow.a_megadott_szavak_nem_egyeznek"}}'; errEl.style.display='block';
if(j.data && j.data.restart){ // the new code is void; the previous one still works — start again
claimedR=''; document.getElementById('code-block').textContent='';
btn.style.display='none'; document.getElementById('verify-restart').style.display='inline-block'; return; }
btn.disabled=false; return;
}
}catch(e){ errEl.textContent='{{T "backups_escrow.hiba_2"}} '+e.message; errEl.style.display='block'; btn.disabled=false; return; }
}
document.getElementById('finish-block').style.display='block';
}
function finishWizard(){