Files
felhom-controller/controller/internal/web/templates/recovery.html
T
admin 8fb2f9ef9d
gates / gates (push) Successful in 23s
v0.255.0 — the globe on the sign-in-flow pages: styled, and inside the card
Two defects in v0.254.0's globe, both plain on a browser and neither catchable by anything
that existed — every test read the MARKUP, and the fault was in which CSS file the browser
fetched.

The shells requested /static/style.css with NO ?v=, while layout.html has carried one since
v0.166.0. A browser holding a copy from before v0.254.0 kept serving CSS with no .lang-globe
rules, so the globe came out as a bare unstyled <details> — a stray triangle and two plain
words at the edge of the window. It was FIVE shells, not the three named: both guest share
pages have the same fault for any CSS change, and their visitor is the likeliest of all to be
holding an old copy. And .Version was missing from three of those five data maps, which is
exactly how the next one would be forgotten — it is now filled at the one choke point every
shell renders through.

The globe also floated outside the card, pinned to the corner of the VIEWPORT, reading as part
of the browser rather than the page. It now sits inside the card, centred under the footer, with
the menu opening upward via the shared rule — so the dashboard and the shells cannot drift.

AND A THIRD, caught by a test that already existed: putting the version on the guest share pages
would have printed the controller build onto a page a stranger with a capability URL can open.
TestShareGuest_HeadersTilesNoAdminChrome refused it. Those two now take an opaque per-build tag
— same cache-busting, no disclosure. The fill is ONE function shared with the parity harness,
because a fixture rendered through a different data path is a picture of a page nobody serves,
which the previous release got wrong twice.

15 shell fixtures re-captured; 91 identical, every dashboard page among them.

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 15:07:49 +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?v={{.Version}}">
</head>
<body class="login-body">
<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 class="shell-lang">{{template "lang_globe" .}}</div>
</div>
</body>
</html>
{{end}}