Commit Graph
2864 Commits
Author SHA1 Message Date
4grayandClaude Fable 5.1 c00ebe69a2 fix(epg): let coverage reject on lookup failures and compare whole entries for the active guide row
EpgQueryService.getChannelMetadata swallowed database errors into {}, so the
guide's coverage read could publish an empty set after a transient failure;
the guide now uses the strict resolveChannelMetadata (getChannelMetadata is
the fail-soft wrapper around it). The active guide row is matched on the
whole channel entry (all fields except the reducer-rewritten epgParams)
before widening to url and id, so copies that differ only in playback
headers or logo are told apart.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-06 17:16:23 +02:00
4grayandClaude Fable 5.1 a49e56f5de fix(epg): tell duplicate guide rows apart by group, keep G out of dialogs, use prototype-safe answers
The store spreads the selected channel, so the active row is matched by
id, url, group and name before widening; G no longer closes the guide from
a dialog or menu; guide answers use null-prototype records so a key named
__proto__ stays an own property.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-06 17:02:27 +02:00
4grayandClaude Fable 5.1 378e57add7 fix(epg): clear guide search on scope change, match the active duplicate by url, keep failed coverage unknown
Search hits carry scope-local row ids, so a scope change drops them.
Two playlist entries can share an id but not a stream, so the active row
is matched by id + url before falling back to the id. A failed coverage
query now rejects instead of answering an empty set, which the guide
already treats as "coverage unknown" (every row stays visible).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-06 16:46:43 +02:00
4gray 2ab44ebb19 Merge remote-tracking branch 'origin/master' into claude/multi-epg-view-feature-1efd4d 2026-09-06 15:20:25 +02:00
4grayandClaude Fable 5.1 71a1a76b5a docs(epg): describe guide row ids as scope-local
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-06 15:20:25 +02:00
4grayandClaude Fable 5.1 1425279a0a fix(epg): keep guide keys on the grid, reconcile focus with filtered rows and wrap the toolbar
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-06 15:19:38 +02:00
4grayandClaude Fable 5.1 503f009a1f fix(m3u): make guide row ids collision-proof and gate the G shortcut
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-06 15:13:54 +02:00
4grayandClaude Fable 5.1 0a2373f192 feat(portals): fold live TV panels in nested levels with a category dropdown (#1556)
## Summary

Live TV panels now fold from the outside in, in three nested levels, instead of one toggle that hid the categories rail and the channel list together:

1. **Categories + channels + player** (browse, unchanged).
2. **Channels + player** — a new `chevron_left` in the categories rail header hides only that rail. The channels header then turns its title into a **category dropdown** that opens the same shell panel as a popover (search, sort, counts, selection are one implementation), plus a `chevron_right` that brings the rail back.
3. **Player only** — the channels header chevron, as before. The floating restore handle and `Cmd/Ctrl+B` return to the level the user collapsed from, not always to level 1.

Every level is restored as stored, per surface (`live-sidebar-state:<surface>`, from #1555): a hidden rail is discoverable through the workspace header toggle and the hidden-list empty state that #1555 added, so this PR no longer needs its original "player-only never restores" rule. The level `Cmd/Ctrl+B` comes back to is seeded from the restored level and kept for the session.

## Design notes

- Nested levels rather than two independent booleans: "channels hidden, categories visible" makes no sense since a category click has to bring the channels back anyway. The model follows the outside-in collapse of three-pane apps (Mail, Slack, Plex).
- The categories rail folds at level 2 **only while a category is selected**: the live root ("All Items" grid) has no channels header to host the way back, so folding there would strand the user. Level 3 folds it regardless, because the floating restore handle lives in the content area.
- `LIVE_CATEGORIES_POPOVER` (`@iptvnator/portal/shared/util`) is the DI bridge: the workspace shell provides `WorkspaceLiveCategoriesPopoverService` (CDK overlay hosting `WorkspaceContextPanelComponent` in `presentation="popover"`), the Xtream and Stalker live layouts inject it optionally and keep their plain heading without a provider.
- M3U and the unified live tab have no categories rail and treat level 2 like level 1; their code is untouched.

## Merged with #1555 (per-surface rail state)

#1555 landed while this PR was open and reworked the same service: state per surface (`m3u` / `portal` / `collection`), a workspace header toggle, the hidden-list empty state, and the legacy shared key forgotten on startup. This PR keeps that model and layers the three levels onto the `portal` surface (`areCategoriesHiddenFor`, `hideCategories` / `showCategories` / `collapse` / `expand` per surface; `toggle(surface)` returns to the level the surface collapsed from). "Show playing channel" uses `expand('portal')` so it keeps a deliberately hidden categories rail folded, and the category sort preference moved to `PortalCategorySortStateService` so the popover copy of the context panel and the retained rail agree.

## Also fixed along the way

- The channels header showed "Channels" instead of the category name: provider category ids are strings, the selection is numeric. Compared via `String()` now.
- A collapsed context panel left a 22px padding strip beside the channels rail.
- The panel toggle labels said "Hide channels list" while also hiding categories; labels and tooltips are honest now (8 new i18n keys, all 18 locales).



Docs: `docs/architecture/iptvnator-ui-guidelines.md` ("Collapsible Live Sidebar" rewritten), `docs/architecture/workspace-shell.md`. Release note: `.changes/portals-live-panel-collapse-levels.md`.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-06 14:42:59 +02:00
4grayandClaude Fable 5.1 e00e5d6476 refactor(epg): split the guide shell, add a roving focus model and offset-aware search times
The shell component now owns rows, focus and the viewport only: the day,
zoom, density, filters, clock and day geometry move to EpgGuideViewState,
and every programme-dialog entry point to EpgGuideDialogController.

Keyboard navigation is reachable by assistive technology: exactly one grid
cell carries tabindex="0" (the focused cell, else the playing row's channel
cell, else the first row's), the guide moves DOM focus with it after each
handled key, a click hands the roving index to the clicked cell, and the
viewport, rows and cells expose grid/row/gridcell roles.

Search results were formatting raw provider instants, so they ignored the
EPG display offset; they go through getProgramTimeMs like every other time
the guide renders.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-06 14:34:39 +02:00
4grayandClaude Fable 5.1 cb599c7bb4 fix(m3u): stabilise guide row identity, seed the sidebar group and provide translations in every player fixture
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-06 14:31:04 +02:00
4grayandClaude Fable 5.1 465c5dc5d1 perf(epg): let the guide window predicate use the programme time index
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-06 14:23:02 +02:00
4grayandClaude Fable 5.1 c28555e516 fix(epg): match guide favorites by channel URL and skip re-activating the playing row
Favorites are persisted by channel URL (FavoritesActions.updateFavorites),
so the Favorites scope compared the wrong key; the id stays as a legacy
fallback. A double-click arrives as click, click, dblclick and each
activate restarts playback, so the guide now leaves the already-playing row
alone and the commit path only closes.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-06 14:03:45 +02:00
4grayandClaude Fable 5.1 49fdd40c9d Merge origin/master into the programme guide branch
Adopts the compact icon-only timeline toolbar (#1557): the Guide button is
now an icon-only `epg-timeline__iconbtn` with its label in the tooltip and
aria-label, so the unused EPG.TIMELINE.GUIDE key is dropped from all locales.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-06 13:59:04 +02:00
4grayandClaude Fable 5.1 7270520100 chore(epg): tidy guide docs, palette gating and the unbound output
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-06 13:47:07 +02:00
4grayandClaude Fable 5.1 6f8bc3a529 test(e2e): cover the programme guide flow
Imports a two-channel playlist with XMLTV, opens the guide from the
timeline toolbar and asserts the row list, the "Only with EPG" filter, a
channel switch that keeps the guide open, the hidden-but-mounted sidebar,
and that the player element survives both the mode and channel switches.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-06 13:40:47 +02:00
4grayandClaude Fable 5.1 1fc8e1cf73 fix(m3u): keep the sidebar mounted while the guide is open
Guide mode wrapped the sidebar in `@if (!guideOpen())`, so opening the
guide destroyed `app-channel-list-container`, whose `ngOnDestroy`
dispatches `resetActiveChannel()`. That cleared the active channel, which
unmounted the block hosting `app-epg-guide` and tripped the
`!canOpenGuide()` effect into closing the guide again: the guide never
appeared and the page dropped to "Please select a channel".

The sidebar now stays mounted and is hidden with
`.sidebar--guide-hidden` plus `inert`, so it is neither focusable nor read
by assistive technology while the guide owns the layout. Hiding also
preserves the channel list's scroll position across guide toggles.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-06 13:40:47 +02:00
4grayandClaude Fable 5.1 8d73286d93 fix(m3u): let the guide own the keyboard and gate its entry points
While the programme guide is open the docked player carries
`data-player-shortcuts-suspended`, which `ControlsShortcuts` now honours
alongside `[inert]` — the arrows moved the player's volume instead of the
guide's row focus. The external-player strip loses its Collapse toggle
(nothing to reveal, no preference to write), the header action and its
palette command report `disabled` when the guide cannot open, the docked
strip derives its programme from the active channel's own schedule instead
of the retained NgRx value, switching playlists closes the guide, and the
collapsed strip can reach 48 px on phones.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-06 13:35:51 +02:00
4grayandClaude Fable 5.1 376fde096d i18n(epg): translate the programme guide
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-06 13:33:07 +02:00
4grayandClaude Fable 5.1 e5688ccd28 docs(epg): document the programme guide and its release note
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-06 13:26:08 +02:00
4grayandClaude Fable 5.1 5c2cf3f345 refactor(epg): remove the multi-EPG overlay and the channel-range IPC
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-06 13:25:37 +02:00
4grayandClaude Fable 5.1 e9359750bf fix(epg): scope guide keys to the grid, clip the now-line and re-measure on resize
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-06 13:19:54 +02:00
4grayandClaude Fable 5.1 44ad33beb9 feat(m3u): open the programme guide in place with a docked player
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-06 13:16:18 +02:00
4grayandClaude Fable 5.1 e20b9cc7a0 fix(m3u): guard the guide's initial group scope and track language changes
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-06 13:08:28 +02:00
4grayandClaude Fable 5.1 07af71e49d feat(m3u): adapt the playlist channel list to the guide contract
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-06 13:02:00 +02:00
4grayandClaude Fable 5.1 688fd4584d feat(epg): add a Guide button to the timeline toolbar
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-06 12:58:23 +02:00
4grayandClaude Fable 5.1 44e421103e feat(epg): add the programme guide grid components
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-06 12:53:16 +02:00
4grayandClaude Fable 5.1 5f86631786 fix(epg): make guide programme cache robust to first-run effects and coverage failures
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-06 12:40:12 +02:00
4grayandClaude Fable 5.1 36be2cbb58 feat(epg): add guide keyboard navigation controller
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-06 12:34:49 +02:00
4grayandClaude Fable 5.1 9f33706404 feat(epg): cache guide programmes per day with batched loading
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-06 12:32:23 +02:00
4grayandClaude Fable 5.1 19aee624f7 fix(epg): make guide geometry DST-safe and tighten the contract
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-06 12:30:04 +02:00
4grayandClaude Fable 5.1 35354c85c9 docs(epg): guide search hits carry a row id
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-06 12:22:41 +02:00
4grayandClaude Fable 5.1 53df760cd8 fix(epg): key guide IPC answers by trimmed, present keys only
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-06 12:21:30 +02:00
4grayandClaude Fable 5.1 59f27d1549 feat(epg): add guide source contract, day layout maths and preferences
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-06 12:15:18 +02:00
4grayandClaude Fable 5.1 2701a82905 docs(epg): separate coverage chunk size in the guide plan
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-06 12:13:13 +02:00
4grayandClaude Fable 5.1 0dcfba7045 fix(live-tv): keep a hidden channel list discoverable and scoped per surface (#1555)
* fix(live-tv): keep a hidden channel list discoverable and scoped per surface

The second report in #1458 ("all channels disappear after clearing the
playback history, reset does not bring them back") was not data loss: the
history write never touches playlist items. The reporter's screenshot shows
a collapsed channel rail, a state persisted under one localStorage key
shared by the M3U player, the Xtream/Stalker live layouts and the
favorites/recent live tab. It survived restart, "Remove all playlists" and
re-import, and the only way back was a 32px chevron or Ctrl/Cmd+B.

- LiveLayoutSidebarStateService keeps the state per surface (m3u / portal /
  collection) under live-sidebar-state:<surface>; the M3U player now goes
  through the service instead of its own signal. The legacy shared key is
  forgotten on startup and never read, so the update itself restores the
  list for everyone who got stuck.
- The workspace header renders a view_sidebar toggle on every route that
  renders its own rail (M3U all/groups, Xtream live, Stalker itv/radio), so
  the control exists in both states instead of disappearing with the rail.
  Collection pages keep their own toggle beside the content switch.
- While the rail is collapsed and nothing plays, every live host shows
  app-channel-list-hidden-state (title, shortcut hint, full-size "Show
  channels list" button) instead of asking to pick from a list that is not
  on screen. app-portal-empty-state gained optional hint/action inputs.
- New LAYOUT.CHANNELS_LIST_HIDDEN(_HINT) strings in en plus 18 locales.

Tests: service, empty-state, hidden-state and header component specs, a
separate video-player-sidebar spec (the main M3U spec sits at the test
line budget), and an Electron E2E covering history clearing, restore via
button/header/shortcut across restart and re-import, per-surface scoping
against an Xtream portal, and legacy-key cleanup.

Refs #1458

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* test(live-tv): mirror the EPG offset setting in the sidebar spec mock

Master's player reads `resolvedEpgOffsetMinutes` from the settings store; the
new sidebar spec was cloned from the movie-gate harness before that field
landed, so its playing-channel case threw inside the EPG effect.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* test(web-e2e): scope the Stalker radio rail toggles to the rail

The workspace header now carries a second "Hide/Show channels list" toggle,
so the role+name locators matched more than one button and tripped
Playwright's strict mode. Target the rail's own chevron and the floating
restore button, and assert the header toggle mirrors the state.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* fix(live-tv): honour Cmd/Ctrl+B on collection pages and hide the header rail toggle on phones

Codex review follow-ups on #1555:

- The hidden-list state advertises Cmd/Ctrl+B, but the favorites/recent
  collection page had no handler; only the routed M3U/Xtream/Stalker live
  layouts did. The page now toggles the collection surface while its live
  tab is on screen, with the same typing/inert guards as the other hosts.
- At the phone breakpoint the header already holds the drawer toggle,
  switcher, search and Add; the live rail is a bottom drawer with its own
  toggle there, so the header rail toggle is hidden below 640px.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* fix(live-tv): migrate the live navigation helpers to the per-surface sidebar API

master (#1554) added `XtreamLiveChannelNavigationService` and
`stalker-live-navigation.ts`, which expand the rail through
`sidebar.setState('expanded')` on the pre-split signature. Point them at the
`portal` surface and update their specs; drop the now-unused hidden-state
stub from the Xtream layout spec, which master pushed to the max-lines
budget.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-06 11:48:58 +02:00
4grayandClaude Fable 5.1 f6ac75b8db feat(epg): expose guide programme and coverage reads over the bridge
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-06 11:48:13 +02:00
4grayandClaude Fable 5.1 9a455048b6 fix(epg): render the guide predicate in tests and document its scope
Correct the guide query's JSDoc: it runs one query accepting the union
of requested-source and unsourced legacy rows, unlike EpgQueryService's
two-query scoped-then-legacy fallback. Replace the hand-maintained
plain-SQL twin of the Drizzle overlap predicate with a rendered copy of
the real predicate (SQLiteSyncDialect().sqlToQuery) in the spec, add a
source-scoping case, and drop the now-redundant operator-sequence test.
warnIfTruncated reuses uniqueTrimmedStrings and names which read
(programme/coverage) was truncated. Rename epg-query.service.ts's local
EpgProgramRow to EpgProgramSelectRow so it isn't confused with the
shared EpgProgramRow type, and document getProgramCoverage like its
sibling.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-06 11:38:41 +02:00
4gray ddf811c6de fix(epg): harden guide query scoping, caps and row mapping
- Scoped guide programme/coverage queries now include legacy
  (unsourced) rows via source_url IN (...) OR IS NULL OR '',
  mirroring EpgQueryService's legacy fallback.
- getProgramsForChannels/getProgramCoverage build their result from
  the normalized, capped window.channelIds instead of the raw
  request, so a key cut by the cap is absent rather than [] — an
  invalid window now returns {}. Truncation logs counts only.
- Split the 100-channel guide cap from a new 2000-key coverage cap,
  and cap sourceUrls at 50; normalizeGuideWindow takes the cap as a
  parameter and moved (with guideWindowOverlapSqlText) into
  epg-guide-window.util.ts.
- Extracted shared row mapping (toEpgProgramFromRow/isValidEpgProgram)
  into epg-program-row.util.ts, used by both EpgQueryService and
  EpgGuideQueryService so invalid start/stop rows are dropped
  identically in both.
- Added a real-SQLite-backed test for the overlap predicate's exact
  text, plus per-key array copies in the response.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-06 11:23:24 +02:00
4grayandClaude Fable 5.1 4300cb0c46 feat(epg): add window-scoped guide programme queries
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-06 11:05:12 +02:00
4grayandClaude Fable 5.1 2bac120eff docs(epg): add programme guide implementation plan
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-06 11:01:24 +02:00
4gray 436825bdec fix(xtream): try advertised TS after initial web HLS HTTP failure (#1558)
* fix(xtream): try advertised TS after initial web HLS HTTP failure

* refactor(playback): extract fullscreen channel panel state

* test(xtream): keep synthetic media within the mock project
2026-09-06 11:00:09 +02:00
4grayandClaude Fable 5.1 ef2418a2c5 feat(epg): compact timeline toolbar with icon-only Now and cycling zoom (#1557)
* feat(epg): compact timeline toolbar with icon-only Now and cycling zoom

Replace the 96px zoom range slider with a single icon button that cycles
day overview → by hour → detailed, make the Now button icon-only (label in
tooltip + aria-label), and add Ctrl/⌘ + wheel zoom around the cursor on the
ribbon (preventDefault keeps Chromium from page-zooming the same gesture).
Zoom math moves into TimelineZoomController so the component stays under
the max-lines limit. The channel/programme heading no longer carries a 60%
width cap and takes the space the controls free up.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* fix(epg): coalesce wheel-zoom anchoring and make the e2e zoom reset deterministic

- TimelineZoomController keeps a logical pending scrollLeft across a burst
  of wheel/pinch events and applies it in one animation frame, so every
  event anchors on the same minute instead of the stale DOM position
- the e2e settles on the detail preset from whichever band the wheel
  landed in (contrast checks need the block's time line, hidden at
  narrower tiers)
- zoom tooltip advertises ⌘/Ctrl per the repo's shortcut convention

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-06 10:34:28 +02:00
4grayandClaude Fable 5.1 304f0952f7 docs(epg): add programme guide redesign spec for the M3U host
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-06 10:32:51 +02:00
4gray 61b06b9f31 fix(portals): preserve live channel navigation while browsing (#1554)
* fix(portals): preserve live channel navigation while browsing

* test(portals): await media source assertion in remote E2E

* fix(xtream): capture destination queue for live auto-open
2026-09-06 10:22:07 +02:00
4gray 5a8c5ca4a4 fix(stalker): keep live search within the selected category (#1552)
* fix(stalker): keep live search within the selected category

* test(stalker): assert retained video ownership without source timing

* test(stalker): distinguish paged All Items from the initial cache grid

* fix(stalker): reveal remote selections in uncached search results

* test(stalker): wait for category rows and retain settled playback
2026-09-06 09:05:21 +02:00
4gray 5febe28eba fix(web-backend): validate and pin provider redirect hops (#1553)
* fix(web-backend): validate and pin every provider redirect hop

* fix(web-backend): separate provider metadata from connection authority
2026-09-06 08:59:28 +02:00
4gray 9de480826c fix(epg): remove cached XMLTV data after source deletion (#1548)
* fix(epg): remove cached XMLTV data after source deletion

* fix(epg): close source reconciliation review races

* fix(epg): serialize cleanup with replacement imports

* fix(epg): report retired worker exits as cancellations

* fix(epg): preserve source metadata through cache cleanup

* refactor(epg): separate worker runtime and import lifecycle

* fix(epg): skip cleanup for unchanged source settings

* fix(epg): cancel retired error rows and pending retries

* fix(epg): redact diagnostics and mirror committed settings after cleanup errors

* fix(epg): preserve metadata writer order independently of timestamps
2026-09-06 07:32:30 +02:00
4grayandClaude Fable 5.1 baba0529ef feat(website): rebuild the hero, features, download and support sections (#1551)
* feat(website): rebuild the hero, features, download and support sections

Third landing redesign PR: the home page now speaks the app's own
language instead of a template's.

- Hero: left-aligned headline, a primary button that follows the
  visitor's OS (server-rendered fallback goes to /download/), an "All
  platforms" button and a text link for self-hosting. Project numbers
  (stars, downloads, languages, license) are set in the page's own
  typography from the GitHub API at build time (`lib/github-stats.ts`);
  anything the build cannot resolve is left out rather than faked. A
  small "Now playing" card with fictional demo content replaces the
  mascot, badges and social chips; the screenshot is lit by a blurred
  copy of itself, like the player's ambient mode.
- Features: a bento of interface fragments (EPG rows with progress,
  posters with a resume bar, a download queue with a REC dot, a remote
  D-pad) instead of numbered cards with icon watermarks and a marquee.
  The marquee CSS is gone.
- Download: one row per platform with the release's file names and a
  "Detected" badge for the visitor's OS (`lib/detect-platform.ts`),
  package-manager commands in the same panel.
- Support becomes a single row; the disclaimer moves into the footer,
  which also gets the mascot and a link list.
- Home sections drop the decorative eyebrows and the divider rules; the
  unused broadcast-wave, support-signal and watermark illustrations are
  removed.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* test(website): cover the rebuilt home sections and the OS-aware download CTA

`website-home-sections.test.mjs` (in the website test target) reads the
built home page — generic hero CTA to /download/, project facts, no
badge images or dashed borders, every feature page linked from the
bento, one download row per platform with release file names and the
Detected badge hidden, the disclaimer and mascot in the footer — and
then drives it in Chromium under Windows, macOS, Linux and Android user
agents: the primary button and the Detected row follow the platform,
the phone keeps the generic markup, and the copy button writes the
command to the clipboard.

`detectPlatform()` now consults the user-agent string before the
client-hints platform and the deprecated `navigator.platform`, so the
source a visitor's tools actually change decides first; silent sources
fall through instead of vetoing.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* fix(website): keep iPads on the generic download path

iPadOS asks for desktop sites with a Macintosh user agent and a
`MacIntel` platform, so the OS guess sent iPad visitors to the macOS
installers. A "mac" verdict is now trusted only on a device without
touch points: no Mac has a touch screen, every iPad reports several.
The browser test adds an iPad-in-desktop-mode row to the generic-device
matrix next to the Android phone.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-06 00:51:46 +02:00
4gray 9bcdbc0efb fix(migration): preserve and recover legacy desktop sources (#1550)
* fix(migration): recover legacy desktop sources without replacing current data

* test(migration): cover legacy recovery IPC contracts

* test(migration): use static legacy Electron bootstrap
2026-09-06 00:45:35 +02:00
4gray 002faafbd7 fix(xtream): improve sync overlay contrast in both themes (#1546) 2026-09-06 00:44:08 +02:00