Adds a paused state to the Electron download manager with a full partial-file lifecycle:
- Pause keeps the .part and byte progress; cancel discards them; every lifecycle stage (queued, active, mid-transfer) is pausable.
- Resume continues via HTTP Range with If-Range entity validation (strong ETag / Last-Modified persisted in the new resume_validator column, idempotent migration incl. legacy-table rebuild). Non-206 answers restart from zero over the same .part; the 206 Content-Range offset is verified; responses that end before the advertised size are retained for a Range retry instead of being committed as completed.
- Crash recovery converts interrupted transfers to paused, keeps queued-with-partial rows resumable, and commits finalizations that crashed before the DB update.
- Destination collisions are non-destructive (retained partials finalize to the next numbered name); locked .part files never lose their DB owner across cancel/remove/restart; resume claims rows atomically and the queue dedupes ids.
- Stored request headers are re-filtered through the User-Agent/Origin/Referer allowlist on read, URL-derived extensions are sanitized, resume appends never follow symlinks, and transfer errors are logged by message only.
- UI: pause/resume/cancel/retry/remove surface failures in a snackbar; paused items show an active Resume button in VOD/episode detail views; translations for all 18 locales.
- Runtime split into download-runtime/transfer/finalize/broadcast modules; +30 unit tests and an Electron E2E covering pause -> retained .part -> Range/If-Range resume -> byte-exact assembly.
Co-authored-by: genrichh93-ui <genrichh93@users.noreply.github.com>
🤖 Generated with [Claude Code](https://claude.com/claude-code)
* feat(epg): manual EPG-to-channel mapping with mapping fallback in all EPG paths
* fix: epg mapping in live tv list
* fix(epg): harden manual EPG mapping — upgrade safety, perf, playlist-scoped keys
Follow-up fixes on top of the manual EPG-to-channel mapping feature:
- epg-database: dedupe existing epg_programs rows before creating the
unique (channel_id, start, title) index — a plain CREATE UNIQUE INDEX
crashed the EPG worker on upgrade when historical duplicates exist;
replace INSERT OR REPLACE with ON CONFLICT DO UPDATE so the
epg_programs_fts delete trigger is not bypassed (REPLACE skips delete
triggers unless recursive_triggers is on), with a plain-INSERT
fallback when the index cannot be created
- db: add idx_content_epg_channel — the mapping fallback scanned the
whole content table on every single-channel EPG lookup
- keys: scope Xtream mapping keys per playlist via shared
buildXtreamEpgMappingKey (xtream:{playlistId}:{id}) — bare stream ids
collide across portals; the backend fallback now joins categories to
resolve the playlist id
- pwa: hide "Map EPG channel" entries behind the supportsEpgMapping
capability — the menu item was a dead end in the PWA
- parser: parse the XMLTV offset sign from the string — Math.sign(0)
dropped the minutes of ±00:xx offsets
- cleanup: typed window.electron access instead of ad-hoc casts, drop
unused resolveChannelId and dialog data field, shared
EpgMappingDialogComponent.open() for all seven call sites
- dialog UX: minimum-characters search hint, save/remove snackbars,
current mapping shows the EPG channel display name,
takeUntilDestroyed on the search stream
- i18n: fill the new keys in all 17 locales
- tests: cover mapping CRUD/search escaping, the dedup-index guard,
offset parsing and playlist-scoped keys; update stale stream-resolver
specs for the new 50-item limit and 10s timeout
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(epg): escape backslashes in EPG channel search LIKE pattern
CodeQL js/incomplete-sanitization: a lone trailing backslash in the
search term paired with the closing wildcard under the ESCAPE clause
and corrupted the pattern.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* perf(epg): batch mapping lookups in the viewport preview queue
Expose the existing getEpgMappingsBatch operation over a new
EPG_MAPPING_GET_BATCH IPC channel and use it in resolveManualMappings —
the per-entry lookup issued one IPC round-trip per visible channel on
every scroll event.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* perf(epg): batch mapping prefetch in the collection preview loader
Resolve all candidate mapping keys for an Xtream preview batch with a
single getEpgMappingsBatch IPC call instead of per-channel lookups.
Also fix a worker early-exit: a channel without tvgId/name returned out
of the shared-iterator loop and silently killed one of the three
concurrent preview workers.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
---------
Co-authored-by: 4gray <serega05@gmail.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
* feat(ui): add custom title bar window controls for Windows and Linux
Hide the native title bar on win32/linux (titleBarStyle: 'hidden', frame
untouched so native resize borders and snapping keep working) and render
minimize / maximize-restore / close buttons in the renderer, mirroring the
existing macOS traffic-light setup.
- New WINDOW:* IPC contract (minimize, toggle-maximize, close, get-state)
handled in window.events.ts, resolved from the sender WebContents;
close goes through win.close() so window-bounds persistence still runs.
- WINDOW:STATE_CHANGED pushed on maximize/unmaximize/fullscreen so the
maximize/restore glyph stays correct for OS-triggered changes; controls
hide while fullscreen.
- WindowControlsComponent mounts once in app-root as a manual popover so
it stays in the browser top layer above CDK overlays (dialogs,
multi-EPG) - same behavior as macOS traffic lights.
- Theme-aware via CSS vars (--app-on-surface, --app-hover-overlay);
Windows-red close hover. Drag regions get right padding through a
body-level frameless-platform class.
- Gated by RuntimeCapabilitiesService.usesCustomWindowControls; PWA and
macOS never mount the controls.
Includes unit specs for the component and IPC handlers, an Electron E2E
suite (window-controls.e2e.ts), and a window-chrome section in
docs/architecture/workspace-shell.md.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* feat(linux): upgrade Electron to 41 for frameless window decorations on Wayland
With the native title bar hidden, Linux windows lost the WM-drawn shadow
and rounded corners. Electron draws client-side decorations only on
native Wayland, and frameless-window CSD (GTK drop shadow + extended
resize boundaries) landed in Electron 41 - before that, frameless
windows render as plain rectangles.
- electron ^39.8.5 -> ^41.7.2 (Wayland auto-detected since 38.2; X11
sessions remain undecorated, matching other frameless Electron apps;
Windows keeps its DWM shadow and rounded corners).
- better-sqlite3 pinned to exactly 12.9.0: the last release shipping
prebuilt binaries for both Node 20 (ABI 115, Jest) and Electron 41
(ABI 145, runtime). 12.10.0 dropped the Node 20 prebuilds, forcing a
from-source build that fails without a C++ toolchain.
- pnpm override node-abi 3.85.0 -> 3.92.0 so electron-builder
install-app-deps can map Electron 41 to ABI 145.
Reviewed Electron 40/41 breaking changes: only the renderer clipboard
deprecation, which this app does not use.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(e2e): address review feedback and window-managerless Linux CI
- Skip the three window-manager-dependent E2E assertions (maximize
toggle, main-process state sync, minimize) on Linux CI: GitHub's
ubuntu runners drive Electron under xvfb without a window manager, so
maximize/minimize state never materializes there. Windows CI and
local Linux/macOS runs keep the coverage.
- WINDOW:TOGGLE_MAXIMIZE now returns the requested state instead of
re-reading isMaximized() right after the call, which races on Linux
window managers where maximize()/unmaximize() complete
asynchronously; the WINDOW:STATE_CHANGED push stays authoritative.
- Skip attaching window-state push listeners on macOS, where the
custom controls never mount and the IPC traffic had no subscriber.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(ui): gate custom window controls on the full bridge surface
Include getWindowState and onWindowStateChange in the
usesCustomWindowControls capability check — the controls rely on both
for initial state and for keeping the maximize/restore glyph in sync
with OS-triggered changes, so a partial bridge should not mount them.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>