mirror of
https://github.com/got-feedBack/feedBack.git
synced 2026-07-22 04:41:23 +00:00
Puts every external `<script>` in the v3 shell into the deferred queue, and
keeps each script's boot() firing at DOMContentLoaded exactly as it does today.
Behaviourally a no-op; it is what makes the ES-module flips safe.
WHY. `type="module"` defers execution to after HTML parse. Classic-`defer` and
module scripts share ONE "execute after parsing" list and run in DOCUMENT ORDER,
but a plain classic script runs DURING parse — ahead of all of them. So the
moment capabilities.js becomes a module while app.js is still plain, app.js runs
FIRST, and its 11 top-level `window.feedBack.on(...)` calls (app.js:6245-6722)
hit a bare `{}` — `_ensureFeedBackEventBus()` (capabilities.js:33), which
attaches .on/.emit/.off, would not have run yet. TypeError, app.js dies
mid-parse. Deferring everything now keeps document order == execution order
through the rest of the migration.
THE CATCH (Codex preflight caught this — a real ordering change). 22 scripts
guard their boot with `if (document.readyState === 'loading')`. A deferred
script runs at readyState 'interactive', so that test is FALSE and the else-branch
fires boot() immediately, at the script's position in document order — instead of
at DOMContentLoaded, after every script has evaluated.
That matters far more than one call site: a scan of the shell's scripts found
**43 forward references** where a script's boot() reads a global that a LATER
script defines (shell.js -> profile.js's window.v3Onboarding, songs.js ->
settings.js's window._confirmDialog, badges.js -> songs.js's
window.displayTuningName, ...). Every one of them resolves today only because
all boots happen at DOMContentLoaded. So the guards now treat 'interactive' as
not-ready (`!== 'complete'`), restoring that exactly.
Codex's specific finding (first-run onboarding silently skipped) did NOT
reproduce — shell.js's boot() awaits /api/profile, and that yield lets the
remaining deferred scripts run first. But the race it described is real, the
guard is silent when it fails (`&& window.v3Onboarding`), and the other 42
forward refs have no such await protecting them. Fixed at the root rather than
at the one site.
VERIFIED. A/B against origin/main on a fresh profile, 13 probes (onboarding
overlay, v3Onboarding/v3Songs/v3Profile/fbNotify/v3Badges/uiPrompt/showScreen,
bus, capabilities.version, createHighway, plugin scripts, mounted screens):
IDENTICAL, zero console/page errors on both. pytest 2396, node 1028/1028,
ESLint 0 errors, Codex 0.
New guard: test_every_external_script_defers_so_document_order_is_execution_order
fails if any external tag is plain classic — verified to fail on a single
reverted tag, so it actually bites.
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
82 lines
3.2 KiB
JavaScript
82 lines
3.2 KiB
JavaScript
// v3 first-run "Interface size" nudge.
|
|
//
|
|
// The Accessibility → Interface size control is the primary discovery path, but
|
|
// the exact person who needs it — someone on a large, low-DPI display (e.g. a
|
|
// 32" 1440p panel with no OS scaling) — is the one least likely to go hunting
|
|
// for it. So, ONCE, for that specific display profile, surface a gentle,
|
|
// dismissible toast that deep-links to the control. Never fires if the user has
|
|
// already touched the setting, on smaller/high-DPI displays, or more than once.
|
|
//
|
|
// Plain non-module script; degrades to a no-op without the bus, fbNotify, or DOM.
|
|
(function () {
|
|
'use strict';
|
|
|
|
var SEEN_KEY = 'v3-interface-size-nudged';
|
|
var SCALE_KEY = 'v3-interface-scale';
|
|
|
|
function alreadyHandled() {
|
|
try {
|
|
return localStorage.getItem(SEEN_KEY) === '1' || localStorage.getItem(SCALE_KEY) != null;
|
|
} catch (_) { return true; }
|
|
}
|
|
|
|
// The target profile: a physically large viewport rendered near 1:1 (so the
|
|
// OS isn't already enlarging things). This is the eye-strain case.
|
|
function isLargeLowDpiDisplay() {
|
|
var w = window.innerWidth || 0;
|
|
var dpr = window.devicePixelRatio || 1;
|
|
return w >= 1800 && dpr <= 1.25;
|
|
}
|
|
|
|
// Don't interrupt a first-run modal (e.g. profile onboarding). If one is up,
|
|
// leave the flag UNSET so the nudge gets another chance on a later launch.
|
|
function aModalIsOpen() {
|
|
var dialogs = document.querySelectorAll('[role="dialog"], .fixed.inset-0');
|
|
for (var i = 0; i < dialogs.length; i++) {
|
|
var el = dialogs[i];
|
|
if (el.offsetParent !== null && el.getClientRects().length) return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
function openSetting() {
|
|
try {
|
|
if (typeof window.showScreen === 'function') window.showScreen('settings');
|
|
document.querySelectorAll('#settings-tabbar .fb-tab').forEach(function (b) {
|
|
if (b.dataset.tab === 'accessibility') b.click();
|
|
});
|
|
} catch (_) { /* noop */ }
|
|
}
|
|
|
|
function maybeNudge() {
|
|
if (alreadyHandled()) return;
|
|
if (!isLargeLowDpiDisplay()) return;
|
|
if (!window.fbNotify || typeof window.fbNotify.show !== 'function') return;
|
|
if (aModalIsOpen()) return; // try again next launch
|
|
|
|
try { localStorage.setItem(SEEN_KEY, '1'); } catch (_) { /* private mode */ }
|
|
|
|
var card = window.fbNotify.show({
|
|
title: 'Text looking small?',
|
|
message: 'Make the menus and text larger — tap to open Interface size.',
|
|
icon: '🔍',
|
|
accent: '#0ea5e9',
|
|
durationMs: 9000,
|
|
});
|
|
if (card && card.addEventListener) card.addEventListener('click', openSetting);
|
|
}
|
|
|
|
function start() {
|
|
// Let the app settle (boot, any onboarding) before offering the nudge.
|
|
setTimeout(maybeNudge, 4000);
|
|
}
|
|
|
|
// `defer` runs this at readyState 'interactive' — later scripts have not
|
|
// evaluated yet, so wait for DOMContentLoaded (see static/v3/index.html).
|
|
if (document.readyState !== 'complete') {
|
|
document.addEventListener('DOMContentLoaded', start, { once: true });
|
|
} else {
|
|
start();
|
|
}
|
|
})();
|