v0.127.1: escrow wizard polish — escrowed-card CTA as btn-outline, Hungarian preflight detail map (raw agent English only as muted failure diagnostic), typed-back word highlight (span-per-word, textContent-only), blur-on-verify toggle

This commit is contained in:
2026-07-13 20:17:36 +02:00
parent a5041dac80
commit 156e7d5565
4 changed files with 120 additions and 12 deletions
+29
View File
@@ -1,5 +1,34 @@
## Changelog
### v0.127.1 — escrow wizard polish: CTA visibility + Hungarian preflight + typed-back highlight + blur-on-verify (2026-07-13)
Four findings from Viktor's first supervised wizard passes (drill + demo). Presentation-only —
no endpoint, agent call, or state change; §10 red-proofs N/A.
- **Escrowed-card CTA** (`backups_remote.html`): "Új helyreállítási kód készítése" is a real
`btn btn-sm btn-outline` secondary button (was an inline link inside the muted hint — nearly
invisible). Outline, NOT primary: escrowed is healthy, the CTA is available-not-urgent (the
stale variant stays primary). Render-tested.
- **Hungarian preflight details** (`backups_escrow.html` `pfDetail`): the agent's operator-English
`detail` strings no longer leak into the customer UI. OK rows keep only VALUE details (storage
id, age path; `staged_secret` → "előkészítve"); boolean-OK rows (dr_tier/hub_upload/sudo_grant)
render no detail; not-OK rows get the Hungarian explanation + the raw agent detail as a muted
diagnostic span; unknown ids fall back to the raw detail (never blank a failure);
`staged_secret` keeps its informational dot. Render test pins the not-staged copy + asserts the
three English literals never appear in the page source.
- **Typed-back highlight**: the code renders as span-per-word (createElement + textContent +
createTextNode ONLY — R still never flows through innerHTML; no parse context = no injection
surface); the two verification words get `var(--warn)` + 600 weight so they're findable on
paper. `verifyIdx` is now chosen BEFORE rendering. `finishWizard`'s `textContent=''` clears the
spans (child-node replacement — verified).
- **Blur-on-verify** (Part 4, optional — implemented; trivial to strike): first focus on either
verification input blurs the code (`filter: blur(6px)`, inline-style toggle — no style.css
change, no cache-bust) + a `btn-ghost` "Megjelenítés"/"Elrejtés" toggle. The typed-back now
exercises the WRITTEN copy, not screen transcription. R stays in the JS closure.
JS behavior (spans/blur) is review-covered; the visual leg awaits Viktor's next login. All four
UI gates green.
### v0.127.0 — customer-facing escrow ceremony wizard + stale-blob re-check (2026-07-13) — MinAgent: 0.88.0 (wizard only; everything else unchanged)
The missing friend-alpha piece: the recovery-code ceremony moves from operator-SSH to a
@@ -325,6 +325,10 @@ func TestEscrowTemplates_Render(t *testing.T) {
"Ez a kód többé nem jeleníthető meg.",
"nem ezen a szerveren",
"biztonsági okból újra nem kérhető le",
// v0.127.1 polish: the Hungarian preflight detail map + the blur-on-verify toggle.
"nincs előkészített jelszó — a varázsló indításkor automatikusan előkészíti",
"a rendszerjogosultság hiányzik",
"Megjelenítés",
} {
if !strings.Contains(html, want) {
t.Errorf("wizard missing %q", want)
@@ -333,6 +337,13 @@ func TestEscrowTemplates_Render(t *testing.T) {
if strings.Contains(html, "érvényét veszti") {
t.Error("fresh wizard must not show the re-ceremony supersede warning")
}
// v0.127.1: the agent's operator-English detail strings must never be hardcoded into the
// page — they may only arrive at runtime, inside the muted failure-diagnostic span.
for _, raw := range []string{"DR tier applied", "hub upload target configured", "sudo grant listed"} {
if strings.Contains(html, raw) {
t.Errorf("raw English agent detail %q leaked into the wizard page source", raw)
}
}
})
t.Run("wizard re-ceremony variant", func(t *testing.T) {
d := base()
@@ -360,6 +371,11 @@ func TestEscrowTemplates_Render(t *testing.T) {
if !strings.Contains(html, "Új helyreállítási kód készítése") {
t.Error("escrowed card must offer the secondary re-ceremony link")
}
// v0.127.1: the CTA is a real secondary BUTTON (outline — available-not-urgent), no longer
// an inline link buried in the muted hint.
if !strings.Contains(html, `class="btn btn-sm btn-outline">Új helyreállítási kód készítése`) {
t.Error("escrowed-card CTA must render as a btn-outline button")
}
if strings.Contains(html, "Letét megerősítése") {
t.Error("the deprecated manual-confirm button must be GONE from the card")
}
@@ -71,12 +71,13 @@
<div id="reveal-shown" style="display:none">
<div class="alert alert-warning" style="margin-bottom:.5rem">Ez a kód többé nem jeleníthető meg.</div>
<pre class="mono" id="code-block" style="white-space:pre-wrap;word-break:break-all;background:var(--bg-0);border:1px solid var(--line);border-radius:var(--radius);padding:1rem;font-size:1.05rem"></pre>
<button type="button" class="btn btn-ghost btn-sm" id="code-blur-toggle" style="display:none" onclick="toggleCodeBlur()">Megjelenítés</button>
<h3 style="margin-top:1.25rem">6. Ellenőrzés</h3>
<div class="form-group">
<label id="verify-label" for="verify-w1">Megerősítés: írja be a kód szavait</label>
<div style="display:flex;gap:.5rem">
<input type="text" id="verify-w1" class="form-control" autocomplete="off" style="max-width:180px">
<input type="text" id="verify-w2" class="form-control" autocomplete="off" style="max-width:180px">
<input type="text" id="verify-w1" class="form-control" autocomplete="off" style="max-width:180px" onfocus="armCodeBlur()">
<input type="text" id="verify-w2" class="form-control" autocomplete="off" style="max-width:180px" onfocus="armCodeBlur()">
</div>
<div id="verify-error" class="alert alert-error" style="display:none;margin-top:.5rem">A megadott szavak nem egyeznek — ellenőrizze a felírt kódot.</div>
</div>
@@ -112,6 +113,27 @@ var PF_LABELS = {
sudo_grant: 'Rendszerjogosultság engedélyezve'
};
// Hungarian DETAIL mapping (v0.127.1): the agent's `detail` strings are operator-English by
// design — presentation is the controller's job. OK rows keep only VALUE-bearing details
// (storage id / binary path); boolean-OK rows render no detail at all. Not-OK rows get the
// Hungarian explanation; unknown/future ids fall back to the raw detail (never blank a failure).
var PF_FAIL_DETAILS = {
pbs_storage_id: 'nincs beállítva mentési tároló',
age_binary: 'a titkosító eszköz (age) nem található',
dr_tier: 'a DR-szint nincs alkalmazva ezen a gépen',
hub_upload: 'a központi feltöltés nincs beállítva',
sudo_grant: 'a rendszerjogosultság hiányzik — az ügynök frissítése szükséges',
staged_secret: 'nincs előkészített jelszó — a varázsló indításkor automatikusan előkészíti, ha a távoli mentés be van állítva'
};
function pfDetail(it){
if(it.ok){
if(it.id==='pbs_storage_id' || it.id==='age_binary'){ return it.detail||''; } // the VALUE
if(it.id==='staged_secret'){ return 'előkészítve'; }
return ''; // dr_tier / hub_upload / sudo_grant: the check mark + label is complete
}
return PF_FAIL_DETAILS[it.id] || (it.detail||'');
}
async function loadPreflight(){
try{
var r = await fetch('/api/escrow/preflight'); var j = await r.json();
@@ -125,9 +147,13 @@ async function loadPreflight(){
var mark = it.ok ? '<span style="color:var(--blue-bright)">&#10003;</span>'
: (informational ? '<span style="color:var(--text-3)">&#8226;</span>'
: '<span style="color:var(--crit)">&#10007;</span>');
html+='<li style="padding:.35rem 0;border-bottom:1px solid var(--line-soft)">'+mark+' '
+esc(PF_LABELS[it.id]||it.id)
+(it.detail?(' <span class="form-hint">— '+esc(it.detail)+'</span>'):'')+'</li>';
var det = pfDetail(it);
var line = mark+' '+esc(PF_LABELS[it.id]||it.id);
if(det){ line += ' <span class="form-hint">— '+esc(det)+'</span>'; }
// On failure keep the agent's raw detail as a muted diagnostic (English acceptable there) —
// unless the fallback above already IS the raw detail (unknown id).
if(!it.ok && it.detail && det!==it.detail){ line += ' <span class="form-hint" style="color:var(--text-3)">('+esc(it.detail)+')</span>'; }
html+='<li style="padding:.35rem 0;border-bottom:1px solid var(--line-soft)">'+line+'</li>';
});
if(!d.agent_supported){
html+='<li style="padding:.35rem 0"><span style="color:var(--crit)">&#10007;</span> A funkcióhoz az ügynök frissítése szükséges — a frissítés automatikusan megérkezik.</li>';
@@ -202,18 +228,49 @@ async function claimCode(){
if(r.status===410){ document.getElementById('reveal-card').style.display='none'; document.getElementById('void-card').style.display='block'; return; }
if(!j.ok || !j.data || !j.data.recovery_code){ errEl.textContent=j.error||'A kód lekérése nem sikerült.'; errEl.style.display='block'; btn.disabled=false; return; }
claimedR=j.data.recovery_code;
document.getElementById('code-block').textContent=claimedR; // textContent — never innerHTML
document.getElementById('reveal-gate').style.display='none';
document.getElementById('reveal-shown').style.display='block';
// Typed-back: two random distinct 1-based word positions of the dash-separated code.
var n=claimedR.split('-').length;
// Typed-back positions FIRST (v0.127.1: the rendering below highlights them): two random
// 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)];
// 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
// customer must type back get the warn highlight so they can be found on paper too.
var block=document.getElementById('code-block');
block.textContent='';
words.forEach(function(w,i){
if(i>0){ block.appendChild(document.createTextNode('-')); }
var sp=document.createElement('span');
sp.textContent=w;
if(i+1===verifyIdx[0] || i+1===verifyIdx[1]){ sp.style.color='var(--warn)'; sp.style.fontWeight='600'; }
block.appendChild(sp);
});
document.getElementById('reveal-gate').style.display='none';
document.getElementById('reveal-shown').style.display='block';
document.getElementById('verify-label').textContent='Megerősítés: írja be a kód '+verifyIdx[0]+'. és '+verifyIdx[1]+'. szavát';
}catch(e){ errEl.textContent='Hiba: '+e.message; errEl.style.display='block'; btn.disabled=false; }
}
// Blur-on-verify (v0.127.1, Part 4): the typed-back should exercise the WRITTEN copy, not screen
// transcription — the first focus on a verification input blurs the code and offers a local
// show/hide toggle. R stays in the JS closure throughout; the blur is pure presentation.
var codeBlurred=false, blurArmed=false;
function armCodeBlur(){
if(blurArmed){ return; }
blurArmed=true;
setCodeBlur(true);
document.getElementById('code-blur-toggle').style.display='inline-block';
}
function setCodeBlur(on){
codeBlurred=on;
document.getElementById('code-block').style.filter=on?'blur(6px)':'';
document.getElementById('code-blur-toggle').textContent=on?'Megjelenítés':'Elrejtés';
}
function toggleCodeBlur(){ setCodeBlur(!codeBlurred); }
function verifyWords(){
var words=claimedR.split('-');
var w1=(document.getElementById('verify-w1').value||'').trim().toLowerCase();
@@ -224,7 +281,8 @@ function verifyWords(){
}
function finishWizard(){
// Drop every copy of the code this page holds, then leave.
// Drop every copy of the code this page holds, then leave. (Assigning textContent replaces ALL
// child nodes with one empty text node — the per-word spans are removed with it, verified.)
claimedR='';
document.getElementById('code-block').textContent='';
window.location='/backups/remote';
@@ -77,7 +77,12 @@
{{end}}
</div>
{{else if .OffboxConfigured}}
<p class="form-hint" style="margin:.5rem 0">A helyreállítási kód letétbe helyezve.{{if .EscrowAgentOK}} <a href="/backup/escrow">Új helyreállítási kód készítése</a>{{end}}</p>
{{/* v0.127.1: the re-ceremony CTA is a real secondary button (outline, NOT primary — escrowed
is a healthy state; available-not-urgent, unlike the stale variant above). */}}
<div style="margin:.5rem 0">
<p class="form-hint" style="margin:0 0 .4rem">A helyreállítási kód letétbe helyezve.</p>
{{if .EscrowAgentOK}}<a href="/backup/escrow" class="btn btn-sm btn-outline">Új helyreállítási kód készítése</a>{{end}}
</div>
{{end}}
{{if .OffboxConfigured}}
<div class="schedule-actions" style="margin-top:1rem">