Files
iptvnator/libs/workspace/dashboard/feature
4grayandClaude Opus 5 6c065124ed feat(stalker): add account info dialog for Stalker portals (#1330)
* feat(stalker): add account info dialog for Stalker portals

Xtream playlists have had an account-info dialog for a while; Stalker
portals stored the same facts (login, expiry, tariff, status captured at
import) as dead weight in the database and showed them nowhere.

Add StalkerAccountInfoComponent mirroring the Xtream dialog's visual
language: status pill, days-left/tariff/MAC hero stats, account and
portal panels. Data is cached-first — the import-time snapshot renders
instantly with a "Saved data" badge, then StalkerAccountInfoService
refreshes it: full /stalker_portal/ installations re-run
handshake+get_profile, portal.php panels are queried best-effort via
account_info/get_main_info. A failed refresh keeps the cached snapshot;
no data at all shows a retry-able error state.

Entry points are unified behind shared portal-account predicates
(isXtreamAccountPlaylist / isStalkerAccountPlaylist in shared/interfaces)
so both portal types get the same set: header playlist switcher (bottom
section + new per-row ⋮ Account info item), dashboard source card ⋮ menu,
and the command palette (now visible on stalker routes with its own
description). The header service picks the dialog by playlist type; the
per-row path works for non-active playlists and skips the session-scoped
stream counts.

Also adds the missing top-level LOADING/RETRY i18n keys the Xtream dialog
already referenced (they rendered as raw keys), a get_main_info handler
in the stalker mock server, and STALKER.ACCOUNT_INFO translations for all
19 locales.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix(stalker): unwrap nested js.account_info envelope in get_main_info

Ministra-style portals nest the account block — fetchStalkerExpireDate()
in stalker-player-request.utils already consumes exactly that shape, so
the flat-only mapper silently discarded valid responses and legacy
imports (which have no cached snapshot) got an empty account panel.

Merge nested fields over flat aliases, send the JsHttpRequest parameter
the existing get_main_info caller sends, switch the mock server to the
nested envelope so the E2E covers the realistic shape, and document the
account-info feature in CLAUDE.md (review feedback from Greptile and
Codex on #1330).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* test(stalker): pin account-info expiry fixture below the day boundary

Math.round on the epoch could round up half a second, putting the
fixture's expiry just past the 30-day mark so daysLeft ceil'd to 31 on
CI. Floor keeps the interval strictly inside 30 days regardless of when
within the second the spec runs.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* refactor(stalker): address account-info review round two

Three P2s from Codex on #1330:

- Normalize the cached stalkerAccountInfo snapshot before rendering:
  the import path persists portal values verbatim, so expireDate can be
  a date string or milliseconds at runtime despite the declared number
  type. normalizeStoredStalkerAccountInfo() runs the same parsers as
  the fresh path.
- Publish the re-auth token into StalkerSessionService's cache: strict
  portals invalidate the previous token per handshake, so the dialog's
  authenticate() would otherwise strand an active portal session on a
  dead token.
- Extract the duplicated ~460-line account-dialog stylesheet into
  libs/ui/styles/_account-dialog.scss, shared by both dialogs with the
  provider accent injected via --account-dialog-accent; each consumer
  keeps only its accent and layout overrides.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix(stalker): serialize account-profile refresh with session auth

The dialog's direct authenticate() call bypassed the pendingAuth map
ensureToken() uses, so a refresh could run a second handshake while a
catalog or watchdog request was still authenticating. On strict portals
each handshake invalidates the other's token, and the later
setCachedToken() could publish an already-dead one.

Move the refresh into StalkerSessionService.refreshAccountProfile(): it
waits for any in-flight authentication, registers its own so later
callers wait for it, and republishes the resulting token. A failed
pending auth no longer aborts the refresh, and the pendingAuth entry is
only cleared when it is still this call's.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix(stalker): move pendingAuth cleanup out of the promise initializer

TS2454 under the Angular compiler: the finally block referenced
authPromise inside its own initializer, so every Electron/web production
build failed even though jest and lint accepted it. Await the promise at
the call site and retire the map entry there instead — same
only-clear-our-own-entry semantics.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix(stalker): harden account-info portal detection and expiry math

Review round four (Codex P2s on #1330):

- Fall back to the URL rule when isFullStalkerPortal is undefined: a
  playlist restored from an older backup carries no flag once the
  one-shot metadata migration has run, and it would then be sent down
  the unauthenticated legacy path and labelled a legacy panel.
- Parse a bare YYYY-MM-DD expiry as a local calendar date. Date.parse
  reads it as UTC midnight, which renders as the previous day west of
  UTC and shifts the days-left boundary; timestamps carrying a time or
  offset keep standard parsing.
- Decide expiry from the raw timestamp, not the rounded counter: an
  expiry that passed less than a day ago ceil's to 0/-0, so the hero
  stat claimed "0 days left" on a dead subscription.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix(stalker): make account-profile refresh own the auth slot

Review round five (Codex P2s on #1330):

- Claim the pendingAuth slot in a loop and publish it before the first
  await. One settled promise releases every waiter at once, so a single
  pre-check let two queued refreshes both start handshakes that
  invalidate each other on strict portals.
- Retire the cached token before the handshake: ensureToken() reads
  tokenCache before pendingAuth, so catalog and watchdog requests
  starting mid-handshake were handed a token this refresh was about to
  kill instead of queueing on the slot.
- Render the portal type from the same resolver the fetch path uses, so
  a restored backup without an explicit flag is no longer labelled a
  legacy panel while authenticating as a full portal.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix(stalker): retire only the token that actually failed auth

A request dispatched with the previous token can see its authorization
failure arrive after a profile refresh has already cached a fresh one.
The retry path deleted the cache blindly, killing the fresh token and
kicking off another handshake that in turn invalidated tokens of newer
requests — cascading retries on strict portals.

makeAuthenticatedRequest() now retires the cached token only while it
still equals the token that failed; a late failure of a stale token
leaves the refreshed token in place and the retry reuses it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* docs(stalker): distinguish the two no-data outcomes of the account dialog

A portal that answers but publishes no account facts renders the
ready-state "No account details" panel; only an unreachable portal
without a cached snapshot enters the error state with retry. The doc
conflated both as "error with retry" (review feedback on #1330).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix(stalker): reject negative expiry sentinels before date parsing

Portals encode unlimited/missing expiry as "-1" or "0"; the
unsigned-digit check let "-1" fall through to Date.parse, which V8
reads as January 1, 2001 — an unlimited account rendered as expired.
Signed numeric strings now take the numeric branch, whose non-positive
guard already discards them.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix(stalker): reject out-of-range calendar components in expiry dates

The multi-argument Date constructor normalizes invalid components
('2026-00-00' becomes Nov 30, 2025), fabricating an expiry and countdown
from a placeholder. Round-trip the parsed year/month/day and reject any
date that does not survive unchanged.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-08-02 09:58:03 +02:00
..

Workspace Dashboard Feature

This library owns the workspace dashboard rails UI. The dashboard is a read-only surface over existing playlist, recent, favorites, EPG, and Xtream catalog data; it should not introduce Electron IPC, SQLite schema, or route contracts on its own.

Dashboard Surfaces

The dashboard renders a surface only when the matching setting is enabled. Data rails also require the underlying data slice to have at least one item.

  • hero shows the large top banner for the most recent global item. When that item is a live TV channel, the hero looks up the current XMLTV programme and displays the programme title, time range, and EPG progress bar when data is available.
  • continueWatching shows recent movies and series from DashboardDataService.globalRecentVodItems() using cover cards with playback progress when a saved resume position is available.
  • liveFavorites shows favorited live TV channels from DashboardDataService.globalFavoriteLiveItems() using the channel layout with EPG title, time range, and progress when XMLTV data is available.
  • recentlyWatchedLive shows recently watched live TV channels from DashboardDataService.globalRecentLiveItems() using the same channel layout.
  • favoriteMoviesAndSeries shows favorited movies and series from DashboardDataService.globalFavoriteItems(), excluding live favorites, using cover cards.
  • recentSources shows recently used playlist/source entries.
  • xtreamRecentlyAdded shows recently added Xtream catalog items.

Settings

Per-surface visibility lives on Settings.dashboardRails as a DashboardRailsSettings object. Every surface defaults to enabled. Stored settings are deep-merged with DEFAULT_DASHBOARD_RAILS_SETTINGS by SettingsStore, so existing users and older partial settings keep newly added surfaces visible unless they explicitly turn them off.

The global showDashboard flag remains a top-level Settings property because workspace startup and route guards already depend on that contract. The Settings UI groups showDashboard and the per-surface checkboxes in the Dashboard section. When showDashboard is off, the per-surface checkboxes are disabled because the dashboard route itself is hidden.

Navigation

Rail cards use the navigation state provided by DashboardDataService for the underlying item. Rail "See all" links may also pass router state:

  • live rails open the relevant collection on Live TV.
  • cover rails for movies/series open Global Recent or Global Favorites on Movies when movie items are present, otherwise on Series.

This keeps the global collection pages from defaulting to Live TV when a dashboard rail is clearly about movies or series.