18 Commits
Author SHA1 Message Date
4grayandClaude Opus 5.5 43358e10f2 fix(ui): welcome screens and drop overlay follow the app theme (#1886)
* fix(ui): welcome screens and drop overlay follow the app theme

The welcome dashboard, the empty Sources page and the playlist drop
overlay switched on `prefers-color-scheme`, so with the app set to dark on
a light OS (or light on a dark OS) they painted the other theme's colours.
They now read `--app-*` tokens, which follow the `.dark-theme` class set
from Settings, and the hard-coded blues are derived from the selection
colour (a local "strong" accent mixed toward the heading ink keeps chips
and filled labels at 4.5:1 in both themes).

White rgba() fills that vanished in the light theme (season empty panel,
catalog refinement chips and menu divider, Xtream archive banner and
disabled paginator icons, shell download-activity track, search field)
now use the widget surface, on-surface mixes or the search tokens.

Reads of custom properties that nothing declares are fixed: the release
notes error, the search-layout empty state and the collection reload dim
now use declared tokens; unset hooks are replaced by their fallback value.

New guard `pnpm run styles:theme-references:validate` (CI) rejects
undeclared var() reads and prefers-color-scheme outside the settings
resolver. Electron E2E os-color-scheme.e2e.ts flips the OS scheme under
each explicit app theme and checks colours, contrast and screenshots.

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

* test(e2e): retry the rejected-drop comparison when the card dismisses mid-read

The rejection hides itself after 1.8s, and the OS-scheme comparison reads
the overlay twice with an emulateMedia call between. A slow runner could
lose the card between the reads; the comparison now drops again until both
reads see it. A colour that follows the OS still fails every attempt.

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

* test(e2e): measure welcome text contrast from rendered pixels

The feature and source cards paint gradients, which a backgroundColor
walk ignores, so card titles, descriptions and chips could pass while
falling short on the actual card. Every text on the three surfaces is now
measured against the pixels under it, as the filled button labels were.

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

* fix(tools): scan only runtime sources in the theme-references guard

The guard read every tracked file under apps/ and libs/ except specs, so
an E2E, mock-server or test-helper declaration could satisfy a runtime
var() read that nothing in the app declares (dashboard-rail-focus.e2e.ts
sets --cover-rail-width), and a test-only read could fail the check. It
now skips spec/test/e2e files, test helpers and stubs, testing projects,
the E2E and mock-server apps and the marketing website.

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

* fix(xtream): set live paginator tokens through mat.paginator-overrides

The live layout hand-declared --mat-paginator-* tokens, which the UI
guidelines route through the overrides mixin so a mistyped name fails the
build instead of silently doing nothing. Same values, same output.

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

* fix(tools): skip every test-only naming convention in the theme guard

The runtime scan still read .stub, .harness, .fixtures, .spec-stubs,
.spec-helpers, .spec-fixtures, test-setup and test-double sources, and
test-stubs/ or *fixtures/ directories, so a declaration in one could mask
an undeclared runtime read. A file is now test-only when a dot segment
after its name marks it (spec, stub, fixture, harness, mock, spec-*,
test-*, *-fixtures), its stem is a test bootstrap, or it sits in a test
directory. Runtime names such as xtream-connection-test.service.ts stay
in the scan; no runtime source imports an excluded file.

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

* fix(ui): keep welcome card text legible while hovered

At the 16%/10% hover tint the dark theme's body text on a feature card
measured 4.45:1. The hover fill steps up to 10%/6% instead (4.78:1 in
dark, 6.6:1 in light); lift, border and shadow carry the rest. The E2E
now measures feature and source card text while hovered, in both themes;
with the old tint it fails at 4.45.

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 12:22:23 +02:00
4grayandClaude Opus 5.5 2dff91c9f2 fix(ui): replace icon ligatures missing from the font and guard them (#1864)
* fix(ui): replace icon ligatures missing from the font and guard them

The download and recording queues showed the literal text "file_off" for a
missing file: the bundled Material Icons font (material-design-icons-iconfont
6.7.0) has no such ligature, so it rendered as text overflowing the icon box.
Use error_outline, which the missing state's tertiary palette keeps distinct
from the filled error glyph of a failed download.

Add `pnpm run styles:icon-ligatures:validate` (CI): it parses templates with
@angular/compiler and TypeScript with the compiler API, collects static
<mat-icon> text, the string results of its bindings, icon/*Icon inputs and
icon-named TypeScript values, and fails on a name missing from the font's
codepoints file. Two listed codepoints the font has no ligature for
(rounded_corner, stairs) are excluded.

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

* fix(tools): check quoted inline templates in the icon guard

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

* fix(tools): accept every codepoint and report escaped template lines

The first measurement rendered the names without the package's CSS. With
material-design-icons.css, all 2,193 codepoint names, rounded_corner and
stairs included, draw as one glyph, so the guard accepts the whole file.

Inline templates now map each cooked position back to the source through
escapes, line continuations and CRLF, so a name after `\n` in a quoted
template is reported on the line it is written on.

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 10:31:21 +02:00
4grayandClaude Fable 5.1 e29f36426b perf(tooling): compositing probe, report command and route-wide clip-mask guard (#1858)
* perf(tooling): compositing probe, report command and route-wide clip-mask guard

Make the instrument that found the dashboard's clip masks part of the
repository: `src/performance/compositing-probe.ts` reads the layer tree
over CDP (bounds in device pixels, compositing reasons and owner nodes,
waiting for Blink's layer debug info), classifies synthesized clip masks
(no owner node and no compositing reason) and reads the renderer's
`cc/tile_memory` from a memory-infra dump. `pnpm run perf:compositing`
seeds one profile against the Xtream mock on a dedicated port, serves the
artwork from memory, fixes the window at 1600x1000 and records tile and
image memory, layer counts, the largest layers, masks and tile warnings
for Live TV, the movie and series details, the dashboard (idle,
crossfade, scrolled) and settings into
dist/performance/compositing/<timestamp>/summary.json and a console
table. Both helpers have node:test coverage in the performance harness.

The dashboard-only compositing E2E becomes `compositing.e2e.ts` and
guards Live TV, a movie detail, a series detail and the dashboard in one
launch, with the backdrop-filtered controls confirmed as composited
layers on the dashboard and the movie detail.

The rule now lives where it triggers: a Rendering Cost section in the
theme/style skill, a validation line in the UI design skill, a context
map row, and a Compositing budget section in the performance journeys
contract (budget, mask mechanism, instruments, why megabytes are
evidence rather than a ratchet). Validation map and README mention the
command.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* perf(tooling): sample the dashboard idle reading with rotation paused

The hero advances after 8 s, so a 9 s wait sampled the next slide one
second into its zoom (backdrop scale 1.116 instead of 1.1, the previous
slide's tiles still pooled): 224 MB read as idle, 122 MB with rotation
paused and the zoom and tile pool settled. The warning delta and its
checkpoint are now taken at one boundary after the sampling, so a
"tile memory limits exceeded" line logged while the layers or the dump
are read counts for that route instead of for none. The report's test
artifacts go under dist/test-results like the other configs'.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* perf(tooling): retry a memory dump that carries no renderer tile memory

One reading of seven came back without `cc/tile_memory`: the dump's
trace events reach the buffer shortly after the request resolves, and
a process can skip a dump it is busy for. The capture now waits briefly
before ending the trace and retries up to three times before a reading
stays null.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* perf(tooling): run the compositing report with motion enabled

With the OS set to reduced motion the hero neither rotates nor renders
its pause button, so the report's click timed out before the summary was
written. The report now emulates `prefers-reduced-motion: no-preference`,
which also keeps the idle and crossfade readings on the same animation
set on every machine.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* perf(tooling): sample the crossfade frozen, count split stderr lines, drop stale layer samples

Review findings on the pushed head, plus the CI failure:

- A layer that vanished between the LayerTree snapshot and its reasons
  query was kept with empty reasons; without an owner node it would pass
  for a synthesized mask. Such a snapshot is now discarded and retaken.
- The crossfade reading sampled layers, owners and memory across the
  700 ms fade, so the row depended on how long the sampling took. The
  page's animations are frozen 300 ms into the fade while the reading is
  taken, resumed afterwards, and the incoming slide's zoom is allowed to
  finish before the scrolled reading.
- A stderr chunk can end mid-line; the warning counter now keeps the
  unfinished line for the next chunk.
- The mock server's launch spec lists the new Playwright config.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-09 21:34:57 +02:00
4grayandClaude Opus 5.5 e8b181fcea fix(ui): Cyrillic/Greek weights, html lang, weight normalisation (#1780)
Load Roboto 600/700 and DM Sans 700 so Cyrillic and Greek headings render
real semibold and bold faces instead of a synthetic bold, keep
<html lang> in step with the UI language, and move every font weight onto
the 400/500/600/700 scale (JetBrains Mono at 500 or lighter; the dashboard
LIVE badge now uses the interface font at 700).

Add the `styles:font-weights:validate` ratchet guard and its CI step. It
reads stylesheets much as Sass and the browser do (cascade, layers,
mixins, content blocks, `@extend`, `@at-root`, `:is()`/`:where()`,
keyframes) and lists what it deliberately does not trace in its header.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-04 08:55:23 +02:00
4grayandClaude Opus 5.5 23a1860119 fix(ui): destructive confirmations, verb labels and provider icons (#1783)
* 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>
2026-10-02 20:36:16 +02:00
4gray 572034f3be fix(ui): declare Material system tokens and migrate dead --mdc overrides (#1775) 2026-10-01 18:02:50 +02:00
4gray 0e4e1d2169 chore(release): begin 0.25 development and publish 0.24 article (#1674) 2026-09-26 17:13:13 +02:00
4gray dc4b33991d chore(release): prepare v0.24.0 (#1659)
* chore(release): prepare v0.24.0

* docs(release): select sculptural v0.24 cover

* docs(release): preserve announcement cover prompt
2026-09-22 08:43:42 +02:00
4gray eb602db5fc fix(ui): restore channel and detail keyboard scrolling (#1542)
* fix(ui): restore channel and detail keyboard scrolling

* test(ui): drag below the Windows scrollbar arrow
2026-09-05 15:02:57 +02:00
4gray 29ca94aa43 feat(release): announcement formats, highlight cards, and draft verification (#1480) 2026-08-29 10:16:07 +02:00
4grayandClaude Fable 5 f58460380f fix(stalker): fall back to short EPG when bulk EPG misses the current programme (#1386)
* fix(stalker): fall back to short EPG when bulk EPG misses the current programme

Some portals' bulk get_epg_info returns only future programmes — the one
airing now is absent. The ITV channel-list previews read exclusively from
that bulk map, so every row showed 'No program information available', and
the EPG panel preferred any non-empty bulk list over the short-EPG fallback,
so it showed upcoming shows with no 'on now' entry. Recently Viewed uses
get_short_epg per channel, which is why the same channel worked there.

Panel: merge the short-EPG fallback into the bulk list instead of either/or,
and trigger the fallback whenever the bulk list has no currently airing
programme (not only when it is empty).

Rows: new throttled StalkerEpgPreviewQueue (mirroring Xtream's
EpgQueueService — bounded concurrency, inter-request spacing, 5-minute cache
including empty results, reset on playlist switch) fetches get_short_epg for
rendered channels the settled bulk guide cannot answer.

Docs: stalker-epg.md fallback contract updated accordingly.

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

* fix(stalker): bound the preview-queue burst and keep mapped channels portal-free

Review follow-up (Codex P1/P2 on #1386):

P1 — request volume: each queue sync is now capped at 30 channels (top of
the list first, where a freshly opened category is scrolled to), and the
sidebar scroll handler re-syncs (throttled, 300 ms) to fill the next gaps.
Request count now tracks how far the user scrolls instead of how many rows
are rendered; caching (including empty results) and 200 ms pacing remain.

P2 — manual mappings: a channel whose bulk record comes from a manual XMLTV
mapping never falls back to the portal short EPG. The panel path resolves
the channel's mapping before falling back and bails when an override owns
the channel; the row path excludes overridden channels from both the queue
and its cache. New store query hasItvEpgMappingOverride() exposes override
ownership; merging portal data into a mapped schedule could otherwise
surface the portal's programme — the exact thing the mapping replaces.

Docs updated (stalker-epg.md); regression tests for the cap, the mapped-
channel suppression, and the override query.

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

* fix(stalker): scope the panel EPG fallback by channel and revalidate queued previews

Review follow-up (Codex round 2 on #1386):

The panel's short-EPG fallback is now stored with the channel id it was
fetched for, and activeEpgPrograms merges it only while that channel is
still selected. A channel switch moves the selection synchronously but the
old fallback is replaced only after the new channel's EPG load runs, so the
unscoped merge mixed the previous channel's programmes into the new panel
during slow playback resolution — and left them there when resolution
failed.

The row-preview queue's completion callback now revalidates ownership: a
row claimed while the fetch was in flight — by a manual mapping override or
by bulk data — is never overwritten by the late portal response.

Both races covered by a new focused spec (verified to fail on the pre-fix
component); stalker-epg.md updated.

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

* fix(stalker): supersede the preview backlog on view exit and own empty mappings

Review follow-up (Codex round 3 on #1386):

The preview queue is now superseded when the rendered channel list empties
(a legacy-paged category switch clears it before the new channels arrive)
and when the view leaves ITV for radio — an abandoned view's backlog no
longer keeps issuing get_short_epg requests for rows that are gone.

Mapping ownership is now tracked separately from the mapped guide's
programs: a saved mapping whose XMLTV channel currently has no entries
still owns its channel, so hasItvEpgMappingOverride() keeps the portal
short-EPG fallback out — consistent with a mapping's purpose of replacing
portal data.

Both covered by regression tests (verified to fail pre-fix; the ITV-exit
test re-arms the backlog after init because the playlist effect's first run
resets the queue); stalker-epg.md updated.

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

* docs(stalker): reconcile every EPG contract reference with the row fallback

Review follow-up (Codex round 4 on #1386): the overview, get_short_epg API
notes, and data-mapping sections of stalker-epg.md still stated that rows
never issue per-row requests, and the stalker-portal skill instructed that
only the active channel may fall back — contradicting the contract this PR
establishes. All references now describe the bulk-first row previews with
the throttled short-EPG fallback queue. skills:validate passes (the skill
stays within its 500-word budget).

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

* fix(stalker): publish empty-mapping ownership reactively

Review follow-up (Codex round 5 on #1386): when the row-preview fetch
finishes before the mapping lookup, a portal programme is already rendered.
An empty mapped guide then recorded ownership only in a plain Set — no
state was patched, the preview effect never reran, and the stale portal row
survived. applyMappedItvEpg now re-patches bulkItvEpgByChannel (identical
content, new reference — deliberately) whenever it establishes new
ownership, even without programs, so the rerun sync removes the fallback
row. Store regression test extended (fails pre-fix); stalker-epg.md updated.

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

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-08-08 21:42:42 +02:00
4grayandClaude Opus 5 5e4f2ca3dd docs(stalker): reconcile the Stalker docs after the API-compatibility series (#1375)
Nine PRs landed between 2026-08-01 and 2026-08-04 in parallel worktrees, each
editing its own section of docs/architecture/stalker-portal.md and CLAUDE.md.
Sections that were correct when written disagreed with each other, or with
master, afterwards. Every claim here was verified against the code.

Corrected in stalker-portal.md: routes listed without the /workspace prefix;
"simple portals carry only the mac= cookie" (every request goes through the
shared identity builder — but the direct branch forwards no serial, so no
SN/__cfduid either, while playback headers are NOT mode-gated); a facade
introduced as "three modules" above a list of five; the pre-#1370 "blank
fields are not generated" opening; an ambiguous stalker-identity.utils.ts
citation (two files share the name); two of the three surfaces that apply the
scoped header override; a bare {status: 1} now being a refusal; and the
session-state fields #1354 added to the backup exclusion list (mirrored in
playlist-backup-restore.md).

CLAUDE.md had no entry at all for portal mode / endpoint discovery / lazy
repair — the largest change of the series; added one. Its session-facade list
was missing two modules and status 1 still read as plain "blocked".

Mock server: documented the /stalker, /stream/gated and marketing-poster
routes and the HOST variable; replaced the global POST /reset guidance with
the real per-MAC isolation contract (OWNED_MACS, the sibling 00:1A:79:5F:*
range, mode: 'serial'); added get_main_info; refreshed the project tree; fixed
a broken anchor; and corrected MOCK_PORT, which moves the client side only —
nothing maps it to the server's PORT.

The repo skill's "keep Stalker request rules in Stalker data access" no longer
holds: the wire-format, identity, portal-mode and auth-failure contracts live
in shared/interfaces because the Electron main process cannot import renderer
libs.

Also fixes four stale code comments carrying the same claims, including
"Single choke point for Stalker API calls" — four callers deliberately go
direct, and only fetchViaProfile() wires repair itself.

Docs and comments only; no executable change. No release note (no user-visible
behavior); no-release-note label applied for the libs/** paths.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-06 17:49:49 +02:00
4grayandClaude Opus 5 c741815b97 fix(build): include shared UI stylesheets in Nx cache inputs (#1360)
* fix(build): include shared UI stylesheets in Nx cache inputs

`libs/ui/styles` held shared SCSS partials but had no `project.json`, so its
files belonged to no Nx project and were absent from every task hash. Editing
a partial and running `pnpm nx build web` reported 4 of 4 tasks cached and
shipped the previous CSS — a silent wrong build rather than a failure.

Nx derives its project graph from TypeScript imports only, so a relative Sass
`@use` that crosses a project root creates no edge. Verified directly: after
adding the project but before declaring anything, `ui-styles` still had zero
dependents in the graph.

Make it the `ui-styles` project (no targets — it exists to be hashed) and
declare `implicitDependencies` on the 8 consumers. Chosen over adding the path
to `sharedGlobals`, which would put shared styles into every project's hash and
make a one-line SCSS tweak mark the whole workspace affected. A styles edit now
marks 15 projects affected and leaves electron-backend, website, the mock
servers and the shared libs alone.

`libs/ui/styles` was the only projectless directory holding files under `libs/`
or `apps/`.

Add `pnpm run styles:inputs:validate` to keep it closed: it resolves every
relative stylesheet import against Nx's real project graph and fails when one
escapes the input closure of a build that compiles it, naming the project to
declare. It exits 1 with 21 diagnostics on the pre-fix tree. Imports of
`apps/web/src/nav-list.scss` are deliberately accepted — `web` already hashes
that file, and a lib -> app edge would make the graph cyclic.

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

* fix(build): spawn git without a shell in the stylesheet check

`execSync("git ls-files '*.scss'")` runs through `cmd.exe` on Windows, where
single quotes are literal characters rather than quoting. Git received the
pathspec with the quotes intact, matched nothing and exited 0, so
`styles:inputs:validate` reported success after checking zero stylesheets —
silently disabling the check for Windows developers while staying green.

Spawn with `execFileSync` so no shell is involved and git expands its own
pathspec; verified to return the identical 133 files.

Both this and the eslint glob trap next to it in the docs report success while
covering nothing, so also make an empty scan fail rather than pass: the
workspace always contains SCSS, and a listing that returns none means the scan
broke.

Reported by Codex review on #1360.

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

* refactor(styles): move nav-list partial into ui-styles (#1361)

* fix(build): count every target of a comma-separated Sass @import

`@import` is the only rule that takes a list, and the scan read just its
first target. A later entry crossing an Nx project boundary escaped the
cache key while the check still reported success — the same silent-pass
failure the tool exists to prevent.

Parse every target of an `@import` list. The obvious "read all quoted
strings" fix trades one silent gap for a phantom one, so the rule decides:
`@use`/`@forward` load exactly one module and a quoted string after it is
`with (...)` configuration, and `url(...)` stays a plain CSS import the
browser resolves at runtime. Neither is a module Sass compiles.

The workspace has no relative `@import` at all today, so the scan still
finds the same 42 imports across 133 files; this closes the gap before
someone writes one.

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

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-08-02 23:18:37 +02:00
4gray 2ac0de752f fix(skills): align repository guidance with implementation (#1315)
* docs(skills): design implementation synchronization

* docs(skills): plan implementation synchronization

* fix(release): filter internal notes from public body

* docs(release): synchronize release workflow guidance

* fix(stalker): normalize catalog series flags

* fix(stalker): preserve progress with scoped episode IDs

* fix(playback): expose strict position persistence

* docs(stalker): record series position compatibility

* test(skills): validate repository skill contracts

* fix(database): keep SQL trace values private

* docs(skills): refresh Nx and SQLite ownership

* docs(skills): align provider and UI guidance

* docs(skills): tighten validated guidance

* docs(release): require exact release pushes

* style(electron): remove trailing blank line

* fix(ci): classify repository skills coverage
2026-07-31 08:00:59 +02:00
4grayandClaude Opus 5 b4ec68c1fa feat(release): manifest-driven screenshot capture with fail-closed mock-data guards (#1261)
Third slice of the release-notes pipeline (#1256 format+generator, #1257 CI
gate): release screenshots become reproducible and provably mock-only.

The v0.20 capture script was single-use (hard-coded slugs, paths, hero) and
fail-open: a lost IPTVNATOR_E2E_DATA_DIR silently fell back to the user's
real ~/.iptvnator database, `...process.env` leaked ambient TMDB keys and
proxies, nothing gated network access, and no frame content was ever
validated. Each hole leaks real playlists, credentials, or copyrighted
artwork into published screenshots without a single signal.

New pipeline:

- tools/release/screenshots.manifest.json — declarative shots (slug, title,
  named setup steps, themes). Adding a feature shot = one manifest entry.
- capture-release-screenshots.ts — orchestrator; output goes to
  apps/website/public/blog/<release>/screenshots/<slug>-<theme>.png, release
  slug derived from package.json (or --release), --only/--theme filters.
- capture-app-driver.ts / capture-navigation.ts — launch, seeding, theme,
  and the named-action vocabulary; actions are order-independent (every
  portal action starts from the dashboard).
- screenshot-guards.mjs — the fail-closed policy, pure and unit-tested:
  G1 the real database is snapshotted (sha256+mtime) before launch and must
     be byte-identical after; the isolated DB must actually exist
  G2 the app receives an allowlisted environment, never ...process.env
  G3 deny-by-default network gate; known app-level calls (GitHub update
     check) are answered by local stubs; any other blocked request fails
     the run — a silently-blocked TMDB call would leave a frame that looks
     broken rather than unsafe
  G4 every frame is scanned before capture: external img/background URLs,
     credential-shaped text, MAC addresses, non-localhost m3u8 references
  G5 TMDB enrichment asserted disabled via the renderer's IndexedDB
  Any violation deletes every frame captured in the run and exits non-zero.

The guards paid for themselves on the first live run: G3 caught the mock
server redirecting stream endpoints to a public demo HLS
(test-streams.mux.dev) — meaning earlier hand-run captures could embed
third-party video frames. The M3U shot now deliberately captures the groups
layout without starting playback.

`.changes` validation now cross-checks `screenshot:` slugs against the
manifest, so a note cannot reference an image the capture run never
produces.

Verified end-to-end: 10/10 shots (5 slugs × dark/light) captured against
dist build + xtream-mock-server, frames visually inspected (fictional
titles/artwork only), guard-violation paths exercised live. 67 unit tests
in release-tools, lint green, script files within the repo size limit.

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-07-26 00:19:02 +02:00
4grayandClaude Opus 5 4e5132cbb5 ci(release): gate PRs on an authored release note (#1257)
* ci(release): gate PRs on an authored release note

Second slice of the release-notes pipeline (#1256 landed the format and
generator): make the .changes/ habit survive contact with reality.

- "Release note gate" job in ci.yml, PR-only: validates every .changes/*.md,
  then requires an added note (or the no-release-note label) when the PR
  touches runtime code under apps/ or libs/. Tests, e2e projects, the
  website, mock servers, shared testing helpers, snapshots and docs are
  auto-exempt.
- Policy lives in tools/release/check-release-note-gate.mjs as a pure
  function fed PR files+labels as JSON — unit-tested (10 cases) instead of
  encoded in workflow bash. The failure message lists the triggering files
  and names the exact fix.
- Labels are fetched live rather than from the stale event payload, so
  applying the label and re-running the check works without a new push.
- The job is dependency-free Node: no pnpm install, runs in seconds.
- release-notes and release-cut skills added under .claude/skills/ and
  mirrored to .codex/skills/; CLAUDE.md/AGENTS.md sections updated to point
  at the gate and the skills.

The no-release-note label itself was created in the repository.

Tests: 47 passing in release-tools (10 new gate cases); gate-step shell
verified with shellcheck at the CI severity; ci.yml YAML-parse checked.

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

* chore(agents): make the release skills discoverable by Claude Code too

`.codex/skills/**` was un-ignored so Codex picks up repository skills in any
clone, but `.claude` was ignored wholesale — and Claude Code only discovers
skills under `.claude/skills/`. The release-notes and release-cut skills
therefore existed only on whichever machine authored them.

Mirror both skills into `.claude/skills/` and opt them in by name rather than
un-ignoring the directory: contributors keep personal skills there
(i18n-fill, website, …) which must stay local and out of `git status`.

CLAUDE.md/AGENTS.md updated so the "skills live under .codex/skills/" claim
does not go stale, including the requirement to keep mirrored copies in sync.

The CI gate and the CLAUDE.md/AGENTS.md section remain the load-bearing
enforcement; skills only carry the detail.

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

* fix(ci): only a note this PR authored satisfies the release-note gate

Review follow-ups on #1257 (Codex P2 ×2, Greptile P1).

- Drop `renamed` from the accepted statuses. The PR files API compares
  base…head, so a note created and then renamed inside the same PR still
  reports as `added`; a `renamed` entry means the file already existed on the
  base branch. Accepting it let a runtime-code PR pass by moving another
  PR's unconsumed note, which documents nothing and gives the generator no
  adding commit to resolve a PR link from.
- Require a direct child of `.changes/`. `loadNotes()` reads only the
  immediate directory, so `.changes/sub/note.md` satisfied the old prefix
  check while never being validated or rendered into any release surface.

Tests: renamed and nested notes now assert a failing gate (12 gate cases).

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

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-07-25 21:51:04 +02:00
4gray d308749e2c fix(stalker): preserve is_series episode metadata (#1218) 2026-07-21 07:51:37 +02:00
4gray d24c77a143 chore(nx): enforce scoped workspace boundaries (#942) 2026-05-15 09:59:06 +02:00