Files
iptvnator/.changes
4grayandClaude Opus 5 30d76a3a46 fix(search): keep in-flight typing when the page rewrites its query params (#1432)
* fix(search): keep in-flight typing when the page rewrites its query params

`WorkspaceShellSearchSyncService` re-read `q` on every `NavigationEnd` and
unconditionally called `setSearchState(...)`, which cancels the pending
debounce and overwrites the search box. Any same-page navigation that carried
no search intent — a downloads filter chip writing `?filter=…`, a refresh bump,
or the router echoing back our own `q` — therefore ate whatever had been typed
since the last applied term.

While input is debouncing, ignore a navigation that stays on the same page and
carries the term already applied. Real search intent (route change,
back/forward, a different `q`) still syncs as before.

This is the race behind the flaky `@downloads @electron keeps global and scoped
libraries truthful …` e2e test: it clicks a filter chip and fills the searchbox
with nothing awaited in between, so under CI load the chip's navigation lands
after the keystroke, wipes the term, and `q` is never written.

Reproduced deterministically by dispatching the chip click and the `input`
event in the same page task; the searchbox value goes to `""` and `q` stays
`null`. The new spec fails on the old code for the two regression cases and
passes for the three guard cases on both.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix(search): keep history authoritative and retire the debounce on Enter

Two follow-ups from review of the same-page navigation guard.

Greptile: the guard could not tell an app-initiated `q` echo from back/forward
landing on a history entry that carries the same term. Key the exemption on
`Navigation.trigger === 'imperative'` instead, so browser history always wins
over in-flight typing. `lastSuccessfulNavigation` is set immediately before
`NavigationEnd` is emitted, so it describes the navigation being handled.

Codex: with the guard in place, an Enter commit no longer had its queued
debounce cancelled as a side effect of the resulting `NavigationEnd`. Typing
"Beta " and pressing Enter before the debounce expired applied the trimmed
term, then the stale timeout reapplied the untrimmed one — leaving the box and
URL on "Beta" while the provider store searched "Beta ". `applySearchQuery()`
now cancels the pending debounce itself, which is the correct owner of that
rule rather than relying on a navigation side effect.

Both new tests were mutation-checked: dropping either sub-fix fails exactly its
own test and no other.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-08-13 09:10:24 +02:00
..

Release notes (.changes/)

Every PR with a user-visible change drops one file here describing that change in plain language. At release time tools/release/build-release-notes.mjs turns the accumulated files into the GitHub release body, the CHANGELOG.md section, and a blog-post scaffold for the website — then deletes them.

The point is to write the note while the context is still fresh, instead of reconstructing three months of work from commit titles at release time.

File

Name it <area>-<short-slug>.md, e.g. .changes/playback-up-next-rail.md.

---
type: feature
area: playback
issues: [1187]
screenshot: up-next-rail
---

Series now show an "Up Next" rail beside the player on wide windows: the rest
of the current season, watch progress, and click-to-play inline.
Field Required Value
type yes breaking, feature, fix, perf, or internal
area yes lowercase slug, same as the conventional-commit scope
issues no issue numbers this closes — [1187] or 1187
screenshot no slug from tools/release/screenshots.manifest.json

There is no version field. The release version is chosen deliberately at release time, not derived from these files.

You never write a PR number: the generator resolves it from the commit that added the file.

Writing the body

One to three sentences, present tense, written for a user, not a reviewer. The body is capped at 400 characters — depth belongs in the blog post.

  • ❌ "Refactor WebVideoControlsAdapter to hoist volume state into the session"

  • ✅ "The player now remembers volume between episodes"

  • ❌ "Fix off-by-one in resolveEnrichmentSeasonNumber"

  • ✅ "Series whose title carries a season marker no longer show the wrong season"

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. An internal-only release can therefore have an empty authored body.

When a note is not needed

The gate auto-exempts website, E2E and mock-server apps, *.spec.{js,ts}, *.e2e.{js,ts}, snapshots, any /testing/ path, and Markdown. For other test-only, documentation, CI/workflow, or pure-refactor changes under apps//libs/, apply no-release-note when no user-visible note is warranted.

Commands

pnpm run release:notes:validate
pnpm run release:notes:github
pnpm run release:notes:changelog
pnpm run release:notes:blog
node tools/release/build-release-notes.mjs --consume

The release version comes from the root package.json — bump it first, then generate. --version 0.24.0 overrides it to preview a release before the bump:

pnpm run release:notes:github --version 0.24.0

A bare -- separator is accepted and ignored, so the npm habit of pnpm run release:notes:github -- --version 0.24.0 works too: pnpm forwards that separator to the script rather than consuming it the way npm does.

--validate and --format github only read and print. --format changelog and --format blog write their target file (rerunning changelog for the same version replaces that section rather than duplicating it). Only --consume deletes anything.

The release sequence is: bump the version → release:notes:changelog → release:notes:blog → --consume → commit → tag → push. The tag build then extracts the new CHANGELOG.md section into the GitHub release body (tools/release/extract-changelog-section.mjs) and fails the release if the section is missing — a tag cut without the changelog step cannot silently ship PR-title-only notes.

The website publishes one post per minor version (v0-18 … v0-22), and release screenshots live under the matching blog/v0-24/ directory. A patch release therefore edits the existing post rather than generating a new one, so --format blog refuses to overwrite unless you pass --force.

Screenshots

pnpm run release:screenshots captures every manifest shot in dark and light against the built app plus the Xtream mock server — never a real account. The run is fail-closed: it proves the real ~/.iptvnator/databases directory (including the SQLite WAL sidecars, checked after Electron exits) was not touched, launches the app with an allowlisted environment, records and blocks all non-localhost traffic, scans every frame for external resources and credential-shaped text, and asserts TMDB enrichment stays disabled. Frames are staged outside the repository and published only once every shot and every guard has passed.

Adding a shot for a new feature = one entry in tools/release/screenshots.manifest.json (plus, if navigation is new, one named action in tools/release/capture-navigation.ts).

pnpm nx run electron-backend:build-e2e   # once, before capturing
pnpm run release:screenshots             # all shots, both themes
pnpm run release:screenshots -- --only dashboard --theme dark
pnpm run release:screenshots -- --release v0-24