A movie that exists in several imported Xtream playlists now shows a
"Sources N" chip on its detail page and in the player. Switching playlist
mid-film keeps the timecode, a preferred source can be pinned per movie, and
a failed stream offers the alternatives instead of a dead end.
The governing rule is that a guess is never presented as a fact. Every
metadata value carries where it came from — `api` (the provider said so),
`parsed` (inferred from the title) or `probe` (we contacted the stream).
Facts render as plain tags, guesses are prefixed `~` in a warning colour, and
an unknown value renders no tag at all plus a "check" affordance. Ranking and
failover read through `factualOnly()`, so a filename claiming 4K is
structurally unable to outrank a source that was actually reached. A probe
that could not complete reports "unknown", never "unavailable".
Scope is deliberately narrow: Xtream to Xtream, movies only, Electron only.
Stalker never reaches the `content` table and M3U is a JSON blob whose search
forces live content; both are additive later, since the candidate type
already carries all three portal kinds. In the PWA every entry point is gated
off and the chip renders nothing.
Auto-failover is opt-in and off by default. Each source is tried at most once
per session, so it terminates structurally, and the switch is never silent —
the toast names the new playlist, offers an undo, and warns that the dub may
differ only when both sides state an audio track as fact.
Notable details:
- Playlist names are routinely the pasted URL, credentials included. They are
never rendered raw; a short host-only label is derived instead.
- Quality is derived from pixel width, not height: a 2.39:1 1080p master is
1920x800, and bucketing that by height would publish "720p" as a fact.
- Switching is a single `inlinePlayback.set()` so the player and engine
survive and re-seek; the carried position is read before the 15s
persistence throttle so it does not rewind.
- Sources from one playlist collapse into a group, since the same film often
appears there several times under different stream ids.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Adds contract-focused regression coverage for the Electron HTTP server,
remote-control events, settings events, and managed download paths, and makes
Tier A coverage fail closed when instrumentation fails or a runtime-owning
production file disappears from a project or from the merged Istanbul report.
The old `coverage:ci` exited 0 despite a `Failed to collect coverage`
diagnostic: libs/m3u-state/src/lib/effects.ts was simply absent from the merged
map. All 30 Tier A reports are now required, the merged map covers 710 files,
and effects.ts is reported as 0/159 instead of silently disappearing.
Also fixes remote static-file path containment for encoded, malformed, NUL,
POSIX and Win32-style traversal inputs, with behavior-preserving testability
seams.
Statements 69.27% -> 69.54%; http-server.ts 0% -> 90.21%,
remote-control.events.ts 0% -> 96.55%, settings.events.ts 59.25% -> 96.29%.
Pipeline audit follow-up: reduce wasted runner time on PRs and tighten CI
security, without reducing what actually gets validated.
Runner-time waste:
- Concurrency with PR-only cancel-in-progress on CI, E2E, and docker-build,
so a new push cancels the previous commit's still-running checks. Non-PR
runs use the unique run_id as the group, because GitHub keeps at most one
pending run per group even with cancel-in-progress: false — a shared ref
group could silently drop a queued master run.
- paths-ignore for docs-only changes (Markdown, docs/, .plans/, .codex/,
.claude/) on the Electron build matrix and the E2E suites; E2E also skips
apps/website/**. The build workflow keeps apps/website/** because its Linux
job builds the website to verify AppStream assets. Tag pushes are
unaffected: GitHub does not evaluate paths filters for tags.
- PRs lint affected projects only; master pushes keep the full run-many.
Lint-global inputs (eslint.config.mjs, tools/eslint/**) now mark all 41
lint projects affected, including the run-commands targets database and
packaging, so the max-lines baseline cannot be widened without lint.
Hardening:
- Explicit least-privilege permissions on CI, E2E, and build-and-make; the
create-release job keeps its job-level contents: write. The repository
default workflow token was switched to read-only.
- New actionlint job (image pinned by digest, shellcheck at warning+), with
the shared-anchor false positive suppressed in .github/actionlint.yaml.
Fixed one real finding: unquoted $GITHUB_OUTPUT.
- .github/dependabot.yml: weekly cadence, minor+patch grouped per ecosystem
(npm, GitHub Actions, Docker), majors stay individual PRs.
Docs updated: CLAUDE.md, docs/architecture/nx-workspace-boundaries.md, and
docs/architecture/validation-map.md now describe affected-lint on PRs and the
E2E path-filter exceptions.
* fix(lint): resolve module-boundary and prefer-inject errors
Retag workspace-shell-util as type:data-access to match its injectable
services that depend on @iptvnator/services, and convert
RemoteControlService to inject(HttpClient).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* feat(lint): enforce max-lines 400 with generated baseline
Add a max-lines ESLint error (hard cap 400 raw lines per TypeScript
file) per the repo file-size rule. The 134 pre-existing offenders are
baselined in tools/eslint/max-lines-baseline.mjs, regenerable via
generate-max-lines-baseline.mjs; the list should only shrink.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* feat(ci): enforce lint on PRs and guard coverage policy drift
- Add a Lint job to ci.yml running nx run-many -t lint --all, so
module-boundary tags, legacy-alias bans, and max-lines gate merges.
- Fix the root lint script (was linting only electron-backend).
- Add tools/coverage/check-coverage-policy.mjs: fails CI when a project
with a test target is missing from coverage-policy.json; wired into
coverage:ci as coverage:policy:check.
- Run Tier B/C unit tests in CI without coverage (list derived from the
policy), so website/packaging/remote-control tests run on PRs.
- Replace the hand-picked 16-project test:unit:ci list with --all.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* docs: document CI lint enforcement and coverage policy guard
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(ci): address bot review feedback on policy guard and baseline generator
- Drive Tier B/C validation from each policy entry's validationCommand
(falling back to nx test), skipping projects with an e2e target since
the E2E workflow already runs them (Codex).
- Fail when a Tier A entry has no test target (Greptile, adapted:
checking all entries against test targets would false-positive on the
intentionally spec-less e2e/mock-server tiers).
- Guard against missing JSON array in nx show projects output (Greptile).
- Scan .tsx files in the max-lines baseline generator to match the
ESLint rule's file patterns (Greptile).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>