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:
@@ -1,5 +1,34 @@
|
|||||||
## Changelog
|
## 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)
|
### 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
|
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.",
|
"Ez a kód többé nem jeleníthető meg.",
|
||||||
"nem ezen a szerveren",
|
"nem ezen a szerveren",
|
||||||
"biztonsági okból újra nem kérhető le",
|
"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) {
|
if !strings.Contains(html, want) {
|
||||||
t.Errorf("wizard missing %q", 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") {
|
if strings.Contains(html, "érvényét veszti") {
|
||||||
t.Error("fresh wizard must not show the re-ceremony supersede warning")
|
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) {
|
t.Run("wizard re-ceremony variant", func(t *testing.T) {
|
||||||
d := base()
|
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") {
|
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")
|
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") {
|
if strings.Contains(html, "Letét megerősítése") {
|
||||||
t.Error("the deprecated manual-confirm button must be GONE from the card")
|
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 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>
|
<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>
|
<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>
|
<h3 style="margin-top:1.25rem">6. Ellenőrzés</h3>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label id="verify-label" for="verify-w1">Megerősítés: írja be a kód szavait</label>
|
<label id="verify-label" for="verify-w1">Megerősítés: írja be a kód szavait</label>
|
||||||
<div style="display:flex;gap:.5rem">
|
<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-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">
|
<input type="text" id="verify-w2" class="form-control" autocomplete="off" style="max-width:180px" onfocus="armCodeBlur()">
|
||||||
</div>
|
</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 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>
|
</div>
|
||||||
@@ -112,6 +113,27 @@ var PF_LABELS = {
|
|||||||
sudo_grant: 'Rendszerjogosultság engedélyezve'
|
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(){
|
async function loadPreflight(){
|
||||||
try{
|
try{
|
||||||
var r = await fetch('/api/escrow/preflight'); var j = await r.json();
|
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)">✓</span>'
|
var mark = it.ok ? '<span style="color:var(--blue-bright)">✓</span>'
|
||||||
: (informational ? '<span style="color:var(--text-3)">•</span>'
|
: (informational ? '<span style="color:var(--text-3)">•</span>'
|
||||||
: '<span style="color:var(--crit)">✗</span>');
|
: '<span style="color:var(--crit)">✗</span>');
|
||||||
html+='<li style="padding:.35rem 0;border-bottom:1px solid var(--line-soft)">'+mark+' '
|
var det = pfDetail(it);
|
||||||
+esc(PF_LABELS[it.id]||it.id)
|
var line = mark+' '+esc(PF_LABELS[it.id]||it.id);
|
||||||
+(it.detail?(' <span class="form-hint">— '+esc(it.detail)+'</span>'):'')+'</li>';
|
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){
|
if(!d.agent_supported){
|
||||||
html+='<li style="padding:.35rem 0"><span style="color:var(--crit)">✗</span> A funkcióhoz az ügynök frissítése szükséges — a frissítés automatikusan megérkezik.</li>';
|
html+='<li style="padding:.35rem 0"><span style="color:var(--crit)">✗</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(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; }
|
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;
|
claimedR=j.data.recovery_code;
|
||||||
document.getElementById('code-block').textContent=claimedR; // textContent — never innerHTML
|
// Typed-back positions FIRST (v0.127.1: the rendering below highlights them): two random
|
||||||
document.getElementById('reveal-gate').style.display='none';
|
// distinct 1-based word positions of the dash-separated code.
|
||||||
document.getElementById('reveal-shown').style.display='block';
|
var words=claimedR.split('-');
|
||||||
// Typed-back: two random distinct 1-based word positions of the dash-separated code.
|
var n=words.length;
|
||||||
var n=claimedR.split('-').length;
|
|
||||||
var a=1+Math.floor(Math.random()*n), b=1+Math.floor(Math.random()*n);
|
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); }
|
while(b===a){ b=1+Math.floor(Math.random()*n); }
|
||||||
verifyIdx=[Math.min(a,b),Math.max(a,b)];
|
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';
|
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; }
|
}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(){
|
function verifyWords(){
|
||||||
var words=claimedR.split('-');
|
var words=claimedR.split('-');
|
||||||
var w1=(document.getElementById('verify-w1').value||'').trim().toLowerCase();
|
var w1=(document.getElementById('verify-w1').value||'').trim().toLowerCase();
|
||||||
@@ -224,7 +281,8 @@ function verifyWords(){
|
|||||||
}
|
}
|
||||||
|
|
||||||
function finishWizard(){
|
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='';
|
claimedR='';
|
||||||
document.getElementById('code-block').textContent='';
|
document.getElementById('code-block').textContent='';
|
||||||
window.location='/backups/remote';
|
window.location='/backups/remote';
|
||||||
|
|||||||
@@ -77,7 +77,12 @@
|
|||||||
{{end}}
|
{{end}}
|
||||||
</div>
|
</div>
|
||||||
{{else if .OffboxConfigured}}
|
{{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}}
|
{{end}}
|
||||||
{{if .OffboxConfigured}}
|
{{if .OffboxConfigured}}
|
||||||
<div class="schedule-actions" style="margin-top:1rem">
|
<div class="schedule-actions" style="margin-top:1rem">
|
||||||
|
|||||||
Reference in New Issue
Block a user