feat(stalker): protocol-correct auth lifecycle

Portals that answer get_profile with status 2 ("login/password required")
never authenticated: the status was treated as success and the do_auth path
had no callers. Parse js.status (numeric or stringified), run do_auth with the
credentials the import dialog now collects, and retry get_profile with
auth_second_step=1.

- Surface the portal's own msg/block_msg (markup-stripped) in the import
  dialog and the workspace category error state instead of a generic message.
- Classify the plain-text failure bodies at the Electron transport boundary as
  a structured marker rather than by JSON.stringify regex.
- Re-present the persisted stalkerToken in the handshake (which is
  idempotent), skipping get_profile when it comes back unchanged and not
  flagged not_valid, and write a renegotiated session back. The watchdog
  cadence is persisted with the token because that reuse skips the only
  response carrying it.
- Take the watchdog cadence from the profile (watchdog_timeout + timeslot),
  defaulting to the documented 120 s instead of pinging every 25 s.

Splits stalker-session.service.ts into an auth API, a watchdog controller, a
portal-error module and a response classifier; the service is now a facade and
drops off the max-lines baseline.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
4grayandClaude Fable 5 committed 2026-08-03 18:41:54 +02:00
1 parent e197409b10
commit 28429580a5
51 files changed
+3626 -656

No files matched your search

@@ -157,9 +157,7 @@
} @else if (isStalkerCategoryFailed()) {
<app-workspace-context-error-view
[title]="'WORKSPACE.CONTEXT.LOAD_CATEGORIES_ERROR' | translate"
[description]="
'WORKSPACE.CONTEXT.LOAD_CATEGORIES_ERROR_HINT' | translate
"
[description]="stalkerCategoryErrorDescription()"
[showActionButtons]="false"
/>
} @else {
@@ -17,7 +17,10 @@ import { MatMenuModule } from '@angular/material/menu';
import { MatTooltip } from '@angular/material/tooltip';
import { Router } from '@angular/router';
import { TranslatePipe, TranslateService } from '@ngx-translate/core';
import { StalkerStore } from '@iptvnator/portal/stalker/data-access';
import {
StalkerStore,
asStalkerPortalError,
} from '@iptvnator/portal/stalker/data-access';
import {
PortalCategorySortMode,
persistPortalCategorySortMode,
@@ -152,6 +155,28 @@ export class WorkspaceContextPanelComponent {
this.stalkerStore.isCategoryResourceLoading;
readonly isStalkerCategoryFailed =
this.stalkerStore.isCategoryResourceFailed;
/**
* When category loading failed because the portal refused the session,
* the portal's own explanation (msg/block_msg or the documented
* plain-text failure body) replaces the generic hint; a login-required
* refusal gets its own actionable text.
*/
readonly stalkerCategoryErrorDescription = computed(() => {
const portalError = asStalkerPortalError(
this.isStalkerCategoryFailed()
);
if (portalError?.portalText) {
return portalError.portalText;
}
if (portalError?.kind === 'login-required') {
return this.translate.instant(
'PORTALS.ERROR_VIEW.STALKER_LOGIN_REQUIRED'
);
}
return this.translate.instant(
'WORKSPACE.CONTEXT.LOAD_CATEGORIES_ERROR_HINT'
);
});
// Category count badges are only available for Stalker Live TV, where the
// full channel list is cached — VOD/series/radio still page lazily, so
// their per-category totals are unknown.