* feat(settings): split settings into per-section pages with an unsaved-changes bar Replace the single scrolling settings page with routed section pages (/workspace/settings/:section): the context-panel rail links each section, only the active section renders, and unknown or capability-gated sections redirect to General. The shared form lives on the parent component, so staged edits survive section switches; a floating unsaved-changes bar (Save/Discard) replaces the always-visible footer Save button. Rail links navigate with replaceUrl so Back still leaves settings in one step. Along the way: - delete the unreachable settings dialog mode and the dead AppPortalNavigationActionsService with both of its never-injected DI tokens (PORTAL_NAVIGATION_ACTIONS, PLAYLIST_PLAYER_ACTIONS) - delete the scroll-spy directive and pendingScrollTarget plumbing - revive the EPG panel's "Open EPG settings" empty-state button as a deep link to /workspace/settings/epg; the M3U player now reports m3u-needs-setup only when the channel has no programmes and no EPG source exists in settings or on the playlist itself - load TMDB cache stats when the Metadata page opens (the section component now only exists while its page is open) - add SETTINGS.UNSAVED_CHANGES / SETTINGS.DISCARD_CHANGES to all 19 locales Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(settings): confirm before leaving with unsaved changes Add settingsUnsavedChangesGuard (canDeactivate on the :section route) with a three-action dialog: save and leave, leave without saving, keep editing. The guard only intercepts leaving the settings AREA — section switches share the one settings form and pass unconditionally, so the dialog can never nag while moving between pages. A failed save cancels the navigation instead of silently dropping the edits it promised to keep; leaving without saving also reverts the live theme preview. Save-and-leave is disabled while the form is invalid, with a hint explaining why. New SETTINGS.UNSAVED_DIALOG_* keys in all 19 locales. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(settings): stage cover size and EPG view mode; adapt e2e to section pages Cover size and EPG view mode were the only two controls that persisted eagerly on click, which made Discard (and leave-without-saving) unable to revert them: hydrateFromStore() faithfully reloaded the just-persisted edit. They now stage in the form like every other setting and reach the store on Save. Review finding by Greptile (P1) and Codex. E2E suites that walk through settings are updated for one-section-page rendering (epg, backup-roundtrip, xtream-epg, remote-control) and for the staged cover size (downloads asserts the dataset after Save); the EPG icon fallback test saves before leaving settings so the new unsaved-changes dialog does not block its navigation. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
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
WebVideoControlsAdapterto 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