* fix(search): keep a trailing space when the trimmed q echo lands after the debounce
The #1432 guard only held while a debounce was still pending. Once it
fired, the router echo of our own trimmed q (replaceUrl navigation)
reset the one-way-bound search box to the trimmed term, deleting the
just-typed trailing space — typing "Bein Sports" collapsed into
"BeinSports".
Applied terms are now always trimmed at the apply choke point (URL sync
and portal stores only ever act on the trimmed form anyway), so the
echoed q compares directly, and the echo guard no longer requires a
pending debounce. Back/forward stays authoritative via the untouched
imperative-trigger check.
Residual part of #1338.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(search): make the trimmed-applied-term invariant structural and update the shell contract doc
Review follow-ups on the echo-guard widening:
- setSearchState now trims too, so URL-sourced terms (deep links with
?q=Bein%20, actor/discover prefills passing raw provider titles) cannot
put an untrimmed term into appliedSearchQuery — previously that path
failed the echo guard's equality check, snapped the box, and dispatched
the portal search twice. Regression spec added.
- docs/architecture/workspace-shell.md item 8 updated: the applied-term
echo is now always ignored, not only while input is still debouncing.
- The facade spec's router mock exposes a mutable navigation trigger so
facade-level tests can exercise the popstate branch.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(search): compare the echoed q in trimmed form in the echo guard
Adoption trims, so a same-page imperative navigation still carrying a
not-yet-rewritten untrimmed q adopts to exactly the applied state —
syncing it could only cancel a pending debounce. Comparing the trimmed
form closes that window.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
---------
Co-authored-by: 4gray <fourgray@proton.me>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
* 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>
* 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>
Rebuilt from #1251 so the group could merge, on top of the transitive-CVE
overrides from #1258. Supersedes #1230 and #1251.
Carries axios 1.16.0 -> 1.18.1, closing seven runtime-scope advisories
including the proxy-credential leak on redirects, and sharp 0.34.5 -> 0.35.3
for the libvips CVEs.
`esModuleInterop` moves to tsconfig.base.json. artplayer 5.4.0 switched from a
Parcel build exposing `module.exports.default` to UMD assigning
`module.exports` directly; the flag was only set in apps/web, so every lib
compiled `import Artplayer from 'artplayer'` to `.default` and got undefined.
Production was never affected — esbuild resolves the ESM entry.
Two packages are deliberately held back, each for its own PR:
- epg-parser ^0.5.0 — grouped as a minor, but 0.x minors are breaking and this
one reshapes the parse output (`channel.name` -> `displayName`, icons/urls
become objects, `credits` becomes role-keyed, dates switch to ISO). Its only
consumer is the uncovered web-backend `/parse-xml` endpoint.
- electron-builder ^26.15.3 — rewrote the snap target, and the resulting snap
cannot start (`command.sh` execs a `desktop-init.sh` that never lands at the
snap root under our core22 strict config). Its two required fixes go with it:
the `engines` node floor from @electron/rebuild 4, and resolving upstream
node-gyp instead of the dropped `@electron/node-gyp` fork.
The custom minimize/maximize/close controls stayed hidden forever after
leaving HTML-element (video player) fullscreen on Windows: window state was
polled at event time, and isFullScreen() can still report the pre-transition
value while 'leave-full-screen' fires, leaving a stale push with no later
event to correct it. The same polling on the companion flag cleared
isMaximized during fullscreen transitions and stuck the maximize/restore
glyph on the wrong icon.
attachWindowStateEvents now seeds the state once at window creation and each
event patches only the flag it names, sending a copy per push. The
enter/leave-html-full-screen variants are wired too.
Regression coverage: app-window-state.spec.ts (9 cases, 6 of which fail
against the old implementation) and an Electron E2E case that toggles HTML
element fullscreen and asserts the controls come back.
Register an "Switch player to Embedded MPV" command in the Cmd+K command
palette so the embedded MPV player can be activated like the other players.
Visibility is gated on an async getEmbeddedMpvSupport() check, mirroring the
Settings dropdown so the command only appears when embedded MPV is usable.
Generalizes the per-command visibility flag from desktopOnly to a `requires`
discriminator ('none' | 'managed-external' | 'embedded-mpv').
Co-Authored-By: Claude Opus 4.8 (1M context) <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>
Adds a "Recently used" section at the top of the command palette (capped at 5,
persisted via StorageMap) and five "Switch player to ..." commands for one-step
player switching from anywhere. MPV/VLC are gated to Electron; the active player
entry is shown disabled. Switching applies to the next playback session.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Entire-Checkpoint: c2cea9c530e6
- Added `XtreamCachedContentScope` type to manage content loading states.
- Updated live stream layout to show a loading skeleton when content is being fetched.
- Introduced styles for loading content in the live stream layout.
- Enhanced unit tests to cover loading states and caching behavior.
- Modified `XtreamWorkspaceRouteSession` to handle cached content loading and state management.
- Improved handling of optional connection values in playlist data.
- Updated workspace context panel to reflect import states and loading conditions.
Entire-Checkpoint: c2cea9c530e6
- Moved Stalker-related components and services from apps/web to libs/portal for better modularity.
- Introduced SQLite DB Worker to handle non-EPG database operations, improving UI responsiveness.
- Updated documentation for the Workspace Dashboard and Shell, detailing current implementation and routing structure.
- Added new EPG fixture scenarios to the Xtream mock server for testing purposes.
- Enhanced the overall architecture documentation to reflect recent changes and improvements.