The app has been sending `flavor` since yesterday's change and PocketBase was
throwing it away. An unknown key in a create payload is dropped with no error, no
warning and HTTP 200 -- the same silent-drop behaviour already documented for hidden
fields in amber-app/CLAUDE.md. So the dashboard's "Podle varianty" row read
`neznámá` for every record while the client believed it was reporting the variant,
and the only way to notice was to query the stored rows and find the field absent.
Found by verifying instead of assuming: a real play/exit on the TV produced two
session_summary rows, both with flavor empty, on a build that definitely sends it.
Not folded into `meta`, though that needs no migration and the dashboard already
falls back to reading it there. The variant is a first-class dimension of every
record, exactly like platform and appVersion beside it, and burying it in json would
make the one query it exists for -- group errors by variant -- awkward for nothing.
Additive and nullable so existing rows stay valid. Validated on a clean local boot
of the pinned PocketBase against the real migrations directory: health 200, no
errors, and client_logs now reports the field.
The family runs Amber on hardware I can't debug directly, so the app batches
a low-volume diagnostic stream here: uncaught errors, player error codes,
per-session playback summaries, and the audio-delay a user dials in to fix
desync (the "a track falls behind" signal).
- Migration 1788000000_client_logs.js: owner-create-only, superuser-read-only
(list/view/update/delete = null); fields kind/event/message/meta/appVersion/
platform/device/ts + created.
- client_logs.pb.js: nightly cron trims rows older than 14 days.
- docs/telemetry-contract.md: event catalogue, how I query it, and the
privacy/redaction guarantees (host-only, never a token or full URL).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
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>
Each profile may carry a maxRating token ("kids" | "twelve" | "fifteen" |
"unrestricted") capping which movies/TV it can browse and play. Empty means
"no explicit tier" — the client falls back to the account default (a child
profile resolves to "kids"). Plain text, opaque to the server: all filtering
is client-side like the kids filter, and a string column lets the app add
tiers without another migration.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The E2E caught a real leak: a non-nsfw (even anonymous) account could download
an adult artifact. In PocketBase, file protection is a per-FIELD flag, not
derived from the collection view rule — the original migration left releases.file
unprotected, so its URL was public despite the gated read rules.
- Add protected:true to the file field (correct for fresh installs).
- 1786500001_releases_protect_file.js: alter the field on the already-deployed
instance (applied migrations don't re-run, so the fix needs its own migration).
- Doc: correct the gating explanation (protection is the field flag; the file
token grant then re-checks the view rule).
With this, a protected file needs a file token whose grant re-checks the view
rule, so a clean account is denied the adult artifact.
Adds the server side of auto-update (issue #16, was epic #6's [H]).
- pb_migrations/1786500000_releases.js: a superuser-only 'releases' collection
(platform, variant, version, buildNumber, file, sha256, size, notes). Read is
gated in the rules — any signed-in account sees clean builds; only an
nsfwEnabled account sees adult ones — so PocketBase's native protected-file
serving hands adult bytes only to flagged accounts (no custom streaming).
- pb_hooks/update.pb.js: GET /api/update/manifest?platform=… (auth). Picks the
variant server-side from the caller's nsfwEnabled (adult) vs clean — the client
can't request adult — and returns the latest build's version/buildNumber/notes/
sha256/size + the protected downloadPath.
- scripts/publish-release.sh: uploads a built artifact as a superuser (computes
sha256 + size, multipart POST). Token or email+password via env.
- docs/auto-update-contract.md: the collection, endpoint, download flow, gating.
Migration + hook + script syntax-checked. Live verification pends deploying this
to the PB (collection auto-applies on boot, hook loads from pb_hooks/).
A new device (TV) signs into an account with no password typed on it and
no pairing code, approved from an already-logged-in phone/PC.
- device_auth collection (migration): code (unique), deviceName, status,
user, expiresAt + hidden authToken/keyCiphertext/pollSecret/devicePubKey.
Owner-scoped list/view/delete only (the device list + revoke); create/
update locked to the hooks.
- pb_hooks/device_auth.pb.js: public request + info + pollSecret-gated
poll routes (the TV is unauthenticated), auth-gated approve (mints
e.auth.newAuthToken(), stores the E2E vault-key ciphertext) + decline,
and a cleanup cron that scrubs delivered tokens and stale rows.
- pb_hooks/device_page.pb.js: self-contained approve page at GET /device.
Signs in, derives the addon-config vault key (#20) with WebCrypto PBKDF2,
seals {salt,keyB64} to the TV's X25519 pubkey (vendored TweetNaCl +
WebCrypto HKDF/AES-GCM), and approves. The server never sees the key.
- Dockerfile ships pb_hooks (--hooksDir); verify.py covers the schema +
that the routes are served.
Crypto matches the app byte-for-byte (proven by the app's fixed-vector
tests). Full contract: docs/device-auth-contract.md. JWT-revocation
limitation documented (v1).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Adds the addon_config collection so a logged-in user's addon configuration
(TorBox key, Czech-dub creds, adult addon, TMDB key) can follow their account
to a fresh device — encrypted client-side, so the server only ever holds
ciphertext.
- pb_migrations/1785200000_addon_config.js: one row per profile (unique index),
OWNS access rules like the other per-profile collections. Stores blob
(AES-GCM ciphertext), salt (per-account KDF salt; not secret), kdf
(derivation descriptor), plus the same two-clock model as #11 (client
updatedAt for LWW, server updated as the pull cursor).
- README.md: data model + a section on why this one blob is encrypted.
- scripts/verify.py: schema assertions + ciphertext round-trip + cross-user
isolation checks for addon_config.
Stacks on the #11 sync-fields backend work (PR #1). Client half is
myanime-app's encrypted addon-config sync PR (issue #20).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The #9 schema sketched these collections before the client existed. Reconciling
two devices turned up three gaps:
- `meta` (json) on watch_state/watchlist. Local rows carry display fields —
episode, title, cover art, MAL id, completed/dismissed — with nowhere to live
server-side, so a freshly-signed-in device pulled resume points it couldn't
render.
- A client-owned clock. `updatedAt`/`addedAt` were autodate, i.e. stamped on
server receipt, so a device that edited offline and pushed a day later beat a
device that edited afterwards and synced at once. LWW needs the time the user
acted. The server autodate lives on as `updated` and is now the pull cursor —
that one must be server-side, or a skewed client clock would write rows behind
another device's cursor and stay invisible to it.
- `deletedAt` tombstones on watchlist. Hard deletes are an absence, and an
absence isn't pullable — the next device to push just resurrects the title.
The type change means dropping and re-adding the fields, which drops their column
data, so the migration snapshots the old timestamps and writes them back. Live is
believed empty, but blanking `updated` would leave rows invisible to every future
pull, which is too quiet a failure to risk on an assumption.
Verified against a local docker compose on both paths — a fresh install, and an
upgrade over the init-only schema with rows already in it (timestamps preserved).
scripts/verify.py grows the field-shape assertions plus behavioural checks: the
client clock round-trips unrestamped, duplicate (profile,itemId) is rejected, and
delete/re-add reuses the row instead of duplicating. 36/36 pass.
Refs richiexec/myanime-app#11
Foundation for the user-accounts platform. Self-hosted PocketBase v0.39.6,
version-pinned from the official release binary, schema-as-code so the
collections auto-apply on boot and never drift from hand-clicking.
Collections: users (auth, admin-only nsfwEnabled), profiles, watch_state,
watchlist, prefs. Per-owner access rules traverse profile.user; nsfwEnabled
is never client-writable (superuser-only). Relations cascadeDelete; unique
indexes keep one resume/watchlist row per (profile,itemId) and one prefs per
profile.
Verified locally against v0.39.6: migration applies clean, and scripts/verify.py
proves two users can't read/write each other's data and can't set their own
nsfwEnabled (19/19 checks). Coolify deploy (domain/TLS, superuser, SMTP,
backups) documented in README as the manual half.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>