* perf(playlist): make the playlist import and shared UI components OnPush
Plan item C6 step 3 for libs/playlist (import/feature and shared/ui): the
twelve Eager components switch to OnPush. Two of them rendered plain fields
written after an await, outside any template event, which only an Eager
check on the next zone tick picked up:
- playlist-item's portal status dot (PWA, after the async portal check)
now reads a signal;
- playlist-info's playlist is backed by a signal behind its existing
getter/setter name, so the EPG source list follows removals and file
picks that land after awaited cleanup and dialogs.
A regression test for each fails on OnPush with the plain field and passes
with the signal. The Stalker import's post-await patchValue needs no change
(see the zoneless checklist). The m3u feature-player components stay Eager
for the playback PR.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* test(playlist): check the OnPush dialogs without forcing a render
Review follow-ups (Greptile, Codex):
- The portal-status and EPG-row tests forced detectChanges() after their
await, so they passed with plain fields. They now let the fixture render
on its own; with portalStatus back on a plain field the status test fails.
- New: the playlist info dialog enables Save and shows the path after a
native EPG file pick, without a forced render. pristine and valid read
the form's state signals, so the OnPush dialog follows on its own.
- New render spec for the add-playlist dialog with the real URL form: Add
enables after typing and after a patch from outside the child (as an
auto-detect prefill does).
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>
* 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>
* fix(ui): destructive confirmations, verb labels and provider icons
Confirmations: ConfirmDialogData.confirmLabel is required, so no dialog can
fall back to "Yes"/"No"; the dismiss defaults to "Cancel" and
`tone: 'destructive'` styles the confirm with .app-destructive-button. Every
caller names its action ("Remove playlist", "Clear", "Refresh playlist",
"Cancel download" with a "Close" dismiss). The confirm button has the
confirm-dialog-confirm test id and drops its no-op color="primary".
The no-op `warn` color input becomes .app-destructive-button on the EPG
mapping, playlist item, error view, EPG/reset settings, delete-all and source
cleanup buttons, and on the unsaved-changes dialog's Discard.
Provider icons come from SOURCE_TYPE_ICONS in shared/interfaces (Xtream
cloud, Stalker cast, M3U playlist_play / link / description / subject) in the
add dialog, auto-import, empty state, playlist switcher, playlist rows,
dashboard source rail, command palette, Sources filters and both reset
summaries. Stalker no longer borrows the Dashboard icon, and Xtream no longer
shares a glyph with M3U URL playlists.
The playlist error view removed a playlist through the stale
PlaylistActions.removePlaylist: it dropped the playlist from state before the
delete ran, swallowed failures, skipped the source activity guard and showed
no toast. It now uses PlaylistDeleteActionService like every other removal,
commits only a completed delete, toasts and goes home. The unused action and
its effect are removed.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* fix(ui): one provider icon per playlist row, imperative Korean remove label
A restored Stalker or Xtream playlist can also carry a URL, and the row's
independent checks then showed the M3U URL icon next to the provider icon.
The row now switches on resolvePlaylistSourceIconKey(), the precedence every
other surface uses, so each source shows exactly one icon.
HOME.PLAYLISTS.REMOVE now names the confirm button and the row's delete
tooltip; in Korean it read "the playlist has been removed". It now says
"remove playlist", like every other locale.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* fix(ui): keep the auto-refresh badge on playlist rows with one provider icon
Showing one provider icon per row moved the auto-refresh badge into the M3U
branches only, so a restored Stalker playlist with a URL and auto-refresh
lost it although the URL is still re-fetched. The row now renders one icon
container: the provider icon from the shared precedence, then the badge for
any row with a URL or a local M3U, exactly the rows that showed it before.
The Xtream portal-status dot, used without source health, keeps that corner.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* fix(ui): let the playlist row's cancel action render in the error color
The row's action buttons set `color: inherit`, and the selected row does so
again with more specific selectors. Both beat Material's token-driven icon
color, so the .app-destructive-button cancel action kept the row color
(selection blue on the active row). Pin the cancel button to
--mat-sys-error in both row states.
The large-deletion Electron E2E now checks the cancel color in both themes;
without this rule it reads rgb(47, 123, 255) instead of the error red.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* test(ui): give the dialog service spec the now-required confirm labels
ConfirmDialogData.confirmLabel became required, and the spec still built
confirmations without one. Jest only transpiles, so the suite stayed green,
but the "Typecheck Jest spec programs" CI step rejected it.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* build(test): make spec tsconfigs resolve what Jest resolves
Lib spec tsconfigs used module: commonjs with node10 resolution, which cannot
see Angular's exports-only secondary entry points, and dropped global.d.ts, so
tsc reported thousands of resolution errors and no window.electron typing.
Switch them to module: preserve with bundler resolution (ts-jest still forces
CommonJS emit outside ESM mode), add global.d.ts to every spec program, type
jest.unstable_mockModule for the ESM workspace, include the ui-epg and
ui-playback specs that jest.web-esm.workspace.ts runs under the web spec
config, and drop the snack-bar stub that shadowed the real Material types.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
* ci(test): gate spec type-checking with typecheck:spec
Add tools/typecheck/spec-typecheck.mjs, which runs tsc --noEmit over every
tsconfig.spec.json with a small pool and fails on any diagnostic, wire it into
the unit-and-typecheck job after typecheck:ci, and document the gate and the
spec tsconfig conventions in the validation map. Also bring the non-Tier-A
spec configs (remote-control-web, ui-remote-control, stalker-mock-server) to
the same conventions so the gate covers the whole workspace.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
* test: fix the spec type errors surfaced by typecheck:spec
With the spec programs resolving modules and ambient typings correctly,
tsc reported 432 genuine errors across the Tier A projects: read-only
capability flags assigned on Partial<> doubles, signal-store values used as
types, fixtures missing required fields, index-signature property access,
partial bridge doubles cast through incompatible shapes, and deferred
resolvers narrowed to never. Type the doubles instead of casting to any:
writable mapped types for capability flags, InstanceType<typeof StalkerStore>,
typed jest.fn signatures, protectedState: false on test signal stores, and
completed fixtures. Production changes are limited to bracket access for
index-signature properties under the libs' noPropertyAccessFromIndexSignature
setting and two narrowing guards in the global favorites loader.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
* test(playback): use the ESM setup's jest global in the controls fixtures
The fixture imported jest from @jest/globals, which is not a direct
dependency. Jest provides that module at runtime, so tests passed, but on a
clean pnpm install tsc cannot resolve it and typecheck:spec failed in CI.
The ESM test setup already installs import.meta.jest as the global, typed
by @types/jest, as the other ESM specs use it.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* test: type the parental lock doubles merged since the gate was written
The parental lock feature (#1601) and the Stalker actor route landed on master
with spec doubles declared as zero-argument jest.fn()s that the tests then
drive with the real arguments, plus a copy of the ResizableDirective override
imported from a library that does not export it. Give the doubles the lock
service's real signatures, drop the dead override as in the sibling layout
specs, use bracket access for the actor route's personId param, and keep the
Stalker layout spec within the 1200-line limit.
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>
Tier A coverage runs projects a few at a time (largest first, bounded Jest workers, buffered output, fail-fast kept) and ts-jest transpiles with isolatedModules instead of type-checking per process; five type re-exports become export type, two decorated inputs use import type. Unit Tests and Typechecks job: 26 min -> 9 min (Tier A step 23 min -> 6.5 min).
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* 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
Adds an "Auto-detect" method to the Add playlist dialog: paste the message a
provider sent — links, Xtream credentials, a MAC address with device identity
— and a deterministic parser recognizes the source(s) and prefills the
matching import form.
- detectProviderImportCandidates (libs/shared/interfaces) extracts URLs, MAC
addresses and labeled fields, classifies each finding as Xtream, Stalker or
an M3U link/body, and returns ranked candidates. Pure and synchronous.
- Built against a corpus of 19 real reseller handouts kept verbatim in the
spec: Unicode "font" labels, arrow/dingbat separators, separator-less hex
serials, dual device IDs, multi-MAC lists, bare three-line handouts, and a
guard so a parental PIN is never read as the account password.
- Detection only proposes: the target form's own validation and behavioral
probes remain the sole path into the store, and no pasted text leaves the
app. Passwords are masked on candidate cards, including query and HTTP
Basic userinfo forms.
- Covered by parser, component and dialog unit tests plus two web E2E specs
for the paste → pick → prefilled form workflow; i18n for all 19 languages.
MAC addresses are canonicalized to the uppercase colon form a real STB
sends and validated at the input boundary, with a hint when they fall
outside Infomir's OUI — which the stock server's default filter refuses
with a bare {status: 1} no user could diagnose. Normalization applies
only to a value the user actually edits: rewriting stored bytes would
move the session fingerprint for every existing playlist with no user
action, and the MAC is the account key.
Device IDs can optionally be derived from the MAC the way StbEmu and
stalker-to-m3u do — SHA256(MAC) and SHA256(MAC + "stalker"), which a
real box never reports as equal. The portal pins the first non-empty
device_id/device_id2 it sees to the MAC permanently, refuses a different
one, and treats a later empty value as an unrecoverable lockout, so
derived values are written into the visible fields and persisted as
literal strings, never recomputed at request time. The option is offered
at import only; the edit dialog warns instead once an ID has actually
reached the portal.
get_profile now reports one coherent MAG250 (ver, stb_type — previously
empty —, hw_version, image_version, client_type), and a device conflict
gets its own StalkerPortalError kind so the UI can explain it instead of
relaying the portal's "Your STB is damaged".
Closes the identity-fields cluster: #927, #860.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* feat(stalker): endpoint probing + behavior-based portal mode with lazy repair
Replace the URL-shape guess behind isFullStalkerPortal with real endpoint
discovery: at import, probe portal.php -> server/load.php ->
stalker_portal/server/load.php (the pasted .php endpoint first) and
classify the portal by observed behavior — a token-less itv/get_genres
answering data proves a token-free panel, the middleware's plain-text
auth failure proves the endpoint enforces the token, confirmed by the
real handshake + get_profile. The proven endpoint and mode are persisted.
The three diverging portal-mode predicates (import, session service,
legacy migration) collapse into one shared helper in
@iptvnator/shared/interfaces; executeStalkerRequest becomes the single
request choke point (search and the collection stream resolver fold in),
and the production-dead makeStalkerRequest copy is removed.
Existing misclassified playlists repair themselves lazily: only after a
request actually fails with the plain-text auth bodies, HTTP 404, or a
terminal handshake error, at most once per playlist per session, and only
a configuration discovery proved to answer is persisted — via a minimal
portalUrl/isFullStalkerPortal patch, so favorites, recents and playback
positions survive. Working reseller panels are never probed or rewritten;
there is deliberately no eager one-shot migration, because tolerant
portal.php panels cannot be told apart from misclassified canonical
portals without probing.
The Electron handler now embeds the HTTP status code in the error message
(ipcRenderer.invoke strips custom properties from rejections), and probe
requests carry silent:true so expected 404s do not toast error snackbars.
The stalker mock gains a portal.php-less /ministra host so e2e can prove
the 404 fallthrough end to end.
Fixes#850, #686, #755.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(stalker): sync watchdog, PWA proxy errors and cmd resolution with lazy repair
Review round 1 (Greptile P1, Codex P1/P2):
- A successful repair now re-syncs the ACTIVE watchdog playlist via the new
StalkerSessionService.refreshActiveWatchdogPlaylist(): a simple-to-full
repair starts the required keepalive mid-session, full-to-simple stops it,
and an endpoint change repoints the pings instead of leaving them on the
activation-time snapshot.
- PwaService.forwardStalkerRequest surfaces the web-backend proxy's
normalized { message, status } no-payload envelope as an HTTP error
carrying the status, so endpoint discovery and the lazy repair can
classify upstream 404s in the PWA too (previously payload unwrapping
returned undefined and dead endpoints were unrepairable there). Probe
requests pass silent:true and skip the error snackbar.
- fetchStalkerPlaybackLink and the collection StreamResolverService re-apply
the repair override AFTER the request, so a relative create_link reply
resolves against the endpoint that actually answered (the resolver keeps
the /stalker_portal path segment as base, so this matters beyond origin).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(stalker): parse candidate URLs and tie repair overrides to their source config
Review round 2 (Codex P2 x2):
- Endpoint candidates are now derived from the parsed origin + pathname:
a pasted URL carrying a query or fragment (host/c?key=value) no longer
gets /portal.php bolted onto the query, which made every probe hit /c
and persisted the non-API URL.
- A repair override is tied to the failing configuration it replaced.
Playlists carrying anything else (the user edited the portal URL or mode
through the playlist dialog) drop the override and re-arm the
once-per-session probe latch, so edited metadata is used verbatim and
may repair again if it fails.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(stalker): auth-gated probes, normalized offline fallback, mock docs sync
Review round 3 (Codex P1 x2, P2):
- A probe answered with HTTP 401/403 now classifies the endpoint as
auth-required and attempts the real handshake instead of skipping the
candidate: non-standard middlewares answer 401 where the stock server
sends HTTP 200 + plain text, and such portals authenticated fine before
discovery existed.
- The unreachable-host import fallback normalizes the pasted URL (origin +
pathname) before the legacy /c -> portal.php rewrite, so a query or
fragment can no longer make it persist the browser page URL - a 200 HTML
answer from /c is not a repair trigger, which would have left the
playlist empty for good.
- The stalker mock-server README and architecture doc now describe
behavior-based discovery and the /ministra host instead of the retired
URL-shape rule and its "known inconsistency" note.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(stalker): recognize JSON auth failures and guard repairs against mid-probe edits
Review round 4 (Codex P1 + P2):
- isStalkerAuthFailureResponse() recognizes the JSON envelope some panels
answer instead of the plain-text body ({js:{error:"Authorization
failed"}} / {js:{msg:...}}). Probe classification treats it as
auth-required instead of token-free data, and the lazy-repair trigger
fires on it at runtime — previously such a portal was persisted simple
with no repair path at all.
- A repair is committed only after re-reading the persisted row and
verifying it still carries the configuration that failed: a user who
edits the portal URL (or deletes the playlist) during the multi-second
probe now wins over the in-flight repair result for the old URL.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(stalker): probe past endpoint 5xx, sibling fallbacks, identity-aware repair guard
Review round 5 (Codex P2 x3):
- A probe that fails with a RESOLVABLE HTTP status keeps discovery going:
a broken /portal.php handler answering 500 must not hide a healthy
sibling endpoint. Only status-less failures (true network level) stop
the loop. The Electron handler now gives real HTTP 5xx responses the
same parseable "HTTP Error <code>" message shape as 4xx, so the
renderer can tell them apart from ECONNREFUSED/timeouts after
ipcRenderer strips the object shape.
- Standard fallback candidates for a nonstandard pasted endpoint
(.../cp/api.php) derive from its DIRECTORY, so recovery probes hit
/cp/portal.php instead of /cp/api.php/portal.php.
- The repair's row re-verification also compares the MAC and all Stalker
identity fields: a probe authenticated as the old identity must not
install its token/watchdog or persist onto a row whose credentials were
edited mid-probe.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(stalker): reactivation-safe watchdog, wider JSON auth phrases, per-config probe latch
Review round 6 (Greptile 4/5 concern + Codex P1/P2):
- setCurrentPlaylist applies the repair override before feeding the
watchdog and store state: re-activating the portal route with the stale
NgRx meta no longer stops or repoints the repaired keepalive back to
the broken configuration.
- The structured js.error/js.msg fields accept the full phrase set the
session service recognizes (Invalid token, Auth failed, bare
unauthorized/authorization) — panels answering those envelopes were
still classified token-free. Plain-text body matching stays narrow on
purpose (HTML false positives).
- The once-per-session probe latch is keyed by the SOURCE configuration
fingerprint (endpoint, mode, MAC, identity) instead of the playlist id:
a repair discarded because of a mid-probe edit no longer blocks the
edited configuration from repairing, while stale snapshots of an
already-probed configuration still cannot loop the probe.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(stalker): identity-aware override invalidation and timeout-tolerant probing
Review round 7 (Greptile P1 + Codex P2):
- The repair override records the identity fingerprint the probe
authenticated as. Editing the MAC or any Stalker identity field
afterwards drops the override, the per-config probe latch AND the cached
token, so requests and watchdog pings never pair the edited identity
with a session negotiated for the previous one.
- A status-less probe failure that is a TIMEOUT (renderer budget, axios
request timeout, ETIMEDOUT) continues to the next candidate — one
hanging handler must not hide healthy siblings; connection-level
failures (refused, unresolvable host) still stop discovery, so dead
hosts keep failing fast.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(stalker): watchdog pings authenticate as the persisted row
Review round 8 (Greptile 4/5 concern):
The watchdog held its activation-time playlist snapshot for the whole
session, so portal metadata edited (or repaired) mid-session kept the
keepalive authenticating as the previous identity/endpoint — its pings
could keep the old session alive and repopulate the playlist-scoped token
cache with a token for the pre-edit identity.
Each ping now resolves the playlist from the persisted row first (the
single source of truth), falling back to the snapshot only when the store
cannot be read, and refreshes the snapshot on every successful read. Any
edit — identity, endpoint or mode — reaches the keepalive within one ping
cycle; a row now marked simple (or deleted) stops the watchdog. The
in-flight guard is claimed before the row read so overlapping pings
cannot double-fire.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(stalker): identity-tagged tokens, watchdog override overlay, retire-on-failure
Review round 9 (Greptile 4/5 concern + Codex P2):
- The session token cache is tagged with the identity fingerprint (MAC +
all Stalker identity fields) the session was negotiated for; ensureToken
re-authenticates instead of handing an edited identity the previous
token. The fingerprint helper is shared (stalker-identity.utils) with
the repair layer's override/latch checks.
- Watchdog pings overlay the repair layer's in-session override on the
resolved row (registered decorator, no import cycle): a simple-to-full
repair whose persistence is pending or failed no longer reads the stale
row and stops the freshly started keepalive.
- makeAuthenticatedRequest retires a failed token even on the no-retry
path (watchdog pings), so a dead session is never handed to the next
caller.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(stalker): pending authentications are identity-scoped
Review round 10 (Greptile 4/5 concern):
pendingAuth entries carry the identity fingerprint they authenticate as.
A request for an edited identity no longer adopts an in-flight result
negotiated for the previous identity: it waits the old authentication out
(a competing handshake would strand it with a dead token on strict
portals) and then negotiates its own session. This was the last
id-only-keyed session structure — override, probe latch, token cache,
watchdog snapshot and pending auth are now all identity-aware.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(stalker): atomic repair persistence, full probe history, normalized offline classify
Review round 11 (Codex P2 x3 + P1 docs):
- The repair's row verification and patch now run ATOMICALLY inside the
per-playlist write queue via the new
PlaylistsService.transformPlaylistMeta(): a user edit that is queued but
not yet committed wins over the repair — the transform sees the edited
row and aborts instead of overwriting it. Write failures after a
successful verification keep the session-only override, read failures
discard the repair.
- The per-playlist probe latch keeps EVERY attempted source fingerprint,
so alternating edits (A -> B -> A) cannot evict a fingerprint and let
stale snapshots re-run discovery.
- The unreachable-host import fallback classifies the normalized
origin+pathname, so a query merely mentioning /server/load.php cannot
make a panel URL look canonical and abort the offline import.
- docs/architecture/stalker-portal.md documents the actual probe
sequencing: any resolvable HTTP status (incl. 5xx) and timeouts continue,
401/403 classify as auth-required, only connection-level failures abort.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(stalker): collision-proof session fingerprints
Review round 12 (Greptile P1): identity values are unrestricted strings,
so the delimiter-joined fingerprint could alias distinct identity tuples
(serial "a|b" + empty device vs serial "a" + device "b") and bypass the
identity invalidation. Both the identity fingerprint and the repair
source fingerprint are JSON-encoded now; regression test pins the exact
aliasing pair.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(stalker): preserve URL authority in normalization; document per-config latch
Review round 13 (Codex P1 docs + P2):
- normalizeStalkerPortalInputUrl mutates the parsed URL (clear query/
fragment, trim pathname) instead of rebuilding from origin, and the
candidate builder swaps only the path — file: URLs (origin "null") no
longer make the builder throw, and basic-auth credentials are not
silently dropped before probing.
- The canonical docs and the repair service JSDoc now describe the actual
loop guard: at most one probe per SOURCE CONFIGURATION (endpoint, mode,
MAC, identity) per playlist per session, not once per playlist.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(stalker): HTTP 401/403 failures trigger the lazy repair
Review round 14 (Codex P1): discovery classifies 401/403 endpoints as
auth-required, but the repair trigger accepted only 404 — a legacy
playlist misclassified token-free against an HTTP-auth-gated middleware
could never reach discovery and stayed unusable. 401/403 now qualify;
endpoint-specific 5xx still do not.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(stalker): re-enter repair for edited configurations after a pending probe
Review round 15 (Codex P2): a request carrying an edited configuration
that raced an in-flight probe only awaited it and inherited its outcome —
the edited fingerprint stayed unattempted and the first request failed
without triggering its own discovery. repairPortal now re-enters after
awaiting the pending probe, so the per-config latch decides: already
attempted -> reapply, never attempted -> own probe.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(stalker): probe history remembers outcomes so restored configs repair again
Review round 16 (Greptile P1): the per-config latch kept A's fingerprint
after an edit to B dropped A's override, so restoring A left it latched
with nothing to reapply — broken until restart. The history now stores
each probe's OUTCOME (override or null): a restored configuration
reinstalls its remembered repair without a second discovery, and the
anti-ping-pong property (A<->B alternation never re-runs discovery from
stale snapshots) is preserved.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(playlist): serialize deletion behind the per-playlist write queue
Review round 17 (Codex P2): deletePlaylist bypassed
serializePlaylistWrite, so a queued mutation (e.g. the Stalker portal
repair's conditional transform) finishing after an unserialized delete
could upsert the row back and resurrect the playlist. Deletion now runs
through the same queue: queued writes commit first, the delete lands
last, and a transform enqueued after the delete reads a missing row and
aborts. Regression test pins the write-then-delete ordering.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(stalker): reinstalled repairs re-sync the watchdog like fresh ones
Review round 18 (Greptile P1): the restored-configuration branch
reinstalled the remembered override without the watchdog refresh the
fresh-repair path performs — if the intermediate edit stopped the
keepalive, the restored full-portal session recovered requests but never
its pings. The reinstall now calls refreshActiveWatchdogPlaylist with the
override applied, symmetric with a fresh repair.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(stalker): discarded probes retry once their configuration is restored
Review round 19 (Greptile P1): the pre-probe history reservation survived
the row-mismatch discard, so restoring the original configuration hit the
latch with nothing to reinstall — lazy repair stayed disabled for the
session. Probe records are now explicit (override / no-change /
discarded): a discarded configuration probes again once one cheap row
read confirms the row was RESTORED to it, while stale snapshots of it
stay declined without a discovery run.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(stalker): IPC-safe transport errors, repairable profile path, nested base paths
Review round 20 (Codex P2 x4):
- The Electron handler throws a real Error for axios failures without a
response: Electron serializes rejections via toString(), so a plain
object arrived as "[object Object]" and discovery could not tell a
timeout (keep probing) from a dead host (stop).
- isAuthorizationError parses HTTP 401/403 out of the IPC-wrapped message,
so an expired-token 403 retires the token and re-authenticates instead
of surfacing as a plain failure.
- The account-info full-profile path (which bypasses
executeStalkerRequest) routes repair-trigger failures through
StalkerPortalRepairService and retries with the repaired playlist, so
opening the dialog can fix a stale endpoint.
- resolveStalkerPlaybackUrl derives the installation base from the
endpoint's API suffix instead of a fixed stalker_portal|c|portal
allowlist: relative create_link replies now resolve correctly under
arbitrary discovered installations such as /cp/server/load.php.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(stalker): strict probe data shape, mode-aware profile retry, docs API name
Review round 21 (Codex P1 docs + P2 x2):
- Probe classification requires the real get_genres shape (array, or a
{data: []} envelope without an error) instead of a bare `js` key: a 200
error envelope ({js:{error:"Unknown action"}}, {js:false}) no longer
ends discovery on a broken candidate and persists an empty catalog.
- After a repair that flips the portal to simple mode, the account-info
retry re-enters the mode routing and uses get_main_info instead of
handshaking against a token-free panel again.
- docs/architecture/stalker-portal.md names transformPlaylistMeta and its
atomic source-check invariant (plus the serialized deletion) rather than
the race-prone updatePlaylistMeta.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(stalker): account dialog re-routes after a simple-to-full repair
Review round 22 (Codex P2): fetchViaMainInfo runs through
executeStalkerRequest, whose lazy repair retries the SAME action, so a
repair proving the portal is actually full left the dialog calling
get_main_info — canonical installations publish subscription details only
through handshake + get_profile, leaving the dialog empty. The routing is
now symmetric with the full-to-simple case: an empty main-info result
whose repair flipped the mode re-enters the profile flow.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(stalker): row-gate override reinstall; document mode-based account routing
Review round 23 (Codex P2 + P1 docs):
- Reinstalling a remembered override now requires the persisted row to
actually carry that configuration again. A stale request for A while the
row holds an unrelated C no longer resurrects A's override, which would
retry against B and repoint the active watchdog away from C. (The
edit-back-to-A case stays as documented: there the row IS A.)
- docs/architecture/stalker-portal.md and CLAUDE.md describe account-info
routing by the observed portal MODE instead of the endpoint shape — a
token-enforcing portal.php is a full portal now — and note the
mode-change re-routing in both directions.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(stalker): share the auth-failure predicate; prefer profile over partial main-info
Review round 24 (Codex P1 + P2):
- isAuthorizationError now reuses isStalkerAuthFailureResponse, so the
phrases discovery and the lazy repair already classify as auth failures
(Access denied., Unauthorized request., and their JSON envelopes) also
retire the session token. Previously a full portal expiring with either
phrase kept its dead token: the repair rediscovered the same
endpoint/mode, recorded no-change, and every later request stayed broken.
- After a simple-to-full repair, even a PARTIAL get_main_info answer no
longer wins over the profile flow — expiry and tariff live only behind
handshake + get_profile. The partial facts are kept only if the profile
path itself publishes nothing.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(stalker): keep a literal c installation directory in candidate derivation
Review round 25 (Codex P2): the /c landing-page rewrite ran after the
endpoint file was stripped, so `/tenant/c/portal.php` collapsed to
`/tenant` and the sibling probes went one level too high, rejecting a
valid portal whose installation directory is literally named `c`. The
rewrite now applies only when the pathname itself ends in `/c` (no
endpoint file); pasted endpoints strip only the file part.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(stalker): route rejected post-repair main-info retries to the profile flow
Review round 26 (Codex P2): a simple-to-full repair during
fetchViaMainInfo makes executeStalkerRequest retry the same action against
the repaired full portal, and installations that do not implement
get_main_info answer 404 — the rejection escaped before the repaired-mode
check, so the dialog failed instead of switching to get_profile. The
rejection is captured and reaches the same check; without a mode change it
is rethrown unchanged.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(stalker): full predicate for wrapped denials; record the removed store prop
Review round 27 (Codex P2 + P1 docs):
- The repair trigger applies the shared auth-failure predicate to the error
MESSAGE too, so authentication's wrapped structured denials
(Error('Profile error: Access denied.')) reach the repair instead of
bypassing it and leaving a healthy sibling endpoint unprobed.
- docs/architecture/stalker-store-api-baseline.md records makeStalkerRequest
as removed, with the reason it gets no facade alias: it was
production-dead and held a fourth private copy of the portal-mode branch
that the shared predicate exists to prevent.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(stalker): complete auth predicate for wrapped error messages
Review round 28 (Codex P2): the plain-text BODY matcher deliberately knows
only the three middleware phrases, so passing an error message through it
let authenticate()'s wrapped denials — Error('Profile error: Invalid
token') / 'Auth failed' — bypass both the repair trigger and the session
auth predicate. A dedicated isStalkerAuthFailureMessage() applies the wide
phrase set to controlled error strings, while arbitrary portal bodies keep
the narrow matcher that cannot false-positive on HTML pages.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(stalker): reject denied profiles during confirmation; document all repair triggers
Review round 29 (Codex P2 + P1 docs):
- Full-portal confirmation validates the get_profile envelope with the
shared structured predicate: a handshake can hand out a token whose
profile still answers {js:{error:"Invalid token"}}, and authenticate()
inspects only msg/block_msg — discovery would have persisted an unusable
endpoint and stopped before the healthy sibling. authenticate() now
returns the raw profile response for that check.
- The canonical lazy-repair contract lists the complete trigger set: the
plain-text bodies AND their JSON envelopes, HTTP 404, HTTP 401/403, and
terminal handshake/profile errors.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Supersedes #1252 and #872. uuid 14 is ESM-only, apps/web/jest.config.ts only
kept v9 working by mapping `^uuid$` at a `wrapper.mjs` that v14 no longer
ships, and the specifier also has to be synced in
libs/shared/m3u-utils/package.json or @nx/dependency-checks fails lint.
All four call sites only used `v4()`, so the dependency goes away instead.
`createRandomId()` prefers `crypto.randomUUID()` and falls back to building the
same v4 shape from `crypto.getRandomValues()` — that fallback is load-bearing,
because randomUUID is only exposed in secure contexts and the self-hosted PWA
is regularly served over plain http on a LAN address. getRandomValues stays
available there, and it is what uuid's own v4 used.
`@types/uuid` goes too; it only existed for the untyped v9 package.
The custom .apd-head was getting visually orphaned from the rest of the
dialog body: zero horizontal padding so the title sat flush against the
dialog edge (cards inside mat-dialog-content are inset by 24px), plus a
~44px vertical gap above the title even though the head reported
padding: 0.
The gap was Material's .mdc-dialog__title::before — a 40px-tall
invisible inline element used to baseline-align dialog titles to a
grid. Useful for single-line titles, hostile to a two-line head with
explicit subtitle.
Set .apd-head padding to 20px 24px 4px so it shares the dialog
content's 24px horizontal rhythm, then suppress the ::before ghost.
Title now sits 20px from the dialog top edge with the same left inset
as the method cards and the form fields below.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The 5-card method picker was rendering at the old 560px dialog width
which left each card too narrow — titles wrapped to 2-3 lines and the
subtitles to 6, all visible in the user-reported screenshot.
Three fixes:
1. Bump the dialog width from 560px → 780px to match the v0.22 mockup.
maxWidth: 92vw keeps the responsive collapse path intact.
2. Replace the inherited "Add via URL" / "Add via file upload" /
"Add Xtreme Code" / "Add Stalker Portal" / "Import from text" tab
labels with short noun-only card titles: "M3U URL", "M3U file",
"Xtream credentials", "Stalker portal", "Raw m3u text". The dialog
itself is titled "Add playlist" so the "Add" prefix on every card
was redundant. The "Xtreme" misspelling on the old key disappears
as a side effect.
3. Lock cards to min-height: 132px so the URL card (short subtitle)
stops collapsing while the Xtream/Stalker cards (longer subtitle)
stretch — selection states now look visually consistent. Slightly
tighter padding (10/11/12 vs 12/14) to fit the new width without
feeling cramped.
i18n: 5 new METHOD_*_LABEL keys translated across all 17 locales by
per-locale agents; placeholder integrity verified across all 920+
leaf keys.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The Add Playlist dialog used a nested control: pick category
(M3U / Xtream / Stalker) then pick subtype (URL / file / text). That's
9 cells of which only 5 are real — Xtream-via-file and Stalker-via-text
don't exist. Users spent visible time hunting the right path through
the matrix.
Replace it with a flat 5-card method grid that matches the v0.22 mockup
exactly: each card IS a method (M3U URL, M3U file, Xtream credentials,
Stalker portal, Paste raw m3u text), shown side by side with an icon,
label, and one-line description of when to pick it.
Component state collapses from `category` + `m3uSubType` (two signals
with an entangled `playlistType()` mapping) into a single `method`
signal of type PlaylistType. `initFromType` simplifies to one line.
`playlistType()` is kept as a thin alias so the template @switch and
the action-button branches stay untouched.
The selected card is the only place that needs to change for users to
pick a different method — no more "first click type, then click
subtype, then look for the form" two-step.
i18n: 6 new HOME.ADD_PLAYLIST.* keys (subtitle + 5 method subs)
translated across all 17 locales by per-locale agents, with the
existing HOME.TABS.* keys reused for card titles. Placeholder
integrity verified.
Tests: existing clearCurrentForm scenarios still pass after the signal
refactor; added 6 new tests covering the default method (URL) and the
MAT_DIALOG_DATA.type deep-link path for each of the 5 method values.
25/25 in playlist-import-feature.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Swap M3 primary palette from violet to azure so checkboxes, radio
buttons, raised CTAs, and active states read as the same blue used by
the rail selection token. Cascading template + SCSS updates align the
remaining hand-rolled surfaces (Add Playlist dialog, VOD play button,
radio player, multi-EPG, empty-state CTAs) with the unified system.
LIVE stays red (broadcast role), cyan stays on EPG "now" indicator,
green stays on completed-download — semantic colors keep their meaning;
only the indiscriminate accent uses get folded into the blue primary.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
PortalStatusService previously did one IPC + HTTPS round-trip per call,
forcing every consumer to roll its own cache (or, more often, not).
The result: opening the homepage rendered N playlist-item components
that each fired their own check, then opening the playlist switcher
fired N more for the same portals.
Move the cache and dedup into the service:
- 30 s TTL cache keyed by `${serverUrl}|${username}|${password}`. Same
credentials = same cache entry, regardless of which playlist row
triggered it.
- In-flight dedup via Map<key, Promise<PortalStatus>>. Two callers
hitting the same portal in the same tick share one network request
instead of racing.
- New `getCachedStatus()` for sync read (used by playlist-switcher to
hydrate the UI on menu open without awaiting).
- New `clearStatusCache()` for log-out / debug flows.
Add `{ skipCache: true }` opt-out for the Xtream import dialog's
"Test Connection" button — that's a user-initiated check that must
return fresh truth, not a 30 s old cached result.
Net result: in the common flow (homepage → switcher), the switcher
opens with cached status indicators instantly. The single in-flight
dedup prevents the playlist-item ngOnInit + switcher onMenuOpened from
racing for the same portal.
Removed the component-local cache from playlist-switcher; service is
now the single source of truth.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Entire-Checkpoint: a635db375527
- Set default appearance for mat-form-field to 'outline' and dynamic subscript sizing in app.config.ts.
- Adjust dialog dimensions in workspace-shell-actions.service.ts for better responsiveness.
- Refactor settings.component.scss to remove specific mat-form-field font size overrides.
- Enhance global styles in m3-theme.scss for outlined inputs, ensuring consistency across the application.
- Modify workspace-command-palette styles to improve visual integration with the overall theme.
- Revamp add-playlist-dialog component to utilize segmented controls for category selection, improving UX.
- Update file-upload component to provide better feedback on file selection and drag-and-drop interactions.
- Clean up text and URL upload components by removing unnecessary placeholders and improving layout.
- Refine stalker-portal-import component by removing placeholder text for clarity.
Entire-Checkpoint: c6e522b4276c