Commit Graph

73 Commits

Author SHA1 Message Date
will.anderson 9892d89c01 Fix implicit declaration of page_close on Linux: wrap extern as native El fn
Dev — Build & local smoke test / build-smoke (pull_request) Successful in 3m25s
2026-05-09 17:49:15 -05:00
will.anderson 90609c7aaf Convert page_open to native El; fix corrupted CSS
Dev — Build & local smoke test / build-smoke (pull_request) Failing after 1m57s
elc's heredoc tokenizer was corrupting the inline CSS:
- #FAFAF8 -> FAFAF8 (# treated as comment character)
- 'Playfair Display' -> PlayfairDisplay (quotes + space stripped)
- padding: 0 2.5rem -> padding:02.5rem (spaces between tokens stripped)

The CSS and other complex head content (GA script, JSON-LD schema)
have been pre-compiled to C functions (page_css, page_ga_script,
page_schema) so they bypass the tokenizer entirely and are stored as
properly-escaped C string literals.

page_head() now assembles the <head> content using el-html vessel
calls (el_meta_charset, el_meta, el_title, el_link_stylesheet, etc.)
plus string literals for the vessel gaps. page_open() returns the
complete document prologue as a string concatenation with no heredocs.

page_close() remains pre-compiled in dist/page_close.c (unchanged).
2026-05-09 13:07:06 -05:00
will.anderson 7f27f4be9f Fix broken payment page: escape html/body heredocs in page_open
Dev — Build & local smoke test / build-smoke (pull_request) Failing after 1m27s
elc's heredoc parser treats <html> as an opener and scans forward for
</html>, which exists inside page_close's return statement. This caused
the entire El source of page_close to be injected verbatim into the
page_open output string, terminating the document before Stripe scripts
could load.

Fix: put <!DOCTYPE html><html lang="en"> in a quoted string literal
and use <head>...</head> as the sole heredoc in page_open — closes
within the same function, no cross-boundary scanning. Stub page_close
in styles.el as extern fn so dist/page_close.c supplies the definition.

Also fix elc-broken hyphenated attributes in dist/page_close.c:
aria-label, stroke-width, stroke-linecap, &times;, and several
text nodes that had whitespace stripped by the heredoc parser.
2026-05-09 12:56:50 -05:00
will.anderson 2553a6b7ac feat(native-el-ui): convert all component files to el-html vessel API
Dev — Build & local smoke test / build-smoke (pull_request) Failing after 4m48s
Replaced raw HTML heredoc returns with native el_ function calls across
all 21 component files. styles.el intentionally excluded.
2026-05-08 22:35:41 -05:00
will.anderson f2ab12e65b feat(native-el-ui): rewrite checkout/main to use el-html vessel, fix page_close linker error
- checkout.el and main.el: replace raw import of el-html vessel with direct
  extern fn declarations; implementations come from dist/elhtml_impl.c (c_source)
- Add src/elhtml.el as reference file (all el-html extern fn declarations; not imported)
- dist/elhtml_impl.c: pre-compiled el-html vessel C (strips int main + sample global)
- dist/page_close.c: pre-compiled page_close implementation; elc OOMs after emitting
  the ~71KB page_open body and silently drops page_close, so supply it as c_source
- manifest.el: add elhtml_impl.c and page_close.c as c_source entries
- .gitignore: un-ignore dist/elhtml_impl.c and dist/page_close.c
2026-05-08 22:14:38 -05:00
will.anderson 2a3f998827 fix(build): c_source stubs, manifest directives, gallery module-level global
Dev — Build & local smoke test / build-smoke (pull_request) Failing after 13m24s
Add window/neuronCheckoutFree stubs to web_stubs.c — needed to link
without undefined symbol errors on macOS (vessel stubs were already
handled, web platform stubs were not).

Add c_source directives to manifest.el so elb includes web_stubs.c and
vessel_stubs.c in the link — requires the matching elb update in
foundation/el PR #46.

Move gallery_share_allowlist from module scope into gallery_page() to
prevent elc from emitting a second main() for the gallery module, which
caused a duplicate-symbol link error when combined with main.el.

Update elc-linux-amd64 binary (rebuilt with RBrace fix from PR #46).
2026-05-07 17:58:58 -05:00
will.anderson 96fca7ebf7 fix(checkout): split checkout_page into helpers to avoid single-function OOM in elc --emit-header
Dev — Build & local smoke test / build-smoke (pull_request) Failing after 11m34s
Extract nav and style blocks into checkout_nav_html() and checkout_style_html()
so the compiler processes each template in isolation rather than one 490-line
function with mixed HTML template AST and BinOp string concat.
2026-05-07 16:00:53 -05:00
will.anderson e3e6ec7ade fix: move script/style inside their parent elements in nav.el and enterprise.el
Dev — Build & local smoke test / build-smoke (pull_request) Failing after 18s
2026-05-07 13:10:54 -05:00
will.anderson 5a8783ff0c fix: handle {#if} template conditionals and raw-text style/script in elc
Dev — Build & local smoke test / build-smoke (pull_request) Failing after 51s
Parser now supports {#if cond}...{#else}...{/if} blocks as HtmlIf AST nodes.
Style and script elements collect content as raw text, bypassing El expression
parsing entirely — eliminating O(n²) CSS parse time on large style blocks.
2026-05-07 13:06:25 -05:00
will.anderson 5f35ddde39 feat(demo): header countdown switches to reset timer when questions exhausted
Dev — Build & local smoke test / build-smoke (pull_request) Failing after 8s
When a user hits the 10-question limit, the header countdown flips from
'0 questions left' to a live 'resets in HH:MM:SS' ticker counting to
midnight UTC. Clears automatically when the session resets.
2026-05-07 02:49:11 -05:00
will.anderson 7c4c0d9963 feat(demo): server-side 8000-char (~2000 token) input limit on /api/demo
Dev — Build & local smoke test / build-smoke (pull_request) Failing after 45s
2026-05-07 02:35:29 -05:00
will.anderson c24b9b179b feat(demo): cap chat input at 8000 chars (~2000 tokens) 2026-05-07 02:33:36 -05:00
will.anderson 494f4ef585 Merge stage fixes into dev — HAVE_CURL, free-tier checkout, Stripe dedup, escaped styles 2026-05-07 01:05:36 -05:00
will.anderson 00e62bb010 Fix free tier checkout and Stripe duplicate customers
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
2026-05-07 01:00:51 -05:00
will.anderson 8d741fac20 Fix pentest security findings
- 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)
2026-05-06 20:58:29 -05:00
Will Anderson 5cb13d67f7 feat: convert web El source to native HTML template syntax
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.
2026-05-05 05:21:19 -05:00
Will Anderson 70820cf078 feat(chat): IP-keyed daily rate limit (10/day), live reset countdown, web_demo Anthropic key 2026-05-05 04:10:22 -05:00
Will Anderson 260ea4edaa Remove letter.html 2026-05-05 03:51:30 -05:00
Will Anderson 566cd568b7 Add Google Ads conversion tracking (AW-18140150015) 2026-05-05 03:49:14 -05:00
Will Anderson 94f6e749a0 Add El source files for all client-side JS
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
2026-05-04 11:23:21 -05:00
Will Anderson 246a5f0967 Fix gallery HTML structure bug and replace email auth with OTP flow
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.
2026-05-04 10:04:22 -05:00
Will Anderson 42f0786f97 feat(account): add email/password sign-up to account page
Dev — Build & local smoke test / build-smoke (push) Successful in 2m34s
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
2026-05-04 08:16:20 -05:00
Will Anderson 0508cd77fd fix(chat): raise history cap and add 30s frontend timeout
Deploy marketing to Cloud Run / deploy (push) Successful in 3m43s
Stage — Build, push & deploy to marketing-stage / deploy-stage (push) Successful in 3m26s
Dev — Build & local smoke test / build-smoke (push) Successful in 2m29s
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.
2026-05-04 01:57:38 -05:00
Will Anderson 4c5d4b3c84 fix(chat): restore history on Turnstile verification
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.
2026-05-03 19:51:59 -05:00
Will Anderson 047be5ae02 Remove chat with Neuron links from gallery page
Deploy marketing to Cloud Run / deploy (push) Successful in 3m57s
Dev — Build & local smoke test / build-smoke (push) Successful in 2m19s
Stage — Build, push & deploy to marketing-stage / deploy-stage (push) Successful in 2m34s
2026-05-03 19:12:35 -05:00
Will Anderson c75d8a9563 ci: add gitflow — dev/stage/main branches with CI workflows
Dev — Build & local smoke test / build-smoke (push) Successful in 2m51s
Stage — Build, push & deploy to marketing-stage / deploy-stage (push) Successful in 2m52s
Deploy marketing to Cloud Run / deploy (push) Successful in 3m37s
- 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.
2026-05-03 11:28:43 -05:00
Will Anderson 102343c8fe fix(gallery): implement voting JS + fix change-vote server path
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.
2026-05-03 11:19:09 -05:00
Will Anderson 254afd2fb2 fix(ci+chat): drop docker buildx flags + style chat code blocks
Deploy marketing to Cloud Run / deploy (push) Failing after 2m27s
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.
2026-05-03 10:15:24 -05:00
Will Anderson 8cd07b9107 fix: copilot svg - strip xml declaration so runtime detects image/svg+xml
Deploy marketing to Cloud Run / deploy (push) Failing after 11s
2026-05-03 04:03:02 -05:00
Will Anderson 22d2dab19b fix: copilot logo - use svg (already deployed) instead of missing png
Deploy marketing to Cloud Run / deploy (push) Failing after 37s
2026-05-03 03:57:02 -05:00
Will Anderson d9eeed9767 fix: replace broken copilot logo with correct PNG
Deploy marketing to Cloud Run / deploy (push) Failing after 18s
2026-05-03 03:52:54 -05:00
Will Anderson 8704f7cdfc update el runtime to v1.2.0, refactor checkout script to external asset
Deploy marketing to Cloud Run / deploy (push) Has been cancelled
2026-05-03 01:13:25 -05:00
Will Anderson 88ee3d53de fix: free tier shows payment form only after account created (auth-badge reveal)
Deploy marketing to Cloud Run / deploy (push) Has been cancelled
2026-05-02 18:48:38 -05:00
Will Anderson 6702fb7f9a fix: free tier checkout - full Stripe form with SetupIntent (card saved, no charge)
Deploy marketing to Cloud Run / deploy (push) Has been cancelled
2026-05-02 18:38:51 -05:00
Will Anderson d2908099c4 fix: add sign-up form to free tier checkout - was showing text with no form
Deploy marketing to Cloud Run / deploy (push) Has been cancelled
2026-05-02 18:23:28 -05:00
Will Anderson 2e73fce3b3 assets: use official Copilot PNG in comparison table
Deploy marketing to Cloud Run / deploy (push) Has been cancelled
2026-05-02 16:43:11 -05:00
Will Anderson 15105b665a assets: add Copilot icon to social assets
Deploy marketing to Cloud Run / deploy (push) Has been cancelled
2026-05-02 16:42:34 -05:00
Will Anderson 609a123012 assets: serve social icons locally, remove simpleicons CDN dependency
LinkedIn, X, Facebook, WhatsApp, TikTok, Snapchat icons now served from
/assets/social/ instead of cdn.simpleicons.org. LinkedIn uses the official
brand PNG from Downloads; remaining icons are scraped SVGs.
2026-05-02 15:11:38 -05:00
Will Anderson 46f93fd6eb security: replace denylist sanitize_share_html with allowlist el_html_sanitize
Deploy marketing to Cloud Run / deploy (push) Failing after 5s
A real attacker probed /api/share earlier today with <script>alert(1),
<iframe src=evil>, <img onerror>, <a href="javascript:...">, and a
<form action="/steal"> payload. Nothing executed because the chat
bubble at /share/<id> renders the served HTML inside marked.js's
already-escaped output, but the prior denylist sanitizer was fragile:

  - It comment-wrapped dangerous tags ("<!--script>...-->") which a
    literal "-->" inside an attacker-supplied attribute value can close
    early, re-exposing the original payload.
  - It renamed on*= attributes to data-x-on*= which left attack
    indicators visible in the served HTML.
  - It was a denylist; every new attack vector required a code change.
  - It didn't validate <a href> URL schemes properly.

The replacement is a runtime-level state-machine allowlist parser
(foundation/el af480f6: el_html_sanitize). The product just specifies
the JSON allowlist of allowed tags + attributes; the runtime drops
everything else, validates href/src URL schemes (http/https/mailto/
fragment/relative only), and drops whole subtrees of script/style/
iframe/object/embed/form regardless of the allowlist.

Phase 4 of bl-dc55ae07: deletes sanitize_share_html (main.el) and
gal_sanitize_html (gallery.el); replaces 3 call sites with
el_html_sanitize(html, allowlist). Defines default_share_allowlist
in main.el and the identical gallery_share_allowlist in gallery.el
(separate bindings to avoid a forward-reference at build-concat
order — gallery is concatenated before main).

Phase 5: migrations/20260502185500_backfill_resanitize_share_cards.sql
nulls answer_html for any share_cards row older than 1 hour. Applied
via the Supabase Management API; 0 rows in scope (the column was
added today and existing rows pre-date its first write).

Also fixes an orthogonal duplicate-symbol bug: unix_timestamp() was
defined in both dist/web_stubs.c and the runtime (the latter is a
recent runtime addition picked up by the runtime sync). Removed the
stub.

Backlog: bl-dc55ae07
2026-05-02 12:56:33 -05:00
Will Anderson ff054b9980 fix(gallery): render answer_html so card previews match the share page
Gallery was reading the plain answer field and HTML-escaping it,
showing literal `&lt;ol&gt;...` text where the actual share page rendered
the markdown. Now selects answer_html alongside, runs it through
the same sanitizer as the share-card render, and falls back to
escaped plaintext for legacy rows.
2026-05-02 12:38:48 -05:00
Will Anderson 42f8602457 fix: resend email send path - http_post_auth was dropping silently
The wrapper now logs the response and returns a structured ok/error
shape. Four call sites converge on a single send_email helper.
Resend deliveries verified end to end against
will.anderson@neurontechnologies.ai (delivery IDs 492fa066, 74258223,
69a3d9ab, f6d1c889).

Root cause: http_post_auth in dist/web_stubs.c only set the
Authorization: Bearer header. Resend rejects requests without
Content-Type: application/json with HTTP 422 missing_required_field
because it parses the body as form-urlencoded. The 422 response was
being captured by the El handler but not parsed, so callers logged
the error body and returned ok-200 to the client. Two endpoints also
built malformed JSON by interpolating the raw request body unquoted
into the text field.

Fix:
- Added http_post_auth_json (Bearer + Content-Type: application/json)
  alongside http_post_auth in dist/web_stubs.c. Stripe form-POST
  callers stay on http_post_auth, JSON callers (Resend now, others
  later) move to the json variant.
- New send_email(from_addr, to, subject, html, text) wrapper in
  src/main.el. JSON-escapes all user-provided fields, parses the
  Resend response into a structured ok/error envelope, and println's
  the outcome ([email] sent id=<id>) for Cloud Run log surfaces.
- Refactored four call sites onto the wrapper: /api/enterprise-inquiry,
  /api/developer-interest, /api/waitlist, /api/attest, the family
  invite branch in /api/family/invite, and both DocuSeal completion
  branches in /api/docuseal/webhook/<token>.
- Untracked dist/ source files (web_stubs.c, vessel_stubs.c,
  soul-demo.c, entrypoint.sh, engram-snapshot.json) are now committed
  - generated artifacts (main.c, binaries) stay ignored. Without this
  the next CI rebuild would regress the fix.
2026-05-02 12:37:54 -05:00
Will Anderson 79cd461b83 feat: phase 1 of runtime config store (neuron_config table, chat.model)
New Supabase table neuron_config keyed on (key, scope) with jsonb
value column. Web tier reads chat.model via /api/demo with 60s TTL
caching, passes to soul via dharma envelope payload.model. No more
revision-rollout-per-model-swap.

Admin read endpoint at /api/admin/config gated by NEURON_ADMIN_TOKEN.
Write surface and Realtime subscription land in Phase 2.

Backlog: bl-6eb51893
2026-05-02 12:24:00 -05:00
Will Anderson e121038382 fix(gallery): proper auth-gated voting with persistence, undo, and change
Replaces the broken counter-bump RPC with a per-user share_votes table
(PK share_id+user_id, RLS-enforced ownership). One vote per user per
card, change direction or undo any time. Auth required for write;
read is public. share_cards.upvotes/downvotes/score stay in sync via
recalc trigger. New endpoints: POST /api/vote (auth-gated), GET
/api/vote-state/:id (auth-aware).
2026-05-02 12:14:31 -05:00
Will Anderson ccbe243eab share: render markdown + preview-before-publish + soul-history probe
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.
2026-05-02 12:11:12 -05:00
Will Anderson 3a18c37f7d fix: shadow-let soul_url and neuron_origin so the default actually stores
The native elc silently drops bare reassignment inside an if body
(`name = expr` compiles to an orphan expr statement with no store).
Both soul_url and neuron_origin defaulted to empty, breaking every
http_post call to the in-container soul-demo. Switch to let-shadowed
if-expressions until the compiler is fixed.

See: products/web/src/main.el `/api/soul-health` route output.
2026-05-02 11:47:23 -05:00
Will Anderson 8107c4c0e8 add /api/soul-health diagnostic route 2026-05-02 11:41:54 -05:00
Will Anderson b76c0c995a swap simpleicons silhouettes for actual brand marks on comparison table 2026-05-02 11:30:14 -05:00
Will Anderson 640813e42e migrate stage build to native elc; chat restores from localStorage on return
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.
2026-05-02 11:15:09 -05:00
Will Anderson cae5028130 auto-signup webhook + chat polish (10q, bold-white, last-q awareness)
Webhook handler now reacts to payment_intent.succeeded and
setup_intent.succeeded (in addition to the legacy
checkout.session.completed) and auto-provisions a Supabase account
for every successful purchase via /auth/v1/invite. The buyer gets a
magic-link email; landing on /account?welcome=1 with that link signs
them in and the plan card renders.

Stripe Customer is updated with metadata.supabase_user_id so the
cross-reference is durable. New stub: supabase_admin_invite() in
web_stubs.c (POST {project_url}/auth/v1/invite with service-key
bearer auth + apikey header).

Chat widget:
  * MAX 5 → 10 questions per session
  * countdown is now bold white at all times; the red threshold
    at <=5 was loud and made the chat feel rationed
  * greeted-once flag in localStorage so reopening the panel
    doesn't replay the canned hello (only first open greets)
  * questions_remaining + is_last_question travel to the soul on
    each turn so it can close in voice on the final turn instead
    of leaving the visitor at a hard cap

Soul side of the last-turn handshake is still a TODO - the wrapper
plumbs the fields through but soul-demo.el has to be updated to
read and act on them.
2026-05-02 10:10:36 -05:00
Will Anderson 40691c05c8 nav order matches page; pricing moved last; setup-intent attach fix
Page section order is now: hero → pillars → how-it-works → inference →
efficiency → comparison → enterprise → mission → local-first → safety →
environmental → marketplace → viral → pricing → footer. Pricing is the
last content block so visitors scroll the whole story before being asked
to commit. Nav (desktop + mobile + dropdown) reordered to mirror the
page: How it works → Enterprise → Mission ▾ → Marketplace → Pricing →
About / Gallery / Account.

Stripe attach bug: /api/link-customer was hitting
POST /v1/payment_intents/{id} for every Intent regardless of type, so
SetupIntents (seti_*) returned 404 from Stripe. Branched on the id
prefix - SetupIntents go to /v1/setup_intents/{id} and skip the
receipt_email param (PaymentIntent-only field). Caught from a live
422 in the dashboard the moment Will tested timing=later.
2026-05-02 00:52:59 -05:00