Both ElectronService.listenOn() and PwaService.listenOn() called
window.addEventListener('message', callback) but neither could remove
the listener afterwards:
- ElectronService.removeAllListeners() called a placeholder
getListenerForCommand() that returned a fresh () => undefined function
on every invocation — never matching the registered listener (with a
comment confessing as much).
- PwaService.removeAllListeners() was a literal `// not implemented`
no-op.
The result was a latent memory leak: every listenOn() call accumulated
a global window listener that nothing could remove. Calling listenOn()
twice for the same command also stacked duplicate listeners.
Track callbacks in a messageListeners Map keyed by command name. On
listenOn(), drop any existing listener for that command before adding
the new one. On removeAllListeners(type), remove either the named
listener or all of them (when type === 'all').
The fix lands proactively: grep finds no current callers in the
renderer, so today's leak is theoretical. But the API is exposed via
the abstract DataService and the placeholder comment explicitly invited
the bug. Closing the footgun is cheaper than discovering it later.
Inspired by matracey/iptvnator@df7e1dc — extends the fix to PwaService,
which had the same bug in even more obvious form.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Entire-Checkpoint: 3f453cc0085a
Channel-list rendering called EpgService.getCurrentProgramsForChannels(),
which forkJoined N getChannelPrograms() Observables — each firing its own
IPC round-trip and its own SQL query. For a 500-channel visible window on
first scroll, that was 500 IPC calls and 500 SELECTs hammering the EPG
table.
Add GET_CURRENT_PROGRAMS_BATCH IPC handler that takes the channel-id
array and runs a single SELECT with WHERE channel_id IN (...) AND
start <= now AND stop >= now. The renderer-side cache and TTL behavior
are preserved; only the network of IPC calls collapses to one. A
fallback path keeps the old per-channel behavior if the preload lacks
the new endpoint.
Per-channel display-name fallback (NOCASE id, then NOCASE display name)
is preserved from handleGetChannelPrograms so behavior matches the
existing single-channel handler.
Inspired by matracey/iptvnator@d25a7e8.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Entire-Checkpoint: 6719280e397b
Three placeholders were rendering at once when a search filter cleared
both the groups rail and the channel list, and they each used a
different visual treatment:
1. Groups rail (.groups-nav-empty) was bare 0.82rem muted text with no
icon and no hint, and routed through CHANNELS.NO_SEARCH_RESULTS
("No channels found") which is wrong copy for the *groups* column.
2. Channel list (.groups-content-empty-state) was the canonical L4
icon + title + hint pattern (correct).
3. Player area (<app-portal-empty-state>) was a 72px icon at 0.2
opacity, which read as ghosted on dark theme.
Bring the groups rail into the L4 family scaled down for the narrow
column: 28px search_off / folder_off icon at 0.5 opacity, 0.85rem
title, 0.78rem hint at 0.65 opacity, centered. Add a CHANNELS.NO_GROUPS_FOUND
i18n key so the copy reflects the column ("No groups found").
Bump portal-empty-state's icon opacity from 0.2 -> 0.35 and title
opacity from 0.5 -> 0.6 so the player-area placeholder feels present
rather than abandoned.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Entire-Checkpoint: 6719280e397b
When a search filter inside a category produces zero results, the grid
@empty fallback used to render the EMPTY_CATEGORY view ("No content in
this category" with the empty-tv illustration). That message was
misleading — the category itself wasn't empty, only the search filter
excluded everything.
grid-list now takes a searchTerm input and branches the @empty fallback:
when a search is active it renders the NO_SEARCH_RESULTS view with the
parameterised "No results found for {term}" title, otherwise it falls
back to EMPTY_CATEGORY as before. category-content-view derives the
searchTerm signal from the ?q= query param and passes it through.
Also: bump the NO_SEARCH_RESULTS icon from a 64px mat-icon to a clamp()
sized icon (120-180px) at 0.5 opacity so it visually balances with the
SVG illustrations used by the other viewTypes, swap the icon to
search_off (struck-through magnifier) so it reads as "no results"
rather than "search now", and fix the EN i18n typo
("change you search request" -> "change your search request").
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Entire-Checkpoint: 2e1a36e2f0f6
Clearing all recents of a given content type fired N concurrent IPC calls,
each opening its own implicit transaction in the recently_viewed table.
For users with hundreds of recently-watched VOD/series rows this added
real overhead even though the UI updates optimistically.
Add a new DB_REMOVE_RECENT_ITEMS_BATCH path end-to-end:
- removeRecentItemsBatch() Drizzle op: one transaction, one prepared
statement reused per row
- Wire through worker → IPC handler → preload → window.electron typings
→ DatabaseService
- UnifiedRecentDataService.removeRecentItemsBatch() groups items by
source. Xtream items go through the new batch IPC. M3U/Stalker items
still go per-playlist because they update a JSON column on the
playlist row, not the recently_viewed table — but they now run in
parallel with the Xtream batch via a single Promise.all.
- Single call site updated: unified-collection-page "Clear all of type"
confirmation handler.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Entire-Checkpoint: 4bfb62f988b7
Replace the dashed-bordered "Ooops" placeholder in the EPG side panel
(rendered when the selected date has no programmes) with the icon +
title + hint pattern already used by groups view, season container,
and portal empty states.
The dashed border was a visual outlier — every other empty state in
the codebase is borderless, and a dashed stroke conventionally signals
a drop-zone or "add new" affordance, neither of which applies to a
passive informational state. The new layout uses a 48px event_busy
icon at 0.5 opacity, a 1.05rem title, and a 0.85rem hint at 0.65
opacity, centered in the panel via flex.
Soften the EN microcopy ("Ooops, EPG is not available for the selected
date" -> "No program guide", which also fixes the "Ooops" typo) and
add a NO_GUIDE_HINT key with localized strings across all 18 locales,
nudging the user toward the date arrows or playlist refresh.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Entire-Checkpoint: 4bfb62f988b7
The inner loop in reorderGlobalFavorites() rebuilt the same
update().set().where() AST per row — up to thousands of times for large
favorite lists. Hoist a prepared statement using sql.placeholder() so
Drizzle generates the SQL string once and SQLite caches the parsed plan.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Entire-Checkpoint: 3d6901072049
SQLite's recommended approach for keeping query plans current: cheap when
nothing needs analyzing, runs incremental ANALYZE on tables/indexes that
have grown significantly since the last run. Wrapped in try/catch since
optimize is advisory and must never block connection close.
Applied to both the main connection and the EPG worker connection.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Entire-Checkpoint: 3d6901072049
Each chunk in a bulk insert/update/delete loop was running as its own
implicit transaction, triggering one WAL commit (and one fsync, even with
synchronous=NORMAL) per chunk-internal statement. Wrap each chunk in a
single Drizzle transaction so the chunk commits as a unit.
Per-chunk transactions (not whole-loop) preserves:
- Cancellation between chunks via checkpointOperation()
- Async progress reporting via reportOperationProgress()
- Bounded write-lock duration (no minutes-long single transaction)
Sites updated:
- content.operations.ts: Xtream content bulk insert + clearXtreamImportCache deletes
- playlist.operations.ts: upsertAppPlaylists loop + cascade delete chunks
- favorites.operations.ts: reorderGlobalFavorites nested update loop
- xtream.operations.ts: cascade content/category deletes + favorites/recently-viewed restore
Highest impact: Xtream content imports (10k-100k+ rows) and M3U playlist
upserts. EPG worker already used transactions correctly — unchanged.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Entire-Checkpoint: 3d6901072049
Pair the existing journal_mode=WAL with the companion pragmas it needs to
actually pay off, and reduce read latency on hot query paths.
- synchronous=NORMAL: ~2-10x faster writes when paired with WAL. Safe — only
risks losing the last committed txn on power loss; DB stays consistent.
- cache_size=-64000: 64MB page cache (default is 2MB).
- temp_store=MEMORY: keep sort/group temp tables in RAM.
- mmap_size=268435456: 256MB memory-mapped I/O for reads.
Applied to both connection sites: the main read-write/read-only connection
in libs/shared/database and the EPG worker connection in electron-backend.
The per-connection pragmas (cache_size, temp_store, mmap_size) apply to the
read-only agent-backend connection too.
Inspired by matracey/iptvnator@4ad8f88; ported manually since the file has
diverged significantly and the worker connection didn't exist in that fork.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Entire-Checkpoint: 3d6901072049
Use Map and Set lookups for M3U favorite aggregation, dashboard global favorite mapping, and channel-list favorite/group resolution.
Remove the duplicate NgxIndexedDB provider registration and add regression coverage for duplicate URL and earliest-match behavior.
Entire-Checkpoint: c2cea9c530e6
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
Two issues kept the scrollbar invisible on the global Favorites and
Recently viewed live-tv lists:
1. The wrong files. The visible Favorites/Recent pages render through
<app-global-favorites-list> from portal/shared/ui (and the
workspace-shell variant), not the channel-list-container's tabs.
Apply .app-scrollbar to both of those components.
2. Specificity. Component CSS gets an Angular [_ngcontent-xxx]
attribute selector, so `.nav-list { overflow-y: auto }` resolves to
(0,2,0) — same as the doubled `.app-scrollbar.app-scrollbar` and
wins on source order (component CSS loads after globals). Mark
overflow-y: scroll !important on the utility so the contract holds:
applying .app-scrollbar always reserves the gutter.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Entire-Checkpoint: 8ae011808369
The favorites and recent views had local #id { overflow-y: auto }
which has higher CSS specificity than the global .app-scrollbar class.
On macOS, overflow-y: auto means overlay scrollbars that auto-fade,
so the gutter and themed thumb were never reserved.
Bake overflow-y: scroll into the utility itself (its whole purpose) and
drop the now-redundant overflow-y: auto from the two local rules.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Entire-Checkpoint: b2d2124cd992
Move the always-visible themed scrollbar from epg-list into a global
.app-scrollbar utility class in styles.scss. Apply it to the All
channels and Groups view virtual-scroll viewports so the channel list
gets the same persistent gutter and themed thumb as the EPG list.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Entire-Checkpoint: c470abf78379
Drop the gradient + bevel + drop-shadow chrome from EPG rows so the panel
speaks the same flat-by-default visual language as the channel list. Add
--app-live-color token, tokenise the LIVE badge, tighten typography
(weight 600, text-wrap balance/pretty), and bump the info icon to 32px
for Fitts-compliant hit area.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Entire-Checkpoint: 4cc5371f549e
- Implemented a TypeScript script that generates original fictional demo artwork for IPTVnator release screenshots.
- The script supports multiple artwork kinds (poster and backdrop) and qualities (low, medium, high, auto).
- It includes functionality for dry runs, generating artwork, manifest creation, and validation of generated assets.
- Integrated with OpenAI's image generation API to create artwork based on provided prompts and fixture data.
- Added a manifest file to track generated assets and their metadata.
- Ensured output directories are created as needed and included error handling for API requests.
Entire-Checkpoint: c6e522b4276c
- Expanded EmptyStateType to include 'welcome-dashboard', 'welcome-sources', 'no-data'.
- Introduced FeatureCard and SourceCard interfaces for better structure.
- Added FEATURE_CARDS and SOURCE_CARDS constants for feature and source card configurations.
- Updated empty-state.component.ts to handle new types and actions.
- Created responsive styles in empty-state.responsive.scss for better mobile support.
- Added theme styles in empty-state.themes.scss for light mode.
- Developed specific styles for welcome dashboard and sources in empty-state.welcome-dashboard.scss and empty-state.welcome-sources.scss.
- Updated recent-playlists.component.html to utilize new empty state types.
- Modified recent-playlists.component.ts to support new playlist actions.
- Introduced PlaylistFileImportService for handling playlist file imports.
- Updated downloads.component.html and downloads.component.ts to integrate new empty state handling.
- Enhanced unified-collection-page.component.html and unified-collection-page.component.ts to utilize empty state for no results.
Entire-Checkpoint: c6e522b4276c
- Added support for M3U file and URL integration across various languages.
- Introduced Xtream Codes and Stalker portal connection options.
- Enhanced value propositions for live TV, VOD, multi-source library, and offline downloads.
- Updated dashboard welcome messages and descriptions for better user guidance.
- Improved file upload instructions and error handling for playlist imports.
- Added global favorites and recent viewing history sections with appropriate messages.
- Enhanced download functionality with clear instructions and feedback for users.
Entire-Checkpoint: c6e522b4276c
- Updated confirmation dialog messages to provide detailed consequences of deleting playlists.
- Added backup hints and progress messages for better user experience during playlist deletion.
- Ensured consistency in language across all supported translations (de, el, en, es, fr, it, ja, ko, nl, pl, pt, ru, tr, zh, zhtw).
Entire-Checkpoint: c6e522b4276c