hzz docs implementation-log.md
35 kB · 464 lines
Markdown
at main

Implementation log #

Concise record of important choices, spike results, deviations and blockers. Newest entries last.

2026-10-01 — Bootstrap #

  • Toolchain: JDK 17 (Temurin 17.0.16+8 via mise), Gradle 8.10.2 wrapper, Android SDK platform 35 + build-tools 34.0.0 installed under ~/android-sdk. Reproducible via scripts/setup-android-sdk.sh and scripts/setup-gradle.sh.
  • Version pins chosen (see docs/build-environment.md). Notable: Compose BOM 2026.01.00 (compose 1.10.1) is the newest BOM compatible with AGP 8.6.1 + compileSdk 35; BOM 2026.09.00 pins compose 1.12.1 which requires compileSdk 37/AGP 9.1 — rejected. Compose 1.10.x requires compileSdk 35, so compileSdk=35/targetSdk=34 everywhere.
  • Full Gradle multi-module skeleton created per the spec's module layout; assembleDebug passes; libtorrent4j native lib for armeabi-v7a is packaged in the debug APK (11.7 MB).
  • :core:model domain contracts (MediaRef, SourceResolver, ResolvedAnswer/playback targets, state machines, QualityProfile, provider capabilities, primary-account policy) with 13 JVM tests passing.

Hardware gates (status) #

A Google TV Streamer is now reachable through an authorized laptop bridge. The 2026-10-02 read-only inventory records real firmware/ABI/package evidence. M0 remains incomplete pending DRM and reboot/reconnect acceptance; every M0.5 spike still lacks the required device outcomes and remains blocked, not skipped. The read-only M0 inventory script and docs/spikes/<name>.md protocols document prerequisites and measurements; the spikes still need their isolated harnesses and real-device execution. Implementation proceeded for everything that does not depend on those results, per the spec's M0.5 rule ("A spike may block its dependent feature without blocking unrelated milestones").

Follow-up — safe canonical mapping for Letterboxd imports #

  • Continued the existing, uncommitted account-client work. Letterboxd matching now requires a unique title-and-year match across movie and series search results, rather than selecting the first same-year hit. Title comparison ignores case and repeated whitespace only.
  • Missing dates, ambiguous matches and incomplete search result sets remain unmatched for a future manual-pick UI. Provider failures still propagate instead of returning a successful partial import. No database replacement or UI integration is implemented here.
  • Series matches preserve their series flag and canonical tmdb:tv identity. TMDB mapping accepts the known endpoint type; recommendations use the seed type, with separate movie and series cache keys even when numeric IDs collide.
  • Added 11 synthetic-fixture/unit regression tests covering matching, ambiguity, original rank/provenance, provider failure, endpoint typing and recommendation cache separation. These fixtures do not establish live provider compatibility.
  • Host verification: :core:model:test, :core:network:test, :accounts:test, :merge:test, :resolver:test, :app:testDebugUnitTest and :app:assembleDebug pass. Device gates remain blocked; no milestone completion, launcher replacement or engine enablement is claimed.

Follow-up — transactional library persistence #

  • Added the initial version-1 Room schema in core/database, exported to schemas/, with users/Household, accounts, feeds, canonical title/episode metadata, source provenance, local watchlists/progress, normalized audience membership and primary write selections.
  • LibraryStore provides domain/Flow boundaries, atomic complete/delta feed updates, non-destructive failure recording, stale-attempt rejection and explicit reauthorization. Sparse metadata updates preserve richer cached fields; recommendation provenance retains every seed. Deltas require a complete baseline and cannot contain contradictory operations.
  • Foreign keys implement ownership cascades without deleting shared canonical metadata. Household protection and exactly-one progress targets are also enforced by SQLite triggers. Same-provider/same-label accounts are allowed; primary choices are capability- and user-scoped.
  • Added 27 Robolectric/Room tests covering actual SQLite constraints, cascade retention, retries, failed/partial syncs, transaction rollback, primary policy, progress, Flow updates, file persistence and unsupported-version failure without destructive recreation.
  • Review fixes: successful-attempt retries no longer overwrite later shared metadata changes; ItemSource carries its originating feed ID through merge. Fixed shared-item detection to count distinct users, not multiple source records from one user, with another JVM regression.
  • Full host regression: 159 tests pass across model, network, database, accounts, merge, resolver and app; :app:assembleDebug passes. No real-device behavior was exercised.
  • Storage is not yet wired into app startup, provider sync or credentials. Remaining tables, integration boundaries and migration requirements are documented in docs/storage.md. M5 and device-dependent milestones remain incomplete; no architecture deviation selected.

Reviewed track integration #

  • Landed provider progress/subtitle storage (schema 3, migrations 1→2→3), corruption-preserving startup boundary, app startup/recovery UI, the credential-store boundary, and provider-independent sync coordination after review/fixes and full verification before and after each rebase.
  • tracks.json now declares the user-approved gate: every Gradle test task, including debug/release Android host variants, plus debug APK assembly. The broader gate caught a debug-only Compose host registration; fixed test setup without shipping a release test activity or dropping release coverage. See docs/track-integration.md for deferred nonblocking checks.
  • Domain/credential/sync boundaries remain separate: live account credential entry, concrete Room/provider sync adapters and durable write retries are not implemented. Stremio remains on its unfinished track. Hardware gates and milestone acceptance remain unclaimed.
  • Configured the user-supplied SSH origin; no push was performed during track landing.

M0 — first read-only device inventory #

  • Collected real firmware/ABI, package/version/installer, memory/storage and display evidence through the authorized laptop bridge. No device changes, install, reboot or denied-command workaround were attempted. The report and sanitized JSON contain no network endpoints, SSH identities, transport IDs or raw package dumps.
  • Measured build UTTK.260317.003, Android 14/API 34, ARMv7 app ABIs; existing target/native pins remain unchanged. User 0 has 137 installed packages, 136 enabled and one already disabled.
  • Recorded the actual Apple TV package (com.apple.atve.androidtv.appletv) as a follow-up to the stale application default; application behavior was not changed in this inventory task.
  • Generated text-only recovery instructions for the five confirmed spec candidates, without executing them. DRM quality, Widevine/Integrity, reboot/reconnect and M0.5 outcomes remain unverified. See docs/device/2026-10-02/.

Follow-up — measured pin, provider cancellation and Stremio primitives #

  • Corrected the fresh HomeLayout Apple TV pin using the captured package identity. Explicit saved lists are not rewritten; no device/configuration changes were made.
  • Landed reviewed fractional-rate/cancellation fixes in the shared provider client, with dispatcher-release and interrupted/cancelled no-network tests. Strict sliding-window provider quota orchestration remains separate from the token-bucket mechanism.
  • Landed bounded Stremio manifest/catalog/direct-HTTP primitives after iterative review fixes: duplicate decoded-key rejection, last-good retention, safe extras encoding, required headers, finite retry/redirect/body limits, cancellation through permit and body/delivery phases, abort-before-close cleanup, and strict range/framing handling. Prefix evidence stays UNVERIFIED; it does not establish device compatibility, whole-file playability or live-addon protocol compatibility. No production resolver/UI integration or native engine enablement.
  • Final integrated host gate passes: 276 distinct cases / 357 debug-release-inclusive test executions, zero failures/errors/skips, plus debug assembly. Detailed contracts and limitations are in docs/stremio-http.md, docs/network-cancellation.md, and docs/track-integration.md.

Cached library → Home integration #

  • Added transactional, credential-free Room snapshots of users/accounts/feeds/items/provenance, with invalidation across all contributing tables and no schema change.
  • Wired the actual Home activity to those snapshots through an app repository and FeedMerge: Everyone/person-plus-Household scopes, stable rows, cached-only inspection, complete scoped provenance, closed stale/error status, audience selection and deleted-user fallback.
  • Review-driven regressions cover populated cache retention on failure/retry/resubscription, actual Room-to-dialog shared-title privacy, and audience selection/deletion/cancellation races. Publication-time revalidation prevents a late selection result from reviving a deleted user.
  • Full integrated host gate passes: 285 distinct cases / 375 variant-inclusive executions, zero failures/errors/skips, plus debug APK assembly. No production demo rows, provider calls, device mutation or playable-answer claims were introduced. Hardware focus remains untested.
  • See docs/cached-home.md for viewing semantics and limitations. Live account setup/sync, Continue watching integration, configurable pinned/hide-watched rows, detail/playback and physical-device acceptance remain separate work; this is not complete M5/M6/M7 acceptance.

First beside-stock device smoke test #

  • Installed dev.hzz.streamer.debug version 0.1.0 / code 1 from source 3ddcd79 on the authorized Google TV Streamer; ADB confirmed successful installation and package identity.
  • Operator reported successful initial launch and passed the five-step remote/app-launch, Home-to-stock, reopen and Settings/Back checklist. No default-HOME or protected-package changes were performed. See docs/device/2026-10-04-launcher-smoke.md for exact scope.
  • This is partial M2 hardware evidence with an empty library. It does not establish DRM playback, populated-row focus, gamepad support, reboot/wake, or launcher replacement.

Durable account-wide tracking storage #

  • Added opt-in RoomTrackingSyncStore over account-wide TrackingStorage transactions: reserved attempts, success cursors, full/delta title+episode facts, explicit removals, independent authorization/failure status and retry idempotency.
  • Schema 4 migrates explicitly from 3, preserving older data/exports and safe-opening behavior. Imported episode metadata is retained without treating it as parent metadata or projecting episode history into whole-series rows. Partial playback progress remains a separate API.
  • Independent review and regressions cover cancellation/rollback, deletion/recreation and stale completion, reader isolation, migration/reopen throttling and malformed late stages through the actual coordinator while a healthy sibling commits.
  • Full gate passed before/after landing: 296 distinct cases / 397 variant executions, zero failures/errors/skips and debug assembly. No APK update or other device mutation.
  • Production binding remains off. A parallel audit recorded the current Simkl protocol and transport gaps in docs/simkl-adapter-gaps.md; synthetic clients cannot establish complete pulls or safe deltas. See docs/tracking-sync-storage.md for the storage contract.

Account-free cached search and bounded HTTP #

  • Home now has typed cached-library search with deterministic, scoped title/year results, bounded input/results and shared metadata/provenance inspection. Open inspection follows current ownership snapshots; tests cover scope/deletion changes, Back/Home/guide, keyboard controls, focus restoration and lifecycle reset. No online search or Play action is implied.
  • JsonHttp now cancels real OkHttp calls on coroutine cancellation and bounds decoded bodies (4 MiB default), including chunked/gzip responses. Rejection cancels before close to avoid hidden draining; callback failures are sanitized and complete the caller. POST bodies are one-shot, with explicit redirect caveats rather than unsafe mutation retries.
  • Independent review found and fixed tied-provenance ordering, a suspended-caller decoder Error path and a flaky cancellation test. Network tests passed three fresh runs by the writer and three by the reviewer; both tracks passed follow-up review.
  • Declared gate passed after rebasing and again on integrated main: 316 distinct cases / 422 variant-inclusive executions, zero failures/errors/skips and debug assembly. No real accounts, live providers, device changes or production demo data were used. See docs/cached-search.md and docs/json-http.md; physical-TV acceptance remains outstanding.

Persistent audience and provider timestamps #

  • Home now exposes Who's watching with explicit Apply/Cancel, persisted multiple-person membership, coherent user/audience snapshots and an atomic conflict-aware replacement. Viewing scope remains independent; choosing one person still supplies its audience default, while Everyone does not reset it. Existing Household membership can be retained/removed, but neither the UI nor a constructed draft can introduce it as a person.
  • Actual Room/UI tests cover external changes, deletion/recolouring, rollback, reopen, cancellation before/after commit and modal navigation/lifecycle. No schema change.
  • Strict minSdk-23-compatible timestamp parsing replaces substring/calendar normalization and receipt-clock fallbacks in tracking imports. Missing/unsupported required dates abort imports; supported formats are explicitly narrow and do not claim real API compatibility.
  • Independent reviews and audience validation fix passed follow-up checks. Declared gate passed after rebase and on integrated main: 332 distinct cases / 448 variant executions, zero failures/errors/skips, plus debug assembly. No device or live-account changes. Contracts: docs/audience-ui.md, docs/provider-time.md. Scrobbling/hide-watched remain unimplemented.

Quarantined web diagnostic harness and firmware system tiles #

  • Added an explicit debug-only CIO/Activity diagnostic harness with short-code pairing, bounded session/request state, ordered Start/Stop and independent maximum run lifetime. Reviews exposed transport limits: CIO idle expiry is not autonomous and ambiguous framing can produce extra rejection responses. Network mode is therefore runtime-quarantined; only same-device loopback diagnostics are approved. No production branch/M0.5 pass selected.
  • Real host tests distinguish invalid-request state mutation from an independently credentialed valid follow-up, characterize the remaining parser defect without claiming compliance, and enforce quarantine. Debug/release APK checks prove harness exclusion from release.
  • Installed the reviewed debug build after authorized laptop reconnect. Operator reported failed Inputs/Notifications tiles. Device resolution identified an available notification action, depending on stock Google TV; no input picker was found. Corrective system navigation now resolves accessible system handlers and shows honest unsupported-screen explanations.
  • Corrective source 0e32dcc installed successfully and Home opened. Operator retest is pending; shell dashboard launch is not ordinary-app or input-switching acceptance. See docs/device/2026-10-04-system-tiles.md and docs/system-tiles.md.
  • Final integrated gate: 367 distinct cases / 490 variant-inclusive executions, no failures, errors or skips; debug/release assemblies and APK boundary checks pass. Working hardware baseline, DRM, real tailnet/browser lifecycle and populated-library acceptance remain separate.

TV focus and app-card visual correction #

  • Operator reported low-contrast focus without a visible border and oversized capsule app buttons with small top-left icons. Added shared TV controls with a 3dp white focus border, contrasting focused fill/text and modest scale. App cards are 160×148dp rounded rectangles with centered 72dp icons, centered two-line labels and neutral missing-artwork placeholders.
  • Independent review and the full gate passed before/after landing: 369 distinct cases / 494 variant executions, zero failures/errors/skips and debug assembly. Host tests cover style contrast, geometry and real D-pad events, not physical-TV visual acceptance.
  • Source 511d887 installed with data retained; Home launched successfully. Operator visual retest pending. No changes to the unresolved Notifications/Inputs routes or stock defaults. See docs/tv-focus.md for the visual contract. The operator subsequently judged the visual change good enough for now and requested continued functional work.

Saved progress → Continue watching #

  • Corrected reconciliation so older explicit watched state cannot override newer partial replay progress. Added completed/incomplete states, exact overflow-safe 90% completion, deterministic ties and lossless per-account provider provenance.
  • Extended the transactional snapshot to all nine relevant tables and wired a real Home Continue watching row before pinned apps. Progress supplies ownership independently of feed sources; cards retain each visible user's position. Next-up requires cached same-season adjacency and never invents missing episodes or a shared resume offset. Inspection only: no playable answers, playback, scrobbling or live sync were added.
  • Review found a persisted-negative-progress Compose crash path. Validation now fails the snapshot read before publication, retaining valid cached rows and a closed retry status. Actual Room tests cover initial failure, hidden/losing invalid records, correction recovery and cancellation; no production repairs or schema changes were introduced.
  • Full gate passed before/after landing: 391 distinct cases / 530 variant executions, zero failures/errors/skips and debug assembly. Source a4d3aa5 installed with -r; Home opened successfully, no demo progress seeded. Populated-row device acceptance remains outstanding. See docs/progress-reconciliation.md and docs/continue-watching.md.

User-approved Netty evaluation and native startup fix #

  • User approved a debug-only Netty evaluation instead of maintaining a CIO fork (D5). Existing Ktor 2.3.12 pin retained; Netty 4.1.111.Final stays out of release. Public hooks implement bounded admitted channels, autonomous phase deadlines, response-aware streaming lifetime and fixed-length-only requests. Security review discovered permissive chunk parsing; all request transfer encodings now reject before that parser path. Network quarantine remains.
  • Serial real-socket and Android host tests cover framing/state mutation, cookies/origin/local destination, slow clients, SSE, admission, Stop/cancellation and release isolation. Independent security/build review approved only the loopback evaluation, not production or network access.
  • Native deployment exposed a shared protocol regex rejected by Android despite host passes. Reviewed fixed-vocabulary, message-free diagnostics identified PROTOCOL_CONSTRUCT / INITIALIZER / REGEX_SYNTAX. Escaping literal braces fixed it without loosening wire grammar.
  • Source 77b366c installed: native Netty start, page 200, status 401, single bad-framing 400/EOF, silent connection expiry, short survival after stock Home, and explicit Stop passed. No pairing codes/cookies captured; no phone test. Listener stopped and prior standby restored.
  • Final gate: 440 distinct cases / 579 variant executions, zero failures/errors/skips, debug/ release builds and APK/dependency boundary checks. See docs/netty-evaluation.md, docs/netty-android.md and docs/device/2026-10-04-netty-loopback.md. Next is a separately reviewed network-enabling change and actual paired-phone/tailnet/standby measurement.

Per-row hide-watched policy over the audience #

  • Added HideWatched in :merge: a WatchedIndex of completed movie/series states per user from the existing reconciler, and apply for the two HideWatchedPolicy values over an audience with Household removed. Completed episodes never mark a series watched; an empty or Household-only audience hides nothing. RowPolicy.defaultFor exempts history and defaults every other feed kind to hide-if-all-watched.
  • CachedHomePresenter.rows() applies the row default to each cached Home row using the durable playback audience, while the selected person still scopes sources. Imported progress counts only through an account the same user owns. Cached search asks for unfiltered rows so a watched title is still findable.
  • Per-row settings are not stored or editable; the older unused HideWatchedFilter in ForYouSeeds.kt is left in place. No provider, network, playback or device changes. Merge and presenter tests cover the policy semantics and the wiring. See docs/hide-watched.md.

Encrypted configuration backup format #

  • Added webserver/backup: a passphrase-encrypted file (BackupCodec, BackupFormat.kt) with a fixed 46-byte header of magic, version, Argon2id parameters, salt and nonce as the AES-256-GCM associated data, so any header change fails the tag. Argon2id through BouncyCastle derives the key from the passphrase alone; the local Keystore key is never involved, so a file restores after a reset or on another device. Export defaults are 64 MiB, 3 iterations, parallelism 1 for the 32-bit Streamer; restore checks size, magic, version and parameter caps before deriving a key, so a hostile file cannot make the device allocate. Wrong passphrase and tampering both surface as AUTHENTICATION_FAILED.
  • BackupPayload is the versioned JSON the file encrypts: users, accounts with their secrets in the clear through a CredentialExporter seam, primary write accounts, subtitle preferences, home layout and quality profile. Cached library data, progress, audience and device profile stay out. Decoding is strict (MALFORMED) and a newer payloadVersion reports NEWER_VERSION before any other key is read. An account whose secret cannot be read is exported without one and unauthorized, so restore keeps it and asks for re-entry.
  • RestorePlanner.plan decodes, migrates by version and validates the constraints the database enforces with keys and triggers, reporting one ValidationFault. RestoreFailure wraps the file, payload and validation layers as separate variants. BackupRestorer.restore makes exactly one ConfigurationWriter.replaceAll call; RestorePlannerTest proves every BackupFailure, PayloadFailure and ValidationFault returns with the writer uncalled, and that decrypted bytes and plan credentials are erased even when the writer throws.
  • Host-only: no HTTP route, no Room read or write, no ConfigurationWriter implementation and no device run. Declared gate passed on the track: 570 distinct cases / 722 variant executions, zero failures/errors/skips, plus debug assembly. Contract and limits are in docs/backup.md.

Subscribed answers from TMDB where-to-watch #

  • Added :resolver:subscribed with SubscribedBackend: a movie resolves by its TMDB id, a series or episode by the series id (where-to-watch is title-level), and only the configured region's flatrate list answers. ProviderAppDirectory.MEASURED maps the two packages measured on the device (Netflix, Apple TV+); an unmapped provider yields no answer because a guessed package name would produce a dead launch button. TMDB carries no deep links, so a title link exists only through the ServiceTitleIds seam and only for Netflix; the default opens the app's home. Verify keeps installed apps only; a thrown TMDB failure becomes a fixed tmdb_unavailable error that never echoes the URL or key.
  • 14 host tests against synthetic MockWebServer fixtures, plus ranking through SourceResolverImpl. No live TMDB key, no device. Nothing yet assembles a production list of resolver backends, so the backend is not reachable from the app; the household "Streaming subscriptions" setting also does not exist, so an installed app stands in for a subscription. SubscribedBackend.ATTRIBUTION must be shown beside any Subscribed answer. See docs/subscribed.md.

Stremio addon as an Instant source #

  • Added StremioBackend in :resolver:stremio, the first ResolverBackend over the landed addon primitives: StremioIds maps a MediaRef to the addon's tt… id (null means no request), discover fetches the stream page through AddonMetadataHttp.streams and turns each direct-HTTP candidate into an UNVERIFIED Instant answer whose release metadata comes from the stream title and whose id is a digest of the request, never the URL or a header.
  • verify probes at most the probe's candidate limit in page order and promotes only container prefix evidence, verified for fifteen minutes; rejected and deferred (HLS/DASH) candidates are dropped because no manifest verifier exists and only verified answers are offered. open hands the player the URL and required headers. Page failures are closed codes with fixed text and flip the backend's health until the next complete page.
  • Supporting changes: AddonHttpCandidate.releaseName keeps the first non-blank of title and name, bounded and control-free, for ReleaseNameParser; AddonHttpProbe.limits is readable. No shared model, network or primitive contract changed otherwise.
  • Host-only: ten MockWebServer tests, no live addon, no device, no Room id lookup and no production backend list, so the app still offers no Instant answers. Declared gate passed on the track: 590 distinct cases / 743 variant executions, zero failures/errors/skips, plus debug assembly. Contract and limits are in docs/stremio-backend.md.

Mullvad connection-check probe behind the leak guard #

  • Added MullvadProbe in :resolver implementing LeakGuardPolicy.Probe: one GET of https://am.i.mullvad.net/json, passing only on a 200 JSON object with mullvad_exit_ip the boolean true and a non-blank mullvad_exit_ip_hostname. Seven closed reasons separate a service-shape change (MALFORMED) from a route change (NOT_MULLVAD_EXIT); lastObservation keeps only checkedAt, passed and reason, and the IP, hostname, city, country and body text are parsed on one stack frame and discarded.
  • One request per check: no redirects, no authenticators, Connection: close, Retry-After normalised on 503/408, and a network interceptor that refuses any second exchange. Connection-failure fallback to the host's next address stays on because it is still one request and a single broken address must not fail every check. The 5 s deadline belongs to withTimeoutOrNull, with OkHttp's call timeout one second later as a backstop, so a cancelled body read is reported as TIMEOUT rather than NETWORK. Bodies are capped at 64 KiB. :resolver now depends on :core:network, OkHttp and MockWebServer for tests.
  • Host-only: 15 MockWebServer cases including the policy pause/resume sequence through the real probe. No Android glue (connectivity callbacks, transfer pauser, the player's "VPN check failed" state, wiring ProviderHttp.client("mullvad")) and no device or Tailscale measurement; the VPN spike stays blocked. Contract and limits are in docs/leak-guard.md.

Configuration backup restore writer and export side #

  • Added app/backup: RoomConfigurationWriter is the ConfigurationWriter behind BackupRestorer. The Keystore and DataStore cannot join a Room transaction, so a restore runs in three stages: every plan secret is stored under a fresh CredentialReference first (a failure deletes what was created and stops before the database is touched); one Room transaction reads the old references, runs LibraryStore.clearConfiguration, renames Household, inserts users and accounts under new ids with the fresh references and the plan's authorized flag, and restores selections through LibraryStore.restorePrimary (which, unlike selectPrimary, keeps a selection of an unauthorized account); after the commit the old references are deleted best effort and the home layout and quality profile are written. A plan id never survives into a row, so a backup from another device cannot collide with local rows.
  • ConfigurationReader builds the ConfigurationSnapshot an export encodes from LibraryStore.configuration() (one transaction over users, accounts, every primary selection and subtitle preferences) plus the layout and profile read after it. CredentialStoreExporter is the CredentialExporter over CredentialStore.read: a copy of the secret, null for a lost envelope or a value the store never generated, and a propagated CredentialStoreException rather than a backup that silently dropped a readable secret.
  • BackupRoundTripTest exports from one Robolectric device and restores into a second, comparing the configurations modulo ids, references and keys, and shows a wrong passphrase and a tampered file leaving a populated target unchanged with the same file still restoring afterwards; RoomConfigurationWriterTest covers each failure stage on its own.
  • Host-only: no HTTP route, no passphrase entry and no device run. Declared gate passed on the track uncached, zero failures, plus debug assembly. Contract and limits are in docs/backup.md.

Production resolver assembly #

  • Added app/.../resolver/: ResolverAssembly exposes resolver: StateFlow<SourceResolver>, rebuilt whenever the configuration derived from the accounts changes, with one StremioBackend per STREMIO_ADDON account (source key account-<id>, addon-scoped HTTP client) and one SubscribedBackend when a TMDB account exists. Credential bytes are the raw UTF-8 manifest URL or API key with no envelope; an account whose secret is unreadable is skipped alone. RoomStremioIds maps media to the addon id from the cached IMDb id; PackageInstalledApps asks the package manager. ResolverModule provides the assembly in Hilt from StorageStartup; before storage is ready, or after a failed configuration read, the resolver has no backends.
  • Health and wanted ids are shared across rebuilds; the answer cache is cleared on each one because cached answers name backends. Unrelated account edits do not rebuild.
  • SourceResolverImpl.status() now emits for a resolver with no backends; combine over zero flows never emitted.
  • Host-only: 8 Robolectric tests over a file-backed database, software-key credential store and MockWebServer. Nothing injects the assembly, so the app still offers no answers; leak guard stays UNKNOWN, the device profile is the default, and no credential entry exists for either account type. Declared gate passed on the track with --no-build-cache: 613 distinct cases / 774 variant executions, zero failures/errors/skips, plus debug assembly. Contract and gaps are in docs/resolver-assembly.md.

Production pairing, device tokens and the first API routes #

  • Added webserver/pairing: PairingService keeps one QR token (128 bits, ten minutes, single use, issued only when the TV screen asks) and one six-digit code (five minutes; expiry, the fifth wrong attempt and every success replace it), and issues the device token inside its own lock so success and rotation are one step. DeviceTokens hands out 256-bit tokens, stores only their SHA-256 behind the suspend PairedDeviceStore seam and compares every stored hash in constant time. RequestOrigin classifies by the numeric local address (tailnet inside 100.64.0.0/10) and names the exact authority a browser would send.
  • Added webserver/api: Application.streamerApi serves POST /api/v1/pair, GET /api/v1/devices and DELETE /api/v1/devices/{id} with the spec.md error envelope. Each request passes cookie (401), exact origin on mutations (403 BAD_ORIGIN), tailnet-only mutations except pairing (403 LAN_READ_ONLY) and JSON content type (415), in that order. No CORS headers; 500 carries no exception text. The local address comes through the RequestOriginResolver seam because the test engine cannot set it; production reads the socket and ignores every header.
  • ApiEvents records each answered request, pairing result and revoke with closed values and the answered status only. StreamerApiEventsTest checks that no token, code, name, address, port, origin or body text reaches a line or a field. /pair and unauthenticated refusals record without a rate limit, so a flood can push older events out.
  • Host-only: no engine, listener, Room store, QR rendering, app wiring or device run; an IPv4-mapped IPv6 local address is treated as LAN. 48 tests in :webserver cover the core, the routes and the events. Contract and limits are in docs/web-pairing.md.

Usenet discovery through Newznab indexers #

  • Filled the empty :resolver:usenet module: NewznabClient (t=movie by IMDb id, t=tvsearch by TVDB id, season and episode; o=json, 1 MiB body and 200-item caps, all-or-nothing parse of the shape variants indexers return), IndexerUsage (per-indexer API and grab counters per UTC day, reserved before each request so a refused one costs nothing) and NzbCache (24 hours under nzb:<sha256>, 8 MiB per NZB). Every failure is a closed NewznabException reason with fixed text, because indexer and NZB URLs carry the API key.
  • UsenetBackend searches every indexer concurrently, drops camera, telesync and screener releases, and returns UNVERIFIED candidates with a pending: placeholder target. Indexer failures fold into the one backend error (indexers_failed when several fail). verify checks only the top 3 by AnswerRanker, reusing cached NZBs, and verifies only on an Available article sample, expiring with the cached NZB.
  • The NNTP engine is gated on the M0.5 Usenet spike, so the only sampler is ArticleSampler.UNAVAILABLE, and with it verify fetches and verifies nothing; open throws. No app wiring, Room stores, id lookup or credential entry exists. Host-only: 38 MockWebServer tests with synthetic fixtures, no live indexer and no device. Contract and limits are in docs/newznab.md.