mirror of
https://github.com/4gray/iptvnator.git
synced 2026-10-15 05:36:17 -08:00
master
121
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
8098ea8bb8 |
feat(self-hosted): keep playlists on the server for every device (#1903)
* feat(services): server storage connection state and access token Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * feat(services): server kind for the playlist storage backend Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * feat(services): server storage API client The @iptvnator/services/server-storage sub-entry keeps server mode out of the eager services barrel. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * feat(services): playlist document store over the server API Full documents are cached by ETag (weak tags included) and served as private copies; an upsert whose payload is the one the store handed out sends keepPayload=1 without it and retries in full on payload_missing. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * feat(services): server-synced user data storage Synced Xtream maps live in a write-through cache under the iptvnator-server: prefix; writes send only the playlist entries that changed, debounced, and flush with keepalive when the page hides. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * feat(services): server storage session and token interceptor The session plugs the HTTP document store into PlaylistStorageBackend, hydrates synced user data before the first read (bounded to 3 s), retries while offline, flushes on hide, and routes to sign-in when the server answers 401. The interceptor adds the access token to HttpClient calls to the backend. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * feat(services): move browser playlists to the server An empty server gets the browser playlists with their ids plus the legacy Xtream user-data maps; a server that already has playlists goes through the backup export and import, so duplicates merge. Browser data is never deleted; the decision is remembered per server instance. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * feat(pwa): probe for server storage and wire server mode Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * feat(pwa): server sign-in page and migration dialog Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * feat(settings): server section for self-hosted storage Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * feat(workspace): server status in the rail and an offline banner Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * feat(settings): warn that delete-all clears the server Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * feat(i18n): server storage strings Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * test(e2e): server storage across browsers and the migration dialog Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * docs(self-hosted): server storage mode, sign-in and the release note Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(pwa): stay in server mode when a known server does not answer A failed start-up probe used to fall back to browser mode, which showed an empty library while the backend restarted and let new playlists land in the browser. The browser now remembers the server; without an answer it starts in server mode, offline, and reloads once the server is back. Settings → Server shows the backend address instead of the page's. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(server-storage): keep Xtream data, ordered sync, sign-in and stale-cache fixes - Moving browser playlists matches them to the server's by id or source and carries the legacy Xtream maps under the server id; it no longer goes through the backup format, whose PWA export has no Xtream user data. A partial move stays on offer. - User-data batches go out in order, and a re-read keeps entries edited while it was in flight. - A keepPayload write keeps its cached copy only when the server replaced the version its payload came from (new X-Replaced-ETag header). - 502/503/504 from the proxy count as offline. - The start-up screen lets the sign-in page through while a token is required. - Settings → Server offers a reload after a move. - The probe detects Electron through RuntimeCapabilitiesService. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(server-storage): merge user data on move, keep failed writes, sign-in route gate - A merged playlist gains the browser document's favorites and history; Xtream values for one server entry are combined (lists unioned, maps merged, the server winning conflicts) and written once. - A failed user-data write stays queued and survives a re-read; the page-hide flush waits for a batch still travelling. - The start-up screen lets the sign-in route through whatever its check reports. - The backend refuses IPTVNATOR_ACCESS_TOKEN without server storage. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(server-storage): positions by content, acknowledged keepalive, serial re-reads, token changes - Moved playback positions are matched by content type and Xtream id and name the server playlist. - Page-hide writes report whether the server took them; refused ones go back into the queue. Re-reads run one at a time. - The access guard's 401 carries WWW-Authenticate: Bearer realm="iptvnator"; the interceptor and PwaService's raw fetches turn it into sign-in, so a token changed on the server is noticed on provider calls too. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(server-storage): Stalker keys by category, keepalive ordering, cut-off probe - Moving Stalker favorites and history keys items by id and category, so a channel and a movie with one id both survive. - Writes after a page-hide flush wait for its keepalive requests. - A probe whose body is cut off counts as unreachable and keeps the remembered server. - Settings → Server renders without @defer so settings search can reveal its rows on the first visit. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(server-storage): cache baseline before first read, versioned retries, M3U favorite ids - Before the first user-data read finishes, an edit is compared with the cache, so it sends only its own playlist entry. - Queued entries carry their edit number; a failed write is retried only when nothing newer replaced it. - Moving M3U favorites and history translates channel ids through the stream URL (ids are random per import). - The server-storage E2E runs in Chromium only: one shared database. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(server-storage): keep URL favorites, flush before moving, cache-full and cut-off bodies - Moving M3U favorites keeps URL favorites and anything the server cannot resolve; only older channel ids are translated. - Queued user-data edits are sent before the move writes combined values. - A re-read that does not fit the cache keeps the old cache and baseline together. - A response body cut off after its headers counts as offline. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(server-storage): unsent writes survive reloads, guarded move, lazy storage - User-data writes the server has not confirmed are kept under iptvnator-server:unsent-writes and sent first on the next visit. - Moving browser data does not start while user-data writes are unsent, and a duplicate of a failed upload counts as failed, not merged. - The session reaches localStorage through the lazy adapter, so a browser that denies storage still boots. - Raise launch/renderer.domMutationsToFirstCard by 2 (569 → 571): the rail server shortcut and the offline banner each leave an @if anchor in every build. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(server-storage): locks follow merged playlists, typed legacy Xtream ids, instance-scoped cache - Moving a playlist under another server id takes its parental locks along (they stay in the browser). - Bare legacy Xtream ids in each copy are typed from that copy's own snapshots before favorites, history and snapshots are combined, so a live channel and a movie sharing an id both survive. - Writes still travelling are saved on page hide too; saved writes and the cache belong to one server database (cache owner = instanceId). - A late successful answer no longer turns unauthorized back to online. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(server-storage): apply early edits to the server value, newest unsent write, sticky sign-out - Edits made before the first successful user-data read are kept as before → after and applied to the server's value once it arrives (applyEdit), so a new device or a stale cache adds a favorite instead of replacing the server's list. - Saved unsent writes keep only the newest per entry; an answered older write no longer retires a newer one still travelling. - A late failure after sign-out no longer turns unauthorized into offline. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(server-storage): keep early edits across reloads and tabs, numeric-string legacy ids - Edits made before the first read are saved (before → after) and restored after a reload; a second early edit keeps the first one's baseline, even an empty one; a cache-full first read still applies them to the server's value. - Queued writes are saved as soon as they are queued, and a re-read keeps entries another tab has not confirmed yet. - Legacy Xtream ids stored as numeric strings are typed too. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(server-storage): per-tab saved writes, memory overflow cache, snapshot aliases - Unconfirmed writes and early edits are kept in the tab's own sessionStorage, so tabs never clear each other's; a shared registry of unconfirmed entries (one record per tab, ignored after ten minutes) keeps a re-read in one tab from replacing another tab's values. - A cache value too big for localStorage stays readable from memory, so later edits are diffed against what the app reads. - Typing legacy Xtream ids keeps each snapshot's bare key as an alias, so a reference not yet rewritten still resolves and a retry can type it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(server-storage): conditional user-data writes, scoped token, deletes - User-state entries carry a content tag; PUT/DELETE accept If-Match or If-None-Match: * and answer 412 with the current value and tag. The PWA sends each edit on the tag it read and replays it (before → after) on the server's value after a conflict, so another device's change to the same playlist's favorites or positions is kept instead of overwritten. - The access token goes only to the backend URL (same origin and path), never to another host or a sibling path. - Deleting a playlist (or all) first drops its queued user-data edits and waits for writes in flight, so none recreates deleted data. - The migration copy writes on the tags it read. - The write queue moves out of ServerUserDataStorage into UserDataWriteQueue. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * docs(server-storage): conditional user-state writes Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(server-storage): keep tags with their values, rebase page-hide writes, hold deletes - A re-read that keeps an entry from the cache (another tab's or this tab's unconfirmed value) no longer takes the server's tag for it, so the next edit conflicts and is replayed instead of overwriting another device's change. - Page-hide writes behind a travelling one are taken from the queue when it is answered, so its new version becomes their baseline. - A playlist delete holds that playlist's user-data edits instead of discarding them up front; only a successful delete drops them, a failed one sends them as before. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(server-storage): replay edits behind travelling writes in order, hold writes after an offline start - An edit queued behind a travelling write keeps its own before → after and carries the travelling write's edit as `prior`, also in the saved writes. A conflict replays both in order, so adding and then removing a favorite removes it whether or not the first write landed, and a lost write is not dropped. - Page-hide writes now report their new tag or a 412 like normal writes: a confirmed one rebases the next edit, a conflict is replayed. - After an offline start from a remembered instance, writes are held and the retry only checks /instance before reloading, so saved writes never reach another database behind the same address. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(server-storage): durable per-page journal, conflicts for edits on other tabs' changes, lock readability - Unconfirmed writes and early edits are saved in localStorage per page load; the tab remembers its page load in sessionStorage, so a reload takes them over, and another tab replays a closed tab's writes once its record is stale. Live tabs renew their record every 30 s. - An edit made on another tab's unsent change, and an entry a re-read keeps from the cache, are sent on a version no entry has, so the write conflicts and is replayed instead of carrying another tab's change past a later removal. - Moving browser data counts the parental-lock step as failed while the lock store cannot be read, so the move stays on offer. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(server-storage): keep edits as ordered steps, treat probe refusals as transient, read the cache through memory - A queued write keeps its edits as steps: an edit continuing the last one joins it, one made behind a travelling write or on another tab's change stays apart, and a conflict replays them in order. Saved writes and early edits store the steps too. - The instance probe treats any refusal but 404 (a proxy's 429 or 403) as unreachable, so a browser keeps its remembered server. - Cache reads that browser storage refuses fall back to memory, so a re-read and later edits still work. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(server-storage): keep steps of possibly-landed writes apart, queue closed tabs' edits even when the cache shows them - Steps of a write whose answer was lost, of saved writes taken over after a reload, and of a closed tab's saved edits are marked uncertain, so a later edit never joins them: adding and then removing an item after a lost answer still removes it. - A closed tab's saved edits are queued ahead of this tab's own even when the cache already shows them, so a conflict replays them too. - The tag map moves into UserStateTags. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * perf(workspace): style the server banner from the shell, baseline 570 The offline banner's own stylesheet added a <style> element to every launch, one of the two mutations the launch journey counted on top of master. Its rules move into the workspace shell's stylesheet. The other one is a parser-inserted modulepreload link: the new lazy server-storage chunks split one shared initial chunk in two. A/B on macOS: master 555, this branch 556 (was 557); the baseline note now says so instead of blaming @if anchors, which cost nothing here. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(server-storage): apply a caller's edits to the cache as it is now ServerUserDataStorage remembers the map getItem handed out. When the cache changed since (another tab saved, a re-read), a write applies only the entries the caller changed to the current cache, so a map read before another tab's save no longer reads as deleting that save. The cache moves into UserDataCache. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(server-storage): retry unsynced user data while online, keep newer typed snapshots, explain a refused token store - The 30 s retry also re-reads and flushes while online when the first user-data read failed or writes the server refused (a 5xx) are queued; the same failure notice shows at most every five minutes. - Moving browser data adds a legacy snapshot alias only where no typed snapshot exists, so a newer typed copy is not overwritten. - writeServerAccessToken reports whether storage kept the token; the sign-in page says so (SERVER_STORAGE.SIGN_IN_STORAGE, 19 locales) instead of calling a right token rejected. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(server-storage): conditional playlist metadata writes, retried on a fresh read A favorite or rename (keepPayload PUT) is sent with If-Match on the version it was read at. The server answers 412 when another device wrote since; the store throws PlaylistWriteConflict and PlaylistsService's per-playlist write queue runs the mutation again, which reads the playlist afresh, so two devices' metadata edits both survive. A retry on the same unread copy writes unconditionally, as before; refresh and import writes stay last write wins. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(server-storage): conditional writes for every playlist mutation, ordered adoption, keep journals until copied - Every playlist mutation is sent on the version it was read at: the payload's ETag, else the playlist's last read (portal documents have no payload). Refused versions are tracked per playlist, so a conflict on one never disables the check on another at the same revision. Imports and start-up migrations (dbUpsertAppPlaylists) stay unconditional. - A closed tab's adopted edits replay before this tab's queued ones, from the base of its first step, so newer progress is not replaced by older. - A reload or adoption removes the old page load's saved writes only when this page saved its copy; each closed tab is adopted once per page. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(server-storage): copy-bound portal versions, stale 401s, unreadable browser maps, early edits on recovered writes - A served copy carries the version it came from (a symbol property that spreads keep and JSON drops), so a mutation of a portal document names the version it was built on, not the latest read. - A 401 for a token replaced since (a new sign-in) neither clears the new token nor reports unauthorized, in the API client, the HttpClient interceptor and PwaService's proxy calls. - Moving browser data counts a browser user-data map that cannot be read as a failure, so the move stays on offer. - An edit made before the first read on top of this tab's recovered unsent write is applied to that write, not to the server's value. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(server-storage): favorite toggles as deltas, early edits as steps on recovered writes, release kept journal copies - PlaylistEffects.updateFavorites$ applies the toggled channel through transformPlaylistFavorites, on the favorites read at write time, instead of writing the reducer's whole array: a conflict retry keeps another device's favorites (#1919). - Edits made before the first read on top of this tab's recovered unsent write are appended to it as their own steps, so a removal of an item the recovered write adds survives a conflict replay. - Journal copies of earlier page loads kept because this page could not save its own are removed once it can. - adopt/extend move into server-user-data-adopt.ts over a small queue API (unsent, isQueued, put); the queue exposes its tag map. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(server-storage): refuse user state for deleted playlists PUT /v1/user-state/:namespace/:playlistId answers 404 playlist_missing when the server has no such playlist, so an edit queued on another device (including its conflict replay with If-None-Match: *) cannot recreate the state of a playlist deleted meanwhile. The client drops such an edit and its cache entry instead of retrying; page-hide writes report it as gone. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(server-storage): keep duplicated tabs off live journals, drain writes around remove-all, normalize legacy Stalker rows on the move - A page marks its unconfirmed-writes record closed on pagehide (not into the back-forward cache); a new page takes over the previous page load's saved writes only when that record is closed, stale or missing, so a duplicated tab, which copies sessionStorage, leaves the running tab its writes. - PlaylistsService.removeAll lets queued playlist writes land first and makes writes issued meanwhile wait until it finished; the server store refuses a document without an id. - Moving browser data sets the explicit Stalker portal-mode flag on rows from before it (withExplicitStalkerPortalFlag, now shared with PlaylistsService). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(server-storage): take over a reload's writes once the previous page load stops looking live When the close flag could not be saved (storage full), a reload took the previous page load for a running duplicated tab and left its saved writes unsent. The page now remembers that page load and its 30 s touch takes it over as soon as its record is closed, stale or gone, so the session's retry sends the writes. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(server-storage): adopt a late previous page load instead of restoring it When touch() finds that the previous page load no longer looks live, this page may already have read and queued newer edits. Its saved writes and early edits are now adopted like a closed tab's (their steps ahead of this page's newer ones, queued at once) instead of restored as if this page had just started, which replaced newer progress with older and parked early edits that were never sent after the first read. Cache forgetting moves into UserDataCache.forget(). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(server-storage): adopted writes without a known version conflict instead of overwriting A page that never read the server has no version tags, so an adopted write went out unconditionally and replaced newer server data with its cached value. Adopted edits now use the never-matching version when this page knows none (and for a queued write without a tag), so the server answers 412 and the edits are replayed on its value. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(server-storage): a re-read sent before a playlist delete skips that playlist ServerUserDataStorage records each successful delete; a re-read sent before it drops the deleted playlist's entries and tags from the server's answer (withoutDeleted), so a late answer cannot write the deleted favorites and positions back into the cache. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(server-storage): keep a playlist id like __proto__ when filtering a read withoutDeleted() builds its maps without a prototype, so a surviving playlist whose id is __proto__ stays a key instead of changing the object's prototype and losing its favorites and tag. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(server-storage): reorders on the stored favorites, URL-insensitive remembered server, prototype-free id maps - PlaylistEffects.setFavorites$ applies the drag's order through transformPlaylistFavorites (reorderFavorites): favorites added on another device stay, ones removed there stay out. - The remembered server matches the backend URL by origin and path without trailing slashes, so /api and /api/ start offline in server mode alike. - Maps keyed by playlist id are built without a prototype (idMap), so an id such as __proto__ is an ordinary key in the cache, conflict replays, re-reads and tags. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(server-storage): one registry key per page load, server token only in server mode - The unconfirmed-writes registry stores each page load's record under its own key (iptvnator-server:unconfirmed:<instance>:<page>), listed through a new keys() on the user-data storage adapter, so tabs never overwrite each other's records. Saved writes without a fresh record, also when the record was lost, are adopted as a closed tab's (#1920). - PwaService sends the stored server token on its raw fetch calls only while server mode runs, and a backend that answers without server storage makes the browser forget the token. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(server-storage): forget the token for another server, listen before sign-in, adopt only stale records - A server that answers as another backend URL or instance id than the remembered one makes the browser forget the stored token, so server A's token never reaches server B. - The session installs its page-hide and heartbeat listeners before it returns for sign-in, so a page holding another page load's writes still marks itself closed on unload. - Closed tabs are found by stale records only again: saved writes without a record may belong to a running tab whose record storage refused, and are left to that tab's own reload. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(server-storage): no token for an unverified instance, sign-in remembers its server - ServerStorageStatusService.accessToken() returns the stored token only while server mode runs on an instance this start verified. The API client, the HttpClient interceptor and PwaService's raw fetch calls use it, so after an offline start from a remembered instance no request (including the /instance reachability check) carries the token to whatever answers at that address, and a 401 then does not clear it. - Sign-in remembers the server along with the token; when storage refuses either, it reports SIGN_IN_STORAGE instead of reloading into a start that would drop the token. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(server-storage): check a remembered instance before sign-in sends the token A page started from memory (startedOffline) withholds the token until the instance is verified, so sign-in on that page sent no token and the server's 401 cleared the right one. Sign-in now first calls ServerStorageSession.ensureVerified(): it reads /instance without a token, marks the status verified when the instance id matches, and reloads the page when another server answers at the address. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(server-storage): read migration maps by own playlist ids only The browser's legacy maps and the server's user-state entries are read into null-prototype maps, so a playlist id such as `toString` is not taken for an entry and a missing server value no longer resolves to an inherited function. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(server-storage): bind the token to its instance; adopted edits yield to newer changes The access token is stored with the instance id it was given for, and a page reads it only for its own instance: a tab still open on the old backend after BACKEND_URL changed no longer sends the new server's token there. The API client hands every 401 to reportAccessDenied(), which now also sends a page without a token for its instance to sign-in, while one that holds its token back until the instance is checked keeps it. A closed tab's adopted edits carry `yields`: replayed, they change only what is still as that tab saw it, so they never replace progress this tab or another device saved since. The flag survives the journal. The sync spec's fake server and memory storage move to a test-helpers file to keep the spec under its line limit. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(server-storage): per-instance cache, idle tabs adopt closed ones, backups wait for the first read The user-data cache is keyed by server instance (`iptvnator-server:cache:<instanceId>:<namespace>`): a tab still open on the old backend after BACKEND_URL changed no longer writes over the new server's cache, which the new tab's next edit would have sent there. claimCache also removes the old owner's keys and the unscoped keys of earlier builds. touch(), which runs every 30 s, adopts closed tabs' saved writes once this tab has read: a tab with nothing to sync never re-reads, so they waited until a tab switch or reload. UserDataStorage gains an optional ready(); the server storage resolves it after its first read and rejects while that fails. A PWA backup export awaits it, so a fresh browser whose /user-state read is slow or failed cannot export empty favorites, history and positions. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(server-storage): token only to backend URLs in raw fetches; Stalker series keys in the move PwaService joins paths to the backend URL without its trailing slashes, so BACKEND_URL=/ no longer yields a protocol-relative //… URL that names another host, and it attaches the server token only when the request URL resolves to the backend (isBackendUrl), like the interceptor. The move's item key adds the Stalker series flag to id and category: a provider category can hold a movie and a series of one id, and the series favorite was dropped as a duplicate. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(server-storage): Xtream reads and backups wait for the first read; keepalive deadline Startup waits for the first user-data read only a few seconds. ready() now waits for a first read in flight without starting one (an offline page is not held up) and tells whether one succeeded: - PwaXtreamDataSource awaits it before reading favorites, recent items and playback positions, so a new device whose /user-state read is slow does not show empty lists or start playback without its resume point. - A PWA backup export and restore fail while no read succeeded; the restore stays parked, since its replacement lists would otherwise merge into the server's (an empty favorites list changed nothing). A page-hide keepalive write gets the request timeout: a server that takes it and never answers no longer holds every later write, re-read and delete behind it until reload. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(server-storage): a failed sign-in keeps another tab's token; moved history newest first A sign-in that fails clears the token only while it is still the one this attempt stored: another tab that signed in meanwhile keeps its valid token. The move orders the combined Xtream recent items by viewedAt, newest first, as the app keeps them: browser views appended after the server's history were cut by the next view once the server held 50. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(server-storage): recheck the instance after offline, back-forward cache pages, Stalker server rows Going offline now marks the instance unchecked: the token and writes wait until /instance answers with this page's instance (another one reloads the page). A replaced volume at the same address no longer gets the old token and queued user-state writes on reconnect. The session checks as soon as it goes offline, so a blip costs no retry interval. A page restored from the back-forward cache whose record another tab adopted meanwhile (its timers were frozen, so the record went stale) reloads instead of sending those writes again after newer edits. The Stalker playlist metadata migration also runs on server rows, once per server: a desktop database copied to the server can predate it, and its Stalker rows lacked isFullStalkerPortal. The take-over and change-diff loops move out of the user-data facade, and the adoption specs get their own file. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(server-storage): adopted edits keep yielding through conflicts; newest view per item; same-token sign-ins A conflict replay collapsed every step into one edit, so a closed tab's adopted step lost `yields` after the first conflict and a second one replayed its old position over a newer one. Its steps now stay as they are, and only this page's own collapse into one edit on top of them. The move combines recently viewed items one per item, keeping the newest view and its artwork, then newest first; before, the server's older copy of an item won over a later browser view. A failed sign-in clears the stored token only on a 401: a network failure says nothing about the token, and another tab may have stored the same one meanwhile. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(server-storage): suspended tabs give up adopted writes; history stays newest first; favorite checks wait A background tab the browser suspended past the record lifetime can have its writes adopted elsewhere, like a back-forward cache page: shown again, it now checks resumed() and reloads instead of sending them. Conflict replays appended a newly viewed item at the end of the history, where the next 50-item cut dropped it; a replayed recently viewed list is now kept newest first. isFavorite() waits for the first user-data read like the list reads, so a detail page opened during a slow first read does not toggle an existing favorite the wrong way. A parked backup restore replayed when the playlist opens waits too, and stays parked while no read succeeded. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(server-storage): offline recheck checks only the instance; sign-out always navigates The recheck on going offline ran a full refresh: when /instance answered but /user-state kept failing, each failed read set the page offline again and started the next one at once, bypassing the retry interval. It now checks the instance only; reads wait for the retry. Sign-out navigates to sign-in itself, also when the page is signed out already: a guard (unsaved Settings) may have cancelled the redirect the first time, and the state change that drives it does not repeat. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(server-storage): a superseded page sends and saves nothing; re-reads keep other tabs' newer saves A suspended tab's retry timer could run before it was shown again: touch() recreated the record another tab had removed when it adopted the page's writes, so the page sent them again over newer edits. The facade now treats a page whose record is gone as superseded: the queue sends nothing (maySend hook), its journal and record are not written again, and the retry timer reloads it like a page shown again. A re-read answered after another tab saved an entry no longer writes the older answer over the shared cache: entries changed while the read travelled are kept, with an unknown tag, so the next write here conflicts and replays instead of removing what the other tab saved. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * test(web-e2e): never reuse another backend as the storage E2E backend server-storage.e2e.ts deletes every playlist before each test. Reusing a backend already listening on STORAGE_BACKEND_PORT outside CI could have wiped a real library; an occupied port now fails the run instead. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(server-storage): an outage on the sign-in page also unverifies the instance reportOffline() left `verified` set while the page was unauthorized, so a sign-in retried after an outage skipped the instance check and could send the old instance's token to a replacement database. Verification is now cleared on any outage; only the state change stays guarded, so the workspace remains locked. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(server-storage): interceptor and raw fetches report backend outages too Only the storage API client reported an unreachable backend as offline. When the first sign of an outage was an HttpClient call through the interceptor (provider targets, playlist parsing) or one of PwaService's raw fetches (Stalker, connectivity reset), the instance stayed verified, and the next request after a replacement instance came up at the same address would carry the old token. Both now report no answer (status 0, a failed fetch) and a gateway's 502/503/504 as offline, so the session checks /instance first. The backend's own JSON 502 for a failed provider is an answer and changes nothing; neither does a fetch this page aborted on its own timeout. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(server-storage): the immediate offline check runs once per retry interval A response whose headers arrived but whose body was cut off reports success and then offline, which started the next immediate instance check at once: repeated cut-off answers kept requests going and the connection indicator flickering. The check on going offline now runs at most once per retry interval; later failures wait for the timed retry. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(server-storage): a saved write without a readable tag counts as saved; unverified online pages keep retrying A write whose headers said saved but whose body (with the new tag) was cut off was retired without confirmation: a newer queued edit kept its steps and old tag, and a later conflict replayed the saved step over a newer value from another device. It now counts as saved on an unknown version: the landed steps drop out and the next write conflicts, so only edits made after it replay. Writes that were unconditional stay so. The keepalive path does the same. A late answer to a request sent before an outage can set an unverified page online; with nothing to sync the retry then never ran and the token stayed withheld. The retry now also runs for online pages that are not verified. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(web): keep server storage off the startup bundle; form-field defaults on sign-in PwaService imported isBackendUrl from the @iptvnator/services/server-storage barrel, which pulled the whole storage subsystem (sync, migration, API client) into main, though server mode loads it by dynamic import. The helper now lives with the token helpers in @iptvnator/services (re-exported from the interceptor): renderer.initialBytes 1,392,902 -> 1,357,795 bytes locally. The remaining 11,988 bytes over #1931's baseline are what must run before the first route (instance probe, connection status, token bound to its instance, storage backend and write serialization); the baseline is raised with a note, under the PR's perf-baseline-increase label. The sign-in component provides the form-field defaults per component, as master's check (#1928) now requires. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(server-storage): conflict-safe Stalker metadata migration on the server; newest document views in the move The Stalker metadata migration wrote server rows back through the unconditional bulk upsert, from the list read: a device that edited a Stalker playlist in between (or a second browser migrating the same copied database) lost its change. Server rows are now read again one by one and written as conditional mutations on that version; a conflict leaves the flag unset, so the next start retries. Moving a document's history kept the server's copy of an item both held, even when the browser had viewed it later, and skipped the write when the length did not change. The combined history is now newest first by added_at, one entry per item, and any content change is written. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(server-storage): M3U favorites by URL and history channel ids in the move; restores wait for the server The move's M3U remap swapped references for server channel ids, but the app looks favorites up by stream URL and history entries carry the URL as their id: legacy favorites became invisible and history duplicated. Legacy id favorites now become their channel's URL on both sides, and history keeps its URL id with the server's channel id as channel_id. A restore in server mode reported success once its lists were queued: a list the server refused (413) left the old data while the parked restore was consumed. Both restore paths now wait for that playlist's writes (UserDataStorage.flushed, a few flush rounds for conflict replays) and stay parked when the server did not take them. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(server-storage): a closed page record stays closed when a late write rewrites it On pagehide the page flushes with keepalive and then marks its record closed. When writes were held (offline), the keepalive write fails at once and its requeue saved the journal again a microtask later, which rewrote the record without `closed`: the reload took the previous page load for live and left its favorites and progress unsent until the record aged out (ten minutes). markUnconfirmed now keeps an existing record's closed flag. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(server-storage): the move uploads playlists create-only and merges into a copy moved first Two tabs or devices moving browser copies with the same playlist id both planned an upload; the browser documents carry no served version, so both PUTs were unconditional and the later one overwrote the first copy (and any change made on it since), with both moves reporting success. The backend's playlist PUT now honours `If-None-Match: *` (412 when the playlist exists; checked and written in one synchronous handler). The move uploads with it through ServerPlaylistDocumentStore .dbCreateAppPlaylist, and an upload that finds its id taken merges its favorites and history into that copy instead, counted as merged. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(server-storage): Delete All waits for repairs holding a reservation; backups read the server again Delete All raised its write barrier before it held the exclusive authority reset. A Stalker repair already holding its row reservation then waited on that barrier for its conditional write, while the reset waited for the reservation: neither finished, and later playlist operations stalled behind them. The barrier now goes up inside the reset, once repairs in flight have finished; writes queued so far land first and new ones wait for the delete, as before. ready() said yes after any earlier read, so an export could miss what another device added since, and restoring an empty list did nothing when the cache was already empty. Backup export, restore and the restore replay now ask for a fresh read (ready(true)) and fail, the restore staying parked, when it does not succeed. The facade's adoption loop moves to the adopt module to keep the facade under its line limit. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(services): Delete All drains queued writes before its reset; reserved writes skip its barrier The previous commit raised Delete All's barrier inside the exclusive authority reset and drained queued writes there, but a queued deletion or import takes the shared side of that lock: the reset waited for it while it waited for the reset. Delete All drains the writes queued before it outside the lock again, then takes the reset, with its barrier up from the start. The deadlock with a Stalker repair (or Edit) that holds a row reservation and writes under it is broken on the other side: such a write does not wait for the barrier (holdsPlaylistAuthority, counted per reservation in this context), and the reset waits for the reservation as before. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(server-storage): a re-read keeps writes another tab had unconfirmed when it went out A re-read checked other tabs' unconfirmed entries only after its answer arrived. A tab whose write was unconfirmed when the read went out (its edit already in the snapshot) and confirmed before the old answer came back matched neither check, so the stale answer replaced its saved value in the shared cache; that tab's next edit, holding the newer tag, then wrote the stale list and deleted the item on the server. The entries unconfirmed when the read is sent are kept too. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(services): writes wait for Delete All before they queue; restores replace only settled lists Delete All, third variant: a write that arrived after Delete All waited for it inside its playlist's queue. A Stalker repair that reserved that playlist meanwhile skipped the barrier, but its write still queued behind that waiting one, while the reset waited for the repair's reservation. While a Delete All runs, writes now wait for it before they join their queue, so no queue holds work blocked on the reset; a write under a reservation held here joins at once. A restore replaced lists while this page's own write for the playlist was still unsent (refused, or lost): that write's edits joined the replacement and a conflict replay kept items the backup did not have. Both restore paths now require the playlist's writes to be on the server first (flushed), and otherwise stay parked. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(server-storage): a reload finds page loads a reload before it kept; restore master's tick baselines When storage refused a reload's copy of the last page load's saved writes, the old copy stayed but only this page's memory named it: the tab's pointer already named the new page, so a second reload before any copy was saved recovered from the empty new page, and without a registry record no other tab could find the old one. The pointer now also lists the page loads a page keeps (keepPages), and a reload takes them over, oldest first, before its last page load. The pointer helpers move to server-page-pointer.ts. Both merges of master resolved journey-baselines.json by taking this branch's whole file, which dropped #1929's cdTicksToFirstCard, cdTicksIdle30s and cdTicksToFirstPage. The file is master's again with only this branch's initialBytes and domMutationsToFirstCard raises. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(server-storage): a duplicate leaves a running tab's kept page loads to it A duplicated tab copies the tab's session storage, so after a reload that kept older page loads its pointer lists them too, and the duplicate took them over at once, although the tab still running owns those edits and may have newer ones queued; its replay could then write older watch progress over newer. When the previous page load still runs, the kept ones now wait with it and `touch` adopts them, oldest first, once it no longer looks live. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(server-storage): a restore merged by a conflict stays parked; first-read tests wait for the read A restore whose write conflicted with an edit another device made during it had the conflict replay merge that edit in, and flushed() still said the lists were taken, so the restore was consumed with lists that were not the backup's. Both restore paths now compare the stored lists with the ones they wrote once the server took them, and a difference keeps the restore parked to run again on a fresh read. The two first-read tests released the mocked read after a fixed number of microtask turns, which could come before the read started; they now wait until it has. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(server-storage): page-hide keepalive writes report an outage too sendKeepalive() turned a failed fetch, a timeout or a gateway's 502/503/504 into a failed outcome without telling the status, so the instance stayed verified: when the hidden tab came back, its refresh skipped the /instance check and resent the queued writes, which could reach a replacement database behind the same address. Like requests, a keepalive write now reports these as offline, so the instance is checked before anything goes out again. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(server-storage): a keepalive answer cut off after its headers counts as offline A page-hide write whose headers said saved but whose body broke off was returned as saved with the read error dropped, so the instance stayed verified. Like readJson() for requests, it now reports offline (the outcome stays saved on an unknown version), and the instance is checked before the next token or write goes out. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(server-storage): kept page loads replay oldest first; restore checks compare normalized reads A page load and an older one it kept could both hold steps for one entry. Adopting them, each adopted page went ahead of what was queued, so adopting the oldest first put the newer page's steps before the older's; restoring them after a reload collapsed the newer page's steps into one, so its removal of an item the older page had added was lost on a conflict replay. Adoption now goes newest first, and a reload appends a later page's steps (and early edits) after an older copy's. restoreUserData() compared the lists it wrote with reads that drop duplicate favorites, so a saved restore listing one twice never finished, also in browser-only mode. It now compares reads right after writing with reads after the save. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(server-storage): flushes wait for a page-hide batch sent at once; keepalive 401s; parked restores move A flush scheduled in the same turn as a page-hide flush was chained on the queue before the keepalive batch, which went out at once beside it: a later edit (removing a favorite the keepalive batch added) could land first and be undone when the keepalive write arrived. A scheduled flush now waits for a page-hide batch sent at once before it takes entries. A keepalive write refused with 401 returned failed without telling the status; like requests it now reports the token it sent as refused. The move copies a parked Xtream restore (a browser backup import that could not finish) to the server playlist id a playlist merged into, so it is applied and exported there; one parked there already wins. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(server-storage): a slow server does not reload a page started from memory forever The start probe gives /v1/instance 2.5 s. A remembered server slower than that started the page from memory; the recovery check (with the request timeout) then got an answer and reloaded, and the reloaded page timed out again: an endless reload loop for a server answering in 2.5 to 20 s. The reload now leaves a one-time mark in sessionStorage, and the next probe waits as long as requests do. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
2139ddd5c0 |
feat(ui): one dialog frame, a shared dialog shell and three width presets (#1942)
* feat(ui): one dialog frame, a shared dialog shell and three width presets
Every dialog gets the kit frame from m3-theme.scss: a 16px panel on the
widget surface with a hairline border and the popover shadow, over a tinted
backdrop blurred at 6px. The command palette and the programme dialog keep
their own surface through their panel classes.
`app-dialog-shell` (@iptvnator/ui/components) draws the header (optional
icon tile, the heading as mat-dialog-title, subtitle, a header-end slot and
a close button), the body and a footer that renders only when an element
marked appDialogFooter wraps the actions. The close button is last in the
DOM, so first-tabbable autofocus lands in the body. Footer buttons take the
kit look: ghost, accent (new accent-button mixin) or secondary; destructive
buttons keep their tint and get the kit's 10px radius.
`dialogSize('sm' | 'md' | 'lg')` owns the width and viewport limits with a
type-only MatDialogConfig import; MAT_DIALOG_DEFAULT_OPTIONS is deliberately
not provided at the root, which would put Material's dialog back on the
startup path.
Migrated: the confirm dialog (sm), the add-playlist dialog (lg) and the
keyboard shortcuts dialog (lg, close button only). Slot wrappers render as
display: contents, because a control-flow block at the top level of the
content is projected into the body, not into a named slot.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
* fix(ui): name the dialog shell's close button "Close dialog"
A confirm dialog opened with a "Close" dismiss (the download recovery
prompt, the VOD download prompt) had two buttons named "Close" once the
shell added its header ×, and the Electron download-recovery test failed
on a strict-mode violation for `getByRole('button', { name: 'Close' })`.
The × is now named by the new `CLOSE_DIALOG` key ("Close dialog") in every
locale, so a footer "Close" stays the only button of that name; the same
holds for the dialogs PR 3 migrates, whose E2E flows select "Close".
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
* fix(ui): let short windows scroll the whole dialog shell
With the shell capped at the viewport, a stacked footer (the add-playlist
Xtream mode has five rows) left the body a 40px sliver at 360×480. Under
600px of viewport height the cap comes off and Material's surface scrolls
the whole dialog as the single scroll owner, which is how the dialog
behaved before the shell.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
---------
Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
|
||
|
|
ced7386dfa |
perf(web): render the radio play control as an icon button, not a fab (#1938)
The audio player's play/pause control was the app's only mat-fab. It is now a mat-icon-button drawn as the same 56px primary circle (icon-button overrides: 56px state layer, 32px white icon, 50% shape, selection-colour fill, hover scale), so the eager button chunk no longer carries the fab directives and the theme no longer emits the fab tokens: renderer.initialBytes 1,349,048 -> 1,329,249 bytes (-19,799: -13,799 in button.mjs, -6,028 in the stylesheet), measured locally against the merge base. The fab also reset the keyboard focus ring with `outline: none`; the icon button keeps the app ring. - tools/nx/check-material-theme-scope.mjs: `button` brings button and icon-button; `fab` is required only while a runtime template renders mat-fab / mat-mini-fab (matFab / matMiniFab), found through the component templates the way check-form-field-defaults.mjs resolves them. That resolution moves to tools/nx/component-templates.mjs, shared by both. Templates know HTML comments only, so the scan strips just those: a `//` in text or an attribute value no longer hides a fab on the same line. - apps/web/src/m3-theme.scss drops `fab` from $material-components. - apps/web-e2e: the Stalker radio test checks the control's size, shape, fill, icon and keyboard focus ring. - journey-baselines.json: renderer.initialBytes 1,345,807 -> 1,329,249. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> |
||
|
|
90a31d9be9 |
feat(sources): redesign the Sources page and the playlist switcher (#1932)
* feat(sources): add the shared source row and credential-free formatting One app-source-row (regular for the Sources page, compact for the playlist switcher) with an app-source-tile, built on pure helpers: - resolvePlaylistSourceKind: one provider rule for icons, filters and counts (the list filter and the panel counts used to disagree). - sourceDisplayName / sourceDisplayUrl: a name or URL never shows a password, token or MAC. URL imports that stored "get.php?username=…&password=…" as the title now show the host. - stripUrlCredentials (shared/logging): drops what the log redactor would mask, plus short user/pass keys. - formatSourceDate / sourceMetaParts / sourceStatusTag: one meta line order and Intl dates and durations in the UI language. - SourceStatusService + SourceHealthHistoryService: one status per source (desktop health check, browser Xtream portal status) and the last conclusive result kept across restarts, so "not responding for 3 days" survives a relaunch. No URL or credential is stored. Also fixes playlistNameHasCredentials answering differently on repeated calls (global regex lastIndex), and adds the UI-kit button, menu and segmented-control mixins (libs/ui/styles/_app-controls.scss); menus opt in with class="app-menu", styled by the components that use them. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * feat(sources): rebuild the Sources page on the shared source row - Side list (UI-kit side list): title with the playlist count and an Add button, a Type group, and on desktop a Status group (Any / Responding / Not responding). The two filters combine and persist; each group counts within the other's choice. The chosen row uses one selected-row token (soft tint, outlined in light theme). - Page: a 28px title with the visible count instead of the bordered header bar; Clean up and the sort menu are secondary buttons. Clean up shows how many sources do not respond. Sort adds Last updated, Most channels and (desktop) Not responding first, and keeps Name Z-A and Custom order. - List: app-source-row with hover actions (Refresh, Edit, ⋯); a failing row keeps Retry in view. "No matches" offers Reset filters. Dragging in custom order no longer reuses positions of rows a filter hides. - Delete with Undo: no confirmation; the row leaves at once and the delete runs when the toast closes (its close button commits at once). Deleting the active source switches to the next row. A large Xtream delete shows its row as "Removing…" with progress and Cancel, as before. - The list's refresh flows moved into SourceListRefreshService; E2E and performance drivers follow the new markup and delete flow. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * feat(switcher): restyle the toolbar trigger and open it with Cmd/Ctrl+P - The trigger is a 40px secondary button: the active source's tile with its status dot, its name and "type · channels" (or the source count), and a chevron that turns while open. The name comes from sourceDisplayName, so a URL-named playlist no longer shows its URL or credentials in the toolbar. - Cmd+P / Ctrl+P opens the switcher wherever it is shown (Cmd+K stays the command palette) and is listed in the keyboard shortcuts dialog. - The trigger's inline refresh button is gone; refresh stays in the playlist's ⋯ menu and the command palette. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * feat(switcher): rebuild the playlist switcher as an overlay popover The switcher's mat-menu becomes a CDK connected overlay (app-playlist-switcher-panel), 440px wide, with a dimmed, blurred backdrop; only its list scrolls and row menus open above it. - Header with the source count, Add playlist and All sources. - Search by name or host, autofocused; the type filter is one segmented control (All / M3U / Xtream / Stalker with counts) instead of three checkboxes, remembered between openings (the old checkbox setting is migrated). The search is no longer kept between openings. - "Recent" (the three most recently opened, kept across restarts) above "All" in the Sources page order, as compact source rows: the current one tinted with a check. Each row's ⋯ menu holds Refresh, Playlist info, Account info, Check again and Remove (with Undo); the footer's info, account and add actions moved there and to the header. - Keyboard: a listbox driven by aria-activedescendant from the search field. Arrow keys move and wrap, Enter opens, Shift+F10 opens the row's menu, Escape closes and returns focus to the trigger. Hints in the footer. - Deleting from a row menu moves focus to the toast's Undo, since the row (and its focus) left the page. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * refactor(sources): remove the old source row, filter state and dead styles - Delete app-playlist-item and SourceHealthIndicatorComponent, replaced by app-source-row and its tile dot. - Delete the ngrx type-filter slice (FilterActions, selectedFilters, selectActiveTypeFilters); the Sources filters live in SourcesViewStateService. - Drop the empty state's unused "no results" variant and its styles. - The switcher and Sources list components are no longer in the max-lines baseline. - Docs: UI guidelines (source row, Sources page and switcher, status dot, selected-row tokens, UI-kit mixins), the source-health contract and the zoneless inventory. Release notes for the redesign and the credentials fix. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(catalog): keep the refine menu whole in short windows The refine menu held 12 rows at 48px plus two section titles (657px). Angular Material 22 sets no max-height on a menu panel, so below ~800px the overlay was pushed up over its own Refine button, and at 600px the last thresholds were clipped behind an invisible scrollbar. The minimum-rating thresholds now open in a submenu from one row that names the active threshold. The menu is about 390px and opens below its button at 600px. The submenu trigger sits in a role="none" wrapper: Material offsets a submenu by the first direct mat-menu-item's offsetTop, and with the sort rows inside a role="group" that item was the trigger itself, which opened the submenu 335px off screen. The Electron E2E opens both menus by keyboard in a 600px-tall window and checks that each lies whole on screen without scrolling. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * style(workspace): drop the side panel's divider line The context panel beside Sources, categories, Settings and collections drew a 1px outline-variant border against the content surface, whose background, shadow and rounded top-left corner already mark the edge; the line clashed with that corner. The border stays 1px but transparent, so the panel width, the persisted sidebar widths and the resize handle's position do not move. The handle keeps its hover and drag highlight. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(switcher): keep the search field and type filter whole with many sources The switcher popover is a height-capped flex column. With a long list (hundreds of sources) the browser shrank every part in proportion to its size, not only the list: the type filter, which clips its overflow, collapsed to 2px and showed no labels, and the search field dropped from 36px to its 21px text line. The header, search, type filter, empty state and footer are now flex: none, so only the list shrinks and scrolls. The search field now matches the header and side-panel search fields: the shared --app-search-* tokens, a 10px radius, an 18px icon, the same text size and focus halo. The Electron E2E opens the switcher in a 320px-tall window and checks the search height, the type toggles and the scrolling list; it fails without the fix (search 21px). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(sources): checks behind filters, keyboard Undo in the switcher, check causes Local review round 1 (Codex + Greptile) on the redesign: - A saved Responding / Not responding filter hid every source without a result, and the hidden rows were what started the checks, so the list stayed empty. keepSourcesChecked (portal shared data-access) now keeps every library source checked while the Sources page lives, on desktop where a check is one shared job that rows join. - Removing from the switcher left its focus trap open around the search, outside of which the Undo toast sits: Tab could not reach Undo. The popover now closes first; Undo takes the focus and returns it to the trigger (SourceRemovalOptions.returnFocusTo). Every switcher action closes through one path, so a closing row menu no longer pulls the focus back into the popover. - A running desktop Xtream delete brought its source back into the switcher as an ordinary option that could be opened or removed again; the switcher now leaves it out until the delete ends (the Sources page shows its progress and Cancel). - Undo re-activated the removed source over a source the user had chosen since, when nothing had replaced it (it was the only row a filter showed). Undo now restores only while no other source is active. - The switcher highlight stays on its option, or in range, when the list changes under the same search, so Enter still opens something. - The status dot named every failure "Could not connect". SourceStatus now carries the check's cause (access denied, account disabled, timeout…, kept with the health history and validated on read), and M3U dots again say the check covers the playlist URL, not its channels. Tests: new specs for keepSourcesChecked, the saved-filter check, the Undo guard and focus return, the switcher close/highlight/deleting cases and the dot's cause; the filter and Undo specs fail without their fixes. The switcher keyboard E2E now deletes and undoes from the keyboard. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(sources): own Undo timer, per-connection health history, race-free perf waits Local review round 2 (Codex + Greptile): - The delete committed whenever the shared snack bar was dismissed, so another message (a refresh finishing elsewhere) cut the four-second Undo window short and deleted the source early. The window is now the service's own timer; only the toast's close button (commitNow) or a second delete commits sooner. - The health history was keyed by playlist ID only, so an edited address or account inherited the old connection's result, failing run included. Entries now keep a 32-bit FNV-1a fingerprint of the source health key (no URL or credential); a result for another connection starts over and a stale entry no longer shows. - The M3U refresh-cancellation probe still queried the removed row's .cancel-btn, .busy-state__message and .action-spinner, so its cancel never fired; it now uses the source row's selectors (smoke run passes, cancelEffect=true). - The Xtream delete benchmark waited to catch the "Removing…" row coming back before it left, which a fast delete could finish before the wait began. The driver now arms a delete-playlist operation window before the commit click and ends on its completed event and the absent row. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(sources): sort by the shown name; start the benchmark's Undo window in its trigger Local review round 3 (Greptile; Codex was clean): - Name order compared the stored title, while the rows show a URL-named or credential-bearing source by its host: a source stored as get.php?username=… sorted by its hidden account details. sortPlaylists takes the list's name function, and Sources and the switcher pass sourceSortName, the name they display. - The Xtream delete benchmark clicked Remove while preparing, which starts the four-second Undo window; capture setup before the trigger could outlast it, so the delete committed on its own before the measurement and the close button was gone. Preparation now stops at the open menu, and the trigger clicks Remove and then the toast's close button, with the trigger time taken right before the commit click as before. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(sources): never hand over to a leaving source; browser checks per connection Local review round 4 (Codex + Greptile): - Removing the active source handed over to its neighbour even when that neighbour's own delete was still running ("Removing…"), activating a source about to disappear. replacementAfterRemoval (playlist shared util) picks the nearest row that is not leaving, on the Sources page and in the switcher; SourceRemovalService also refuses a leaving replacement. - In the browser build a "Check again" for an account the user then edited could finish last and overwrite the new account's status under the same playlist ID. Portal statuses are now keyed by playlist and connection fingerprint, so the old check lands under the old key. - The switcher scrolled the highlighted option into view through a selector built from the playlist ID, which a restored ID with "." or ":" broke (or made throw). It now finds the row by its exact DOM ID. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(sources): keep ⌘P out of open dialogs; redact refresh errors Local review round 5 (Greptile; Codex was clean): - Ctrl/Cmd+P opened the playlist switcher over an open dialog (an editor, a confirmation), taking the focus out of it and handing it back to the toolbar behind the dialog on close. The shortcut now leaves the key alone while MatDialog has open dialogs. - SourceListRefreshService logged a failed refresh's raw error, which can carry the playlist URL and its credentials. It now logs through the redacting portal logger (createLogger). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(sources): show a new search's first match; keep inconclusive check causes Local review round 6 (Codex; Greptile was clean at 5/5): - Typing a search or picking a type in the switcher highlighted the first match but kept the list scrolled where the previous result left it, so the highlighted row could sit above the view. The list returns to the top whenever the search or type changes. - A source with no kept history whose check was inconclusive (connection checks paused by the connectivity guard, a cancelled check) lost the live cause and time, and its dot said only "Not verified". The status now keeps them; a running check still reads as checking. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(switcher): fall back to All when the remembered type has no sources left Local review round 7 (Greptile 5/5 with one P2; Codex was clean): when the last source of the remembered switcher type was removed while two other types remained, the list kept filtering by the missing type. Its segment was gone, no segment was selected, and the list said "Nothing found". The list now filters by All until a source of that type returns; the remembered choice is kept. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(sources): clean credentials nested in kept URL parts Codex bot (PR review, P1): stripUrlCredentials copied every non-sensitive parameter value unchanged, so a URL nested in one (a proxy's ?url=http%3A%2F%2Fjoe%3Ahunter2%40origin.test%2F…) kept its userinfo and password and reached the Sources row through sourceDisplayUrl. Kept query and fragment values and path segments are now cleaned recursively, up to four levels: a nested URL loses its credentials the same way, a nested parameter list loses its sensitive keys, and a part that still names a secret (joe:hunter2@host, token=…) is dropped. Deeper nesting is dropped rather than shown. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(sources): keep fragment and nested URL secrets out of source names Greptile bot (PR review, P1 security): an unnamed import of https://host.test/list.m3u#api_key=secret123 stored list.m3u#api_key=… as its title. isUrlDerivedName only knew names ending in a query, and sourceSecrets only read the top-level query, so the row and the toolbar showed the secret while the displayed URL hid it. A name that ends the URL with a query or a fragment is now URL-derived, and shows the host. The secrets a typed name is checked against now include sensitive fragment parameters and, as stripUrlCredentials cleans them, the credentials of URLs and parameter lists nested in parameters (nested account names included). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(sources): keep deletes busy until settled and on screen; refresh E2E via the switcher PR #1932 round 1 (CI + local Codex/Greptile on the bot fixes): - CI: category-management.e2e refreshed the active playlist through the old switcher trigger's inline refresh button, which the redesign replaced with the switcher row menu (⋯ → Refresh playlist). The helper now goes through that menu; it failed on all three OS shards. - A desktop Xtream delete's completed event cleared its progress while PlaylistsService still cleaned up (parental lock, stored state), so the already-deleted source was actionable again on Sources and back in the switcher until the store dropped it. A terminal event now only ends Cancel; the row stays busy until the delete settles and the removal service clears it. - A running delete (from the switcher, say) disappeared from Sources when the page's saved filters excluded it, hiding its only progress and Cancel. Running deletes stay listed whatever the filters. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(sources): keep counting a failure's duration while the list stays open Local Greptile (5/5, one P2) on the PR round: the row's status tag formatted "Not responding · 3 days" against the time the row mounted (or the language last changed), so a new failure never gained its duration and an existing one froze while Sources stayed open. A shared root SourceClock now ticks once a minute and only the status tag reads it, so dates and the meta line do not recompute every minute. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(sources): keep the page header in view; mark the active source's row Local Greptile (two P2s) on the PR round, both regressions against master: - The redesigned Sources page scrolled as one, so the title, Clean up and Sort left the view in a long library (master kept a sticky header, and the UI guidelines say headers stay and lists scroll). The header now stays and only the list below it scrolls, which also keeps a row reached by keyboard from scrolling under the header. - Master marked the active playlist on the Sources page; the new row only tinted the current row in the compact (switcher) size. The active source's row is now tinted with the same --app-list-selected-* token on the page too; the switcher keeps its check. The design handoff called the header non-sticky and the current tint switcher-only; both followed master and the guidelines instead. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(sources): name the Sources sort group; refit the switcher on resize Local review on the PR round: - Greptile: the Sources sort menu's radio group had no accessible name, unlike every other menu group (UI guidelines). It is named by its section, "Sort by". - Codex: the switcher popover capped its height once, when it opened, so shrinking the window while it was open left the footer and the lower rows out of reach. A window resize refits the open popover. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * wip: linear URL-cleaning regexes * fix(sources): linear URL-cleaning patterns; keep the stripper off the startup path PR #1932 CI: - CodeQL (polynomial regular expression on uncontrolled data): the embedded-URL and keyed-pair patterns in stripUrlCredentials could retry from every position of a long run of letters. Matches now start only where a name can start (lookbehinds) and every name part is bounded, as is the bare-userinfo pattern; a spec times 50,000-character runs. The preceding "wip: linear URL-cleaning regexes" commit holds that change. - Initial bytes ratchet (+135 B over after master's #1928 lowered the baseline): stripUrlCredentials sat behind the shared logging barrel, which startup code imports, so esbuild placed it in an initial chunk although only the lazy Sources page and switcher use it. It now lives beside its only consumer in playlist/shared/util source-display, and the logging barrel exports the two redaction helpers it needs. Measured locally on the merge with master: 1,537,497 bytes, within 1,538,700. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(sources): case-insensitive Xtream path markers; no removal during a refresh - Strip and redact the account segments after `/LIVE/`, `/Movie/` or an encoded marker such as `/%6Cive/`, as after `/live/`. The display-name check also treats an Xtream path password as a URL secret. - A source cannot be removed while a switcher refresh clears and refills it: the removal service ignores the request, the switcher disables Remove, and the Sources page shows the refresh as busy. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(sources): search URLs nested in path segments for name secrets A proxy URL can carry a whole source URL, or a token pair, in one path segment. The display stripper already cleans such segments; the name check now collects their secrets too, so a name holding them collapses to the host. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(sources): decode nested URLs before classifying; narrow Xtream paths - A URL nested in a parameter or path segment is percent-decoded one round at a time until it reads as a URL or parameter list, so one encoded twice is cleaned too; a value still encoded after the last round is dropped. The name check unwraps nested values the same way. - The two segments after `/live/` and its siblings count as an account only when a stream follows them, so a playlist path such as `/live/news/list.m3u` keeps its file name in the URL and the title. - Adding a playlist from the phone drawer closes the drawer first, so two focus-trapped modals never stack. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(sources): clean URL-shaped fragments; one registry for busy sources - A fragment is cleaned whatever its shape: `#https://joe:pw@…` loses its userinfo, an unclassifiable secret is dropped, and the name check reads a URL in the fragment as well as a parameter list. - Every refresh holds its source in the root SourceActivityService, which deletes and the startup auto-refresh already use: Sources-row refreshes, the switcher's M3U refresh and the shared Xtream flow. The switcher no longer offers Refresh or Remove for a source the page is refreshing, the removal service ignores a held source, a second refresh from either entry point is skipped, and the page disables Remove while it is held. - The long-input timing spec allows 3 s: the inputs take about 0.25 s, a backtracking pattern takes minutes, and a loaded runner took 1.6 s. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(sources): keep a browser refresh busy until the backend answers The browser build's PLAYLIST_UPDATE returned at once while its request ran on, so the row stayed refreshable and removable and the shared registry saw the source as idle. The event now settles when the refresh does (the PWA service resolves after its success or error report; the Electron fallback awaits its handler), and both the Sources row and the switcher keep the source busy and held until then. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(sources): name check hides values under short account keys The name check used the log redactor's key list, which leaves out the short keys `pass`, `pas`, `user` and `usr` that the URL stripper drops. Both now share `isDisplaySensitiveKey`, so a name holding `?pass=…` collapses to the host; `user`/`usr` are account names like `username` and stay shown at the top level, as by design. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(sources): read keys, markers and secrets through every encoding layer Each check decoded its input once, so one more encoding layer slipped a secret past it: `?%2570assword=…` kept its value, and `/%256Cive/…` kept its account segments, both on screen and in logs. `decodeFully` (shared logging) undoes up to four percent-encoding layers and is now the one form every name check reads: the redactor's key normaliser and Xtream marker check, the display key predicate and path check, and the name check's secret values. Logs and the Sources lists gain the fix together. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(sources): read Xtream paths decoded; keep short decoded forms out - An Xtream account could hide behind encoded separators (`live%2Fjoe%2Fhunter2%2F1.ts` is one raw segment). `findXtreamAccount` reads the path as decoded parts, maps them back to raw segments, and is the one scanner for the log redactor, the on-screen URL and the name check. Account segments go whole, joined ones included. - The name check required a minimum length only of the raw secret, so a decoded form as short as one letter (`%2542` is `B`) hid ordinary names. Each form must now be long enough on its own. - The decoding helpers move to `url-credential-parts.ts`, which keeps the redactor under the line limit. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(sources): read an Xtream path both whole-segment and split Reading the path only as decoded parts misplaced the account when the user name itself held an encoded slash (`/live/joe%2Fx/hunter2/1.ts`): the password segment stayed on screen and in logs. `findXtreamAccount` now scans both readings, each raw segment decoded whole and the decoded path split on every separator, and drops what either finds. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(sources): drop JSON configs from shown URLs; read their secrets A nested value such as `?config={"password":"…"}` was neither a URL nor a parameter list, and the quoted key slipped past the free-text check. - A nested value that parses as a JSON object or array is dropped from the shown URL (JSON can spell a key so no pattern reads it, such as `w`); brackets that are not JSON, like `[1080p]`, stay. - The free-text check accepts a quoted key (`"token": …`). - The name check parses JSON values and collects what sits under sensitive keys, and follows URLs in its other strings. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(sources): hold Xtream sources through re-import; match secrets in any case - An Xtream refresh released its hold when navigation ended, while the re-import ran on from the route session, so Remove came back during the import's writes. The route session now takes the hold synchronously on NavigationEnd and keeps it until its sync, and the import it awaits, settle; the store also holds the source for every content initialization, retries included. - Secret matching was case-sensitive against encodeURIComponent's upper-case escapes, so `%3a` or a lower-case MAC slipped through. Text and secrets are now compared lower-cased, the text also fully decoded. - The Xtream `· username` label is left out when it is, or holds, the password. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(sources): read a bracketed key as parameters unless it is JSON The name check returned early for any value starting with `{` or `[`, so `[password]=…` (not JSON) skipped the parameter check and its password stayed in the name. It now returns early only when the value parses as a JSON object or array. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(sources): no opening a source from the switcher mid-refresh While an Xtream refresh was still deleting the catalog, ⌘P could reopen the switcher and selecting that source navigated at once, starting its import before the user data to restore was parked. The refresh flow now exposes `isRunning` (its set of runs is a signal), and the switcher marks a refreshing source aria-disabled and ignores its selection by pointer and Enter alike. Imports alone do not block opening a source. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(sources): hand over only to an idle source; hide names that are a short secret - Removing the active source could hand over to a neighbour a refresh was still clearing, opening it and starting its import early. `SourceRemovalService.canTakeOver` (not leaving, not deleting, not held in SourceActivityService) now picks the replacement in both lists and gates it in the service. - Secrets under three characters were dropped from the check entirely, so a name that was just a two-letter password showed it. Short secrets now count as the whole text (trimmed, any case); longer ones still match inside a text. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(sources): offer Refresh only when it can run; drag handle never opens - PlaylistRefreshActionService runs one refresh at a time, but canRefresh checked only the source, so the switcher, the command palette and the dashboard offered a Refresh that refresh() then ignored. canRefresh now applies the same global guard. - In Custom order a click on a row's drag handle bubbled to the row and opened the source; the handle now stops it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(sources): no idle Refresh on held rows; Recent counts the filtered list - The Sources page offered Refresh on a source the startup auto-refresh, an import or a delete held, and refresh() then refused it. The row now offers Refresh only while it can run, keeping it (with its spinner) while that source refreshes. - The switcher showed Recent whenever the library had six sources, so a type filter down to one or two listed them twice and the keyboard visited each twice. Recent now depends on the filtered count. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(sources): compare passwords in their own case; only a MAC caselessly Matching every secret lower-cased hid any name holding the password word in another case: the Electron E2E source "Stress Xtream Portal" (password `stress`) showed its host, and the deletion test could not find its row. A password is case-sensitive, so it is compared as written; the text is still read fully decoded, so `%3a` escapes count as `:`. A MAC is the same address in either letter case and is compared lower-cased. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(sources): match a MAC from the URL in any letter case Only the source's own `macAddress` was compared caselessly, so an M3U URL carrying `?mac=…` (nested ones too) left the MAC visible in a name written in another case. Any MAC-shaped secret, and every form of the source's own MAC, is now compared lower-cased; passwords keep their case. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * perf(sources): keep source-health helpers off the startup path Master's #1931 lowered the initial-bytes baseline, and this branch then sat 535 bytes over its slack. Most of its growth was runtime helpers in `@iptvnator/shared/interfaces` (reachability, health history, reason and fingerprint helpers) that only the lazily loaded source services call: the bundler put them in a chunk the app preloads. They now live in `@iptvnator/portal/shared/data-access` beside those services (types stay in interfaces); initialBytes 1,350,438 → 1,349,048, 855 under the limit. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
4281194cb4 |
fix(a11y): accessible names for icon-only buttons and a guard (#1880)
* fix(a11y): accessible names for icon-only buttons and a guard
Icon-only buttons named only by a matTooltip (or by nothing) had no
accessible name: the icon ligature is hidden from assistive technology
and a tooltip only adds a description. 22 buttons on master, in .html
and inline templates, now carry a translated aria-label bound to their
tooltip key. The channel row's favorite star keeps one label
("Favorite") and reports its state through aria-pressed.
New guard `pnpm run a11y:icon-buttons:validate` (CI step) fails on a
<button> whose only content is mat-icons (through control flow,
ng-container and spinners) without aria-label, an aria-label binding
or aria-labelledby. The inline-template lookup moves to a shared
tools/nx/inline-templates.mjs used by the icon-ligature guard too.
web-e2e icon-button-names.e2e.ts runs axe's button-name rule on a
channel list, the channel details dialog, Sources, the playlist info
dialog and an Xtream search. Five new keys are translated in all 18
locales.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* fix(a11y): text hidden from assistive technology never names an icon button
Greptile: a static aria-hidden="true" child's text counted as the
button's name, so <button><mat-icon/><span aria-hidden="true">Close</span>
passed the guard. Hidden subtrees now add only their icons.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
|
||
|
|
039238b7bc |
fix(settings): show the installed version without the GitHub release check (#1879)
The settings facade filled its version signal only inside the GitHub releases callback, so offline or under an api.github.com rate limit the installed version was missing from the sidebar's About item and the About page. The version is known locally from DataService, so the signal starts with it. The release check also gets an error handler: SettingsService already logs the failure, and without it the error resurfaced uncaught. The sidebar layout E2E no longer needs to stub the releases request; a new E2E aborts it and expects the version in both places. Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
91e277e9ba |
fix(settings): fit long versions in the sidebar and keep side panels off the window edge (#1875)
* fix(settings): fit long versions in the sidebar and keep side panels off the window edge The About item's version was flex-shrink: 0 beside a zero-basis label, so a nightly build string squeezed "About" to nothing and overflowed the panel. The label now has an auto basis and the version yields first, truncating with an ellipsis and keeping the full string in its tooltip. The context panel used height: 100% plus padding without a border-box reset, so it ran 24px past its host and the footer item sat on the window edge. The host is now a flex column and the panel fills it as a flex item, which restores the bottom padding for every side panel variant. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * test(settings): answer the GitHub releases check in the version layout E2E The sidebar renders the version only after the releases request answers. CI runners are rate-limited on api.github.com, so the tag never appeared and the test failed on its first locator. Fulfil the request locally with an older release so the version shows without an update badge. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
17a2b1b3b0 |
fix(ui): visible keyboard focus everywhere via a global focus-visible fallback (#1866)
* fix(ui): visible keyboard focus everywhere via a global focus-visible fallback The global stylesheet removed the outline from every input, button, textarea and `:focus`, so Tab users saw no focus on about 110 raw buttons: detail actions, season tabs, chips, title results, list rows. - Remove the outline only under `:focus:not(:focus-visible)` and draw a fallback ring on every other `:focus-visible` element. Both rules sit in `:where()`, so any component that styles its own focus still wins. - One recipe: `focus-ring-declarations` moves to `libs/ui/styles/_focus-ring.scss` and reads `--app-focus-ring`, declared per theme (light #1d63e0, dark #8cbaff) with at least 3:1 on every app surface and selection tint; `m3-theme.spec.ts` measures it. The card ring, the detail actions, the portal sidebar and both context panels use the mixin (the panels' selection-blue ring fell to 2.97:1 on the active item). - Material Tab stops (buttons, switches, button toggles, checkboxes) take the ring over their 12% focus state layer; menu items and options keep Material's highlight. - Surfaces over video (player controls, vendor chrome, fullscreen panels, Up Next rail) point the ring at the player's text colour. - The selected season tab drew its border with `outline`, which outranks the fallback; it is a spread shadow now. - Guard: `pnpm run styles:focus-visible:validate` (CI) rejects a global `outline: none` on an unscoped selector and a missing fallback. - Electron E2E `keyboard-focus-ring.e2e.ts` tabs through the detail actions and season tabs, a catalog grid with its refinement chips, the Sources list and Settings in both themes: one ring per stop, 3:1 where measurable, none after a click. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(ui): keep the player ring in the fullscreen panel; tighten the focus guard and E2E Local review round 1 (Codex P2, Greptile 3×P2): - The fullscreen channel panel carries `dark-theme`, whose context declares the theme ring again, so its own controls ringed in #8cbaff. Point the token back at the player's text colour on `.fullscreen-channel-panel .dark-theme`; the web series-playback E2E checks the close button's ring. - The guard took a container-scoped rule (`.panel :focus-visible`), a mixin body or a media query as the global fallback. The fallback is now the whole selector, outside any at-rule. - The keyboard-focus E2E now fails a ring that an `overflow: hidden` ancestor (up to the scroll container) cuts. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(ui): show focus where a component removes its outline; catch invisible rings in the guard GitHub review round on #1866 (Codex P2, Greptile P2): - A component rule that sets `outline: none` outranks the zero-specificity fallback. Re-audited every one: two hid a Tab stop with nothing else to show. The EPG list row (`role="button"`) now draws an inset ring, and the command palette underlines its search row while the field has focus. The others already show focus on the field's wrapper, on another element, or as a highlight inside an arrow-key composite (the "…" menu, the guide's search listbox); the guidelines now state the rule. - The guard read only a bare zero or `none` as a removal. It now reads a zero width, a `none`/`hidden` style or a transparent colour anywhere in the shorthand or longhands, so `outline: 0 solid transparent` is reported and `outline: 2px solid transparent` is no fallback. - The keyboard-focus E2E walks the live EPG list and the command palette too, and reads an inset or offset shadow line as a ring. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(player): keep --pc-text a literal palette colour Master's palette spec (#1854) reads `--pc-text` from the controls host as a literal; this branch had made it `#{palette.$text}`, failing "sets the timeline label on the dense glass" on the merge ref. The host declares the literal again, and a spec keeps the palette's `$text` (the focus ring token for surfaces beside the host) equal to it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * build(epg): declare the ui-styles dependency of the list row's focus ring The EPG list row now `@use`s `libs/ui/styles/_focus-ring.scss`; Nx cannot infer a Sass import, so `ui-epg` declares `ui-styles` like the other consumers of the shared partials. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(tooling): a styleless outline shorthand hides focus too `outline: 2px` or `outline: red` resets the style to `none`, yet the guard counted either as a visible fallback. A shorthand without a drawn style (or a `var()` that may carry one) now counts as removing the outline, as do `initial` and `unset`. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
e74a03b8e0 |
feat(playback): report Embedded MPV file chapters on the timeline (re-land #1771) (#1874)
Re-lands #1771, which was merged into the branch of #1768 after #1768 itself had been squash-merged and so never reached master: - Native: the macOS addon, the Windows libmpv addon and the frame-copy helper observe mpv `chapter-list`; snapshots carry `chapters: [{ timeSeconds, title? }]`, cleared on every START_FILE. - Main: `normalizeEmbeddedMpvChapters` validates the field into `EmbeddedMpvSession.chapters` (older binaries give []). - Renderer: `buildChapterTimelineSegments` maps chapters to timeline segments; host catch-up segments keep precedence, and a closing-credits chapter times the Up next card. Chapters flow through the same timelineSegments path as catch-up programmes, so they get #1854's keyboard and screen-reader naming, bounded label and 3:1 separators; specs and the frame-copy E2E now assert the chapter name reaches the slider's aria-valuetext. The packaged Linux smoke registers each media server for cleanup as it starts. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
98784815b1 |
fix(xtream): preserve content identity in PWA collections (#1870)
* fix(xtream): invalidate detail initialization on close * fix(xtream): preserve typed PWA collection identity * fix(xtream): complete typed live collection actions * test(xtream): cover PWA collection identity through reloads * fix(xtream): keep typed collection actions local * fix(xtream): tolerate quota limits during collection migration * fix(xtream): prioritize requested collection saves * fix(xtream): align collection identity with current playback contracts * test(xtream): confirm playback before collection history assertions |
||
|
|
50d45bc7c0 |
feat(details): redesigned episode list, section rhythm and hero fade (#1859)
* fix(details): continue the hero artwork under the first section The vertical scrim stopped at the hero's bottom edge, so a bright backdrop ended in a visible band above the Episodes heading. The hero now grows by a 140px tail that the shell pulls the sections up over, and the scrim resolves to the exact page surface behind the heading. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * feat(details): one section header and a 56px rhythm below the hero Episodes, Cast & crew and Similar now share app-detail-section-header: an 18px/600 title, a muted tabular counter and lead/end slots. The Episodes counter adds the watched count ("8 episodes · 3 watched"), and sections sit 56px apart, with 56px after the last one. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * feat(details): flat episode rows with a reserved action slot Each episode is now one app-episode-item, a list row or a grid card: - a number column, a 168px thumbnail with a watched check, a progress bar only for started episodes and a play overlay - title with "46 min · 12 Jan" (time left once started) and a plot clamped to two lines at 74ch - mark watched, download and a "…" menu in a 112px slot that is always reserved and shows on hover or keyboard focus, so nothing shifts - the whole item is one stretched button: Tab focuses it, Enter plays - the playing or last unfinished episode is highlighted The "…" menu holds Episode details and, for a started episode, Play from beginning, which Xtream and Stalker now start at 0. TMDB's episode runtime fills the meta line when the provider sends none. The list is the default view for anyone who has not chosen one. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * feat(details): segmented list/grid switch and a 30px season pill The view toggle becomes a 2px-padded segmented track with 30×26 segments and a neutral checked segment; the season pills and the season dropdown share the section header's 30px pill. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * feat(details): no season synopsis that repeats the series description Providers often send the series plot, or its first sentences, as every season's description, so the same text showed twice a few hundred pixels apart. The season strip now drops a synopsis that equals the hero's description, or is cut short from it, and clamps its own to two lines at 72ch. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(details): draw the episode thumbnail hairline as a border An inset shadow over the artwork is invisible to the surface-contrast checks and to forced-colors mode; a 1px border is what both read. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * test(e2e): follow the list-first, flat episode items The list is now the default view, rows and cards share .episode-item classes, and grid titles no longer carry the "N." prefix. The surface checks measure the thumbnail hairline instead of a row border, assert that hovering a row does not move its title, and capture list, grid and hover screenshots. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * docs(ui): episode list, section rhythm and hero tail contracts Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(dashboard): continue the hero artwork under the first rail The dashboard hero's fade ended at its edge, so the artwork stopped just above the Continue Watching heading. Like the details hero, it now grows by a 160px tail that the rails are pulled up over: the art fades behind the first rail's heading and reaches the page colour before its cards, so the rail's scroll-edge fades never show as a box. The narrow layout, whose slide carries its own scrim block, keeps no tail. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * docs(changes): note the smoother hero fade Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * feat(details): season chips up to four, a counts menu from five, no season art The season picker drops every poster: chips for up to four seasons, a menu button from five whose rows read the season and "N episodes · M watched". The season synopsis loses its cover, sits on the number column 24px above the list and renders nothing without a plot of its own. The player's episode panel shares the picker and loses its dropdown thumbnails too; its season strip stays. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * feat(details): bare rows for seasons without metadata, skeletons while it loads A season whose final episode data has no plot and no usable still (the series poster or season cover repeated as a still counts as none) renders 44px rows: number, title, meta and an inline check or progress bar, the same action slot, no grid toggle. While the provider list or a TMDB lookup that could still fill a bare-looking season is outstanding, the episodes are skeleton rows at the full row's exact geometry instead of a spinner; a season the provider already describes renders at once. Xtream tracks the show match and each season's enrichment in the store; Stalker tracks its show match in the selection state and settled season fetches in its TMDB service, and now reports a regular series' season request as loading instead of an empty series. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(details): loading skeletons laid out like the loaded page The hero skeleton targeted ngx-skeleton-loader's old `.loader` class, so its blocks kept the library's light default fill and margins; `display: block` stacked the chips and buttons into columns; and the details column sat at the top while the loaded one is bottom-aligned, so the title dropped ~150px on load. It is now plain shimmer blocks at the loaded hero's geometry with the stage height kept while loading. The Xtream series page shows an episodes section skeleton under it, and the Stalker series hero holds the Play button's place while seasons load. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * test(e2e): the modeled Stalker series renders bare rows Its episodes repeat the series poster and carry no plots, so the shared series surface check now expects 44px bare rows without thumbnails or a grid toggle in both themes. The thumbnail hairline and grid checks stay in the Xtream suite, whose episodes have stills. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * style(details): format the season picker stylesheet Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(details): episode skeletons never outlive their metadata The loading state added for TMDB-enriched episode rows could stick or flicker: - an empty season is never enriched, so its host never settled it and the season showed six skeleton rows forever instead of its empty state; - Xtream re-marked an enriched season pending on every selection write, turning rendered rows back into skeletons (and dropping row focus) for each cache read; - a superseded lookup of a reopened series, or of a Stalker item without an id, could settle a newer lookup's pending state or never clear it; - TMDB requests have no timeout, so a blocked TMDB host held provider episodes back for minutes. Metadata now only holds back a season that has episodes, the wait is capped at 4s per season, only the latest lookup of a key (or selection) settles it, a settled Xtream season stays settled within a visit and a new visit starts its seasons afresh. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(details): described seasons render at once; room for titles on phones Metadata lookups held every season as skeletons until TMDB answered, even one the provider had already described with plots and stills. The state now waits for metadata only when the current data would render bare; described rows show at once and the metadata lands in place. At a pane width of 480px or less the list row kept the 112px thumbnail and the action slot beside the text, leaving a 320px phone about 18px for the title. The thumbnail shrinks to 96px there and the actions take their own row under the text. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * fix(details): keep phone-width grid cards stacked The phone-width grid-area placements applied to grid cards too, which declare no template areas, so a card's artwork and text overlapped in a narrow pane. The placements are scoped to list rows; the surface check now also asserts a card's text stays under its artwork at 360px. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * fix(details): a still that only one episode carries is not a repeat The distinct-stills check counted unique image URLs and treated a single one as the series poster repeated, so a season where one episode has a genuine still and the rest have none lost that still and went bare, with its grid toggle. One image on one episode is now a still; only one image on several episodes counts as a repeat. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * fix(details): phone-width skeleton rows match the rows they precede At a pane width of 480px or less the finished list row uses a 96px thumbnail and an action row under the text, but its skeleton kept the 112px single-row layout, so pictures and text moved when loading ended. The skeleton row now carries an empty 34px action slot and mirrors the phone layout. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * fix(details): no air date from a placeholder or an impossible day The Date constructor turned a provider's "0000-00-00" into a day in 1899 and rolled "2025-02-31" into March, so the episode meta line showed dates nobody sent. An ISO day is now validated part by part and an invalid one leaves the date out. The season menu's container colour goes through mat.menu-overrides(), as the UI guidelines require. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * style(details): spinner colours through mat.progress-spinner-overrides() The episode item set the spinner's indicator colour as raw --mat-* declarations; the UI guidelines want Material tokens set through the component's overrides mixin, which rejects unknown names at build time. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * fix(details): unknown season counts, resume position, hero-skeleton spec A lazy Stalker VOD season the portal has not answered yet showed "0 episodes" in the season menu: the picker now takes the per-season load states and shows no count for such a season. A started episode whose duration nobody knows (no provider or TMDB runtime, none saved with the position) lost its saved position from the meta line; it now reads "Resume at 12:34". The downloads offline-detail spec queried the ngx-skeleton-loader the hero skeleton no longer uses; it queries the hero-skeleton test id. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * fix(playback): a reused external player starts each load at its own offset MPV and VLC are launched with a global --start / --start-time for a resumed title, and a reused process applies that to every later load: "Play from beginning", the next episode and a later resume all began at the first launch's offset. The seek sent right after loadfile does not help — mpv rejects it before the file is loaded, and the command sender never reads the reply (verified against a real mpv over IPC). Every reuse load now carries a per-file start (0 unless an offset is requested): mpv's loadfile options and VLC's :start-time input option. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * fix(details): loading flags follow the detail container, menus know unloaded seasons The fullscreen episode picker reports its season through the same onSeasonSelected as the detail container, so the Stalker view's episode-list and metadata loading flags followed whichever season was picked last: choosing another lazy season in fullscreen turned the detail page's loaded season into skeletons until the portal answered. Both flags now follow the detail container's own selection. The fullscreen panel also shares the season picker but never forwarded its per-season load states, so a lazy season read "0 episodes" in its menu; the states are forwarded now. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * fix(details): Xtream metadata loading follows the detail container too Like the Stalker view, the Xtream seasons service keyed its metadata loading flag off the season selected last, which the fullscreen episode picker also sets: picking another season there could hide the detail page's settled bare rows behind skeletons for up to four seconds. The flag now reads the detail container's own selection; the picker's choice still gets enriched. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
8030ced95a |
feat(settings): grouped navigation, cards and switches from the settings concept (#1853)
* feat(settings): grouped navigation, cards and switches from the settings concept Implements the Claude Design "Settings concept" handoff. Navigation: the sidebar is a 20px "Settings" title over App, Library, Devices and Data groups, with About pinned to the footer beside the installed version and an update badge. The active item is a soft fill without a border. Esc leaves settings like the header Back. Reset is no longer a page: its one destructive action is the last card of "Backup & data". Pages: a title and one-line subtitle, then rows grouped into titled cards with one right-aligned control column. Selects are compact without floating labels, checkboxes are switches, segmented controls are pills, and descriptions are capped at 56ch. "Show subtitles" moves to Playback, the Embedded MPV note becomes a callout under the player select (only while it is selected), and the TMDB attribution becomes the About footer. EPG: Add and Refresh all sit in the Sources header, the empty state has its own Add, the format examples are one line, Clear EPG data is its own row and the offset is a stepper. About: version, update state and channel share one card; the nightly warning is a callout shown only while Nightly is selected, with a shortcut to the backup page; the support buttons are neutral with coloured icons. Save model: the save bar stays (design option B) and now floats over the content column, counts the staged changes, answers Cmd/Ctrl+S, and marks pages with staged edits in the sidebar. A saved change that waits for a restart shows a dismissible notice. Rows stack under 600px of pane width (the page is a size container, so the persistent sidebar is accounted for). Page-specific styles moved into the About, EPG, Backup and Remote control section stylesheets to stay within the component style budget. New SETTINGS keys are translated in all 19 locales and NAV_RESET is removed; brand names and loanwords that stay English are in the identical-value baseline. E2E: Material slide toggles report aria-checked after the next render, so both suites toggle through a setSwitch helper; the settings nav is a navigation landmark, so the Dashboard rail link is scoped to the rail. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * fix(settings): compare restart notices against the running app, fix CI fallout The restart notice now lists only the restart controls whose saved value differs from what the running app uses: the stored values at first load, or what the embedded engine reports it actually runs for the frame-copy opt-in. Saving the launch value back withdraws the notice instead of leaving it up with no chip to explain it. The refresh reads the current list untracked and writes only a changed one, because the engine probe effect calls it. CI: the zoneless checklist dropped the deleted Reset section component, and the theme-tokens Electron E2E floats the recording folder label on the Playback page now that the settings selects carry no floating label. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * fix(settings): keep a dismissed restart reminder away until the setting changes "Later" now records the saved value it dismissed, for the rest of the app run, so an unrelated save (or reopening Settings) does not bring the same reminder back. It returns once a restart setting is saved with another value. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com> |
||
|
|
acb8cb0318 |
fix(workspace): lead header Back to a parent route when the page opened the session (#1830)
* fix(workspace): lead header Back to a parent route when the page opened the session Settings, Discover, actor and in-portal search registered a header Back that only ran Location.back(). As the first entry of the session (deep link, reload, restored view) that did nothing in Electron and left the app in a browser. WorkspaceBackNavigationService.back(resolveParent) keeps Location.back() while the previous entry is an in-app one, and while that is unknown because the Navigation API is missing. Otherwise it opens the page's parent with replaceUrl, so history Back cannot return to the page: - Settings: the first workspace view (resolveDashboardPath()). - Discover: the catalog section it lists (vod for movies, series for TV). - Actor and search: the portal root, which redirects to its default section within the same navigation. The web E2E opens these pages in a fresh tab: a page.goto in the same tab leaves the previous document behind, often at the parent's URL, so history Back passed without the fix. Electron covers settings after a window reload. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(workspace): lead first-entry Back to the parent without the Navigation API Review follow-ups (Greptile): - Without the Navigation API (older Safari and Firefox) back() always called Location.back(), so a page that opened the session still left the app. The service now tracks the router's in-app history depth there (trackRouterHistoryDepth): first navigation 0, push +1, replacement keeps it, a traversal restores the depth recorded for its entry. Depth 0 opens the parent; an unknown depth (an entry from before a reload) keeps Location.back(). - Stalker's Discover (movie/tv section), actor and search pages now have tests that they hand the service the parent under the portal :id. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(workspace): adopt the router navigation the Back depth tracker missed Review follow-up (Codex, Greptile): the lazy workspace shell creates the Back service after the first NavigationStart, so the tracker saw only its NavigationEnd, left the depth unknown and counted the next push as the first entry. It now adopts the router's current or last successful navigation when it starts: a first navigation is depth 0, a later one leaves the depth unknown (browser history Back), and a late start of the adopted navigation is not counted again. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: 4gray <fourgray@proton.me> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
2dfdd65f53 |
refactor(details): give the detail-page files real max-lines headroom (#1813)
* refactor(xtream): split the series details page into focused services serial-details.component.ts sat at the 400-line max-lines limit and its playback service and spec were close behind. Move cohesive concerns out without changing behavior: - route params, the provider-only flag and the (re)load of the addressed series go to SerialDetailsRouteService; the component still registers the effect, so effect order is unchanged - season descriptions, posters and the TMDB season enrichment go to SerialDetailsSeasonsService - actor, Similar and Discover navigation go to injectXtreamDetailNavigation, shared with the movie page - the watched toggles and the episode playback payload leave SerialDetailsPlaybackService for SerialDetailsWatchToggles and buildSerialEpisodePlayback - the spec's TestBed moves to a harness and the watched-toggle cases to serial-details.season-watch.spec.ts Counted lines: component 400 -> 318, playback service 388 -> 342, component spec 1196 -> 674. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * refactor(xtream): move VOD route selection and position state out of the page vod-details-route.component.ts grew from 618 to 741 counted lines and vod-details-playback.service.ts reached 395. Extract without changing behavior: - the route-derived read model (selected movie, category, catalog item, fallback view, multi-source identity, session and content keys) goes to VodDetailsSelectionService; the component keeps the same member names - trailer state and the Similar-rail click move into the hero presenter, the cancel-download prompt and the progress-ring geometry into the downloads service, navigation into injectXtreamDetailNavigation - stored positions (last seen, route row, guarded load) become VodDetailsPositionState and the external-launch bookkeeping becomes VodExternalLaunchClaim - drop the unused MatTooltip import (NG8113) and eight unused imports Counted lines: route component 741 -> 492, playback service 395 -> 344. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * refactor(stalker): extract series-view positions and watched toggles stalker-series-view.component.ts grew from 1601 to 1813 counted lines and its spec reached 1198 of 1200. Move code out verbatim: - saved positions, their reconcile and the persist/clear writes go to the component-provided StalkerSeriesPositionsService; the ordering of reads and writes goes to StalkerSeriesPositionQueue - episode, season and series watched toggles go to StalkerSeriesWatchToggleService, the batch core to runStalkerWatchToggleBatch - the lazy VOD season loads go to StalkerVodSeasonEpisodeLoader Every effect stays registered in the component constructor in its original order; template bindings and public member names are unchanged. The spec setup moves to a shared harness and the spillover-prefetch and TMDB season cases to their own specs; the 184 cases of the directory are unchanged. Counted lines: component 1813 -> 1136, component spec 1198 -> 670. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * refactor(stalker): extract catalog-detail position and search paging - stalker-catalog-detail.component.ts (397): the stored VOD position and its runtime updates become StalkerCatalogVodPosition, the Play/Resume start becomes startStalkerCatalogVodPlayback - stalker-search.component.ts (776, baselined): the paging resource, the accumulated results and the parental-lock bookkeeping become StalkerSearchPagingController, with pure helpers in stalker-search-results.util.ts. The spec reaches the moved members through component.paging; its cases and assertions are unchanged. Counted lines: catalog detail 397 -> 317, search 776 -> 482. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * refactor(ui): give season-container and vod-details real line headroom - season-container.component.ts (396): season auto-selection and the seasonSelected emission move to createSeasonAutoSelectState, called at the same place in the constructor so effect order is unchanged; the episode subline becomes buildEpisodeSubline - vod-details.component.ts (393): the cross-portal Similar loader, the provider-only download state and the actor/Similar route helpers move to sibling modules Inputs, outputs, selectors and public members are unchanged. Counted lines: season container 396 -> 341, vod details 393 -> 337. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * test(web-e2e): move Stalker portal helpers into fixtures stalker.e2e.ts was at 1196 of 1200 counted lines. Move the mock endpoints, scenario MACs and page helpers to stalker-portal.fixture.ts and the embedded-series steps three tests repeated to stalker-embedded-series.fixture.ts. Every test stays in stalker.e2e.ts in the same order, with the same serial mode and OWNED_MACS reset. Counted lines: 1196 -> 981. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * docs: follow the detail-page extractions Name the Stalker watched-toggle and position services that now own the batch and reconcile code, point AUTH_REJECTED_MAC and the scenario MACs at stalker-portal.fixture.ts, and drop two stale statements about components sitting at the max-lines cap. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * docs(xtream): note why the seasons service is injected last Its TMDB enrichment effect keeps the position it had as the first effect of the component constructor only while it is created after the other services' effects. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> --------- Co-authored-by: 4gray <fourgray@proton.me> Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com> |
||
|
|
84aef83a6c | feat(workspace): move page Back buttons into the header and add a history fallback (#1814) | ||
|
|
ab8460338a | feat(ui): cinematic movie and series details pages and dashboard hero (#1792) | ||
|
|
a8dd1eaa97 |
fix(import): consistent add-source forms with masked passwords and URL errors (#1796)
* fix(import): consistent add-source forms with masked passwords and URL errors - Mask the Xtream password in add and edit (and the Stalker one in edit) behind a shared PasswordVisibilityToggleDirective: one translated "Show password" label, state in aria-pressed, type="button". - Give the Xtream server URL its own mat-error and a neutral hint instead of the EPG file error; give the M3U URL a mat-error. - Use "Playlist title" in every add form, "MAC address" casing, a single ellipsis in "Validating portal…" and one "Add playlist" submit label; translate the method radiogroup's aria-label. - Show Stalker refusals inline under the portal URL (role="status", like the Xtream connection test), translated in the template and cleared by edits; translate the snackbars for outcomes that close the dialog. - Translate new strings into all locales; reuse the identical Stalker URL error translations; fix MAC casing and ellipses; drop unused keys. - Unit specs per form, edit-dialog spec, new add-source-forms web E2E; update E2E locators; UI guidelines Forms section; Stalker contract. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(import): mask the password again when an add form is cleared Clear erased the password but left the visibility toggle on, so the next password typed in the Xtream or Stalker form showed in plain text. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
cb252940b2 | fix(workspace): move detail Back into the header and drop the rail brand (#1789) | ||
|
|
9c77e9e41a | test(web-e2e): assert M3U movie metadata in the visible About block (#1791) | ||
|
|
8b6fcf3560 | fix(e2e): let web-e2e:e2e run outside CI (#1779) | ||
|
|
adb4889b0f |
fix(player): keyboard focus, contrast and ARIA for controls and settings (#1769)
* fix(player): keyboard focus, contrast and ARIA for controls and settings
Dock and settings-panel icon buttons draw a 2px --pc-text ring on
:focus-visible, and Material's theme-coloured focus layer is off, so
keyboard focus shows on video in the light theme too. A focused selected
subtitle swatch now differs from one that is only selected.
Settings headings read --pc-text-secondary on denser glass
(--pc-glass-bg-dense, 0.86): 4.5:1 or more over mid-grey and white
frames. They wrap (overflow-wrap: anywhere, hyphens: auto), so long
German and Russian headings stay inside the sheet's heading column.
The settings panel is now radio groups only (SettingsRadioGroupDirective
over a CDK FocusKeyManager): one Tab stop per group on the checked option,
arrows, Home and End move focus without applying, and Space/Enter checks.
The dialog and its groups are named by real h2/h3/h4 headings, the
load-file action sits outside the subtitle radio group, the subtitle and
speed chips carry their value in their name ("Subtitles: English"), and
tune has aria-haspopup="dialog".
Adds a web E2E for the keyboard path in both themes with an axe check on
the open panel, and de/ru sheet heading wrapping; axe-core is a new dev
dependency for it.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* fix(player): arrows check settings radios; subtitle chip reads On
Review follow-up:
- Arrow keys, Home and End now check the settings radio they reach, as a
native radio group does (the directive clicks it, so the template's
handler applies the choice); an option the engine already reports as
checked is not applied again.
- With subtitles on but no track marked selected yet (the engine can
report the switch before the track list), the subtitle chip reads and
announces "On" (new SUBTITLES_ON key, 19 locales) instead of "Off".
- The swatch row has 4px padding on every side, so the outer focus ring
is not clipped at the scroll edge of the panel body.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* fix(player): re-apply a settings radio while a switch is pending
The arrow-key check skipped any option the engine still reported as
checked. Arrowing from audio track A to B and back to A before the engine
confirmed B therefore sent no command for A, and playback ended on B with
focus on A. An arrow move always lands on an option other than the last
one applied, so it now applies unconditionally.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
|
||
|
|
444ec06e94 | fix(detail): keep the Back button from covering detail content (#1763) | ||
|
|
525ca7bc44 |
fix(playback): show the Up next card near the real end of an episode (#1768)
* fix(playback): show the Up next card near the real end of an episode The card appeared a fixed 8 minutes before the end: most of a short episode, and far into the story of a long one, with no way to hide it. - Adaptive lead: 4% of the episode, clamped to 40 s … 3 min. - A closing-credits chapter in the last third, when timeline segments carry one, brings the card forward to its start (capped at 5 min). - Close button and Escape dismiss the card for the current next episode. - After 10 s (not while hovered) the card collapses into a one-line pill. - Seconds countdown in the last minute. - New playback setting "Up next card" (default on) turns it off. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(playback): address Up next card review feedback - Offer the Up next card setting for Embedded MPV only under the frame-copy engine; native view never mounts the shared controls. - Hovering pauses the collapse delay instead of restarting it. - Document that the card stays visible when the controls auto-hide. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(playback): restart the Up next collapse delay for a new episode - A card that stays mounted while its next episode changes gives the new item the full delay instead of the previous item's leftover. - The setting description no longer promises credit-based timing: no current series path supplies chapters yet. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
cb317bad4c |
fix(ui): keep dialog action rows on one line (#1762)
* fix(ui): keep dialog action rows on one line Settings "Unsaved changes" dialog: - Cancel / Discard / Save replace the phrase labels in all 19 locales, and the dismiss now comes first; the shared CANCEL key replaces the unused UNSAVED_DIALOG_STAY. - At the 640px phone breakpoint the actions stack one per row, full width, in DOM order. EPG programme dialog: - mat-dialog-title gives the dialog an accessible name. - The footer Close is the only dismiss; it comes first and the primary action last. - The archive copy/download tools move under their notice, so the footer stays on one row. - Channel rows now open it through EpgProgrammeDialogService, which owns the 540px config and a panel class scoping the surface overrides. Adds a web-e2e layout spec (en, de, ru, fr, hu, ar on one row; de and ru stacked on a phone), extends the Electron EPG spec to all three openers, and documents the dialog contract in the UI guidelines. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(epg): stack programme dialog actions on phones Below the 640px phone breakpoint the viewport caps the programme dialog, and a long translated primary label ("Regarder depuis le début") no longer fit beside Close. The footer had no wrap, and the dialog hides overflow, so the label was clipped. - At the phone breakpoint, the archive tools and the footer now stack one full-width button per row, in DOM order. - Buttons grow to fit their label, so a long label wraps inside its button instead of being clipped. - On desktop the footer can wrap again as a last resort. The new Electron test opens a past programme in French at a 360px viewport and measures both rows. It fails against the previous stylesheet (the footer buttons are 44px narrower than the row). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
dc3f829807 | fix(parental-lock): focus, tokens and phone width for lock UI (#1761) | ||
|
|
4e17fbed4e | fix(e2e): stop the web backend from outliving Playwright runs (#1747) | ||
|
|
d8229b98fa |
feat(playback): record recently viewed only after the stream plays (#1732)
* feat(playback): record recently viewed only after the stream plays A channel, movie or episode used to enter Recently Viewed (and the dashboard's Continue Watching hero) the moment it was selected or its link was resolved, so streams that failed straight away cluttered the history. Writers now defer the write to a root PlaybackHistoryGate, keyed by the stream URL and/or the playback session key. The inline players confirm those keys once the owned engine's position has advanced by two seconds (seeks, stalls, pauses and a previous stream's progress do not count), the radio player does the same, and a launched MPV/VLC session confirms on `opened`/`playing`. M3U with MPV/VLC configured keeps recording on selection. Covers M3U (live, radio, movie detail), Stalker (live, radio, VOD, series), Xtream VOD and series, and the global live collection. The M3U host's embeddedPlayback is now compared by value: the history write updates the playlist meta mid-playback, and a new but identical playback object remounted the engine and restarted the stream. E2E flows that relied on recording-on-click now play local fixtures (HLS/TS/WebM routed in place of unreachable or public streams) and wait for confirmed playback. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(playback): tighten recently viewed confirmation per review - Correlate by session key first: when both the deferred write and the confirmation carry a playbackSessionKey, only that is compared, so the same stream URL played in another playlist no longer records a failed attempt. URLs remain the fallback (portal writes, MPV/VLC sessions). The M3U radio player now receives the host's session key. - Count only playing progress: engines report `playing` (not paused, not seeking) with each time update, so short seeks of paused media no longer confirm a view. - Xtream: a write confirmed after a playlist switch still saves to its own playlist but no longer replaces the current playlist's recent list. - Global live tab: a row confirmed after another row was selected still moves to the top of an open Recently Viewed list (only a disposed tab skips the notification). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(playback): confirm session-keyed history only by its own session A write deferred with a playback session key (M3U) is now confirmed only by that key. The app-wide MPV/VLC session confirmation carries just the URL, so opening the same stream externally from another playlist could still commit an abandoned attempt. An "Open in MPV/VLC" recovery launch is instead confirmed by the WebPlayerViewComponent that requested it, under its own session key, once the launch has opened. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * perf(playback): keep the history gate off the initial bundle The `@iptvnator/services` barrel ships in the initial bundle, so adding PlaybackHistoryGate there (and subscribing to it from the app-wide ExternalPlaybackService) grew renderer.initialBytes by 1,141 bytes. - Move the gate to a new lazy-only `playback-data-access` project (`@iptvnator/playback/data-access`; scope:shared, domain:playback, type:data-access) and register it in the coverage policy. - The gate subscribes to MPV/VLC session updates itself; it is created by the first deferred write, which precedes the launch it waits for. ExternalPlaybackService is back to master. - The Xtream "playlist switched before confirmation" check moves to the lazy helper; the initial-path store only takes a `skipListRefresh` flag. Net effect on this branch: +27 bytes over master (master itself is 108 bytes over the ratchet baseline already). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * perf(playback): drop late Xtream confirmations off the initial path The Xtream store ships in the initial bundle, so even the small `skipListRefresh` flag cost 27 bytes there. A confirmation can only arrive after a switch to another playlist from a slow MPV/VLC launch (the inline player goes with the page), so the lazy helper now drops it instead: recording it would misfile the item or replace the other playlist's recent list. with-recent-items is back to master. This branch is now 3 bytes below master on renderer.initialBytes; the ratchet still reports master's pre-existing overage. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * test(playback): pass the spec type-check gate from master - playback-data-access: align tsconfig.spec.json with the epg-data-access config #1705 updated (bundler resolution, global.d.ts for window.electron). - M3U recent-history spec: type the selectSignal override. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(playback): keep late Xtream confirmations in their own playlist history A confirmation that arrives after a switch to another playlist (a slow MPV/VLC launch) is no longer dropped: the lazy helper saves it to the captured playlist through the data source, without reloading the store's recent list, which belongs to the other playlist by then. The store and its barrel ship in the initial bundle, so the save path stays in the feature helper; renderer.initialBytes stays under the baseline. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(playback): correlate global live-tab history by its session key The unified Favorites/Recent live tab deferred its history write by stream URL only, so the same URL played from another playlist could confirm a failed selection, and a switch to catch-up before confirmation could never match. It now defers with the tab's playlist-scoped playbackSessionKey (the key its players confirm with), and the tab's radio player receives it too. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(playback): keep MPV/VLC rows of the live tab confirmable by URL The live tab's session key can only be confirmed by its own inline players; MPV/VLC confirm the launched URL alone. A row that goes to an external player (also later, after a double-click) now defers by URL, and only rows played inline carry the session key. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(playback): per-channel M3U history attempts, capability-based Xtream fallback - M3U: the recently-viewed dedupe key now includes the channel id, so a second row of the same URL defers its own write (its session key) and is recorded when it plays after the first row failed. - Xtream late write: key uncached content by Xtream id per supportsXtreamSqliteDataSource (the data-source factory's contract), not by a generic Electron bridge. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: 4gray <fourgray@proton.me> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
be217d5cf5 | feat(playback): Hybrid redesign of the shared player controls (#1709) | ||
|
|
6b855eb73b |
fix(e2e): stop mock servers from outliving Playwright runs (#1710)
* fix(e2e): stop mock servers from outliving Playwright runs Playwright stops a webServer with a SIGKILL to the process group it spawned, but `nx run-commands` starts its command in a detached process group of its own. Launching the Xtream/Stalker mocks through `pnpm nx run *-mock-server:serve` therefore left the tsx server running (reparented to PID 1) and holding its port after every run, so the next run failed with "…/health is already used" or silently reused a stale server. Every Playwright config now starts the mocks as a single `node --import tsx apps/<mock>/src/main.ts` process with TSX_TSCONFIG_PATH=tsconfig.base.json, which stays in Playwright's group. A project-config spec guards all playwright*.config.ts files against regressing to the Nx launch. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(e2e): read sidebar categories atomically; tighten mock launch guard - category-management: readVisibleSidebarCategoryNames read items one by one; when Save removed an item between isVisible() and textContent(), textContent() auto-waited for the gone label through the whole 15 s poll, so expect.poll never retried (ubuntu shard 1 failed 3/3 while the UI already showed "No categories available"). Take one snapshot with filter({ visible: true }).evaluateAll() instead. - project-config.spec: pin which Playwright configs start which mock, reject any Nx form that mentions a mock server, and fail when a new config starts a mock without being listed (the old count check passed vacuously on zero matches). - docs: state which configs start which mock instead of "every config starts both". Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * revert(e2e): leave the sidebar category read race to #1728 #1728 fixes the same readVisibleSidebarCategoryNames race with a shared helper; keeping a second copy here would only conflict. This PR stays about mock-server lifecycle. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: 4gray <fourgray@proton.me> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
e45cd85a78 |
feat(settings): PIN-protected parental lock for categories (#285) (#1601)
* feat(settings): add PIN-protected parental lock for categories (#285) Locks are per category (Xtream category ids, Stalker genre ids, M3U group titles) and kept in one renderer lock store persisted to app_state / localStorage; `categories.locked` is the SQLite index re-stamped from it. While the lock is active the DB worker filters every content read, the PWA data source, the Stalker store and the M3U channel list filter in memory, and the enforcement service reloads the stores and steps off withheld selections. Settings → Parental lock sets the PIN (PBKDF2, never in Settings), the relock timeout and Lock now; lock toggles live in the Xtream/M3U management dialogs and a new Stalker lock dialog, all behind the PIN. Backups carry the locks per playlist entry. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * fix(settings): harden the parental lock after review - The M3U group dialog opens only after the PIN, like the Xtream and Stalker dialogs: it lists locked group names and can rewrite the locks. - Change PIN and Disable always verify the stored hash, even while the session is unlocked, so an app left unlocked cannot lose its lock. - Stalker paging judges progress on the raw portal page: withheld ids the list has not seen count as progress, a page made only of locked rows requests the next one itself, and the VOD total is reduced by withheld ids so the grid stops asking once every visible row is in. - Parental lock contract linked from the agent context map after the guidance reorganization; bridge helpers split out to stay under the file-size cap. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * fix(settings): guard locked categories on routes, PWA search and paging - Xtream and Stalker `:categoryId` routes carry a parental-lock guard: a locked category reached by URL prompts for the PIN and redirects to the section root on refusal (Electron row ids are mapped to provider ids). - PWA search filters withheld categories like the catalog reads. - Electron warm-cache detection confirms an empty, lock-filtered read with the unfiltered existence check instead of refetching from the provider. - A Stalker lock flip past page 1 drops withheld rows at once and restarts the list from page 1 instead of appending onto stale pages. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * fix(settings): compile the PIN hashing helper in the Node backend build The web backend compiles the shared interfaces library without DOM typings, so the DOM-only `SubtleCrypto` / `BufferSource` names broke its Docker build. The helper now describes the WebCrypto surface it needs structurally and reaches it through `globalThis`. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * fix(settings): close the remaining parental-lock gaps from review - Detail routes check the item's own category: a locked movie or series paired with an unlocked category id in the URL is still refused. - `requestUnlock()` awaits the settings load before it can answer "not active", so a slow startup cannot open a management dialog unguarded. - `SETTINGS_UPDATE` only persists the `parentalLockEnabled` mirror and releases the worker on switch-off; it no longer re-locks the worker on every ordinary settings save under a renderer that shows "unlocked". Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * fix(settings): cover PWA cold navigation, Stalker search and the PWA lock editor - The Xtream detail guard hydrates the PWA session cache before judging an item on a cold navigation and fails closed when the catalog cannot place the item. - The dedicated Stalker search route filters withheld genres, re-fires on lock changes, judges paging on the raw page and restarts from page 1 on a lock flip. - The Xtream category dialog loads its lock candidates through the capability-selected data source; the PWA source now lists its raw categories with lock flags, so locks can be configured there too. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * fix(settings): arm the relock timer on enable and harden Stalker search relock - The idle timer follows the unlocked transition instead of `active`, so the session that just enabled the lock still locks itself later. - Stalker search closes an open detail whose genre became withheld on relock and advances by itself past pages made only of locked rows (only while they add ids the list has not seen). Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * fix(settings): close lock editors on relock and clear withheld details opened from All The Xtream, Stalker and M3U category editors are gated by the PIN only when they open; an idle relock left them on screen listing locked names with a lock-rewriting Save. Each now closes itself when the session relocks. ParentalLockEnforcementService also judges the selected Xtream/Stalker item by its own category: a detail opened from All, recently added or search has no selected category to vanish with, so it stayed open after a relock. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * fix(settings): fail closed on unreadable settings and finish relock clean-up - Unreadable settings (IndexedDB load failure) left the feature switch at its default and announced "unlocked" to the main process. A stored PIN now stands in for the switch, and without one nothing is announced, so the worker keeps its mirrored locked default. - Lock applies run one at a time and abandon superseded results; the Electron data source keys its in-flight share by lock version so a relock can never reuse an unlock refresh's unfiltered rows. - The stored in-portal Xtream search is re-run on a lock change. - Stalker live/radio selections are judged by tv_genre_id, and both live layouts drop the playback of a channel whose category became withheld. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * fix(settings): fail closed on an unreadable lock store and make lock writes reliable - A lock store that cannot be read is no longer treated as empty: while the lock is active every category is withheld (renderer predicates and set-based filters alike) until the PIN is entered or the store reads again, and writes are refused meanwhile so an empty in-memory store can never wipe the persisted locks. The lock set now lives in its own ParentalLockLockStore service. - The M3U group dialog's lock write is awaited and a failed save is reported in a snackbar instead of being silently dropped. - The Electron categories.locked re-stamp clears and re-locks inside one transaction, so a failed restamp keeps the previous index. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * fix(settings): drop pre-relock Stalker search pages and fail closed on a corrupt lock store - A Stalker search page issued before a relock was filtered with the pre-relock withheld set and could still be applied after it; the staleness check now includes the parental lock version. - A lock store payload that does not parse or is not an object is a failed read (everything withheld until it reads again), no longer an empty store. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * fix(settings): close the startup, re-stamp, relock-refresh and switch-persistence gaps - The window before the initial lock store read settles now withholds everything, like an unreadable store: settings can report the feature as on before the locks are known. - The store commits before the SQLite index re-stamp; a failed re-stamp now rolls the store back, a failed rollback re-stamps on the next access, and every launch re-derives the index from the store. - Xtream category/content reloads fail closed: a rejected reload empties the affected lists (content types drop back to idle) instead of keeping rows read under the previous lock state. - Enabling/disabling the feature persists through one guarded path that undoes the in-memory switch and skips the Electron mirror on a failed settings write. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * test(xtream): move the parental-lock reload specs beside the content spec The content feature spec sits at the 1200-line spec cap. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * fix(settings): await the startup lock-index reconciliation and withhold genre-less rows when failing closed - The lock store is readable only once the SQLite index has been re-derived from it, and a re-stamp that keeps failing keeps the session fail-closed, so catalog reads can never serve rows stamped unlocked by a stale index. - While everything is withheld, Stalker rows without a genre are withheld as well (the store filter and the renderer predicate). Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * fix(settings): await the enablement mirror, restore partial lock stamps and validate nested lock-store entries - The Electron mirror of the feature switch is awaited; a mirror that cannot be written undoes the settings write, so a reload never starts from a mirror that disagrees with the persisted switch. - A failed multi-type re-stamp rolls the store back AND re-stamps every touched type from it, since earlier types may already carry the new locks; a failed rollback keeps the playlist stale (fail-closed). - A persisted lock store whose nested entries are not what writeLocks produces is a failed read, not an empty store. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * fix(settings): withhold the Xtream catalog at relock time, keep exact M3U titles in backups, roll back a failed relock-timeout save - A relock now fails closed immediately: the selected detail is stepped off against the lock store, the catalog lists and stored search results are emptied, and the filtered reloads publish only while the captured lock version is still current. - Backups carry M3U lock titles verbatim (exact dedup), since the locks match group titles exactly. - A relock-timeout write that fails reverts the in-memory value and shows the settings save-failure snackbar. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * fix(settings): clear the lock index before a playlist's last lock leaves the store, retry failed PIN reads, guard backups on the lock store - A write that removes a playlist's last lock clears the SQLite index first and drops the store key afterwards, so an interruption between the two can only leave a state the startup reconcile repairs toward locked. - A PIN hash read failure is distinct from an absent PIN: the session stays locked and every PIN-protected step re-reads it first. - Backup export awaits parental lock initialization and refuses to run while the lock store is not readable, since an absent lock field means "no opinion" on restore. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * fix(settings): withhold Electron Xtream reads while locks are unknown, persist the switch when settings are unreadable, re-stamp after a recovered read - ElectronXtreamDataSource serves no categories, content or search hits while the lock store withholds everything; its SQLite index may still carry a stale stamp. - setupPin decides whether to persist the switch from the settings value before the PIN is stored, since enabled follows hasPin while the switch is unknown. - A lock store recovered by a later read marks its playlists stale so the index is re-derived, a persisted entry must carry all three lists, and a stale Stalker search page is dropped before touching the withheld-id bookkeeping. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * fix(settings): keep unlocked category routes reachable and defer a relock reload that overtakes the initial hydration - The Xtream category guard no longer runs the item check on category-only routes (Number(null) is 0), which prompted for the PIN on every unlocked VOD and series category while the lock was active. - A lock change during the initial Xtream hydration withholds the rows the hydration publishes and runs the filtered reload once it has settled, on every path that marks the content initialized. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * fix(settings): resolve hidden live categories before relocking playback and reload categories in the deferred hydration path - The Xtream live layout resolves a playing channel's category through the unfiltered rows when the visible list lacks it (search can play a hidden category's channel); until that lookup lands the category is unknown and a relock stops the channel. - A relock that overtakes the initial hydration now withholds the category publications too and reloads categories with the content. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * fix(settings): step off the M3U channel and Stalker selection before awaiting the Xtream relock reload The Xtream store stays populated after leaving that portal, so its reload runs on every apply; a locked M3U channel no longer keeps playing behind a slow database or provider read. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * fix(settings): gate the workspace on parental lock init, edit only a readable lock store, guard the deferred reload, validate backup lock entries - The workspace route resolver awaits ParentalLockService.initialize() next to the settings load, so no route or catalog activates before the PIN and lock store are known. - Every lock write re-reads a failed store before building its edit, so a recovered store is edited rather than overwritten. - The deferred hydration reload runs under the publish guard of the request that deferred it. - Backup import validates every parental lock entry and rejects a damaged list instead of erasing the persisted locks on restore. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * fix(settings): discard stale hidden-category lookups and key withheld Stalker rows by their real identity - A hidden-category lookup that lands after a later playback (same provider id, another playlist) no longer overwrites the newer channel's category; resolutions are generation- and playlist-checked. - Withheld Stalker rows are keyed by id, stream_id, movie_id, series_id or the row's cmd/name, so id-less rows no longer collapse onto one key and stall paging past locked pages. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * fix(settings): gate the Electron cached category/content reads while locks are unknown The warm-route hydration reads the cache directly; it now returns nothing while the lock store withholds everything, like the live reads. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * fix(settings): retire in-flight searches on relock clearing and publish lock revisions after the stamps - clearSearchResults() advances the search request version, so a search issued under the previous lock state cannot republish what a relock just cleared. - A lock write publishes its store revision only once every touched type is stamped, so a reload triggered by it cannot read a later type through its old stamps. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * fix(settings): retire a resolving Stalker live playback when the session relocks The embedded player defers selecting the channel until its stream resolves, so the enforcement service's cleared selection could not retire the request; it now carries the lock version it was issued under and is dropped when a relock happened meanwhile. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * feat(settings): one lock entry point per rail plus a right-click Lock/Unlock - Stalker's dedicated lock button becomes the same "Manage categories" (tune) button the Xtream rail has; it opens the lock-only dialog, so every portal type shares one entry point and the rail header keeps three actions. - Right-clicking a category (Xtream, Stalker) or an M3U group offers a single-row Lock / Unlock through the shared CategoryLockMenuComponent, behind the same PIN gate and lock store as the dialog. - The settings hint explains where locks are set; group lock strings added to all locales (ru/de translated). Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * fix(settings): serialize lock-store writes and drop a deleted playlist's locks - Lock-store mutations run through one write queue: each rewrites the whole persisted store, so overlapping edits could otherwise snapshot the same store and the later write would drop the earlier edit. - Deleting a playlist removes its locks through the PLAYLIST_DELETE_CLEANUP hook; "Remove all playlists" clears the lock store once the deletion has succeeded. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(settings): apply single-row lock toggles inside the lock store's write queue The right-click Lock/Unlock (portal categories and M3U groups) built the new list before entering the queue, so two quick toggles shared one snapshot and the second dropped the first. Lock writes now accept an edit of the current list, evaluated inside the queue. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(settings): retry a failed settings read before any parental-lock settings write updateSettings writes the whole settings object, which after a failed startup read is the defaults; enabling the lock or changing the relock timeout then replaced the user's persisted preferences. The read is retried first and the write refused while settings stay unreadable. The settings writes move to parental-lock-settings-writer.ts. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(settings): show the locked-groups row on the M3U rail and roll the relock timeout back to the recovered value - The M3U groups rail now renders the same "N locked · Enter PIN to show" row as the portal category rail, so locked groups no longer vanish without an in-context unlock. - A failed relock-timeout write rolls back to the value read after the settings retry, not to the pre-retry default. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(settings): retry a failed clear-all of the lock store and keep restored new playlists free of stale locks A lock-store clear that failed after "Remove all playlists" only logged, so a later restore reusing a playlist id could inherit the deleted playlist's locks. The in-memory store now empties at once and the persisted clear is retried on the next access; a restore that creates a playlist starts it from empty locks. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(settings): count radio playback as lock activity and read the lock store before a restore's stale-id check - The idle relock no longer interrupts a playing radio station: playing <audio> counts as activity, like video. - A restore retries a failed lock-store read before checking a reused id for stale locks, and aborts while the store stays unreadable. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(settings): drop withheld Stalker search rows at relock time and keep the M3U unlock row when every group is locked - A relock during a page-1 Stalker search now filters the rows already on screen at once, so old unlocked results are not clickable while the replacement page is pending. - When every M3U group is locked the groups rail still renders, with its "N locked · Enter PIN to show" row, instead of the plain empty state. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * perf(settings): keep the PIN dialog and the Stalker enforcement step off the initial path Master (#1712) moved the UI component barrel and the Stalker data layer out of main.js and tightened the initial budget to 2 MB. The parental-lock prompt imported the PIN dialog through the ui/components barrel and the enforcement service injected the Stalker store at startup, which pulled both back in (2.55 MB, over budget). The PIN dialog now loads through a local lazy file on the first prompt, and the Stalker step loads only while a Stalker route is open: initial total 1.65 MB. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(settings): restore the lock index when an emptying write fails and publish rollbacks after re-stamping - Removing a playlist's last lock clears the SQLite index first; if the clear or the store write then fails, the index is re-stamped from the previous locks at once. Title matching and multi-source discovery query the worker directly and trust the index, so the stale flag alone did not protect them. - A rollback publishes its store revision only after every type is re-stamped, so a reload cannot read a later type through the attempted stamps. - docs: restore the index rules the earlier surfaces rewrite dropped from the contract, now in the Lock store lifetime section. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(settings): keep the M3U groups view when every group is locked With every group locked the filtered channel list is empty, so the container showed its generic empty state and the groups rail's "N locked · Enter PIN to show" row never appeared. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(settings): fail closed when the lazy Stalker enforcement step cannot load A rejected chunk (e.g. a stale PWA page after a deployment) escaped applyStalker(), so a locked Stalker selection kept playing after a relock and the Xtream step was skipped. The step now leaves the Stalker route on a load failure, which clears the selection and stops playback, and the Xtream step still runs. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(settings): defer a stale Xtream hydration as soon as the catalog is withheld On Electron a relock that overtook the initial content hydration waited for the category reload before the content reload set the deferral flag; the older unlocked hydration could publish its streams in that window. withholdCatalog() now sets the flag itself, before anything is awaited. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(settings): restore backup locks after the Xtream merge and snapshot the Stalker lock dialog's categories - A backup restore now writes the parental locks last, so a failed Xtream merge leaves the playlist's previous locks in place instead of the backup's possibly smaller set. - The Stalker lock dialog snapshots the category list before its lazy import and opens only if the route is unchanged, so another portal's categories can never be saved under this playlist. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(settings): fail closed on relock ahead of the apply queue and judge Xtream selections by the lock store - On relock the synchronous fail-closed steps (M3U channel, Stalker selection, the locked Xtream detail, catalog lists, stored search) run immediately instead of queueing behind an earlier apply that may still wait on a slow or hung read. - The post-reload Xtream checks decide by the lock store through the unfiltered category rows rather than by absence from the reloaded list, which also omits merely hidden categories; unreadable rows fail closed. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(settings): require the PIN for lock-bearing backup restores and fail closed during index re-stamps - A backup carrying lock lists replaces the matching playlists' locks, possibly with an emptier set; the import now asks for the PIN (after the file was chosen) and aborts when it is refused. - While a write re-stamps the SQLite index the playlist counts as stale, so a relock inside that window reloads fail-closed instead of through the old stamps. The internal store write now needs only a readable store, so a rollback can still land. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(settings): drop parental-lock Xtream reloads once the playlist is switched A reload issued for playlist A no longer publishes into the shared Xtream store after the user opened playlist B: the store's reloads guard on the playlist they read for, and the enforcement apply retires its search refresh and selection checks on a playlist switch as on a newer lock version. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(settings): re-ask the PIN before a relocked backup merge and keep the PIN cooldown across prompts A backup merge now asks for the PIN again right before it replaces a playlist's locks when the app relocked during the import, instead of relying on the answer given at the start. The wrong-PIN count and the 30-second pause move from the dialog into the lock service, so dismissing and reopening the prompt no longer resets them. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(settings): refuse lock removals that commit after a relock and keep the index stale until the store write lands Lock edits that take a lock away now commit only while the session is unlocked, checked inside the write queue at commit time, so an editor save still in flight (or queued) when the app relocks cannot remove locks. Adding locks stays allowed. The Xtream category dialog drops its lock draft after a relock, and a backup restore re-asks the PIN only when it would remove a lock. Clearing a playlist's last lock keeps its index stale until the store write has landed. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(settings): clear an Xtream detail from a hidden category synchronously on relock The synchronous relock step now clears a selected Xtream item whose category the visible category list cannot place (a manually hidden category opened through search), instead of leaving it usable until the awaited reloads and lookup finish. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(settings): fail closed when the Electron bridge lacks the parental lock worker filter A new runtime capability requires the lock-state and index-stamping IPC. When Electron reads Xtream through the SQLite worker without it (a partial or older preload), the locked session withholds every category instead of trusting a worker that never learned the lock state. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(settings): re-check lock removals at their durable commit points A lock removal that passed the unlocked check before its write is asked again right after the store write and, for Xtream, after the index stamps. A relock in between writes the previous store back or rolls the stamps back before anything is published. The stale-index bookkeeping, index stamping and store merge move into helpers to keep the lock store within the file size limit. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(settings): retry a failed revert of a refused lock removal and fail closed meanwhile When writing the previous store back after a relock-refused removal fails, the lock store now keeps a pending rewrite, is not readable (the locked session withholds everything) and rewrites the persisted store from memory on the next access, so a restart cannot load the removal. A failed "Remove all playlists" clear shares the same retry. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(settings): authorize lock removals when issued and close genre-less Stalker details in fail-closed mode A lock removal is now authorized right before its first write is issued; a relock that lands after that is ordered after the write, which completes. This drops the post-write rollback, whose own failure could leave the persisted store diverged from memory across a restart. The Stalker search closes a detail without a genre on relock while every category is withheld. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(settings): restore the previous locks when an Xtream rollback write fails When an Xtream lock edit's re-stamp fails and the rollback store write fails too, memory now goes back to the previous locks and a pending rewrite persists them on the next store access before the index is re-stamped, so the failed edit cannot take effect through that re-stamp. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * test(stalker): cover page-one rows leaving the screen on relock while the reload hangs Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(settings): offer the portal unlock row in fail-closed mode When the lock store cannot be read every portal category is withheld but no locked ids are known, so the rail showed no "Enter PIN to show" row. It now shows the row without a count in that state. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(settings): fail closed for direct worker title lookups and capture the Stalker lock dialog context before the PIN Catalog title matching and multi-source discovery query the SQLite worker directly; they now return nothing while the parental lock withholds everything (unreadable store or a bridge without the worker filter). The Stalker lock dialog captures its playlist, provider and section before the PIN prompt and re-checks them after it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(settings): retire multi-source alternatives on a lock change and keep reconcile off in-flight stamps The VOD multi-source host keys its discovery session to the parental lock version: a lock change drops the discovered sources, retires discoveries and switches in flight, and rediscovers through the worker's new lock state. Stale-index entries of a write still stamping are no longer retried by a concurrent reconcile, which could re-stamp from a store the write had not committed yet. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(settings): fail closed on a rejected worker lock sync, tear down Stalker synchronously and capture the Xtream dialog context before the PIN - A rejected lock-state sync to the SQLite worker makes the locked session withhold everything until a later sync succeeds. - The Stalker enforcement chunk is preloaded when a Stalker route opens; a relock runs it synchronously, or leaves the route at once while it is not loaded, instead of awaiting the chunk. - Xtream "Manage categories" captures playlist, provider and section before the PIN prompt and re-checks them after it and after the dialog import. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(settings): keep the relock timeout behind the PIN and bind M3U group lock toggles to their playlist A locked session can no longer change the relock timeout: the Settings selector is disabled until the PIN is entered and the service refuses the change while locked. An M3U right-click lock toggle now captures its playlist before the PIN prompt and is saved only if that playlist is still open. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(settings): reset a locked M3U channel however it becomes active The enforcement service now checks the active M3U channel whenever it changes while locked, so numeric zapping, next/previous and remote commands, which select from the full channel list, cannot start a channel of a locked group. Numeric zapping also skips such a channel. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(settings): bind the M3U group management result to its playlist The groups view captures the playlist before the PIN prompt and drops the management dialog's hidden and locked group lists once another playlist is open, so they cannot be saved under that playlist. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * docs(parental-lock): record the accepted restart case of a failed rollback write Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(settings): build bulk lock drafts only from a readable lock store The Xtream and M3U management dialogs offer lock toggles, and the Stalker lock dialog opens, only once the lock store has been read. A draft built from the empty fail-closed snapshot would otherwise replace the real locks with nothing on Save if storage recovered in between. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(settings): keep the parental lock switch on the saved state and roll back to the recovered value The Settings switch snaps back to the saved state when clicked and follows it once the PIN action succeeds, so a cancelled or refused PIN no longer leaves it showing the opposite state. A failed switch write is undone to the value read after the settings retry instead of the hard-coded inverse. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(settings): match noncanonical PWA Xtream category ids against their locks The PWA data source compared raw provider category ids such as "009" with locks stored as numbers, so such a category stayed visible while locked. Both sides are now compared in canonical numeric form. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(settings): close the M3U group editor on any relock The group management dialog lists every group name, locked ones included, even when it opened without lock toggles (unreadable lock store). It now closes on any relock, and the groups view re-checks the lock state before opening it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: 4gray <fourgray@proton.me> Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com> |
||
|
|
ea51cae3db |
feat(settings): search settings from the header and the command palette (#1714)
* feat(settings): search settings from the header and the command palette The header search on the Settings page was shown but disabled. It now searches a shared index of all 56 settings rows by translated title, description and English synonyms, replaces the section page with ranked results, and opens a result by scrolling to, focusing and briefly highlighting its row. Enter opens the best match, and the section navigation shows per-section match counts. The command palette gains a "Settings" group that lists the best six matches for a non-empty query, so any setting is one Ctrl/Cmd+K away. Rows hidden by the current form state fall back to the control that reveals them; rows the runtime cannot render are never returned. The index ships through a new @iptvnator/workspace/shell/util/settings-search sub-entrypoint so it stays out of the eager bundle, and a registry spec keeps it in step with the section templates. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(settings): let search reveals win over pending input and gate embedded MPV rows - A reveal (result click, command palette, Enter) now cancels a search keystroke still waiting for its debounce, so its q navigation can no longer supersede the reveal and leave the results open. - Embedded MPV extra options and auto-reconnect require a lazily probed embedded MPV capability; frame copy also needs frameCopyAvailable, so search never offers a row the settings page cannot render. - Keyboard users keep a focus-visible ring on the revealed row after the highlight fades. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(workspace): wait for palette probes without Promise.allSettled The web tsconfig lib predates Promise.allSettled; use Promise.all over rejection-safe probes instead. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: 4gray <fourgray@proton.me> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
7abaad29e7 |
chore(performance): commit the initial-bytes baseline and ratchet checker (#1693)
Second step of the performance-journeys ratchet, stacked on #1692 (merge that first; this PR retargets to `master` automatically). - `tools/performance/journey-baselines.json`: J1 `launch` / `renderer.initialBytes` = **2,739,510 bytes**, the ubuntu runner's production build of `apps/web` at this content (after #1692 stopped bundling `package.json` into `main.js`). A local macOS build of this pre-#1695 code is 2 bytes smaller in `main.js` (the eager locale imports); once #1695 removes them the two are byte-identical. Correction to an earlier version of this description: the "556-byte macOS vs Linux difference" was almost entirely `package.json` text embedded in `main.js`, which moved with every script edit in this stack, plus this 2-byte residue. The runner is the canonical measurer; the CI run on the stacked #1694 branch (this content plus the job) is where the number is confirmed. - `tools/performance/check-journey-ratchet.mjs` compares a journey summary with the baselines: a counter above its value fails (exact, no slack), wall-clock entries fail above `value × toleranceRatio`, a baseline without a measurement fails so dropping a measurement cannot disable the ratchet, values below baseline print a "tighten" hint, and measured counters without a baseline are noted only. After review: checking nothing (empty file, or `--only` naming a missing entry) fails; a counter is read only from `counters` and a wall-clock entry only from `wallClock`; the repeatable `--only <journey>/<counter>` flag scopes a check. - Root scripts: `perf:initial-bytes:check` (measures into its own `dist/performance/initial-bytes.summary.json`, then checks `--only launch/renderer.initialBytes`) and `perf:ratchet:check` (full check); `perf:tools:test` runs both test files, as does `pnpm nx test performance-tools`. - `docs/architecture/performance-journeys.md` gains the Ratchet section (file format, rules, "baselines only move down"); the validation map lists the check. The CI job that runs the check on every PR is #1694; C1 (lazy Angular date locales, #1695) then lowers the baseline with the measured output as evidence. Note: `ci.yml` only triggers on pull requests targeting `master`, so this stacked PR shows no Actions runs until #1692 merges. The evidence runs above were dispatched with `gh workflow run ci.yml --ref <branch>`. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> |
||
|
|
f80a21beef |
feat(collections): open a Stalker live favorite inside its portal (#1639)
Live channels in Favorites and Recently viewed now offer "Open in <playlist>" for Stalker portals, the counterpart of the VOD "View in portal" action. The chip in the programme panel and the channel's context menu jump to the channel inside its portal's Live TV, with its genre selected and the channel playing. Radio stays hidden: it is a separate legacy-paged section with no open-on-arrival contract. The handoff refuses to guess. The store records which genre the rendered ITV list belongs to, so the deferred play waits for a list that can actually serve the channel instead of inferring it from array identity. An id claimed by one channel as its `id` and another as its `stream_id` is ambiguous, so neither is played and the user still lands in the right genre. The handoff is abandoned when the user changes genre, search, portal or section first. Two pre-existing defects surfaced during review and are fixed here: - A blank provider id shadowed a valid one. `a ?? b` keeps an empty string, so a channel with a blank `stream_id` was stored with no identity at all and could not be selected, played or found again. Six writers had that shape and three private copies of the skip-the-blank rule; all now go through one `firstNonBlankStalkerId` helper. - Route-session readiness could publish before the store held the portal's row. The constructor starts one sync and the first navigation starts another, and the second skipped the bootstrap because the playlist id was claimed before the awaits that install it. Arrivals are now serialized, the id is claimed only after the store write resolves, and only the newest sync publishes readiness. Both fixes carry regression tests that fail on the old behavior. |
||
|
|
4cce4acaad |
feat(portal): season thumbnails in the season dropdown + PR #1628 follow-ups (#1633)
* feat(portal): season thumbnails in the season dropdown + PR #1628 follow-ups Follow-ups to the season posters shipped in #1628: - The >6-seasons dropdown (`SeasonTabsComponent`) now carries a 28×42 season thumbnail at the start of each menu row that has a poster and in the closed trigger for the selected season, fed by a new `seasonPosters` input from the season container and the fullscreen episode panel. Rows without a poster get no placeholder, a failed image is dropped, and the pill row stays text-only as the design review decided. - The fullscreen season strip's episode count uses its own `PORTALS.EPISODE_COUNT_ONE/OTHER` keys instead of borrowing the download manager's; all 18 locales filled through the i18n merger from their existing `DOWNLOADS.EPISODE_COUNT_*` translations. - The Stalker mock's serve targets no longer pin `PORT` (an nx:run-commands `env` entry overrides the shell), and `main.ts` resolves `PORT`, then the Playwright-side `MOCK_PORT` alias, then 3210 — so `MOCK_PORT=3310` now relocates the whole E2E run. The Xtream mock honours `XTREAM_MOCK_PORT` the same way. - `resolveAutoSelectedSeason` gets a direct spec covering every branch. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * fix(xtream-mock): mint marketing asset URLs on the port the server bound Greptile P1 on #1633: the listener honoured `XTREAM_MOCK_PORT`, but `marketingAssetOrigin()` still read `PORT` alone, so a run relocated only through the alias sent every poster/backdrop/logo/episode URL to 3211. One resolver (`resolveXtreamMockPortString` in `mock-port.ts`: `PORT`, then `XTREAM_MOCK_PORT`, then 3211) now feeds the environment parser, the marketing asset origin and the demo-guide origin fallback. A spec pins the precedence and that `marketingAssetUrl` follows the bound port. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com> |
||
|
|
4bab307152 |
test(e2e): give the DASH collection round-trip its cold-load budget (#1632)
The "ClearKey reopens from recent and favorites collections" spec does five cold `page.goto` loads of the dev-served app. On the CI runner each one costs ~6 s, so the default 30 s test timeout expired on the last route: every attempt in the affected runs ended as `timedOut`, and the retry trace's final screencast frame shows `/workspace/global-favorites` still on the startup screen at 29.7 s. The two reported "shapes" were just where the clock ran out. Size the test like the other multi-load specs (`test.setTimeout(90_000)`) and assert the "All playlists" radio is checked before waiting for a row only that scope can show, so a lost click fails on the toggle instead of surfacing as a missing row. Closes #1630 Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com> |
||
|
|
7790e68147 |
feat(portal): show each season's own poster beside the season tabs (#1628)
Series detail pages now render the selected season's poster as a season
cover next to the season tabs and description, and the fullscreen episode
panel shows the same poster as a season strip above its tabs.
Resolution is TMDB-first, like the show artwork merge: the lazy season
enrichment stores `/tv/{id}/season/{n}` `poster_path` as a w342 URL in
`tmdb_season_posters` (Xtream) or `StalkerSeriesTmdbSeasonsService.posters()`
(Stalker), under the same write-only-if-changed convergence guard as the
season overview. Xtream falls back to the provider's `seasons[].cover_big`/
`cover` when it is an http(s) URL other than the show poster, because panels
repeat the show poster on every season. Stalker is TMDB-only.
The cover column is not rendered for one-season items, seasons without a
poster, or a failed image, so every fallback is today's markup. It is sized
by a new `--season-cover-width` token (96/120/144px per Settings.coverSize).
The hero poster never follows the season.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
|
||
|
|
43c1ceac16 |
test(web-e2e): reach row buttons with Option+Tab on WebKit (#1629)
The two "channel scrolling keeps focus after selection" cases (and the Xtream "channel focus and separate scrollbar" cases, which press Tab the same way) failed deterministically on Playwright WebKit while passing on Chromium and Firefox. Playwright's WebKit emulates Safari's default keyboard preference, under which plain Tab visits only text fields and links: from the focused channel pane the key landed on the sidebar search field instead of the first row button. Option+Tab reaches the button and then the favorite action in the same DOM order Chromium's Tab follows, so the app's focus contract (ChannelScrollFocusDirective) is intact and this is Safari's Tab semantics, not an app bug. Add a `pressTab` E2E helper that presses Alt+Tab only on webkit and keeps the literal Tab / Shift+Tab on chromium and firefox, use it at the four Tab presses toward buttons, and note Safari's behaviour in the keyboard scrolling contract. Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com> |
||
|
|
c76a901e8d |
fix(ui): keep one Back arrow on detail pages (#1627)
Movie and series detail pages showed two arrow_back controls while the inline player was open: the shell's sticky arrow (added in #1576 so Back survives scrolling) meant "Close player" in watch state, while the now-playing bar carried a second arrow that meant route-level Back — the same icon with two meanings, next to a "Close player" button that duplicated the first. The shell's sticky arrow is now route-level Back in browse and watch alike, and the bar carries no arrow of its own. Closing the player is the bar's "Close player" button and Escape, which still unwinds one level (close, then back). Hosts without a browse Back target (M3U, downloads) render no arrow in either state. Unit specs for the shell and the inline player cover the new contract; the Electron and web E2E helpers that pressed Back from watch are updated, and the M3U flow closes the player through the bar's button. Docs, the mirrored CLAUDE.md/AGENTS.md paragraph and a release note follow the change. Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com> |
||
|
|
fa8ce26991 |
feat(playback): fullscreen episode panel for series playback (#1620)
Series playing in fullscreen get the same slide-in side panel the live channel list has, with season tabs and the episode list: rest the mouse on the left edge, click it, or press C; pick an episode and it plays inline without leaving fullscreen. - Panel contract: `FullscreenChannelPanelHost` gains optional `panelSearchEnabled` and `panelKind`; the template context gains `open`. Pointer/keyboard rules and the four live providers are unchanged. - Series host: `PortalInlinePlayerComponent` provides the token through `createEpisodePanelHost()` and stamps `app-fullscreen-episode-panel` (SeasonTabsComponent over rows with TMDB still or numeral tile, label, runtime, clamped overview, progress, watched check, now-playing marker; playing row centred on open). Episode clicks reuse the Up Next rail's inline path; season tab clicks reach the hosts' `onSeasonSelected` (Xtream TMDB season enrichment, Stalker lazy VOD load with a Retry row after a failed request). - Gates: `Settings.fullscreenChannelPanel` (label now covers both lists in all locales), episode content only, native-view Embedded MPV withheld by the view, external players excluded. - Inline-series e2e moved to `xtream-series-playback.e2e.ts` with shared Xtream helpers in a fixture; adds a fullscreen episode switch through the panel. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> |
||
|
|
e9eca1c386 |
chore(deps): upgrade Angular to 22.1 and Nx to 23.2 (#1603)
* chore(deps): upgrade Angular to 22.1 and Nx to 23.2 * fix(deps): complete Angular migrations after rebasing on master * fix(ci): use the Node pin for Windows runtime refresh * docs(deps): synchronize the workspace-shell Node requirements |
||
|
|
ef3f98d026 |
feat(portals): posters-only cover wall for movie and series grids (#1604)
* feat(portals): posters-only cover wall for movie and series grids Add `Settings.showCoverTitles` (Settings > General, default on). Turning it off drops the title row under VOD/series covers in catalog, favorites and recent grids and reveals the title as a bottom-gradient overlay on hover and keyboard focus, pinned open for items whose cover is missing or failed. `CoverTitlesService` is the single resolver: the opt-out AND a hover-capable pointer, so touch-only devices keep their titles. Live channel grids, search results, "recently added" rails and dashboard rails always keep labels. Catalog and collection cards become keyboard buttons (role, tabindex, aria-label, Enter/Space, focus ring) and poster alt text is the title. The default-on boolean coercion moves into `settings-opt-out.util.ts` because the settings store reached the max-lines limit. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * fix(portals): keep nested Remove key presses from activating the card Enter/Space on the content card's nested Remove button bubbled into the card's own key handlers: Enter opened the item before removing it and Space opened it while cancelling the removal. Only keys pressed on the card element itself now activate it. Regression spec added. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * fix(portals): keep cover titles while an in-section search filters the grid The posters-only wall exempts search results because they are identified by the name the user typed; the category grid's own in-section filter is the same case, so `app-grid-list` now keeps the title row while its `searchTerm` is non-blank. Contract docs updated, regression spec added. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * fix(portals): keep cover titles while the collection tab search is active The unified favorites/recent tab filters by its own search term, so its matches are identified by name like every other search result. The tab now opts its cards out of the posters-only wall while the term is non-blank. Contract docs updated, regression spec added. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * fix(portals): move the card Remove control out of the button surface An interactive control nested inside a role="button" is an invalid accessibility structure. The content card's activation surface is now its own inner element and the Remove button a sibling positioned over the poster corner, labelled by its tooltip text. Spec asserts the control is never a descendant of the button. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * fix(portals): draw the collection card focus ring where it is not clipped The card's overflow: hidden clipped an outline drawn on the inner activation surface on every edge, so keyboard users saw no focus indication. The ring now sits on the outer card via :has(> .content-card__activation:focus-visible). Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * fix(portals): detect any hover-capable pointer for the posters-only wall `hover` describes only the primary pointer, so a touch-first tablet with a mouse or hover-capable stylus attached lost the wall. The resolver now reads `(any-hover: hover)`. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com> |
||
|
|
17b8aa309d | fix(m3u): restore DASH playback from favorites and recently viewed (#1597) | ||
|
|
a417826b01 | fix(m3u): determine VOD playback independently of TMDB (#1594) | ||
|
|
7d1265d566 | fix(xtream): detect HTTP portals during explicit connection tests (#1588) | ||
|
|
fff022afe4 | fix(ui): keep detail back navigation available while scrolling (#1576) | ||
|
|
93e759e1da |
fix(m3u): accept standard Base64 ClearKey values (#1575)
* fix(m3u): accept standard Base64 ClearKey values * refactor(release): move M3U fixture generation out of capture driver |
||
|
|
c952b55da1 | feat(playback): enrich failure diagnostics and add safe support reports (#1574) | ||
|
|
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> |
||
|
|
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> |
||
|
|
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 |