fix: recover completed startup fade cleanup (client-reliability-10)

Extract existing delegated cleanup from index into one cohesive helper and reconcile finished interface CSS animations when initialization arrives after animationend. Preserve active/replacement effects, future end/cancel events, thumbnail eligibility and unrelated classes. No CSS or other bootstrap changes.

New17 controlled scenarios including real isolated index cleanup wiring;19distinct mutants killed/all17casescovered, full lint11pass. Existing assertions unchanged. Full gate, build and native point validation still require separate slots.
This commit is contained in:
thePR0M3TH3AN committed 2026-10-26 21:39:42 -04:00
1 parent f01ce01259
commit aff8841694
11 files changed
+318 -54

No files matched your search

@@ -0,0 +1,7 @@
# Client10 goal and scope
- Authority exact dispatch ROUTING-2026-10-27-client10-animation-cleanup.md read; sourcef01ce012 after safe ff, untracked client9 note preserved.
- Goal recover already-finished transient interface fade/delay classes during delayed cleanup init; preserve active/future interface animation and thumbnail end/cancel semantics. Existing listener-only cleanup cannot observe an event already dispatched.
- Done now: minimal index block extraction + helper, scenario/wiring regressions, killing mutants for all cases, lint under freshhealth. Fullgate/landing/build/nativebrowser are later explicit slots. No other bootstrap/CSS/app edits or oldoverflow closure.
- Plan: create/claim10; record contract/testintegrity; extract existing cleanup; reconcile finished own-target CSS interface animations with no stillactive counterpart; test real helper and extracted bootstrap wiring; isolated mutants; lint; namedpathcommit/merge and READY only, no submit.
- getAnimations/finished/fill reference https://www.w3.org/TR/web-animations-1/; finished forwards effects remain relevant and can retain stacking context. Avoid sleeps, timers, layout reads, arbitrary phase-duration guesses and broad class stripping.
@@ -0,0 +1,7 @@
# Client10 decisions
- Extract only existing animationcleanup block into59line ui/fadeInCleanup helper; index1456→1405lines. No other bootstrap/CSS/app files changed. Existing thumbnail eligibility retained on end/cancel; pseudo effects are excluded from element cleanup.
- Install one capture listenerpair then query Document.getAnimations once for already-finished own-target interface CSS animations. Skip running/paused/idle effects and finished effects with same-target live counterpart. No elapsedtime guesses/timers/MutationObserver/layout reads/promises to retain.
- Unsupported animation inspection retains event-based cleanup; reducedmotion animation:none retains original hooks without animation effects or new motion. No unsupported-browser late-recovery guarantee. Reference https://www.w3.org/TR/web-animations-1/ describes relevant finished/filling effects and stacking contexts.
- Per-document WeakMap makes repeated init idempotent and gives safe explicit disposal/reinstall. Stale disposer cannot erase replacement registration.
- Focused scenarios use real helper/JSDOM and controlled CSSAnimation metadata; wiring executes real isolated indexcleanup statements with actual imported helper, not fullapp/bootstrap dependencies. Native CSS animation/hit-testing requires later granted QA. Preserve originaloverflow362/360+394/390 and live/wholeStage3 boundaries.
+2
View File
@@ -80,6 +80,8 @@
## client-reliability
- 2026-10-27 client10 scoped cleanup READY: late-init recovery extracted from index animation block to59line helper;17newscenarios pass/19distinct killedmutants/all17covered/lint11pass. Existing tests/CSS/app/bootstrap untouched outside block/import. No full/build/browser/submission grant; native360point+390/1280/reducedmotion and normalgate/landing remain. Preserve prior362/360+394/390 overflow and item9live/native limits. Evidence test_logs/TEST_LOG_2026-10-27_01-39-00_client-reliability-10-ready.md.
- 2026-10-26 19:43UTC: separately granted local build/artifact passes exit0 at7594c54d (runtimeaf61c271); artifact15868a91/indexSHA9c48e0fe, clean tree, no previewserver/deploy. Sandbox build swcompat history probe skipped; prior unchanged-input hostlint11pass retained. Slot RELEASED; native torrent/browser+QA timing need distinct routing, item9 staysopen. test_logs/TEST_LOG_2026-10-26_19-43-00_client-reliability-9-build.md.
- 2026-10-26 19:41UTC: item9 active-torrent product LANDED af61c271 via normal eafa364d queue gate exit0; independent438discovered/438unique starts/missingextra0 and ancestry0. Complete log/result/discovery hashed under state/evidence/client9-torrent-validation-20261026, prior rejected275partial intact. lint11/savedfocused60/mutants19 retained. Normalgate slot RELEASED; separate build/native-browser routing remains, item9 open. No real peers/byte teardown/wholeStage3 claim. See test_logs/TEST_LOG_2026-10-26_19-41-00_client-reliability-9-torrent-landed.md.
@@ -0,0 +1,11 @@
{
"id": "client-reliability-10",
"area": "client-reliability",
"status": "in-progress",
"title": "Recover completed startup fade animation after delayed cleanup initialization",
"blockedOn": null,
"addedOn": "2026-10-27",
"filedAt": "f01ce012595d74abac3c8adca7c8eec219294c7d",
"brief": "/home/user/Documents/GitHub/bitvid-dispatch/docs/agents/ROUTING-2026-10-27-client10-animation-cleanup.md",
"evidence": "Dispatch independently measured actualapp360 animationend514.8ms before listener3245.7ms; samepointASIDEfailure\u2192onlyapp.fade-in removalINPUTtoggle, geometry unchanged. Relevantinputdiff0 vs2754a8af/currentf01ce012; PRcheck0 attributed dispatch. Explicit bounded indexcleanup+optionalhelper/focusedtests; noCSS/app/otherbootstrap. Focused/mutants/lint only; full/build/browser/submit awaitREADY+slot. Minimal cleanup extracted at f01ce012: index1405/helper59, new17scenarios17pass/fail0/cancelled0;19distinctisolatedmutantsallkilled/all17casesmapped;hostlint11pass, diffcheck0. Noexistingtestedits/full/build/browser/submission. Evidence test_logs/TEST_LOG_2026-10-27_01-39-00_client-reliability-10-ready.md; sourcewiring scenario executes actualisolatedindexsection, notfullapp; nativeCSS/hittesting remains later."
}
+3 -54
View File
@@ -56,6 +56,7 @@ import { relayManager } from "./relayManager.js";
import createApplication from "./bootstrap.js";
import SearchFilterModal from "./ui/components/SearchFilterModal.js";
import { initializeSiteUpdateNotice } from "./ui/siteUpdateNotice.js";
import { initializeFadeInCleanup } from "./ui/fadeInCleanup.js";
import { installTestHarness } from "./testHarness.js";
validateInstanceConfig();
@@ -497,61 +498,9 @@ if (typeof window !== "undefined" && typeof window.addEventListener === "functio
window.addEventListener("pageshow", handlePageShow);
}
const INTERFACE_FADE_IN_ANIMATION = "interface-fade-in";
const VIDEO_THUMBNAIL_FADE_IN_ANIMATION = "video-thumbnail-fade-in";
//
// Centralized animation cleanup
// ------------------------------
// We attach a single capture-phase listener for animation events so that any
// chrome element or thumbnail using our fade-in helpers can automatically shed
// the temporary classes/data attributes that trigger the transition. This is
// critical because these DOM nodes frequently persist while the surrounding
// lists re-render; if the classes stick around, future layout shuffles would
// replay the fade and make the UI flicker. Clearing the hooks immediately after
// the first animation keeps the graceful entrance without introducing future
// flashes.
const handleFadeInAnimationComplete = (event) => {
const { animationName, target } = event;
if (!(target instanceof HTMLElement)) {
return;
}
if (animationName === INTERFACE_FADE_IN_ANIMATION) {
if (!target.classList.contains("fade-in")) {
return;
}
// Remove the transient fade-in class so future renders keep the element
// visible. The delays piggyback on `fade-in`, so we strip those too.
target.classList.remove("fade-in");
Array.from(target.classList).forEach((className) => {
if (className.startsWith("fade-in-delay-")) {
target.classList.remove(className);
}
});
return;
}
if (animationName !== VIDEO_THUMBNAIL_FADE_IN_ANIMATION) {
return;
}
if (
target.dataset &&
target.dataset.videoThumbnail === "true" &&
target.dataset.thumbnailLoaded === "true"
) {
// Thumbnails use the same pattern: once the fade finishes we clear the flag
// so a later `load` event (or template reuse) does not restart the
// animation.
delete target.dataset.thumbnailLoaded;
}
};
document.addEventListener("animationend", handleFadeInAnimationComplete, true);
document.addEventListener("animationcancel", handleFadeInAnimationComplete, true);
// Recover completed startup fades as well as future animation events.
initializeFadeInCleanup(document);
// 1) Load modals (login, application, etc.)
const fetchPartial = async (url) => {
+59
View File
@@ -0,0 +1,59 @@
const INTERFACE_ANIMATION = "interface-fade-in";
const THUMBNAIL_ANIMATION = "video-thumbnail-fade-in";
const registrations = new WeakMap();
function clearInterfaceFade(target) {
if (!target.classList.contains("fade-in")) return;
target.classList.remove("fade-in");
for (const className of Array.from(target.classList)) {
if (className.startsWith("fade-in-delay-")) target.classList.remove(className);
}
}
// Install before reconciling: initial CSS animation events can precede the
// application module's evaluation. Finished forwards-fill effects still appear
// in getAnimations() and retain their stacking context until the class is gone.
export function initializeFadeInCleanup(doc = document) {
if (registrations.has(doc)) return registrations.get(doc);
const isElement = (target) => target instanceof doc.defaultView.HTMLElement;
const onComplete = ({ animationName, target, pseudoElement }) => {
if (!isElement(target) || pseudoElement) return;
if (animationName === INTERFACE_ANIMATION) {
clearInterfaceFade(target);
} else if (
animationName === THUMBNAIL_ANIMATION &&
target.dataset.videoThumbnail === "true" &&
target.dataset.thumbnailLoaded === "true"
) {
delete target.dataset.thumbnailLoaded;
}
};
doc.addEventListener("animationend", onComplete, true);
doc.addEventListener("animationcancel", onComplete, true);
const dispose = () => {
doc.removeEventListener("animationend", onComplete, true);
doc.removeEventListener("animationcancel", onComplete, true);
if (registrations.get(doc) === dispose) registrations.delete(doc);
};
registrations.set(doc, dispose);
// No duration guesses or timers: running, delayed and paused entrances must
// keep their classes. Unsupported animation inspection retains event cleanup.
const animations = typeof doc.getAnimations === "function" ? doc.getAnimations() : [];
const interfaceAnimations = animations.filter((animation) =>
animation.animationName === INTERFACE_ANIMATION &&
isElement(animation.effect?.target) && !animation.effect.pseudoElement
);
for (const animation of interfaceAnimations) {
const target = animation.effect.target;
if (
animation.playState === "finished" &&
!interfaceAnimations.some((other) =>
other.effect.target === target && other.playState !== "finished"
)
) {
clearInterfaceFade(target);
}
}
return dispose;
}
@@ -0,0 +1,32 @@
# Client10 transient startup cleanup contract
Authority: exact dispatch ROUTING-2026-10-27-client10-animation-cleanup.md. Existing transient interface cleanup is intended after animation end/cancel; late initialization currently misses already-delivered events. Extend initialization to reconcile finished own-target interface CSS animations, while keeping current/future active animations, unrelated effects and thumbnail event rule. No existing tests/expectations changed. Reduced-motion animation:none does not acquire a new animation or timer.
```yaml
test_integrity_note:
change_type: [new_tests]
scenarios:
- id: SCN-late-interface-cleanup
given: interface CSS animation finished before initializer
when: actual cleanup initializer or real index cleanup wiring executes
then: fade-in and all fade-in-delay hooks removed, unrelated classes preserved
- id: SCN-live-future-animation
given: running pending paused or future entrance and unrelated animations
when: initializer and genuine completion or cancellation events occur
then: live effects preserved until completion, unrelated/pseudo effects do not trigger interface cleanup
- id: SCN-thumbnail-and-registration
given: loaded or unloaded thumbnail and repeated cleanup registration
when: matching event, repeated init, disposal and reinstall occur
then: existing thumbnail eligibility respected, single listener pair and deterministic teardown
observable_outcomes: [DOM classes and dataset, listener lifecycle, actual bootstrap cleanup call]
determinism_controls: [JSDOM DOM and events, explicit CSSAnimation-state boundary, no public IO or elapsed-time waits]
anti_cheat_rationale:
prevents: [unconditional class stripping, unrelated-animation cleanup, missing bootstrap call, duplicate listener retention]
relaxation:
did_relax_any_assertion: false
if_true_explain_spec_basis: ''
```
Browser geometry/hit testing, real CSSAnimation state, original overflow and real-device qualification are not established by these controlled offline checks.
Validation:17/17newscenarios pass;19distinct isolated mutants naturally fail withcancelled0, all17cases covered. Exact maps/logs state/evidence/client10-animation-cleanup-20261027/mutants-summary.json and mutants-coverage.json. Existing tests unchanged. Full/browser/build/landing pending.
@@ -0,0 +1,9 @@
# Client10 focused candidate READY
- Exact dispatch routing read, safe ff tof01ce012; relevant inputs unchanged from independently reproduced2754a8af. No baseline/browser repeat. PublicPR0 is dispatch-attributed evidence. Item10 created/claimed, client9note preserved.
- Source scoped js/index.js cleanup block+js/ui/fadeInCleanup.js only; unique new interface-fade-cleanup.test.mjs. No existing test edits/weakening. Integrity contract written before tests.
- Freshhealth01:36:12 OK/CPU50C/load0.57/RAM68.2%/swap-in0. `env -u NODE_TEST_CONTEXT -u UNIT_TEST_SHARD node tests/interface-fade-cleanup.test.mjs > state/evidence/client10-animation-cleanup-20261027/focused-0136.log 2>&1`: naturalexit0,17tests/17pass/0fail/0cancelled/0skip (~137ms TAPduration).
- Fresh01:37:17 OK/CPU48C/load0.41/RAM68.3%/swap-in0. `env -u NODE_TEST_CONTEXT -u UNIT_TEST_SHARD python3 state/evidence/client10-animation-cleanup-20261027/run-mutants.py > .../mutants-0138.log 2>&1`: runnernaturalexit0; all19distinctvariants eachnaturalexit1/fail>0/cancelled0,17/17scenario names have killingvariant. Mutants only isolatedcopies; worktree never modified/restored. Full summary and per-case map mutants-summary.json/mutants-coverage.json. Runner durable state/deps/client10-animation-cleanup, depsmanifest updated.
- Fresh01:37:50 OK/CPU51.1C/load0.59/RAM68%/swap-in0. Approved host `env -u NODE_TEST_CONTEXT -u UNIT_TEST_SHARD npm run lint > .../lint-host-0138.log 2>&1`: naturalexit0/all11checks. Known sandbox gitgrepEPERM avoided via normal approval; no lint baseline/config changes.
- `git diff --check`: exit0. Product index1456→1405lines/helper59; test177lines. Complete source/test snapshots/logs/mutants/manifest saved state/evidence/client10-animation-cleanup-20261027.
- No fullunit/build/browser/submission/landing. Controlled offline test models Animation state; real CSSAnimation enumeration/nativepoint360+390/1280/reducedmotion/realdevice unverified. Originaloverflow remains separate; item9 native/live/CPU/wholeStage3 limits retained.
+177
View File
@@ -0,0 +1,177 @@
import assert from "node:assert/strict";
import test from "node:test";
import { readFile } from "node:fs/promises";
import { JSDOM } from "jsdom";
import { initializeFadeInCleanup } from "../js/ui/fadeInCleanup.js";
function fixture(t) {
const dom = new JSDOM('<!doctype html><body><main id="app" class="fade-in fade-in-delay-100 fade-in-delay-300 keep"></main></body>');
const doc = dom.window.document;
const app = doc.getElementById("app");
const errors = [];
dom.window.addEventListener("error", event => {
errors.push(event.error); event.preventDefault();
});
const animations = [];
const listeners = new Map();
const add = doc.addEventListener.bind(doc), remove = doc.removeEventListener.bind(doc);
doc.addEventListener = (type, fn, capture) => {
if (type.startsWith("animation")) {
assert.equal(capture, true, "cleanup must capture descendant events");
if (!listeners.has(type)) listeners.set(type, new Set());
listeners.get(type).add(fn);
}
add(type, fn, capture);
};
doc.removeEventListener = (type, fn, capture) => {
listeners.get(type)?.delete(fn); remove(type, fn, capture);
};
doc.getAnimations = () => [...animations];
function animation(target = app, playState = "finished", name = "interface-fade-in", pseudoElement = null) {
const value = { animationName: name, playState, effect: { target, pseudoElement } };
animations.push(value); return value;
}
function event(target = app, name = "interface-fade-in", type = "animationend", pseudoElement = "") {
const value = new dom.window.Event(type, { bubbles: true });
Object.assign(value, { animationName: name, pseudoElement });
target.dispatchEvent(value);
}
function thumbnail(loaded = "true", marked = "true") {
const el = doc.createElement("img");
el.dataset.videoThumbnail = marked; el.dataset.thumbnailLoaded = loaded;
doc.body.append(el); return el;
}
function cleaned(el = app) {
assert.equal(el.classList.contains("fade-in"), false);
assert.equal([...el.classList].some(x => x.startsWith("fade-in-delay-")), false);
assert.equal(el.classList.contains("keep"), true);
}
function retained(el = app) {
assert.equal(el.classList.contains("fade-in"), true);
assert.equal(el.classList.contains("fade-in-delay-100"), true);
assert.equal(el.classList.contains("fade-in-delay-300"), true);
}
t.after(() => {
initializeFadeInCleanup(doc)(); dom.window.close();
assert.deepEqual(errors, [], "cleanup events must not throw");
});
return { doc, app, animations, animation, event, thumbnail, cleaned, retained, listeners };
}
test("late initialization recovers finished entrance and all delay hooks", t => {
const f = fixture(t); f.animation(); initializeFadeInCleanup(f.doc); f.cleaned();
});
for (const state of ["running", "paused", "idle"]) {
test(`initialization preserves ${state} entrance until completion`, t => {
const f = fixture(t); f.animation(f.app, state);
initializeFadeInCleanup(f.doc); f.retained();
f.event(); f.cleaned();
});
}
test("finished older effect cannot remove a same-target active replacement", t => {
const f = fixture(t); f.animation(); f.animation(f.app, "running");
initializeFadeInCleanup(f.doc); f.retained();
f.event(); f.cleaned();
});
test("unrelated finished animation preserves entrance hooks", t => {
const f = fixture(t); f.animation(f.app, "finished", "spinner");
initializeFadeInCleanup(f.doc); f.retained();
f.event(f.app, "spinner"); f.retained();
});
test("pseudo-element finished effects and events do not clear the element", t => {
const f = fixture(t); f.animation(f.app, "finished", "interface-fade-in", "::before");
initializeFadeInCleanup(f.doc); f.retained();
f.event(f.app, "interface-fade-in", "animationend", "::before"); f.retained();
});
test("finished child does not clear an ancestor with an active entrance", t => {
const f = fixture(t); const child = f.doc.createElement("div");
child.className = f.app.className; f.app.append(child);
f.animation(f.app, "running"); f.animation(child);
initializeFadeInCleanup(f.doc); f.retained(); f.cleaned(child);
});
test("future dynamically inserted entrance cleans on delegated end", t => {
const f = fixture(t); initializeFadeInCleanup(f.doc);
const later = f.doc.createElement("div"); later.className = f.app.className;
f.doc.body.append(later); f.retained(later); f.event(later); f.cleaned(later);
});
test("active entrance cancellation cleans the same transient hooks", t => {
const f = fixture(t); f.animation(f.app, "running"); initializeFadeInCleanup(f.doc);
f.retained(); f.event(f.app, "interface-fade-in", "animationcancel"); f.cleaned();
});
test("thumbnail end and cancellation preserve existing loaded eligibility", t => {
const f = fixture(t); initializeFadeInCleanup(f.doc);
for (const type of ["animationend", "animationcancel"]) {
const loaded = f.thumbnail(), pending = f.thumbnail("false"), unrelated = f.thumbnail("true", "false");
for (const el of [loaded, pending, unrelated]) f.event(el, "video-thumbnail-fade-in", type);
assert.equal(loaded.hasAttribute("data-thumbnail-loaded"), false);
assert.equal(pending.dataset.thumbnailLoaded, "false");
assert.equal(unrelated.dataset.thumbnailLoaded, "true");
}
f.retained();
});
test("interface completion preserves absent fade-in and unrelated delay classes", t => {
const f = fixture(t); f.app.classList.remove("fade-in"); initializeFadeInCleanup(f.doc);
f.event();
assert.equal(f.app.className, "fade-in-delay-100 fade-in-delay-300 keep");
});
test("repeated initialization shares one capture listener pair and disposes idempotently", t => {
const f = fixture(t); const first = initializeFadeInCleanup(f.doc);
assert.equal(initializeFadeInCleanup(f.doc), first);
assert.equal(f.listeners.get("animationend").size, 1);
assert.equal(f.listeners.get("animationcancel").size, 1);
first(); first();
assert.equal(f.listeners.get("animationend").size, 0);
assert.equal(f.listeners.get("animationcancel").size, 0);
f.event(); f.retained();
const next = initializeFadeInCleanup(f.doc); assert.notEqual(next, first);
first(); // A stale disposer must not erase the current registration.
assert.equal(initializeFadeInCleanup(f.doc), next);
f.event(); f.cleaned();
});
test("missing animation inspection retains event cleanup without guessing completion", t => {
const f = fixture(t); delete f.doc.getAnimations;
initializeFadeInCleanup(f.doc); f.retained(); f.event(); f.cleaned();
});
test("reduced-motion no-animation initialization preserves hooks and thumbnail eligibility", t => {
const f = fixture(t); const img = f.thumbnail();
f.doc.defaultView.matchMedia = query => ({ matches: query === "(prefers-reduced-motion: reduce)" });
initializeFadeInCleanup(f.doc); f.retained();
assert.equal(img.dataset.thumbnailLoaded, "true");
assert.equal(f.animations.length, 0);
});
test("non-element animation events do not interrupt future cleanup", t => {
const f = fixture(t); initializeFadeInCleanup(f.doc);
f.event(f.doc); f.retained(); f.event(); f.cleaned();
});
test("real index cleanup wiring recovers a fade whose end preceded bootstrap evaluation", async t => {
const f = fixture(t); f.animation();
f.event(); f.retained(); // End occurred before production registration.
const index = await readFile(new URL("../js/index.js", import.meta.url), "utf8");
const imported = index.match(/import\s*\{\s*(\w+)\s*\}\s*from\s*["'](.+fadeInCleanup\.js)["'];/);
assert.ok(imported, "index must import the cleanup initializer");
const actual = await import(new URL(imported[2], new URL("../js/index.js", import.meta.url)));
const start = index.indexOf("// Centralized animation cleanup");
const end = index.indexOf("// 1) Load modals", start);
assert.ok(start >= 0 && end > start, "execute actual isolated bootstrap cleanup section");
const cleanupSection = index.slice(start, end);
// Real bootstrap statements and the real exported initializer; other app IO
// is outside this focused wiring scenario, not replaced by a pretend boot.
new Function("document", imported[1], cleanupSection)(f.doc, actual[imported[1]]);
f.cleaned();
const future = f.doc.createElement("div"); future.className = "fade-in fade-in-delay-100 keep";
f.doc.body.append(future); f.event(future); f.cleaned(future);
});
@@ -0,0 +1,5 @@
- [x] Read exactrouting/rules/knownissues/testintegrity/logging; sourcef01ce012 and preserveclient9note.
- [ ] Claim10 and write integrity/contract.
- [ ] Extract minimal cleanup+finishedanimation recovery, real wiring regressions.
- [ ] Focused+deliberate mutants+lint serial with freshhealth.
- [ ] Namedpathcommit/merge, evidence and READY; no full/build/browser/submit yet.
@@ -0,0 +1,6 @@
- [x] Exactrouting+claim10/sourcef01ce012; no repeated baseline/client9checks.
- [x] Minimal indexblock/helper extraction and17faithful scenarios including real index section.
- [x] All19distinctmutantskilled/all17casescovered; full lint11pass.
- [x] Durable snapshots/logs/mutant runner+deps manifest and integrity notes.
- [ ] Namedpathcommit then merge currentunstable; input comparison and READY review.
- [ ] Explicit fullgate/build/browser/submission grants and durablelanding/ancestry; no current grant.