diff --git a/docs/onboarding-contract.md b/docs/onboarding-contract.md index 32e6640..65888c8 100644 --- a/docs/onboarding-contract.md +++ b/docs/onboarding-contract.md @@ -232,10 +232,32 @@ only on an instance that never had a template. So adding a friends row cannot ma the editor wander onto it, and cannot make account creation preload the wrong group. -**The one-click preload on account creation is family-only.** Other groups are -maintained in the PocketBase admin UI and their members type their own code on the -account page. A group picker in the dashboard is a further piece of work, listed as -not done rather than half-built. +### The dashboard edits any group (added same day) + +A `skupina` dropdown above the fields, listing every row plus **+ nová skupina**. +The selection lives in `tplSel` rather than being read off the ` ' + "povolit 18+ (jinak dostane clean build)" + - '" + + '
sdílené nastavení
' + + '" + + '

Zdroje i TMDB klíč nastavím hned a nemusí zadávat kód. ' + + "Kdo si nese vlastní účty, dostane „žádné“.

" + '
věkový strop
because `load()` re-renders the section from scratch + and the choice has to survive that. */ +var tplSel = null; + +function viewTemplate(list){ + var rows = list || []; + var sel = tplSel ? rows.filter(function(r){ return r.id === tplSel; })[0] : null; + if (!sel && tplSel !== "new") { + // Default to the family row, so opening the page lands where it always did. + sel = rows.filter(function(r){ return r.isDefault; })[0] || rows[0] || null; + tplSel = sel ? sel.id : "new"; + } + var v = sel || {}; + var opts = rows.map(function(r){ + return '"; + }).join("") + '"; + return '

Sdílené nastavení — co dostane nový člen

' + '
' + + '
skupina
' + + '
" + + '
název skupiny
' + + '
kód téhle skupiny
' + '
TorBox doplněk (addonUrl)
' + '
' + @@ -679,6 +703,10 @@ function viewTemplate(tpl){ 'autocomplete="off" value="' + esc(v.tmdbKey || "") + '">
' + '
poznámka (jen pro tebe)
' + + '" + '" + @@ -688,42 +716,82 @@ function viewTemplate(tpl){ "
" + '

V těchhle adresách jsou přihlašovací údaje k prehraj.to a ' + "klíč k TorBoxu — proto jsou schované a proto je tahle stránka jen pro tebe. " + - "Nový člen si je stáhne při prvním nastavení pomocí rodinného kódu.

" + - '

Tohle je nastavení rodiny. Další skupiny (třeba přátelé) mají svoje vlastní řádky s vlastními údaji a vlastním kódem — ty se spravují v PocketBase adminu a jejich lidi si kód zadají sami na svém účtu.

' + - (tpl ? "" : '
Žádné nastavení ještě není uložené — ' + - "první uložení ho vytvoří.
") + ""; + "Kdo dostane kód téhle skupiny, stáhne si přesně tyhle zdroje.

" + + '

Každá skupina má vlastní údaje a vlastní kód, takže přátelé ' + + "nesdílí účty s rodinou. Kód musí být unikátní; dvě skupiny se stejným kódem " + + "databáze odmítne.

" + + (rows.length ? "" : '
Žádná skupina ještě není uložená — ' + + "první uložení ji vytvoří.
") + ""; } async function saveTemplate(){ var out = el("tOut"); + var name = el("tName").value.trim(); + if (!name) { + out.innerHTML = '
Skupina musí mít název — jinak ji ' + + "v seznamu nerozeznáš od ostatních.
"; + return; + } out.innerHTML = '

ukládám…

'; var body = { + name: name, + code: el("tCode").value.trim(), + isDefault: el("tDefault").checked, addonUrl: el("tAddon").value.trim(), czechAddonUrl: el("tCzech").value.trim(), tmdbKey: el("tTmdb").value.trim(), note: el("tNote").value.trim(), }; try { - // The family row specifically. Several rows exist now (one per group), so - // "whichever was updated last" would let this editor wander onto the friends' - // credentials -- see familyTemplate(). A first-ever save creates the family row - // and flags it, so both this page and the route's legacy-code fallback agree on - // which row that is. - var rec = await familyTemplate(); - if (!rec) { body.isDefault = true; body.name = "family"; } + var creating = (!tplSel || tplSel === "new"); var r = await fetch("/api/collections/onboarding_template/records" + - (rec ? "/" + rec.id : ""), { - method: rec ? "PATCH" : "POST", + (creating ? "" : "/" + tplSel), { + method: creating ? "POST" : "PATCH", headers: { "Content-Type": "application/json", Authorization: tok }, body: JSON.stringify(body), }); if (!r.ok) { var d = await r.json(); - throw new Error((d && d.message) || ("HTTP " + r.status)); + // Per-field, because the one that actually happens here is a duplicate code + // hitting the unique index, and a bare "400" would not say so. + var why = d && d.data && Object.keys(d.data).length + ? Object.keys(d.data).map(function(k){ + return k + ": " + (d.data[k].message || "?"); }).join("; ") + : (d && d.message) || ("HTTP " + r.status); + throw new Error(why); } - out.innerHTML = '
Uloženo. Nový člen to dostane při ' + - "prvním nastavení; už nastavené účty se tím nemění.
"; - load(); + var saved = await r.json(); + tplSel = saved.id; + + // **Exactly one default.** The route's fallback for the old AMBER_INVITE_CODE + // takes "the" isDefault row, so two of them would + // put us straight back to the bug this whole change fixed: which credentials the + // old code hands out would depend on edit order. Ticking the box therefore + // unticks it everywhere else rather than being quietly ignored. + var moved = ""; + if (body.isDefault) { + var all = await allTemplates(); + for (var i = 0; i < all.length; i++) { + if (all[i].id !== saved.id && all[i].isDefault) { + await fetch("/api/collections/onboarding_template/records/" + all[i].id, { + method: "PATCH", + headers: { "Content-Type": "application/json", Authorization: tok }, + body: JSON.stringify({ isDefault: false }), + }); + moved = " Výchozí skupina je teď „" + esc(name) + "“, ne „" + + esc(all[i].name || "(bez názvu)") + "“."; + } + } + } + // `load()` replaces the whole section, so writing the message first and calling + // it after meant the confirmation was destroyed a moment later -- it has always + // flashed and vanished here. That is worse now than it was: `moved` reports that + // the default group changed, which is exactly the sentence somebody needs to + // read. So re-render first, then put the message into the fresh element. + var msg = '
Uloženo. Nový člen to dostane při prvním ' + + "nastavení; už nastavené účty se tím nemění." + moved + "
"; + await load(); + if (el("tOut")) el("tOut").innerHTML = msg; } catch (e) { out.innerHTML = '
nepodařilo se: ' + esc(e.message) + "
"; } @@ -780,9 +848,13 @@ function newSaltB64(){ return b64e(crypto.getRandomValues(new Uint8Array(16))); If they later change their password the blob stops opening and the app flips `addonConfigNeedsReentry`, which is the documented, already-handled path. */ -async function preloadFamilyConfig(userId, name, password){ - var tpl = await familyTemplate(); - if (!tpl) throw new Error("rodinné nastavení není uložené"); +async function preloadSharedConfig(userId, name, password, tplId){ + // By id, because the dashboard now asks WHICH group. Reading "the" template here + // is what made this preload family-only. + var tpl = null; + try { tpl = await api("/api/collections/onboarding_template/records/" + tplId); } + catch (_) { tpl = null; } + if (!tpl) throw new Error("sdílené nastavení téhle skupiny se nepodařilo načíst"); var pr = await fetch("/api/collections/profiles/records", { method: "POST", @@ -849,17 +921,18 @@ async function createAccount(){ throw new Error(why); } var extra = ""; - if (el("nFamily").checked) { + var gid = el("nGroup") ? el("nGroup").value : ""; + if (gid) { // The account EXISTS at this point. If pre-loading fails, say so instead // of reporting a clean success or rolling the account back — a half-made // account you were not told about is the worst of the three. try { - await preloadFamilyConfig(d.id, name, pass); - extra = " Rodinné zdroje jsou nastavené — po přihlášení je uvidí sám."; + await preloadSharedConfig(d.id, name, pass, gid); + extra = " Sdílené zdroje jsou nastavené — po přihlášení je uvidí sám."; } catch (e2) { - extra = ' Účet je založený, ale rodinné ' + + extra = ' Účet je založený, ale sdílené ' + "nastavení se nepodařilo přiřadit (" + esc(e2.message) + - "). Zdroje si zadá sám rodinným kódem."; + "). Zdroje si zadá sám kódem své skupiny."; } } out.innerHTML = '
Hotovo. Účet ' + esc(email) + @@ -867,7 +940,6 @@ async function createAccount(){ "." + extra + " Předej heslo, tady už ho znovu neuvidíš.
"; el("nEmail").value = ""; el("nPass").value = ""; el("nName").value = ""; el("nNsfw").checked = false; el("nRating").value = ""; - el("nFamily").checked = true; load(); } catch (e) { out.innerHTML = '
nepodařilo se: ' + esc(e.message) + "
"; @@ -898,9 +970,15 @@ function wire(){ }); if (el("createBtn")) el("createBtn").onclick = createAccount; if (el("tSave")) el("tSave").onclick = saveTemplate; + if (el("tGroup")) el("tGroup").onchange = function(){ + // Re-render from the selection. Unsaved edits in the fields are dropped, which + // is the honest behaviour: they belonged to the group you just left. + tplSel = el("tGroup").value; load(); + }; if (el("tShow")) el("tShow").onchange = function(){ var ty = el("tShow").checked ? "text" : "password"; - ["tAddon", "tCzech", "tTmdb"].forEach(function(id){ el(id).type = ty; }); + // The code is a shared secret too, so it hides and reveals with the rest. + ["tAddon", "tCzech", "tTmdb", "tCode"].forEach(function(id){ el(id).type = ty; }); }; if (el("days")) el("days").onchange = function(){ prefs.days = Number(el("days").value); savePrefs(); load(); }; @@ -926,11 +1004,8 @@ async function load(){ if (prefs.view === "admin") { var us = await records("users", { sort: "-created", perPage: 200 }); - var tp = null; - try { - tp = await familyTemplate(); - } catch (_) { /* no template yet, or the collection is missing */ } - el("main").innerHTML = viewAdmin(us.items || []) + viewTemplate(tp); + var tps = await allTemplates(); + el("main").innerHTML = viewAdmin(us.items || [], tps) + viewTemplate(tps); wire(); return; }