diff --git a/src/routes/+page.svelte b/src/routes/+page.svelte index 5db4ce2a..33a1f4c8 100644 --- a/src/routes/+page.svelte +++ b/src/routes/+page.svelte @@ -560,6 +560,15 @@ } let pyodideReady = $state(false); let pyodideLoading = $state(false); + // True once startup `bootstrapToolboxes()` has finished (or failed). The + // engine wheel being up (`pyodideReady`) is not enough: the bootstrap still + // installs the preloaded catalog toolboxes afterwards (and, in engine builds + // that resolve dependencies, the engine base + docutils) via micropip. The + // run button folds this in so it stays in its loading state until that work + // is done, instead of unlocking the moment the wheel is ready. + let bootstrapComplete = $state(false); + let runLoading = $derived(pyodideLoading || !bootstrapComplete); + let runReady = $derived(pyodideReady && bootstrapComplete); let simRunning = $state(false); let isRunStarting = false; // Synchronous flag to prevent race conditions let isContinuing = false; // Synchronous flag to prevent rapid continue calls @@ -592,10 +601,16 @@ await autoDetectBackend(); await initBackendFromUrl(); await initPyodide(); + statusText = 'Loading toolboxes...'; await bootstrapToolboxes(); + statusText = 'Ready'; } catch (e) { console.error('[startup] backend init failed', e); throw e; + } finally { + // Unlock the run button even if bootstrap failed — a broken + // toolbox shouldn't leave the button stuck in its loading state. + bootstrapComplete = true; } })(); void loadFromUrlParam(backendReady).catch((e) => { @@ -995,7 +1010,7 @@ // Run simulation (auto-initializes if needed) async function handleRun() { // Prevent concurrent simulation runs (synchronous check for rapid key presses) - if (simRunning || isRunStarting || pyodideLoading) return; + if (simRunning || isRunStarting || runLoading) return; // Set flag before any async operations to prevent race conditions isRunStarting = true; @@ -1331,18 +1346,18 @@ {:else} -
+