Files
admin de39e47f53 R-241 part 4: the three-state surface, and the copy tells the truth about the date
FULL PAGE ONCE PER ENTRY, NOT ONCE EVER. "Most nem" used to set a flag that
nothing ever cleared, so a box that abandoned its history and was rebuilt
months later - a genuinely NEW situation - would never see the page again. The
offer now carries an EPOCH, advanced on the edge into the offered state, and a
dismissal is recorded against the epoch it was made in. A fresh entry passes
the dismissal by arithmetic, with nothing to clear and nothing that can be
forgotten to clear.

That is NOT the flag the operator's ruling forbids. The forbidden thing
remembers that the customer decided so the screen can be suppressed while the
state stays wrong. This records WHICH SITUATION a dismissal was about.

A REAL BUG, caught by the test and not by review: the first draft returned
early from recoveryInterrupts when the offer was false, so the FALLING edge
was never recorded, RecoveryOfferActive stayed true through a settled period,
and the next entry counted as a continuation. The page never came back - the
exact defect the epoch exists to fix, reintroduced inside the fix. The sync is
now unconditional and the ordering is commented as load-bearing.

THREE LEVERS, THREE SCOPES, and none of them removes the route:
  - clicking the bar away  -> a browser SESSION cookie, cleared on login, so
    the reminder is genuinely back at the next login. Nothing persisted.
  - "ne emlekeztessen ujra" -> durable, epoch-scoped, silences the BANNER ONLY.
    It starts no countdown, abandons nothing, and a fresh entry reminds again.
  - "most nem" -> suppresses the full page only, as before.
The entry point on /backups/remote is bound to the OFFER and to nothing else,
pinned by a test that fires all three dismissals and asserts it survives.

SEC 7.3 / Q7 - THE TRAP DOES NOT SURVIVE THIS SESSION. While a recovery is
outstanding the "Helyrealitasi kod letrehozasa" button is UNAVAILABLE, not
merely captioned: creating a new code seals the current key, demotes the
package that opens the earlier history to retained custody that no shipped
path can read (R-199), and re-enables the recovery screen through the orphan
route while invalidating the code that screen accepts. A warning beside a
button is a warning people click past. The card now explains and points at
/recovery instead.

SEC 2.4 - the abandon confirmation changes with the behaviour. It used to
promise "felretesszuk - nem toroljuk". It now states the grace in days (from
the constant the countdown actually uses, never a literal in prose), that the
sealed package goes with it, that the customer can change their mind, where
the date is visible, and that the question does not come back afterwards.

The countdown is shown on /backups/remote for the WHOLE window - the bar
elsewhere is a nudge, this is the record, and a deletion date must be findable
on a quiet day too.

Tests: once-per-entry across a full settle-and-re-enter cycle; the banner
dismissal proven to be a session cookie (MaxAge 0, no Expires) and to persist
nothing; the opt-out proven to silence the banner while leaving the offer, the
route and the countdown untouched, and to remind again on a fresh entry; the
entry point surviving all three dismissals; a settled box showing nothing; and
the back-redirect refusing "//evil.example".

An existing test (TestRecovery_E) was updated: it asserted the legacy boolean,
which the epoch replaces. It now asserts the dismissal landed on the current
epoch, which is the stronger property.

Green: go build, go vet, go test ./... all pass; controller gates OK.
2026-08-07 12:01:30 +02:00

211 lines
11 KiB
HTML

{{define "recovery"}}
<!DOCTYPE html>
<html lang="hu">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="robots" content="noindex, nofollow">
<title>Adatok visszaszerzése — Felhom</title>
<link rel="stylesheet" href="/static/style.css">
</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">A mentéseid <span class="title-accent">elérhetők</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">
A tároló megnyílt, de <strong>nincs benne egyetlen mentés sem</strong>. Ez azt jelenti, hogy a
kulcs jó volt, de ehhez a géphez nem tartozik korábbi mentés. Ha korábban biztosan készültek
mentések, keresd a Felhom ügyfélszolgálatát, mielőtt bármit tennél.
</div>
{{else if .InvUntagged}}
<div class="alert alert-warning">
A tároló megnyílt, és van benne tartalom, de nem tudtuk alkalmazásokhoz rendelni. A Biztonsági
mentés oldalon nézheted meg részletesen.
</div>
{{else}}
<p class="login-subtitle" style="margin-bottom:1rem">
Ezek a <strong>te</strong> mentéseid, a lent jelzett időpontokból. Nézd át, hogy tényleg azt
találod-e itt, amire számítasz — <strong>semmit nem állítottunk vissza és semmi nem változott.</strong>
</p>
<table class="data-table" style="width:100%;margin-bottom:1rem">
<thead><tr><th>Alkalmazás</th><th>Legutóbbi mentés</th><th>Méret</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">
A visszaállítás alkalmazásonként történik, a <strong>Biztonsági mentés → Visszaállítás</strong>
oldalon. Ott választhatod ki, melyik alkalmazás mit hozzon vissza.
</p>
<div class="form-actions">
<a href="/backups/restore" class="btn btn-primary">Tovább a visszaállításhoz</a>
<a href="/launcher" class="btn btn-outline">Vissza a kezdőlapra</a>
</div>
{{else}}
<!-- ── BEFORE ANY CODE: explain, then take the code. ───────────────────────────────────── -->
<h1 class="login-title">Adatok <span class="title-accent">visszaszerzése</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>
Ezt a gépet újratelepítették. A korábbi, <strong>házon kívüli mentéseid megvannak</strong> — a
Felhom központi rendszere őriz hozzájuk egy lezárt csomagot{{with .SealedAt}}, amelyet
<strong>{{.}}</strong> zártunk le{{end}}. A csomagot csak a <strong>te helyreállítási
kódoddal</strong> lehet kinyitni.
</p>
<div class="alert alert-warning">
<strong>A helyreállítási kódot senki nem tudja pótolni</strong> — sem a Felhom, sem az
ügyfélszolgálat, sem az üzemeltető. Ez szándékos: így a mentéseidet rajtad kívül senki nem
tudja megnyitni. Ha a kód elveszett, a korábbi mentések nem nyithatók meg többé.
</div>
<p>
Ha megadod a kódot, <strong>feloldjuk a mentéseid zárolását és megmutatjuk, mi van bennük</strong>
— melyik alkalmazás, mikorról, mekkora. <strong>Ebben a lépésben semmit nem állítunk vissza és
semmi nem változik.</strong> A visszaállítást utána, alkalmazásonként külön választhatod.
</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">Helyreállítási kód (tíz szó)</label>
<input type="password" id="recovery_code" name="recovery_code"
autocomplete="off" autocapitalize="off" autocorrect="off" spellcheck="false"
placeholder="tíz szó, szóközökkel elválasztva" required>
<div class="form-actions">
<button type="submit" class="btn btn-primary">Mentések feloldása</button>
<form method="POST" action="/recovery/postpone" style="display:inline">
{{.CSRFField}}
<button type="submit" class="btn btn-outline">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 = 'Feloldás 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 = 'Mentések feloldása'; }
box.textContent = 'A gép éppen újraindul, ezért most nem tudtuk befejezni a műveletet. '
+ 'Semmi nem változott. Várj néhány másodpercet, és próbáld újra — a kódodra továbbra is szükséged lesz, '
+ 'úgyhogy tartsd kéznél.';
box.style.display = '';
});
});
})();
</script>
<p class="form-hint">
A „Most nem” csak azt jelenti, hogy nem zavarunk vele többet a kezdőlapon. A mentéseid ettől
megmaradnak, és ez az oldal a <strong>Biztonsági mentés → Távoli mentés</strong> oldalról
bármikor újra elérhető.
</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>Biztosan nem kéred vissza a korábbi mentéseket?</strong></p>
<p>Ha megerősíted:</p>
<ul>
<li>a korábbi mentéseket <strong>most félretesszük</strong>, és <strong>{{.AbandonGraceDays}} nap múlva véglegesen töröljük</strong> — a lezárt helyreállítási csomaggal együtt;</li>
<li>a {{.AbandonGraceDays}} nap alatt <strong>meggondolhatod magad</strong>: ha előkerül a helyreállítási kódod, a mentéseid visszaszerezhetők, és a törlés elmarad;</li>
<li>a pontos dátumot a <strong>Távoli mentés</strong> oldalon végig látni fogod, és emlékeztetni is fogunk;</li>
<li>a gép <strong>új, üres mentési tárolót kezd</strong>, és mostantól oda ment;</li>
<li>a törlés után <strong>ez a kérdés nem jön vissza többé</strong> — mert nem marad mit visszaszerezni.</li>
</ul>
<p>Ha csak most nincs kéznél a kódod, válaszd inkább a „Most nem” lehetőséget — az semmit nem indít el.</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">Igen, félretehetitek a korábbi mentéseket</button>
</form>
<a href="/recovery" class="btn btn-outline">Mégsem</a>
</div>
{{else}}
<p class="form-hint">
Ha a helyreállítási kódod véglegesen elveszett, és tudomásul veszed, hogy a korábbi mentések
így nem nyithatók meg többé:
{{if .CanSetAside}}
<a href="/recovery?setaside=1">nem kérem vissza a korábbi adatokat</a>.
{{else}}
ez a lehetőség akkor válik elérhetővé, ha a gép már újra kapcsolódott a házon kívüli
tárhelyhez. Addig a mentéseid érintetlenül megmaradnak.
{{end}}
</p>
{{end}}
{{end}}
</div>
</body>
</html>
{{end}}