diff --git a/pb_migrations/1794000000_czech_mail_templates.js b/pb_migrations/1794000000_czech_mail_templates.js
new file mode 100644
index 0000000..dd98ac7
--- /dev/null
+++ b/pb_migrations/1794000000_czech_mail_templates.js
@@ -0,0 +1,83 @@
+///
+
+// The account emails were in English, and pointed at PocketBase's admin console.
+//
+// **Why it matters here.** Every other surface of this app is Czech, on purpose:
+// the people using it are the owner's parents and his children. The one moment
+// they are most likely to be stuck — locked out, asking for a new password — was
+// the one moment the product started speaking English and sent them to
+// `pb.petruzalekr.cz/_/#/auth/confirm-password-reset/…`, which is the admin UI's
+// own page and reads to anybody's parent like a developer tool opened by
+// mistake.
+//
+// Found by looking at a delivered message rather than at the settings: the
+// templates were simply PocketBase's defaults, and defaults are invisible until
+// somebody receives one.
+//
+// The links now point at amber.petruzalekr.cz, which hosts the confirm form in
+// Czech (see pb_public/index.html, `?reset=`). Verification and email-change
+// keep PocketBase's own confirm pages: nobody has hit those yet, they are not
+// part of the reported problem, and pointing them somewhere that does not exist
+// would be worse than English.
+const RESET_SUBJECT = "Nové heslo k Amberu";
+const RESET_BODY = `
Ahoj,
+
Někdo (snad ty) požádal o nové heslo k Amberu. Nastavíš si ho tímhle odkazem:
Pokud jsi o změnu nežádal, ozvi se Richardovi — někdo se ti hrabe v účtu.
+
Amber
`;
+
+// The defaults, so the down migration restores exactly what was there.
+const EN = {
+ reset: {
+ subject: "Reset your {APP_NAME} password",
+ body: `
Zadej si nové heslo k účtu. Odkaz z e-mailu platí omezenou
+ dobu — když nezabere, požádej o nový.
+
+
+
+
+
+
+
+
+
@@ -1354,10 +1373,52 @@ $("acChange").onclick=async function(){
await loadAddons();
};
+// ── setting a new password from an emailed link ──────────────────────────────
+
+/** The token the reset email put in the address bar, or null. */
+function resetToken(){
+ try { return new URLSearchParams(location.search).get("reset"); }
+ catch(_){ return null; }
+}
+
+function showReset(){
+ $("anon").hidden = true;
+ $("reset").hidden = false;
+ $("rNew").focus();
+}
+
+$("rSave").onclick=async function(){
+ var tok=resetToken();
+ var a=$("rNew").value, b=$("rNew2").value;
+ if(a.length<8){ setMsg($("rMsg"),"Heslo musí mít aspoň 8 znaků.","err"); return; }
+ if(a!==b){ setMsg($("rMsg"),"Hesla se neshodují.","err"); return; }
+ setMsg($("rMsg"),""); busy($("rSave"),true,"Nastavuji…");
+ var r=await api("POST","/api/collections/users/confirm-password-reset",
+ { token:tok, password:a, passwordConfirm:b }, {anon:true});
+ busy($("rSave"),false);
+ if(!r.ok){
+ // The overwhelmingly likely cause, and the one worth naming: these links
+ // expire, and a person who tried yesterday's email has no way to know that
+ // from "something went wrong".
+ setMsg($("rMsg"),"Nepovedlo se. Odkaz nejspíš vypršel — požádej o nový.","err");
+ return;
+ }
+ $("rNew").value=$("rNew2").value="";
+ setMsg($("rMsg"),"Hotovo. Přihlas se novým heslem.","ok");
+ // Drop the token from the address bar: it is spent, and a URL with a
+ // credential in it is one that ends up in a bookmark or a shared screenshot.
+ try { history.replaceState(null,"",location.pathname); } catch(_){}
+ setTimeout(function(){ $("reset").hidden=true; $("anon").hidden=false; }, 2500);
+};
+
// ── boot ─────────────────────────────────────────────────────────────────────
(async function(){
fillLangs();
$("tvUrl").textContent=location.host;
+ // A reset link wins over a stored session: somebody arriving with one is
+ // trying to fix their account, and dropping them into a signed-in page they
+ // did not ask for hides the very thing they came to do.
+ if (resetToken()) { showReset(); return; }
if (await restore()) await enter();
})();