dev #59

Merged
richard merged 6 commits from dev into main 2026-08-23 19:28:34 +02:00
Owner
No description provided.
YouTube hands the player a primary and a backup CDN host but only moves to
the backup once its own ~5.4s timeout expires. When something on the network
path breaks keep-alive to the primary — a middlebox, a broken ISP-hosted
cache node — the first request on a fresh connection still answers in tens of
milliseconds while every reuse of that connection hangs with no response
headers at all.

A captured session showed the cost: three stalls of 5412/5406/5419ms, 16.2s
spent waiting against 1.8s of actual transfer, and ~10s before the first
frame. Each stall ended in the player aborting and immediately succeeding on
the backup host, so the failover was always available — it was just late.

content-main.js now wraps the page's fetch and aborts a googlevideo
/videoplayback request that has produced no response headers within
mediaStallTimeoutMs (default 2000ms). fetch() settles on headers rather than
body, so this catches a dead connection and never interrupts a healthy
transfer that is merely large. A host that stalls twice is failed without
opening a connection for the next minute, then given one more chance.

Health is in-memory only, never persisted: a laptop moving between networks,
or an ISP repairing the path, must not inherit an earlier verdict.

The decision rules live in defaults.js next to the other FY_* helpers so they
are testable; content-main.js owns only the fetch wrapper, since the MAIN
world is the only place the player's fetch is reachable.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
i18n.js carries the complete en/nl/de/fr dictionaries for the popup,
settings, dashboard, help and changelog pages — 194 KB of object literal that
the manifest was parsing at document_start on every YouTube navigation,
before YouTube's own HTML starts rendering. content.js reads eleven strings
from it: the SponsorBlock category labels and skip verb, and the search
duration filter's controls.

scripts/build-i18n-content.js now generates i18n-content.js from those paths
and the content script injects that instead: 5.9 KB, 97% smaller. Extension
pages keep loading the full i18n.js, and the generated file exposes an
identical FY_I18N surface, so no caller changes.

The failure mode of a generated file is silence — a stale copy, or a new
lookup in content.js that nobody added to CONTENT_PATHS, both surface as an
English fallback for everyone who is not on English rather than an error.
test/i18n-content.test.js covers both: it regenerates and compares bytes, and
it recovers the i18n paths from content.js by scanning the source, so a
lookup added tomorrow is checked the moment it is written. Verified by
introducing each failure and confirming the suite goes red.

build-store-zip.sh refuses to package a stale bundle rather than
regenerating one behind the packager's back.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
content.js has carried auditSelectors() for a while: it walks the 25 entries
in FY_AUDITED_SELECTORS and reports which ones match nothing. It is tested,
it works, and it only ever ran if someone typed __CW_AUDIT_SELECTORS__() into
a console — which nobody was ever going to do. So the detector for this
project's known recurring failure existed while the failure kept reaching
users, exactly as it did when the members badge moved from
.yt-badge-shape__text to .ytBadgeShapeText.

It now runs on its own, six seconds after a page settles, throttled to once
per ten minutes across all tabs.

A single reading is far too noisy to act on — a feed that has not rendered
reports every feed selector as dead. So readings fold into a rolling record
in chrome.storage.local and a selector is only called broken after three
consecutive dead readings. One live reading resets the streak, so a selector
that recovers heals itself with no migration and no user action. A run in
which nothing matched at all is discarded rather than believed: across 25
selectors on several page types, a simultaneous rename does not happen, but
an unpopulated page does.

The popup shows a calm amber notice when something is broken and the
Dashboard card carries the detail plus a copy-to-clipboard report worth
pasting into an issue. That card lists registry keys rather than translated
feature names on purpose — its audience is a bug report, and 25 selectors in
four languages would be a hundred strings nobody reads.

New toggle, on by default: Data & Sync -> Updates -> "Warn when filters stop
matching". Nothing leaves the device; the record holds selector names and
match counts, never anything about what was watched. Both privacy policies —
the website and the in-extension Help page — name it in all four languages.

The dashboard constants sit with the other module constants rather than beside
their functions: applyDashboardTranslations() runs while the script is still
loading, so a const below that point is in its temporal dead zone. The
page-load suite caught that on the first run, which is what it is for.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Version bumped in manifest.json and package.json, both changelogs written,
release zip rebuilt.

CHANGELOG.md carries the full technical record of the three changes;
changelog-data.js gets three curated user-facing entries in all four
languages, which is what the changelog page renders and what the update
toast mines. README updated for the two new features, the i18n split, and
the filter-health record's place in the privacy section.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
365DevNet/CleanWatch!59
No description provided.