import { isDownRangeHeroPage } from './home-hero-routing'; declare const __APP_GIT_SHA__: string; declare const __APP_BUILD_TIME__: string; declare const __APP_BUILD_IDENTITY__: string; declare const __APP_BUILD_SOURCE_HASH__: string; const canvas = document.querySelector('#hero-commander-canvas'); if (!canvas) { // eslint-disable-next-line no-console console.warn('[HomeHero] Hero canvas missing.'); } else { type IdleWindow = Window & { requestIdleCallback?: ( callback: () => void, options?: { timeout?: number } ) => number; cancelIdleCallback?: (handle: number) => void; __homeHeroCoreBootPromise?: Promise; __DOWN_RANGE_BUILD__?: { gitSha: string; buildIdentity: string; sourceHash: string; buildLabel: string; runtime: string; }; }; const BOOT_PROMISE_KEY = '__homeHeroCoreBootPromise' as const; const LOADER_SOURCE = 'home-hero-loader'; const loaderHudEnabled = !/\bheroBootstrapHud=0\b/i.test(window.location.search); const win = window as IdleWindow; const liveUrlBuildIdentity = new URLSearchParams(window.location.search).get('downRangeBuild')?.trim(); const buildIdentity = liveUrlBuildIdentity && liveUrlBuildIdentity.length > 0 ? liveUrlBuildIdentity : __APP_BUILD_IDENTITY__; const [identityGitSha = __APP_GIT_SHA__, ...identityHashSegments] = buildIdentity.split('-'); const liveBuildGitSha = identityGitSha || __APP_GIT_SHA__; const liveBuildSourceHash = identityHashSegments.length > 0 ? identityHashSegments.join('-') : __APP_BUILD_SOURCE_HASH__; const shortSourceHash = liveBuildSourceHash.slice(0, 12); const buildLabel = `${liveBuildGitSha} @ ${__APP_BUILD_TIME__.replace('T', ' ').replace('Z', ' UTC')} [${liveBuildSourceHash.slice(0, 8)}]`; const buildInfoUrl = new URL('./down-range-build.json', window.location.href); buildInfoUrl.searchParams.set('v', buildIdentity); const runtimeQuery = new URLSearchParams(window.location.search).get('heroRuntime'); const pathname = window.location.pathname.toLowerCase(); const prefersStreamlinedByPage = isDownRangeHeroPage(pathname); const useLegacyRuntime = runtimeQuery === 'legacy' || (!prefersStreamlinedByPage && runtimeQuery !== 'streamlined') || /\bheroLegacy=1\b/i.test(window.location.search); const runtimeModulePath = useLegacyRuntime ? './home-hero-core' : './home-hero-streamlined-core'; const runtimeLabel = useLegacyRuntime ? 'legacy' : 'streamlined'; const loadRuntimeModule = useLegacyRuntime ? () => import('./home-hero-core') : () => import('./home-hero-streamlined-core'); document.title = `DOWN RANGE ${liveBuildGitSha} ${shortSourceHash}`; document.documentElement.dataset.downRangeBuild = buildLabel; document.documentElement.dataset.downRangeBuildIdentity = buildIdentity; document.documentElement.dataset.downRangeBuildSourceHash = liveBuildSourceHash; win.__DOWN_RANGE_BUILD__ = { gitSha: liveBuildGitSha, buildIdentity, sourceHash: liveBuildSourceHash, buildLabel, runtime: runtimeLabel }; { const taggedUrl = new URL(window.location.href); if ((taggedUrl.searchParams.get('downRangeBuild') ?? '').trim() !== buildIdentity) { taggedUrl.searchParams.set('downRangeBuild', buildIdentity); window.history.replaceState(window.history.state, '', taggedUrl.toString()); } } console.info('[DownRangeBuild] active build', { identity: buildIdentity, label: buildLabel, runtime: runtimeLabel, modulePath: runtimeModulePath }); let buildBadgeStatusNode: HTMLDivElement | null = null; const staleReloadStorageKey = 'downRangeBuildReloadedFor'; const currentBuildUrl = (identity: string) => { const nextUrl = new URL(window.location.href); nextUrl.searchParams.set('downRangeBuild', identity); nextUrl.searchParams.set('downRangeBuildReloadedFor', identity); return nextUrl.toString(); }; const ensureFreshBuildBundle = async (): Promise => { try { const response = await fetch(buildInfoUrl.toString(), { cache: 'no-store', credentials: 'same-origin' }); if (!response.ok) { return true; } const data = await response.json() as { gitSha?: string; buildTime?: string; identity?: string; }; const serverIdentity = (typeof data.identity === 'string' && data.identity.trim().length > 0) ? data.identity.trim() : `${data.gitSha ?? 'unknown'}-${data.buildTime ?? 'unknown'}`.replace(/[^a-z0-9-]/gi, ''); if (!serverIdentity || serverIdentity === buildIdentity) { if (buildBadgeStatusNode) { buildBadgeStatusNode.textContent = `FRESH OK ${buildIdentity.slice(0, 16)}`; buildBadgeStatusNode.style.color = '#8ff5c3'; } window.localStorage.removeItem(staleReloadStorageKey); return true; } if (buildBadgeStatusNode) { buildBadgeStatusNode.textContent = `FRESH STALE -> ${serverIdentity.slice(0, 16)}`; buildBadgeStatusNode.style.color = '#ff8b8b'; } document.documentElement.dataset.downRangeBuildServer = serverIdentity; const staleLabel = `Stale bundle detected. Server is ${serverIdentity}.`; const staleReport = { message: staleLabel, clientIdentity: buildIdentity, clientLabel: buildLabel, serverIdentity, buildInfoUrl: buildInfoUrl.toString(), currentUrl: window.location.href, reloadKey: staleReloadStorageKey }; const staleReportJson = JSON.stringify(staleReport, null, 2); console.error('[DownRangeBuild] stale bundle detected', staleReportJson); console.log('[DownRangeBuild] stale bundle detected'); console.log(staleReportJson); console.error('[DownRangeBuild] stale bundle detected', { ...staleReport }); stopFreshnessWatchdog(); completeBootstrapHud(false, staleLabel); postLoaderEvent('error', 'Build Mismatch', staleLabel, 0.01, false); const reloadMarker = window.localStorage.getItem(staleReloadStorageKey); if (reloadMarker !== serverIdentity) { window.localStorage.setItem(staleReloadStorageKey, serverIdentity); window.setTimeout(() => { window.location.replace(currentBuildUrl(serverIdentity)); }, 450); } return false; } catch { if (buildBadgeStatusNode) { buildBadgeStatusNode.textContent = 'FRESH CHECK FAILED'; buildBadgeStatusNode.style.color = '#ffc98b'; } return true; } }; const postLoaderEvent = ( type: 'status' | 'heartbeat' | 'ready' | 'error', title: string, detail: string, progress: number, completed = false ) => { const safeProgress = Math.max(0, Math.min(1, progress)); try { if (window.parent !== window) { window.parent.postMessage( { source: LOADER_SOURCE, type, title, detail, progress: safeProgress, completed, timeMs: performance.now() }, '*' ); } } catch { // Cross-origin bridge errors are non-fatal. } }; type BootstrapHud = { node: HTMLDivElement; statusNode: HTMLDivElement; detailNode: HTMLDivElement; progressNode: HTMLDivElement; }; const createBootstrapHud = (): BootstrapHud | null => { if (!loaderHudEnabled) return null; if (document.getElementById('home-hero-bootstrap-hud')) return null; const hud = document.createElement('div'); hud.id = 'home-hero-bootstrap-hud'; hud.style.position = 'fixed'; hud.style.left = '16px'; hud.style.bottom = '16px'; hud.style.minWidth = '260px'; hud.style.maxWidth = 'min(420px, calc(100vw - 32px))'; hud.style.padding = '10px 12px'; hud.style.border = '1px solid rgba(127, 180, 255, 0.34)'; hud.style.borderRadius = '10px'; hud.style.background = 'rgba(6, 12, 20, 0.78)'; hud.style.backdropFilter = 'blur(6px)'; hud.style.zIndex = '28'; hud.style.pointerEvents = 'none'; hud.style.userSelect = 'none'; hud.style.boxShadow = '0 14px 36px rgba(0, 0, 0, 0.45)'; const buildNode = document.createElement('div'); buildNode.style.marginTop = '6px'; buildNode.style.font = '600 10px/1.2 "Consolas", "Courier New", monospace'; buildNode.style.letterSpacing = '0.08em'; buildNode.style.color = '#7faed6'; buildNode.textContent = `BUILD ${buildLabel}`; const statusNode = document.createElement('div'); statusNode.style.font = '700 12px/1.2 "Consolas", "Courier New", monospace'; statusNode.style.letterSpacing = '0.04em'; statusNode.style.textTransform = 'uppercase'; statusNode.style.color = '#d8ebff'; statusNode.textContent = 'Boot Loader'; const detailNode = document.createElement('div'); detailNode.style.marginTop = '6px'; detailNode.style.font = '600 11px/1.35 "Consolas", "Courier New", monospace'; detailNode.style.letterSpacing = '0.02em'; detailNode.style.color = '#9ec4e7'; detailNode.textContent = 'Preparing startup shell...'; const track = document.createElement('div'); track.style.marginTop = '8px'; track.style.height = '6px'; track.style.borderRadius = '999px'; track.style.overflow = 'hidden'; track.style.background = 'rgba(96, 141, 187, 0.26)'; const progressNode = document.createElement('div'); progressNode.style.height = '100%'; progressNode.style.width = '2%'; progressNode.style.borderRadius = 'inherit'; progressNode.style.background = 'linear-gradient(90deg, #63c8ff 0%, #90ffd7 100%)'; progressNode.style.boxShadow = '0 0 12px rgba(102, 206, 255, 0.45)'; progressNode.style.transition = 'width 150ms linear'; track.appendChild(progressNode); hud.append(statusNode, detailNode, buildNode, track); document.body.appendChild(hud); return { node: hud, statusNode, detailNode, progressNode }; }; const createBuildBadge = () => { const badge = document.createElement('div'); badge.id = 'home-hero-build-badge'; badge.style.position = 'fixed'; badge.style.top = '12px'; badge.style.right = '12px'; badge.style.padding = '8px 10px 9px'; badge.style.border = '1px solid rgba(127, 180, 255, 0.32)'; badge.style.borderRadius = '10px'; badge.style.background = 'rgba(6, 12, 20, 0.72)'; badge.style.backdropFilter = 'blur(6px)'; badge.style.zIndex = '27'; badge.style.pointerEvents = 'none'; badge.style.userSelect = 'text'; badge.style.display = 'grid'; badge.style.gap = '2px'; badge.style.minWidth = '224px'; badge.style.boxShadow = '0 14px 36px rgba(0, 0, 0, 0.35)'; const title = document.createElement('div'); title.textContent = 'DOWN RANGE BUILD'; title.style.font = '700 10px/1.1 "Consolas", "Courier New", monospace'; title.style.letterSpacing = '0.12em'; title.style.color = '#9fc8e9'; const shaLine = document.createElement('div'); shaLine.textContent = `SHA ${liveBuildGitSha}`; shaLine.style.font = '700 11px/1.2 "Consolas", "Courier New", monospace'; shaLine.style.letterSpacing = '0.08em'; shaLine.style.color = '#d8ebff'; const srcLine = document.createElement('div'); srcLine.textContent = `SRC ${shortSourceHash}`; srcLine.style.font = '700 11px/1.2 "Consolas", "Courier New", monospace'; srcLine.style.letterSpacing = '0.08em'; srcLine.style.color = '#d8ebff'; const runtimeLine = document.createElement('div'); runtimeLine.textContent = `RTM ${runtimeLabel}`; runtimeLine.style.font = '700 10px/1.2 "Consolas", "Courier New", monospace'; runtimeLine.style.letterSpacing = '0.08em'; runtimeLine.style.color = '#b9d7f2'; const statusLine = document.createElement('div'); statusLine.textContent = `FRESH CHECK ${buildIdentity.slice(0, 16)}`; statusLine.style.font = '700 10px/1.2 "Consolas", "Courier New", monospace'; statusLine.style.letterSpacing = '0.08em'; statusLine.style.color = '#9fc8e9'; buildBadgeStatusNode = statusLine; badge.append(title, shaLine, srcLine, runtimeLine, statusLine); return badge; }; const bootstrapHud = createBootstrapHud(); const buildBadge = createBuildBadge(); document.body.appendChild(buildBadge); const updateBootstrapHud = (title: string, detail: string, progress: number) => { if (!bootstrapHud) return; bootstrapHud.statusNode.textContent = title; bootstrapHud.detailNode.textContent = detail; bootstrapHud.progressNode.style.width = `${Math.round(Math.max(0, Math.min(1, progress)) * 100)}%`; }; const completeBootstrapHud = (ok: boolean, detail: string) => { if (!bootstrapHud) return; bootstrapHud.statusNode.textContent = ok ? 'Range Live' : 'Startup Error'; bootstrapHud.detailNode.textContent = detail; bootstrapHud.progressNode.style.width = ok ? '100%' : '100%'; if (ok) { bootstrapHud.node.style.transition = 'opacity 260ms ease'; bootstrapHud.node.style.opacity = '0'; window.setTimeout(() => { if (bootstrapHud.node.parentNode) { bootstrapHud.node.parentNode.removeChild(bootstrapHud.node); } }, 320); } else { bootstrapHud.node.style.borderColor = 'rgba(255, 122, 122, 0.5)'; bootstrapHud.node.style.background = 'rgba(30, 8, 8, 0.82)'; } }; const waitForNextFrame = () => new Promise((resolve) => { window.requestAnimationFrame(() => resolve()); }); const waitForFirstPaint = async () => { await waitForNextFrame(); await waitForNextFrame(); }; const waitForIdleSlot = (timeoutMs: number) => new Promise((resolve) => { if (typeof win.requestIdleCallback === 'function') { win.requestIdleCallback(() => resolve(), { timeout: timeoutMs }); return; } window.setTimeout(resolve, timeoutMs); }); let autoBootTimer = 0; let idleBootHandle = 0; let heartbeatTimer = 0; let freshnessPollTimer = 0; const clearBootSchedulers = () => { if (autoBootTimer) { window.clearTimeout(autoBootTimer); autoBootTimer = 0; } if (idleBootHandle && typeof win.cancelIdleCallback === 'function') { win.cancelIdleCallback(idleBootHandle); idleBootHandle = 0; } }; const stopFreshnessWatchdog = () => { if (freshnessPollTimer) { window.clearInterval(freshnessPollTimer); freshnessPollTimer = 0; } }; const startFreshnessWatchdog = () => { if (freshnessPollTimer) { return; } freshnessPollTimer = window.setInterval(async () => { if (document.visibilityState !== 'visible') { return; } if (!(await ensureFreshBuildBundle())) { stopFreshnessWatchdog(); } }, 30000); }; const bootCoreRuntime = async (reason: string) => { updateBootstrapHud('Boot Loader', `Preparing startup shell (${reason})...`, 0.03); postLoaderEvent('status', 'Boot Loader', `Preparing startup shell (${reason}).`, 0.03, false); await waitForFirstPaint(); if (!(await ensureFreshBuildBundle())) { return; } updateBootstrapHud('Boot Loader', 'Page painted. Waiting for an idle slot...', 0.08); postLoaderEvent('heartbeat', 'Boot Loader', 'Page painted. Waiting for an idle slot.', 0.08, false); await waitForIdleSlot(320); let heartbeatProgress = 0.12; const importStartMs = performance.now(); updateBootstrapHud('Runtime Module', `Importing ${runtimeLabel} runtime module...`, heartbeatProgress); postLoaderEvent('status', 'Runtime Module', `Importing ${runtimeLabel} runtime module.`, heartbeatProgress, false); heartbeatTimer = window.setInterval(() => { const elapsedSec = (performance.now() - importStartMs) / 1000; heartbeatProgress = Math.min(0.88, heartbeatProgress + 0.02); const detail = `Compiling ${runtimeLabel} runtime (${elapsedSec.toFixed(1)}s)...`; updateBootstrapHud('Runtime Module', detail, heartbeatProgress); postLoaderEvent('heartbeat', 'Runtime Module', detail, heartbeatProgress, false); }, 420); await loadRuntimeModule(); if (heartbeatTimer) { window.clearInterval(heartbeatTimer); heartbeatTimer = 0; } startFreshnessWatchdog(); const readyDetail = `Runtime initialized (${runtimeLabel}).`; updateBootstrapHud('Range Live', readyDetail, 1); postLoaderEvent('ready', 'Range Live', readyDetail, 1, true); completeBootstrapHud(true, readyDetail); }; const ensureBootStarted = (reason: string) => { if (win[BOOT_PROMISE_KEY]) return; clearBootSchedulers(); win[BOOT_PROMISE_KEY] = bootCoreRuntime(reason).catch((error) => { if (heartbeatTimer) { window.clearInterval(heartbeatTimer); heartbeatTimer = 0; } stopFreshnessWatchdog(); const detail = error instanceof Error ? error.message : String(error ?? 'Unknown error'); postLoaderEvent('error', 'Startup Error', detail, 1, false); completeBootstrapHud(false, detail); // eslint-disable-next-line no-console console.error('[HomeHero] Boot runtime import failed.', error); }); }; const startOnInteraction = () => { ensureBootStarted('interaction'); }; document.addEventListener('pointerdown', startOnInteraction, { passive: true, once: true }); document.addEventListener('keydown', startOnInteraction, { passive: true, once: true }); document.addEventListener('touchstart', startOnInteraction, { passive: true, once: true }); if (typeof win.requestIdleCallback === 'function') { idleBootHandle = win.requestIdleCallback(() => ensureBootStarted('idle'), { timeout: 700 }); } autoBootTimer = window.setTimeout(() => ensureBootStarted('timeout'), 760); updateBootstrapHud('Boot Loader', 'Startup shell ready. Scheduling runtime import...', 0.02); postLoaderEvent('status', 'Boot Loader', 'Startup shell ready. Scheduling runtime import.', 0.02, false); }