48f3336956
gates / gates (push) Successful in 23s
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
196 lines
9.4 KiB
HTML
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}}
|