* fix(security): harden Electron IPC against MITM, SSRF, path and injection risks
S1 TLS: validate certs by default on playlist/EPG fetches (opt-out via IPTVNATOR_ALLOW_INSECURE_TLS); new util/secure-https.ts.
S2: write-file IPC restricted to save-dialog-authorized paths.
S3: XTREAM_PROBE_URL guarded by assertRemoteUrlAllowed + maxRedirects:0; new events/url-safety.ts (+19 tests).
S4: EPG titles rendered via interpolation, not [innerHTML].
S5: downloads reveal/play limited to recorded download paths.
S6: Stalker cmd encoded (slash-preserving) to block query injection.
EPG-worker and Stalker fetches reject file://-style/credentialed URLs; LAN/self-hosted targets remain allowed.
* perf(player): lazy-load web video players via @defer
Wrap Video.js/HTML5/ArtPlayer in @defer (on immediate) so video.js, hls.js,
artplayer and mpegts.js split into a deferred chunk loaded on first playback
instead of eagerly on the player route. Embedded MPV (native) stays eager.
Spec uses DeferBlockBehavior.Playthrough.
* fix(player): remove leaked HTML video listeners on destroy
volumechange used a mismatched removeEventListener reference, while
loadedmetadata and timeupdate were never removed at all. Bind all three to
stable handler fields used for both add and remove, and add a teardown
regression test asserting each listener is detached on destroy.
* refactor(dashboard): extract pure navigation helpers from DashboardDataService
Move the 8 stateless link/navigation-state/type-kind helpers into a new
dashboard-navigation.util.ts so the routing logic is independently testable and
the 1260-line god-service shrinks. DashboardDataService keeps the public methods
as thin delegators (facade) so the public API and the single consumer
(workspace-dashboard-rails) are unchanged. First slice of the DashboardDataService
decomposition; verified by the existing service spec (33/33) and the app typecheck.
* fix(review): address PR feedback (IPv6 link-local, write-path cap, @defer placeholder)
- url-safety: broaden IPv6 link-local detection to the full fe80::/10 range
(fe80:: through febf::), not just the fe80:: prefix (+ regression tests).
- playlist.events: cap authorizedWritePaths (evict oldest past 32) so a save
dialog opened without a following write cannot accumulate entries until restart.
- web-player-view: add a @placeholder to each @defer (on immediate) player block
to avoid the one-frame blank/layout-shift before the chunk resolves.
* fix(security): close Electron network and download gaps
* test(downloads): cover cancellation and restart cleanup
* fix(downloads): address Greptile review gaps
* test(security): reproduce remaining Greptile findings
* fix(security): close remaining Greptile findings
* test(downloads): reproduce early database queue stall
* fix(downloads): release queue after setup failures
* test(downloads): reproduce completion queue stall
* fix(downloads): release queue after completion failures
* 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>
- merge origin/master into PR #964 and keep embedded MPV test on the isolated playback sub-entrypoint
- centralize EPG capability through DataService.supportsEpg and update PWA web-e2e expectations
- split BrowserAccessError copy between Electron and PWA diagnostics
The Zoom/Filter/Search Programs buttons in the Multi-EPG header
hard-coded English matTooltip strings, so they never localized — even
when the rest of the toolbar (Close, Previous/Next day) used keys. Adds
six new EPG.* keys (ZOOM_IN, ZOOM_OUT, FILTER_CHANNELS, CLOSE_FILTER,
SEARCH_PROGRAMS, CLOSE_PROGRAM_SEARCH), wires both [matTooltip] and
[attr.aria-label] through `| translate`, and fans the strings out to
all 17 non-English locales via the i18n-fill pipeline.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Radio audio-player buttons (skip-prev, play/pause FAB, skip-next, mute,
volume slider) had zero matTooltip or aria-label. The UX audit flagged
this: "Skip-prev / Skip-next — what do they skip? In radio, is the
next 'track' the next station? Next in favourites? Random? Without a
tooltip the buttons trade silently." Same critique applied to mute
and play/pause for screen-reader users.
Add hover tooltips and screen-reader labels on all 5 controls. Skip
prev/next say "Previous station" / "Next station" so the behaviour is
explicit (they walk the active filtered list). Play/Pause flips its
label with state. Mute/unmute flips when isMuted() || volume === 0.
Volume slider gets its own label so the slider thumb is announced
correctly.
i18n: 7 new AUDIO_PLAYER.* keys translated across all 17 non-English
locales by per-locale agents; all 18 locales pass coverage and
placeholder integrity checks.
52/52 ui-playback tests still pass.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Four UX-audit fixes for the Multi-channel EPG view:
1. Theme harmonization
- $bg-deep / $bg-surface / $bg-elevated / $bg-hover now resolve through
--app-content-bg / --app-widget-bg / --app-widget-header-bg /
--app-card-hover-bg. Multi-EPG was the only screen that switched to
near-black (#0a0a0f) instead of sitting on the app's dark surface, so
the audit described it as "visually detaching from the rest of the
app". It now inherits the same dark graphite chain.
- Cleaned up 29 remaining hard-coded rgba(139,92,246,…) purples and
rgba(0,212,170,…) teal accents that survived the earlier color
unification commit. Purple shadows/glows are gone; the cyan glow
used by the now-line is now driven by the unified $5cd6ff token.
2. Channel column widened 140px → 180px
- Long names like "13th Street Universal HD" no longer truncate to 5
characters. Logo size dropped to 24px so the name actually has room.
The label allows up to two lines and wraps cleanly on long titles.
- Responsive breakpoint mirrors the change: 100px → 140px at <768px.
3. Now-line time badge
- New `currentTimeLabel` computed signal renders an "HH:MM" pill
pinned to the top of the now-line. Recomputes on the same 60s tick
as the line position so they always stay in lockstep.
4. "Airing now" program highlight
- New `isProgramAiringNow(program)` returns true when the now-line's
x-coordinate falls inside [startPosition, startPosition + width].
Programs that match get a .is-now class → 1.5px cyan border. Users
can now see what's on every channel at a glance without tracing the
line down each row.
Toolbar harmonization (audit item 3 for this screen) intentionally
deferred — that's part of the app-wide toolbar unification work.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Swap M3 primary palette from violet to azure so checkboxes, radio
buttons, raised CTAs, and active states read as the same blue used by
the rail selection token. Cascading template + SCSS updates align the
remaining hand-rolled surfaces (Add Playlist dialog, VOD play button,
radio player, multi-EPG, empty-state CTAs) with the unified system.
LIVE stays red (broadcast role), cyan stays on EPG "now" indicator,
green stays on completed-download — semantic colors keep their meaning;
only the indiscriminate accent uses get folded into the blue primary.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(playback): handle IPTV proxy URLs without file extensions
IPTV proxy servers (Acexy, Xtream Codes, etc.) use query-string URLs
like `https://proxy.example.com/ace/getstream?infohash=abc` that return
raw MPEG-TS streams without a file extension in the path.
`getExtensionFromUrl()` was returning garbage for these URLs (e.g.
`com/ace/getstream` from splitting on dots across path segments). All
four player backends then misrouted the stream — typically to HLS.js
which failed because the response is raw MPEG-TS, not an HLS manifest.
Root fix: rewrite `getExtensionFromUrl()` to extract the extension from
the last path segment only, returning `undefined` when there is no
dot-separated extension.
Player adjustments: treat `undefined` extension as MPEG-TS (the most
common format for live IPTV proxy streams) across all four backends
(HTML5, ArtPlayer, VideoJs, web-player-view).
* test(m3u-utils): cover URL extension parsing
* fix(playback): preserve query-declared stream extensions
---------
Co-authored-by: 4gray <serega05@gmail.com>
* fix(playback): avoid redundant VideoJS source resets
* fix(playback): clean up cleared VideoJS sources
* refactor(playback): use signal APIs in VideoJS player
closes#608
Audit followup to the support-loop and volume-restart fixes. Two more
effects pulled in transitive signal deps that would have caused the same
class of regression the next time the surrounding helpers grew.
1. Component session-fan-out effect: the body called
scheduleControlsHide(), which reads isPlaying/menus.anyOpen/statusLabel/
controlsVisible. Those became tracked deps of the effect, so opening a
popover, pausing, or hovering re-ran the whole body — re-emitting
timeUpdate. If a parent ever wires timeUpdate back into
playback.startTime as a "resume where I left off" feature, this would
have been the next stream-restart bug. Wrap the side-effect block in
untracked() so the effect only listens to session changes.
2. Controller stalled-tracker effect: tracked the full session signal
even though only status was needed. The session payload updates ~2 Hz
during playback (positionSeconds advances), making the effect re-run
constantly to call a no-op. Add a sessionStatus computed and track
that instead — fires only on real status transitions.
No behavior change for current users; both fixes are preventative.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Entire-Checkpoint: f957cd9849e0
Same root cause as the loading-loop fix: the session-creation effect
read this.volume() while building startSession's initialVolume argument,
which made volume a tracked dependency of the effect. Each volume tick
re-ran the effect, the cleanup disposed the active session, and a fresh
one was created — which for VOD/series meant restarting playback from
the beginning.
Read volume via untracked() inside the effect. The value is only needed
once at session creation; subsequent volume changes flow through
controller.applyVolume() and never go near the effect.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Entire-Checkpoint: f957cd9849e0
Regression introduced in the bundle 3 refactor. After calling
prepareEmbeddedMpv, the controller wrote the response back into
this.support — but the component's session-creation effect tracks
this.support(), so updating it cleaned up the just-created session and
ran startSession again, which prepared again, set support again, and so
on. Net effect: endless "Loading stream…" spinner because every session
was disposed before MPV could finish loading the file.
The initial loadSupport() in the constructor already populates support
(including capabilities). Drop the redundant set in startSession; leave
a comment explaining why so it does not get re-added.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Entire-Checkpoint: f957cd9849e0