Files
felhom-controller/controller/internal/web/templates/recovery.html
T
admin 48f3336956
gates / gates (push) Successful in 23s
v0.254.0 — the saved notes follow the language, and the switch becomes a globe (R-557 slice 2 release C; SLICE 2 CLOSED)
The notes a background run SAVES — last night's backup line, the last error, the proof
result, the restore outcome — are written in the BOX's language at the moment they are
written. A household that switches sees the previous run's note in the old language until
the next run rewrites it: the operator's §16 option 1, stated rather than hidden.
EndRestoreOp no longer receives a Hungarian literal from anywhere.

The language switch is a globe. Two text links wrapped in the sidebar footer and asked the
reader to recognise "Magyar"/"English" as links; a globe is the one symbol every web user
already reads as "language", so nobody has to read Hungarian to escape Hungarian. It is
<details>/<summary> — a menu with no script, drawn inline because the icon sprite lives
only in layout.html and the visitor pages have their own shell.

Those visitor pages get the same globe, and a visitor's choice stays theirs: a display-only
felhom_lang cookie that langFor reads ONLY when there is no session. A signed-in household
can never inherit a language a previous visitor picked in the same browser. POST /lang is
CSRF-exempt for a narrow reason written at the exemption — its only achievable effect is the
language of the page the victim's own browser shows them — and safeBackPath refuses
//evil.example as well as https://, because "starts with /" alone is not the test. §16 taken:
a successful claim carries the cookie into the household's setting.

TWO PARITY EXCEPTIONS, MEASURED: 106 fixtures compared with a real diff — exactly two change
shapes (the dashboard footer, the globe in the shells) and 5 byte-identical, which are the
three pages that must not change.

I INTRODUCED A DEADLOCK AND THE SUITE CAUGHT IT BY HANGING. UpdateOffboxStatus holds the
settings write lock while running its callback; boxLang() wants the read lock; sync.RWMutex
is not reentrant. On a real box an off-site run would have hung forever HOLDING the settings
lock. Fixed by resolving the language before the callback, and guarded by a test that names
the file and line in a second instead of hanging for 25 minutes.

MinAgent: 0.131.0 (unchanged). No hub release needed.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0159rPz1ZhFKsS53msqPYxtS
2026-09-18 14:19:31 +02:00

196 lines
9.4 KiB
HTML

{{define "recovery"}}
<!DOCTYPE html>
<html lang="{{T "layout.html_lang"}}">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="robots" content="noindex, nofollow">
<title>{{T "recovery.adatok_visszaszerzese_felhom"}}</title>
<link rel="stylesheet" href="/static/style.css">
</head>
<body class="login-body">
<div class="shell-lang">{{template "lang_globe" .}}</div>
<div class="login-card" style="max-width:46rem">
<img src="/static/felhom-logo.svg" alt="Felhom.eu" class="login-logo">
{{if .Unlocked}}
<!-- ── AFTER THE UNLOCK: what is in there. Read-only — nothing was restored. ────────────── -->
<h1 class="login-title">{{T "recovery.a_menteseid"}} <span class="title-accent">{{T "recovery.elerhetok"}}</span></h1>
{{if .Flash}}<div class="alert alert-info">{{.Flash}}</div>{{end}}
{{if .Error}}<div class="alert alert-error">{{.Error}}</div>{{end}}
{{if .InvUnavailable}}
<!-- R-217: the unlock SUCCEEDED but the repository could not be read. It renders NO listing and
claims nothing about the contents — the .Error above already says what is pending. This
branch exists because the previous code passed a zero-value OffsiteInventory here, whose
Empty=false fell through to .InvUntagged and asserted the store had opened with content. -->
{{else if .InvEmpty}}
<div class="alert alert-warning">
{{T "recovery.a_tarolo_megnyilt_de_nincs"}}
</div>
{{else if .InvUntagged}}
<div class="alert alert-warning">
{{T "recovery.a_tarolo_megnyilt_es_van"}}
</div>
{{else}}
<p class="login-subtitle" style="margin-bottom:1rem">
{{T "recovery.ezek_a_te_menteseid_a"}}
</p>
<table class="data-table" style="width:100%;margin-bottom:1rem">
<thead><tr><th>{{T "recovery.alkalmazas"}}</th><th>{{T "recovery.legutobbi_mentes"}}</th><th>{{T "recovery.meret"}}</th></tr></thead>
<tbody>
{{range .InvApps}}
<tr>
<td>{{.App}}</td>
<td>{{fmtTime .LatestAt}}</td>
<td>{{if gt .SizeBytes 0}}{{humanBytes .SizeBytes}}{{else}}—{{end}}</td>
</tr>
{{end}}
</tbody>
</table>
{{end}}
<p class="form-hint">
{{T "recovery.a_visszaallitas_alkalmazasonkent_torteni"}}
</p>
<div class="form-actions">
<a href="/backups/restore" class="btn btn-primary">{{T "recovery.tovabb_a_visszaallitashoz"}}</a>
<a href="/launcher" class="btn btn-outline">{{T "recovery.vissza_a_kezdolapra"}}</a>
</div>
{{else}}
<!-- ── BEFORE ANY CODE: explain, then take the code. ───────────────────────────────────── -->
<h1 class="login-title">{{T "recovery.adatok"}} <span class="title-accent">{{T "recovery.visszaszerzese"}}</span></h1>
<p class="login-subtitle">{{.CustomerName}}</p>
{{if .Flash}}<div class="alert alert-info">{{.Flash}}</div>{{end}}
{{if .Error}}<div class="alert alert-error">{{.Error}}</div>{{end}}
<p>
{{T "recovery.ezt_a_gepet_ujratelepitettek_a"}}{{with .SealedAt}}{{T "recovery.amelyet_zartunk_le"}}{{end}}{{T "recovery.a_csomagot_csak_a_te"}}
</p>
<div class="alert alert-warning">
{{T "recovery.a_helyreallitasi_kodot_senki_nem"}}
</div>
<p>
{{T "recovery.ha_megadod_a_kodot_feloldjuk"}}
</p>
<div id="unlock-gateway-error" class="alert alert-error" style="display:none" role="alert"></div>
<form id="unlock-form" method="POST" action="/recovery/unlock" autocomplete="off">
{{.CSRFField}}
<label for="recovery_code">{{T "recovery.helyreallitasi_kod_tiz_szo"}}</label>
<input type="password" id="recovery_code" name="recovery_code"
autocomplete="off" autocapitalize="off" autocorrect="off" spellcheck="false"
placeholder="{{T "recovery.tiz_szo_szokozokkel_elvalasztva"}}" required>
<div class="form-actions">
<button type="submit" class="btn btn-primary">{{T "recovery.mentesek_feloldasa"}}</button>
<form method="POST" action="/recovery/postpone" style="display:inline">
{{.CSRFField}}
<button type="submit" class="btn btn-outline">{{T "recovery.most_nem"}}</button>
</form>
</div>
</form>
{{/* R-227 — A RESTART MID-UNLOCK MUST NOT SHOW A RAW ENGLISH GATEWAY ERROR.
Measured 2026-08-05 (CAMPAIGN-11 F8): the controller was restarted 0.7 s into an unlock and
the customer got traefik's `Bad Gateway` — a raw upstream error, in English, naming no reason
and saying nothing about whether the key was installed. The state was clean; only the page
was not. It breaches I3 (every refusal names a reason a person can act on, in Hungarian, with
no raw error).
WHICH LAYER ANSWERS: traefik, and its config IS generated by this repo
(internal/infra/templates/traefik*.tmpl). A fully branded proxy error page is therefore
possible here — but traefik v3 serves no static files itself, so it would need a new
always-up container purely to hold an error page, for every 502 on the box. That is out of
proportion to this finding and is scoped in the report rather than built.
What ships instead is the second sanctioned option: the unlock posts via fetch, so a gateway
error or a dropped connection is caught in the page and answered in Hungarian, without
leaving it. PROGRESSIVE ENHANCEMENT — with no JS the plain POST is unchanged, and that path
still shows the proxy's own error. Said plainly rather than implied. */}}
<script>
(function () {
var form = document.getElementById('unlock-form');
var box = document.getElementById('unlock-gateway-error');
if (!form || !box || !window.fetch) { return; }
form.addEventListener('submit', function (ev) {
ev.preventDefault();
box.style.display = 'none';
var btn = form.querySelector('button[type=submit]');
if (btn) { btn.disabled = true; btn.textContent = '{{T "recovery.feloldas_folyamatban"}}'; }
fetch(form.action, {
method: 'POST',
body: new FormData(form),
credentials: 'same-origin',
redirect: 'follow'
}).then(function (resp) {
if (resp.status >= 500) { throw new Error('gateway'); }
return resp.text().then(function (html) {
document.open(); document.write(html); document.close();
});
}).catch(function () {
// A 5xx from the proxy, or no response at all: the machine is very likely restarting.
// NOTHING is claimed about the code — we do not know whether it was used.
if (btn) { btn.disabled = false; btn.textContent = '{{T "recovery.mentesek_feloldasa"}}'; }
box.textContent = '{{T "recovery.a_gep_eppen_ujraindul_ezert"}} '
+ '{{T "recovery.semmi_nem_valtozott_varj_nehany"}} '
+ '{{T "recovery.ugyhogy_tartsd_keznel"}}';
box.style.display = '';
});
});
})();
</script>
<p class="form-hint">
{{T "recovery.a_most_nem_csak_azt"}}
</p>
<!-- ── THE EXCEPTIONAL PATH. Deliberately not an equal third button. ───────────────────── -->
<hr style="margin:1.5rem 0;border:none;border-top:1px solid var(--border,#2a3142)">
{{if .ConfirmSetAside}}
{{/* §7.3 / §2.4 — THE COPY CHANGES WITH THE BEHAVIOUR (v0.206.0, R-241).
It used to promise "félretesszük — nem töröljük". After this change the set-aside history IS
deleted, on a date, together with the sealed package that protects it — which is what lets
the question end instead of returning at every login. A confirmation that still said "we do
not delete" would be the most consequential false sentence on the whole surface. */}}
<div class="alert alert-error">
<p><strong>{{T "recovery.biztosan_nem_kered_vissza_a"}}</strong></p>
<p>{{T "recovery.ha_megerosited"}}</p>
<ul>
<li>{{T "recovery.a_korabbi_menteseket_most_felretesszuk"}}</li>
<li>{{T "recovery.a_nap_alatt_meggondolhatod_magad"}}</li>
<li>{{T "recovery.a_pontos_datumot_a_tavoli"}}</li>
<li>{{T "recovery.a_gep_uj_ures_mentesi"}}</li>
<li>{{T "recovery.a_torles_utan_ez_a"}}</li>
</ul>
<p>{{T "recovery.ha_csak_most_nincs_keznel"}}</p>
</div>
<div class="form-actions">
<form method="POST" action="/backup/offbox/reset">
{{.CSRFField}}
<input type="hidden" name="confirm" value="1">
<button type="submit" class="btn btn-danger">{{T "recovery.igen_felretehetitek_a_korabbi_menteseket"}}</button>
</form>
<a href="/recovery" class="btn btn-outline">{{T "recovery.megsem"}}</a>
</div>
{{else}}
<p class="form-hint">
{{T "recovery.ha_a_helyreallitasi_kodod_veglegesen"}}
{{if .CanSetAside}}
{{T "recovery.nem_kerem_vissza_a_korabbi"}}
{{else}}
{{T "recovery.ez_a_lehetoseg_akkor_valik"}}
{{end}}
</p>
{{end}}
{{end}}
</div>
</body>
</html>
{{end}}