# AGENTS.md This file provides guidance to coding agents working in this repository. ## Plan Mode - When an agent is in Plan Mode and produces a final ``, it must also save that finalized plan as a Markdown file in the repo-root `.plans/` directory. - Save only finalized plans. Do not write interim exploration, questions, or draft revisions to `.plans/`. - Use the filename pattern `YYYY-MM-DD-short-topic.md` such as `.plans/2026-03-12-channel-filtering.md`. - If the intended filename already exists, append a numeric suffix such as `-2`, `-3`, and so on. ## Agent Bootstrap - In a fresh worktree, run `pnpm install --frozen-lockfile` before relying on Nx project discovery, lint, test, or build commands. Without `node_modules`, `pnpm nx show projects` will fail because the local Nx modules are unavailable. - Re-run the install whenever the checkout moves — `git pull`, `git reset --hard`, a rebase, or a worktree branch being re-pointed. Git rewrites `pnpm-lock.yaml` but never re-links `node_modules`, so a tree installed at an older commit keeps serving the old dependency versions and tests fail locally while CI stays green. Check with `cmp pnpm-lock.yaml node_modules/.pnpm/lock.yaml`; any difference means the tree is stale, and a plain `pnpm install --frozen-lockfile` in that directory repairs it. Each worktree needs its own install — with no local `node_modules`, Nx aborts with `Could not find ".modules.yaml"`. - After dependencies are installed, verify workspace discovery with `pnpm nx show projects`. - Use scoped path aliases from `tsconfig.base.json` such as `@iptvnator/services`, `@iptvnator/shared/interfaces`, and `@iptvnator/ui/components`. Do not add new imports from legacy bare aliases such as `services`, `shared-interfaces`, `components`, `m3u-state`, or `database`. - Every Nx project should keep `scope:*`, `domain:*`, and `type:*` tags in `project.json` so `@nx/enforce-module-boundaries` remains useful for humans and agents. - See `docs/architecture/nx-workspace-boundaries.md` for the current Nx tag and alias policy. - Keep `nx` and every official `@nx/*` package on the same exact version; run `pnpm run deps:nx:validate` after dependency updates. - Vite `7.3.6`, resolved through Angular's build tooling, is patched with bounded transform prefilters and the upstream precise matchers in `patches/vite@7.3.6.patch`. Keep the patch until supported Angular tooling resolves a Vite version containing the fix, and run `pnpm run deps:vite:test` after related dependency updates. - `app-builder-lib` `26.15.7` (electron-builder's macOS signing) is patched in `patches/app-builder-lib@26.15.7.patch` with the upstream backport electron-userland/electron-builder#10172: `security set-key-partition-list -k` must receive the temporary keychain's own password, not the `.p12` import password. macOS runner images since `macos-26-arm64` 20260831 verify that password, and `Build on macos arm64` failed with `SecKeychainUnlock: The user name or passphrase you entered is not correct`. Keep the patch until electron-builder resolves an `app-builder-lib` containing the fix (26.16.1+), and run `pnpm run deps:electron-builder:test` after related dependency updates — the test fails when the patched version no longer matches the installed one. - A directory holding files consumed by other projects must be an Nx project. Nx builds its graph from TypeScript imports only, so a relative SCSS `@use` across project roots creates no edge and the imported file lands in no task hash — edits then return a cache hit instead of rebuilding. Shared partials live in `libs/ui/styles` (project `ui-styles`), and each consumer declares `"implicitDependencies": ["ui-styles"]`. Run `pnpm run styles:inputs:validate` after adding a cross-project stylesheet import. - Update Nx with `pnpm nx migrate nx@ --skipInstall`, regenerate the lockfile, run generated migrations when present, and validate before opening a PR. Major updates are always manual. Replace incomplete Dependabot security PRs with a coordinated update instead of editing the bot branch. - ESLint enforces `max-lines` on TypeScript files: production code targets under 300 with a hard maximum of 400, while tests (`**/*.spec.ts`, `**/*.spec-data.ts`, `**/*.e2e.ts`, `apps/*-e2e/**`) are held to 1200 — a long spec signals coverage, not the design debt the production limit catches. Blank lines and comments are not counted, so a docblock never forces a split. Limits live in `tools/eslint/max-lines-config.mjs`, imported by both `eslint.config.mjs` and the generator so the rule and the baseline cannot drift. Files that predate the rule are baselined in `tools/eslint/max-lines-baseline.mjs`; after splitting a file, regenerate it with `node tools/eslint/generate-max-lines-baseline.mjs` (it runs ESLint's own rule rather than counting lines itself). Never add new files to the baseline — the list must only shrink. A new file that genuinely cannot be split (for example a function serialized into another process) instead carries its own file-wide `/* eslint-disable max-lines -- */`; the generator skips those files, so a justified exemption never lands in the baseline. Remove such a directive once ESLint reports it as unused. - Project `lint` targets that shell out to eslint must quote the glob, e.g. `eslint "apps//**/*.ts"`. An unquoted `**` is expanded by the POSIX shell on Linux and macOS (which has no `globstar`, so it matches only a shallow subset of files) while Windows passes the literal pattern to ESLint, which expands it recursively — the two hosts then lint different file sets. The target still reports success either way, so a broken glob hides missing coverage instead of failing. After changing such a target, compare the linted file count against `find -name '*.ts' | wc -l`. - Repository-specific skills live under `.codex/skills/`. - Frontmatter descriptions are trigger-only and begin with `Use when`; keep each skill at or below 500 words. - Run `pnpm run skills:validate` after editing a committed skill or a literal path it documents. - Keep `.codex` and `.claude` copies of `release-notes` and `release-cut` byte-identical. ## Documentation After Changes - After implementing a meaningful change, agents must assess whether canonical repo docs need updates before considering the task complete. - Meaningful changes include new or changed user-visible behavior, architecture or data-flow changes, non-obvious maintenance workflows, new setup/debugging steps, and new subsystem contracts or boundaries. - Skip doc updates for trivial refactors with unchanged behavior, formatting-only edits, and isolated test-only changes. - Prefer updating an existing authoritative doc before creating a new one: 1. `README.md` for top-level developer or user workflows 2. `docs/architecture/` for architecture, ownership, and behavior contracts 3. the nearest module `README.md` for local usage or behavior - Keep the root `CLAUDE.md` and this file up to date. They are living documents: whenever a change touches something they describe — monorepo structure (new/moved/renamed apps or libs), routes, database schema/tables, stores and their features, key components, commands, environment behavior, or coding conventions — update the affected sections as part of the same task, and keep the process sections mirrored between `AGENTS.md` and `CLAUDE.md` in sync. - When adding a new feature area, check whether the Architecture or Key Features sections of `CLAUDE.md` describe the surrounding area; if they do, reflect the addition there instead of leaving the description stale. - Do not let `CLAUDE.md` or `AGENTS.md` drift: a stale path or route in these files poisons the context of every future agent session. If you notice an outdated claim while working, fix it (or flag it in the final summary) even if it is unrelated to the current task. - Repo docs are canonical even when they were originally drafted by an LLM. - Final task summaries should state whether docs were updated and which doc changed. ## Release Notes For User-Visible Changes - Any change a user could notice — new behavior, changed behavior, bug fix, performance win, breaking change — must add one note file under `.changes/` in the same PR. Format, field table, and writing rules: `.changes/README.md`. - Name it `-.md`; `area` matches the conventional-commit scope. There is no version field — the release version is chosen at release time. - Write the body for a user, not a reviewer: "the player now remembers volume between episodes", not "hoist volume state into the session". Max 400 characters; depth belongs in the release blog post. - `type: internal` records invisible maintenance. Internal notes stay collapsed in `CHANGELOG.md`, are omitted from the blog scaffold, and are removed from the authored public GitHub body by `extract-changelog-section.mjs --public`; GitHub's generated commit list remains separate, so an internal-only release can have an empty authored body. - `highlight: ` (max 60 characters, rejected on `type: internal`) marks a note as one of the release's two or three headline changes. Highlights lead the Telegram/Reddit announcement drafts, become ready-made blog section headings, and are the input the highlight-card generator renders from. A release where everything is a highlight has none. - Skip the note for test-only changes, docs, CI/workflow plumbing, and pure refactors with no behavior change. When skipping on a PR that touches `apps/**` or `libs/**`, apply the `no-release-note` label. - CI enforces this: the "Release note gate" job in `.github/workflows/ci.yml` fails PRs that change runtime code without an added `.changes/*.md` or the label (policy in `tools/release/check-release-note-gate.mjs`; tests/e2e/website/mock-server/docs paths are auto-exempt). - The `release-notes` skill covers writing notes; the `release-cut` skill covers the full release sequence. Canonical contract — surfaces, ordering constraints, the required draft asset set: `docs/architecture/release-pipeline.md`. - Validate before finishing: `pnpm run release:notes:validate`. - Announcement drafts and highlight cards are built from the same notes: `pnpm --silent run release:notes:telegram` and `pnpm --silent run release:notes:reddit` print paste-ready posts to stdout (Telegram is guaranteed to fit its 4096-character limit; `--silent` keeps pnpm's lifecycle banner out of a redirected post), and `pnpm run release:cards:generate` renders branded 1200×630 highlight cards plus a release hero into `dist/release-highlight-cards/v/`. All three read `highlight:` metadata that exists only in the note files, so they must run before `build-release-notes.mjs --consume`; the cards additionally need `release:screenshots` to have run. Nothing is posted or copied into the website tree automatically. - Pushes to `master` and `v*` can publish Docker images. A `v*` tag build creates a draft GitHub release. - `pnpm run release:verify:draft` waits for that tag build (polling until the run is indexed, then `gh run watch`) and verifies the draft's status, authored body, and complete required asset set. It is read-only and deliberately fails on an already-published release, because it is the gate that runs before publication. - Publishing the GitHub release verifies its Snap assets and automatically uploads them to `edge`; installed-Snap smoke and candidate/stable promotion remain manual. - Release-post screenshots come only from the release capture script running against the mock servers. Never add a screenshot taken from a real playlist or account to `apps/website/public/blog/**` — real streams, logos, and metadata are copyrighted, and credentials must never reach a published image. - Final task summaries should state whether a release note was added or why it was skipped. ## Regression Prevention And Test Updates - Before the final summary for any feature, behavior change, bug fix, data-flow change, Electron IPC/database change, or user-visible UI workflow change, complete a test impact pass. Identify the affected projects and decide whether unit, integration, E2E, build, lint, or manual/CDP verification is required. - Bug fixes must normally include regression coverage that fails on the old behavior and passes with the fix. If automated coverage is not practical, document why in the final summary and include the strongest manual validation performed. - Feature work and behavior changes must update existing tests when assertions, fixtures, mocks, routes, or E2E flows are now stale, incomplete, or missing. Prefer extending the closest existing spec or E2E file before adding a new suite. - Default validation ladder: 1. Run targeted unit tests for directly affected projects with `pnpm nx test ` or existing scripts such as `pnpm run test:frontend`, `pnpm run test:backend`, or `pnpm run test:unit:ci` when the scope is broader. 2. Run affected E2E coverage when changing user-visible workflows, routing, persistence, playback, portals, settings, import flows, or Electron-only behavior. 3. Use `pnpm nx show projects --withTarget test` and `pnpm nx show projects --withTarget e2e` when project ownership or available validation targets are unclear. 4. Prefer specific atomized E2E targets before broad suites when they cover the changed behavior, for example `pnpm nx run web-e2e:e2e-ci--src/xtream.e2e.ts` or `pnpm nx run electron-backend-e2e:e2e-ci--src/search.e2e.ts`. - Electron-specific changes affecting IPC, SQLite, packaged runtime, external players, native file access, or Electron-only routes require Electron E2E coverage where available, or CDP/manual verification with `agent-browser` and the tracing flags documented below. - Final task summaries must list tests added or updated, validation commands run with results, and any skipped validation with the reason. For docs-only changes, state that unit/E2E validation was not required and verify the changed Markdown instead. ## Electron Debugging (CDP) - Start the Electron development app with: `nx serve electron-backend` - Package-script equivalent: `pnpm run serve:backend` - Electron is configured to start with: `--remote-debugging-port=9222` - Connect Chrome DevTools Protocol tools to: `127.0.0.1:9222` - For Electron automation/debugging tasks, use the `electron` skill - Do not auto-open DevTools during normal CDP automation. In development, DevTools is opt-in via `ELECTRON_OPEN_DEVTOOLS=1`. - If DevTools is open, `agent-browser --cdp 9222 ...` may attach to the DevTools page instead of the IPTVnator window. Symptoms: `tab list` shows `about:blank`, snapshots are empty, and screenshots are black. - If that happens, inspect targets with `curl http://127.0.0.1:9222/json/list` and connect directly to the IPTVnator page websocket from the `webSocketDebuggerUrl` field. - The app holds a single-instance lock (`acquireSingleInstanceLock` in `apps/electron-backend/src/app/services/single-instance.ts`): a second launch against the same `userData` quits immediately and focuses the running window. To attach a second CDP-enabled instance to the same profile, set `IPTVNATOR_ALLOW_MULTIPLE_INSTANCES=1` — knowing that only one of the two processes will own the renderer's IndexedDB, so settings written by the other are lost. Before focusing, the guard forwards the second launch's argv to `onSecondInstance`, which is how a playlist path handed to an already-running app reaches the open queue. ### Trace / Debug Startup - Full startup tracing: ```bash IPTVNATOR_TRACE_STARTUP=1 nx serve electron-backend ``` - Narrower trace flags: - `IPTVNATOR_TRACE_IPC=1` traces renderer `window.electron.*` bridge calls - `IPTVNATOR_TRACE_DB=1` traces DB worker requests and request-scoped DB events - `IPTVNATOR_TRACE_SQL=1` traces SQLite statements in the main process and DB worker - `IPTVNATOR_TRACE_WINDOW=1` traces BrowserWindow lifecycle and unresponsive events - `IPTVNATOR_TRACE_PLAYER=1` traces external-player activity and bounded Embedded MPV runtime-probe stderr - `IPTVNATOR_TRACE_RENDERER_CONSOLE=1` mirrors renderer console output into the Electron terminal - `IPTVNATOR_PERF_CAPTURE=1` enables development/test-only, redacted M3U and Xtream preload IPC request/completion markers plus count-only M3U acquire/parse/normalize, Xtream main network/JSON-transform/success-response-ready/cancel-dispatch, and renderer store phase capture; renderer wrappers emit only while the benchmark installs its Symbol hook, benchmark tooling sets the flag explicitly, and production launches must leave it unset - `IPTVNATOR_PERF_WORKER_PROFILING=1` enables development/test-only, request-scoped worker receive/work/response-post timestamps, thread CPU, event-loop utilization/delay, count-only playlist serialization/SQLite write/read/deserialization plus Xtream category/content/cache-clear/delete/in-source-search phase events, profiling-only worker cancel-receipt acknowledgements, valid-sample-counted isolate peak memory, and the database worker's idle-only one-shot post-GC heap probe; overlapping database requests are explicitly invalidated instead of misattributed, the performance benchmark sets the flag automatically, and production launches must leave it unset - Settings, portal request/response, and trace payloads must use `@iptvnator/shared/logging` or the redacting portal logger before reaching `console.*`; never log raw credentials while debugging. - If local Nx state gets weird before a rerun: ```bash pnpm nx reset ``` ### agent-browser (global install) ```bash agent-browser --cdp 9222 tab list agent-browser --cdp 9222 tab 1 agent-browser --cdp 9222 snapshot -i -c -d 4 agent-browser --cdp 9222 screenshot /tmp/iptvnator-cdp.png ``` ### Fallback ```bash npx --yes agent-browser --cdp 9222 tab list ``` ### DevTools Workaround ```bash ELECTRON_OPEN_DEVTOOLS=1 nx serve electron-backend curl http://127.0.0.1:9222/json/list agent-browser connect ws://127.0.0.1:9222/devtools/page/ agent-browser screenshot /tmp/iptvnator-cdp.png ``` ## Xtream Category Management The Electron Live TV, Movies, and Series category dialog applies Select/Deselect to search results while a filter is active and to the whole type otherwise. Button states use the matching group; "Total selected" counts the whole catalog. Save persists the complete draft, Close discards it, and refresh restores hidden categories by provider ID and type. See `docs/architecture/category-management.md`. ## Portal Connectivity Preference - Desktop Settings > General > Portal connections exposes default-on `Settings.portalConnectivityGuard`. Only explicit false opts out. Save mirrors the value to Electron `PORTAL_CONNECTIVITY_GUARD` and applies it without restart; settings bootstrap restores it before the renderer loads. It controls Xtream and Stalker together. Preference transitions clear cooldowns and invalidate old request completions; unchanged saves preserve evidence. The environment switch `IPTVNATOR_DISABLE_CONNECTIVITY_GUARD=1` remains authoritative. PWA clients do not control the shared backend's guard. - Both account-info dialogs explain guard refusals with localized paused-request copy and Retry now; Stalker preserves cached account data on a failed refresh. Contract: `docs/architecture/host-connectivity-guard.md`. ## Channel and Detail Keyboard Scrolling Channel scroll owners use `ChannelScrollFocusDirective`; pointer selection focuses the viewport, native scrolling survives virtual row recycling, and row Enter/Space activation stays separate from focus movement. Portal Live TV uses ArrowRight from the selected category and ArrowLeft from the channels pane to move between columns. Shared live sidebars reserve scrollbar space beside the resize handle. `PortalDetailShellComponent` owns a visible native scrollbar and guarded initial page focus. Contracts: `docs/architecture/iptvnator-ui-guidelines.md` and `docs/architecture/portal-detail-navigation.md`. ## Radio / Audio Player M3U playlists can contain radio channels identified by the `radio="true"` attribute on `#EXTINF` lines. When a radio channel is selected: - The dedicated `AudioPlayerComponent` (`libs/ui/playback/src/lib/audio-player/`) renders instead of a video player - The audio player always uses the built-in inline player — external player settings (MPV/VLC) are ignored - The EPG panel is hidden (radio streams have no EPG data) - The layout uses a cinematic hero pattern: the station logo is blurred as a full-area backdrop with a vignette overlay, and the artwork card + controls float above it - Volume is shared with the video player via `localStorage` key `'volume'` - Keyboard shortcuts: ArrowUp/ArrowDown (volume +/-5%), M (mute toggle) - Radio detection in the video player template: `activeChannel.radio === 'true'` — this is a string comparison, not boolean Key files: - `libs/ui/playback/src/lib/audio-player/audio-player.component.ts` — the audio player component - `libs/ui/playback/src/lib/audio-player/audio-player.component.scss` — cinematic hero styling - `libs/playlist/m3u/feature-player/src/lib/video-player/video-player.component.html` — template conditionals for radio vs video - `libs/shared/interfaces/src/lib/channel.interface.ts` — `radio: string` field on Channel interface ## M3U URL User-Agent - The URL import form accepts an optional User-Agent and stores it as `Playlist.userAgent`. Electron sends it on initial download, manual refresh, and startup auto-update. The self-hosted PWA sends it through the registered target `/parse` backend proxy for import and refresh; a matching backend is required, and browser playback-header restrictions still apply. - Reuse the existing source editor and channel-over-playlist playback header precedence. Contract: `docs/architecture/m3u-playlist-module.md` ("User-Agent for URL sources"). ## Shared Player Controls - `libs/ui/playback/src/lib/player-controls/` contains the additive, engine-neutral `PlayerController` contract, standalone `app-player-controls`, generic web-video adapter/helper, and component-scoped `WEB_PLAYER_SHARED_CONTROLS` rollout token. - The subtitle menu carries capability-gated advanced subtitle support (#1408): external subtitle file loading, a ±0.5 s timing-offset row, and size/color styling persisted in the shared `subtitleStyle` localStorage key. HTML5/ArtPlayer implement it through the neutral source bridge (`.srt`/`.vtt` via a DOM file picker with encoding detection, native `TextTrack` rendering, `::cue` styling, delay only while the loaded file is the selected track; picks are source-generation-guarded and engine deselection precedes external track activation). The canonical style shape and clamp/normalize rules are shared with the main process via `@iptvnator/shared/interfaces` (`subtitle-style.util.ts`). Embedded MPV frame-copy implements it through helper protocol commands (`sub-add`/`sub-delay`/`sub-scale`/`sub-color`, main-process file dialog, ASS supported, delay for all tracks). Video.js shared mode, vendor-chrome paths, native-view, and the Linux out-of-process path advertise no such capability and render no UI. Contract details: `docs/architecture/player-controls-contract.md` ("Advanced subtitle support"). - In fullscreen, `app-player-controls` shows a pointer-transparent media-title overlay at the top while controls are revealed (`mediaTitle` input: movie/channel/series name, plus an `S01E03` second line for episodes). Series names flow from the Xtream/Stalker detail views through `PortalInlinePlayerComponent.seriesTitle` and `WebPlayerViewComponent.mediaTitle`; movie and live hosts fall back to `playback.title`, skipping raw stream-URL fallbacks. Outside fullscreen the overlay stays hidden. - Auto-hide pauses while the pointer is over the controls bar or keyboard focus is inside it, but only keyboard-originated focus pins the bar open. Chromium also focuses a clicked `