From 32a259aea4e1fe99093296958028cb7ba4f70419 Mon Sep 17 00:00:00 2001 From: 4gray Date: Sat, 1 Aug 2026 20:03:49 +0200 Subject: [PATCH] fix(stalker): unwrap nested js.account_info envelope in get_main_info MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- CLAUDE.md | 6 +++ .../src/app/handlers/get-main-info.handler.ts | 19 ++++--- .../lib/stalker-account-info.service.spec.ts | 47 +++++++++++++++- .../src/lib/stalker-account-info.service.ts | 53 +++++++++++++------ 4 files changed, 101 insertions(+), 24 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 32868bb61..6f6c85c3d 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -1016,6 +1016,12 @@ engine` (restart required) or - TMDB attribution (logo + disclaimer) is required and shown in the settings TMDB section and About - See `docs/architecture/tmdb-metadata-enrichment.md` +**Portal Account Info**: + +- Both portal types expose an account-info dialog through the same entry points: header playlist switcher (bottom section for the active playlist + per-row ⋮ menu), dashboard source card ⋮ menu, and the command palette. Gates use the shared predicates in `libs/shared/interfaces/src/lib/portal-account-playlist.utils.ts`; `WorkspaceShellHeaderService.openAccountInfoFor()` picks the dialog by playlist type. +- Xtream: `AccountInfoComponent` (`libs/portal/xtream/feature/src/lib/account-info/`), queries `get_account_info` live. +- Stalker: `StalkerAccountInfoComponent` (`libs/portal/stalker/feature/src/lib/stalker-account-info/`), cached-first — renders the import-time `stalkerAccountInfo` snapshot instantly, then `StalkerAccountInfoService` refreshes (full portals: handshake+`get_profile`; `portal.php`: best-effort `account_info/get_main_info`, nested `js.account_info` envelope or flat fields). Details: `docs/architecture/stalker-portal.md` ("Account Info Dialog"). + **Favorites and Recently Viewed**: - Per-playlist favorites and global favorites diff --git a/apps/stalker-mock-server/src/app/handlers/get-main-info.handler.ts b/apps/stalker-mock-server/src/app/handlers/get-main-info.handler.ts index ab0d4a1f8..4f0e4793f 100644 --- a/apps/stalker-mock-server/src/app/handlers/get-main-info.handler.ts +++ b/apps/stalker-mock-server/src/app/handlers/get-main-info.handler.ts @@ -2,22 +2,25 @@ import { Request, Response } from 'express'; /** * Stalker `account_info/get_main_info` — subscription facts for the - * account-info dialog. Field names deliberately mirror the loosely - * standardized panels in the wild (end_date as a date string, tariff_plan, - * numeric status where 1 = active). + * account-info dialog. Uses the nested `js.account_info` envelope that + * Ministra-style portals answer with (the shape `fetchStalkerExpireDate()` + * already consumes), plus flat aliases some panels emit alongside it. */ export function handleGetMainInfo(req: Request, res: Response): void { const expiry = new Date(); expiry.setDate(expiry.getDate() + 180); + const mac = (req.query['mac'] as string) || '00:1A:79:00:00:01'; res.json({ js: { - mac: (req.query['mac'] as string) || '00:1A:79:00:00:01', + mac, phone: '10042', - login: 'mockuser', - status: 1, - tariff_plan: 'Mock Premium 180', - end_date: expiry.toISOString().slice(0, 10), + account_info: { + login: 'mockuser', + status: 1, + tariff_plan: 'Mock Premium 180', + end_date: expiry.toISOString().slice(0, 10), + }, }, }); } diff --git a/libs/portal/stalker/data-access/src/lib/stalker-account-info.service.spec.ts b/libs/portal/stalker/data-access/src/lib/stalker-account-info.service.spec.ts index 53dcdcf01..600e325be 100644 --- a/libs/portal/stalker/data-access/src/lib/stalker-account-info.service.spec.ts +++ b/libs/portal/stalker/data-access/src/lib/stalker-account-info.service.spec.ts @@ -115,7 +115,10 @@ describe('StalkerAccountInfoService', () => { expect.objectContaining({ url: portalPlaylist.portalUrl, macAddress: portalPlaylist.macAddress, - params: { type: 'account_info', action: 'get_main_info' }, + params: expect.objectContaining({ + type: 'account_info', + action: 'get_main_info', + }), }) ); expect(snapshot).toEqual({ @@ -128,6 +131,48 @@ describe('StalkerAccountInfoService', () => { }); }); + it('unwraps the nested js.account_info envelope used by Ministra-style portals', async () => { + // Same envelope fetchStalkerExpireDate() consumes in + // stalker-player-request.utils — nested facts must win over + // flat aliases. + dataService.sendIpcEvent.mockResolvedValue({ + js: { + mac: '00:1A:79:00:00:01', + tariff_plan: 'Flat Alias Plan', + account_info: { + login: 'nested-user', + expire_date: 1_790_000_000, + tariff_plan_name: 'Nested Premium', + status: '1', + }, + }, + }); + + const snapshot = await service.fetchAccountInfo(portalPlaylist); + + expect(snapshot).toEqual({ + login: 'nested-user', + expireDate: 1_790_000_000, + tariffPlanName: 'Nested Premium', + status: 1, + mac: '00:1A:79:00:00:01', + phone: undefined, + }); + }); + + it('sends the JsHttpRequest parameter legacy panels expect', async () => { + dataService.sendIpcEvent.mockResolvedValue({ js: {} }); + + await service.fetchAccountInfo(portalPlaylist); + + expect(dataService.sendIpcEvent).toHaveBeenCalledWith( + expect.anything(), + expect.objectContaining({ + params: expect.objectContaining({ JsHttpRequest: '1-xml' }), + }) + ); + }); + it('returns null when get_main_info yields no usable facts', async () => { dataService.sendIpcEvent.mockResolvedValue({ js: {} }); diff --git a/libs/portal/stalker/data-access/src/lib/stalker-account-info.service.ts b/libs/portal/stalker/data-access/src/lib/stalker-account-info.service.ts index a3eee8cf0..39e4217b4 100644 --- a/libs/portal/stalker/data-access/src/lib/stalker-account-info.service.ts +++ b/libs/portal/stalker/data-access/src/lib/stalker-account-info.service.ts @@ -24,19 +24,30 @@ export interface StalkerAccountSnapshot { phone?: string; } +interface StalkerMainInfoFields { + mac?: string; + phone?: string; + login?: string; + fname?: string; + status?: number | string; + tariff_plan?: string; + tariff_plan_name?: string; + end_date?: number | string; + expire_date?: number | string; + expire_billing_date?: number | string; +} + interface StalkerMainInfoResponse { - js?: { - mac?: string; - phone?: string; - login?: string; - fname?: string; - status?: number | string; - tariff_plan?: string; - tariff_plan_name?: string; - end_date?: number | string; - expire_date?: number | string; - expire_billing_date?: number | string; - } | null; + js?: + | (StalkerMainInfoFields & { + /** + * Ministra-style portals nest the account block — + * `fetchStalkerExpireDate()` in stalker-player-request.utils + * reads this exact envelope. Other panels answer flat. + */ + account_info?: StalkerMainInfoFields | null; + }) + | null; } /** @@ -101,14 +112,26 @@ export class StalkerAccountInfoService { stalkerSession: this.stalkerSession, }, playlist, - { type: 'account_info', action: 'get_main_info' } + { + type: 'account_info', + action: 'get_main_info', + JsHttpRequest: '1-xml', + } ); - const info = response?.js; - if (!info || typeof info !== 'object') { + const flat = response?.js; + if (!flat || typeof flat !== 'object') { return null; } + // Nested envelope wins field-by-field; flat top-level values remain + // as aliases for panels that answer without the account_info block. + const nested = + flat.account_info && typeof flat.account_info === 'object' + ? flat.account_info + : {}; + const info: StalkerMainInfoFields = { ...flat, ...nested }; + return normalizeSnapshot({ login: info.login || info.fname || undefined, expireDate: