Merge remote-tracking branch 'origin/master' into claude/parental-control-feature-31dde2

# Conflicts:
#	apps/web-e2e/dist/.playwright/apps/web-e2e/screenshots/settings/@settings @web Change app language.png
#	apps/web-e2e/dist/.playwright/apps/web-e2e/screenshots/settings/@settings @web Change app theme.png
#	apps/web-e2e/dist/.playwright/apps/web-e2e/screenshots/settings/@settings @web Change video player.png
#	apps/web-e2e/dist/.playwright/apps/web-e2e/screenshots/settings/@settings @web Check settings page.png
#	apps/web-e2e/dist/.playwright/apps/web-e2e/screenshots/settings/@settings @web Deep links open one section page and unknown sections redirect.png
#	apps/web-e2e/dist/.playwright/apps/web-e2e/screenshots/settings/@settings @web Leaving with unsaved edits asks for confirmation.png
#	apps/web-e2e/dist/.playwright/apps/web-e2e/screenshots/settings/@settings @web Save-and-leave persists the staged edit.png
#	apps/web-e2e/dist/.playwright/apps/web-e2e/screenshots/settings/@settings @web Unsaved bar survives section switches and discard reverts.png
This commit is contained in:
4gray committed 2026-09-26 14:18:36 +02:00
commit 1d945e91b1
46 files changed
+2647 -82

No files matched your search

+1 -1
View File
@@ -12,7 +12,7 @@
- Date display should follow the user-selected app language from `TranslateService`.
- When a template renders localized month or weekday names, pass the normalized app locale explicitly to `DatePipe`.
- Angular locale data is registered in [apps/web/src/app/app-date-locales.ts](/apps/web/src/app/app-date-locales.ts).
- Angular locale data is loaded lazily per language by `registerAppDateLocale()` in [apps/web/src/app/app-date-locales.ts](/apps/web/src/app/app-date-locales.ts): `main.ts` awaits the initial language's data before bootstrapping, and every `TranslateService.use()` call site registers the data first (through `AppDateLocaleService`), because the switch re-renders dates with the new locale and a locale without data throws. English needs no data. `AppDateLocaleService.use()` applies only the language requested last when switches overlap, and a locale whose chunk fails to load falls back to English formatting under that locale id until a later call loads it.
- App language aliases are normalized in [libs/ui/pipes/src/lib/date-format.util.ts](/libs/ui/pipes/src/lib/date-format.util.ts):
- `ary` -> `ar-MA`
- `by` -> `be`
+146
View File
@@ -0,0 +1,146 @@
# Performance journeys and the CI ratchet
IPTVnator measures performance through a small set of everyday user journeys.
Each journey has deterministic counters that are asserted exactly, and
wall-clock timings that are recorded as evidence. Counters are ratcheted in CI:
a committed baseline may only be lowered, and only with the measured output as
evidence. This document is the contract for that loop; `tools/performance/`
holds the scripts.
## Journeys
| Journey | Start | End |
| ---------------- | -------------------------------------------- | ----------------------------------------------------------------------------- |
| J1 `launch` | Electron process spawn | first playlist or portal card rendered on `/workspace`, inline splash removed |
| J2 `open-source` | click on a portal card | live category list and first channel page painted |
| J3 `playback` | click on a channel | HTML5 `playing` event |
| J4 `search` | six-character query typed into global search | results list settled |
Only the J1 counter `renderer.initialBytes` is instrumented today. The other
journeys and counters follow the plan in `.plans/` and are added one thread at
a time; each thread names its journey and counter in the PR description.
## `renderer.initialBytes`
The bytes a browser fetches before Angular can bootstrap, read from the built
`dist/apps/web/index.html`:
- `index.html` itself,
- every same-origin `<script src>`, including `assets/app-config.js`,
- every `<link rel="stylesheet">`,
- every `<link rel="modulepreload">` chunk.
Manifest, icons, external URLs, commented-out tags and lazy chunks are not
counted, so the value is the same for every language: a non-English launch
additionally fetches that language's Angular locale chunk (about 2 KB), which
belongs to the per-profile J1 benchmark rather than to this counter. A file
that `index.html` references but the build did not emit is an error, never
zero bytes. The value is raw (uncompressed) size, which is what the renderer
parses. It is Angular's "Initial total" plus `index.html` and
`assets/app-config.js` (about 4 KB together), so it sits slightly above the
rounded figure the build prints; never copy that figure into a baseline, use
the script's output. The bundle embeds only the app version from
`package.json` (a named import, which esbuild tree-shakes), not the whole
file, so editing scripts or dependencies does not move the counter.
```bash
pnpm nx build web # production configuration
pnpm run perf:initial-bytes # human-readable breakdown
pnpm --silent run perf:initial-bytes -- --json # machine-readable; --silent keeps pnpm's headers out of stdout
node tools/performance/measure-initial-bytes.mjs --summary dist/performance/journey-summary.json
```
`--summary` writes the journey summary shape (`journeys.<journey>.counters`)
that the ratchet checker consumes. `--dist <dir>` points the script at another
build output, for example the `electron-performance` configuration.
The measurement script is `tools/performance/measure-initial-bytes.mjs`; its
Node tests run with `pnpm nx test performance-tools` (Tier B in the coverage
policy) and lint with `pnpm nx lint performance-tools`.
## Ratchet
`tools/performance/journey-baselines.json` holds one entry per journey and
counter:
```json
{
"journeys": {
"launch": {
"renderer.initialBytes": {
"value": 2739510,
"unit": "bytes",
"updatedAt": "2026-09-26",
"evidencePr": 1693,
"measuredWith": "pnpm nx build web && pnpm run perf:initial-bytes"
}
}
}
}
```
`tools/performance/check-journey-ratchet.mjs` compares a journey summary with
that file:
- a counter above its `value` fails; counters are exact, there is no slack;
- a wall-clock entry carries `toleranceRatio` and fails above
`value × toleranceRatio`;
- a baseline with no measurement in the summary fails, so dropping a
measurement cannot disable the ratchet; a counter is read only from
`journeys.<journey>.counters` and a wall-clock entry (one with
`toleranceRatio`) only from `journeys.<journey>.wallClock`, so a value in
the wrong section also counts as missing;
- a measurement below its baseline passes and prints a "tighten" hint;
- a measured counter without a baseline is noted, not failed;
- checking nothing fails: an empty baselines file, or `--only` naming an
entry that does not exist, cannot exit 0.
`--only <journey>/<counter>` (repeatable) restricts the check to the named
baselines. A script that measures one counter writes its own summary file
and checks only its counter, so it neither overwrites another measurement's
summary nor fails the other baselines as unmeasured.
```bash
pnpm run perf:initial-bytes:check # measure dist/apps/web into dist/performance/initial-bytes.summary.json, check only that counter
pnpm run perf:ratchet:check # check every baseline against dist/performance/journey-summary.json
```
CI runs `perf:initial-bytes:check` in the `Initial bytes ratchet` job of
`.github/workflows/ci.yml` after a production build of `apps/web`, and uploads
`dist/performance/` as the `performance-journey-summary` artifact. Like the
rest of that workflow it runs for pull requests that target `master` and for
pushes to `master`; a stacked PR that targets another branch gets no run until
it is retargeted, so dispatch one with `gh workflow run ci.yml --ref <branch>`
when you need the number. A PR that grows the counter fails that job.
That runner is the canonical measurer: take baseline values from its output,
not from a local build. A local macOS build of the code before #1695 is 2
bytes smaller in `main.js` (the eager locale imports); since #1695 the two
have been byte-identical. (An apparent 556-byte platform difference during
the first measurements was otherwise `package.json` text embedded in
`main.js`, which moved with every script edit; #1692 fixed that by importing
only the version.)
The job also refuses a weakened baselines file:
`tools/performance/check-baseline-direction.mjs` compares
`journey-baselines.json` with the revision the change is measured against
(the target branch of a pull request, the previous head of a `master` push,
`master` for a manual dispatch) and fails when any
entry's enforced limit (`value × toleranceRatio`) went up, a tolerance widened
or an entry disappeared, so a PR cannot grow the payload and raise the
baseline to match. Lowered limits and new entries pass.
Baselines only move down. Lower `value` in the same PR as the change that
earned it, set `updatedAt` and `evidencePr`, and paste the measurement output
into the PR. Never raise a value to make a PR pass: if growth is a deliberate
trade-off, say so in the PR and let the maintainer decide.
## Adding a counter
1. Produce the value from the built output or from a deterministic probe, not
from source heuristics. Missing inputs must fail the measurement.
2. Emit it under `journeys.<journey>.counters.<name>` in the summary JSON.
3. Cover the extraction and the failure modes with `node --test` and register
the test file in `tools/performance/project.json`.
4. Validate the counter before it becomes a guardrail: one PR must show that
lowering it moved wall-clock in the same journey.
+15
View File
@@ -439,6 +439,21 @@ candidate/stable promotion remain manual (see
draft during artifact verification, then publish it in a follow-up commit and
verify the website deployment.
If a Store upload fails after publication, run `publish-snap.yaml` from
`master` with its `tag` input set to the existing public stable tag, for example
`gh workflow run publish-snap.yaml --ref master -f tag=v0.24.0`. The workflow
resolves the public release through the API, rejects drafts/prereleases and
invalid tags, and repeats the full released-tooling, asset and source-archive
verification before uploading to `edge`. Do not move the release tag, rebuild
its assets or republish the GitHub release to retry a Store upload.
Snapcraft extracts metadata into a temporary sibling of the input `.snap`.
The publisher therefore gives it root-owned read-only hard links in a separate
root-owned sticky directory. Temporary siblings are writable, while the sticky
bit prevents the unprivileged uploader from replacing the root-owned inputs.
The original verified snapshot stays sealed; upload filenames are enumerated
only from that snapshot, never from the writable scratch directory.
## Validation
```bash
+18
View File
@@ -153,6 +153,24 @@ Identical English fallback values are reported as warnings by default; use
`node tools/i18n/check-drift.mjs --fail-on-identical` for a stricter translation
audit.
## Performance
```bash
pnpm nx build web
pnpm run perf:initial-bytes # breakdown only
pnpm run perf:initial-bytes:check # measure, then compare with the committed baseline
pnpm nx test performance-tools
```
`perf:initial-bytes` reads the built `dist/apps/web/index.html` and sums the
bytes on the initial path (the J1 counter `renderer.initialBytes`).
`perf:initial-bytes:check` then fails if the value exceeds
`tools/performance/journey-baselines.json`; baselines only move down. CI runs
the same check in the `Initial bytes ratchet` job of `ci.yml` for PRs that
target `master` and for `master` pushes (dispatch it with
`gh workflow run ci.yml --ref <branch>` for a stacked branch). The contract, what counts and how to add a counter are in the
[performance journeys](performance-journeys.md) document.
## Logging
Runtime playback and EPG debug logs should use the existing logger or trace
+1
View File
@@ -14,6 +14,7 @@ are not prerequisites for reading repository contracts.
| Bootstrap, project placement, dependencies, aliases and lint configuration; root Nx config and project-local project.json files | [Nx boundaries](../architecture/nx-workspace-boundaries.md), [security overrides](../architecture/dependency-security-overrides.md) | [Nx architecture](../../.codex/skills/iptvnator-nx-architecture/SKILL.md) |
| Angular conventions; docs and skills maintenance | [Agent workflow](../development/agent-workflow.md) | Use the area's skill below |
| Unit, E2E, lint and coverage; `tools/coverage` | [Validation map](../architecture/validation-map.md) | Use the area's validation section |
| Performance journeys, counters and the CI ratchet; `tools/performance` | [Performance journeys](../architecture/performance-journeys.md) | Read the contract directly |
| Electron entry/events/preload and CDP; `apps/electron-backend` | [Debugging and trace flags](../development/electron-debugging.md), [Electron security](../architecture/electron-security.md) | Use the available global electron skill for automation |
| Releases, notes, screenshots, native assets, Linux manager metadata; `tools/release` | [Release pipeline](../architecture/release-pipeline.md), [note format](../../.changes/README.md) | [Release notes](../../.codex/skills/release-notes/SKILL.md), [release cut](../../.codex/skills/release-cut/SKILL.md) |