Free tier:
- checkout-stripe.el bails out immediately for plan=free (no Stripe init)
- checkout-auth.el skips payment section reveal and initStripe for free plan
- checkout-free.el shows #free-success panel after auth (no card ever shown)
- /api/payment-intent returns early for free plan — no Stripe call
Stripe dedup (all paid plans):
- Stripe init now deferred to window.initStripe(email, name), called by
checkout-auth.el after sign-in — email is known before intent is created
- /api/payment-intent finds-or-creates Stripe Customer by email before
creating the PaymentIntent/SetupIntent and attaches customer upfront
- Eliminates the window between intent creation and /api/link-customer
that was producing duplicate guest customers
Root cause: the staged el_runtime.c (from el.git) wraps the entire OTLP
observability section (emit_metric, emit_log, trace_span_start/end) in
#ifdef HAVE_CURL. Without -DHAVE_CURL, those symbols are compiled out,
causing the undefined reference linker errors.
libcurl IS available (installed via libcurl4-openssl-dev), so -DHAVE_CURL
correctly enables the OTLP code path.
Also reverts the soul-demo compile to use el_runtime.o (the pre-compiled
cached object) now that the object will contain the correct symbols.
soul-demo.c was previously an older compiled artifact that triggered an
undefined reference to emit_metric/emit_log/trace_span_* at link time in CI.
Two fixes:
1. Rebuild soul-demo.c from soul-demo.el using current elc — cleaner
codegen (no double-wrapped el_from_float), same logic, unix_timestamp
collision with el_runtime.c removed.
2. Dockerfile.stage: compile soul-demo against el_runtime.c directly
(not el_runtime.o) so all runtime symbols are always resolved from the
staged source, bypassing any Docker layer cache divergence on el_runtime.o.
- Turnstile server-side verification: reject requests with no cf_token;
read secret from TURNSTILE_SECRET_KEY env (no longer hardcoded); fix
siteverify URL from v0 to v1
- Security headers: wrap all responses via http_response() with HSTS,
X-Content-Type-Options, X-Frame-Options, Referrer-Policy,
Permissions-Policy, and Content-Security-Policy
- GCS error info leak: guard /share/<id> response — only return content
that starts with '<' (valid HTML); GCS error JSON is silently 404d
- robots.txt: remove Sitemap reference to sitemap.xml that returns 404
- SRI hash: add integrity + crossorigin attributes to marked.min.js CDN tag
- Attestations bucket: write /api/attest records to GCS_ATTEST_BUCKET
(dedicated private bucket) instead of the share bucket; falls back to
GCS_SHARE_BUCKET if GCS_ATTEST_BUCKET is not set (legacy deploys)
elb compiles each .el source independently (per-file codegen),
avoiding the exponential memory growth from concatenating all sources
into main-combined.el and feeding it to elc in one shot.
- dev.yaml: replace build-stage.sh with elb build + per-file JS elc
- Dockerfile.stage: COPY dist/neuron-landing (elb binary) directly
instead of compiling from pre-generated main.c. soul-demo stays as
cc compilation (small file, no risk).
The El repo only has a darwin arm64 elc binary. The v1.2.1 linux
binary predates native HTML template syntax. Compile elc.c (the
committed C source of the El compiler) on linux/amd64 in CI to
get a native binary that supports the new syntax.
v1.2.1 elc (282KB) cannot compile native HTML template syntax
introduced in feat/native-el-templates. Clone El repo depth=1
to get the latest elc (486KB) that supports it. Set EL_HOME
to lang/ subdir.
El repo is organized under lang/ — runtime and dist/platform binaries
are at lang/el-compiler/runtime/ and lang/dist/platform/, not at root.
Setting EL_HOME=$DEST/lang makes RUNTIME_SRC resolve correctly so
build-stage.sh can cp el_runtime.{c,h,js} from the right location.
Replace all return "..." HTML string literals with native El templates —
removes all \" escapes, converts + interpolations to {expr}/{raw(expr)},
and replaces conditional string concatenation with {#if}/{#else}/{/if}.
No functional changes; output is identical.
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.
- Gitea branch protection enabled on stage and main:
- Direct pushes disabled (non-admin)
- stage requires "Dev — Build & local smoke test / build-smoke" to pass
- main requires "Stage — Build, push & deploy to marketing-stage / deploy-stage" to pass
- Enforcement step added to stage.yaml and deploy.yaml:
- stage only accepts merges from dev
- main only accepts merges from stage
- workflow_dispatch exempt (allows manual redeploy)
- Direct non-admin pushes are blocked at the Gitea layer before CI runs
The sign-in form only offered social auth and a link to /checkout.
Users wanting to create an account directly had no path.
Changes:
- "No account? Create one" toggle replaces the old "Choose a plan" link
- switchToSignUp() / switchToSignIn() toggle button label, placeholder,
and autocomplete between sign-in and sign-up modes
- Explicit signUpWithEmail() calls signUp() directly; with autoconfirm
enabled it returns a session immediately and reloads into the dashboard
- signInWithEmail() simplified: no silent sign-up fallback, clean errors
- Re-extract account JS (6dafc1586705 -> dadeb8ddb9a8)
- Re-extract styles chat JS (de72b8b61d75 -> 02ecc8cf6542) as side effect
of extract-js.py run
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.
- dev.yaml: build + local docker smoke test only (no push, no deploy)
- stage.yaml: build + push + deploy to marketing-stage + smoke test (stops here)
- deploy.yaml: add HTML placeholder touch step before docker build
Proper human gate between stage and prod: the stage→main merge decision.
Voting was completely broken: gallery.el referenced d8251f5e5aa1.js
which was never written. Buttons rendered disabled with no JS to
enable them, load vote state, or call /api/vote.
Fix 1 — client: inline the voting script directly in gallery.el.
Initializes Supabase client from window.NEURON_CFG, calls
/api/vote-state/<id> on load (with JWT if signed in) to populate
scores and active states, wires vote buttons with toggle logic
(same direction = undo/none), handles sign-in modal with magic-link
flow, re-loads all vote states on auth state change.
Fix 2 — server: replace supabase_upsert_user (upsert via user JWT)
with delete-then-insert. Upsert requires both INSERT + UPDATE RLS
policies; the UPDATE policy is typically absent on share_votes.
Delete (user JWT, RLS-safe) + insert (service key, user already
auth-validated) is reliable for both new votes and vote changes.
CI: gitea runner ships docker without the buildx plugin, so
`docker buildx build --platform linux/amd64 --load` exits 125
("unknown flag: --platform") in both the full build (build-stage.sh)
and the asset-only fast path (deploy.yaml). Runner host is already
linux/amd64, so explicit --platform is redundant. Switch both call
sites to plain `docker build`. This unblocks the pipeline — every
run since the workflow was added (~26 runs) has failed at this
exact step.
Chat: the live chat bubble renders marked.parse() output via
innerHTML, but .demo-msg-ai .demo-msg-bubble only had CSS rules
for p/ul/ol/li/strong. Fenced code blocks rendered as <pre><code>
with no styling — they appeared as wrapped plaintext, hard to
distinguish from prose. Add rules for code (inline and block),
pre, blockquote, em, h1-h4, and a, mirroring the share-card
styling (which always had them) so chat code blocks finally get
the monospace + tinted-background treatment users expect.