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>
81 lines
4 KiB
JavaScript
81 lines
4 KiB
JavaScript
/// <reference path="../pb_data/types.d.ts" />
|
|
|
|
// The services a BYOC user has to buy, described in data rather than in markup.
|
|
//
|
|
// **Why a collection and not copy in index.html.** Two of these fields go stale
|
|
// on somebody else's schedule: the price, and the click-path through a signup
|
|
// form the provider is free to redesign. A wrong price on our own page is worse
|
|
// than no price — it reads as a promise — and the fix must not require editing
|
|
// HTML and redeploying the backend the family streams through. Everything here
|
|
// is editable in the admin UI, and `priceCheckedAt` is rendered next to the
|
|
// number so a stale figure is visibly stale instead of quietly wrong.
|
|
//
|
|
// **Why not onboarding_template.** That collection holds the family's actual
|
|
// credentials and is superuser-only on every rule for that reason. Public-facing
|
|
// marketing copy has no business sharing a table with secrets, and widening its
|
|
// read rule to serve a price list is exactly the kind of accident this comment
|
|
// exists to prevent.
|
|
//
|
|
// **Why `kind`.** TMDB is not a source and is not optional — no key means no
|
|
// posters and no descriptions — so the chooser must not offer it as one of three
|
|
// things to weigh up. `source` rows are choices; `metadata` rows are steps that
|
|
// happen regardless. Keeping it a token rather than a boolean leaves room for a
|
|
// third kind without another migration.
|
|
//
|
|
// **Why `price` is text.** These are quoted in different currencies and
|
|
// different periods ("5 $/měsíc", "199 Kč/měsíc", "$30/rok"), and the page only
|
|
// ever displays it. A number plus a currency column would be three fields to get
|
|
// wrong in exchange for arithmetic nobody does.
|
|
//
|
|
// Read is limited to signed-in users. There is no public signup — accounts are
|
|
// owner-created — so nobody who cannot log in has any use for this, and the
|
|
// tighter rule is free. Writing stays superuser-only: this is content the owner
|
|
// maintains, not something a user submits.
|
|
|
|
migrate(
|
|
(app) => {
|
|
const c = new Collection({
|
|
type: "base",
|
|
name: "providers",
|
|
listRule: "@request.auth.id != ''",
|
|
viewRule: "@request.auth.id != ''",
|
|
createRule: null,
|
|
updateRule: null,
|
|
deleteRule: null,
|
|
fields: [
|
|
// torbox | prehrajto | webshare | tmdb. The page keys its per-service
|
|
// wiring off this, so it is the one field that must not be edited
|
|
// casually — hence the note here rather than only in the UI.
|
|
{ type: "text", name: "slug", required: true, max: 40 },
|
|
{ type: "text", name: "name", required: true, max: 80 },
|
|
{ type: "url", name: "url", required: true },
|
|
{ type: "text", name: "kind", required: true, max: 20 },
|
|
// One line: what this unlocks, in the viewer's terms.
|
|
{ type: "text", name: "tagline", max: 300 },
|
|
{ type: "json", name: "pros", maxSize: 4000 },
|
|
{ type: "json", name: "cons", maxSize: 4000 },
|
|
{ type: "text", name: "price", max: 120 },
|
|
{ type: "text", name: "priceNote", max: 300 },
|
|
{ type: "date", name: "priceCheckedAt" },
|
|
// Ordered strings, rendered as a numbered list. Plain text on purpose:
|
|
// this is read by someone with a signup form open in the next tab, and
|
|
// markup would be one more thing to get wrong in an admin textarea.
|
|
{ type: "json", name: "steps", maxSize: 20000 },
|
|
{ type: "number", name: "sort" },
|
|
// A row is hidden rather than deleted when a service stops being worth
|
|
// recommending, so its guide survives for anyone who still has it.
|
|
{ type: "bool", name: "enabled" },
|
|
{ type: "bool", name: "recommended" },
|
|
{ type: "autodate", name: "created", onCreate: true },
|
|
{ type: "autodate", name: "updated", onCreate: true, onUpdate: true },
|
|
],
|
|
indexes: [
|
|
"CREATE UNIQUE INDEX idx_providers_slug ON providers (slug)",
|
|
],
|
|
})
|
|
app.save(c)
|
|
},
|
|
(app) => {
|
|
app.delete(app.findCollectionByNameOrId("providers"))
|
|
},
|
|
)
|