go-yaml (Gitea's parser) mishandles << inside block scalars, treating the
bash heredoc delimiter as a YAML merge key. Move the migration logic to a
standalone script called via python3 scripts/run_migrations.py.
Recovers original JS from git history and ports it into proper El source
files under src/js/. Each file wraps the original JS in a native_js call
inside a main() function, making it valid El that compiles to a
self-contained IIFE via elc --target=js --bundle.
Files added:
src/js/account-auth.el - Supabase OTP magic-link (sendMagicLink)
src/js/account-dashboard.el - Account dashboard: session, plan card, family
src/js/chat-widget.el - Demo chat widget (neuronDemoToggle/Send/Reset)
src/js/checkout-auth.el - Checkout auth: OAuth, email sign-in/up
src/js/checkout-free.el - Free plan: auth-badge watch -> payment reveal
src/js/checkout-stripe.el - Stripe Payment Element (reads NEURON_CFG)
src/js/enterprise.el - Enterprise inquiry form + headcount filter
src/js/environmental.el - Efficiency calculator slider
src/js/gallery.el - Gallery nav, search/sort, Supabase voting
src/js/main.el - Share page voting + copyForPlatform
src/js/marketplace.el - Developer interest form
src/js/nav.el - Nav hamburger + Mission dropdown
src/js/styles.el - Landing: nav scroll, reveal, founding counter
Gallery: remove <a> from share allowlist. Gallery cards wrap content in
<a class="gal-link">; allowing <a> in sanitized answer HTML causes nested
anchors that the HTML5 adoption agency algorithm resolves by restructuring
the DOM, producing mismatched </div> tags that leave gallery-grid open and
pull sibling elements into the grid as spurious grid columns.
Account: replace email+password sign-up/sign-in with magic-link OTP.
supabase.auth.signInWithOtp handles both new and existing users in one
flow. Existing onAuthStateChange listener (dadeb8ddb9a8.js) retained for
post-redirect dashboard display. sendMagicLink added to extract-js
RESERVED_GLOBALS so the obfuscator does not mangle the onclick reference.
The demo chat was silently dropping conversation context past 40 turns
and leaving the thinking bubble spinning forever when the soul backend
hung — visitors saw a frozen UI with no way to know what went wrong.
Changes:
- Stored history cap raised from 40 → 200 messages so longer
conversations actually persist across page refreshes.
- History sent to backend per turn raised from 20 → 50 messages.
- 30s AbortController timeout on the /api/demo fetch — surfaces a
distinct "Took too long to respond" error instead of hanging.
- Restore script (restore-chat-js-with-preview.py) is now correctly
idempotent in both directions: detects when modal HTML is inlined
but chat JS got extracted to an asset, and re-injects fresh source
so extract-js picks up changes on the next build.
Turnstile callback unconditionally showed the greeting message, wiping
session history for returning visitors who hadn't verified yet.
Callback now uses the same restoreOrGreet pattern as neuronDemoToggle:
replays prior messages if session.messages is non-empty, else shows the
greeting once and marks session.greeted.
Also extracts the gallery voting inline script (a49ca0a129e8.js) as a
side effect of re-running extract-js.py. Chat JS rebuilds to de72b8b61d75.js.
Share card now displays the AI bubble's marked-rendered HTML (after
basic tag allowlist sanitization) instead of escaped plaintext.
Markdown bold, lists, code, headers all show.
Share click in chat now opens a preview modal. Publishing to the
gallery only happens when the user explicitly clicks Publish in the
modal - removes the click-and-immediately-public surprise.
Build pipeline
- build-stage.sh replaces the old in-Dockerfile bootstrap.py path. Host
pre-compiles src/*.el into dist/main.c via the canonical native elc at
foundation/el/dist/platform/elc and applies the stub-decl sed before
docker buildx runs.
- Dockerfile.stage drops bootstrap.py + python3 from the builder stage
and just runs cc on the host-supplied dist/main.c.
- Pre-rendered HTML shells under /srv/landing/ are now chowned to the
landing user so the El page-builder's fs_write at startup can rewrite
them — without that, post-COPY edits never reach the served HTML and
the served page stays as the stale build-time fallback.
Chat restore
- session.verified + session.verifiedAt persist through localStorage so
a return visit within 24h skips the Turnstile gate and lands directly
in the restored conversation.
- restoreOrGreet() is the single source of truth for what shows up in
the message pane after the gate clears: replays prior messages with
skipSave, else drops the canned hello once and remembers it.
- applyVerifiedDom() hides the gate / reveals the chat row, called both
from the verified-on-load path (DOMContentLoaded if loading, else
immediate) and from the Turnstile callback.
- neuronDemoReset clears verified + verifiedAt so the gate returns next
open.
Extracted JS assets (src/assets/js/*.js + manifest.json) and the
extract-js.py helper land here too — they match what the new build-stage
flow produces and removes the inline <script> blobs from the served HTML.