Commit graph

13 commits

Author SHA1 Message Date
9ee36181cb Stop a harmless verification link reading as an error
The failure copy says the expired link costs the person nothing, and then
rendered it in the error colour. Neutral text matches what the sentence says.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-05 19:33:52 +02:00
f72c9e7986 Finish confirming an email address on our own site
The verification email still pointed at pb.petruzalekr.cz/_/#/auth/..., the
PocketBase admin console. The previous pass left it there reasoning that nobody
had reached it yet; the accounts say otherwise. Registration fires a verification
email every time (auth_service.dart, fire and forget) and four of the nine
accounts are marked verified, so four family members have already landed in the
admin console and clicked a button in it.

?verify=<token> now confirms in Czech beside ?reset=. It confirms on page load
rather than behind a button: a provider that prefetches links only issues a GET,
the POST comes from the page's own script, and asking someone to press a second
button on a page they reached by pressing one is friction with nothing behind it.

Nothing is gated on verified. The users collection has an empty authRule and five
accounts, the owner's included, work fine without it. The flag stays because it
is the only evidence an address was typed correctly, which is exactly the
question that comes up when somebody reports a missing email. The failure copy
says so plainly, so an expired link reads as harmless rather than as a lockout.

Email-change is deliberately untouched: no surface can trigger it today.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-05 19:31:51 +02:00
eaff4abbca Make the reset link look like it belongs
Every other interactive element on the account page is amber; the browser
default made this one blue and underlined, which reads as something pasted in
from another site — not the impression to give somebody who already thinks they
have lost their account.

Spotted in a screenshot of the rendered page, not in the markup.
2026-09-05 19:08:37 +02:00
fb59f504fe Czech account emails, and a reset that finishes on our own domain
The reset email works — confirmed by a delivered message, after every attempt on
record had failed at the lookup and the mail path had never once been exercised.
What the delivered message showed was a different problem: it was in English,
and its button pointed at `pb.petruzalekr.cz/_/#/auth/confirm-password-reset/…`,
which is PocketBase's admin console. Every other surface of this app is Czech on
purpose, because the people using it are the owner's parents and his children —
and the one moment they are most likely to be stuck is the one moment it started
speaking English and sent them to what reads like a developer tool.

Found by looking at a delivered email rather than at the settings. The templates
were simply PocketBase's defaults, and a default is invisible until somebody
receives one.

The reset link now lands on amber.petruzalekr.cz, which hosts the form: two
fields, in Czech, on the domain people already know. A reset link wins over a
stored session, because somebody arriving with one is trying to fix their
account and dropping them into a signed-in page hides the thing they came to do.
The token is stripped from the address bar once spent — a URL carrying a
credential is one that ends up in a bookmark or a screenshot.

A failed confirm names the likely cause. These links expire, and "something went
wrong" gives somebody who opened yesterday's email no way to know that.

Verification and email-change keep PocketBase's own confirm pages: nobody has
used those, they are not part of the report, and pointing them at a page that
does not exist would be worse than English.

As a migration rather than a click in the admin UI, so restoring the backend
from migrations does not quietly put English back.
2026-09-05 19:04:50 +02:00
2b458fb9f6 Let people ask for a new password on the website, and stop capitals breaking it
Two halves of the same report. A family member could not get in, four password
resets produced nothing, and the site offered no way to ask for one anyway.

**Capitals.** The web sign-in trimmed the address but did not lower-case it, and
PocketBase looks accounts up case-sensitively — so `Vojta.Markup@seznam.cz` for
an account stored in lowercase matched nothing and read as a wrong password.
Phones capitalise the first letter of a text field by themselves, so this is the
mistake people will actually make. The same fix went into the app.

**The reset itself.** A link under the sign-in button, hidden until asked for:
the overwhelmingly common visit is somebody who remembers their password, and a
reset field on the way in invites people to reach for it first.

The message it shows says what was *done*, never whether the address matched.
The endpoint deliberately answers the same either way so that nobody can use it
to discover which addresses are registered — and pretending otherwise is exactly
what made this so hard to read from outside: four attempts, four "sent", nothing
arriving, no way to tell that the lookup had simply found nothing. It also
points at the one thing that always works when mail does not, which is asking
for it to be set directly.

Whether delivery itself works is still unverified: every reset attempt on record
failed at the lookup, so the mail path has never once been exercised. A reset
has been triggered against a real account to settle that.
2026-09-05 19:01:02 +02:00
dd37b36f6e Mint the download token when the button is pressed
A friend got "The requested resource wasn't found." downloading the adult
Windows installer. It is not a permissions problem and not missing data:
the row and the file are both there, and the same download returns 206 with
a valid token. That message is exactly what a protected file answers when
the token is missing or expired, verified against production.

releases.file is protected, so the URL needs a file token, and a file token
lives 180 seconds -- read off the JWT's own exp claim, not guessed. The
download list minted three of them while building the rows and baked them
into the hrefs, so three minutes after the page loaded every button was
permanently dead, and pressing it again could not help because the stale
token was in the markup. Nothing suggested reloading.

The owner never saw it because he presses the button as soon as the tab
renders. The setup flow I added this week makes it near-certain for anyone
else: it walks somebody through choosing services, creating accounts,
paying and fetching a TMDB key before pointing them at Stáhnout, which is
many minutes after the download tab was first rendered.

So the token is minted in the click handler now, and the link carries
download=1 so PocketBase sends an attachment rather than leaving the
browser to decide what to do with a .exe or a .zip.

The app was already correct: UpdateService mints its token immediately
before _dio.download, so auto-update on the family's devices was never
affected. This was the web page only.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-12 22:18:33 +02:00
874eab21dc Don't tell an account about 18+ when it hasn't got any
The child-profile checkbox read "Dětský profil (skryje anime a 18+, na
odchod chce PIN)" for everyone. Three of the four accounts are
nsfwEnabled = false, so for most of the household that sentence announced
a section they have never seen and cannot reach, and it was also untrue:
there is no 18+ content on those accounts to hide.

The adult source field on the Zdroje tab has always been gated on the same
flag. This label was the one place on the page that was not, because it was
static markup rather than something rendered from USER.

Now set in enter(): "skryje anime" alone, or "skryje anime a 18+" when the
account has it. Checked both branches against a clean and an adult test
user on a local instance.

The app is already clean here: childProfileDesc is only "K opuštění tohoto
profilu je potřeba PIN", and every 18+ string in app_cs.arb lives inside
the adult section itself, which a clean build does not contain.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-12 22:04:36 +02:00
8f2cb994ec The code has to choose the template, not the clock
Sharing a second set of paid accounts with friends means a second template,
and the route was not ready for one. It checked the code and then took the
most recently updated row:

  findRecordsByFilter("onboarding_template", "id != ''", "-updated", 1, 0)

With a single row that is invisible. With two it hands the family's
credentials to friends, or the reverse, decided purely by which row was
edited last, silently and with no error. status.html carried a comment
warning about exactly this: "never create a second".

So the code now selects the row, bound as a filter parameter rather than
concatenated. AMBER_INVITE_CODE becomes a fallback that opens the row
flagged isDefault, which means nobody halfway through setup broke and
Coolify needed no edit; once the family row has its own code the env var
stops mattering. A wrong code and a code with no row behind it give the
same 403, since telling them apart would confirm which codes exist.

Codes live on the rows rather than in more env vars, so a new group is one
row in the admin UI instead of a redeploy, and each code is revocable on its
own. The unique index is partial because PocketBase text fields default to
'' and SQLite calls two empty strings equal.

status.html read the template in three places, all by recency. They now go
through familyTemplate(), which selects on isDefault, so adding a friends
row cannot make the editor wander onto it or make account creation preload
the wrong group. Its one-click preload stays family-only; a group picker
there is left undone rather than half-built.

The website says "rodinný kód" in four places and friends are not family, so
that copy widens. The setup flow also offers "Mám kód" up front now: someone
Richard shares accounts with has nothing to buy, and walking them through
three price lists first would be actively misleading.

Verified against PocketBase 0.39.6 with two rows and friends as the most
recently updated, the state that used to break: each code resolved to its own
credentials, the legacy env code resolved to family via isDefault, wrong and
empty codes gave 403, a duplicate code was refused by the index, and
familyTemplate() returned family while -updated returned friends.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-12 21:35:46 +02:00
57585d8ad2 Tell a new viewer what to buy before asking them to fill in a form
Someone with their own accounts had to work out on their own that Amber
needs a paid subscription somewhere before it can find anything, and
which of three services that means. The Zdroje box already turns their
usernames into addon URLs; nothing anywhere told them which usernames to
go and get, or what it would cost.

Six steps as a new #start tab: change the generated password, choose
services, create the accounts and pay, take a free TMDB key, enter it all
in Zdroje, download. Steps 1, 5 and 6 hand off to the tabs that already do
that work. This screen owns no credentials and no crypto on purpose, since
a second encryptor for one blob is how a vault gets corrupted.

The password is step 1, not step 6. Changing it re-keys the vault with a
fresh salt, so every device already signed in holds a stale key until it
authenticates again. Done first, while the vault is empty, there is
nothing to re-encrypt and no paired television to strand.

State is three fields on the user record and nothing more: skipped, chosen
and pwChangedAt. Whether the sources exist is CONFIGS.length and whether
TMDB is set is a field in the decrypted config, so storing those again
would let two answers disagree. pwChangedAt is the one thing that cannot be
derived, because PocketBase records no password-changed timestamp.

Prices and click-paths live in a providers collection, editable in the
admin UI, with priceCheckedAt rendered beside the number so a stale figure
looks stale rather than reading as a promise. Owner supplied the three
signup guides; TorBox Free is called out as unusable because it has no API
access, which is the only way Amber talks to it.

byGo also stops demanding a Czech host. Nothing downstream needed one, and
someone who only wants anime was being told to buy a service they had no
use for.

Verified against PocketBase 0.39.6 from a throwaway data dir with this
repo's real migrations, hooks and page: both migrations applied clean, the
flow opened itself for a user with no sources, a choice persisted as
{"chosen":["prehrajto","torbox"]} and nothing else, guides rendered
numbered with working links, skipping raised the banner and survived a
reload, resuming came back in.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-12 18:36:55 +02:00
dc4f85fd4f The TV instructions stop naming an app that may not be installed
"Na televizi otevři aplikaci Downloader" was carried over from the old landing page
and never checked. Downloader is a third-party sideloading app people install on Fire
TV sticks; it is not standard on Android TV, and the owner has never seen it on his.
An instruction that points at something absent is worse than no instruction — the
family would simply be stuck.

The browser route is what he actually uses and has confirmed works, so it is the
instruction now: open the TV's browser, go to the site, press a button. The button is
new — telling someone to type a second URL when a link can do it was needless, and
`/tv` stays as the shortcut for anyone who would rather type than click.

Both places said it: the signed-out card and the Stáhnout tab.
2026-08-07 09:33:28 +02:00
5bfc45cd23 The sources form no longer insists on prehraj.to
The addon accepts either host on its own as of stremio_prehrajto 7ef248d, so the
page that mints the URLs stops claiming otherwise. Each host stays all-or-nothing
and at least one must be complete — the same rule /encode enforces, checked here
too so the message arrives in Czech rather than as a 400.
2026-08-07 09:09:56 +02:00
12e7f4103f The unified page becomes the site; retire the two it replaced
index.html was the old landing page with a signup form, and that form can now only
fail — registration is owner-only. A dead button is worse than no button, so the
unified account page takes its place rather than sitting beside it. settings.html
goes too: it is a strict subset of the sources section, and onboarding.html was the
signup wizard.

Verified against the live backend before the swap, signed in as a throwaway account
created the way the owner now creates them (and deleted afterwards — the instance is
back to its five real accounts):

  registration    403 "Only superusers can perform this action", with and without
                  the old invite header
  template route  401 anonymously
  session         survives a full page reload; the password does NOT, so the
                  sources section re-asks for it — the intended split
  prefs           one row, updatedAt distinct from the server cursor, and all 13
                  fields present including the three the page does not render, so
                  the merge did not clobber what the app writes
  sources (BYOC)  the browser reached the addon's /encode cross-origin, minted the
                  prehraj.to token, built TorBox's config locally, encrypted and
                  saved; the stored blob contains no plaintext and carries
                  kdf=pbkdf2-sha256-210000
  downloads       all three platforms resolve to 1.1.6 · clean with a file token

Checked rather than assumed: the Czech URL is stored with /manifest.json (matching
the existing template) and the app strips it on both read and write via
_stripManifest, so minting it that way is correct and not a latent 404.
2026-08-07 02:10:48 +02:00
0b65fd7f49 Family onboarding site: invite-gated signup, template config, /get/tv
Distribution site served by PocketBase itself (pb_public/, same origin):
- Landing (/): create-account CTA, TV sign-in steps, login-gated PC
  downloads via the existing update manifest + file tokens
- Wizard (/onboarding.html): invite code -> account -> named default
  profile -> family template fetched and encrypted IN THE BROWSER under
  the new user's password (PBKDF2-210k + AES-GCM, byte-compatible with
  the app's AddonConfigCrypto - a wizard blob decrypts in Dart, verified)
  -> pushed as their addon_config ciphertext. Adult fields never included.
- /get/tv: public direct download of the latest clean Android APK
  (Downloader-friendly; adult builds stay account-gated)

Backend:
- onboarding_template collection (admin-only, maintained via admin UI)
- AMBER_INVITE_CODE env gates BOTH users creation (X-Amber-Invite header
  or ?invite=) and the template route (per-IP rate limit 10/5min);
  fail-closed when unset. Note: this closes the previously-open in-app
  registration too.
- Dockerfile ships pb_public + --publicDir; compose passes the env var

Contract + Coolify steps (second domain amber.petruzalekr.cz) in
docs/onboarding-contract.md. Verified locally end-to-end against
pocketbase 0.39.6 (real-browser wizard run + Dart decrypt interop).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-22 18:23:05 +02:00