Rename clicktrack.js — a content blocker was eating it and killing the app
A file called `clicktrack.js` matches the click-tracking telemetry patterns that EasyPrivacy and similar lists block by substring. In a browser with a content blocker the request never completes, and because it is a module import that takes the entire graph with it: main.js never runs, no handler is ever bound, and every control in the UI sits there looking correct and doing nothing. The failure is unusually expensive to diagnose because everything else looks healthy. The dev server returns 200 with the right MIME type, curl fetches it fine, node imports it fine, and the app loads perfectly in any browser without a blocker — which is how it passed every check here. Only the console names it, and only as one line about a module that failed to load. Renamed to metronome.js, which is also the better name for what it does. The button id went with it, since cosmetic filter rules can hit ids too. The general rule, recorded at the top of the file: anything shipped to a browser and named like tracking will be treated as tracking. Avoid click, track, analytics, pixel, beacon and ad in filenames and URL paths, however honest the code behind them is. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
parent
4267c588aa
commit
4217cca3d3
@ -45,7 +45,7 @@ under the playhead; **lock** protects a section from further rerolls. Every
|
||||
parameter the generator chose is exposed under the *scene* tab and can be edited
|
||||
live.
|
||||
|
||||
The **click track** button (look tab) mixes an audible click onto the detected beat
|
||||
The **metronome** button (look tab) mixes an audible click onto the detected beat
|
||||
grid. If the clicks don't sit on the beat, tempo detection is wrong and everything
|
||||
downstream inherits it — check this first when a track looks off.
|
||||
|
||||
@ -133,7 +133,7 @@ Scenes that composite over a background rather than being one declare
|
||||
|
||||
```
|
||||
src/
|
||||
audio/ decode, STFT analysis, tempo, segmentation, FeatureTrack, click track
|
||||
audio/ decode, STFT analysis, tempo, segmentation, FeatureTrack, metronome
|
||||
engine/ Timeline, Renderer, Layer, Compositor, passes, seeded rng, flash safety
|
||||
look/ palette (OKLCH), LookGenerator, ArcDriver
|
||||
params/ declarative schema, validation, serialisation
|
||||
|
||||
@ -80,6 +80,73 @@
|
||||
|
||||
<input type="file" id="file-input" accept="audio/*" hidden>
|
||||
<audio id="audio" hidden></audio>
|
||||
|
||||
<div id="boot-error" hidden>
|
||||
<div class="be-inner">
|
||||
<div class="be-title">the app did not start</div>
|
||||
<div class="be-body">
|
||||
<p>Every control is inert because <code>main.js</code> never ran — the
|
||||
markup you are looking at is the static page. Nothing is wrong with
|
||||
your audio file.</p>
|
||||
<pre id="be-detail"></pre>
|
||||
<p class="be-hint">A module that fails to load takes the whole graph
|
||||
with it, and the usual cause is a fetch that hit the dev server while
|
||||
it was restarting. A hard reload normally fixes it; if it does not,
|
||||
restart <code>npm run dev</code>.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// BOOT GUARD. Loaded before the module and deliberately not a module
|
||||
// itself, so it survives whatever kills the module graph.
|
||||
//
|
||||
// A module that fails to load — a 404, a MIME error, a syntax error, a
|
||||
// dev server caught mid-restart — aborts main.js silently. The page
|
||||
// still renders, every button is still there, and not one of them is
|
||||
// wired to anything. That failure cost a real debugging session: the UI
|
||||
// looked correct and simply did nothing, which is the most expensive
|
||||
// way for a front end to break.
|
||||
(function () {
|
||||
var shown = false;
|
||||
function show(detail) {
|
||||
if (shown) return;
|
||||
shown = true;
|
||||
var box = document.getElementById('boot-error');
|
||||
var pre = document.getElementById('be-detail');
|
||||
if (pre) pre.textContent = detail || 'no detail available';
|
||||
if (box) box.hidden = false;
|
||||
}
|
||||
|
||||
// A module that never executes never sets this.
|
||||
window.addEventListener('error', function (e) {
|
||||
// Module load failures arrive as an error event on the script
|
||||
// element rather than as a window error with a message.
|
||||
if (e && e.target && e.target.tagName === 'SCRIPT') {
|
||||
show('failed to load: ' + (e.target.src || 'unknown module') +
|
||||
'\n\nOpen the console — the first "Loading failed for the ' +
|
||||
'module" line names the file that actually broke.');
|
||||
} else if (e && e.message) {
|
||||
show(e.message + (e.filename ? '\n at ' + e.filename + ':' + e.lineno : ''));
|
||||
}
|
||||
}, true);
|
||||
|
||||
window.addEventListener('unhandledrejection', function (e) {
|
||||
var r = e && e.reason;
|
||||
show('unhandled rejection during startup:\n' + ((r && r.stack) || r));
|
||||
});
|
||||
|
||||
// The backstop for the case no error event fires at all: if the app
|
||||
// has not announced itself in a few seconds, it is not going to.
|
||||
setTimeout(function () {
|
||||
if (!window.__FLOW_STATE_READY__) {
|
||||
show('main.js did not finish starting within 8 seconds and no ' +
|
||||
'error was reported.\n\nCheck the console for the first red ' +
|
||||
'line — a failed module import is the usual cause.');
|
||||
}
|
||||
}, 8000);
|
||||
}());
|
||||
</script>
|
||||
<script type="module" src="/src/main.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@ -1,6 +1,17 @@
|
||||
// Click track — the Phase 1 gate, and the most useful validation tool in the
|
||||
// The metronome — the Phase 1 gate, and the most useful validation tool in the
|
||||
// project.
|
||||
//
|
||||
// NAMED CAREFULLY. This file was `clicktrack.js` until a content blocker ate it:
|
||||
// `clicktrack` is a click-tracking telemetry pattern that EasyPrivacy and
|
||||
// friends block by substring, so the request died in the browser, the module
|
||||
// graph died with it, main.js never ran, and every control in the app went
|
||||
// inert while looking perfectly fine. The server was serving it with a 200 the
|
||||
// whole time.
|
||||
//
|
||||
// Anything shipped to a browser and named like tracking will be treated as
|
||||
// tracking. Avoid `click`, `track`, `analytics`, `pixel`, `beacon` and `ad` in
|
||||
// filenames and URL paths, however honest the code behind them is.
|
||||
//
|
||||
// Beat detection cannot be judged by watching visuals: a grid that is 20ms late
|
||||
// or at half tempo still "looks kind of right". Mixing an audible click onto the
|
||||
// detected grid makes the answer immediate and unambiguous. Downbeats get a
|
||||
@ -4,7 +4,7 @@
|
||||
// a causal detector has to converge, and lags for the first several bars of every
|
||||
// section. Here the grid is exact from frame zero, and phase is fitted globally.
|
||||
//
|
||||
// The click track (audio/clicktrack.js) exists to validate this by ear. If the
|
||||
// The metronome (audio/metronome.js) exists to validate this by ear. If the
|
||||
// clicks don't sit on the beat, nothing downstream can be trusted — every timing
|
||||
// artefact in the finished video traces back to this file.
|
||||
|
||||
|
||||
@ -5,7 +5,7 @@ import { formatTime } from './audio/decode.js';
|
||||
import { describeLook } from './look/LookGenerator.js';
|
||||
import { toHex } from './look/palette.js';
|
||||
import { applyGrainToPost, describeGrain, GRAIN_MASKS, GRAIN_MODES } from './look/grain.js';
|
||||
import { renderClickTrack, audioBufferToWavBlob } from './audio/clicktrack.js';
|
||||
import { renderClickTrack, audioBufferToWavBlob } from './audio/metronome.js';
|
||||
import { Exporter, exportSegment, downloadBlob, PRESETS, isSupported } from './export/Exporter.js';
|
||||
|
||||
const QUALITY = {
|
||||
@ -328,10 +328,10 @@ function renderPanel() {
|
||||
${s.shots ? `<i class="dim">${s.shots.length} shots</i>` : ''}</span>
|
||||
<b>${(s.variants || [s.layers]).map((v) => v[0].module.name).join(' / ')}</b>
|
||||
</div>`).join('')}
|
||||
<button id="btn-clicktrack" class="wide">download click track</button>
|
||||
<button id="btn-metronome" class="wide">download click track</button>
|
||||
<div class="hint">Mixes clicks onto the detected beat grid. If they don't sit on
|
||||
the beat, tempo detection is wrong and everything downstream inherits it.</div>`;
|
||||
document.getElementById('btn-clicktrack').addEventListener('click', downloadClickTrack);
|
||||
document.getElementById('btn-metronome').addEventListener('click', downloadClickTrack);
|
||||
return;
|
||||
}
|
||||
|
||||
@ -562,11 +562,11 @@ document.getElementById('btn-segment').addEventListener('click', () => runExport
|
||||
|
||||
async function downloadClickTrack() {
|
||||
if (!state.show.ready) return;
|
||||
const button = document.getElementById('btn-clicktrack');
|
||||
const button = document.getElementById('btn-metronome');
|
||||
button.textContent = 'rendering…';
|
||||
try {
|
||||
const buffer = await renderClickTrack(state.show.audioBuffer, state.show.track.tempo);
|
||||
downloadBlob(audioBufferToWavBlob(buffer), `${state.show.fileName}-clicktrack.wav`);
|
||||
downloadBlob(audioBufferToWavBlob(buffer), `${state.show.fileName}-metronome.wav`);
|
||||
button.textContent = 'download click track';
|
||||
} catch (err) {
|
||||
button.textContent = `failed: ${err.message}`;
|
||||
@ -652,3 +652,9 @@ if (import.meta.env && import.meta.env.DEV) {
|
||||
|
||||
requestAnimationFrame(frame);
|
||||
resize();
|
||||
|
||||
// Tell the boot guard in index.html that the module graph made it all the way
|
||||
// through. Without this the guard cannot distinguish "still starting" from
|
||||
// "never going to start", and the failure it exists to catch is exactly the one
|
||||
// that produces no error at all in the page.
|
||||
window.__FLOW_STATE_READY__ = true;
|
||||
|
||||
Loading…
Reference in New Issue
Block a user