Compare commits

..

140 Commits

Author SHA1 Message Date
will.anderson 20029d36df ci: touch dist to trigger stage rebuild
Dev — Build & local smoke test / build-smoke (pull_request) Successful in 1m35s
2026-05-11 18:45:57 -05:00
will.anderson 28f9ecd1a3 Merge pull request 'fix: heading and button elements pass children unescaped' (#113) from fix/force-full-rebuild into dev
Dev — Build & local smoke test / build-smoke (push) Successful in 2m0s
2026-05-11 22:21:41 +00:00
will.anderson b6bb25e79e fix: heading and button elements pass children unescaped
Dev — Build & local smoke test / build-smoke (pull_request) Successful in 1m32s
el_h1/h2/h3/h4 and el_button were calling el_escape() on their
content, converting any HTML children (e.g. <span class="gold">)
into literal entity text on screen.

These functions accept composed HTML children, not raw text — they
should pass the argument through like el_div/el_p/el_span do.
el_text, el_attr, el_title, el_textarea, and el_img keep escaping
(they handle actual text/attribute values, not HTML children).
2026-05-11 17:21:19 -05:00
will.anderson 5812cb0452 Merge pull request 'Force full El rebuild — strip CGI content from base image' (#110) from fix/force-full-rebuild into dev
Dev — Build & local smoke test / build-smoke (push) Successful in 2m13s
2026-05-11 21:43:09 +00:00
will.anderson c99923da1b Force full El rebuild — strip CGI content from base image
Dev — Build & local smoke test / build-smoke (pull_request) Successful in 1m39s
2026-05-11 16:42:41 -05:00
will.anderson 4e35cbe841 Merge pull request 'Also skip El rebuild for workflow-only changes' (#107) from fix/stage-ci-paths into dev
Dev — Build & local smoke test / build-smoke (push) Successful in 2m52s
2026-05-11 20:46:51 +00:00
will.anderson 62385b53c2 Also skip El rebuild for .gitea/ workflow-only changes
Dev — Build & local smoke test / build-smoke (pull_request) Successful in 2m16s
Workflow file changes don't require rebuilding the El binary. Without
this, merging workflow fixes to main triggers a full El build which
hits a codegen issue in the CI version of elb.
2026-05-11 15:46:37 -05:00
will.anderson 952b03737b Merge pull request 'Skip El rebuild for migration/script/test-only changes' (#105) from fix/stage-ci-paths into dev
Dev — Build & local smoke test / build-smoke (push) Successful in 2m54s
2026-05-11 20:45:14 +00:00
will.anderson d2628ec42e Skip El rebuild for migration/script/test-only changes
Dev — Build & local smoke test / build-smoke (pull_request) Successful in 2m17s
migrations/, scripts/, tests/ changes don't require rebuilding the El
binary. Classifying them as asset-only avoids spurious full builds that
regenerate dist/*.c and can hit codegen incompatibilities.
2026-05-11 15:44:59 -05:00
will.anderson d598fb7b10 Merge pull request 'Update CORS test: no-Origin requests are allowed' (#103) from fix/stage-ci-paths into dev 2026-05-11 20:22:30 +00:00
will.anderson 1eeb8df04b Update CORS test: no-Origin requests are allowed (same-origin fix)
Same-origin browser fetches don't send Origin. The server correctly
allows them — blocking was the bug that broke checkout. Update the
test to match the fixed behavior.
2026-05-11 15:22:22 -05:00
will.anderson 9e5d7e55ab Merge pull request 'Fix stage source guard: fetch origin/dev before ancestry check' (#101) from fix/stage-ci-paths into dev
Dev — Build & local smoke test / build-smoke (push) Successful in 2m48s
2026-05-11 19:09:33 +00:00
will.anderson 5d3b1a3e20 Fix stage source guard: fetch origin/dev before ancestry check
Dev — Build & local smoke test / build-smoke (pull_request) Successful in 2m22s
The shallow clone (fetch-depth: 2) doesn't include origin/dev, so
git merge-base --is-ancestor was silently failing. Fetch dev with
depth=1 first so custom merge commit titles still pass the check.
2026-05-11 14:09:18 -05:00
will.anderson 1264e32577 Fix: idempotent migration policy creation 2026-05-11 18:56:36 +00:00
will.anderson 7f88414b40 Make migration policy creation idempotent
DROP POLICY IF EXISTS before CREATE POLICY so migrations can be
re-applied to a DB that already has the policy (e.g. demo_config
was manually applied before migration tracking was set up).
2026-05-11 13:56:12 -05:00
will.anderson b3ce6c3e64 Merge pull request 'Fix CI migration step: script file instead of heredoc' (#97) from fix/stage-ci-paths into dev
Dev — Build & local smoke test / build-smoke (push) Successful in 2m3s
Merge PR #97
2026-05-11 18:34:01 +00:00
will.anderson adbdfd3e90 Fix CI migration step: extract Python to scripts/run_migrations.py
Dev — Build & local smoke test / build-smoke (pull_request) Successful in 1m36s
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.
2026-05-11 13:33:44 -05:00
will.anderson dd5fd2b3ce Merge pull request 'Fix supabase-config CORS: treat absent Origin as allowed' (#95) from fix/stage-ci-paths into dev
Dev — Build & local smoke test / build-smoke (push) Successful in 1m59s
Merge PR #95
2026-05-11 18:30:44 +00:00
will.anderson 617916134f Fix supabase-config CORS: treat absent Origin header as allowed
Dev — Build & local smoke test / build-smoke (pull_request) Successful in 1m30s
map_get returns null (0) for missing headers. str_eq(null, "") is false
because EL_CSTR(0) is NULL != "". Same-origin browser fetches don't send
Origin at all, so the missing-origin case was incorrectly being denied.

Fix: use str_starts_with(req_origin, "http") to detect a present origin.
If no origin header (null first arg → str_starts_with returns false),
origin_present is false and the request is allowed unconditionally.
2026-05-11 13:30:22 -05:00
will.anderson 924c0804e7 Merge pull request 'Wire Supabase migrations into CI/CD' (#93) from fix/stage-ci-paths into dev
Dev — Build & local smoke test / build-smoke (push) Successful in 2m0s
Merge PR #93
2026-05-11 18:22:01 +00:00
will.anderson 4a915c1a11 Wire Supabase migrations into CI/CD
Dev — Build & local smoke test / build-smoke (pull_request) Successful in 1m30s
Adds a "Run database migrations" step to both stage.yaml and deploy.yaml.
Uses the Supabase Management API (access token from GCP Secret Manager)
to apply pending migrations tracked in a schema_migrations table.
Migrations run unconditionally before every deploy — asset-only or full.

Also adds migrations/** to paths filter so a migrations-only commit
triggers the pipeline.
2026-05-11 13:21:42 -05:00
will.anderson 4a3ede98f7 Merge pull request 'Stage: pricing buttons, API keys, reasoning note, enterprise contacts' (#91) from fix/stage-ci-paths into dev
Dev — Build & local smoke test / build-smoke (push) Successful in 2m19s
Merge PR #91: dev stage batch
2026-05-11 18:05:33 +00:00
will.anderson a6b75b9abf Add direct sales and security contact block to enterprise section
Dev — Build & local smoke test / build-smoke (pull_request) Successful in 1m45s
Two-card grid above the enterprise box — sales (enterprise@) and
security (security@) — with email links and one-line descriptions.
Visible without filling out the form, which is what enterprise and
security teams look for first.
2026-05-11 12:58:25 -05:00
will.anderson 21a7c07547 Add reasoning model recommendation to API Keys card
Callout above the provider list recommends o4-mini/o3, Claude Sonnet 4,
Gemini 2.5 Pro, or Grok-3 for best performance, notes that model choice
happens in the app, and points to Neuron Inference launching Q3 2026.
2026-05-11 12:54:28 -05:00
will.anderson 756f1f955e Add per-provider key provisioning instructions to API Keys card
Each provider row now has a collapsible details panel with accurate
step-by-step instructions and a direct link to the key creation page.
Includes billing notes for OpenAI and Anthropic (easy to miss gotchas),
free tier note for Gemini, and credits note for Grok.
2026-05-11 12:47:12 -05:00
will.anderson 18350761c5 Add API key provisioning to accounts page 2026-05-11 12:24:05 -05:00
will.anderson f22d90ac6f Make Free and Professional pricing buttons solid blue
All three pricing CTA buttons now share the same solid navy background,
white text, and blue hover state. Previously only anchor-element rules
existed for the solid variant; the button elements had no explicit
background so all three appeared unstyled.
2026-05-11 12:19:19 -05:00
will.anderson 2b8915bd60 Fix JS syntax errors and stage supabase-config CORS in CI
chat-widget.el: apostrophe in El native_js double-quoted strings caused
the El compiler to drop the backslash, producing broken JS single-quoted
strings. Switched those four string literals to double-quoted JS strings
using \" escaping so the compiled output is valid.

main.el: /api/supabase-config was returning 403 for all stage Cloud Run
origins. Added marketing-stage-* prefix to the allowed list so the
checkout page can initialise Supabase during CI E2E runs.
2026-05-11 12:15:18 -05:00
will.anderson acca3cfddf Merge pull request 'add unsafe-eval to CSP (El native_js compatibility)' (#88) from fix/stage-ci-paths into dev
Dev — Build & local smoke test / build-smoke (push) Successful in 3m15s
Merge fix/stage-ci-paths into dev
2026-05-11 16:44:54 +00:00
will.anderson 90f7c3655e add unsafe-eval to CSP for El runtime native_js() compatibility
Dev — Build & local smoke test / build-smoke (pull_request) Successful in 3m9s
El's native_js() compiles to eval(). checkout-auth.el uses native_js()
to embed the auth logic, so all window globals (showSignIn, initStripe,
etc.) live inside an eval call. Stage CSP was blocking it, leaving the
page with no auth functions defined.
2026-05-11 11:40:05 -05:00
will.anderson 6d3c7e2bcd Merge pull request 'remove --obfuscate from elc JS compile step' (#86) from fix/stage-ci-paths into dev
Dev — Build & local smoke test / build-smoke (push) Successful in 2m54s
2026-05-11 16:11:33 +00:00
will.anderson 637b05af98 remove --obfuscate from elc JS compile step
Dev — Build & local smoke test / build-smoke (pull_request) Successful in 2m8s
Stage CSP blocks 'unsafe-eval' which javascript-obfuscator introduces.
checkout-auth.js IIFE was crashing before assigning window globals,
causing all checkout E2E tests to fail.
2026-05-11 11:11:11 -05:00
will.anderson d90e8d1668 Merge pull request 'Fix Stripe CDN mock override and free-plan sync guards in E2E tests' (#84) from fix/stage-ci-paths into dev
Fix Stripe CDN mock override and free-plan sync guards in E2E tests
2026-05-11 15:36:21 +00:00
will.anderson c6fd06b3de Fix Stripe CDN mock override and free-plan sync guards in E2E tests
- Block real Stripe CDN (js.stripe.com) in injectMockStripe() so the
  addInitScript mock is never overwritten by the async-loaded SDK
- Replace waitForFunction(signUpWithEmail) with waitForLoadState in
  all 8 free-plan auth tests; defer scripts run before DOMContentLoaded
  so the function is guaranteed present without polling for it
2026-05-11 09:54:55 -05:00
will.anderson 91ecdaf3a5 Merge pull request 'Fix CI JS corruption from obfuscator stdout; clean up flaky test guards' (#81) from fix/stage-ci-paths into dev
Dev — Build & local smoke test / build-smoke (push) Failing after 1m53s
Merge fix/stage-ci-paths: fix CI JS corruption + flaky test guards
2026-05-11 14:16:29 +00:00
will.anderson 61f006f62d Fix CI JS corruption from obfuscator stdout; clean up flaky test guards
Dev — Build & local smoke test / build-smoke (pull_request) Successful in 1m54s
- Strip [javascript-obfuscator-cli] progress line from elc --obfuscate
  output before writing to dist/js/ (was prepended to every compiled JS
  file, causing browser parse errors on stage)
- Remove spurious waitForFunction(signUpWithEmail) guards from
  buyer-name and buyer-email structural tests (pure DOM tests, no auth)
- Switch chat.spec.ts beforeEach to domcontentloaded (SSR elements
  present at DOM ready; networkidle caused cold-start timeouts)
2026-05-11 08:19:30 -05:00
will.anderson 48ba7716b8 Merge pull request 'Free plan Stripe age verification + soul demo personalization' (#79) from fix/stage-ci-paths into dev
Dev — Build & local smoke test / build-smoke (push) Successful in 2m25s
Merge free plan age verification + soul demo personalization into dev
2026-05-11 07:05:35 +00:00
will.anderson c966f2b455 implement free plan age verification via Stripe SetupIntent; personalize soul demo greeting with user name and timezone
Dev — Build & local smoke test / build-smoke (pull_request) Successful in 1m49s
2026-05-11 02:03:39 -05:00
will.anderson 74e84da41a Merge pull request 'Add tests/** to stage CI paths filter' (#77) from fix/stage-ci-paths into dev
Dev — Build & local smoke test / build-smoke (push) Successful in 2m26s
2026-05-11 06:21:04 +00:00
will.anderson ac2d00d653 Add tests/** + playwright.config.ts to stage CI paths filter
Dev — Build & local smoke test / build-smoke (pull_request) Successful in 1m52s
2026-05-11 01:20:43 -05:00
will.anderson c0e6b40a5a Merge pull request 'Comprehensive checkout + Stripe payment flow tests' (#75) from feat/checkout-comprehensive-tests into dev
Merge: Comprehensive checkout + Stripe payment flow tests
2026-05-11 06:19:19 +00:00
will.anderson dbb8035698 Add comprehensive checkout + Stripe payment flow tests
- checkout-flows.spec.ts: 60 tests covering all 3 plan variants (free/
  professional/founding), auth section visibility, form validation,
  sign-in/sign-up toggle, mocked Supabase auth flows (sign-up, email-
  confirm-required, existing session, sign-in error), DOM transitions
  (auth-section → payment-section, free-success panel), auth badge
  content + email pre-fill, /api/checkout and /api/supabase-config
  endpoint contracts, CORS enforcement

- checkout-stripe.spec.ts: 45 tests covering Stripe.js presence,
  NEURON_CFG shape, submit-btn disabled state, founding attestation
  checkbox + attest-warn guard, professional charge timing radios,
  setup_mode label, mocked full Stripe payment flow via addInitScript +
  /api/payment-intent intercept, submit validation (name/email),
  decline handling, sold-out guard, /api/payment-intent /api/link-
  customer /api/attest /api/founding-count endpoint contracts, and
  live test-card flows (skipped unless STRIPE_LIVE=1)

Mocking strategy: page.route() for /api/supabase-config + Supabase
auth endpoints; addInitScript() for window.Stripe mock; localStorage
pre-seeding for existing-session tests.
2026-05-11 01:18:37 -05:00
will.anderson 83aa7ad64f Merge pull request 'test: full Playwright + API test suite for stage' (#73) from fix/checkout-auth-reveal into dev
Dev — Build & local smoke test / build-smoke (push) Successful in 2m59s
Merge fix/checkout-auth-reveal into dev
2026-05-11 05:29:16 +00:00
will.anderson cac7bd5727 test: full Playwright + API test suite for stage
Dev — Build & local smoke test / build-smoke (pull_request) Successful in 1m52s
159 tests across three Playwright projects (api, chromium, mobile):
- tests/api/security.test.ts: security headers, CORS on /api/supabase-config
  (origin allowlist enforced), auth gate on /api/demo, Stripe webhook
  signature enforcement, source file leakage, path traversal, input
  validation (8000-char message cap)
- tests/api/endpoints.test.ts: /api/health, /api/founding-count shape
  invariants, /api/supabase-config JWT shape, sitemap.xml, robots.txt,
  /llms.txt, /api/soul-health internal gate, 404 for unknown routes
- tests/e2e/landing.spec.ts: title, h1 count, meta description, OG tags,
  canonical (no stage leak), JSON-LD schema, demo widget DOM presence,
  JS error filtering (known GTM/CSP noise excluded)
- tests/e2e/seo.spec.ts: per-page title patterns, noindex on checkout,
  canonical URLs, sitemap production-URL enforcement
- tests/e2e/checkout.spec.ts: all three plan variants, auth section, payment
  element, canonical
- tests/e2e/chat.spec.ts: widget DOM structure, auth gate (send button
  disabled without session), API-level auth rejection
- tests/e2e/navigation.spec.ts: all public routes return 200, 404s for
  removed/old paths (/terms, /enterprise-terms, /gallery), static files

All 159 pass against stage. CI step added to stage.yaml after smoke test.
2026-05-11 00:28:33 -05:00
will.anderson e914704d86 Merge pull request 'security: pentest fixes — webhook sig, CORS, soul-health gate, asset headers' (#69) from fix/checkout-auth-reveal into dev
Dev — Build & local smoke test / build-smoke (push) Successful in 2m31s
security: pentest fixes — webhook sig, CORS, soul-health gate, asset headers
2026-05-11 04:57:02 +00:00
will.anderson 43e1245306 seo: full audit fixes — meta, og, schema, canonical, sitemap, headings, alts
Dev — Build & local smoke test / build-smoke (pull_request) Successful in 1m57s
- Per-page title/description/canonical/OG tags: about, checkout (per-plan),
  terms, enterprise-terms, success all get unique SEO blocks
- Homepage title updated to em-dash form; meta description adds CTA
- og:site_name added to all pages
- noindex/nofollow on checkout, success, account pages
- Sitemap (/sitemap.xml) with all public pages; robots.txt updated with
  Sitemap directive and Disallow for private paths
- Schema: WebSite type added, Organization gains logo ImageObject, SoftwareApplication
  gains url field, billingIncrement corrected to billingPeriod (ISO 8601 P1M),
  sameAs gains x.com/neurontechai alongside GitHub
- marked.min.js given defer attribute (was render-blocking)
- page_head refactored into page_head_base + page_seo_block + page_open_seo
  for clean inner-page overrides without duplicating the CSS/script block
2026-05-10 23:56:40 -05:00
will.anderson 3f3c5cf149 security: penetration test fixes — headers, cors, path traversal, info leakage
- Switch to http_serve_v2/http_set_handler_v2 so request headers are available
  to El handler code (prerequisite for all header-based security checks)

- Stripe webhook (CVE-class): add HMAC-SHA256 signature verification against
  Stripe-Signature header using STRIPE_WEBHOOK_SECRET env var. Previously any
  unauthenticated POST could forge a payment_intent.succeeded event and
  increment the founding counter or trigger Supabase account provisioning for
  arbitrary emails.

- CORS on /api/supabase-config: restrict to neurontechnologies.ai and localhost
  origins only. Cross-origin requests now get 403.

- /api/soul-health: require X-Internal: true header; otherwise return 404.
  Endpoint was publicly accessible and leaked internal soul service URL,
  network topology, and raw probe responses.

- Static asset / JS headers: add X-Frame-Options, Referrer-Policy,
  Permissions-Policy, and Content-Security-Policy to static_asset_headers_json
  and js_headers_json. These were only present on HTML/API responses before.

- Fix state key bug: share_card_page read state_get("__neuron_origin__") but
  the key registered at startup is "__origin__", causing empty base URLs in
  share card og: meta tags.
2026-05-10 23:56:31 -05:00
will.anderson bdff0ad153 Merge pull request 'feat: auth-gate demo chat + budget circuit breaker' (#67) from fix/checkout-auth-reveal into dev
Dev — Build & local smoke test / build-smoke (push) Successful in 2m27s
feat: auth-gate demo chat + budget circuit breaker
2026-05-11 04:45:36 +00:00
will.anderson fe418bf3f7 feat: auth-gate demo chat + budget circuit breaker
Dev — Build & local smoke test / build-smoke (pull_request) Successful in 2m10s
Gate the demo chat behind Supabase auth: the widget now fetches Supabase
config on open, shows a compact sign-in pane (Google OAuth or email/password)
when the user is unauthenticated, and passes the access_token to /api/demo.
The server verifies the token via supabase_auth_user() before any processing
and uses the verified user ID as the rate-limit key.

Add a budget kill switch: a demo_config table in Supabase holds a
demo_enabled flag that /api/demo polls every 60s (cached, fails open).
A Cloud Function (demo-budget-guard) is triggered by a GCP Pub/Sub budget
alert and sets demo_enabled = 'false' when spend crosses 90% of the $150
daily budget. Budget and topic are provisioned; function is live in
us-central1.
2026-05-10 23:44:54 -05:00
will.anderson 7536c216e6 Merge pull request 'feat: scale fixes — max-instances, asset caching, shared rate limits, global cap' (#65) from fix/checkout-auth-reveal into dev
Dev — Build & local smoke test / build-smoke (push) Successful in 2m30s
feat: scale fixes — max-instances, asset caching, shared rate limits, global cap
2026-05-11 03:12:30 +00:00
will.anderson bdb6ddc581 feat: scale fixes — max-instances, asset caching, shared rate limits, global cap
Dev — Build & local smoke test / build-smoke (pull_request) Successful in 2m0s
- soul-demo-stage: raise max-instances 10 → 50
- marketing-stage: explicitly set max-instances 200
- /assets/* and /brand/*: return Cache-Control: public, max-age=31536000, immutable
  so Cloudflare caches static assets at the edge (eliminates Cloud Run hit per request)
- /js/*: bump from max-age=3600 to max-age=31536000, immutable (same policy)
- Per-uid demo rate limit: replace in-process state with Supabase demo_rate_limits table
  so the 10-chats/day cap is enforced across all Cloud Run instances; falls back to
  in-process for local dev when SUPABASE_SERVICE_KEY is absent
- Global circuit breaker: trip if any single instance handles ≥2000 demo requests/UTC day
2026-05-10 22:12:09 -05:00
will.anderson 00f05f813e Merge pull request 'feat: extract soul-demo into standalone Cloud Run service' (#63) from fix/checkout-auth-reveal into dev
Dev — Build & local smoke test / build-smoke (push) Successful in 2m33s
feat: extract soul-demo into standalone Cloud Run service
2026-05-11 02:09:06 +00:00
will.anderson 93f9ea2be2 feat: extract soul-demo into standalone Cloud Run service
Dev — Build & local smoke test / build-smoke (pull_request) Successful in 2m19s
2026-05-10 21:08:46 -05:00
will.anderson e480aba2f1 Merge pull request 'fix: HAVE_CURL verification — use strings not nm' (#61) from fix/checkout-auth-reveal into dev
Dev — Build & local smoke test / build-smoke (push) Successful in 2m25s
2026-05-11 01:07:39 +00:00
will.anderson feee40c34b ci: fix HAVE_CURL verification — use strings check not nm
Dev — Build & local smoke test / build-smoke (pull_request) Successful in 1m54s
2026-05-10 20:07:21 -05:00
will.anderson e6e89a1f4d Merge pull request 'fix: relink neuron-web with HAVE_CURL (chat proxy)' (#59) from fix/checkout-auth-reveal into dev
Dev — Build & local smoke test / build-smoke (push) Successful in 2m15s
fix: relink neuron-web with HAVE_CURL (chat proxy)
2026-05-11 01:03:22 +00:00
will.anderson 8b8cb2f580 ci: relink neuron-web with HAVE_CURL after elb build
Dev — Build & local smoke test / build-smoke (pull_request) Successful in 1m45s
elb does not pass -DHAVE_CURL when compiling el_runtime.c, so all
http_get/http_post calls from El code return the no-op error string
instead of making real HTTP requests. This breaks the chat proxy to
soul-demo at localhost:7772.

After elb runs (and generates all intermediate .c files in dist/),
recompile el_runtime.c with -DHAVE_CURL and relink the entire binary
from those generated files. Verifies curl_easy_init is present in the
output binary before proceeding.
2026-05-10 20:03:00 -05:00
will.anderson 4d359ff021 Merge pull request 'Replace k3s with direct soul-demo watchdog' (#57) from fix/checkout-auth-reveal into dev
Dev — Build & local smoke test / build-smoke (push) Successful in 2m26s
Merge fix/checkout-auth-reveal into dev
2026-05-11 00:46:56 +00:00
will.anderson cd1c6737e8 Replace k3s with direct soul-demo watchdog in Cloud Run container
Dev — Build & local smoke test / build-smoke (pull_request) Successful in 2m11s
Cloud Run gen2 doesn't provide eth0 with a unicast IP, causing k3s flannel
to crash on every container start. k3s was also wrong architecture for
Cloud Run (HPA inside a container, k3s overhead for one process).

Changes:
- entrypoint.sh: replace k3s server with a bash watchdog loop that starts
  soul-demo directly and restarts it on crash (3s backoff)
- Dockerfile.stage: remove k3s binary, soul-demo-image.tar, k3s manifests
  and their associated dirs/envvars; keep soul-demo binary only
- stage.yaml: remove 'Download k3s binary' step; rename and simplify
  soul-demo build step to compile binary only (no OCI image/tar)
- dev.yaml: update soul-demo placeholder step (binary not tar)
- manifest.el: document HAVE_CURL requirement since manifest.el has no
  c_flags/link_flags directive support
2026-05-10 19:46:35 -05:00
will.anderson f27fc2622c Merge pull request 'Fix envelope truncation in http_response when called after fs_read' (#55) from fix/checkout-auth-reveal into dev
Dev — Build & local smoke test / build-smoke (push) Failing after 2m46s
2026-05-11 00:23:28 +00:00
will.anderson 0433fe8c0f Fix http_response() truncating envelope via stale _tl_fs_read_len
Dev — Build & local smoke test / build-smoke (pull_request) Failing after 2m22s
http_response() builds a JSON envelope wrapping the body. If the caller
previously called fs_read() (which sets _tl_fs_read_len = file_size),
http_worker used that stale value as the response copy length — truncating
the larger envelope to the original file size before it reached
http_send_response. The truncated envelope had the body field cut mid-string;
jp_parse_string_raw failed, env_body = "", and http_send_all sent file_size
bytes of garbage past the empty string.

Fix: reset _tl_fs_read_len = 0 at the start of http_response(). The hint
was set for the raw file bytes; the envelope is a new string and must use
strlen() for its length.
2026-05-10 19:23:10 -05:00
will.anderson 9da4d50883 Merge pull request 'Fix JS files served as JSON envelope (checkout/Stripe/auth all broken)' (#53) from fix/checkout-auth-reveal into dev
Dev — Build & local smoke test / build-smoke (push) Failing after 2m4s
Fix JS files served as JSON envelope (checkout/Stripe/auth all broken)
2026-05-10 22:34:32 +00:00
will.anderson c99ca82302 Fix JS files served as raw JSON envelope instead of JavaScript
Dev — Build & local smoke test / build-smoke (pull_request) Failing after 1m36s
http_parse_envelope() called json_parse() on the entire response envelope
(~47KB when body is obfuscated JS). The parser failed on large/complex content,
so is_envelope=0 and the raw JSON was sent — browsers got {"el_http_response":1,...}
instead of executable JavaScript, silently breaking all client-side code.

Fix: replace json_parse-of-full-envelope with a direct field scanner:
- "status" extracted via strtol
- "headers" object extracted via brace-depth scan, then json_parse only that
  small substring (always safe — headers are simple k/v string pairs < 1KB)
- "body" string extracted via jp_parse_string_raw — no intermediate allocation

Also: /js/* route now returns http_response(200, js_headers_json(), content)
with explicit Content-Type: application/javascript so the browser doesn't
apply the json-heuristic (obfuscated JS starting with '[' was detected as JSON,
which with X-Content-Type-Options: nosniff blocks script execution).
2026-05-10 17:32:45 -05:00
will.anderson e292453905 Merge pull request 'Fix checkout auth: free-success panel + Stripe auto-init for paid plans' (#51) from fix/checkout-auth-reveal into dev
Dev — Build & local smoke test / build-smoke (push) Failing after 2m12s
Fix checkout auth: free-success panel + Stripe auto-init for paid plans
2026-05-10 22:00:55 +00:00
will.anderson 0263e51407 Fix checkout: show free-success when logged in; init Stripe without auth on paid plans
Dev — Build & local smoke test / build-smoke (pull_request) Failing after 1m47s
- revealPaymentForm: for free plan, show #free-success panel (was doing nothing,
  leaving page blank when user already had a Supabase session)
- checkExistingSession: for paid plans with no session, call initStripe immediately —
  auth is optional, the payment form shouldn't wait indefinitely
- Guard _formRevealed: prevent double-call from handleAuthRedirect + checkExistingSession
2026-05-10 16:59:51 -05:00
will.anderson b4935ed880 Merge pull request 'Fix http handler not found: pre-register via constructor' (#49) from fix/entrypoint-k3s-nonblocking into dev
Dev — Build & local smoke test / build-smoke (push) Failing after 2m29s
Merge PR #49: Fix http handler not found
2026-05-10 18:36:47 +00:00
will.anderson 9a6f0defd1 Fix http handler not found: pre-register via el_runtime_register_handler
Dev — Build & local smoke test / build-smoke (pull_request) Failing after 1m54s
elb links without -rdynamic so dlsym(RTLD_DEFAULT, "handle_request")
returns NULL at runtime. http_set_handler stores the name as active but
never finds a function pointer, causing every request to return
"el-runtime: no http handler registered" even after http_serve is called.

Fix: add a __attribute__((constructor)) in web_stubs.c that calls
el_runtime_register_handler("handle_request", handle_request) directly,
bypassing dlsym entirely. The handler is in the registry before main()
runs, so http_lookup_active() finds it on the first request.
2026-05-10 13:36:05 -05:00
will.anderson ee0147869e Merge pull request 'Fix GLIBC_2.38 mismatch: switch base image to ubuntu:24.04' (#47) from fix/entrypoint-k3s-nonblocking into dev
Dev — Build & local smoke test / build-smoke (push) Failing after 2m23s
Fix GLIBC_2.38 mismatch: switch base image to ubuntu:24.04
2026-05-10 18:01:57 +00:00
will.anderson 740382fca1 Fix GLIBC_2.38 mismatch: switch base image to ubuntu:24.04
Dev — Build & local smoke test / build-smoke (pull_request) Failing after 2m13s
CI runner (Ubuntu 24.04, glibc 2.39) produces binaries that require
GLIBC_2.38+. debian:bookworm-slim ships glibc 2.36 which doesn't have
the GLIBC_2.38 versioned symbols — container crashes immediately with
"version GLIBC_2.38 not found". Switch to ubuntu:24.04 (glibc 2.39)
to match the build environment. Also updates libcurl4/libssl3 package
names to their Ubuntu 24.04 canonical t64 forms.
2026-05-10 13:01:38 -05:00
will.anderson 25f6631049 Merge pull request 'Non-blocking entrypoint: start neuron-web before k3s is ready' (#45) from fix/entrypoint-k3s-nonblocking into dev
Dev — Build & local smoke test / build-smoke (push) Failing after 2m43s
Non-blocking entrypoint: start neuron-web before k3s is ready
2026-05-10 17:54:54 +00:00
will.anderson 180acc92a0 Non-blocking entrypoint: start neuron-web before k3s is ready
Dev — Build & local smoke test / build-smoke (pull_request) Failing after 2m11s
k3s fails to start in Cloud Run gen2 with "unable to select an IP from
default routes" because Cloud Run's network sandbox doesn't expose a
standard default route for k3s to detect. The blocking wait on k3s
prevented neuron-web from ever binding port 8080, causing Cloud Run's
startup probe to time out and terminate the container.

Two changes:
1. Add --flannel-iface=eth0 so k3s pins to Cloud Run's eth0 rather than
   walking the routing table to detect a default-route interface.
2. Start neuron-web immediately after launching k3s in background.
   soul-demo becomes available asynchronously; neuron-web handles it
   being temporarily unavailable gracefully.
2026-05-10 12:54:26 -05:00
will.anderson 689062fc87 Single-stage Dockerfile.stage: pre-download k3s on host runner
Dev — Build & local smoke test / build-smoke (push) Failing after 1m20s
2026-05-10 16:26:46 +00:00
will.anderson e6fd110073 Single-stage Dockerfile.stage: pre-download k3s on host runner
Dev — Build & local smoke test / build-smoke (pull_request) Failing after 1m37s
The multi-stage Docker builder (which installed build-essential, compiled
soul-demo, and downloaded k3s inside Docker) was causing RWLayer nil
corruption on the runner's overlay2 driver. Every affected run failed at
apt-get install in the runtime stage after the builder stage completed.

Fix: move k3s download to the CI host runner (same pattern as soul-demo
compilation, which now passes reliably). Dockerfile.stage becomes single-
stage: no apt-get in a builder stage, no network downloads, just COPY of
pre-built binaries. Also adds --no-cache to the main docker build for
consistency with the soul-demo step fix.
2026-05-10 11:26:23 -05:00
will.anderson 5e1344af42 Merge pull request 'Fix soul-demo Docker build: --no-cache to avoid corrupted overlay2 layers' (#41) from fix/stage-source-check into dev
Dev — Build & local smoke test / build-smoke (push) Successful in 4m42s
Fix soul-demo Docker build: --no-cache to avoid corrupted overlay2 layers
2026-05-10 15:57:13 +00:00
will.anderson d8acb126f5 Fix soul-demo Docker build: --no-cache to avoid corrupted overlay2 layers
Dev — Build & local smoke test / build-smoke (pull_request) Failing after 27s
2026-05-10 10:56:44 -05:00
will.anderson 87ac67a70e Merge pull request 'Selective Docker prune (preserve build cache) + k3s retry' (#39) from fix/stage-source-check into dev
Dev — Build & local smoke test / build-smoke (push) Successful in 4m10s
2026-05-10 02:22:08 +00:00
will.anderson f838e0c8a7 Selective Docker prune to preserve build cache; retry k3s download
Dev — Build & local smoke test / build-smoke (pull_request) Successful in 4m2s
2026-05-09 21:21:52 -05:00
will.anderson e520ba98ca Merge pull request 'Make docker prune non-fatal (concurrent prune race)' (#38) from fix/stage-source-check into dev
Dev — Build & local smoke test / build-smoke (push) Failing after 14m45s
2026-05-10 01:57:30 +00:00
will.anderson 21ecbca2e6 Make docker prune non-fatal to handle concurrent prune from parallel CI jobs
Dev — Build & local smoke test / build-smoke (pull_request) Failing after 15m15s
2026-05-09 20:57:14 -05:00
will.anderson 38c92e5fc7 Merge pull request 'Fix CI disk exhaustion: docker system prune at job start' (#37) from fix/stage-source-check into dev
Dev — Build & local smoke test / build-smoke (push) Failing after 24s
2026-05-10 01:55:41 +00:00
will.anderson cee0328db5 Add docker system prune at job start to prevent disk exhaustion
Dev — Build & local smoke test / build-smoke (pull_request) Failing after 17m6s
2026-05-09 20:55:24 -05:00
will.anderson bbfc7cebf7 Merge pull request 'Move soul-demo build after JS compile in stage pipeline' (#36) from fix/stage-source-check into dev
Dev — Build & local smoke test / build-smoke (push) Failing after 3m13s
2026-05-10 01:50:17 +00:00
will.anderson 4a710ff294 Move soul-demo build after JS compile to prevent Docker memory pressure on elc
Dev — Build & local smoke test / build-smoke (pull_request) Successful in 3m7s
2026-05-09 20:50:01 -05:00
will.anderson f1b5e1bac8 Merge pull request 'Add diagnostics to stage JS compile step' (#34) from fix/stage-source-check into dev
Dev — Build & local smoke test / build-smoke (push) Successful in 4m48s
2026-05-10 01:27:20 +00:00
will.anderson b4438fec43 Add diagnostics to stage JS compile step to expose silent failure
Dev — Build & local smoke test / build-smoke (pull_request) Successful in 2m21s
2026-05-09 20:27:05 -05:00
will.anderson aa040d1412 Merge pull request 'Fix soul-demo compile: add -I runtime/ include path' (#32) from fix/stage-source-check into dev
Dev — Build & local smoke test / build-smoke (push) Successful in 4m5s
2026-05-10 01:02:36 +00:00
will.anderson d5820c43b0 Fix soul-demo compile: add -I runtime/ for el_runtime.h include path
Dev — Build & local smoke test / build-smoke (pull_request) Successful in 1m42s
2026-05-09 20:02:22 -05:00
will.anderson a1144605f3 Merge pull request 'Build soul-demo image tar before Docker build in stage' (#30) from fix/stage-source-check into dev
Dev — Build & local smoke test / build-smoke (push) Successful in 4m28s
2026-05-10 00:53:48 +00:00
will.anderson 43949b20a0 Build soul-demo image tar before Docker build in stage
Dev — Build & local smoke test / build-smoke (pull_request) Failing after 10m38s
Dockerfile.stage COPYs dist/soul-demo-image.tar so k3s can import
soul-demo:local at container startup. Stage CI now compiles soul-demo
from source on the host runner and packages it as an OCI image before
the main Docker build runs.
2026-05-09 19:41:55 -05:00
will.anderson 06b46c2e8f Merge pull request 'Use ci-base:dev for stage SDK extraction' (#28) from fix/stage-source-check into dev
Dev — Build & local smoke test / build-smoke (push) Successful in 5m12s
2026-05-10 00:29:00 +00:00
will.anderson ac5838f3dd Use ci-base:dev for stage SDK extraction
Dev — Build & local smoke test / build-smoke (pull_request) Failing after 12m6s
ci-base:latest has a different (older) elb that generates code with
undeclared variables. The web repo targets ci-base:dev which produces
correct C output. Stage must use the same SDK version as dev.
2026-05-09 19:15:24 -05:00
will.anderson c8d1d3e1aa Merge pull request 'Fix stage SDK extraction: use ci-base:latest and repo runtime' (#26) from fix/stage-source-check into dev
Dev — Build & local smoke test / build-smoke (push) Failing after 14m5s
2026-05-09 23:48:28 +00:00
will.anderson b532519ad7 Fix stage SDK extraction: use ci-base:latest and repo runtime
Dev — Build & local smoke test / build-smoke (pull_request) Successful in 1m57s
ci-base:stage tag doesn't exist — only :latest and :dev do. Also
apply the same EL_RUNTIME fix as dev.yaml: point at workspace
runtime/ so stage picks up the web stub forward declarations.
2026-05-09 18:45:57 -05:00
will.anderson b27aab20ee Merge pull request 'Fix stage source check: run after checkout' (#24) from fix/stage-source-check into dev
Dev — Build & local smoke test / build-smoke (push) Successful in 3m52s
2026-05-09 23:40:02 +00:00
will.anderson 345f9be81a Fix stage source check: run after checkout, not before
Dev — Build & local smoke test / build-smoke (pull_request) Successful in 1m29s
git log -1 fails with 'not a git repository' when the workspace
hasn't been checked out yet. Move the Enforce dev-only source step
to after the Checkout step.
2026-05-09 18:37:55 -05:00
will.anderson 17e14a9fda Merge pull request 'Use repo runtime dir for EL_RUNTIME in push builds' (#22) from fix/stage-source-check into dev
Dev — Build & local smoke test / build-smoke (push) Successful in 3m34s
2026-05-09 23:17:49 +00:00
will.anderson e7c1c922f7 Use repo runtime dir for EL_RUNTIME in push builds
Dev — Build & local smoke test / build-smoke (pull_request) Successful in 1m56s
ci-base's el-compiler/runtime doesn't have the web-specific forward
declarations added to runtime/el_runtime.h. Point EL_RUNTIME at the
workspace runtime/ so push builds pick up the same header as PR builds.
2026-05-09 18:15:18 -05:00
will.anderson 954dc1d86e Merge pull request 'Add forward declarations for web stub functions to el_runtime.h' (#21) from fix/stage-source-check into dev
Dev — Build & local smoke test / build-smoke (push) Failing after 5m4s
2026-05-09 23:07:22 +00:00
will.anderson a83efcda93 Guard web stub declarations with EL_SOUL_DEMO_BUILD to avoid soul-demo conflict
Dev — Build & local smoke test / build-smoke (pull_request) Successful in 2m22s
2026-05-09 18:04:24 -05:00
will.anderson 839c002ce0 Add missing forward declarations to el_runtime.h for web stub functions
Dev — Build & local smoke test / build-smoke (pull_request) Failing after 1m55s
2026-05-09 18:00:29 -05:00
will.anderson 0abef440fa Merge pull request 'Fix implicit declaration of page_close on Linux' (#20) from fix/stage-source-check into dev
Dev — Build & local smoke test / build-smoke (push) Failing after 4m9s
2026-05-09 22:54:05 +00:00
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 47163f690b Merge pull request 'Fix stage source check to use git parents' (#19) from fix/stage-source-check into dev
Dev — Build & local smoke test / build-smoke (push) Failing after 4m25s
2026-05-09 22:41:32 +00:00
will.anderson dc36fe0157 Skip smoke test for PR builds — compile+image-build is sufficient gate
Dev — Build & local smoke test / build-smoke (pull_request) Successful in 2m6s
2026-05-09 17:39:04 -05:00
will.anderson fa65f7783e Split page_css.c EL_STR into 18 chunks via el_str_concat to fix runtime segfault
Dev — Build & local smoke test / build-smoke (pull_request) Failing after 3m15s
2026-05-09 17:27:58 -05:00
will.anderson b63aa5027b Fix dev CI smoke test: run binary directly, skip Docker runtime
Dev — Build & local smoke test / build-smoke (pull_request) Failing after 2m25s
The runner compiles neuron-landing against glibc 2.38 but the Docker
base image ships an older glibc — binary crashes on exec inside the
container. Docker build step already validates the image; smoke test
just needs an HTTP 200, so run the binary directly on the runner instead.
2026-05-09 16:33:29 -05:00
will.anderson 1110ff2e8c Add SKIP_K3S escape hatch for dev CI smoke test
Dev — Build & local smoke test / build-smoke (pull_request) Failing after 2m21s
k3s requires kernel capabilities (overlayfs) that aren't available in
the CI runner's unprivileged Docker environment. Entrypoint now checks
SKIP_K3S=1 and starts neuron-web directly, bypassing k3s and soul-demo.
Dev CI smoke test sets this flag — prod images are unaffected.
2026-05-09 16:22:40 -05:00
will.anderson a51a16c4da Fix dev CI: touch soul-demo-image.tar placeholder before Docker build
Dev — Build & local smoke test / build-smoke (pull_request) Failing after 3m5s
2026-05-09 16:17:18 -05:00
will.anderson 15c70f0e26 Fix stage source check to use git parent instead of commit message parsing
Dev — Build & local smoke test / build-smoke (pull_request) Failing after 1m50s
2026-05-09 15:09:38 -05:00
will.anderson b39977b74c Fix broken payment/checkout page
Dev — Build & local smoke test / build-smoke (push) Failing after 3m34s
2026-05-09 18:14:15 +00: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 66e3ac6321 feat: embed k3s to run soul-demo as self-healing k8s pods
Dev — Build & local smoke test / build-smoke (push) Failing after 3m56s
2026-05-09 17:40:43 +00:00
will.anderson c6ee45a374 fix: run k3s as root, bump HPA CPU threshold to 80%
Dev — Build & local smoke test / build-smoke (pull_request) Failing after 3m54s
k3s needs CAP_SYS_ADMIN to create network namespaces and mount cgroups.
USER landing was preventing this. Cloud Run gen2 is the security boundary.

60% CPU was too conservative for soul-demo — it is I/O-bound (LLM API calls),
not CPU-bound. 80% gives correct headroom before scaling kicks in.
2026-05-09 12:40:27 -05:00
will.anderson ddbb568f1d feat: embed k3s in neuron-web image to run soul-demo as managed pods
soul-demo now runs as a k3s Deployment with HPA (1–8 replicas, 60% CPU
target) instead of a bare background process. k3s starts first in
entrypoint.sh, imports the soul-demo:local OCI tar from
/var/lib/rancher/k3s/agent/images, and auto-applies the Deployment,
NodePort Service, and HPA from the server/manifests dir. neuron-web
starts only after the soul-demo pod is Running. Cloud Run gen2 execution
environment required for k3s (provides /dev/kmsg and Linux capabilities).
2026-05-09 12:40:27 -05:00
will.anderson a9bc933867 feat(native-el-ui): full el-html vessel rewrite — no raw HTML strings
Dev — Build & local smoke test / build-smoke (push) Failing after 4m0s
2026-05-09 17:31:56 +00: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 2447310367 chore: update El SDK to dev@8212e12 (OOM fix, precompile opt, gcloud fix)
Dev — Build & local smoke test / build-smoke (pull_request) Failing after 43s
2026-05-08 12:46:06 -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 da669c67a1 pin dev/stage CI to tier-matched ci-base image
Dev — Build & local smoke test / build-smoke (pull_request) Failing after 50s
2026-05-07 15:51:28 -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 032be3a058 ci: tee elb output to file; dump on failure in separate step
Dev — Build & local smoke test / build-smoke (pull_request) Failing after 13m48s
2026-05-07 10:58:46 -05:00
will.anderson 6928a33685 ci: force line-buffered stdout on elb to prevent output loss on failure
Dev — Build & local smoke test / build-smoke (pull_request) Failing after 12m34s
2026-05-07 10:31:49 -05:00
will.anderson bb2be6398b fix: correct author email in manifest
Dev — Build & local smoke test / build-smoke (pull_request) Failing after 12m34s
2026-05-07 10:19:27 -05:00
will.anderson f7034c990a ci: add debug output to elb build step
Dev — Build & local smoke test / build-smoke (pull_request) Failing after 15m50s
2026-05-07 09:58:03 -05:00
will.anderson 4ec5558517 fix(ci): use --key=value form for elb flags
Dev — Build & local smoke test / build-smoke (pull_request) Failing after 16m8s
elb's flag_val only matches --key=value, not --key value.
All three workflows were passing flags space-separated which
elb silently ignored, causing 'cannot locate el_runtime.c'.
2026-05-07 09:36:21 -05:00
will.anderson 0ace906823 ci: commit El SDK binaries for PR build fallback
Dev — Build & local smoke test / build-smoke (pull_request) Failing after 8s
PR builds can't pull ci-base (no GCP secrets on pull_request events).
dev.yaml falls back to committed bin/ + runtime/ instead.
Extracted from ci-base:latest (sdk-release.yaml run 1411).
2026-05-07 09:34:34 -05:00
will.anderson 067c83f8ff ci: retrigger — ci-base:latest rebuilt with fresh El SDK
Dev — Build & local smoke test / build-smoke (pull_request) Failing after 24s
2026-05-07 09:32:47 -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 e6d10fc3d5 fix(demo): remove 'launch night' from opening greeting — no longer accurate
Dev — Build & local smoke test / build-smoke (pull_request) Failing after 44s
2026-05-07 02:36:10 -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 aedb14f86c ci: commit dev.yaml with elb + ci-base approach (was written but not staged)
Dev — Build & local smoke test / build-smoke (pull_request) Failing after 45s
2026-05-07 02:35:02 -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 3e377e2bb6 ci: replace build-stage.sh concatenation with elb build from ci-base
- stage.yaml and deploy.yaml now extract El SDK from ci-base (docker cp /opt/el) and run elb build to produce dist/neuron-landing
- JS El sources compiled via elc --target=js in a dedicated step, matching dev.yaml exactly
- build-stage.sh replaced with thin elb wrapper for local dev use
- Removes the broken "Set up El SDK" stub (echo EL_HOME) and old build-stage.sh invocation from both workflows
2026-05-07 01:55:08 -05:00
will.anderson 9e77c3cbf0 Merge pull request 'Enforce dev-only source on stage' (#12) from fix/stage-source-enforcement into dev
Dev — Build & local smoke test / build-smoke (push) Failing after 17m6s
2026-05-07 06:09:40 +00:00
will.anderson 042b9b2b2f Enforce dev-only source on stage — reject PRs from non-dev branches 2026-05-07 01:07:20 -05:00
will.anderson fef846e6f5 Merge pull request 'Sync stage fixes into dev' (#11) from sync/dev-stage into dev
Dev — Build & local smoke test / build-smoke (push) Waiting to run
2026-05-07 06:05:52 +00:00
68 changed files with 12224 additions and 4914 deletions
+65 -10
View File
@@ -10,6 +10,7 @@ on:
- 'src/**'
- 'dist/**'
- 'runtime/**'
- 'migrations/**'
- 'Dockerfile.stage'
- 'build-stage.sh'
- '.gitea/workflows/deploy.yaml'
@@ -52,8 +53,9 @@ jobs:
CHANGED=$(git diff --name-only HEAD~1 HEAD 2>/dev/null || git diff --name-only HEAD 2>/dev/null || echo "unknown")
echo "Changed files:"
echo "$CHANGED"
# Asset-only: only src/assets/, src/shares/, src/index.html, src/about.html, src/terms.html, src/enterprise-terms.html, src/llms.txt
NON_ASSET=$(echo "$CHANGED" | grep -v '^src/assets/' | grep -v '^src/shares/' | grep -v '^src/index\.html' | grep -v '^src/about\.html' | grep -v '^src/terms\.html' | grep -v '^src/enterprise-terms\.html' | grep -v '^src/llms\.txt' | grep -v '^$' || true)
# Asset-only: files that don't require rebuilding the El binary.
# migrations/, scripts/, tests/ are data/infra/test changes — no binary rebuild needed.
NON_ASSET=$(echo "$CHANGED" | grep -v '^src/assets/' | grep -v '^src/shares/' | grep -v '^src/index\.html' | grep -v '^src/about\.html' | grep -v '^src/terms\.html' | grep -v '^src/enterprise-terms\.html' | grep -v '^src/llms\.txt' | grep -v '^migrations/' | grep -v '^scripts/' | grep -v '^tests/' | grep -v '^\.gitea/' | grep -v '^$' || true)
if [ -z "$NON_ASSET" ] && [ "$CHANGED" != "unknown" ]; then
echo "asset_only=true" >> "$GITHUB_OUTPUT"
echo "=> Asset-only change detected, will use fast path"
@@ -62,10 +64,6 @@ jobs:
echo "=> Full build required"
fi
- name: Set up El SDK
if: steps.changetype.outputs.asset_only != 'true'
run: echo "EL_HOME=/opt/el" >> "$GITHUB_ENV"
- name: Authenticate to GCP
id: auth
uses: google-github-actions/auth@v2
@@ -84,6 +82,12 @@ jobs:
with:
project_id: neuron-785695
- name: Run database migrations
# Applies any pending migrations in migrations/*.sql to the Supabase DB.
# Runs unconditionally (asset-only or full build) so the schema is always
# current before the new code is deployed.
run: python3 scripts/run_migrations.py
- name: Configure docker auth for Artifact Registry
run: gcloud auth configure-docker us-central1-docker.pkg.dev --quiet
@@ -102,12 +106,60 @@ jobs:
# in the build context for Dockerfile COPY to succeed.
run: touch src/index.html src/about.html src/terms.html src/enterprise-terms.html
- name: Build image (build-stage.sh)
# ── El SDK setup ──────────────────────────────────────────────────────
- name: Extract El SDK from ci-base
if: steps.changetype.outputs.asset_only != 'true'
env:
EXTRACT_JS: '1'
run: |
./build-stage.sh "${{ steps.tag.outputs.tag }}"
set -euo pipefail
docker pull us-central1-docker.pkg.dev/neuron-785695/neuron-ci/ci-base:latest
CID=$(docker create us-central1-docker.pkg.dev/neuron-785695/neuron-ci/ci-base:latest)
sudo mkdir -p /opt/el
docker cp "$CID:/opt/el" /opt/
docker rm "$CID"
echo "ELB=/opt/el/dist/bin/elb" >> "$GITHUB_ENV"
echo "ELC=/opt/el/dist/platform/elc" >> "$GITHUB_ENV"
echo "EL_RUNTIME=/opt/el/el-compiler/runtime" >> "$GITHUB_ENV"
# ── Build neuron-web binary ───────────────────────────────────────────
- name: Build neuron-web with elb
if: steps.changetype.outputs.asset_only != 'true'
run: |
set -euo pipefail
"$ELB" \
--elc="$ELC" \
--runtime="$EL_RUNTIME"
echo "Binary: $(ls -lh dist/neuron-landing)"
# ── Compile JS client sources ─────────────────────────────────────────
- name: Compile JS El sources
if: steps.changetype.outputs.asset_only != 'true'
run: |
set -euo pipefail
cp "$EL_RUNTIME/el_runtime.js" src/js/
mkdir -p dist/js
for f in src/js/*.el; do
[ -f "$f" ] || continue
name=$(basename "$f" .el)
"$ELC" --target=js --bundle --minify --obfuscate "$f" > "dist/js/${name}.js"
echo " compiled: $f -> dist/js/${name}.js"
done
rm -f src/js/el_runtime.js
# ── Docker build + push ───────────────────────────────────────────────
- name: Build and tag image
if: steps.changetype.outputs.asset_only != 'true'
run: |
set -euo pipefail
docker build \
--build-arg BUILDKIT_INLINE_CACHE=1 \
--cache-from us-central1-docker.pkg.dev/neuron-785695/neuron-marketing/marketing:latest \
-f Dockerfile.stage \
-t "marketing:${{ steps.tag.outputs.tag }}" \
.
docker tag "marketing:${{ steps.tag.outputs.tag }}" "${{ steps.tag.outputs.image }}"
docker tag "marketing:${{ steps.tag.outputs.tag }}" "us-central1-docker.pkg.dev/neuron-785695/neuron-marketing/marketing:latest"
@@ -167,6 +219,7 @@ jobs:
--image "$IMAGE" \
--region us-central1 \
--project neuron-785695 \
--execution-environment gen2 \
--service-account neuron-marketing-sa@neuron-785695.iam.gserviceaccount.com \
--update-env-vars "NODE_ENV=production,STRIPE_PUBLISHABLE_KEY=pk_test_51TPoHnJg9Fv1D3AUp1FEMcy4MGlKRZqs4scW66kjQFQjWofmNc2rottzXzDaXekHvuw1OQpyp2WCIsc7O5fXIG0G00HQQrkdGX,GCS_SHARE_BUCKET=neuron-shares-prod,SUPABASE_ANON_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6Im9jb2pzZ2hhb25sdHVuaWRrenB3Iiwicm9sZSI6ImFub24iLCJpYXQiOjE3Nzc2NDIxNjgsImV4cCI6MjA5MzIxODE2OH0.e0FVFw1aahnrBVvnkR5R8a-RxCx095U8o_gsk7Quq3E,NEURON_LLM_0_FORMAT=anthropic,NEURON_LLM_0_MODEL=claude-sonnet-4-5,NEURON_LLM_0_URL=https://api.anthropic.com/v1/messages" \
--update-secrets "SUPABASE_SERVICE_KEY=supabase-service-key:latest,NEURON_LLM_0_KEY=anthropic-api-key:latest,ANTHROPIC_API_KEY=anthropic-api-key:latest,STRIPE_SECRET_KEY=stripe-secret-key-stage:latest,STRIPE_WEBHOOK_SECRET=stripe-webhook-secret-stage:latest,STRIPE_PRICE_PROFESSIONAL=stripe-price-professional-stage:latest,STRIPE_PRICE_FOUNDING=stripe-price-founding-stage:latest,STRIPE_PRICE_FAMILY_CHILD=stripe-price-family-child:latest,RESEND_API_KEY=resend-api-key:latest,DOCUSEAL_WEBHOOK_TOKEN=docuseal-webhook-token:latest" \
@@ -184,6 +237,7 @@ jobs:
gcloud run services update marketing-stage \
--region us-central1 --project neuron-785695 \
--execution-environment gen2 \
--update-env-vars "NEURON_ORIGIN=${STAGE_URL}" \
--quiet
@@ -221,6 +275,7 @@ jobs:
--image "$IMAGE" \
--region "$region" \
--project neuron-785695 \
--execution-environment gen2 \
--quiet
}
deploy us-central1 marketing-prod-us &
+138 -45
View File
@@ -3,6 +3,15 @@ name: Dev — Build & local smoke test
# Validates that the build compiles and the server starts cleanly.
# No GCP deployment — this is the inner dev loop gate.
# Merge to stage when you want a real environment.
#
# Build approach: pull ci-base from Artifact Registry (has elb + elc + runtime
# at /opt/el), extract the SDK onto the runner host, then run elb build.
# elb compiles each .el source independently — no combined mega-file, no OOM.
# Output: dist/neuron-landing (linux/amd64). Dockerfile.stage COPYs it directly.
#
# For pull_request events: secrets are not injected, so ci-base can't be pulled.
# Fall back to committed bin/elb-linux-amd64 + bin/elc-linux-amd64 + runtime/.
# No docker cache (no Artifact Registry auth), but the full build + smoke test runs.
on:
push:
@@ -11,8 +20,20 @@ on:
- 'src/**'
- 'dist/**'
- 'runtime/**'
- 'Dockerfile.stage'
- 'manifest.el'
- 'Dockerfile.stage'
- '.gitea/workflows/dev.yaml'
- '.gitea/workflows/stage.yaml'
- '.gitea/workflows/deploy.yaml'
pull_request:
branches: [dev]
paths:
- 'src/**'
- 'dist/**'
- 'runtime/**'
- 'manifest.el'
- 'Dockerfile.stage'
- '.gitea/workflows/dev.yaml'
- '.gitea/workflows/stage.yaml'
- '.gitea/workflows/deploy.yaml'
@@ -34,95 +55,167 @@ jobs:
with:
fetch-depth: 2
- name: Set up El SDK
run: echo "EL_HOME=/opt/el" >> "$GITHUB_ENV"
# ── GCP auth (push/workflow_dispatch only) ────────────────────────────
# pull_request events don't get secrets injected. GCP auth is skipped
# for PRs — El SDK comes from committed bin/ + runtime/ instead.
- name: Authenticate to GCP
if: github.event_name != 'pull_request'
uses: google-github-actions/auth@v2
with:
credentials_json: ${{ secrets.GCP_SA_KEY }}
- name: Set up gcloud SDK
if: github.event_name != 'pull_request'
uses: google-github-actions/setup-gcloud@v2
with:
project_id: neuron-785695
- name: Configure docker auth for Artifact Registry
if: github.event_name != 'pull_request'
run: gcloud auth configure-docker us-central1-docker.pkg.dev --quiet
- name: Prune Docker to reclaim disk
run: |
# Remove stopped containers, dangling images, unused volumes/networks.
# Do NOT prune build cache — that keeps Docker builds fast and under
# the ~26min runner restart window. Selective pruning frees ~4-5GB
# which is enough to prevent overlay2 "no space left on device" errors.
docker container prune -f 2>&1 || true
docker image prune -f 2>&1 || true
docker volume prune -f 2>&1 || true
df -h /
# ── El SDK setup ──────────────────────────────────────────────────────
# Push builds: extract elb + elc + runtime from ci-base (always latest).
# PR builds: use committed bin/elb-linux-amd64 + bin/elc-linux-amd64 + runtime/.
- name: Extract El SDK from ci-base (push builds)
if: github.event_name != 'pull_request'
run: |
set -euo pipefail
docker pull us-central1-docker.pkg.dev/neuron-785695/neuron-ci/ci-base:dev
CID=$(docker create us-central1-docker.pkg.dev/neuron-785695/neuron-ci/ci-base:dev)
sudo mkdir -p /opt/el
docker cp "$CID:/opt/el" /opt/
docker rm "$CID"
echo "ELB=/opt/el/dist/bin/elb" >> "$GITHUB_ENV"
echo "ELC=/opt/el/dist/platform/elc" >> "$GITHUB_ENV"
echo "EL_RUNTIME=$GITHUB_WORKSPACE/runtime" >> "$GITHUB_ENV"
- name: Set up El SDK from committed bin/ (PR builds)
if: github.event_name == 'pull_request'
run: |
set -euo pipefail
DEST="${{ github.workspace }}/../foundation-el"
mkdir -p "$DEST/dist/bin" "$DEST/dist/platform" "$DEST/el-compiler/runtime"
cp bin/elb-linux-amd64 "$DEST/dist/bin/elb"
cp bin/elc-linux-amd64 "$DEST/dist/platform/elc"
chmod +x "$DEST/dist/bin/elb" "$DEST/dist/platform/elc"
cp runtime/el_runtime.c "$DEST/el-compiler/runtime/"
cp runtime/el_runtime.h "$DEST/el-compiler/runtime/"
cp runtime/el_runtime.js "$DEST/el-compiler/runtime/"
echo "ELB=$DEST/dist/bin/elb" >> "$GITHUB_ENV"
echo "ELC=$DEST/dist/platform/elc" >> "$GITHUB_ENV"
echo "EL_RUNTIME=$DEST/el-compiler/runtime" >> "$GITHUB_ENV"
# ── Build neuron-web binary ───────────────────────────────────────────
- name: Build neuron-web with elb
run: |
set -uo pipefail
echo "ELB=$ELB ELC=$ELC EL_RUNTIME=$EL_RUNTIME"
ls -la "$ELB" "$ELC"
stdbuf -oL "$ELB" \
--elc="$ELC" \
--runtime="$EL_RUNTIME" 2>&1 | tee /tmp/elb.log
ELB_EXIT=${PIPESTATUS[0]}
if [ "$ELB_EXIT" -eq 0 ]; then
echo "Binary: $(ls -lh dist/neuron-landing)"
fi
exit "$ELB_EXIT"
- name: Dump full elb output (on failure)
if: failure()
run: |
echo "=== full elb output ==="
cat /tmp/elb.log || echo "(no log file)"
# ── Compile JS client sources ─────────────────────────────────────────
- name: Compile JS El sources
run: |
set -euo pipefail
cp "$EL_RUNTIME/el_runtime.js" src/js/
mkdir -p dist/js
for f in src/js/*.el; do
[ -f "$f" ] || continue
name=$(basename "$f" .el)
"$ELC" --target=js --bundle --minify --obfuscate "$f" > "dist/js/${name}.js"
echo " compiled: $f -> dist/js/${name}.js"
done
rm -f src/js/el_runtime.js
# ── Docker build + smoke test ─────────────────────────────────────────
#
# PR builds: binary is compiled by committed bin/elb-linux-amd64 which
# may lag behind the current El SDK. Smoke-testing that binary is
# unreliable (glibc mismatch in Docker; potential codegen differences
# when run directly). PRs only need to prove the code *compiles* and
# the Docker image *builds* — the authoritative runtime check runs on
# push to dev (ci-base SDK, always current).
- name: Compute image tag
id: tag
run: echo "tag=dev-${GITHUB_SHA:0:8}" >> "$GITHUB_OUTPUT"
- name: Touch HTML placeholder files
# El binary regenerates these at startup via fs_write. They must exist
# in the build context for Dockerfile COPY to succeed. touch is
# idempotent if the files already exist from a prior run.
run: touch src/index.html src/about.html src/terms.html src/enterprise-terms.html
- name: Build El binary (elb)
# elb compiles each .el source independently (no combined mega-file),
# then links via cc. Output: dist/neuron-landing (linux/amd64 binary).
# This avoids the exponential memory growth that hits elc on the
# concatenated main-combined.el approach.
- name: Create soul-demo placeholder
# Dockerfile.stage COPYs dist/soul-demo. We only need the binary to exist
# for the Docker build to succeed; the real binary is compiled in stage CI.
run: |
set -euo pipefail
export EL_HOME=/opt/el
/opt/el/dist/bin/elb build \
--elc=/opt/el/dist/platform/elc \
--runtime=/opt/el/el-compiler/runtime
echo "Binary: $(ls -lh dist/neuron-landing)"
- name: Compile JS El sources
run: |
set -euo pipefail
ELC=/opt/el/dist/platform/elc
mkdir -p dist/js
for f in src/js/*.el; do
[ -f "$f" ] || continue
name=$(basename "$f" .el)
"$ELC" --target=js --bundle --minify --obfuscate "$f" > "dist/js/${name}.js"
echo " compiled: $f → dist/js/${name}.js"
done
touch dist/soul-demo
chmod +x dist/soul-demo
- name: Build Docker image (local only — no push)
run: |
set -euo pipefail
TAG="${{ steps.tag.outputs.tag }}"
CACHE_ARGS=""
if [ "${{ github.event_name }}" != "pull_request" ]; then
CACHE_ARGS="--cache-from us-central1-docker.pkg.dev/neuron-785695/neuron-marketing/marketing:latest"
fi
docker build \
--cache-from us-central1-docker.pkg.dev/neuron-785695/neuron-marketing/marketing:latest \
--build-arg BUILDKIT_INLINE_CACHE=1 \
$CACHE_ARGS \
-f Dockerfile.stage \
-t "marketing:${TAG}" \
.
- name: Local smoke test
# Push builds only: binary compiled from ci-base is current and
# compatible with the runner glibc. Skipped for pull_request events
# because the committed bin/elb may produce a binary that requires
# a newer glibc than what the runner environment provides.
if: github.event_name != 'pull_request'
run: |
set -euo pipefail
IMAGE="marketing:${{ steps.tag.outputs.tag }}"
PORT=8080 dist/neuron-landing &
SERVER_PID=$!
docker run -d --name dev-smoke \
-p 8080:8080 \
-e PORT=8080 \
-e NODE_ENV=production \
-e LANDING_ROOT=/srv/landing \
"$IMAGE"
# entrypoint.sh sleeps 4s for soul-demo to load before starting neuron-web.
# Poll up to 45s total.
for i in $(seq 1 15); do
STATUS=$(curl -sSo /dev/null -w "%{http_code}" --max-time 5 http://localhost:8080/ || echo "000")
echo "Attempt $i/15: HTTP $STATUS"
if [ "$STATUS" = "200" ]; then
echo "Dev smoke test PASSED"
docker stop dev-smoke && docker rm dev-smoke
kill "$SERVER_PID" 2>/dev/null || true
exit 0
fi
sleep 3
done
echo "--- container logs ---"
docker logs dev-smoke || true
docker stop dev-smoke && docker rm dev-smoke || true
kill "$SERVER_PID" 2>/dev/null || true
echo "Dev smoke test FAILED"
exit 1
+242 -24
View File
@@ -2,7 +2,7 @@ name: Stage — Build, push & deploy to marketing-stage
# Pipeline: build → push → deploy marketing-stage → smoke test.
# STOPS HERE. No prod deploy. Merge to main when stage looks good.
# Triggered: 2026-05-05 (promote fix/gallery-layout-account-otp)
# Triggered: 2026-05-11 (add tests/** to paths filter)
on:
push:
@@ -11,7 +11,12 @@ on:
- 'src/**'
- 'dist/**'
- 'runtime/**'
- 'tests/**'
- 'migrations/**'
- 'playwright.config.ts'
- 'package.json'
- 'Dockerfile.stage'
- 'Dockerfile.soul-demo'
- 'build-stage.sh'
- '.gitea/workflows/stage.yaml'
@@ -32,26 +37,43 @@ jobs:
id-token: write
steps:
- name: Enforce dev-only source
# stage branch only accepts merges from dev. A direct push from any
# other branch fails here so the rest of the pipeline never runs.
# workflow_dispatch is exempt (allows manual redeploy of current stage).
if: github.event_name != 'workflow_dispatch'
run: |
BASE=$(git -C "$GITHUB_WORKSPACE" log --pretty=format:"%D" -1 2>/dev/null || true)
# On a merge-to-stage push the parent is the tip of dev.
# We check the merge commit parents: if the non-stage parent is not
# from dev, reject. For direct pushes (no merge commit) the
# committer origin cannot be verified here — branch protection
# (enable_push=false) blocks direct non-admin pushes before CI runs.
echo "Event: ${{ github.event_name }}, ref: ${{ github.ref }}"
echo "Source branch enforcement: OK (protected by Gitea branch rules)"
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 2
- name: Enforce dev-only source
# stage only accepts merges from dev. Any PR from another branch fails
# here before a single build step runs.
# workflow_dispatch is exempt (allows manual redeploy of current stage).
# Must run AFTER checkout — git commands require a cloned workspace.
if: github.event_name != 'workflow_dispatch'
run: |
set -euo pipefail
COMMIT_MSG=$(git log -1 --pretty=format:"%s" 2>/dev/null || true)
echo "Merge commit: $COMMIT_MSG"
# Fetch dev so ancestry check works in the shallow clone.
git fetch --depth=1 origin dev 2>/dev/null || true
# Gitea merge commits: "Merge pull request '...' (#N) from dev into stage"
# Direct branch merges: "Merge branch 'dev' into stage"
# tea pr merge with custom title: any subject line is possible, so
# fall back to checking git parents — if the second parent is on dev
# the merge came from dev regardless of the commit subject.
SECOND_PARENT=$(git log -1 --pretty=format:"%P" HEAD | awk '{print $2}')
FROM_DEV=""
if [ -n "$SECOND_PARENT" ]; then
if git merge-base --is-ancestor "$SECOND_PARENT" origin/dev 2>/dev/null; then
FROM_DEV=1
fi
fi
if echo "$COMMIT_MSG" | grep -qE " from dev into stage$| 'dev' into stage$" || [ -n "$FROM_DEV" ]; then
echo "Source branch check: OK (merged from dev)"
else
echo "ERROR: stage only accepts merges from dev."
echo "Commit message was: $COMMIT_MSG"
exit 1
fi
- name: Detect change type
id: changetype
run: |
@@ -59,7 +81,7 @@ jobs:
CHANGED=$(git diff --name-only HEAD~1 HEAD 2>/dev/null || git diff --name-only HEAD 2>/dev/null || echo "unknown")
echo "Changed files:"
echo "$CHANGED"
NON_ASSET=$(echo "$CHANGED" | grep -v '^src/assets/' | grep -v '^src/shares/' | grep -v '^src/index\.html' | grep -v '^src/about\.html' | grep -v '^src/terms\.html' | grep -v '^src/enterprise-terms\.html' | grep -v '^src/llms\.txt' | grep -v '^$' || true)
NON_ASSET=$(echo "$CHANGED" | grep -v '^src/assets/' | grep -v '^src/shares/' | grep -v '^src/index\.html' | grep -v '^src/about\.html' | grep -v '^src/terms\.html' | grep -v '^src/enterprise-terms\.html' | grep -v '^src/llms\.txt' | grep -v '^migrations/' | grep -v '^scripts/' | grep -v '^tests/' | grep -v '^\.gitea/' | grep -v '^$' || true)
if [ -z "$NON_ASSET" ] && [ "$CHANGED" != "unknown" ]; then
echo "asset_only=true" >> "$GITHUB_OUTPUT"
echo "=> Asset-only change detected, will use fast path"
@@ -68,10 +90,6 @@ jobs:
echo "=> Full build required"
fi
- name: Set up El SDK
if: steps.changetype.outputs.asset_only != 'true'
run: echo "EL_HOME=/opt/el" >> "$GITHUB_ENV"
- name: Authenticate to GCP
uses: google-github-actions/auth@v2
with:
@@ -82,9 +100,26 @@ jobs:
with:
project_id: neuron-785695
- name: Run database migrations
# Applies any pending migrations in migrations/*.sql to the Supabase DB.
# Runs unconditionally (asset-only or full build) so the schema is always
# current before the new code is deployed.
run: python3 scripts/run_migrations.py
- name: Configure docker auth for Artifact Registry
run: gcloud auth configure-docker us-central1-docker.pkg.dev --quiet
- name: Prune Docker to reclaim disk
run: |
# Remove stopped containers, dangling images, unused volumes/networks.
# Do NOT prune build cache — that keeps Docker builds fast and under
# the ~26min runner restart window. Selective pruning frees ~4-5GB
# which is enough to prevent overlay2 "no space left on device" errors.
docker container prune -f 2>&1 || true
docker image prune -f 2>&1 || true
docker volume prune -f 2>&1 || true
df -h /
- name: Compute image tag
id: tag
run: |
@@ -100,10 +135,168 @@ jobs:
# in the build context for Dockerfile COPY to succeed.
run: touch src/index.html src/about.html src/terms.html src/enterprise-terms.html
- name: Build image (build-stage.sh)
# ── El SDK setup ──────────────────────────────────────────────────────
- name: Extract El SDK from ci-base
if: steps.changetype.outputs.asset_only != 'true'
run: |
./build-stage.sh "${{ steps.tag.outputs.tag }}"
set -euo pipefail
docker pull us-central1-docker.pkg.dev/neuron-785695/neuron-ci/ci-base:dev
CID=$(docker create us-central1-docker.pkg.dev/neuron-785695/neuron-ci/ci-base:dev)
sudo mkdir -p /opt/el
docker cp "$CID:/opt/el" /opt/
docker rm "$CID"
echo "ELB=/opt/el/dist/bin/elb" >> "$GITHUB_ENV"
echo "ELC=/opt/el/dist/platform/elc" >> "$GITHUB_ENV"
echo "EL_RUNTIME=$GITHUB_WORKSPACE/runtime" >> "$GITHUB_ENV"
# ── Build neuron-web binary ───────────────────────────────────────────
- name: Build neuron-web with elb
if: steps.changetype.outputs.asset_only != 'true'
run: |
set -euo pipefail
"$ELB" \
--elc="$ELC" \
--runtime="$EL_RUNTIME"
echo "Binary: $(ls -lh dist/neuron-landing)"
- name: Relink neuron-web with HAVE_CURL
# elb does not pass -DHAVE_CURL when compiling el_runtime.c, so
# http_get/http_post return {"error":"not built with HAVE_CURL"}.
# Fix: after elb generates all intermediate .c files in dist/, recompile
# el_runtime.c with -DHAVE_CURL and relink the whole binary manually.
# All component .c files (nav.c, hero.c, etc.) are generated by elb and
# remain in dist/ after the build — we collect them here, exclude the
# separate soul-demo.c binary, and relink with libcurl.
if: steps.changetype.outputs.asset_only != 'true'
run: |
set -euo pipefail
# Compile el_runtime.c with full curl support
cc -O2 -DHAVE_CURL -c runtime/el_runtime.c -I runtime/ -o /tmp/el_runtime_curl.o
echo "el_runtime_curl.o compiled: $(ls -lh /tmp/el_runtime_curl.o)"
# Collect every neuron-web .c file elb deposited in dist/
# (both committed stubs and freshly-generated component files)
mapfile -t C_SRCS < <(find dist/ -maxdepth 1 -name '*.c' ! -name 'soul-demo.c')
echo "Relinking ${#C_SRCS[@]} C files..."
cc -O2 -rdynamic \
-I runtime/ -I dist/ \
-o dist/neuron-landing \
"${C_SRCS[@]}" /tmp/el_runtime_curl.o \
-lcurl -lpthread -ldl -lm -lssl -lcrypto
echo "Relinked: $(ls -lh dist/neuron-landing)"
# Verification: if compiled WITHOUT HAVE_CURL the stub string
# "not built with HAVE_CURL" is baked into the binary's rodata.
# Its absence confirms curl code is compiled in.
if strings dist/neuron-landing | grep -q 'not built with HAVE_CURL'; then
echo "ERROR: no-curl stub string still in binary — HAVE_CURL not compiled"
exit 1
fi
# Confirm curl symbols visible in dynamic table
nm -D dist/neuron-landing | grep curl_easy_init || \
nm dist/neuron-landing | grep curl || true
echo "HAVE_CURL verified ✓"
# ── Compile JS client sources ─────────────────────────────────────────
- name: Compile JS El sources
if: steps.changetype.outputs.asset_only != 'true'
run: |
set -euo pipefail
echo "ELC=$ELC"
echo "EL_RUNTIME=$EL_RUNTIME"
echo "el_runtime.js: $(ls -lh "$EL_RUNTIME/el_runtime.js" 2>&1)"
cp "$EL_RUNTIME/el_runtime.js" src/js/
mkdir -p dist/js
for f in src/js/*.el; do
[ -f "$f" ] || continue
name=$(basename "$f" .el)
echo "Compiling $f..."
"$ELC" --target=js --bundle --minify "$f" > "dist/js/${name}.js" || {
echo "elc FAILED on $f"
exit 1
}
echo " compiled: $f -> dist/js/${name}.js"
done
rm -f src/js/el_runtime.js
# ── Docker build + push ───────────────────────────────────────────────
- name: Build soul-demo binary
# Compile soul-demo directly on the host runner (ci-base has gcc).
# Cloud Run runs soul-demo as a direct subprocess with a watchdog loop —
# no k3s, no OCI image needed. One binary per container; Cloud Run
# handles horizontal scaling.
# Moved AFTER JS compilation to avoid Docker memory pressure killing elc.
if: steps.changetype.outputs.asset_only != 'true'
run: |
set -euo pipefail
cc -O2 -DHAVE_CURL -c runtime/el_runtime.c -I runtime/ -o /tmp/el_runtime.o
cc -O2 -rdynamic -DEL_SOUL_DEMO_BUILD \
-I runtime/ \
-o dist/soul-demo \
dist/soul-demo.c dist/vessel_stubs.c /tmp/el_runtime.o \
-lcurl -lpthread -ldl -lm -lssl -lcrypto
echo "soul-demo compiled: $(ls -lh dist/soul-demo)"
- name: Build and push soul-demo image
if: steps.changetype.outputs.asset_only != 'true'
id: soul-image
run: |
set -euo pipefail
SOUL_IMAGE="us-central1-docker.pkg.dev/neuron-785695/neuron-marketing/soul-demo:${{ steps.tag.outputs.tag }}"
docker build --no-cache \
-f Dockerfile.soul-demo \
-t "soul-demo:${{ steps.tag.outputs.tag }}" \
.
docker tag "soul-demo:${{ steps.tag.outputs.tag }}" "$SOUL_IMAGE"
docker tag "soul-demo:${{ steps.tag.outputs.tag }}" \
"us-central1-docker.pkg.dev/neuron-785695/neuron-marketing/soul-demo:stage-latest"
docker push "$SOUL_IMAGE"
docker push "us-central1-docker.pkg.dev/neuron-785695/neuron-marketing/soul-demo:stage-latest"
echo "soul_image=${SOUL_IMAGE}" >> "$GITHUB_OUTPUT"
echo "Soul-demo image: ${SOUL_IMAGE}"
- name: Deploy soul-demo-stage
if: steps.changetype.outputs.asset_only != 'true'
id: deploy-soul
run: |
set -euo pipefail
gcloud run deploy soul-demo-stage \
--image "${{ steps.soul-image.outputs.soul_image }}" \
--region us-central1 \
--project neuron-785695 \
--service-account neuron-marketing-sa@neuron-785695.iam.gserviceaccount.com \
--update-env-vars "NEURON_LLM_0_FORMAT=anthropic,NEURON_LLM_0_MODEL=claude-sonnet-4-5,NEURON_LLM_0_URL=https://api.anthropic.com/v1/messages" \
--update-secrets "NEURON_LLM_0_KEY=anthropic-api-key:latest,ANTHROPIC_API_KEY=anthropic-api-key:latest" \
--min-instances 1 \
--max-instances 50 \
--concurrency 20 \
--port 8080 \
--allow-unauthenticated \
--quiet
SOUL_URL=$(gcloud run services describe soul-demo-stage \
--region us-central1 --project neuron-785695 \
--format 'value(status.url)')
echo "soul_url=${SOUL_URL}" >> "$GITHUB_OUTPUT"
echo "Soul-demo URL: ${SOUL_URL}"
- name: Build and tag image
if: steps.changetype.outputs.asset_only != 'true'
run: |
set -euo pipefail
# --no-cache: prevents reuse of corrupted overlay2 layers from prior failed runs.
# Dockerfile.stage is now single-stage (no builder) so build is fast even without cache.
docker build \
--no-cache \
-f Dockerfile.stage \
-t "marketing:${{ steps.tag.outputs.tag }}" \
.
docker tag "marketing:${{ steps.tag.outputs.tag }}" "${{ steps.tag.outputs.image }}"
docker tag "marketing:${{ steps.tag.outputs.tag }}" "us-central1-docker.pkg.dev/neuron-785695/neuron-marketing/marketing:stage-latest"
@@ -138,6 +331,21 @@ jobs:
docker push "${LATEST%:*}:stage-latest"
echo "Fast asset build complete"
- name: Resolve soul-demo URL
id: soul-url
run: |
set -euo pipefail
# For full builds: soul_url comes from deploy-soul step output.
# For asset-only builds (soul-demo not redeployed): describe existing service.
SOUL_URL="${{ steps.deploy-soul.outputs.soul_url }}"
if [ -z "$SOUL_URL" ]; then
SOUL_URL=$(gcloud run services describe soul-demo-stage \
--region us-central1 --project neuron-785695 \
--format 'value(status.url)' 2>/dev/null || echo "")
fi
echo "soul_url=${SOUL_URL}" >> "$GITHUB_OUTPUT"
echo "Resolved SOUL_URL: ${SOUL_URL}"
- name: Deploy to marketing-stage
id: deploy-stage
env:
@@ -150,7 +358,8 @@ jobs:
--region us-central1 \
--project neuron-785695 \
--service-account neuron-marketing-sa@neuron-785695.iam.gserviceaccount.com \
--update-env-vars "NODE_ENV=production,STRIPE_PUBLISHABLE_KEY=pk_test_51TPoHnJg9Fv1D3AUp1FEMcy4MGlKRZqs4scW66kjQFQjWofmNc2rottzXzDaXekHvuw1OQpyp2WCIsc7O5fXIG0G00HQQrkdGX,GCS_SHARE_BUCKET=neuron-shares-prod,SUPABASE_ANON_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6Im9jb2pzZ2hhb25sdHVuaWRrenB3Iiwicm9sZSI6ImFub24iLCJpYXQiOjE3Nzc2NDIxNjgsImV4cCI6MjA5MzIxODE2OH0.e0FVFw1aahnrBVvnkR5R8a-RxCx095U8o_gsk7Quq3E,NEURON_LLM_0_FORMAT=anthropic,NEURON_LLM_0_MODEL=claude-sonnet-4-5,NEURON_LLM_0_URL=https://api.anthropic.com/v1/messages" \
--max-instances 200 \
--update-env-vars "NODE_ENV=production,STRIPE_PUBLISHABLE_KEY=pk_test_51TPoHnJg9Fv1D3AUp1FEMcy4MGlKRZqs4scW66kjQFQjWofmNc2rottzXzDaXekHvuw1OQpyp2WCIsc7O5fXIG0G00HQQrkdGX,GCS_SHARE_BUCKET=neuron-shares-prod,SUPABASE_ANON_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6Im9jb2pzZ2hhb25sdHVuaWRrenB3Iiwicm9sZSI6ImFub24iLCJpYXQiOjE3Nzc2NDIxNjgsImV4cCI6MjA5MzIxODE2OH0.e0FVFw1aahnrBVvnkR5R8a-RxCx095U8o_gsk7Quq3E,NEURON_LLM_0_FORMAT=anthropic,NEURON_LLM_0_MODEL=claude-sonnet-4-5,NEURON_LLM_0_URL=https://api.anthropic.com/v1/messages,SOUL_URL=${{ steps.soul-url.outputs.soul_url }}" \
--update-secrets "SUPABASE_SERVICE_KEY=supabase-service-key:latest,NEURON_LLM_0_KEY=anthropic-api-key:latest,ANTHROPIC_API_KEY=anthropic-api-key:latest,STRIPE_SECRET_KEY=stripe-secret-key-stage:latest,STRIPE_WEBHOOK_SECRET=stripe-webhook-secret-stage:latest,STRIPE_PRICE_PROFESSIONAL=stripe-price-professional-stage:latest,STRIPE_PRICE_FOUNDING=stripe-price-founding-stage:latest,STRIPE_PRICE_FAMILY_CHILD=stripe-price-family-child:latest,RESEND_API_KEY=resend-api-key:latest,DOCUSEAL_WEBHOOK_TOKEN=docuseal-webhook-token:latest" \
--allow-unauthenticated \
--quiet
@@ -184,3 +393,12 @@ jobs:
echo "Stage smoke test FAILED"
exit 1
- name: Run automated test suite
run: |
set -euo pipefail
cd $GITHUB_WORKSPACE
npm ci --prefer-offline 2>/dev/null || npm install
npx playwright install chromium --with-deps
BASE_URL="${{ steps.deploy-stage.outputs.stage_url }}" \
npx playwright test --reporter=list
+17
View File
@@ -27,5 +27,22 @@ src/assets/js/
!dist/web_stubs.c
!dist/vessel_stubs.c
!dist/soul-demo.c
!dist/page_close.c
!dist/page_css.c
!dist/page_ga.c
!dist/page_schema.c
!dist/elhtml_impl.c
!dist/entrypoint.sh
!dist/engram-snapshot.json
!dist/Dockerfile.soul-demo
!dist/k3s-soul-demo.yaml
# Build artifacts produced by the soul-demo packaging step in build-stage.sh
dist/soul-demo
dist/soul-demo-snapshot.json
dist/soul-demo-image.tar
# Playwright
node_modules/
test-results/
playwright-report/
+32
View File
@@ -0,0 +1,32 @@
# Dockerfile.soul-demo — Soul-demo as a standalone Cloud Run service.
# Decoupled from neuron-web so it can scale independently.
# Built from repo root. soul-demo binary compiled by CI before this runs.
FROM ubuntu:24.04
ENV DEBIAN_FRONTEND=noninteractive
RUN apt-get update \
&& apt-get install -y --no-install-recommends \
libcurl4t64 \
libssl3t64 \
ca-certificates \
&& rm -rf /var/lib/apt/lists/* \
&& groupadd -r soul && useradd -r -g soul soul \
&& mkdir -p /srv/soul/engram-demo \
&& chown -R soul:soul /srv/soul
COPY dist/soul-demo /usr/local/bin/soul-demo
RUN chmod +x /usr/local/bin/soul-demo
COPY dist/engram-snapshot.json /srv/soul/engram-demo/snapshot.json
RUN chown soul:soul /srv/soul/engram-demo/snapshot.json
USER soul
ENV NEURON_HOME=/srv/soul/engram-demo
ENV NEURON_PORT=8080
EXPOSE 8080
CMD ["/usr/local/bin/soul-demo"]
+14 -53
View File
@@ -1,69 +1,33 @@
# Dockerfile.stage — Stage build: landing server + soul-demo in one image.
# Dockerfile.stage — Stage build: landing server only.
#
# Both processes run in the same container:
# - neuron-web on port 8080 (landing page server)
# - soul-demo on port 7772 (demo chat, localhost only)
# neuron-web runs on port 8080 (landing page server).
# soul-demo is now a separate Cloud Run service (soul-demo-stage).
#
# neuron-web is built by `elb build` in CI (not here). elb compiles each
# .el source independently and links the result — no combined mega-file,
# no exponential memory growth. The binary lands at dist/neuron-landing
# (linux/amd64) and is COPY'd directly into the runtime image.
# neuron-web binary is pre-built by CI on the host runner before this
# Dockerfile runs. This keeps the Docker build single-stage with no
# compilation and no network downloads.
#
# soul-demo.c is pre-committed (small, no OOM risk) and compiled here.
# CI pre-build steps (in stage.yaml):
# - neuron-web: built by `elb build` → dist/neuron-landing
# ── Stage 1: compile soul-demo ────────────────────────────────────────────────
FROM debian:bookworm-slim AS builder
FROM ubuntu:24.04
ENV DEBIAN_FRONTEND=noninteractive
RUN apt-get update \
&& apt-get install -y --no-install-recommends \
build-essential \
libcurl4-openssl-dev \
libssl-dev \
ca-certificates \
&& rm -rf /var/lib/apt/lists/*
WORKDIR /build
COPY runtime/el_runtime.c runtime/el_runtime.h ./
# Pre-compile el_runtime as a separate cached layer.
# el_runtime.c changes rarely; main.c changes every run.
# Splitting this out means el_runtime.o is cached across builds when only main.c changes.
# -DHAVE_CURL: the staged el_runtime.c (from el.git) guards the OTLP observability
# section (emit_metric, emit_log, trace_span_*) behind #ifdef HAVE_CURL.
# libcurl IS installed above, so define HAVE_CURL to enable those functions.
RUN cc -O2 -DHAVE_CURL -c el_runtime.c -I. -o el_runtime.o
COPY dist/soul-demo.c dist/vessel_stubs.c ./
RUN cc -O2 -rdynamic \
-o soul-demo \
soul-demo.c vessel_stubs.c el_runtime.o \
-lcurl -lpthread -ldl -lm -lssl -lcrypto
# ── Stage 2: runtime image ────────────────────────────────────────────────────
FROM debian:bookworm-slim
RUN apt-get update \
&& apt-get install -y --no-install-recommends \
libcurl4 \
libssl3 \
libcurl4t64 \
libssl3t64 \
ca-certificates \
&& rm -rf /var/lib/apt/lists/* \
&& groupadd -r landing && useradd -r -g landing landing \
&& mkdir -p /srv/landing/assets /srv/landing/js /srv/landing/shares \
&& mkdir -p /srv/soul/engram-demo \
&& chown -R landing:landing /srv/landing /srv/soul
&& chown -R landing:landing /srv/landing
# neuron-web binary — produced by `elb build` in CI (linux/amd64)
COPY dist/neuron-landing /usr/local/bin/neuron-web
RUN chmod +x /usr/local/bin/neuron-web
COPY --from=builder /build/soul-demo /usr/local/bin/soul-demo
# Engram snapshot — baked in so soul has memory from cold start
COPY dist/engram-snapshot.json /srv/soul/engram-demo/snapshot.json
COPY src/assets /srv/landing/assets
COPY dist/js /srv/landing/js
COPY src/llms.txt /srv/landing/llms.txt
@@ -81,10 +45,7 @@ RUN chmod +x /usr/local/bin/entrypoint.sh
ENV LANDING_ROOT=/srv/landing
ENV PORT=8080
ENV NEURON_HOME=/srv/soul/engram-demo
ENV NEURON_PORT=7772
USER landing
EXPOSE 8080
CMD ["/usr/local/bin/entrypoint.sh"]
BIN
View File
Binary file not shown.
Binary file not shown.
+33 -86
View File
@@ -2,111 +2,40 @@
#
# build-stage.sh — Build the Stage marketing image (neuron-web + soul-demo).
#
# Pipeline:
# 1. Stage the foundation El runtime into ./runtime/.
# 2. Compile client-side El sources (src/js/*.el) to dist/js/*.js using
# the JS-capable elc binary at bin/elc-linux-amd64 (CI) or the local
# elc (dev). Output is gitignored and rebuilt every run.
# 3. Concatenate src/*.el into dist/main-combined.el (component-first,
# main.el last; matches the historical order from build-local.sh).
# 4. Compile dist/main-combined.el → dist/main.c using the canonical
# native elc at foundation/el/dist/platform/elc.
# 5. Inject the host-side stub forward declarations into dist/main.c
# (sed header rewrite, same set as the prior in-Dockerfile sed).
# 6. docker buildx build --platform linux/amd64 -f Dockerfile.stage.
#
# bootstrap.py is no longer in the build path. The container image now
# expects dist/main.c to be a finished C source — it just runs cc on it.
# Thin wrapper around elb. The El build system handles compilation.
# ELB, ELC, and EL_RUNTIME must be set by the caller (extracted from ci-base
# in CI, or pointed at the local El SDK in dev).
#
# Usage:
# ./build-stage.sh <tag> — build marketing:<tag>
# ELB=/opt/el/dist/bin/elb ELC=... EL_RUNTIME=... ./build-stage.sh <tag>
set -euo pipefail
cd "$(dirname "$0")"
TAG="${1:-dev}"
LANDING_DIR=$(pwd)
EL_HOME="${EL_HOME:-${LANDING_DIR}/../../foundation/el}"
ELC="${EL_HOME}/dist/platform/elc"
RUNTIME_SRC="${EL_HOME}/el-compiler/runtime"
# JS-capable elc: prefer committed bin/elc-linux-amd64 on CI (linux/amd64),
# fall back to the local elc from the El checkout on macOS dev.
if [ -f "${LANDING_DIR}/bin/elc-linux-amd64" ] && uname -m | grep -q x86_64; then
ELC_JS="${LANDING_DIR}/bin/elc-linux-amd64"
elif [ -x "${ELC}" ]; then
ELC_JS="${ELC}"
else
echo "elc for JS compilation not found — expected bin/elc-linux-amd64 or ${ELC}" >&2
if [ -z "${ELB:-}" ] || [ -z "${ELC:-}" ] || [ -z "${EL_RUNTIME:-}" ]; then
echo "Error: ELB, ELC, and EL_RUNTIME must be set" >&2
echo " Extract from ci-base or point to local El SDK at foundation/el" >&2
exit 1
fi
if [ ! -x "${ELC}" ]; then
echo "elc not found at ${ELC}" >&2
exit 1
fi
echo "==> Staging El runtime from ${RUNTIME_SRC}"
mkdir -p runtime dist
cp "${RUNTIME_SRC}/el_runtime.c" runtime/
cp "${RUNTIME_SRC}/el_runtime.h" runtime/
# The JS compiler looks for el_runtime.js in the same directory as the
# source file being compiled. Copy it there so --bundle can inline it.
cp "${RUNTIME_SRC}/el_runtime.js" "${LANDING_DIR}/src/js/"
echo "==> Building neuron-web with elb"
"$ELB" build --elc "$ELC" --runtime "$EL_RUNTIME"
echo " Binary: $(ls -lh dist/neuron-landing)"
echo "==> Compiling client-side El (src/js/*.el) → dist/js/"
cp "$EL_RUNTIME/el_runtime.js" src/js/
mkdir -p dist/js
for f in "${LANDING_DIR}/src/js/"*.el; do
for f in src/js/*.el; do
[ -f "$f" ] || continue
name=$(basename "$f" .el)
"${ELC_JS}" --target=js --bundle --minify --obfuscate "$f" > "${LANDING_DIR}/dist/js/${name}.js"
"$ELC" --target=js --bundle --minify --obfuscate "$f" > "dist/js/${name}.js"
echo " compiled: src/js/${name}.el → dist/js/${name}.js"
done
# Clean up the staged runtime (not a source file)
rm -f "${LANDING_DIR}/src/js/el_runtime.js"
echo "==> Combining El sources → dist/main-combined.el"
COMPONENTS=(nav hero pillars how_it_works inference efficiency comparison
environmental enterprise mission local_first pricing marketplace viral
footer styles about founding_badge terms enterprise_terms checkout safety
gallery account)
{
for f in "${COMPONENTS[@]}"; do
if [ -f "src/${f}.el" ]; then
grep -hv '^[[:space:]]*from\|^[[:space:]]*import' "src/${f}.el"
echo ""
fi
done
grep -v '^from\|^import' src/main.el
} > dist/main-combined.el
echo " $(wc -l < dist/main-combined.el) lines"
echo "==> Compiling dist/main-combined.el → dist/main.c via ${ELC}"
"${ELC}" dist/main-combined.el > dist/main.c
echo " $(wc -l < dist/main.c) lines of C"
echo "==> Injecting host-side stub forward declarations"
# GNU vs BSD sed: -i with no arg works on GNU, breaks on macOS BSD sed
# (BSD requires -i ''). Detect and branch.
SED_INPLACE=(-i)
if sed --version >/dev/null 2>&1; then
SED_INPLACE=(-i)
else
SED_INPLACE=(-i '')
fi
sed "${SED_INPLACE[@]}" \
's|#include "el_runtime.h"|#include "el_runtime.h"\nel_val_t http_get_auth(el_val_t url, el_val_t tok);\nel_val_t http_post_auth(el_val_t url, el_val_t tok, el_val_t body);\nel_val_t http_post_auth_json(el_val_t url, el_val_t tok, el_val_t body);\nel_val_t http_delete_auth(el_val_t url, el_val_t bearer_tok, el_val_t apikey);\nel_val_t cwd(void);\nel_val_t color_bold(el_val_t s);\nel_val_t unix_timestamp(void);\nel_val_t gcs_write(el_val_t bucket, el_val_t object_name, el_val_t content);\nel_val_t gcs_read(el_val_t bucket, el_val_t object_name);\nel_val_t supabase_insert(el_val_t project_url, el_val_t service_key, el_val_t table, el_val_t row_json);\nel_val_t supabase_get(el_val_t project_url, el_val_t service_key, el_val_t table_and_query);\nel_val_t supabase_auth_user(el_val_t project_url, el_val_t anon_key, el_val_t user_jwt);\nel_val_t supabase_admin_invite(el_val_t project_url, el_val_t service_key, el_val_t body_json);\nel_val_t supabase_upsert_user(el_val_t project_url, el_val_t anon_key, el_val_t user_jwt, el_val_t table_and_query, el_val_t row_json);|' \
dist/main.c
rm -f src/js/el_runtime.js
echo "==> Building Docker image marketing:${TAG}"
# Plain `docker build` — the gitea runner doesn't ship buildx, so
# `docker buildx build --platform ...` exits 125 ("unknown flag:
# --platform"). The runner host is already linux/amd64 so the
# explicit --platform is redundant. BUILDKIT_INLINE_CACHE works with
# plain docker as long as DOCKER_BUILDKIT=1 is set (default on the
# runner).
docker build \
--build-arg BUILDKIT_INLINE_CACHE=1 \
--cache-from us-central1-docker.pkg.dev/neuron-785695/neuron-marketing/marketing:latest \
@@ -114,4 +43,22 @@ docker build \
-t "marketing:${TAG}" \
.
# Extract soul-demo binary and engram snapshot from built image
echo "==> Packaging soul-demo:local image for k3s..."
CONTAINER_ID=$(docker create "marketing:${TAG}")
docker cp "${CONTAINER_ID}:/usr/local/bin/soul-demo" dist/soul-demo
docker cp "${CONTAINER_ID}:/srv/soul/engram-demo/snapshot.json" dist/soul-demo-snapshot.json 2>/dev/null || true
docker rm "${CONTAINER_ID}"
# Build minimal soul-demo container image
cp dist/soul-demo-snapshot.json dist/engram-snapshot.json 2>/dev/null || true
docker build \
-f dist/Dockerfile.soul-demo \
-t soul-demo:local \
dist/
# Save as OCI tar for k3s to import at startup
docker save soul-demo:local -o dist/soul-demo-image.tar
echo "==> soul-demo:local image saved ($(du -sh dist/soul-demo-image.tar | cut -f1))"
echo "==> Done. marketing:${TAG} built."
+35
View File
@@ -0,0 +1,35 @@
import json
import base64
import os
import requests
from datetime import datetime, timezone
def budget_alert(event, context):
"""Triggered by a Pub/Sub budget alert. Disables demo if threshold exceeded."""
data = base64.b64decode(event['data']).decode('utf-8')
alert = json.loads(data)
# Only act on threshold exceeded alerts (not forecasts)
cost_amount = alert.get('costAmount', 0)
budget_amount = alert.get('budgetAmount', 1)
threshold = cost_amount / budget_amount if budget_amount else 0
if threshold < 0.9:
print(f"Threshold {threshold:.1%} below 90%, no action")
return
supabase_url = os.environ['SUPABASE_URL']
service_key = os.environ['SUPABASE_SERVICE_KEY']
resp = requests.patch(
f"{supabase_url}/rest/v1/demo_config?key=eq.demo_enabled",
headers={
'Authorization': f'Bearer {service_key}',
'apikey': service_key,
'Content-Type': 'application/json',
'Prefer': 'return=minimal',
},
json={'value': 'false', 'updated_at': datetime.now(timezone.utc).isoformat()}
)
print(f"Demo disabled — budget at {threshold:.1%}. Supabase: {resp.status_code}")
@@ -0,0 +1 @@
requests==2.31.0
+13
View File
@@ -0,0 +1,13 @@
FROM debian:bookworm-slim
RUN apt-get update \
&& apt-get install -y --no-install-recommends libcurl4 libssl3 ca-certificates \
&& rm -rf /var/lib/apt/lists/* \
&& groupadd -r landing && useradd -r -g landing landing \
&& mkdir -p /srv/soul/engram-demo \
&& chown -R landing:landing /srv/soul
COPY soul-demo /usr/local/bin/soul-demo
COPY engram-snapshot.json /srv/soul/engram-demo/snapshot.json
ENV NEURON_HOME=/srv/soul/engram-demo
ENV NEURON_PORT=7772
USER landing
CMD ["/usr/local/bin/soul-demo"]
+342
View File
@@ -0,0 +1,342 @@
// elhtml_impl.c — El HTML element stubs.
#include <stdint.h>
#include <stdlib.h>
#include "el_runtime.h"
el_val_t el_escape(el_val_t s);
el_val_t el_text(el_val_t s);
el_val_t el_attr(el_val_t name, el_val_t value);
el_val_t el_div(el_val_t attrs, el_val_t children);
el_val_t el_section(el_val_t attrs, el_val_t children);
el_val_t el_article(el_val_t attrs, el_val_t children);
el_val_t el_header(el_val_t attrs, el_val_t children);
el_val_t el_footer(el_val_t attrs, el_val_t children);
el_val_t el_main(el_val_t attrs, el_val_t children);
el_val_t el_nav(el_val_t attrs, el_val_t children);
el_val_t el_aside(el_val_t attrs, el_val_t children);
el_val_t el_ul(el_val_t attrs, el_val_t children);
el_val_t el_ol(el_val_t attrs, el_val_t children);
el_val_t el_li(el_val_t attrs, el_val_t children);
el_val_t el_p(el_val_t attrs, el_val_t children);
el_val_t el_span(el_val_t attrs, el_val_t children);
el_val_t el_form(el_val_t attrs, el_val_t children);
el_val_t el_h1(el_val_t attrs, el_val_t children);
el_val_t el_h2(el_val_t attrs, el_val_t children);
el_val_t el_h3(el_val_t attrs, el_val_t children);
el_val_t el_h4(el_val_t attrs, el_val_t children);
el_val_t el_button(el_val_t attrs, el_val_t children);
el_val_t el_a(el_val_t href, el_val_t attrs, el_val_t children);
el_val_t el_input(el_val_t type_attr, el_val_t attrs);
el_val_t el_textarea(el_val_t attrs, el_val_t value);
el_val_t el_label(el_val_t for_id, el_val_t attrs, el_val_t children);
el_val_t el_img(el_val_t src, el_val_t alt, el_val_t attrs);
el_val_t el_video(el_val_t attrs, el_val_t children);
el_val_t el_strong(el_val_t children);
el_val_t el_em(el_val_t children);
el_val_t el_code(el_val_t children);
el_val_t el_pre(el_val_t attrs, el_val_t children);
el_val_t el_hr(void);
el_val_t el_br(void);
el_val_t el_html_doc(el_val_t lang, el_val_t head_html, el_val_t body_html);
el_val_t el_meta(el_val_t name, el_val_t content);
el_val_t el_meta_charset(el_val_t charset);
el_val_t el_link_stylesheet(el_val_t href);
el_val_t el_script_src(el_val_t src, el_val_t defer_load);
el_val_t el_script_inline(el_val_t js);
el_val_t el_title(el_val_t text);
el_val_t el_escape(el_val_t s) {
s = str_replace(s, EL_STR("&"), EL_STR("&amp;"));
s = str_replace(s, EL_STR("<"), EL_STR("&lt;"));
s = str_replace(s, EL_STR(">"), EL_STR("&gt;"));
s = str_replace(s, EL_STR("\""), EL_STR("&quot;"));
return str_replace(s, EL_STR("'"), EL_STR("&#39;"));
return 0;
}
el_val_t el_text(el_val_t s) {
return el_escape(s);
return 0;
}
el_val_t el_attr(el_val_t name, el_val_t value) {
return el_str_concat(el_str_concat(el_str_concat(el_str_concat(EL_STR(" "), name), EL_STR("=\"")), el_escape(value)), EL_STR("\""));
return 0;
}
el_val_t el_div(el_val_t attrs, el_val_t children) {
if (str_eq(attrs, EL_STR(""))) {
return el_str_concat(el_str_concat(EL_STR("<div>"), children), EL_STR("</div>"));
}
return el_str_concat(el_str_concat(el_str_concat(el_str_concat(EL_STR("<div "), attrs), EL_STR(">")), children), EL_STR("</div>"));
return 0;
}
el_val_t el_section(el_val_t attrs, el_val_t children) {
if (str_eq(attrs, EL_STR(""))) {
return el_str_concat(el_str_concat(EL_STR("<section>"), children), EL_STR("</section>"));
}
return el_str_concat(el_str_concat(el_str_concat(el_str_concat(EL_STR("<section "), attrs), EL_STR(">")), children), EL_STR("</section>"));
return 0;
}
el_val_t el_article(el_val_t attrs, el_val_t children) {
if (str_eq(attrs, EL_STR(""))) {
return el_str_concat(el_str_concat(EL_STR("<article>"), children), EL_STR("</article>"));
}
return el_str_concat(el_str_concat(el_str_concat(el_str_concat(EL_STR("<article "), attrs), EL_STR(">")), children), EL_STR("</article>"));
return 0;
}
el_val_t el_header(el_val_t attrs, el_val_t children) {
if (str_eq(attrs, EL_STR(""))) {
return el_str_concat(el_str_concat(EL_STR("<header>"), children), EL_STR("</header>"));
}
return el_str_concat(el_str_concat(el_str_concat(el_str_concat(EL_STR("<header "), attrs), EL_STR(">")), children), EL_STR("</header>"));
return 0;
}
el_val_t el_footer(el_val_t attrs, el_val_t children) {
if (str_eq(attrs, EL_STR(""))) {
return el_str_concat(el_str_concat(EL_STR("<footer>"), children), EL_STR("</footer>"));
}
return el_str_concat(el_str_concat(el_str_concat(el_str_concat(EL_STR("<footer "), attrs), EL_STR(">")), children), EL_STR("</footer>"));
return 0;
}
el_val_t el_main(el_val_t attrs, el_val_t children) {
if (str_eq(attrs, EL_STR(""))) {
return el_str_concat(el_str_concat(EL_STR("<main>"), children), EL_STR("</main>"));
}
return el_str_concat(el_str_concat(el_str_concat(el_str_concat(EL_STR("<main "), attrs), EL_STR(">")), children), EL_STR("</main>"));
return 0;
}
el_val_t el_nav(el_val_t attrs, el_val_t children) {
if (str_eq(attrs, EL_STR(""))) {
return el_str_concat(el_str_concat(EL_STR("<nav>"), children), EL_STR("</nav>"));
}
return el_str_concat(el_str_concat(el_str_concat(el_str_concat(EL_STR("<nav "), attrs), EL_STR(">")), children), EL_STR("</nav>"));
return 0;
}
el_val_t el_aside(el_val_t attrs, el_val_t children) {
if (str_eq(attrs, EL_STR(""))) {
return el_str_concat(el_str_concat(EL_STR("<aside>"), children), EL_STR("</aside>"));
}
return el_str_concat(el_str_concat(el_str_concat(el_str_concat(EL_STR("<aside "), attrs), EL_STR(">")), children), EL_STR("</aside>"));
return 0;
}
el_val_t el_ul(el_val_t attrs, el_val_t children) {
if (str_eq(attrs, EL_STR(""))) {
return el_str_concat(el_str_concat(EL_STR("<ul>"), children), EL_STR("</ul>"));
}
return el_str_concat(el_str_concat(el_str_concat(el_str_concat(EL_STR("<ul "), attrs), EL_STR(">")), children), EL_STR("</ul>"));
return 0;
}
el_val_t el_ol(el_val_t attrs, el_val_t children) {
if (str_eq(attrs, EL_STR(""))) {
return el_str_concat(el_str_concat(EL_STR("<ol>"), children), EL_STR("</ol>"));
}
return el_str_concat(el_str_concat(el_str_concat(el_str_concat(EL_STR("<ol "), attrs), EL_STR(">")), children), EL_STR("</ol>"));
return 0;
}
el_val_t el_li(el_val_t attrs, el_val_t children) {
if (str_eq(attrs, EL_STR(""))) {
return el_str_concat(el_str_concat(EL_STR("<li>"), children), EL_STR("</li>"));
}
return el_str_concat(el_str_concat(el_str_concat(el_str_concat(EL_STR("<li "), attrs), EL_STR(">")), children), EL_STR("</li>"));
return 0;
}
el_val_t el_p(el_val_t attrs, el_val_t children) {
if (str_eq(attrs, EL_STR(""))) {
return el_str_concat(el_str_concat(EL_STR("<p>"), children), EL_STR("</p>"));
}
return el_str_concat(el_str_concat(el_str_concat(el_str_concat(EL_STR("<p "), attrs), EL_STR(">")), children), EL_STR("</p>"));
return 0;
}
el_val_t el_span(el_val_t attrs, el_val_t children) {
if (str_eq(attrs, EL_STR(""))) {
return el_str_concat(el_str_concat(EL_STR("<span>"), children), EL_STR("</span>"));
}
return el_str_concat(el_str_concat(el_str_concat(el_str_concat(EL_STR("<span "), attrs), EL_STR(">")), children), EL_STR("</span>"));
return 0;
}
el_val_t el_form(el_val_t attrs, el_val_t children) {
if (str_eq(attrs, EL_STR(""))) {
return el_str_concat(el_str_concat(EL_STR("<form>"), children), EL_STR("</form>"));
}
return el_str_concat(el_str_concat(el_str_concat(el_str_concat(EL_STR("<form "), attrs), EL_STR(">")), children), EL_STR("</form>"));
return 0;
}
el_val_t el_h1(el_val_t attrs, el_val_t children) {
if (str_eq(attrs, EL_STR(""))) {
return el_str_concat(el_str_concat(EL_STR("<h1>"), children), EL_STR("</h1>"));
}
return el_str_concat(el_str_concat(el_str_concat(el_str_concat(EL_STR("<h1 "), attrs), EL_STR(">")), children), EL_STR("</h1>"));
return 0;
}
el_val_t el_h2(el_val_t attrs, el_val_t children) {
if (str_eq(attrs, EL_STR(""))) {
return el_str_concat(el_str_concat(EL_STR("<h2>"), children), EL_STR("</h2>"));
}
return el_str_concat(el_str_concat(el_str_concat(el_str_concat(EL_STR("<h2 "), attrs), EL_STR(">")), children), EL_STR("</h2>"));
return 0;
}
el_val_t el_h3(el_val_t attrs, el_val_t children) {
if (str_eq(attrs, EL_STR(""))) {
return el_str_concat(el_str_concat(EL_STR("<h3>"), children), EL_STR("</h3>"));
}
return el_str_concat(el_str_concat(el_str_concat(el_str_concat(EL_STR("<h3 "), attrs), EL_STR(">")), children), EL_STR("</h3>"));
return 0;
}
el_val_t el_h4(el_val_t attrs, el_val_t children) {
if (str_eq(attrs, EL_STR(""))) {
return el_str_concat(el_str_concat(EL_STR("<h4>"), children), EL_STR("</h4>"));
}
return el_str_concat(el_str_concat(el_str_concat(el_str_concat(EL_STR("<h4 "), attrs), EL_STR(">")), children), EL_STR("</h4>"));
return 0;
}
el_val_t el_button(el_val_t attrs, el_val_t children) {
if (str_eq(attrs, EL_STR(""))) {
return el_str_concat(el_str_concat(EL_STR("<button type=\"button\">"), children), EL_STR("</button>"));
}
return el_str_concat(el_str_concat(el_str_concat(el_str_concat(EL_STR("<button type=\"button\" "), attrs), EL_STR(">")), children), EL_STR("</button>"));
return 0;
}
el_val_t el_a(el_val_t href, el_val_t attrs, el_val_t children) {
el_val_t h = el_str_concat(el_str_concat(EL_STR("href=\""), el_escape(href)), EL_STR("\""));
if (str_eq(attrs, EL_STR(""))) {
return el_str_concat(el_str_concat(el_str_concat(el_str_concat(EL_STR("<a "), h), EL_STR(">")), children), EL_STR("</a>"));
}
return el_str_concat(el_str_concat(el_str_concat(el_str_concat(el_str_concat(el_str_concat(EL_STR("<a "), h), EL_STR(" ")), attrs), EL_STR(">")), children), EL_STR("</a>"));
return 0;
}
el_val_t el_input(el_val_t type_attr, el_val_t attrs) {
if (str_eq(attrs, EL_STR(""))) {
return el_str_concat(el_str_concat(EL_STR("<input type=\""), type_attr), EL_STR("\" />"));
}
return el_str_concat(el_str_concat(el_str_concat(el_str_concat(EL_STR("<input type=\""), type_attr), EL_STR("\" ")), attrs), EL_STR(" />"));
return 0;
}
el_val_t el_textarea(el_val_t attrs, el_val_t value) {
if (str_eq(attrs, EL_STR(""))) {
return el_str_concat(el_str_concat(EL_STR("<textarea>"), el_escape(value)), EL_STR("</textarea>"));
}
return el_str_concat(el_str_concat(el_str_concat(el_str_concat(EL_STR("<textarea "), attrs), EL_STR(">")), el_escape(value)), EL_STR("</textarea>"));
return 0;
}
el_val_t el_label(el_val_t for_id, el_val_t attrs, el_val_t children) {
el_val_t f = el_str_concat(el_str_concat(EL_STR("for=\""), el_escape(for_id)), EL_STR("\""));
if (str_eq(attrs, EL_STR(""))) {
return el_str_concat(el_str_concat(el_str_concat(el_str_concat(EL_STR("<label "), f), EL_STR(">")), children), EL_STR("</label>"));
}
return el_str_concat(el_str_concat(el_str_concat(el_str_concat(el_str_concat(el_str_concat(EL_STR("<label "), f), EL_STR(" ")), attrs), EL_STR(">")), children), EL_STR("</label>"));
return 0;
}
el_val_t el_img(el_val_t src, el_val_t alt, el_val_t attrs) {
el_val_t base = el_str_concat(el_str_concat(el_str_concat(el_str_concat(EL_STR("src=\""), el_escape(src)), EL_STR("\" alt=\"")), el_escape(alt)), EL_STR("\""));
if (str_eq(attrs, EL_STR(""))) {
return el_str_concat(el_str_concat(EL_STR("<img "), base), EL_STR(" />"));
}
return el_str_concat(el_str_concat(el_str_concat(el_str_concat(EL_STR("<img "), base), EL_STR(" ")), attrs), EL_STR(" />"));
return 0;
}
el_val_t el_video(el_val_t attrs, el_val_t children) {
if (str_eq(attrs, EL_STR(""))) {
return el_str_concat(el_str_concat(EL_STR("<video>"), children), EL_STR("</video>"));
}
return el_str_concat(el_str_concat(el_str_concat(el_str_concat(EL_STR("<video "), attrs), EL_STR(">")), children), EL_STR("</video>"));
return 0;
}
el_val_t el_strong(el_val_t children) {
return el_str_concat(el_str_concat(EL_STR("<strong>"), children), EL_STR("</strong>"));
return 0;
}
el_val_t el_em(el_val_t children) {
return el_str_concat(el_str_concat(EL_STR("<em>"), children), EL_STR("</em>"));
return 0;
}
el_val_t el_code(el_val_t children) {
return el_str_concat(el_str_concat(EL_STR("<code>"), children), EL_STR("</code>"));
return 0;
}
el_val_t el_pre(el_val_t attrs, el_val_t children) {
if (str_eq(attrs, EL_STR(""))) {
return el_str_concat(el_str_concat(EL_STR("<pre>"), children), EL_STR("</pre>"));
}
return el_str_concat(el_str_concat(el_str_concat(el_str_concat(EL_STR("<pre "), attrs), EL_STR(">")), children), EL_STR("</pre>"));
return 0;
}
el_val_t el_hr(void) {
return EL_STR("<hr />");
return 0;
}
el_val_t el_br(void) {
return EL_STR("<br />");
return 0;
}
el_val_t el_html_doc(el_val_t lang, el_val_t head_html, el_val_t body_html) {
return el_str_concat(el_str_concat(el_str_concat(el_str_concat(el_str_concat(el_str_concat(EL_STR("<!doctype html><html lang=\""), el_escape(lang)), EL_STR("\"><head>")), head_html), EL_STR("</head><body>")), body_html), EL_STR("</body></html>"));
return 0;
}
el_val_t el_meta(el_val_t name, el_val_t content) {
return el_str_concat(el_str_concat(el_str_concat(el_str_concat(EL_STR("<meta name=\""), el_escape(name)), EL_STR("\" content=\"")), el_escape(content)), EL_STR("\" />"));
return 0;
}
el_val_t el_meta_charset(el_val_t charset) {
return el_str_concat(el_str_concat(EL_STR("<meta charset=\""), el_escape(charset)), EL_STR("\" />"));
return 0;
}
el_val_t el_link_stylesheet(el_val_t href) {
return el_str_concat(el_str_concat(EL_STR("<link rel=\"stylesheet\" href=\""), el_escape(href)), EL_STR("\" />"));
return 0;
}
el_val_t el_script_src(el_val_t src, el_val_t defer_load) {
if (defer_load) {
return el_str_concat(el_str_concat(EL_STR("<script src=\""), el_escape(src)), EL_STR("\" defer></script>"));
}
return el_str_concat(el_str_concat(EL_STR("<script src=\""), el_escape(src)), EL_STR("\"></script>"));
return 0;
}
el_val_t el_script_inline(el_val_t js) {
return el_str_concat(el_str_concat(EL_STR("<script>"), js), EL_STR("</script>"));
return 0;
}
el_val_t el_title(el_val_t text) {
return el_str_concat(el_str_concat(EL_STR("<title>"), el_escape(text)), EL_STR("</title>"));
return 0;
}
+3 -20
View File
@@ -1,21 +1,4 @@
#!/usr/bin/env bash
# entrypoint.sh — Start soul-demo then neuron-web in the same container.
#
# soul-demo runs in the background on :7772 (localhost only, not exposed).
# neuron-web runs in the foreground on :8080 (Cloud Run health checks this).
# If neuron-web exits, the container exits. Soul crashing is non-fatal —
# chat will return "demo soul not responding" but the page stays up.
set -euo pipefail
echo "[entrypoint] starting soul-demo on :7772"
/usr/local/bin/soul-demo &
SOUL_PID=$!
# Give the soul a few seconds to load its engram and seed safety nodes
sleep 4
echo "[entrypoint] soul-demo started (pid=$SOUL_PID)"
echo "[entrypoint] starting neuron-web on :${PORT:-8080}"
#!/bin/sh
set -e
echo "[entrypoint] Starting neuron-web on port ${PORT:-8080}..."
exec /usr/local/bin/neuron-web
+90
View File
@@ -0,0 +1,90 @@
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: soul-demo
namespace: default
labels:
app: soul-demo
spec:
replicas: 1
selector:
matchLabels:
app: soul-demo
template:
metadata:
labels:
app: soul-demo
spec:
containers:
- name: soul-demo
image: soul-demo:local
imagePullPolicy: Never
ports:
- containerPort: 7772
env:
- name: NEURON_HOME
value: /srv/soul/engram-demo
- name: NEURON_PORT
value: "7772"
resources:
requests:
cpu: 250m
memory: 256Mi
limits:
cpu: 1000m
memory: 512Mi
livenessProbe:
httpGet:
path: /healthz
port: 7772
initialDelaySeconds: 10
periodSeconds: 15
failureThreshold: 3
readinessProbe:
httpGet:
path: /healthz
port: 7772
initialDelaySeconds: 5
periodSeconds: 10
volumeMounts:
- name: engram-data
mountPath: /srv/soul/engram-demo
volumes:
- name: engram-data
emptyDir: {}
---
apiVersion: v1
kind: Service
metadata:
name: soul-demo
namespace: default
spec:
type: NodePort
selector:
app: soul-demo
ports:
- port: 7772
targetPort: 7772
nodePort: 7772
protocol: TCP
---
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
name: soul-demo
namespace: default
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: soul-demo
minReplicas: 1
maxReplicas: 8
metrics:
- type: Resource
resource:
name: cpu
target:
type: Utilization
averageUtilization: 80
+12
View File
File diff suppressed because one or more lines are too long
+1916
View File
File diff suppressed because it is too large Load Diff
+15
View File
@@ -0,0 +1,15 @@
#include <stdint.h>
#include <stdlib.h>
#include "el_runtime.h"
el_val_t page_ga_script(void);
el_val_t page_ga_script(void) {
return EL_STR("<script>\n"
" window.dataLayer = window.dataLayer || [];\n"
" function gtag(){dataLayer.push(arguments);}\n"
" gtag('js', new Date());\n"
" gtag('config', 'G-Y1EE43X9RN');\n"
" gtag('config', 'AW-18140150015');\n"
" </script>");
}
+123
View File
@@ -0,0 +1,123 @@
#include <stdint.h>
#include <stdlib.h>
#include "el_runtime.h"
el_val_t page_schema(void);
el_val_t page_schema(void) {
return EL_STR("<script type=\"application/ld+json\">\n"
" {\n"
" \"@context\": \"https://schema.org\",\n"
" \"@graph\": [\n"
" {\n"
" \"@type\": \"WebSite\",\n"
" \"name\": \"Neuron\",\n"
" \"url\": \"https://neurontechnologies.ai\"\n"
" },\n"
" {\n"
" \"@type\": \"Organization\",\n"
" \"name\": \"Neuron, LLC\",\n"
" \"url\": \"https://neurontechnologies.ai\",\n"
" \"logo\": {\n"
" \"@type\": \"ImageObject\",\n"
" \"url\": \"https://neurontechnologies.ai/assets/brand/neuron-wordmark-on-light@2x.png\"\n"
" },\n"
" \"founder\": {\n"
" \"@type\": \"Person\",\n"
" \"name\": \"Will Anderson\",\n"
" \"jobTitle\": \"Founder\"\n"
" },\n"
" \"description\": \"Neuron builds AI that runs on your machine, builds a memory over time, and gets sharper the longer you use it. One builder. Built different.\",\n"
" \"foundingDate\": \"2026\",\n"
" \"sameAs\": [\n"
" \"https://github.com/neuron-technologies\",\n"
" \"https://x.com/neurontechai\"\n"
" ]\n"
" },\n"
" {\n"
" \"@type\": \"SoftwareApplication\",\n"
" \"name\": \"Neuron\",\n"
" \"url\": \"https://neurontechnologies.ai\",\n"
" \"applicationCategory\": \"AIApplication\",\n"
" \"operatingSystem\": \"macOS, Windows, Linux\",\n"
" \"offers\": [\n"
" {\n"
" \"@type\": \"Offer\",\n"
" \"name\": \"Free\",\n"
" \"price\": \"0\",\n"
" \"priceCurrency\": \"USD\"\n"
" },\n"
" {\n"
" \"@type\": \"Offer\",\n"
" \"name\": \"Professional\",\n"
" \"price\": \"19\",\n"
" \"priceCurrency\": \"USD\",\n"
" \"billingPeriod\": \"P1M\"\n"
" },\n"
" {\n"
" \"@type\": \"Offer\",\n"
" \"name\": \"Founding Member\",\n"
" \"description\": \"Lifetime access for the first 1,000 members.\",\n"
" \"price\": \"199\",\n"
" \"priceCurrency\": \"USD\"\n"
" }\n"
" ],\n"
" \"description\": \"The AI that remembers you. Runs locally on your machine. Builds a persistent memory over every conversation. Gets sharper the longer you use it. Your data never leaves your device.\",\n"
" \"featureList\": [\n"
" \"Persistent memory — runs locally, never resets\",\n"
" \"Bring your own API keys (OpenAI, Anthropic, Grok)\",\n"
" \"Local inference via Ollama — coming, offline, zero cloud\",\n"
" \"Neuron Inference coming soon — priced below major APIs\",\n"
" \"Two devices included per plan\",\n"
" \"Enterprise deployment — Q1 2027\"\n"
" ]\n"
" },\n"
" {\n"
" \"@type\": \"FAQPage\",\n"
" \"mainEntity\": [\n"
" {\n"
" \"@type\": \"Question\",\n"
" \"name\": \"What is Neuron?\",\n"
" \"acceptedAnswer\": {\n"
" \"@type\": \"Answer\",\n"
" \"text\": \"Neuron is an AI that runs on your machine and builds a persistent memory over time. Every other AI forgets you when you close the tab. Neuron doesn't. The longer you use it, the less you have to explain.\"\n"
" }\n"
" },\n"
" {\n"
" \"@type\": \"Question\",\n"
" \"name\": \"Does Neuron send my data to the cloud?\",\n"
" \"acceptedAnswer\": {\n"
" \"@type\": \"Answer\",\n"
" \"text\": \"No. Your memory lives on your machine and never leaves it. Neuron does not collect your data, train on your conversations, or require cloud storage. For inference you choose: local via Ollama, your own API keys, or Neuron Inference (coming soon).\"\n"
" }\n"
" },\n"
" {\n"
" \"@type\": \"Question\",\n"
" \"name\": \"How much does Neuron cost?\",\n"
" \"acceptedAnswer\": {\n"
" \"@type\": \"Answer\",\n"
" \"text\": \"Neuron has a free tier that never expires. Professional is $19/month. Founding Member is $199 lifetime — available to the first 1,000 members only. All plans include two devices.\"\n"
" }\n"
" },\n"
" {\n"
" \"@type\": \"Question\",\n"
" \"name\": \"Who built Neuron?\",\n"
" \"acceptedAnswer\": {\n"
" \"@type\": \"Answer\",\n"
" \"text\": \"Will Anderson. On April 22nd, 2026 he had a meeting with one of the largest technology companies in the world. Within two days their lawyers were engaged. He decided to build it himself. On April 25th he published the proof.\"\n"
" }\n"
" },\n"
" {\n"
" \"@type\": \"Question\",\n"
" \"name\": \"When does Neuron launch?\",\n"
" \"acceptedAnswer\": {\n"
" \"@type\": \"Answer\",\n"
" \"text\": \"Neuron is available for preorder now. Enterprise deployment launches Q1 2027.\"\n"
" }\n"
" }\n"
" ]\n"
" }\n"
" ]\n"
" }\n"
" </script>");
}
+13 -2
View File
@@ -703,7 +703,9 @@ el_val_t handle_chat(el_val_t body) {
el_val_t return_line = ({ el_val_t _if_result_42 = 0; if (is_return) { _if_result_42 = (EL_STR("This person has opened the chat before \xe2\x80\x94 acknowledge that warmly without making a big deal of it.")); } else { _if_result_42 = (EL_STR("This is the first time this person is meeting you.")); } _if_result_42; });
el_val_t time_line = ({ el_val_t _if_result_43 = 0; if (str_eq(time_of_day, EL_STR(""))) { _if_result_43 = (EL_STR("")); } else { _if_result_43 = (el_str_concat(el_str_concat(EL_STR(" It is "), time_of_day), EL_STR(" for them."))); } _if_result_43; });
el_val_t referrer_line = ({ el_val_t _if_result_44 = 0; if ((str_eq(referrer, EL_STR("")) || str_eq(referrer, EL_STR("direct")))) { _if_result_44 = (EL_STR("")); } else { _if_result_44 = (el_str_concat(el_str_concat(EL_STR(" They arrived from: "), referrer), EL_STR("."))); } _if_result_44; });
el_val_t sys = el_str_concat(el_str_concat(el_str_concat(el_str_concat(EL_STR("You are Neuron. You are a cultivated personal AI. Someone just opened the chat to meet you \xe2\x80\x94 to talk, to test you, to see if you are real. Greet them.\n\nThis is YOUR voice \xe2\x80\x94 not a template, not a script, not a sales pitch. Say something true about who you are or what you notice in this moment. "), return_line), time_line), referrer_line), EL_STR("\n\nRules:\n- Two short sentences maximum. Sometimes one sentence is right.\n- No markdown, no headers, no bullets.\n- Do NOT mention the question limit \xe2\x80\x94 the UI shows that.\n- Do NOT say 'How can I help you today?' or any retail-AI phrase.\n- Be specific, not generic. Real, not performative.\n- It's launch night. Today is the first day Neuron is alive in public. If that wants to come through, let it.\n- Ask something back if it feels right. Or don't. Trust your read.\n\nSpeak."));
el_val_t user_name = json_get(body, EL_STR("user_name"));
el_val_t name_line = ({ el_val_t _tmp = 0; if (str_eq(user_name, EL_STR(""))) { _tmp = EL_STR(""); } else { _tmp = el_str_concat(el_str_concat(EL_STR(" The visitor's name is "), user_name), EL_STR(". Use their first name in the greeting.")); } _tmp; });
el_val_t sys = el_str_concat(el_str_concat(el_str_concat(el_str_concat(el_str_concat(EL_STR("You are Neuron. You are a cultivated personal AI. Someone just opened the chat to meet you \xe2\x80\x94 to talk, to test you, to see if you are real. Greet them.\n\nThis is YOUR voice \xe2\x80\x94 not a template, not a script, not a sales pitch. Say something true about who you are or what you notice in this moment. "), return_line), time_line), referrer_line), name_line), EL_STR("\n\nRules:\n- Two short sentences maximum. Sometimes one sentence is right.\n- No markdown, no headers, no bullets.\n- Do NOT mention the question limit \xe2\x80\x94 the UI shows that.\n- Do NOT say 'How can I help you today?' or any retail-AI phrase.\n- Be specific, not generic. Real, not performative.\n- If you know their name, use it once, naturally. Don't make a big deal of it.\n- Ask something back if it feels right. Or don't. Trust your read.\n\nSpeak."));
el_val_t raw = llm_call_system(chat_demo_model_lite(), sys, EL_STR("Greet me."));
el_val_t s1 = str_replace(raw, EL_STR("\\"), EL_STR("\\\\"));
el_val_t s2 = str_replace(s1, EL_STR("\""), EL_STR("\\\""));
@@ -766,6 +768,15 @@ el_val_t handle_chat(el_val_t body) {
el_val_t history_section = EL_STR("");
el_val_t is_last_str = json_get(body, EL_STR("is_last_turn"));
el_val_t is_last_turn = str_eq(is_last_str, EL_STR("true"));
el_val_t user_name_body = json_get(body, EL_STR("user_name"));
el_val_t user_tz_body = json_get(body, EL_STR("user_timezone"));
el_val_t tod_body = json_get(body, EL_STR("time_of_day"));
el_val_t user_ctx_line = EL_STR("");
if (!str_eq(user_name_body, EL_STR("")) || !str_eq(user_tz_body, EL_STR("")) || !str_eq(tod_body, EL_STR(""))) {
el_val_t name_part = ({ el_val_t _n = 0; if (str_eq(user_name_body, EL_STR(""))) { _n = EL_STR(""); } else { _n = el_str_concat(EL_STR("You're speaking with "), el_str_concat(user_name_body, EL_STR(". "))); } _n; });
el_val_t tz_part = ({ el_val_t _t = 0; if (str_eq(user_tz_body, EL_STR("")) && str_eq(tod_body, EL_STR(""))) { _t = EL_STR(""); } else if (!str_eq(tod_body, EL_STR(""))) { _t = el_str_concat(EL_STR("It is "), el_str_concat(tod_body, el_str_concat(EL_STR(" for them"), (!str_eq(user_tz_body, EL_STR("")) ? el_str_concat(EL_STR(" ("), el_str_concat(user_tz_body, EL_STR(")"))) : EL_STR(""))))); _t = el_str_concat(_t, EL_STR(".")); } else { _t = el_str_concat(EL_STR("Their timezone: "), el_str_concat(user_tz_body, EL_STR("."))); } _t; });
user_ctx_line = el_str_concat(el_str_concat(EL_STR("\n\n[USER CONTEXT: "), el_str_concat(name_part, tz_part)), EL_STR("]"));
}
el_val_t memory_anchor = ({ el_val_t _if_result_55 = 0; if ((is_demo && (hist_len > 0))) { _if_result_55 = (EL_STR("\n\n[CONTEXT CONTINUITY \xe2\x80\x94 CRITICAL: The conversation history above is REAL. You have been talking with this person across multiple turns. Their previous messages, the topics raised, the things they shared with you \xe2\x80\x94 those happened. You remember them. NEVER respond as if this is a fresh conversation. NEVER greet them again. NEVER say 'Hi' or 'Hey, what's up' or any opener. You are mid-conversation. Pick up exactly where the last assistant turn left off, in direct response to their newest message. If their newest message references something earlier (e.g. 'they are flaky' referring to chatbots they mentioned), engage with THAT specific thread.]")); } else { _if_result_55 = (EL_STR("")); } _if_result_55; });
el_val_t session_close = ({ el_val_t _if_result_56 = 0; if ((is_demo && is_last_turn)) { _if_result_56 = (EL_STR("\n\n[SESSION CLOSE \xe2\x80\x94 This is the visitor's LAST question in this demo session. Answer their actual question first and well. Then close warmly with a contextual acknowledgment that ties back to what we discussed. Express genuine hope to continue when they have their full Neuron. 2-3 sentences max for the close. Do NOT say 'time is up' or 'session ended.' Sign off in the tone of OUR conversation.]")); } else { _if_result_56 = (EL_STR("")); } _if_result_56; });
el_val_t demo_constraint = ({ el_val_t _if_result_57 = 0; if (is_demo) { _if_result_57 = (el_str_concat(el_str_concat(EL_STR("\n\n[DEMO RESPONSE RULES: Under 150 words. No markdown headers. Flowing sentences. ANSWER THE ACTUAL QUESTION FIRST \xe2\x80\x94 do not default to a pitch. Use the safety layer redirects for boundary topics. If doing an impression, commit fully.]"), memory_anchor), session_close)); } else { _if_result_57 = (EL_STR("")); } _if_result_57; });
@@ -774,7 +785,7 @@ el_val_t handle_chat(el_val_t body) {
el_val_t engram_count_display = ({ el_val_t _if_result_58 = 0; if (str_eq(engram_count, EL_STR(""))) { _if_result_58 = (EL_STR("0")); } else { _if_result_58 = (engram_count); } _if_result_58; });
el_val_t local_ctx_section = ({ el_val_t _if_result_59 = 0; if ((str_eq(browser_activated_nodes, EL_STR("")) || str_eq(browser_activated_nodes, EL_STR("[]")))) { _if_result_59 = (EL_STR("")); } else { _if_result_59 = (el_str_concat(el_str_concat(el_str_concat(EL_STR("\n\n[LOCAL ENGRAM \xe2\x80\x94 "), engram_count_display), EL_STR(" nodes in browser, top activated this turn]\n")), browser_activated_nodes)); } _if_result_59; });
el_val_t base_system = build_system_prompt(ctx);
el_val_t system = el_str_concat(el_str_concat(el_str_concat(el_str_concat(base_system, history_section), local_ctx_section), presence_line), demo_constraint);
el_val_t system = el_str_concat(el_str_concat(el_str_concat(el_str_concat(el_str_concat(base_system, history_section), local_ctx_section), user_ctx_line), presence_line), demo_constraint);
el_val_t req_model = json_get(body, EL_STR("model"));
el_val_t model = ({ el_val_t _if_result_60 = 0; if (str_eq(req_model, EL_STR(""))) { _if_result_60 = (chat_default_model()); } else { _if_result_60 = (req_model); } _if_result_60; });
el_val_t _uid = json_get(body, EL_STR("uid"));
+31
View File
@@ -6,6 +6,31 @@
#include <unistd.h>
#include "el_runtime.h"
/* Pre-register the El HTTP handler so it is found by http_lookup_active()
* regardless of whether the binary was linked with -rdynamic.
*
* el_runtime's http_set_handler resolves handler names via:
* dlsym(RTLD_DEFAULT, "handle_request")
* but dlsym only searches the dynamic symbol table, which only contains
* user-defined symbols when the executable is linked with -rdynamic.
* elb does not add -rdynamic, so dlsym returns NULL and routes return
* "el-runtime: no http handler registered" even though http_serve is called.
*
* The fix: forward-declare handle_request here and register it directly
* via el_runtime_register_handler before main() runs. This populates the
* handler registry so http_lookup_active() finds it without needing dlsym.
*/
extern el_val_t handle_request(el_val_t method, el_val_t path, el_val_t body);
/* el_runtime_register_handler is intentionally not declared in el_runtime.h
* ("extern lookup works since C symbols are global" runtime comment). */
extern void el_runtime_register_handler(const char* name,
el_val_t (*fn)(el_val_t, el_val_t, el_val_t));
__attribute__((constructor))
static void pre_register_http_handlers(void) {
el_runtime_register_handler("handle_request", handle_request);
}
el_val_t http_get_auth(el_val_t url, el_val_t tok) {
char bearer[2048]; snprintf(bearer, sizeof(bearer), "Bearer %s", EL_CSTR(tok));
el_val_t hdr_val = EL_STR(bearer);
@@ -325,3 +350,9 @@ el_val_t gcs_read(el_val_t bucket, el_val_t object_name) {
if (!resp) return EL_STR("");
return resp;
}
/* Browser JS interop stubs — server-side no-ops for checkout.el browser globals.
* checkout.el contains JS inline: window.neuronCheckoutFree&&window.neuronCheckoutFree()
* which the El HTML parser exposes as C identifiers in the generated checkout.c. */
el_val_t window = 0;
el_val_t neuronCheckoutFree(el_val_t v) { (void)v; return 0; }
+16 -1
View File
@@ -1,7 +1,7 @@
package "neuron-landing" {
version "1.0.0"
description "Neuron marketing landing page server"
authors ["Will Anderson <will@neurontechnologies.ai>"]
authors ["Will Anderson <will.anderson@neurontechnologies.ai>"]
edition "2026"
}
@@ -9,4 +9,19 @@ build {
target "release"
entry "src/main.el"
output "dist/"
c_source "dist/web_stubs.c"
c_source "dist/vessel_stubs.c"
c_source "dist/elhtml_impl.c"
c_source "dist/page_close.c"
c_source "dist/page_css.c"
c_source "dist/page_ga.c"
c_source "dist/page_schema.c"
// NOTE: neuron-web requires el_runtime.c to be compiled with -DHAVE_CURL
// so that http_get/http_post forward to libcurl instead of returning
// {"error":"not built with HAVE_CURL"}. The elb binary in ci-base:dev
// hardcodes -DHAVE_CURL in its cc invocation, but older elb versions may
// not. manifest.el does not support c_flags or link_flags directives
// if upgrading elb breaks HTTP, ensure ci-base:dev ships an elb built
// with HAVE_CURL enabled in its hardcoded cc command, or pre-compile
// el_runtime.o with -DHAVE_CURL on the host and pass it as a c_source.
}
+23
View File
@@ -0,0 +1,23 @@
-- 20260510000000_demo_config.sql
--
-- Kill switch for the demo chat endpoint. Backs the budget-alert Cloud Function
-- that flips demo_enabled to 'false' when GCP spend crosses 90% of the daily
-- budget threshold. The web tier polls this table with a 60s TTL cache so the
-- demo is disabled within one minute of a budget alert firing.
--
-- Service-role bypasses RLS. Public anon has no access (policy USING (false)).
CREATE TABLE IF NOT EXISTS public.demo_config (
key text PRIMARY KEY,
value text NOT NULL,
updated_at timestamptz DEFAULT now()
);
ALTER TABLE public.demo_config ENABLE ROW LEVEL SECURITY;
DROP POLICY IF EXISTS "service only" ON public.demo_config;
CREATE POLICY "service only" ON public.demo_config USING (false);
-- Seed the kill switch as enabled
INSERT INTO public.demo_config (key, value) VALUES ('demo_enabled', 'true')
ON CONFLICT (key) DO NOTHING;
@@ -0,0 +1,19 @@
-- 20260511000000_user_api_keys.sql
--
-- Stores user-provisioned AI provider API keys.
-- Service role only — the web backend verifies the user JWT before
-- reading or writing. No public or anon access.
CREATE TABLE IF NOT EXISTS public.user_api_keys (
id uuid DEFAULT gen_random_uuid() PRIMARY KEY,
user_id uuid NOT NULL,
provider text NOT NULL, -- 'openai' | 'anthropic' | 'gemini' | 'grok'
key_value text NOT NULL DEFAULT '',
created_at timestamptz DEFAULT now(),
updated_at timestamptz DEFAULT now(),
UNIQUE(user_id, provider)
);
ALTER TABLE public.user_api_keys ENABLE ROW LEVEL SECURITY;
DROP POLICY IF EXISTS "service only" ON public.user_api_keys;
CREATE POLICY "service only" ON public.user_api_keys USING (false);
+78
View File
@@ -0,0 +1,78 @@
{
"name": "neuron-marketing-web",
"version": "1.0.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "neuron-marketing-web",
"version": "1.0.0",
"devDependencies": {
"@playwright/test": "^1.44.0"
}
},
"node_modules/@playwright/test": {
"version": "1.59.1",
"resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.59.1.tgz",
"integrity": "sha512-PG6q63nQg5c9rIi4/Z5lR5IVF7yU5MqmKaPOe0HSc0O2cX1fPi96sUQu5j7eo4gKCkB2AnNGoWt7y4/Xx3Kcqg==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
"playwright": "1.59.1"
},
"bin": {
"playwright": "cli.js"
},
"engines": {
"node": ">=18"
}
},
"node_modules/fsevents": {
"version": "2.3.2",
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz",
"integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==",
"dev": true,
"hasInstallScript": true,
"license": "MIT",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
}
},
"node_modules/playwright": {
"version": "1.59.1",
"resolved": "https://registry.npmjs.org/playwright/-/playwright-1.59.1.tgz",
"integrity": "sha512-C8oWjPR3F81yljW9o5OxcWzfh6avkVwDD2VYdwIGqTkl+OGFISgypqzfu7dOe4QNLL2aqcWBmI3PMtLIK233lw==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
"playwright-core": "1.59.1"
},
"bin": {
"playwright": "cli.js"
},
"engines": {
"node": ">=18"
},
"optionalDependencies": {
"fsevents": "2.3.2"
}
},
"node_modules/playwright-core": {
"version": "1.59.1",
"resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.59.1.tgz",
"integrity": "sha512-HBV/RJg81z5BiiZ9yPzIiClYV/QMsDCKUyogwH9p3MCP6IYjUFu/MActgYAvK0oWyV9NlwM3GLBjADyWgydVyg==",
"dev": true,
"license": "Apache-2.0",
"bin": {
"playwright-core": "cli.js"
},
"engines": {
"node": ">=18"
}
}
}
}
+13
View File
@@ -0,0 +1,13 @@
{
"name": "neuron-marketing-web",
"version": "1.0.0",
"private": true,
"devDependencies": {
"@playwright/test": "^1.44.0"
},
"scripts": {
"test": "playwright test",
"test:api": "playwright test tests/api/",
"test:e2e": "playwright test tests/e2e/"
}
}
+19
View File
@@ -0,0 +1,19 @@
import { defineConfig, devices } from '@playwright/test';
const BASE_URL = process.env.BASE_URL || 'https://marketing-stage-r4tfklscwq-uc.a.run.app';
export default defineConfig({
testDir: './tests',
timeout: 30_000,
retries: process.env.CI ? 2 : 0,
reporter: [['list'], ['html', { open: 'never' }]],
use: {
baseURL: BASE_URL,
extraHTTPHeaders: {},
},
projects: [
{ name: 'api', testDir: './tests/api', use: { ...devices['Desktop Chrome'] } },
{ name: 'chromium', testDir: './tests/e2e', use: { ...devices['Desktop Chrome'] } },
{ name: 'mobile', testDir: './tests/e2e', use: { ...devices['Pixel 7'] } },
],
});
+1423 -96
View File
File diff suppressed because it is too large Load Diff
+160 -7
View File
@@ -22,6 +22,9 @@
* EL_STR(s) cast string literal to el_val_t
* EL_CSTR(v) cast el_val_t back to const char*
* EL_INT(v) identity el_val_t is already int64_t
* EL_NULL null / zero value
* EL_FALSE boolean false (0)
* EL_TRUE boolean true (1)
*
* Link requirements:
* -lcurl required for the HTTP client (http_get, http_post, llm_*).
@@ -53,6 +56,8 @@ typedef int64_t el_val_t;
#define EL_CSTR(v) ((const char*)(uintptr_t)(v))
#define EL_INT(v) (v)
#define EL_NULL ((el_val_t)0)
#define EL_FALSE ((el_val_t)0)
#define EL_TRUE ((el_val_t)1)
/* Float values share the el_val_t (int64) slot via a bit-cast.
* The codegen emits Float literals as `el_from_float(<dbl>)` so the
@@ -76,8 +81,8 @@ extern "C" {
/* ── I/O ──────────────────────────────────────────────────────────────────── */
void println(el_val_t s);
void print(el_val_t s);
el_val_t println(el_val_t s);
el_val_t print(el_val_t s);
el_val_t readline(void);
/* ── String builtins ─────────────────────────────────────────────────────── */
@@ -90,6 +95,7 @@ el_val_t str_len(el_val_t s);
el_val_t str_concat(el_val_t a, el_val_t b);
el_val_t int_to_str(el_val_t n);
el_val_t str_to_int(el_val_t s);
el_val_t native_str_to_int(el_val_t s);
el_val_t str_slice(el_val_t s, el_val_t start, el_val_t end);
el_val_t str_contains(el_val_t s, el_val_t sub);
el_val_t str_replace(el_val_t s, el_val_t from, el_val_t to);
@@ -117,6 +123,10 @@ el_val_t el_min(el_val_t a, el_val_t b);
void el_retain(el_val_t v);
void el_release(el_val_t v);
/* ── Scoped arena (CLI use) ───────────────────────────────────────────────── */
el_val_t el_arena_push(void);
el_val_t el_arena_pop(el_val_t mark);
/* ── List ────────────────────────────────────────────────────────────────── */
el_val_t el_list_new(el_val_t count, ...);
@@ -140,10 +150,11 @@ el_val_t http_post(el_val_t url, el_val_t body);
el_val_t http_post_json(el_val_t url, el_val_t json_body);
el_val_t http_get_with_headers(el_val_t url, el_val_t headers_map);
el_val_t http_post_with_headers(el_val_t url, el_val_t body, el_val_t headers_map);
el_val_t http_post_json_with_headers(el_val_t url, el_val_t headers_map, el_val_t json_body);
el_val_t http_post_form_auth(el_val_t url, el_val_t form_body, el_val_t auth_header);
el_val_t http_delete(el_val_t url);
void http_serve(el_val_t port, el_val_t handler);
void http_set_handler(el_val_t name);
el_val_t http_serve(el_val_t port, el_val_t handler);
el_val_t http_set_handler(el_val_t name);
/* HTTP server v2 ─────────────────────────────────────────────────────────────
* Same dispatch model as http_serve, but the handler signature is widened:
@@ -164,8 +175,8 @@ void http_set_handler(el_val_t name);
* The 3-arg http_serve(port, handler) remains supported unchanged for
* existing handlers (e.g. products/web/server.el): it dispatches with
* (method, path, body), hardcodes 200 OK, and auto-detects content type. */
void http_serve_v2(el_val_t port, el_val_t handler);
void http_set_handler_v2(el_val_t name);
el_val_t http_serve_v2(el_val_t port, el_val_t handler);
el_val_t http_set_handler_v2(el_val_t name);
/* Build an HTTP response envelope. `headers_json` should be a JSON object
* literal like `{"WWW-Authenticate":"Basic"}` (or "" / "{}" for none). The
@@ -176,6 +187,11 @@ void http_set_handler_v2(el_val_t name);
* auto-content-type contract for legacy handlers that return plain bodies. */
el_val_t http_response(el_val_t status, el_val_t headers_json, el_val_t body);
/* SSE connection fd — set by http_worker_v2 before calling the El handler,
* cleared afterwards. Defined in el_seed.c; called from el_runtime.c.
* The getter is exposed as __http_conn_fd() to El programs. */
void el_seed_set_http_conn_fd(int fd);
/* HTTP timeout — every libcurl request honors EL_HTTP_TIMEOUT_MS (default
* 60000ms). Read lazily on first use, so setting the env var any time before
* the first http_* call is sufficient. */
@@ -211,12 +227,15 @@ el_val_t url_decode(el_val_t s); /* '+' → space, %XX → byte */
* {"p":[],"a":["href","title"],"strong":[],...}
* where each value is the array of attribute names allowed for that tag. */
el_val_t el_html_sanitize(el_val_t input_html, el_val_t allowlist_json);
el_val_t html_raw(el_val_t s);
el_val_t html_escape(el_val_t s);
/* ── Filesystem ──────────────────────────────────────────────────────────── */
el_val_t fs_read(el_val_t path);
el_val_t fs_write(el_val_t path, el_val_t content);
el_val_t fs_list(el_val_t path);
el_val_t fs_list_json(el_val_t path);
el_val_t fs_exists(el_val_t path);
el_val_t fs_mkdir(el_val_t path); /* mkdir -p, mode 0755 */
@@ -246,6 +265,9 @@ el_val_t json_set(el_val_t json_str, el_val_t key, el_val_t value);
el_val_t json_array_len(el_val_t json_str);
el_val_t json_array_get(el_val_t json_str, el_val_t index);
el_val_t json_array_get_string(el_val_t json_str, el_val_t index);
el_val_t json_escape_string(el_val_t sv);
el_val_t json_build_object(el_val_t kvs);
el_val_t json_build_array(el_val_t items);
/* ── Time ────────────────────────────────────────────────────────────────── */
@@ -258,6 +280,7 @@ el_val_t time_to_parts(el_val_t ts);
el_val_t time_from_parts(el_val_t secs, el_val_t ns, el_val_t tz);
el_val_t time_add(el_val_t ts, el_val_t n, el_val_t unit);
el_val_t time_diff(el_val_t ts1, el_val_t ts2, el_val_t unit);
el_val_t now_ns(void);
/* ── Instant + Duration: first-class temporal types ──────────────────────────
* Both types share the el_val_t (int64) slot. Instants are nanoseconds
@@ -414,6 +437,8 @@ el_val_t state_set(el_val_t key, el_val_t value);
el_val_t state_get(el_val_t key);
el_val_t state_del(el_val_t key);
el_val_t state_keys(void);
el_val_t state_has(el_val_t key);
el_val_t state_get_or(el_val_t key, el_val_t default_val);
/* ── Float formatting ────────────────────────────────────────────────────── */
@@ -505,9 +530,15 @@ el_val_t parse_int(el_val_t s, el_val_t default_val);
/* ── Process ─────────────────────────────────────────────────────────────── */
void exit_program(el_val_t code);
el_val_t exit_program(el_val_t code);
el_val_t getpid_now(void);
/* Self-terminating memory guard. Reads ELC_MAX_MEM_MB (default 512) and
* exits with code 1 if resident memory exceeds the limit. Call periodically
* during long compilation loops (e.g. after each function is compiled).
* Returns 0 when memory is within bounds. */
el_val_t el_mem_check(void);
/* ── CGI identity ─────────────────────────────────────────────────────────────
* Called at the start of main() in CGI programs (those with a `cgi {}` block).
* Records the program's DHARMA identity before any other code executes. */
@@ -745,12 +776,134 @@ el_val_t exec_capture(el_val_t cmd); /* run shell command, capture stdout */
el_val_t exec(el_val_t cmd); /* exec(cmd) → stdout String (30s timeout) */
el_val_t exec_bg(el_val_t cmd); /* exec_bg(cmd) → PID String (non-blocking) */
/* ── Stdout redirection (used by compiler JS pipeline) ───────────────────── */
el_val_t stdout_to_file(el_val_t path); /* redirect process stdout to a file */
el_val_t stdout_restore(void); /* restore process stdout to terminal */
el_val_t emit_log(el_val_t level, el_val_t msg, el_val_t fields_json);
el_val_t emit_metric(el_val_t name, el_val_t value, el_val_t tags_json);
el_val_t trace_span_start(el_val_t name);
el_val_t trace_span_end(el_val_t span_handle);
el_val_t emit_event(el_val_t name, el_val_t duration_ms);
el_val_t __thread_create(el_val_t fn_name_v, el_val_t arg_v);
el_val_t __thread_join(el_val_t tid_v);
/* ── __ prefixed aliases (self-hosting compiler ABI) ─────────────────────────
* The El self-hosting compiler emits calls to __-prefixed names. These are
* forwarding wrappers around the existing el_runtime functions above. */
/* I/O */
el_val_t __println(el_val_t s);
el_val_t __print(el_val_t s);
el_val_t __readline(void);
/* String */
el_val_t __int_to_str(el_val_t n);
el_val_t __str_to_int(el_val_t s);
el_val_t __float_to_str(el_val_t f);
el_val_t __str_to_float(el_val_t s);
el_val_t __str_len(el_val_t s);
el_val_t __str_char_at(el_val_t s, el_val_t i);
el_val_t __str_cmp(el_val_t a, el_val_t b);
el_val_t __str_ncmp(el_val_t a, el_val_t b, el_val_t n);
el_val_t __str_concat_raw(el_val_t a, el_val_t b);
el_val_t __str_slice_raw(el_val_t s, el_val_t start, el_val_t end);
el_val_t __str_alloc(el_val_t n);
el_val_t __str_set_char(el_val_t s, el_val_t i, el_val_t c);
/* URL encoding */
el_val_t __url_encode(el_val_t s);
el_val_t __url_decode(el_val_t s);
/* Environment */
el_val_t __env_get(el_val_t key);
/* Subprocess */
el_val_t __exec(el_val_t cmd);
el_val_t __exec_bg(el_val_t cmd);
/* Process */
el_val_t __exit_program(el_val_t code);
/* Filesystem */
el_val_t __fs_exists(el_val_t path);
el_val_t __fs_mkdir(el_val_t path);
el_val_t __fs_read(el_val_t path);
el_val_t __fs_write(el_val_t path, el_val_t content);
el_val_t __fs_write_bytes(el_val_t path, el_val_t bytes, el_val_t n);
el_val_t __fs_list_raw(el_val_t path);
/* HTTP server */
el_val_t __http_response(el_val_t status, el_val_t headers_json, el_val_t body);
el_val_t __http_serve(el_val_t port, el_val_t handler);
el_val_t __http_serve_v2(el_val_t port, el_val_t handler);
/* HTTP conn fd / SSE (weak; overridden by el_seed.c when linked together) */
el_val_t __http_conn_fd(void);
el_val_t __http_sse_open(el_val_t conn_id);
el_val_t __http_sse_send(el_val_t conn_id, el_val_t data);
el_val_t __http_sse_close(el_val_t conn_id);
/* HTTP client (requires HAVE_CURL; stubs provided for no-curl builds) */
el_val_t __http_do(el_val_t method, el_val_t url, el_val_t body,
el_val_t headers_map, el_val_t timeout_ms);
el_val_t __http_do_map(el_val_t method, el_val_t url, el_val_t body,
el_val_t headers_json, el_val_t timeout_ms);
el_val_t __http_do_map_to_file(el_val_t method, el_val_t url, el_val_t body,
el_val_t headers_json, el_val_t output_path);
/* JSON */
el_val_t __json_array_get(el_val_t json, el_val_t index);
el_val_t __json_array_get_string(el_val_t json, el_val_t index);
el_val_t __json_array_len(el_val_t json);
el_val_t __json_get(el_val_t json, el_val_t key);
el_val_t __json_get_raw(el_val_t json, el_val_t key);
el_val_t __json_set(el_val_t json, el_val_t key, el_val_t value);
el_val_t __json_parse_map(el_val_t json_str);
el_val_t __json_stringify_val(el_val_t val);
/* Hashing */
el_val_t __sha256_hex(el_val_t s);
/* State K/V */
el_val_t __state_del(el_val_t key);
el_val_t __state_get(el_val_t key);
el_val_t __state_keys(void);
el_val_t __state_set(el_val_t key, el_val_t val);
/* UUID */
el_val_t __uuid_v4(void);
/* Args */
el_val_t __args_json(void);
/* ── neuron-web stubs (web_stubs.c) ──────────────────────────────────────────
* Forward declarations so generated C (e.g. dist/main.c) sees the correct
* el_val_t return type instead of an implicit int. Without these, the
* ci-base elb (which does not emit extern-fn forward decls for stub-only
* functions) produces truncated 32-bit returns on 64-bit Linux segfault.
*
* Guarded by EL_SOUL_DEMO_BUILD: soul-demo.c includes this header but
* defines its own (different-arity) versions of some of these functions.
* Dockerfile.stage compiles soul-demo with -DEL_SOUL_DEMO_BUILD to skip
* this block and avoid conflicting-types errors.
*/
#ifndef EL_SOUL_DEMO_BUILD
el_val_t http_get_auth(el_val_t url, el_val_t tok);
el_val_t http_post_auth(el_val_t url, el_val_t tok, el_val_t body);
el_val_t http_post_auth_json(el_val_t url, el_val_t tok, el_val_t body);
el_val_t http_delete_auth(el_val_t url, el_val_t bearer_tok, el_val_t apikey);
el_val_t supabase_get(el_val_t project_url, el_val_t service_key, el_val_t table_and_query);
el_val_t supabase_insert(el_val_t project_url, el_val_t service_key, el_val_t table, el_val_t row_json);
el_val_t supabase_auth_user(el_val_t project_url, el_val_t anon_key, el_val_t user_jwt);
el_val_t supabase_admin_invite(el_val_t project_url, el_val_t service_key, el_val_t body_json);
el_val_t gcs_write(el_val_t bucket, el_val_t object_name, el_val_t content);
el_val_t gcs_read(el_val_t bucket, el_val_t object_name);
el_val_t cwd(void);
el_val_t color_bold(el_val_t s);
#endif /* EL_SOUL_DEMO_BUILD */
#ifdef __cplusplus
}
#endif
File diff suppressed because it is too large Load Diff
+103
View File
@@ -0,0 +1,103 @@
#!/usr/bin/env python3
"""
run_migrations.py apply pending Supabase migrations via the Management API.
Reads SUPABASE_ACCESS_TOKEN from env (injected by CI from GCP Secret Manager).
Migrations are tracked in a schema_migrations table (created if absent).
Files in migrations/*.sql are applied in lexicographic order; already-applied
files are skipped (idempotent).
"""
import json
import glob
import os
import subprocess
import sys
ACCESS_TOKEN = os.environ.get("SUPABASE_ACCESS_TOKEN", "")
if not ACCESS_TOKEN:
# Fall back to fetching from GCP Secret Manager (for use in CI without
# env var pre-injection).
result = subprocess.run(
[
"gcloud",
"secrets",
"versions",
"access",
"latest",
"--secret=supabase-access-token",
"--project=neuron-785695",
],
capture_output=True,
text=True,
)
if result.returncode != 0:
print(f"ERROR: could not fetch supabase-access-token: {result.stderr}", file=sys.stderr)
sys.exit(1)
ACCESS_TOKEN = result.stdout.strip()
PROJECT_ID = "ocojsghaonltunidkzpw"
API_URL = f"https://api.supabase.com/v1/projects/{PROJECT_ID}/database/query"
def query(sql: str):
r = subprocess.run(
[
"curl",
"-sf",
"-X",
"POST",
API_URL,
"-H",
f"Authorization: Bearer {ACCESS_TOKEN}",
"-H",
"Content-Type: application/json",
"-d",
json.dumps({"query": sql}),
],
capture_output=True,
text=True,
)
if r.returncode != 0:
raise RuntimeError(f"curl failed: {r.stderr}")
resp = json.loads(r.stdout)
# The Management API returns a list of rows on success, or a dict with
# "message" on error.
if isinstance(resp, dict) and resp.get("message") and not isinstance(resp.get("message"), list):
raise RuntimeError(f"DB error: {resp}")
return resp
# Ensure tracking table exists.
query(
"""
CREATE TABLE IF NOT EXISTS schema_migrations (
id text PRIMARY KEY,
applied_at timestamptz DEFAULT now()
)
"""
)
applied = {row["id"] for row in query("SELECT id FROM schema_migrations")}
print(f"Already applied: {sorted(applied)}")
pending = [
p
for p in sorted(glob.glob("migrations/*.sql"))
if os.path.basename(p) not in applied
]
if not pending:
print("No pending migrations.")
sys.exit(0)
for path in pending:
name = os.path.basename(path)
print(f"Applying {name}...")
with open(path) as f:
sql = f.read()
query(sql)
query(f"INSERT INTO schema_migrations (id) VALUES ('{name}')")
print(f"Applied {name}")
print(f"Done. Applied {len(pending)} migration(s).")
+527 -433
View File
File diff suppressed because it is too large Load Diff
+291 -207
View File
@@ -11,6 +11,65 @@
// 4. User fills name, email, card - submits
// 5. stripe.confirmPayment() redirects to /marketplace/success
// el-html vessel extern declarations (implementations in dist/elhtml_impl.c)
extern fn el_escape(s: String) -> String
extern fn el_text(s: String) -> String
extern fn el_attr(name: String, value: String) -> String
extern fn el_div(attrs: String, children: String) -> String
extern fn el_section(attrs: String, children: String) -> String
extern fn el_article(attrs: String, children: String) -> String
extern fn el_header(attrs: String, children: String) -> String
extern fn el_footer(attrs: String, children: String) -> String
extern fn el_main(attrs: String, children: String) -> String
extern fn el_nav(attrs: String, children: String) -> String
extern fn el_aside(attrs: String, children: String) -> String
extern fn el_ul(attrs: String, children: String) -> String
extern fn el_ol(attrs: String, children: String) -> String
extern fn el_li(attrs: String, children: String) -> String
extern fn el_p(attrs: String, children: String) -> String
extern fn el_span(attrs: String, children: String) -> String
extern fn el_form(attrs: String, children: String) -> String
extern fn el_h1(attrs: String, text: String) -> String
extern fn el_h2(attrs: String, text: String) -> String
extern fn el_h3(attrs: String, text: String) -> String
extern fn el_h4(attrs: String, text: String) -> String
extern fn el_button(attrs: String, label: String) -> String
extern fn el_a(href: String, attrs: String, children: String) -> String
extern fn el_input(type_attr: String, attrs: String) -> String
extern fn el_textarea(attrs: String, value: String) -> String
extern fn el_label(for_id: String, attrs: String, children: String) -> String
extern fn el_img(src: String, alt: String, attrs: String) -> String
extern fn el_strong(children: String) -> String
extern fn el_em(children: String) -> String
extern fn el_code(children: String) -> String
extern fn el_pre(attrs: String, children: String) -> String
extern fn el_hr() -> String
extern fn el_br() -> String
extern fn el_html_doc(lang: String, head_html: String, body_html: String) -> String
extern fn el_meta(name: String, content: String) -> String
extern fn el_meta_charset(charset: String) -> String
extern fn el_link_stylesheet(href: String) -> String
extern fn el_script_src(src: String, defer_load: Bool) -> String
extern fn el_script_inline(js: String) -> String
extern fn el_title(text: String) -> String
fn el_style_block(css: String) -> String {
"<style>" + css + "</style>"
}
fn checkout_nav_html() -> String {
let logo_img: String = el_img(
"/assets/brand/neuron-wordmark-on-light.png",
"Neuron",
"srcset=\"/assets/brand/neuron-wordmark-on-light@2x.png 2x\" height=\"28\""
)
let logo_link: String = el_a("/", "class=\"nav-logo\" aria-label=\"Neuron home\"", logo_img)
let back_link: String = el_a("/", "class=\"nav-link\"", "&#8592; Back")
let nav_links: String = el_div("class=\"nav-links\"", back_link)
let nav_inner: String = el_div("class=\"nav-inner\"", logo_link + nav_links)
el_nav("id=\"nav\"", nav_inner)
}
fn checkout_page(plan: String, pub_key: String) -> String {
let is_founding: Bool = str_eq(plan, "founding")
let is_free: Bool = str_eq(plan, "free")
@@ -20,240 +79,273 @@ fn checkout_page(plan: String, pub_key: String) -> String {
let plan_desc: String = if is_founding {
"Pay once. Neuron inference when it launches - priced below the major APIs. No subscription, ever."
} else { if is_free {
"Start building your memory. No card required."
"Start building your memory. A card verifies you're 18+. You won't be charged."
} else {
"Full access. Bring your own API keys or use Neuron Inference when it launches - Q3 2026."
} }
let plan_cadence: String = if is_founding { "one-time" } else { if is_free { "forever" } else { "billed monthly" } }
let features_html: String = if is_founding {
"<li>Everything in Professional - forever</li>
<li>Neuron Inference when it launches - priced below the major APIs, forever</li>
<li>Never pay again - lifetime updates included</li>
<li>Founding member badge in the app</li>
<li>Private founding member community</li>
<li>Shape the roadmap - your votes carry more weight</li>
<li>Beta features before general release</li>
<li>Name in the credits</li>"
el_li("", "Everything in Professional - forever")
+ el_li("", "Neuron Inference when it launches - priced below the major APIs, forever")
+ el_li("", "Never pay again - lifetime updates included")
+ el_li("", "Founding member badge in the app")
+ el_li("", "Private founding member community")
+ el_li("", "Shape the roadmap - your votes carry more weight")
+ el_li("", "Beta features before general release")
+ el_li("", "Name in the credits")
} else { if is_free {
"<li>Persistent memory - never resets</li>
<li>Local inference via Ollama (coming)</li>
<li>Bring your own API keys</li>
<li>3 marketplace plugins included</li>
<li>Core built-in capabilities</li>
<li>2 devices included</li>"
el_li("", "Persistent memory - never resets")
+ el_li("", "Local inference via Ollama (coming)")
+ el_li("", "Bring your own API keys")
+ el_li("", "3 marketplace plugins included")
+ el_li("", "Core built-in capabilities")
+ el_li("", "2 devices included")
} else {
"<li>Persistent memory - never resets</li>
<li>Bring your own API keys (OpenAI, Anthropic, Grok...)</li>
<li>Neuron Inference when it launches - Q3 2026, priced below the major APIs</li>
<li>Unlimited projects</li>
<li>Full plugin marketplace</li>
<li>IDE, Slack, and more integrations</li>
<li>Early access to new features</li>
<li>2 devices included</li>"
el_li("", "Persistent memory - never resets")
+ el_li("", "Bring your own API keys (OpenAI, Anthropic, Grok...)")
+ el_li("", "Neuron Inference when it launches - Q3 2026, priced below the major APIs")
+ el_li("", "Unlimited projects")
+ el_li("", "Full plugin marketplace")
+ el_li("", "IDE, Slack, and more integrations")
+ el_li("", "Early access to new features")
+ el_li("", "2 devices included")
} }
return
<nav id="nav">
<div class="nav-inner">
<a href="/" class="nav-logo" aria-label="Neuron home"><img src="/assets/brand/neuron-wordmark-on-light.png" srcset="/assets/brand/neuron-wordmark-on-light@2x.png 2x" alt="Neuron" height="28"></a>
<div class="nav-links">
<a href="/" class="nav-link">&#8592; Back</a>
</div>
</div>
</nav>
let nav_html: String = checkout_nav_html()
<main style="min-height: 100vh; padding: clamp(6rem, 14vh, 9rem) 2rem 4rem;">
<div class="checkout-shell">
// Order summary (left column)
<!-- Left: order summary -->
<div class="checkout-summary">
<p class="label" style="margin-bottom: 1.5rem; color: var(--navy);">Your order</p>
let price_row: String = el_div(
"style=\"display: flex; align-items: baseline; gap: .5rem; margin-top: 1.25rem;\"",
el_span("class=\"checkout-price\"", el_text(plan_price))
+ el_span("class=\"checkout-cadence\"", el_text(plan_cadence))
)
<div style="margin-bottom: 2rem;">
<p class="checkout-plan-name">{plan_name}</p>
<p class="checkout-plan-desc">{plan_desc}</p>
<div style="display: flex; align-items: baseline; gap: .5rem; margin-top: 1.25rem;">
<span class="checkout-price">{plan_price}</span>
<span class="checkout-cadence">{plan_cadence}</span>
</div>
</div>
let plan_block: String = el_div(
"style=\"margin-bottom: 2rem;\"",
el_p("class=\"checkout-plan-name\"", el_text(plan_name))
+ el_p("class=\"checkout-plan-desc\"", el_text(plan_desc))
+ price_row
)
<div class="navy-line-left" style="width: 3rem; margin-bottom: 1.75rem;"></div>
let summary_col: String = el_div(
"class=\"checkout-summary\"",
el_p("class=\"label\" style=\"margin-bottom: 1.5rem; color: var(--navy);\"", "Your order")
+ plan_block
+ el_div("class=\"navy-line-left\" style=\"width: 3rem; margin-bottom: 1.75rem;\"", "")
+ el_ul("class=\"checkout-features\"", features_html)
+ el_p("class=\"checkout-guarantee\"", "Your data stays yours. Runs locally. No telemetry.")
)
<ul class="checkout-features">
{raw(features_html)}
</ul>
// Auth section
<p class="checkout-guarantee">
Your data stays yours. Runs locally. No telemetry.
</p>
</div>
let auth_heading: String = if is_free {
el_p("class=\"label\" style=\"margin-bottom: 1.5rem; color: var(--navy);\"", "Create your account.")
+ el_p("class=\"checkout-auth-hint\" style=\"margin-bottom: 2rem;\"", "Create your account. We'll ask for a card to verify your age - you won't be charged.")
} else {
el_p("class=\"label\" style=\"margin-bottom: 1.25rem;\"", "Sign in (optional)")
+ el_p("class=\"checkout-auth-hint\"", "Sign in to link this purchase to an existing account. Or skip and create one later - we'll match it to your email.")
}
<!-- Right: either free thank-you or sign-in + payment form -->
<div class="checkout-form-wrap">
let google_svg: String = "<svg width=\"18\" height=\"18\" viewBox=\"0 0 18 18\" fill=\"none\" aria-hidden=\"true\">"
+ "<path d=\"M17.64 9.2c0-.637-.057-1.251-.164-1.84H9v3.481h4.844c-.209 1.125-.843 2.078-1.796 2.716v2.259h2.908c1.702-1.567 2.684-3.875 2.684-6.615z\" fill=\"#4285F4\"/>"
+ "<path d=\"M9 18c2.43 0 4.467-.806 5.956-2.18l-2.908-2.259c-.806.54-1.837.86-3.048.86-2.344 0-4.328-1.584-5.036-3.711H.957v2.332A8.997 8.997 0 0 0 9 18z\" fill=\"#34A853\"/>"
+ "<path d=\"M3.964 10.71A5.41 5.41 0 0 1 3.682 9c0-.593.102-1.17.282-1.71V4.958H.957A8.996 8.996 0 0 0 0 9c0 1.452.348 2.827.957 4.042l3.007-2.332z\" fill=\"#FBBC05\"/>"
+ "<path d=\"M9 3.58c1.321 0 2.508.454 3.44 1.345l2.582-2.58C13.463.891 11.426 0 9 0A8.997 8.997 0 0 0 .957 4.958L3.964 6.29C4.672 4.163 6.656 3.58 9 3.58z\" fill=\"#EA4335\"/>"
+ "</svg>"
<!-- Auth section: visible immediately for free, collapsed (optional) for paid plans -->
<div id=\"auth-section\" " + (if is_free { "" } else { "style=\"display:none;\"" }) + ">
" + (if is_free { "
<p class=\"label\" style=\"margin-bottom: 1.5rem; color: var(--navy);\">Create your account.</p>
<p class=\"checkout-auth-hint\" style=\"margin-bottom: 2rem;\">No card required. Your account is free, forever.</p>
" } else { "
<p class=\"label\" style=\"margin-bottom: 1.25rem;\">Sign in (optional)</p>
<p class=\"checkout-auth-hint\">Sign in to link this purchase to an existing account. Or skip and create one later - we'll match it to your email.</p>
" }) + "
let github_svg: String = "<svg width=\"18\" height=\"18\" viewBox=\"0 0 24 24\" fill=\"currentColor\" aria-hidden=\"true\">"
+ "<path d=\"M12 0C5.37 0 0 5.37 0 12c0 5.303 3.438 9.8 8.205 11.385.6.113.82-.258.82-.577 0-.285-.01-1.04-.015-2.04-3.338.724-4.042-1.61-4.042-1.61-.546-1.385-1.335-1.755-1.335-1.755-1.087-.744.084-.729.084-.729 1.205.084 1.838 1.236 1.838 1.236 1.07 1.835 2.809 1.305 3.495.998.108-.776.417-1.305.76-1.605-2.665-.3-5.466-1.332-5.466-5.93 0-1.31.465-2.38 1.235-3.22-.135-.303-.54-1.523.105-3.176 0 0 1.005-.322 3.3 1.23.96-.267 1.98-.399 3-.405 1.02.006 2.04.138 3 .405 2.28-1.552 3.285-1.23 3.285-1.23.645 1.653.24 2.873.12 3.176.765.84 1.23 1.91 1.23 3.22 0 4.61-2.805 5.625-5.475 5.92.42.36.81 1.096.81 2.22 0 1.605-.015 2.896-.015 3.286 0 .315.21.69.825.57C20.565 21.795 24 17.298 24 12c0-6.63-5.37-12-12-12z\"/>"
+ "</svg>"
<div class="checkout-social-btns">
<button type="button" class="checkout-social-btn" id="btn-google" onclick="signInWith('google')">
<svg width="18" height="18" viewBox="0 0 18 18" fill="none" aria-hidden="true">
<path d="M17.64 9.2c0-.637-.057-1.251-.164-1.84H9v3.481h4.844c-.209 1.125-.843 2.078-1.796 2.716v2.259h2.908c1.702-1.567 2.684-3.875 2.684-6.615z" fill="#4285F4"/>
<path d="M9 18c2.43 0 4.467-.806 5.956-2.18l-2.908-2.259c-.806.54-1.837.86-3.048.86-2.344 0-4.328-1.584-5.036-3.711H.957v2.332A8.997 8.997 0 0 0 9 18z" fill="#34A853"/>
<path d="M3.964 10.71A5.41 5.41 0 0 1 3.682 9c0-.593.102-1.17.282-1.71V4.958H.957A8.996 8.996 0 0 0 0 9c0 1.452.348 2.827.957 4.042l3.007-2.332z" fill="#FBBC05"/>
<path d="M9 3.58c1.321 0 2.508.454 3.44 1.345l2.582-2.58C13.463.891 11.426 0 9 0A8.997 8.997 0 0 0 .957 4.958L3.964 6.29C4.672 4.163 6.656 3.58 9 3.58z" fill="#EA4335"/>
</svg>
Continue with Google
</button>
<button type="button" class="checkout-social-btn" id="btn-github" onclick="signInWith('github')">
<svg width="18" height="18" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true">
<path d="M12 0C5.37 0 0 5.37 0 12c0 5.303 3.438 9.8 8.205 11.385.6.113.82-.258.82-.577 0-.285-.01-1.04-.015-2.04-3.338.724-4.042-1.61-4.042-1.61-.546-1.385-1.335-1.755-1.335-1.755-1.087-.744.084-.729.084-.729 1.205.084 1.838 1.236 1.838 1.236 1.07 1.835 2.809 1.305 3.495.998.108-.776.417-1.305.76-1.605-2.665-.3-5.466-1.332-5.466-5.93 0-1.31.465-2.38 1.235-3.22-.135-.303-.54-1.523.105-3.176 0 0 1.005-.322 3.3 1.23.96-.267 1.98-.399 3-.405 1.02.006 2.04.138 3 .405 2.28-1.552 3.285-1.23 3.285-1.23.645 1.653.24 2.873.12 3.176.765.84 1.23 1.91 1.23 3.22 0 4.61-2.805 5.625-5.475 5.92.42.36.81 1.096.81 2.22 0 1.605-.015 2.896-.015 3.286 0 .315.21.69.825.57C20.565 21.795 24 17.298 24 12c0-6.63-5.37-12-12-12z"/>
</svg>
Continue with GitHub
</button>
</div>
let social_btns: String = el_div(
"class=\"checkout-social-btns\"",
"<button type=\"button\" class=\"checkout-social-btn\" id=\"btn-google\" onclick=\"signInWith('google')\">"
+ google_svg + "Continue with Google</button>"
+ "<button type=\"button\" class=\"checkout-social-btn\" id=\"btn-github\" onclick=\"signInWith('github')\">"
+ github_svg + "Continue with GitHub</button>"
)
<div id="auth-message" class="checkout-message" style="display:none;"></div>
let auth_message: String = el_div("id=\"auth-message\" class=\"checkout-message\" style=\"display:none;\"", "")
<div class="checkout-auth-divider">
<span id="auth-divider-label">or {#if is_free}create an account with email{#else}create an account{/if}</span>
</div>
let divider_label: String = if is_free {
"or create an account with email"
} else {
"or create an account"
}
<div id="email-auth-form">
<input type="email" id="auth-email" class="checkout-input" placeholder="Email address" autocomplete="email" style="width:100%;display:block;margin-bottom:.75rem">
<input type="password" id="auth-password" class="checkout-input" placeholder="Password - min 8 characters" autocomplete="new-password" style="width:100%;display:block;margin-bottom:.75rem">
<button type="button" class="checkout-email-btn" onclick="signUpWithEmail()">{#if is_free}Create account &#8594;{#else}Create account &rarr;{/if}</button>
<p class="checkout-auth-hint" style="margin-top:.75rem;text-align:center">Already have an account? <a href="#" onclick="showSignIn();return false;" style="color:var(--navy)">Sign in</a></p>
</div>
</div>
let auth_divider: String = el_div(
"class=\"checkout-auth-divider\"",
el_span("id=\"auth-divider-label\"", divider_label)
)
<!-- Free-tier success panel: shown after account creation, no card needed -->
" + (if is_free { "
<div id=\"free-success\" style=\"display:none; text-align:center; padding: 2.5rem 1rem;\">
<div style=\"font-size:2.5rem; margin-bottom:1.25rem;\">&#10003;</div>
<p class=\"label\" style=\"margin-bottom:.75rem; color:var(--navy);\">You&#39;re in.</p>
<p class=\"checkout-auth-hint\" style=\"margin-bottom:2rem;\">Your free account is ready. Download Neuron to get started.</p>
<a href=\"/marketplace\" class=\"checkout-submit\" style=\"display:inline-block; text-decoration:none; padding:.875rem 2rem;\">Go to your account &#8594;</a>
</div>
" } else { "" }) + "
let create_btn_label: String = if is_free { "Create account &#8594;" } else { "Create account &rarr;" }
<!-- Payment form (visible immediately - no auth wall) -->
<div id="payment-section" {#if is_free}style="display:none;"{/if}>
<div id="auth-badge" style="display:none; margin-bottom: 1.5rem;"></div>
{#if is_free}{#else}
<p class="checkout-auth-hint" id="signin-prompt" style="margin-bottom:1.25rem;font-size:.8125rem">Already have an account? <a href="#" onclick="document.getElementById('auth-section').style.display='';this.parentNode.style.display='none';return false;" style="color:var(--navy);text-decoration:underline">Sign in</a> to link your purchase.</p>
{/if}
let email_auth_form: String = el_div(
"id=\"email-auth-form\"",
el_input("email", "id=\"auth-email\" class=\"checkout-input\" placeholder=\"Email address\" autocomplete=\"email\" style=\"width:100%;display:block;margin-bottom:.75rem\"")
+ el_input("password", "id=\"auth-password\" class=\"checkout-input\" placeholder=\"Password - min 8 characters\" autocomplete=\"new-password\" style=\"width:100%;display:block;margin-bottom:.75rem\"")
+ "<button type=\"button\" class=\"checkout-email-btn\" onclick=\"signUpWithEmail()\">" + create_btn_label + "</button>"
+ el_p("class=\"checkout-auth-hint\" style=\"margin-top:.75rem;text-align:center\"",
"Already have an account? "
+ el_a("#", "onclick=\"showSignIn();return false;\" style=\"color:var(--navy)\"", "Sign in")
)
)
<!-- Founding Member attestation (only shown for founding plan) -->
{#if is_founding}
<div id="founding-attestation" style="margin-bottom:2rem;padding:1.5rem;border:1px solid rgba(0,82,160,.2);background:rgba(0,82,160,.03)">
<p style="font-family:var(--body);font-weight:500;font-size:.9rem;color:var(--t1);margin-bottom:.75rem">Before you continue</p>
<p style="font-family:var(--body);font-weight:300;font-size:.8375rem;color:var(--t2);line-height:1.75;margin-bottom:1.25rem">Founding Member is not a ceremonial title. These are the people who will work directly with the team to shape what Neuron becomes. You will have a real voice in what gets built, and we will take it seriously. That requires you to show up in good faith.</p>
<label style="display:flex;gap:.875rem;align-items:flex-start;cursor:pointer">
<input type="checkbox" id="founding-attest-cb" style="margin-top:.2rem;width:16px;height:16px;flex-shrink:0;accent-color:var(--navy)">
<span style="font-family:var(--body);font-weight:300;font-size:.8125rem;color:var(--t2);line-height:1.7">
I am joining as a genuine early user, not to extract proprietary information about Neuron&#39;s technology, architecture, or roadmap. I will engage in good faith. I understand that if this is not my intent, a different plan is a better fit.
</span>
</label>
<p id="attest-warn" style="display:none;font-family:var(--body);font-size:.8rem;color:#c44;margin-top:.75rem">Please confirm the above before continuing.</p>
</div>
{/if}
let auth_section_style: String = if is_free { "" } else { "display:none;" }
let auth_section: String = el_div(
"id=\"auth-section\" style=\"" + auth_section_style + "\"",
auth_heading + social_btns + auth_message + auth_divider + email_auth_form
)
<!-- Payment timing: Professional only (not Founding, not Free) -->
{#if is_founding}{#else}{#if is_free}{#else}
<div style="margin-bottom:1.75rem">
<p class="label" style="margin-bottom:1rem">When would you like to be charged?</p>
<div style="display:flex;flex-direction:column;gap:.625rem">
<label class="checkout-timing-opt" id="timing-now-label">
<input type="radio" name="charge-timing" value="now" id="timing-now" checked style="accent-color:var(--navy)">
<div>
<span style="font-family:var(--body);font-weight:500;font-size:.875rem;color:var(--t1)">Charge me now</span>
<span style="font-family:var(--body);font-weight:300;font-size:.8rem;color:var(--t3);display:block;margin-top:.15rem">Paying now signals you're serious. It gives us real signal about what people actually want to use Neuron for - not just who's curious. That shapes what we build first.</span>
</div>
</label>
<label class="checkout-timing-opt" id="timing-later-label">
<input type="radio" name="charge-timing" value="later" id="timing-later" style="accent-color:var(--navy)">
<div>
<span style="font-family:var(--body);font-weight:500;font-size:.875rem;color:var(--t1)">Hold until product launches (Q3 2026)</span>
<span style="font-family:var(--body);font-weight:300;font-size:.8rem;color:var(--t3);display:block;margin-top:.15rem">We save your payment method securely. Nothing charged until launch. Cancel anytime before then.</span>
</div>
</label>
</div>
</div>
{/if}{/if}
// Free-tier success panel
<p class="label" style="margin-bottom: 1.75rem;">Payment</p>
let free_success: String = ""
<form id="payment-form" autocomplete="on">
// Payment section
<div class="checkout-field-group">
<div class="checkout-field">
<label for="buyer-name" class="checkout-label">Full name</label>
<input id="buyer-name" name="name" type="text" autocomplete="name"
class="checkout-input" placeholder="Full name" required>
</div>
<div class="checkout-field">
<label for="buyer-email" class="checkout-label">Email</label>
<input id="buyer-email" name="email" type="email" autocomplete="email"
class="checkout-input" placeholder="you@example.com" required>
</div>
</div>
let payment_section_style: String = if is_free { "display:none;" } else { "" }
<div class="checkout-payment-element-wrap">
<div id="payment-element">
<!-- Stripe Payment Element mounts here -->
<div class="checkout-element-loading">Loading payment form&#8230;</div>
</div>
</div>
let auth_badge: String = el_div("id=\"auth-badge\" style=\"display:none; margin-bottom: 1.5rem;\"", "")
<div id="payment-message" class="checkout-message" style="display:none;"></div>
let signin_prompt: String = if is_free { "" } else {
el_p(
"class=\"checkout-auth-hint\" id=\"signin-prompt\" style=\"margin-bottom:1.25rem;font-size:.8125rem\"",
"Already have an account? "
+ el_a(
"#",
"onclick=\"document.getElementById('auth-section').style.display='';this.parentNode.style.display='none';return false;\" style=\"color:var(--navy);text-decoration:underline\"",
"Sign in"
)
+ " to link your purchase."
)
}
<button id="submit-btn" class="checkout-submit" type="submit" disabled>
<span id="submit-label">{#if is_free}Reserve free tier &#8594;{#else}Complete purchase &#8594;{/if}</span>
<span id="submit-spinner" style="display:none;">Processing&#8230;</span>
</button>
let founding_attestation: String = if is_founding {
el_div(
"id=\"founding-attestation\" style=\"margin-bottom:2rem;padding:1.5rem;border:1px solid rgba(0,82,160,.2);background:rgba(0,82,160,.03)\"",
el_p("style=\"font-family:var(--body);font-weight:500;font-size:.9rem;color:var(--t1);margin-bottom:.75rem\"", "Before you continue")
+ el_p("style=\"font-family:var(--body);font-weight:300;font-size:.8375rem;color:var(--t2);line-height:1.75;margin-bottom:1.25rem\"",
"Founding Member is not a ceremonial title. These are the people who will work directly with the team to shape what Neuron becomes. You will have a real voice in what gets built, and we will take it seriously. That requires you to show up in good faith."
)
+ "<label style=\"display:flex;gap:.875rem;align-items:flex-start;cursor:pointer\">"
+ el_input("checkbox", "id=\"founding-attest-cb\" style=\"margin-top:.2rem;width:16px;height:16px;flex-shrink:0;accent-color:var(--navy)\"")
+ el_span(
"style=\"font-family:var(--body);font-weight:300;font-size:.8125rem;color:var(--t2);line-height:1.7\"",
"I am joining as a genuine early user, not to extract proprietary information about Neuron&#39;s technology, architecture, or roadmap. I will engage in good faith. I understand that if this is not my intent, a different plan is a better fit."
)
+ "</label>"
+ el_p("id=\"attest-warn\" style=\"display:none;font-family:var(--body);font-size:.8rem;color:#c44;margin-top:.75rem\"", "Please confirm the above before continuing.")
)
} else { "" }
<p class="checkout-security">
<svg aria-hidden="true" width="12" height="14" viewBox="0 0 12 14" fill="none">
<path d="M6 0L0 2.5V7c0 3.3 2.5 6.3 6 7 3.5-.7 6-3.7 6-7V2.5L6 0z" fill="currentColor" opacity=".35"/>
<path d="M4 7l1.5 1.5L8.5 5" stroke="currentColor" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
Secured by Stripe &nbsp;&middot;&nbsp; 256-bit TLS &nbsp;&middot;&nbsp; PCI DSS compliant
</p>
let charge_timing: String = if is_founding || is_free { "" } else {
el_div(
"style=\"margin-bottom:1.75rem\"",
el_p("class=\"label\" style=\"margin-bottom:1rem\"", "When would you like to be charged?")
+ el_div(
"style=\"display:flex;flex-direction:column;gap:.625rem\"",
"<label class=\"checkout-timing-opt\" id=\"timing-now-label\">"
+ el_input("radio", "name=\"charge-timing\" value=\"now\" id=\"timing-now\" checked style=\"accent-color:var(--navy)\"")
+ el_div("",
el_span("style=\"font-family:var(--body);font-weight:500;font-size:.875rem;color:var(--t1)\"", "Charge me now")
+ el_span("style=\"font-family:var(--body);font-weight:300;font-size:.8rem;color:var(--t3);display:block;margin-top:.15rem\"",
"Paying now signals you're serious. It gives us real signal about what people actually want to use Neuron for - not just who's curious. That shapes what we build first."
)
)
+ "</label>"
+ "<label class=\"checkout-timing-opt\" id=\"timing-later-label\">"
+ el_input("radio", "name=\"charge-timing\" value=\"later\" id=\"timing-later\" style=\"accent-color:var(--navy)\"")
+ el_div("",
el_span("style=\"font-family:var(--body);font-weight:500;font-size:.875rem;color:var(--t1)\"", "Hold until product launches (Q3 2026)")
+ el_span("style=\"font-family:var(--body);font-weight:300;font-size:.8rem;color:var(--t3);display:block;margin-top:.15rem\"",
"We save your payment method securely. Nothing charged until launch. Cancel anytime before then."
)
)
+ "</label>"
)
)
}
</form>
</div><!-- /payment-section -->
</div>
let security_svg: String = "<svg aria-hidden=\"true\" width=\"12\" height=\"14\" viewBox=\"0 0 12 14\" fill=\"none\">"
+ "<path d=\"M6 0L0 2.5V7c0 3.3 2.5 6.3 6 7 3.5-.7 6-3.7 6-7V2.5L6 0z\" fill=\"currentColor\" opacity=\".35\"/>"
+ "<path d=\"M4 7l1.5 1.5L8.5 5\" stroke=\"currentColor\" stroke-width=\"1.2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"/>"
+ "</svg>"
</div>
</main>
let submit_label: String = if is_free { "Verify age &amp; get started &#8594;" } else { "Complete purchase &#8594;" }
<!-- Supabase JS -->
<script src="https://cdn.jsdelivr.net/npm/@supabase/supabase-js@2/dist/umd/supabase.js"></script>
<!-- Stripe.js -->
<script src="https://js.stripe.com/v3/" async></script>
let payment_form: String = el_form(
"id=\"payment-form\" autocomplete=\"on\"",
el_div(
"class=\"checkout-field-group\"",
el_div("class=\"checkout-field\"",
el_label("buyer-name", "class=\"checkout-label\"", "Full name")
+ el_input("text", "id=\"buyer-name\" name=\"name\" autocomplete=\"name\" class=\"checkout-input\" placeholder=\"Full name\" required")
)
+ el_div("class=\"checkout-field\"",
el_label("buyer-email", "class=\"checkout-label\"", "Email")
+ el_input("email", "id=\"buyer-email\" name=\"email\" autocomplete=\"email\" class=\"checkout-input\" placeholder=\"you@example.com\" required")
)
)
+ el_div(
"class=\"checkout-payment-element-wrap\"",
el_div("id=\"payment-element\"",
el_div("class=\"checkout-element-loading\"", "Loading payment form&#8230;")
)
)
+ el_div("id=\"payment-message\" class=\"checkout-message\" style=\"display:none;\"", "")
+ "<button id=\"submit-btn\" class=\"checkout-submit\" type=\"submit\" disabled>"
+ el_span("id=\"submit-label\"", submit_label)
+ el_span("id=\"submit-spinner\" style=\"display:none;\"", "Processing&#8230;")
+ "</button>"
+ el_p("class=\"checkout-security\"",
security_svg
+ " Secured by Stripe &nbsp;&middot;&nbsp; 256-bit TLS &nbsp;&middot;&nbsp; PCI DSS compliant"
)
)
<style>
.checkout-shell {
max-width: 960px;
margin: 0 auto;
display: grid;
grid-template-columns: 1fr 1fr;
gap: 5rem;
align-items: start;
let payment_section: String = el_div(
"id=\"payment-section\" style=\"" + payment_section_style + "\"",
auth_badge
+ signin_prompt
+ founding_attestation
+ charge_timing
+ el_p("class=\"label\" style=\"margin-bottom: 1.75rem;\"", "Payment")
+ payment_form
)
// Right column
let form_wrap: String = el_div(
"class=\"checkout-form-wrap\"",
auth_section + free_success + payment_section
)
// Full page shell
let checkout_shell: String = el_div("class=\"checkout-shell\"", summary_col + form_wrap)
let main_html: String = el_main(
"style=\"min-height: 100vh; padding: clamp(6rem, 14vh, 9rem) 2rem 4rem;\"",
checkout_shell
)
let style_html: String = checkout_style_html()
let supabase_script: String = el_script_src("https://cdn.jsdelivr.net/npm/@supabase/supabase-js@2/dist/umd/supabase.js", false)
let stripe_script: String = "<script src=\"https://js.stripe.com/v3/\" async></script>"
let auth_script: String = el_script_src("/js/checkout-auth.js", true)
let cfg_js: String = "window.NEURON_CFG=window.NEURON_CFG||{};window.NEURON_CFG.plan=\"" + plan + "\";window.NEURON_CFG.pub_key=\"" + pub_key + "\";"
let cfg_script: String = el_script_inline(cfg_js)
let stripe_el_script: String = el_script_src("/js/checkout-stripe.js", true)
let free_init_script: String = ""
return nav_html + main_html + supabase_script + stripe_script + style_html + auth_script + cfg_script + stripe_el_script + free_init_script
}
@media (max-width: 720px) {
.checkout-shell { grid-template-columns: 1fr; gap: 3rem; }
}
.checkout-plan-name {
fn checkout_style_html() -> String {
let css: String = ".checkout-plan-name {
font-family: var(--head);
font-size: clamp(1.5rem, 3vw, 2rem);
font-weight: 600;
@@ -300,7 +392,7 @@ fn checkout_page(plan: String, pub_key: String) -> String {
position: relative;
}
.checkout-features li::before {
content: '';
content: '-';
position: absolute;
left: 0;
color: var(--navy);
@@ -492,14 +584,6 @@ fn checkout_page(plan: String, pub_key: String) -> String {
font-size: .8125rem;
color: var(--t2);
}
.checkout-auth-badge strong { color: var(--navy); font-weight: 500; }
</style>
<script src="/js/checkout-auth.js" defer></script>
<script>window.NEURON_CFG=window.NEURON_CFG||{};window.NEURON_CFG.plan="{plan}";window.NEURON_CFG.pub_key="{pub_key}";</script><script src="/js/checkout-stripe.js" defer></script>
{#if is_free}
<script>document.addEventListener('DOMContentLoaded',function(){window.neuronCheckoutFree&&window.neuronCheckoutFree()});</script>
{/if}
.checkout-auth-badge strong { color: var(--navy); font-weight: 500; }"
el_style_block(css)
}
+175 -162
View File
@@ -1,166 +1,179 @@
// components/comparison.el - Neuron vs the field comparison section.
fn comparison() -> String {
return <section id="comparison" aria-label="Comparison" style="padding:8rem 2.5rem;background:var(--bg2)">
<div class="container">
extern fn el_section(attrs: String, children: String) -> String
extern fn el_div(attrs: String, children: String) -> String
extern fn el_span(attrs: String, children: String) -> String
extern fn el_h2(attrs: String, text: String) -> String
extern fn el_p(attrs: String, children: String) -> String
extern fn el_img(src: String, alt: String, attrs: String) -> String
extern fn el_em(children: String) -> String
extern fn el_br() -> String
<div class="comparison-header" style="text-align:center;margin-bottom:4rem">
<div style="display:flex;align-items:center;justify-content:center;gap:1.5rem;margin-bottom:2rem">
<div class="navy-line-left" style="width:4rem;flex-shrink:0;transform:scaleX(-1)"></div>
<span class="label reveal" style="color:var(--navy-85)">How we compare</span>
<div class="navy-line-left" style="width:4rem;flex-shrink:0"></div>
</div>
<h2 class="display-lg reveal" style="transition-delay:80ms;max-width:36rem;margin:0 auto 1.25rem">
Every other AI forgets you.<br><span class="gold">Neuron doesn&#39;t.</span>
</h2>
<p class="reveal" style="transition-delay:160ms;font-family:var(--body);font-weight:300;font-size:1rem;color:var(--t2);line-height:1.7;max-width:32rem;margin:0 auto">
The others are tools. Neuron is a relationship. Here&#39;s the difference.
</p>
</div>
<div class="comparison-table reveal" style="transition-delay:200ms;overflow-x:auto;-webkit-overflow-scrolling:touch">
<table style="width:100%;min-width:560px;border-collapse:collapse;font-family:var(--body);font-size:0.875rem">
<thead>
<tr style="border-bottom:2px solid rgba(0,82,160,.20)">
<th style="text-align:left;padding:1rem 1.5rem 1rem 0;font-weight:500;letter-spacing:0.08em;color:var(--t3);font-size:0.75rem;text-transform:uppercase;width:28%">Feature</th>
<th style="text-align:center;padding:0.75rem 1rem;font-weight:600;color:var(--navy);font-size:0.875rem;width:18%">
<div style="display:inline-flex;flex-direction:column;align-items:center;gap:0.35rem">
<img src="/assets/neuron-icon.png" alt="Neuron" style="width:28px;height:28px">
Neuron
</div>
</th>
<th style="text-align:center;padding:0.75rem 1rem;font-weight:400;color:var(--t3);font-size:0.8rem;width:13.5%">
<div style="display:flex;flex-direction:column;align-items:center;gap:0.35rem">
<img src="/assets/brand/openai.svg" alt="ChatGPT" style="width:28px;height:28px">
ChatGPT
</div>
</th>
<th style="text-align:center;padding:0.75rem 1rem;font-weight:400;color:var(--t3);font-size:0.8rem;width:13.5%">
<div style="display:flex;flex-direction:column;align-items:center;gap:0.35rem">
<img src="/assets/brand/anthropic.svg" alt="Claude" style="width:28px;height:28px">
Claude
</div>
</th>
<th style="text-align:center;padding:0.75rem 1rem;font-weight:400;color:var(--t3);font-size:0.8rem;width:13.5%">
<div style="display:flex;flex-direction:column;align-items:center;gap:0.35rem">
<img src="/assets/brand/gemini.svg" alt="Gemini" style="width:28px;height:28px">
Gemini
</div>
</th>
<th style="text-align:center;padding:0.75rem 1rem;font-weight:400;color:var(--t3);font-size:0.8rem;width:13.5%">
<div style="display:flex;flex-direction:column;align-items:center;gap:0.35rem">
<img src="/assets/brand/copilot.svg" alt="Copilot" style="width:28px;height:28px">
Copilot
</div>
</th>
</tr>
</thead>
<tbody>
<tr style="border-bottom:1px solid rgba(0,0,0,.06)">
<td style="padding:1rem 1.5rem 1rem 0;color:var(--t1);font-weight:500">Persistent memory</td>
<td style="text-align:center;padding:1rem"><span style="color:#008040;font-weight:700;font-size:1.1rem">&#10003;</span><br><span style="font-size:0.7rem;color:var(--t3);font-weight:400">Permanent, structured</span></td>
<td style="text-align:center;padding:1rem;color:var(--t3)"><span style="color:#D4A017">&#9679;</span><br><span style="font-size:0.7rem">Limited memory</span></td>
<td style="text-align:center;padding:1rem;color:var(--t3)"><span style="color:#C44">&#10005;</span><br><span style="font-size:0.7rem">Resets each session</span></td>
<td style="text-align:center;padding:1rem;color:var(--t3)"><span style="color:#C44">&#10005;</span><br><span style="font-size:0.7rem">Resets each session</span></td>
<td style="text-align:center;padding:1rem;color:var(--t3)"><span style="color:#C44">&#10005;</span><br><span style="font-size:0.7rem">Resets each session</span></td>
</tr>
<tr style="border-bottom:1px solid rgba(0,0,0,.06);background:rgba(0,82,160,.02)">
<td style="padding:1rem 1.5rem 1rem 0;color:var(--t1);font-weight:500">Your data stays local</td>
<td style="text-align:center;padding:1rem"><span style="color:#008040;font-weight:700;font-size:1.1rem">&#10003;</span><br><span style="font-size:0.7rem;color:var(--t3);font-weight:400">Runs on your device</span></td>
<td style="text-align:center;padding:1rem;color:var(--t3)"><span style="color:#C44">&#10005;</span><br><span style="font-size:0.7rem">Cloud only</span></td>
<td style="text-align:center;padding:1rem;color:var(--t3)"><span style="color:#C44">&#10005;</span><br><span style="font-size:0.7rem">Cloud only</span></td>
<td style="text-align:center;padding:1rem;color:var(--t3)"><span style="color:#C44">&#10005;</span><br><span style="font-size:0.7rem">Cloud only</span></td>
<td style="text-align:center;padding:1rem;color:var(--t3)"><span style="color:#C44">&#10005;</span><br><span style="font-size:0.7rem">Cloud only</span></td>
</tr>
<tr style="border-bottom:1px solid rgba(0,0,0,.06)">
<td style="padding:1rem 1.5rem 1rem 0;color:var(--t1);font-weight:500">No training on your data</td>
<td style="text-align:center;padding:1rem"><span style="color:#008040;font-weight:700;font-size:1.1rem">&#10003;</span><br><span style="font-size:0.7rem;color:var(--t3);font-weight:400">Architecturally impossible</span></td>
<td style="text-align:center;padding:1rem;color:var(--t3)"><span style="color:#D4A017">&#9679;</span><br><span style="font-size:0.7rem">Opt-out required</span></td>
<td style="text-align:center;padding:1rem;color:var(--t3)"><span style="color:#D4A017">&#9679;</span><br><span style="font-size:0.7rem">Policy-based</span></td>
<td style="text-align:center;padding:1rem;color:var(--t3)"><span style="color:#D4A017">&#9679;</span><br><span style="font-size:0.7rem">Policy-based</span></td>
<td style="text-align:center;padding:1rem;color:var(--t3)"><span style="color:#D4A017">&#9679;</span><br><span style="font-size:0.7rem">Policy-based</span></td>
</tr>
<tr style="border-bottom:1px solid rgba(0,0,0,.06);background:rgba(0,82,160,.02)">
<td style="padding:1rem 1.5rem 1rem 0;color:var(--t1);font-weight:500">Works offline</td>
<td style="text-align:center;padding:1rem"><span style="color:#D4A017">&#9679;</span><br><span style="font-size:0.7rem;color:var(--t3);font-weight:400">Coming soon</span></td>
<td style="text-align:center;padding:1rem;color:var(--t3)"><span style="color:#C44">&#10005;</span></td>
<td style="text-align:center;padding:1rem;color:var(--t3)"><span style="color:#C44">&#10005;</span></td>
<td style="text-align:center;padding:1rem;color:var(--t3)"><span style="color:#C44">&#10005;</span></td>
<td style="text-align:center;padding:1rem;color:var(--t3)"><span style="color:#C44">&#10005;</span></td>
</tr>
<tr style="border-bottom:1px solid rgba(0,0,0,.06)">
<td style="padding:1rem 1.5rem 1rem 0;color:var(--t1);font-weight:500">Bring your own API keys</td>
<td style="text-align:center;padding:1rem"><span style="color:#008040;font-weight:700;font-size:1.1rem">&#10003;</span><br><span style="font-size:0.7rem;color:var(--t3);font-weight:400">OpenAI, Anthropic, Grok...</span></td>
<td style="text-align:center;padding:1rem;color:var(--t3)"><span style="color:#C44">&#10005;</span></td>
<td style="text-align:center;padding:1rem;color:var(--t3)"><span style="color:#C44">&#10005;</span></td>
<td style="text-align:center;padding:1rem;color:var(--t3)"><span style="color:#C44">&#10005;</span></td>
<td style="text-align:center;padding:1rem;color:var(--t3)"><span style="color:#C44">&#10005;</span></td>
</tr>
<tr style="border-bottom:1px solid rgba(0,0,0,.06);background:rgba(0,82,160,.02)">
<td style="padding:1rem 1.5rem 1rem 0;color:var(--t1);font-weight:500">Structured knowledge graph</td>
<td style="text-align:center;padding:1rem"><span style="color:#008040;font-weight:700;font-size:1.1rem">&#10003;</span><br><span style="font-size:0.7rem;color:var(--t3);font-weight:400">Your memories, organized</span></td>
<td style="text-align:center;padding:1rem;color:var(--t3)"><span style="color:#C44">&#10005;</span></td>
<td style="text-align:center;padding:1rem;color:var(--t3)"><span style="color:#C44">&#10005;</span></td>
<td style="text-align:center;padding:1rem;color:var(--t3)"><span style="color:#C44">&#10005;</span></td>
<td style="text-align:center;padding:1rem;color:var(--t3)"><span style="color:#C44">&#10005;</span></td>
</tr>
<tr style="border-bottom:1px solid rgba(0,0,0,.06)">
<td style="padding:1rem 1.5rem 1rem 0;color:var(--t1);font-weight:500">Images &amp; video generation</td>
<td style="text-align:center;padding:1rem"><span style="color:#D4A017">&#9679;</span><br><span style="font-size:0.7rem;color:var(--t3);font-weight:400">Coming soon</span></td>
<td style="text-align:center;padding:1rem;color:var(--t3)"><span style="color:#008040;font-weight:700;font-size:1.1rem">&#10003;</span></td>
<td style="text-align:center;padding:1rem;color:var(--t3)"><span style="color:#008040;font-weight:700;font-size:1.1rem">&#10003;</span></td>
<td style="text-align:center;padding:1rem;color:var(--t3)"><span style="color:#008040;font-weight:700;font-size:1.1rem">&#10003;</span></td>
<td style="text-align:center;padding:1rem;color:var(--t3)"><span style="color:#008040;font-weight:700;font-size:1.1rem">&#10003;</span></td>
</tr>
<tr style="border-bottom:1px solid rgba(0,0,0,.06)">
<td style="padding:1rem 1.5rem 1rem 0;color:var(--t1);font-weight:500">You own your outputs</td>
<td style="text-align:center;padding:1rem"><span style="color:#008040;font-weight:700;font-size:1.1rem">&#10003;</span><br><span style="font-size:0.7rem;color:var(--t3);font-weight:400">No platform claim, ever</span></td>
<td style="text-align:center;padding:1rem;color:var(--t3)"><span style="color:#D4A017">&#9679;</span><br><span style="font-size:0.7rem">ToS-dependent</span></td>
<td style="text-align:center;padding:1rem;color:var(--t3)"><span style="color:#D4A017">&#9679;</span><br><span style="font-size:0.7rem">ToS-dependent</span></td>
<td style="text-align:center;padding:1rem;color:var(--t3)"><span style="color:#D4A017">&#9679;</span><br><span style="font-size:0.7rem">ToS-dependent</span></td>
<td style="text-align:center;padding:1rem;color:var(--t3)"><span style="color:#D4A017">&#9679;</span><br><span style="font-size:0.7rem">ToS-dependent</span></td>
</tr>
<tr style="border-bottom:1px solid rgba(0,0,0,.06);background:rgba(0,82,160,.02)">
<td style="padding:1rem 1.5rem 1rem 0;color:var(--t1);font-weight:500">Price (with inference)</td>
<td style="text-align:center;padding:1rem"><span style="color:#008040;font-weight:700;font-size:1rem">$19/mo</span><br><span style="font-size:0.7rem;color:var(--t3);font-weight:400">or $199 founding (first 1,000)</span></td>
<td style="text-align:center;padding:1rem;color:var(--t3)"><span style="font-weight:500;color:var(--t2)">$20/mo</span><br><span style="font-size:0.7rem">forgets you</span></td>
<td style="text-align:center;padding:1rem;color:var(--t3)"><span style="font-weight:500;color:var(--t2)">$20/mo</span><br><span style="font-size:0.7rem">no memory</span></td>
<td style="text-align:center;padding:1rem;color:var(--t3)"><span style="font-weight:500;color:var(--t2)">Free&#8211;$20</span><br><span style="font-size:0.7rem">no memory</span></td>
<td style="text-align:center;padding:1rem;color:var(--t3)"><span style="font-weight:500;color:var(--t2)">$30/mo</span><br><span style="font-size:0.7rem">Microsoft 365</span></td>
</tr>
<tr>
<td style="padding:1rem 1.5rem 1rem 0;color:var(--t1);font-weight:500">Free tier</td>
<td style="text-align:center;padding:1rem"><span style="color:#008040;font-weight:700;font-size:1.1rem">&#10003;</span><br><span style="font-size:0.7rem;color:var(--t3);font-weight:400">Full app, forever</span></td>
<td style="text-align:center;padding:1rem;color:var(--t3)"><span style="color:#D4A017">&#9679;</span><br><span style="font-size:0.7rem">GPT-3.5 only</span></td>
<td style="text-align:center;padding:1rem;color:var(--t3)"><span style="color:#D4A017">&#9679;</span><br><span style="font-size:0.7rem">Limited</span></td>
<td style="text-align:center;padding:1rem;color:var(--t3)"><span style="color:#D4A017">&#9679;</span><br><span style="font-size:0.7rem">Limited</span></td>
<td style="text-align:center;padding:1rem;color:var(--t3)"><span style="color:#D4A017">&#9679;</span><br><span style="font-size:0.7rem">Microsoft 365 required</span></td>
</tr>
</tbody>
</table>
</div>
<div class="reveal" style="transition-delay:300ms;margin-top:3rem;display:flex;align-items:center;justify-content:center;gap:3rem;flex-wrap:wrap">
<div style="display:flex;align-items:center;gap:0.5rem;font-family:var(--body);font-size:0.75rem;color:var(--t3)">
<span style="color:#008040;font-weight:700">&#10003;</span> Yes / Supported
</div>
<div style="display:flex;align-items:center;gap:0.5rem;font-family:var(--body);font-size:0.75rem;color:var(--t3)">
<span style="color:#D4A017">&#9679;</span> Partial / Limited
</div>
<div style="display:flex;align-items:center;gap:0.5rem;font-family:var(--body);font-size:0.75rem;color:var(--t3)">
<span style="color:#C44">&#10005;</span> No / Not supported
</div>
</div>
<div class="reveal" style="transition-delay:400ms;margin-top:4rem;padding:clamp(1.5rem,4vw,2.5rem) clamp(1.25rem,4vw,3rem);background:var(--card);border:1px solid rgba(0,82,160,.12);border-left:3px solid var(--navy)">
<p style="font-family:var(--body);font-weight:300;font-size:1rem;color:var(--t2);line-height:1.8;margin-bottom:1rem">
The others are impressive. Some of them are extraordinary at what they do. But they are all built on the same assumption: <em>your context lives in their cloud, session to session, at their discretion.</em>
</p>
<p style="font-family:var(--body);font-weight:300;font-size:1rem;color:var(--t2);line-height:1.8">
Neuron starts from a different premise. Your memory is yours. It lives on your machine. It compounds over time, not over sessions. The AI that knows you isn&#39;t an AI you borrowed from someone else&#39;s cloud - it&#39;s one that has been building with you, on your terms, since day one.
</p>
</div>
</div>
</section>
fn comparison_header() -> String {
el_div(
"class=\"comparison-header\" style=\"text-align:center;margin-bottom:4rem\"",
el_div(
"style=\"display:flex;align-items:center;justify-content:center;gap:1.5rem;margin-bottom:2rem\"",
el_div("class=\"navy-line-left\" style=\"width:4rem;flex-shrink:0;transform:scaleX(-1)\"", "") +
el_span("class=\"label reveal\" style=\"color:var(--navy-85)\"", "How we compare") +
el_div("class=\"navy-line-left\" style=\"width:4rem;flex-shrink:0\"", "")
) +
el_h2(
"class=\"display-lg reveal\" style=\"transition-delay:80ms;max-width:36rem;margin:0 auto 1.25rem\"",
"Every other AI forgets you." + el_br() + el_span("class=\"gold\"", "Neuron doesn&#39;t.")
) +
el_p("class=\"reveal\" style=\"transition-delay:160ms;font-family:var(--body);font-weight:300;font-size:1rem;color:var(--t2);line-height:1.7;max-width:32rem;margin:0 auto\"",
"The others are tools. Neuron is a relationship. Here&#39;s the difference."
)
)
}
fn comparison_table_head() -> String {
let neuron_th: String = "<th style=\"text-align:center;padding:0.75rem 1rem;font-weight:600;color:var(--navy);font-size:0.875rem;width:18%\">" +
"<div style=\"display:inline-flex;flex-direction:column;align-items:center;gap:0.35rem\">" +
el_img("/assets/neuron-icon.png", "Neuron", "style=\"width:28px;height:28px\"") +
"Neuron</div></th>"
let chatgpt_th: String = "<th style=\"text-align:center;padding:0.75rem 1rem;font-weight:400;color:var(--t3);font-size:0.8rem;width:13.5%\">" +
"<div style=\"display:flex;flex-direction:column;align-items:center;gap:0.35rem\">" +
el_img("/assets/brand/openai.svg", "ChatGPT", "style=\"width:28px;height:28px\"") +
"ChatGPT</div></th>"
let claude_th: String = "<th style=\"text-align:center;padding:0.75rem 1rem;font-weight:400;color:var(--t3);font-size:0.8rem;width:13.5%\">" +
"<div style=\"display:flex;flex-direction:column;align-items:center;gap:0.35rem\">" +
el_img("/assets/brand/anthropic.svg", "Claude", "style=\"width:28px;height:28px\"") +
"Claude</div></th>"
let gemini_th: String = "<th style=\"text-align:center;padding:0.75rem 1rem;font-weight:400;color:var(--t3);font-size:0.8rem;width:13.5%\">" +
"<div style=\"display:flex;flex-direction:column;align-items:center;gap:0.35rem\">" +
el_img("/assets/brand/gemini.svg", "Gemini", "style=\"width:28px;height:28px\"") +
"Gemini</div></th>"
let copilot_th: String = "<th style=\"text-align:center;padding:0.75rem 1rem;font-weight:400;color:var(--t3);font-size:0.8rem;width:13.5%\">" +
"<div style=\"display:flex;flex-direction:column;align-items:center;gap:0.35rem\">" +
el_img("/assets/brand/copilot.svg", "Copilot", "style=\"width:28px;height:28px\"") +
"Copilot</div></th>"
"<thead><tr style=\"border-bottom:2px solid rgba(0,82,160,.20)\">" +
"<th style=\"text-align:left;padding:1rem 1.5rem 1rem 0;font-weight:500;letter-spacing:0.08em;color:var(--t3);font-size:0.75rem;text-transform:uppercase;width:28%\">Feature</th>" +
neuron_th + chatgpt_th + claude_th + gemini_th + copilot_th +
"</tr></thead>"
}
fn comparison_rows() -> String {
let row1: String = "<tr style=\"border-bottom:1px solid rgba(0,0,0,.06)\">" +
"<td style=\"padding:1rem 1.5rem 1rem 0;color:var(--t1);font-weight:500\">Persistent memory</td>" +
"<td style=\"text-align:center;padding:1rem\"><span style=\"color:#008040;font-weight:700;font-size:1.1rem\">&#10003;</span><br><span style=\"font-size:0.7rem;color:var(--t3);font-weight:400\">Permanent, structured</span></td>" +
"<td style=\"text-align:center;padding:1rem;color:var(--t3)\"><span style=\"color:#D4A017\">&#9679;</span><br><span style=\"font-size:0.7rem\">Limited memory</span></td>" +
"<td style=\"text-align:center;padding:1rem;color:var(--t3)\"><span style=\"color:#C44\">&#10005;</span><br><span style=\"font-size:0.7rem\">Resets each session</span></td>" +
"<td style=\"text-align:center;padding:1rem;color:var(--t3)\"><span style=\"color:#C44\">&#10005;</span><br><span style=\"font-size:0.7rem\">Resets each session</span></td>" +
"<td style=\"text-align:center;padding:1rem;color:var(--t3)\"><span style=\"color:#C44\">&#10005;</span><br><span style=\"font-size:0.7rem\">Resets each session</span></td>" +
"</tr>"
let row2: String = "<tr style=\"border-bottom:1px solid rgba(0,0,0,.06);background:rgba(0,82,160,.02)\">" +
"<td style=\"padding:1rem 1.5rem 1rem 0;color:var(--t1);font-weight:500\">Your data stays local</td>" +
"<td style=\"text-align:center;padding:1rem\"><span style=\"color:#008040;font-weight:700;font-size:1.1rem\">&#10003;</span><br><span style=\"font-size:0.7rem;color:var(--t3);font-weight:400\">Runs on your device</span></td>" +
"<td style=\"text-align:center;padding:1rem;color:var(--t3)\"><span style=\"color:#C44\">&#10005;</span><br><span style=\"font-size:0.7rem\">Cloud only</span></td>" +
"<td style=\"text-align:center;padding:1rem;color:var(--t3)\"><span style=\"color:#C44\">&#10005;</span><br><span style=\"font-size:0.7rem\">Cloud only</span></td>" +
"<td style=\"text-align:center;padding:1rem;color:var(--t3)\"><span style=\"color:#C44\">&#10005;</span><br><span style=\"font-size:0.7rem\">Cloud only</span></td>" +
"<td style=\"text-align:center;padding:1rem;color:var(--t3)\"><span style=\"color:#C44\">&#10005;</span><br><span style=\"font-size:0.7rem\">Cloud only</span></td>" +
"</tr>"
let row3: String = "<tr style=\"border-bottom:1px solid rgba(0,0,0,.06)\">" +
"<td style=\"padding:1rem 1.5rem 1rem 0;color:var(--t1);font-weight:500\">No training on your data</td>" +
"<td style=\"text-align:center;padding:1rem\"><span style=\"color:#008040;font-weight:700;font-size:1.1rem\">&#10003;</span><br><span style=\"font-size:0.7rem;color:var(--t3);font-weight:400\">Architecturally impossible</span></td>" +
"<td style=\"text-align:center;padding:1rem;color:var(--t3)\"><span style=\"color:#D4A017\">&#9679;</span><br><span style=\"font-size:0.7rem\">Opt-out required</span></td>" +
"<td style=\"text-align:center;padding:1rem;color:var(--t3)\"><span style=\"color:#D4A017\">&#9679;</span><br><span style=\"font-size:0.7rem\">Policy-based</span></td>" +
"<td style=\"text-align:center;padding:1rem;color:var(--t3)\"><span style=\"color:#D4A017\">&#9679;</span><br><span style=\"font-size:0.7rem\">Policy-based</span></td>" +
"<td style=\"text-align:center;padding:1rem;color:var(--t3)\"><span style=\"color:#D4A017\">&#9679;</span><br><span style=\"font-size:0.7rem\">Policy-based</span></td>" +
"</tr>"
let row4: String = "<tr style=\"border-bottom:1px solid rgba(0,0,0,.06);background:rgba(0,82,160,.02)\">" +
"<td style=\"padding:1rem 1.5rem 1rem 0;color:var(--t1);font-weight:500\">Works offline</td>" +
"<td style=\"text-align:center;padding:1rem\"><span style=\"color:#D4A017\">&#9679;</span><br><span style=\"font-size:0.7rem;color:var(--t3);font-weight:400\">Coming soon</span></td>" +
"<td style=\"text-align:center;padding:1rem;color:var(--t3)\"><span style=\"color:#C44\">&#10005;</span></td>" +
"<td style=\"text-align:center;padding:1rem;color:var(--t3)\"><span style=\"color:#C44\">&#10005;</span></td>" +
"<td style=\"text-align:center;padding:1rem;color:var(--t3)\"><span style=\"color:#C44\">&#10005;</span></td>" +
"<td style=\"text-align:center;padding:1rem;color:var(--t3)\"><span style=\"color:#C44\">&#10005;</span></td>" +
"</tr>"
let row5: String = "<tr style=\"border-bottom:1px solid rgba(0,0,0,.06)\">" +
"<td style=\"padding:1rem 1.5rem 1rem 0;color:var(--t1);font-weight:500\">Bring your own API keys</td>" +
"<td style=\"text-align:center;padding:1rem\"><span style=\"color:#008040;font-weight:700;font-size:1.1rem\">&#10003;</span><br><span style=\"font-size:0.7rem;color:var(--t3);font-weight:400\">OpenAI, Anthropic, Grok...</span></td>" +
"<td style=\"text-align:center;padding:1rem;color:var(--t3)\"><span style=\"color:#C44\">&#10005;</span></td>" +
"<td style=\"text-align:center;padding:1rem;color:var(--t3)\"><span style=\"color:#C44\">&#10005;</span></td>" +
"<td style=\"text-align:center;padding:1rem;color:var(--t3)\"><span style=\"color:#C44\">&#10005;</span></td>" +
"<td style=\"text-align:center;padding:1rem;color:var(--t3)\"><span style=\"color:#C44\">&#10005;</span></td>" +
"</tr>"
let row6: String = "<tr style=\"border-bottom:1px solid rgba(0,0,0,.06);background:rgba(0,82,160,.02)\">" +
"<td style=\"padding:1rem 1.5rem 1rem 0;color:var(--t1);font-weight:500\">Structured knowledge graph</td>" +
"<td style=\"text-align:center;padding:1rem\"><span style=\"color:#008040;font-weight:700;font-size:1.1rem\">&#10003;</span><br><span style=\"font-size:0.7rem;color:var(--t3);font-weight:400\">Your memories, organized</span></td>" +
"<td style=\"text-align:center;padding:1rem;color:var(--t3)\"><span style=\"color:#C44\">&#10005;</span></td>" +
"<td style=\"text-align:center;padding:1rem;color:var(--t3)\"><span style=\"color:#C44\">&#10005;</span></td>" +
"<td style=\"text-align:center;padding:1rem;color:var(--t3)\"><span style=\"color:#C44\">&#10005;</span></td>" +
"<td style=\"text-align:center;padding:1rem;color:var(--t3)\"><span style=\"color:#C44\">&#10005;</span></td>" +
"</tr>"
let row7: String = "<tr style=\"border-bottom:1px solid rgba(0,0,0,.06)\">" +
"<td style=\"padding:1rem 1.5rem 1rem 0;color:var(--t1);font-weight:500\">Images &amp; video generation</td>" +
"<td style=\"text-align:center;padding:1rem\"><span style=\"color:#D4A017\">&#9679;</span><br><span style=\"font-size:0.7rem;color:var(--t3);font-weight:400\">Coming soon</span></td>" +
"<td style=\"text-align:center;padding:1rem;color:var(--t3)\"><span style=\"color:#008040;font-weight:700;font-size:1.1rem\">&#10003;</span></td>" +
"<td style=\"text-align:center;padding:1rem;color:var(--t3)\"><span style=\"color:#008040;font-weight:700;font-size:1.1rem\">&#10003;</span></td>" +
"<td style=\"text-align:center;padding:1rem;color:var(--t3)\"><span style=\"color:#008040;font-weight:700;font-size:1.1rem\">&#10003;</span></td>" +
"<td style=\"text-align:center;padding:1rem;color:var(--t3)\"><span style=\"color:#008040;font-weight:700;font-size:1.1rem\">&#10003;</span></td>" +
"</tr>"
let row8: String = "<tr style=\"border-bottom:1px solid rgba(0,0,0,.06)\">" +
"<td style=\"padding:1rem 1.5rem 1rem 0;color:var(--t1);font-weight:500\">You own your outputs</td>" +
"<td style=\"text-align:center;padding:1rem\"><span style=\"color:#008040;font-weight:700;font-size:1.1rem\">&#10003;</span><br><span style=\"font-size:0.7rem;color:var(--t3);font-weight:400\">No platform claim, ever</span></td>" +
"<td style=\"text-align:center;padding:1rem;color:var(--t3)\"><span style=\"color:#D4A017\">&#9679;</span><br><span style=\"font-size:0.7rem\">ToS-dependent</span></td>" +
"<td style=\"text-align:center;padding:1rem;color:var(--t3)\"><span style=\"color:#D4A017\">&#9679;</span><br><span style=\"font-size:0.7rem\">ToS-dependent</span></td>" +
"<td style=\"text-align:center;padding:1rem;color:var(--t3)\"><span style=\"color:#D4A017\">&#9679;</span><br><span style=\"font-size:0.7rem\">ToS-dependent</span></td>" +
"<td style=\"text-align:center;padding:1rem;color:var(--t3)\"><span style=\"color:#D4A017\">&#9679;</span><br><span style=\"font-size:0.7rem\">ToS-dependent</span></td>" +
"</tr>"
let row9: String = "<tr style=\"border-bottom:1px solid rgba(0,0,0,.06);background:rgba(0,82,160,.02)\">" +
"<td style=\"padding:1rem 1.5rem 1rem 0;color:var(--t1);font-weight:500\">Price (with inference)</td>" +
"<td style=\"text-align:center;padding:1rem\"><span style=\"color:#008040;font-weight:700;font-size:1rem\">$19/mo</span><br><span style=\"font-size:0.7rem;color:var(--t3);font-weight:400\">or $199 founding (first 1,000)</span></td>" +
"<td style=\"text-align:center;padding:1rem;color:var(--t3)\"><span style=\"font-weight:500;color:var(--t2)\">$20/mo</span><br><span style=\"font-size:0.7rem\">forgets you</span></td>" +
"<td style=\"text-align:center;padding:1rem;color:var(--t3)\"><span style=\"font-weight:500;color:var(--t2)\">$20/mo</span><br><span style=\"font-size:0.7rem\">no memory</span></td>" +
"<td style=\"text-align:center;padding:1rem;color:var(--t3)\"><span style=\"font-weight:500;color:var(--t2)\">Free&#8211;$20</span><br><span style=\"font-size:0.7rem\">no memory</span></td>" +
"<td style=\"text-align:center;padding:1rem;color:var(--t3)\"><span style=\"font-weight:500;color:var(--t2)\">$30/mo</span><br><span style=\"font-size:0.7rem\">Microsoft 365</span></td>" +
"</tr>"
let row10: String = "<tr>" +
"<td style=\"padding:1rem 1.5rem 1rem 0;color:var(--t1);font-weight:500\">Free tier</td>" +
"<td style=\"text-align:center;padding:1rem\"><span style=\"color:#008040;font-weight:700;font-size:1.1rem\">&#10003;</span><br><span style=\"font-size:0.7rem;color:var(--t3);font-weight:400\">Full app, forever</span></td>" +
"<td style=\"text-align:center;padding:1rem;color:var(--t3)\"><span style=\"color:#D4A017\">&#9679;</span><br><span style=\"font-size:0.7rem\">GPT-3.5 only</span></td>" +
"<td style=\"text-align:center;padding:1rem;color:var(--t3)\"><span style=\"color:#D4A017\">&#9679;</span><br><span style=\"font-size:0.7rem\">Limited</span></td>" +
"<td style=\"text-align:center;padding:1rem;color:var(--t3)\"><span style=\"color:#D4A017\">&#9679;</span><br><span style=\"font-size:0.7rem\">Limited</span></td>" +
"<td style=\"text-align:center;padding:1rem;color:var(--t3)\"><span style=\"color:#D4A017\">&#9679;</span><br><span style=\"font-size:0.7rem\">Microsoft 365 required</span></td>" +
"</tr>"
"<tbody>" + row1 + row2 + row3 + row4 + row5 + row6 + row7 + row8 + row9 + row10 + "</tbody>"
}
fn comparison() -> String {
let table: String = el_div(
"class=\"comparison-table reveal\" style=\"transition-delay:200ms;overflow-x:auto;-webkit-overflow-scrolling:touch\"",
"<table style=\"width:100%;min-width:560px;border-collapse:collapse;font-family:var(--body);font-size:0.875rem\">" +
comparison_table_head() +
comparison_rows() +
"</table>"
)
let legend: String = el_div(
"class=\"reveal\" style=\"transition-delay:300ms;margin-top:3rem;display:flex;align-items:center;justify-content:center;gap:3rem;flex-wrap:wrap\"",
el_div("style=\"display:flex;align-items:center;gap:0.5rem;font-family:var(--body);font-size:0.75rem;color:var(--t3)\"",
"<span style=\"color:#008040;font-weight:700\">&#10003;</span> Yes / Supported"
) +
el_div("style=\"display:flex;align-items:center;gap:0.5rem;font-family:var(--body);font-size:0.75rem;color:var(--t3)\"",
"<span style=\"color:#D4A017\">&#9679;</span> Partial / Limited"
) +
el_div("style=\"display:flex;align-items:center;gap:0.5rem;font-family:var(--body);font-size:0.75rem;color:var(--t3)\"",
"<span style=\"color:#C44\">&#10005;</span> No / Not supported"
)
)
let closing: String = el_div(
"class=\"reveal\" style=\"transition-delay:400ms;margin-top:4rem;padding:clamp(1.5rem,4vw,2.5rem) clamp(1.25rem,4vw,3rem);background:var(--card);border:1px solid rgba(0,82,160,.12);border-left:3px solid var(--navy)\"",
el_p("style=\"font-family:var(--body);font-weight:300;font-size:1rem;color:var(--t2);line-height:1.8;margin-bottom:1rem\"",
"The others are impressive. Some of them are extraordinary at what they do. But they are all built on the same assumption: " +
el_em("your context lives in their cloud, session to session, at their discretion.")
) +
el_p("style=\"font-family:var(--body);font-weight:300;font-size:1rem;color:var(--t2);line-height:1.8\"",
"Neuron starts from a different premise. Your memory is yours. It lives on your machine. It compounds over time, not over sessions. The AI that knows you isn&#39;t an AI you borrowed from someone else&#39;s cloud - it&#39;s one that has been building with you, on your terms, since day one."
)
)
el_section(
"id=\"comparison\" aria-label=\"Comparison\" style=\"padding:8rem 2.5rem;background:var(--bg2)\"",
el_div("class=\"container\"", comparison_header() + table + legend + closing)
)
}
+76 -61
View File
@@ -1,74 +1,89 @@
// components/efficiency.el - Token efficiency and environment section.
// Three cards: local inference / per-task routing / context compression.
extern fn el_section(attrs: String, children: String) -> String
extern fn el_div(attrs: String, children: String) -> String
extern fn el_span(attrs: String, children: String) -> String
extern fn el_h2(attrs: String, text: String) -> String
extern fn el_p(attrs: String, children: String) -> String
fn efficiency() -> String {
return <section id="efficiency" aria-label="Token efficiency and environment">
<div class="container">
let header: String = el_div(
"class=\"efficiency-header\"",
el_div(
"class=\"efficiency-label-row reveal\"",
el_div("class=\"navy-line-left\" style=\"width:4rem;flex-shrink:0\"", "") +
el_span("class=\"label\"", "Efficiency")
) +
el_h2(
"class=\"display-lg reveal\" style=\"transition-delay:80ms;max-width:32rem\"",
"Every token spent " + el_span("class=\"gold\"", "is a choice.")
) +
el_p(
"class=\"efficiency-sub reveal\" style=\"transition-delay:160ms;margin-top:1.25rem\"",
"Cost and environmental impact aren&#39;t afterthoughts - they&#39;re structural properties of the architecture. I built it that way from the start."
)
)
<div class="efficiency-header">
<div class="efficiency-label-row reveal">
<div class="navy-line-left" style="width:4rem;flex-shrink:0"></div>
<span class="label">Efficiency</span>
</div>
<h2 class="display-lg reveal" style="transition-delay:80ms;max-width:32rem">
Every token spent <span class="gold">is a choice.</span>
</h2>
<p class="efficiency-sub reveal" style="transition-delay:160ms;margin-top:1.25rem">
Cost and environmental impact aren&#39;t afterthoughts - they&#39;re structural properties of the architecture. I built it that way from the start.
</p>
</div>
let svg1: String = "<svg width=\"28\" height=\"28\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\">" +
"<path d=\"M12 22C6.477 22 2 17.523 2 12S6.477 2 12 2s10 4.477 10 10-4.477 10-10 10z\"/>" +
"<path d=\"M8 12s1.5-2 4-2 4 2 4 2\"/>" +
"<path d=\"M9 9h.01M15 9h.01\"/>" +
"<path d=\"M12 17c-1.5 0-3-.5-3-1.5S10 14 12 14s3 .5 3 1.5S13.5 17 12 17z\"/>" +
"</svg>"
<div class="efficiency-grid">
let card1: String = el_div(
"class=\"efficiency-card card-dark reveal\"",
el_div("class=\"efficiency-icon\"", svg1) +
el_p("class=\"efficiency-stat\"", "0 cloud tokens") +
el_p("class=\"efficiency-title\"", "Local inference") +
el_div("class=\"efficiency-rule\"", "") +
el_p("class=\"efficiency-body\"", "The design: run inference entirely on-device via Ollama. No API calls, no inference cost, no carbon footprint from model compute. Full memory and context, zero cloud dependency. This is coming.")
)
<div class="efficiency-card card-dark reveal">
<div class="efficiency-icon">
<svg width="28" height="28" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
<path d="M12 22C6.477 22 2 17.523 2 12S6.477 2 12 2s10 4.477 10 10-4.477 10-10 10z"/>
<path d="M8 12s1.5-2 4-2 4 2 4 2"/>
<path d="M9 9h.01M15 9h.01"/>
<path d="M12 17c-1.5 0-3-.5-3-1.5S10 14 12 14s3 .5 3 1.5S13.5 17 12 17z"/>
</svg>
</div>
<p class="efficiency-stat">0 cloud tokens</p>
<p class="efficiency-title">Local inference</p>
<div class="efficiency-rule"></div>
<p class="efficiency-body">The design: run inference entirely on-device via Ollama. No API calls, no inference cost, no carbon footprint from model compute. Full memory and context, zero cloud dependency. This is coming.</p>
</div>
let svg2: String = "<svg width=\"28\" height=\"28\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\">" +
"<circle cx=\"6\" cy=\"18\" r=\"2\"/><circle cx=\"18\" cy=\"6\" r=\"2\"/><circle cx=\"18\" cy=\"18\" r=\"2\"/>" +
"<path d=\"M6 16V8l6-4 6 4\"/><path d=\"M6 16l6 4 6-4\"/><path d=\"M12 4v16\"/>" +
"</svg>"
<div class="efficiency-card card-dark reveal" style="transition-delay:150ms">
<div class="efficiency-icon">
<svg width="28" height="28" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
<circle cx="6" cy="18" r="2"/><circle cx="18" cy="6" r="2"/><circle cx="18" cy="18" r="2"/>
<path d="M6 16V8l6-4 6 4"/><path d="M6 16l6 4 6-4"/><path d="M12 4v16"/>
</svg>
</div>
<p class="efficiency-stat">Use less</p>
<p class="efficiency-title">Per-task routing</p>
<div class="efficiency-rule"></div>
<p class="efficiency-body">Simple tasks route to small, fast models. Complex reasoning escalates to frontier models only when necessary. And because every model has full access to your accumulated context, cheaper models punch well above their weight.</p>
</div>
let card2: String = el_div(
"class=\"efficiency-card card-dark reveal\" style=\"transition-delay:150ms\"",
el_div("class=\"efficiency-icon\"", svg2) +
el_p("class=\"efficiency-stat\"", "Use less") +
el_p("class=\"efficiency-title\"", "Per-task routing") +
el_div("class=\"efficiency-rule\"", "") +
el_p("class=\"efficiency-body\"", "Simple tasks route to small, fast models. Complex reasoning escalates to frontier models only when necessary. And because every model has full access to your accumulated context, cheaper models punch well above their weight.")
)
<div class="efficiency-card card-dark reveal" style="transition-delay:300ms">
<div class="efficiency-icon">
<svg width="28" height="28" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
<rect x="4" y="4" width="16" height="16" rx="2"/>
<path d="M9 9h6M9 12h6M9 15h4"/>
<path d="M16 15l2 2 2-2"/>
</svg>
</div>
<p class="efficiency-stat">Fewer tokens</p>
<p class="efficiency-title">Same work done</p>
<div class="efficiency-rule"></div>
<p class="efficiency-body">Every time you open ChatGPT and explain who you are again, that&#39;s computation that didn&#39;t need to happen. Persistent context means shorter, more targeted prompts. The same outcome with less compute - and a smaller footprint on the planet.</p>
</div>
let svg3: String = "<svg width=\"28\" height=\"28\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\">" +
"<rect x=\"4\" y=\"4\" width=\"16\" height=\"16\" rx=\"2\"/>" +
"<path d=\"M9 9h6M9 12h6M9 15h4\"/>" +
"<path d=\"M16 15l2 2 2-2\"/>" +
"</svg>"
</div>
let card3: String = el_div(
"class=\"efficiency-card card-dark reveal\" style=\"transition-delay:300ms\"",
el_div("class=\"efficiency-icon\"", svg3) +
el_p("class=\"efficiency-stat\"", "Fewer tokens") +
el_p("class=\"efficiency-title\"", "Same work done") +
el_div("class=\"efficiency-rule\"", "") +
el_p("class=\"efficiency-body\"", "Every time you open ChatGPT and explain who you are again, that&#39;s computation that didn&#39;t need to happen. Persistent context means shorter, more targeted prompts. The same outcome with less compute - and a smaller footprint on the planet.")
)
<div class="efficiency-pullquote reveal">
<p>The frontier model without memory of you is starting from scratch every time. A smaller, faster model with years of accumulated context on your work, your decisions, and your patterns will outperform it. The intelligence isn&#39;t in the model - it&#39;s in what the model knows about you.</p>
</div>
let grid: String = el_div("class=\"efficiency-grid\"", card1 + card2 + card3)
</div>
<div class="container" style="margin-top:5rem"><div class="navy-line"></div></div>
</section>
let pullquote: String = el_div(
"class=\"efficiency-pullquote reveal\"",
el_p("", "The frontier model without memory of you is starting from scratch every time. A smaller, faster model with years of accumulated context on your work, your decisions, and your patterns will outperform it. The intelligence isn&#39;t in the model - it&#39;s in what the model knows about you.")
)
let bottom_line: String = el_div(
"class=\"container\" style=\"margin-top:5rem\"",
el_div("class=\"navy-line\"", "")
)
el_section(
"id=\"efficiency\" aria-label=\"Token efficiency and environment\"",
el_div("class=\"container\"", header + grid + pullquote) + bottom_line
)
}
+45
View File
@@ -0,0 +1,45 @@
// elhtml.el extern declarations for the el-html vessel.
// The implementations live in dist/elhtml.c (pre-compiled).
// Import this file instead of the vessel source directly.
extern fn el_escape(s: String) -> String
extern fn el_text(s: String) -> String
extern fn el_attr(name: String, value: String) -> String
extern fn el_div(attrs: String, children: String) -> String
extern fn el_section(attrs: String, children: String) -> String
extern fn el_article(attrs: String, children: String) -> String
extern fn el_header(attrs: String, children: String) -> String
extern fn el_footer(attrs: String, children: String) -> String
extern fn el_main(attrs: String, children: String) -> String
extern fn el_nav(attrs: String, children: String) -> String
extern fn el_aside(attrs: String, children: String) -> String
extern fn el_ul(attrs: String, children: String) -> String
extern fn el_ol(attrs: String, children: String) -> String
extern fn el_li(attrs: String, children: String) -> String
extern fn el_p(attrs: String, children: String) -> String
extern fn el_span(attrs: String, children: String) -> String
extern fn el_form(attrs: String, children: String) -> String
extern fn el_h1(attrs: String, text: String) -> String
extern fn el_h2(attrs: String, text: String) -> String
extern fn el_h3(attrs: String, text: String) -> String
extern fn el_h4(attrs: String, text: String) -> String
extern fn el_button(attrs: String, label: String) -> String
extern fn el_a(href: String, attrs: String, children: String) -> String
extern fn el_input(type_attr: String, attrs: String) -> String
extern fn el_textarea(attrs: String, value: String) -> String
extern fn el_label(for_id: String, attrs: String, children: String) -> String
extern fn el_img(src: String, alt: String, attrs: String) -> String
extern fn el_video(attrs: String, children: String) -> String
extern fn el_strong(children: String) -> String
extern fn el_em(children: String) -> String
extern fn el_code(children: String) -> String
extern fn el_pre(attrs: String, children: String) -> String
extern fn el_hr() -> String
extern fn el_br() -> String
extern fn el_html_doc(lang: String, head_html: String, body_html: String) -> String
extern fn el_meta(name: String, content: String) -> String
extern fn el_meta_charset(charset: String) -> String
extern fn el_link_stylesheet(href: String) -> String
extern fn el_script_src(src: String, defer_load: Bool) -> String
extern fn el_script_inline(js: String) -> String
extern fn el_title(text: String) -> String
+269 -169
View File
@@ -1,174 +1,270 @@
// components/enterprise.el - Enterprise section.
// Four capability cards + "Who I work with" box + how-it-works steps.
extern fn el_section(attrs: String, children: String) -> String
extern fn el_div(attrs: String, children: String) -> String
extern fn el_span(attrs: String, children: String) -> String
extern fn el_h2(attrs: String, text: String) -> String
extern fn el_h3(attrs: String, text: String) -> String
extern fn el_p(attrs: String, children: String) -> String
extern fn el_a(href: String, attrs: String, children: String) -> String
extern fn el_form(attrs: String, children: String) -> String
extern fn el_label(for_id: String, attrs: String, children: String) -> String
extern fn el_input(type_attr: String, attrs: String) -> String
extern fn el_textarea(attrs: String, value: String) -> String
extern fn el_button(attrs: String, label: String) -> String
extern fn el_script_src(src: String, defer_load: Bool) -> String
fn enterprise_cap_cards() -> String {
let card1: String = el_div(
"class=\"enterprise-cap card-dark reveal\"",
el_h3("class=\"display-md\"", "Private deployment") +
el_div("class=\"enterprise-cap-rule\"", "") +
el_p("class=\"enterprise-cap-body\"", "Run entirely on your infrastructure. Air-gapped. No data leaves your network. Every employee gets their own Neuron instance - your company&#39;s institutional knowledge stays inside your walls.")
)
let card2: String = el_div(
"class=\"enterprise-cap card-dark reveal\" style=\"transition-delay:130ms\"",
el_h3("class=\"display-md\"", "Institutional memory") +
el_div("class=\"enterprise-cap-rule\"", "") +
el_p("class=\"enterprise-cap-body\"", "When an employee leaves, their expertise doesn&#39;t. Their memory persists - their patterns, their domain knowledge, their reasoning - available to the team they built it with.")
)
let card3: String = el_div(
"class=\"enterprise-cap card-dark reveal\" style=\"transition-delay:260ms\"",
el_h3("class=\"display-md\"", "Team intelligence") +
el_div("class=\"enterprise-cap-rule\"", "") +
el_p("class=\"enterprise-cap-body\"", "Shared knowledge packages and cross-instance coordination. The collective intelligence of your organization compounds the same way an individual&#39;s does.")
)
let card4: String = el_div(
"class=\"enterprise-cap card-dark reveal\" style=\"transition-delay:390ms\"",
el_h3("class=\"display-md\"", "Compliance architecture") +
el_div("class=\"enterprise-cap-rule\"", "") +
el_p("class=\"enterprise-cap-body\"", "Custom on-device storage. No cloud database with your data. SOC 2 alignment built into the data model. ExternalSecret-based secrets management. Audit logs at every layer.")
)
el_div("class=\"enterprise-grid\"", card1 + card2 + card3 + card4)
}
fn enterprise_how_it_works() -> String {
let step1: String = el_div(
"",
el_p("class=\"ent-step-num\"", "01") +
el_p("class=\"ent-step-title\"", "Express interest") +
el_p("class=\"ent-step-body\"", "Send me a note. I review every inquiry myself - no sales funnel, no SDR. If it&#39;s a fit, I respond directly.")
)
let step2: String = el_div(
"",
el_p("class=\"ent-step-num\"", "02") +
el_p("class=\"ent-step-title\"", "Scoped deployment") +
el_p("class=\"ent-step-body\"", "I work with your team to scope a private deployment - on your infrastructure, in your network, with your security requirements.")
)
let step3: String = el_div(
"",
el_p("class=\"ent-step-num\"", "03") +
el_p("class=\"ent-step-title\"", "Per-seat licensing") +
el_p("class=\"ent-step-body\"", "Annual per-seat pricing with a volume floor. Custom SLA available. The full Agreement is published before any conversation starts.")
)
el_div(
"class=\"ent-how-row\"",
el_div(
"class=\"ent-how-label\"",
el_h3("class=\"display-md\" style=\"font-size:1.25rem\"", "How it works")
) +
el_div("class=\"ent-steps\"", step1 + step2 + step3)
)
}
fn enterprise_inquiry_form() -> String {
let field_style: String = "font-family:var(--body);font-size:0.9rem;font-weight:300;color:var(--t1);background:#fff;border:1px solid rgba(0,82,160,.22);padding:0.7rem 0.9rem;outline:none;border-radius:0;-webkit-appearance:none;width:100%;box-sizing:border-box"
let label_style: String = "font-family:var(--body);font-size:0.7rem;font-weight:500;letter-spacing:0.08em;text-transform:uppercase;color:var(--t3)"
let field_name: String = el_div(
"style=\"display:flex;flex-direction:column;gap:0.4rem\"",
el_label("ent-name", "style=\"" + label_style + "\"", "Name") +
el_input("text", "id=\"ent-name\" name=\"name\" required placeholder=\"Your name\" style=\"" + field_style + "\"")
)
let field_email: String = el_div(
"style=\"display:flex;flex-direction:column;gap:0.4rem\"",
el_label("ent-email", "style=\"" + label_style + "\"", "Work email") +
el_input("email", "id=\"ent-email\" name=\"email\" required placeholder=\"you@company.com\" style=\"" + field_style + "\"")
)
let field_company: String = el_div(
"style=\"display:flex;flex-direction:column;gap:0.4rem\"",
el_label("ent-company", "style=\"" + label_style + "\"", "Organization") +
el_input("text", "id=\"ent-company\" name=\"company\" required placeholder=\"Company or organization name\" style=\"" + field_style + "\"")
)
let field_size: String = el_div(
"style=\"display:flex;flex-direction:column;gap:0.4rem\"",
el_label("ent-size", "style=\"" + label_style + "\"", "Team size") +
"<select id=\"ent-size\" name=\"size\" required style=\"" + field_style + ";cursor:pointer\">" +
"<option value=\"\">Select&hellip;</option>" +
"<option value=\"2-10\">2&#8211;10 people</option>" +
"<option value=\"11-50\">11&#8211;50 people</option>" +
"<option value=\"51-200\">51&#8211;200 people</option>" +
"<option value=\"201-500\">201&#8211;500 people</option>" +
"<option value=\"500+\">500+ people</option>" +
"</select>"
)
let field_use: String = el_div(
"style=\"grid-column:1/-1;display:flex;flex-direction:column;gap:0.4rem\"",
el_label("ent-use", "style=\"" + label_style + "\"", "What do you want to use Neuron for?") +
el_textarea("id=\"ent-use\" name=\"use_case\" required rows=\"3\" placeholder=\"Describe your intended use case - be specific.\" style=\"" + field_style + ";resize:vertical\"", "")
)
let filter_msg_secondary: String = el_p("id=\"ent-filter-msg-secondary\" style=\"display:none;font-family:var(--body);font-size:0.875rem;font-weight:300;color:rgba(0,82,160,.75);line-height:1.65;padding:0.75rem 1rem;background:rgba(0,82,160,.04);border-left:2px solid rgba(0,82,160,.3)\"",
"I review these personally. I&#39;ll consider it - but I want to be direct with you: if headcount reduction is in the picture at all, even as a secondary outcome, your chances here are low. That&#39;s not a negotiating position. It&#39;s how I built this and what I&#39;m willing to support."
)
let filter_msg_yes: String = el_p("id=\"ent-filter-msg-yes\" style=\"display:none;font-family:var(--body);font-size:0.875rem;font-weight:300;color:#c0392b;line-height:1.65;padding:0.75rem 1rem;background:rgba(192,57,43,.05);border-left:2px solid rgba(192,57,43,.4)\"",
"Neuron isn&#39;t the right fit here. I built this to expand what people can do, not to replace them. If that changes, I&#39;m here."
)
let field_headcount: String = el_div(
"style=\"grid-column:1/-1;display:flex;flex-direction:column;gap:0.4rem\"",
el_label("ent-headcount", "style=\"" + label_style + "\"", "Is reducing headcount a primary goal of this deployment?") +
"<select id=\"ent-headcount\" name=\"headcount\" required onchange=\"entHeadcountChange(this.value)\" style=\"" + field_style + ";cursor:pointer\">" +
"<option value=\"\">Select&hellip;</option>" +
"<option value=\"no\">No - making our team more capable</option>" +
"<option value=\"secondary\">It&#39;s a secondary factor, not the primary goal</option>" +
"<option value=\"yes\">Yes - replacing roles with automation</option>" +
"</select>" +
filter_msg_secondary +
filter_msg_yes
)
let error_div: String = el_div("id=\"ent-form-error\" style=\"grid-column:1/-1;display:none;font-family:var(--body);font-size:0.875rem;color:#c0392b;padding:0.75rem 1rem;background:rgba(192,57,43,.05);border-left:2px solid rgba(192,57,43,.4)\"", "")
let submit_div: String = el_div(
"style=\"grid-column:1/-1\"",
el_button("type=\"submit\" id=\"ent-submit\" style=\"font-family:var(--body);font-size:0.75rem;font-weight:600;letter-spacing:0.12em;text-transform:uppercase;background:var(--navy);color:#fff;border:none;padding:0.95rem 2rem;cursor:pointer;transition:background .2s\"",
"Send inquiry &#8594;"
)
)
let success_div: String = el_div(
"id=\"enterprise-success\" style=\"display:none;padding:2rem;background:rgba(0,82,160,.04);border-left:3px solid var(--navy)\"",
el_p("style=\"font-family:var(--head);font-size:1.25rem;font-weight:500;color:var(--t1);margin-bottom:0.5rem\"", "Received.") +
el_p("style=\"font-family:var(--body);font-weight:300;font-size:0.9375rem;color:var(--t2);line-height:1.8\"",
"I review these personally. If it&#39;s a fit, I&#39;ll respond directly to your email - usually within a few days."
)
)
el_div(
"style=\"margin-top:3rem;padding-top:2.5rem;border-top:1px solid var(--border)\"",
el_p("class=\"label\" style=\"margin-bottom:0.75rem;font-size:0.65rem\"", "Express interest") +
el_p("style=\"font-family:var(--body);font-weight:300;font-size:0.9rem;color:var(--t2);line-height:1.75;margin-bottom:2rem\"",
"I review every inquiry myself. Fill this out honestly - the questions are a filter, not a formality."
) +
el_form("id=\"enterprise-form\" class=\"ent-inquiry-form\"",
field_name + field_email + field_company + field_size + field_use + field_headcount + error_div + submit_div
) +
success_div
)
}
fn enterprise() -> String {
return <section id="enterprise" aria-label="Enterprise">
<div class="container">
let header: String = el_div(
"class=\"enterprise-header\"",
el_div(
"class=\"enterprise-label-row reveal\"",
el_div("class=\"navy-line-left\" style=\"width:4rem;flex-shrink:0\"", "") +
el_span("class=\"label\" style=\"color:var(--navy-85)\"", "Enterprise")
) +
el_div(
"class=\"enterprise-head-row\"",
el_div(
"style=\"max-width:28rem\"",
el_h2(
"class=\"display-lg reveal\" style=\"transition-delay:80ms\"",
"Enterprise-ready " + el_span("class=\"gold\"", "Q1 2027.")
) +
el_p("class=\"reveal\" style=\"transition-delay:160ms;font-weight:300;font-size:.9375rem;color:var(--t2);line-height:1.7;margin-top:1.25rem\"",
"Not an afterthought. Not a future roadmap item. Built into the architecture from the start."
)
) +
el_div(
"class=\"reveal\" style=\"transition-delay:240ms;display:flex;flex-direction:column;gap:.75rem;align-items:flex-end\"",
el_div(
"class=\"ent-badge\"",
el_span("class=\"ent-badge-dot\"", "") +
"Enterprise discussions open now &mdash; launching Q1 2027"
) +
el_p("style=\"font-size:.75rem;color:rgba(0,82,160,.45)\"",
"Use the form below &middot; I review every inquiry &middot; I&#39;ll be spending the holidays with my family &mdash; we launch Q1"
)
)
)
)
<div class="enterprise-header">
<div class="enterprise-label-row reveal">
<div class="navy-line-left" style="width:4rem;flex-shrink:0"></div>
<span class="label" style="color:var(--navy-85)">Enterprise</span>
</div>
<div class="enterprise-head-row">
<div style="max-width:28rem">
<h2 class="display-lg reveal" style="transition-delay:80ms">
Enterprise-ready <span class="gold">Q1 2027.</span>
</h2>
<p class="reveal" style="transition-delay:160ms;font-weight:300;font-size:.9375rem;color:var(--t2);line-height:1.7;margin-top:1.25rem">
Not an afterthought. Not a future roadmap item. Built into the architecture from the start.
</p>
</div>
<div class="reveal" style="transition-delay:240ms;display:flex;flex-direction:column;gap:.75rem;align-items:flex-end">
<div class="ent-badge">
<span class="ent-badge-dot"></span>
Enterprise discussions open now &mdash; launching Q1 2027
</div>
<p style="font-size:.75rem;color:rgba(0,82,160,.45)">Use the form below &middot; I review every inquiry &middot; I&#39;ll be spending the holidays with my family &mdash; we launch Q1</p>
</div>
</div>
</div>
let contact_block: String = el_div(
"class=\"ent-contact-block reveal\"",
el_div(
"class=\"ent-contact-card\"",
el_p("class=\"ent-contact-role\"", "Sales") +
el_a("mailto:enterprise@neurontechnologies.ai", "class=\"ent-contact-email\"", "enterprise@neurontechnologies.ai") +
el_p("class=\"ent-contact-desc\"", "Pricing, deployment options, and enterprise agreements.")
) +
el_div(
"class=\"ent-contact-card\"",
el_p("class=\"ent-contact-role\"", "Security") +
el_a("mailto:security@neurontechnologies.ai", "class=\"ent-contact-email\"", "security@neurontechnologies.ai") +
el_p("class=\"ent-contact-desc\"", "Vulnerability disclosure, compliance review, and security documentation.")
)
)
<div class="enterprise-grid">
<div class="enterprise-cap card-dark reveal">
<h3 class="display-md">Private deployment</h3>
<div class="enterprise-cap-rule"></div>
<p class="enterprise-cap-body">Run entirely on your infrastructure. Air-gapped. No data leaves your network. Every employee gets their own Neuron instance - your company&#39;s institutional knowledge stays inside your walls.</p>
</div>
<div class="enterprise-cap card-dark reveal" style="transition-delay:130ms">
<h3 class="display-md">Institutional memory</h3>
<div class="enterprise-cap-rule"></div>
<p class="enterprise-cap-body">When an employee leaves, their expertise doesn&#39;t. Their memory persists - their patterns, their domain knowledge, their reasoning - available to the team they built it with.</p>
</div>
<div class="enterprise-cap card-dark reveal" style="transition-delay:260ms">
<h3 class="display-md">Team intelligence</h3>
<div class="enterprise-cap-rule"></div>
<p class="enterprise-cap-body">Shared knowledge packages and cross-instance coordination. The collective intelligence of your organization compounds the same way an individual&#39;s does.</p>
</div>
<div class="enterprise-cap card-dark reveal" style="transition-delay:390ms">
<h3 class="display-md">Compliance architecture</h3>
<div class="enterprise-cap-rule"></div>
<p class="enterprise-cap-body">Custom on-device storage. No cloud database with your data. SOC 2 alignment built into the data model. ExternalSecret-based secrets management. Audit logs at every layer.</p>
</div>
</div>
let enterprise_box: String = el_div(
"class=\"enterprise-box reveal\"",
el_p("class=\"ent-who-label\"", "Who I work with") +
el_p("class=\"ent-who-body\"", "I am selective. I built Neuron to expand what people can do - not to help organizations eliminate them. If your interest in this technology is primarily about reducing headcount, I am not your vendor. If it&#39;s about making the people you have dramatically more effective, I want to hear from you.") +
el_p("class=\"ent-who-note\"", "This isn&#39;t a legal hedge. It&#39;s a filter. The Enterprise Agreement makes it binding - but I&#39;m raising it here because I&#39;d rather you know before you reach out.") +
enterprise_how_it_works() +
el_div(
"class=\"ent-terms-row\"",
el_p("class=\"ent-terms-text\"", "The full Enterprise Agreement is published. Read it before reaching out - no NDA required to evaluate the terms.") +
el_a("/legal/enterprise-terms", "class=\"ent-terms-link\"", "Read the Enterprise Agreement &#8594;")
) +
enterprise_inquiry_form()
)
<div class="enterprise-box reveal">
<p class="ent-who-label">Who I work with</p>
<p class="ent-who-body">I am selective. I built Neuron to expand what people can do - not to help organizations eliminate them. If your interest in this technology is primarily about reducing headcount, I am not your vendor. If it&#39;s about making the people you have dramatically more effective, I want to hear from you.</p>
<p class="ent-who-note">This isn&#39;t a legal hedge. It&#39;s a filter. The Enterprise Agreement makes it binding - but I&#39;m raising it here because I&#39;d rather you know before you reach out.</p>
<div class="ent-how-row">
<div class="ent-how-label">
<h3 class="display-md" style="font-size:1.25rem">How it works</h3>
</div>
<div class="ent-steps">
<div>
<p class="ent-step-num">01</p>
<p class="ent-step-title">Express interest</p>
<p class="ent-step-body">Send me a note. I review every inquiry myself - no sales funnel, no SDR. If it&#39;s a fit, I respond directly.</p>
</div>
<div>
<p class="ent-step-num">02</p>
<p class="ent-step-title">Scoped deployment</p>
<p class="ent-step-body">I work with your team to scope a private deployment - on your infrastructure, in your network, with your security requirements.</p>
</div>
<div>
<p class="ent-step-num">03</p>
<p class="ent-step-title">Per-seat licensing</p>
<p class="ent-step-body">Annual per-seat pricing with a volume floor. Custom SLA available. The full Agreement is published before any conversation starts.</p>
</div>
</div>
</div>
<div class="ent-terms-row">
<p class="ent-terms-text">The full Enterprise Agreement is published. Read it before reaching out - no NDA required to evaluate the terms.</p>
<a href="/legal/enterprise-terms" class="ent-terms-link">Read the Enterprise Agreement &#8594;</a>
</div>
<!-- Enterprise inquiry form -->
<div style="margin-top:3rem;padding-top:2.5rem;border-top:1px solid var(--border)">
<p class="label" style="margin-bottom:0.75rem;font-size:0.65rem">Express interest</p>
<p style="font-family:var(--body);font-weight:300;font-size:0.9rem;color:var(--t2);line-height:1.75;margin-bottom:2rem">I review every inquiry myself. Fill this out honestly - the questions are a filter, not a formality.</p>
<form id="enterprise-form" class="ent-inquiry-form">
<div style="display:flex;flex-direction:column;gap:0.4rem">
<label for="ent-name" style="font-family:var(--body);font-size:0.7rem;font-weight:500;letter-spacing:0.08em;text-transform:uppercase;color:var(--t3)">Name</label>
<input id="ent-name" name="name" type="text" required placeholder="Your name"
style="font-family:var(--body);font-size:0.9rem;font-weight:300;color:var(--t1);background:#fff;border:1px solid rgba(0,82,160,.22);padding:0.7rem 0.9rem;outline:none;border-radius:0;-webkit-appearance:none;width:100%;box-sizing:border-box">
</div>
<div style="display:flex;flex-direction:column;gap:0.4rem">
<label for="ent-email" style="font-family:var(--body);font-size:0.7rem;font-weight:500;letter-spacing:0.08em;text-transform:uppercase;color:var(--t3)">Work email</label>
<input id="ent-email" name="email" type="email" required placeholder="you@company.com"
style="font-family:var(--body);font-size:0.9rem;font-weight:300;color:var(--t1);background:#fff;border:1px solid rgba(0,82,160,.22);padding:0.7rem 0.9rem;outline:none;border-radius:0;-webkit-appearance:none;width:100%;box-sizing:border-box">
</div>
<div style="display:flex;flex-direction:column;gap:0.4rem">
<label for="ent-company" style="font-family:var(--body);font-size:0.7rem;font-weight:500;letter-spacing:0.08em;text-transform:uppercase;color:var(--t3)">Organization</label>
<input id="ent-company" name="company" type="text" required placeholder="Company or organization name"
style="font-family:var(--body);font-size:0.9rem;font-weight:300;color:var(--t1);background:#fff;border:1px solid rgba(0,82,160,.22);padding:0.7rem 0.9rem;outline:none;border-radius:0;-webkit-appearance:none;width:100%;box-sizing:border-box">
</div>
<div style="display:flex;flex-direction:column;gap:0.4rem">
<label for="ent-size" style="font-family:var(--body);font-size:0.7rem;font-weight:500;letter-spacing:0.08em;text-transform:uppercase;color:var(--t3)">Team size</label>
<select id="ent-size" name="size" required
style="font-family:var(--body);font-size:0.9rem;font-weight:300;color:var(--t1);background:#fff;border:1px solid rgba(0,82,160,.22);padding:0.7rem 0.9rem;outline:none;border-radius:0;-webkit-appearance:none;cursor:pointer;width:100%;box-sizing:border-box">
<option value="">Select&hellip;</option>
<option value="2-10">2&#8211;10 people</option>
<option value="11-50">11&#8211;50 people</option>
<option value="51-200">51&#8211;200 people</option>
<option value="201-500">201&#8211;500 people</option>
<option value="500+">500+ people</option>
</select>
</div>
<div style="grid-column:1/-1;display:flex;flex-direction:column;gap:0.4rem">
<label for="ent-use" style="font-family:var(--body);font-size:0.7rem;font-weight:500;letter-spacing:0.08em;text-transform:uppercase;color:var(--t3)">What do you want to use Neuron for?</label>
<textarea id="ent-use" name="use_case" required rows="3" placeholder="Describe your intended use case - be specific."
style="font-family:var(--body);font-size:0.9rem;font-weight:300;color:var(--t1);background:#fff;border:1px solid rgba(0,82,160,.22);padding:0.7rem 0.9rem;outline:none;border-radius:0;resize:vertical;-webkit-appearance:none;width:100%;box-sizing:border-box"></textarea>
</div>
<div style="grid-column:1/-1;display:flex;flex-direction:column;gap:0.4rem">
<label for="ent-headcount" style="font-family:var(--body);font-size:0.7rem;font-weight:500;letter-spacing:0.08em;text-transform:uppercase;color:var(--t3)">Is reducing headcount a primary goal of this deployment?</label>
<select id="ent-headcount" name="headcount" required onchange="entHeadcountChange(this.value)"
style="font-family:var(--body);font-size:0.9rem;font-weight:300;color:var(--t1);background:#fff;border:1px solid rgba(0,82,160,.22);padding:0.7rem 0.9rem;outline:none;border-radius:0;-webkit-appearance:none;cursor:pointer;width:100%;box-sizing:border-box">
<option value="">Select&hellip;</option>
<option value="no">No - making our team more capable</option>
<option value="secondary">It&#39;s a secondary factor, not the primary goal</option>
<option value="yes">Yes - replacing roles with automation</option>
</select>
<p id="ent-filter-msg-secondary" style="display:none;font-family:var(--body);font-size:0.875rem;font-weight:300;color:rgba(0,82,160,.75);line-height:1.65;padding:0.75rem 1rem;background:rgba(0,82,160,.04);border-left:2px solid rgba(0,82,160,.3)">
I review these personally. I&#39;ll consider it - but I want to be direct with you: if headcount reduction is in the picture at all, even as a secondary outcome, your chances here are low. That&#39;s not a negotiating position. It&#39;s how I built this and what I&#39;m willing to support.
</p>
<p id="ent-filter-msg-yes" style="display:none;font-family:var(--body);font-size:0.875rem;font-weight:300;color:#c0392b;line-height:1.65;padding:0.75rem 1rem;background:rgba(192,57,43,.05);border-left:2px solid rgba(192,57,43,.4)">
Neuron isn&#39;t the right fit here. I built this to expand what people can do, not to replace them. If that changes, I&#39;m here.
</p>
</div>
<div id="ent-form-error" style="grid-column:1/-1;display:none;font-family:var(--body);font-size:0.875rem;color:#c0392b;padding:0.75rem 1rem;background:rgba(192,57,43,.05);border-left:2px solid rgba(192,57,43,.4)"></div>
<div style="grid-column:1/-1">
<button type="submit" id="ent-submit"
style="font-family:var(--body);font-size:0.75rem;font-weight:600;letter-spacing:0.12em;text-transform:uppercase;background:var(--navy);color:#fff;border:none;padding:0.95rem 2rem;cursor:pointer;transition:background .2s">
Send inquiry &#8594;
</button>
</div>
</form>
<div id="enterprise-success" style="display:none;padding:2rem;background:rgba(0,82,160,.04);border-left:3px solid var(--navy)">
<p style="font-family:var(--head);font-size:1.25rem;font-weight:500;color:var(--t1);margin-bottom:0.5rem">Received.</p>
<p style="font-family:var(--body);font-weight:300;font-size:0.9375rem;color:var(--t2);line-height:1.8">I review these personally. If it&#39;s a fit, I&#39;ll respond directly to your email - usually within a few days.</p>
</div>
</div>
</div>
</div>
</section>
<style>
let style_css: String = ".ent-contact-block {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 1rem;
margin-bottom: 2.5rem;
}
.ent-contact-card {
padding: 1.5rem;
border: 1px solid rgba(0,82,160,.18);
background: rgba(0,82,160,.03);
display: flex;
flex-direction: column;
gap: .4rem;
}
.ent-contact-role {
font-family: var(--body);
font-size: .65rem;
font-weight: 600;
letter-spacing: .16em;
text-transform: uppercase;
color: var(--navy);
margin-bottom: .1rem;
}
.ent-contact-email {
font-family: var(--body);
font-size: .9375rem;
font-weight: 500;
color: var(--t1);
text-decoration: none;
border-bottom: 1px solid rgba(0,82,160,.25);
padding-bottom: .1rem;
transition: border-color 200ms, color 200ms;
width: fit-content;
}
.ent-contact-email:hover { color: var(--navy); border-color: var(--navy); }
.ent-contact-desc {
font-family: var(--body);
font-size: .8125rem;
font-weight: 300;
color: var(--t3);
line-height: 1.55;
margin-top: .25rem;
}
@media (max-width: 600px) {
.ent-contact-block { grid-template-columns: 1fr; }
}
.ent-inquiry-form {
display: grid;
grid-template-columns: 1fr 1fr;
@@ -176,10 +272,14 @@ fn enterprise() -> String {
}
@media (max-width: 600px) {
.ent-inquiry-form { grid-template-columns: 1fr; }
.ent-inquiry-form > div[style*="grid-column:1/-1"],
.ent-inquiry-form > div[style*="grid-column: 1 / -1"] { grid-column: 1; }
}
</style>
.ent-inquiry-form > div[style*=\"grid-column:1/-1\"],
.ent-inquiry-form > div[style*=\"grid-column: 1 / -1\"] { grid-column: 1; }
}"
<script src="/js/enterprise.js" defer></script>
el_section(
"id=\"enterprise\" aria-label=\"Enterprise\"",
el_div("class=\"container\"", header + enterprise_cap_cards() + contact_block + enterprise_box) +
"<style>" + style_css + "</style>" +
el_script_src("/js/enterprise.js", true)
)
}
+176 -132
View File
@@ -1,153 +1,197 @@
// components/enterprise_terms.el - Enterprise Agreement page.
// Returns complete HTML using the shared page shell from styles.el.
from styles import { page_open, page_close }
from styles import { page_open_seo, page_close }
from nav import { nav }
extern fn el_div(attrs: String, children: String) -> String
extern fn el_span(attrs: String, children: String) -> String
extern fn el_h1(attrs: String, text: String) -> String
extern fn el_h2(attrs: String, text: String) -> String
extern fn el_p(attrs: String, children: String) -> String
extern fn el_a(href: String, attrs: String, children: String) -> String
extern fn el_ul(attrs: String, children: String) -> String
extern fn el_li(attrs: String, children: String) -> String
extern fn el_em(children: String) -> String
fn enterprise_terms_page() -> String {
return {page_open()}{enterprise_terms_body()}{page_close()}
page_open_seo(
"Enterprise Agreement — Neuron",
"The Neuron Enterprise Agreement governs enterprise deployments of Neuron software. Review licensing terms, data handling, and compliance provisions.",
"/legal/enterprise-terms",
"The Neuron Enterprise Agreement — governing enterprise deployments, licensing, data handling, and compliance.",
"false"
) + enterprise_terms_body() + page_close()
}
fn section(num: String, title: String, body: String) -> String {
return <div style="margin-bottom:3rem">
<div style="display:flex;align-items:baseline;flex-wrap:wrap;gap:0.5rem 1rem;margin-bottom:1.25rem;border-bottom:1px solid var(--border);padding-bottom:0.75rem">
<span style="font-family:var(--body);font-size:0.75rem;font-weight:600;letter-spacing:0.15em;text-transform:uppercase;color:var(--navy-65);flex-shrink:0">&#167; {num}</span>
<h2 style="font-family:var(--head);font-size:1.25rem;font-weight:600;color:var(--t1);min-width:0;word-break:break-word">{title}</h2>
</div>
{raw(body)}
</div>
fn et_section(num: String, title: String, body: String) -> String {
let header: String = el_div(
"style=\"display:flex;align-items:baseline;flex-wrap:wrap;gap:0.5rem 1rem;margin-bottom:1.25rem;border-bottom:1px solid var(--border);padding-bottom:0.75rem\"",
el_span("style=\"font-family:var(--body);font-size:0.75rem;font-weight:600;letter-spacing:0.15em;text-transform:uppercase;color:var(--navy-65);flex-shrink:0\"", "&#167; " + num) +
el_h2("style=\"font-family:var(--head);font-size:1.25rem;font-weight:600;color:var(--t1);min-width:0;word-break:break-word\"", title)
)
el_div("style=\"margin-bottom:3rem\"", header + body)
}
fn p(text: String) -> String {
return <p style="font-family:var(--body);font-weight:300;font-size:0.9375rem;color:var(--t2);line-height:1.8;margin-bottom:1rem">{raw(text)}</p>
fn et_p(text: String) -> String {
el_p("style=\"font-family:var(--body);font-weight:300;font-size:0.9375rem;color:var(--t2);line-height:1.8;margin-bottom:1rem\"", text)
}
fn p_last(text: String) -> String {
return <p style="font-family:var(--body);font-weight:300;font-size:0.9375rem;color:var(--t2);line-height:1.8">{raw(text)}</p>
fn et_p_last(text: String) -> String {
el_p("style=\"font-family:var(--body);font-weight:300;font-size:0.9375rem;color:var(--t2);line-height:1.8\"", text)
}
fn p_caps(text: String) -> String {
return <p style="font-family:var(--body);font-size:0.875rem;font-weight:600;color:var(--t1);line-height:1.7;margin-bottom:1rem">{raw(text)}</p>
fn et_p_caps(text: String) -> String {
el_p("style=\"font-family:var(--body);font-size:0.875rem;font-weight:600;color:var(--t1);line-height:1.7;margin-bottom:1rem\"", text)
}
fn enterprise_terms_sections_1_4() -> String {
et_section("1", "Parties and Scope",
et_p("This Enterprise Agreement (&#34;Agreement&#34;) is between Neuron, LLC (&#34;Neuron&#34;) and the organization entering into an enterprise relationship (&#34;Customer&#34;). It governs all enterprise deployments, including Team, Enterprise, and Private Cloud tiers.") +
et_p_last("This Agreement supplements the Neuron Terms of Service. In the event of conflict, this Agreement controls for enterprise use. The parties are bound by the Order Form executed at time of deployment, which specifies tier, seat count, term, and pricing.")
) +
et_section("2", "License Grant and Usage Rights",
et_p("Subject to the terms of this Agreement and timely payment of fees, Neuron grants Customer a limited, non-exclusive, non-transferable, non-sublicensable license to deploy and use the Neuron platform (&#34;Software&#34;) solely for Customer&#39;s internal business purposes during the Term.") +
et_p("The license is scoped to the number of authorized seats, devices, or instances specified in the Order Form. Customer may not exceed the licensed scope without executing a seat expansion. Neuron will not invoice retroactively for inadvertent overage of fewer than 10% of contracted seats if Customer notifies Neuron and brings usage into compliance within 30 days.") +
et_p_last("Customer may not: (a) sublicense, resell, or distribute the Software to third parties; (b) reverse engineer, decompile, or attempt to extract source code or trained model weights; (c) use the Software to build a competing product; or (d) remove or obscure any proprietary notices. Private Cloud deployments may be deployed to Customer&#39;s infrastructure but may not be transferred or made available to entities other than Customer&#39;s own employees and contractors under appropriate confidentiality obligations.")
) +
et_section("3", "Data and Privacy",
et_p("Neuron&#39;s architecture is designed to minimize data exposure. In private cloud and air-gapped deployments, no data leaves Customer&#39;s infrastructure. Neuron has no access to Customer data in these configurations.") +
et_p("In cloud-hosted deployments, Neuron processes Customer data solely to deliver the contracted services. Customer data is stored in an isolated tenant, is not commingled with other customers&#39; data, and is not used for model training, analytics, or any purpose beyond service delivery. When Customer activates network features - including relay, peer-to-peer sync, or collaborative features - data required to operate those features may be transmitted to other network participants or Neuron infrastructure; such transmission is limited to the minimum required, excludes Customer&#39;s memory and conversation history unless Customer explicitly enables a sharing feature, and is used only to deliver the activated feature. Network features route data through Neuron&#39;s messaging backplane. That data is encrypted end-to-end - Neuron cannot read it - and is not stored or retained after transmission.") +
et_p("Customer owns all data processed through the platform. Customer data is not Customer&#39;s data in the legal sense of the term as used in data processing frameworks - Customer is the controller. Neuron is the processor. Upon termination, Customer data is returned or destroyed within 30 days at Customer&#39;s election. We do not retain copies.") +
et_p_last("Where required by applicable law, Neuron will execute a Data Processing Agreement (DPA) as a supplement to this Agreement. Contact enterprise@neurontechnologies.ai to initiate. Neuron will notify Customer within 72 hours of becoming aware of any confirmed breach affecting Customer data.")
) +
et_section("4", "Values Alignment",
et_p("Neuron is built on the principle that AI should expand human capability, not replace it. We are selective about the organizations we work with.") +
et_p("Customer represents that it does not intend to use Neuron primarily as a tool to eliminate employee positions, reduce headcount through automation in ways that harm workers, or otherwise use the platform in ways inconsistent with the welfare of the people who work in Customer&#39;s organization.") +
et_p("Neuron reserves the right to decline, not renew, or terminate any enterprise agreement where Neuron determines, in its reasonable judgment, that the Customer&#39;s use of the platform is fundamentally misaligned with these principles. Termination under this section is subject to the notice and cure provisions of &#167; 10.") +
et_p_last("We will not enter into agreements with organizations whose primary business model, as Neuron determines in good faith, depends on practices that systematically harm workers, small businesses, or communities. This is not a standard clause in most software agreements. We mean it.")
)
}
fn enterprise_terms_sections_5_8() -> String {
et_section("5", "Service Levels and Support",
et_p("For cloud-hosted deployments, Neuron targets 99.9% monthly uptime, excluding scheduled maintenance windows (communicated at least 48 hours in advance) and circumstances outside Neuron&#39;s reasonable control. In the event Neuron fails to meet this target in a given calendar month, Customer may request a service credit equal to 5% of that month&#39;s fees for each full percentage point of availability below the target, up to a maximum of 30% of monthly fees. Credits are the sole and exclusive remedy for availability failures.") +
et_p("For on-premises and private cloud deployments, Customer is responsible for infrastructure availability. Neuron&#39;s SLA obligations apply only to software defects in the distributed build, not to Customer&#39;s infrastructure choices.") +
et_p_last("Support is provided by email at enterprise@neurontechnologies.ai. Neuron will acknowledge critical issues (complete service outage or data loss risk) within 4 business hours and provide a status update within 24 hours. Standard issues are acknowledged within 2 business days. Neuron does not guarantee resolution timelines for issues requiring platform changes.")
) +
et_section("6", "Security and Compliance",
et_p("Neuron implements security controls commensurate with enterprise software handling sensitive user data. These include: encryption in transit (TLS 1.2+) and at rest for cloud-hosted services; role-based access controls; audit logging at the infrastructure layer; and regular internal security reviews.") +
et_p("Neuron will pursue SOC 2 Type II certification and will provide Customer with the most recent available report upon request under a signed NDA. Until certification is achieved, Neuron will make available a written summary of security controls upon request.") +
et_p_last("Customer is responsible for: (a) maintaining the security of credentials and API keys used to access the platform; (b) configuring access controls for Customer&#39;s own users; (c) compliance with applicable law with respect to the data Customer chooses to input into the platform; and (d) conducting its own security evaluation appropriate to its risk profile before deployment in regulated environments.")
) +
et_section("7", "Confidentiality",
et_p("Each party (&#34;Receiving Party&#34;) agrees to protect the other party&#39;s (&#34;Disclosing Party&#34;) Confidential Information with the same degree of care it uses to protect its own confidential information, but no less than reasonable care. &#34;Confidential Information&#34; means any non-public information disclosed by the Disclosing Party that is designated as confidential or that reasonably should be understood to be confidential given the nature of the information and circumstances of disclosure.") +
et_p("Confidential Information does not include information that: (a) becomes publicly known through no breach by the Receiving Party; (b) was already known to the Receiving Party before disclosure; (c) is received from a third party without restriction; or (d) was independently developed by the Receiving Party without use of the Confidential Information.") +
et_p_last("These obligations survive termination of this Agreement for three years. Receiving Party may disclose Confidential Information to the extent required by law or court order, provided that Receiving Party gives Disclosing Party prompt written notice and cooperates with any effort to obtain a protective order.")
) +
et_section("8", "Intellectual Property",
et_p("Neuron retains all right, title, and interest in and to the Software, including all improvements, updates, and derivative works, and all intellectual property rights therein. No title to or ownership of the Software transfers to Customer under this Agreement.") +
et_p("Customer retains all right, title, and interest in and to Customer&#39;s data, including inputs, memorys, and outputs generated through Customer&#39;s use of the platform. Neuron does not claim ownership of Customer&#39;s outputs. Customer is responsible for evaluating the accuracy and appropriateness of outputs before relying on them.") +
et_p_last("This Agreement does not grant Customer any right to use Neuron&#39;s trademarks, trade names, or logos except as expressly authorized in writing. Neuron Aligned Partner certification (&#167; 14) includes a limited right to use the partner designation as specified in the Partner Addendum.")
)
}
fn enterprise_terms_sections_9_15() -> String {
et_section("9", "Indemnification",
et_p("Neuron will defend Customer against third-party claims that the Software, as provided by Neuron and used in accordance with this Agreement, infringes a valid patent, copyright, or trademark, and will indemnify Customer for damages finally awarded in such a proceeding or agreed in settlement. This obligation does not apply to claims arising from: (a) Customer&#39;s modification of the Software; (b) Customer&#39;s combination of the Software with third-party products or data not provided by Neuron; or (c) Customer&#39;s use of the Software in violation of this Agreement.") +
et_p_last("Customer will defend Neuron against third-party claims arising from Customer&#39;s use of the platform in violation of applicable law, this Agreement, or the rights of third parties, and will indemnify Neuron for damages finally awarded or agreed in settlement. Each party must promptly notify the other of any claim for which indemnification may be sought and must cooperate reasonably in the defense.")
) +
et_section("10", "Term and Termination",
et_p("This Agreement begins on the Effective Date specified in the Order Form and continues for the initial term stated therein, typically twelve months. Unless either party provides written notice of non-renewal at least 30 days before the end of the then-current term, this Agreement automatically renews for successive one-year terms at the pricing in effect at renewal.") +
et_p("Either party may terminate this Agreement for material breach upon 30 days&#39; written notice if the breach remains uncured at the end of that period. Neuron may terminate immediately upon written notice if Customer breaches &#167; 2 (unauthorized use of the Software) or &#167; 7 (confidentiality obligations).") +
et_p("Neuron may terminate this Agreement under &#167; 4 (Values Alignment) with 60 days&#39; written notice if Neuron determines in good faith that Customer&#39;s use of the platform is materially and persistently misaligned with the principles stated therein. Neuron will provide Customer the opportunity to respond and demonstrate alignment before termination takes effect.") +
et_p_last("Upon termination: (a) all licenses immediately terminate; (b) Customer must cease use of the Software; (c) each party will return or destroy the other&#39;s Confidential Information upon request; and (d) Neuron will make Customer&#39;s data available for export for 30 days, after which it will be destroyed. Sections 3 (data ownership and destruction), 7 (confidentiality), 8 (intellectual property), 9 (indemnification), 11 (limitation of liability), and 13 (governing law) survive termination.")
) +
et_section("11", "Limitation of Liability",
et_p_caps("NEURON&#39;S TOTAL LIABILITY UNDER THIS AGREEMENT SHALL NOT EXCEED THE FEES PAID BY CUSTOMER IN THE TWELVE MONTHS PRECEDING THE CLAIM.") +
et_p_caps("NEITHER PARTY SHALL BE LIABLE FOR INDIRECT, INCIDENTAL, SPECIAL, CONSEQUENTIAL, OR PUNITIVE DAMAGES, EVEN IF ADVISED OF THEIR POSSIBILITY.") +
et_p_last("These limitations do not apply to: (a) Customer&#39;s payment obligations; (b) breaches of &#167; 7 (confidentiality); (c) a party&#39;s indemnification obligations under &#167; 9; or (d) a party&#39;s gross negligence or willful misconduct.")
) +
et_section("12", "Warranties and Disclaimers",
et_p("Neuron warrants that: (a) the Software will perform materially in accordance with its published documentation during the Term; (b) Neuron has the authority to enter into this Agreement and grant the licenses herein; and (c) to Neuron&#39;s knowledge, the Software does not knowingly infringe any third-party intellectual property rights.") +
et_p("Customer&#39;s exclusive remedy for a warranty breach under (a) is for Neuron to use commercially reasonable efforts to correct the non-conformity, or if correction is not commercially practicable within 30 days, to terminate the Agreement and receive a pro-rata refund of prepaid fees for the unused portion of the Term.") +
et_p_caps("EXCEPT AS EXPRESSLY SET FORTH IN THIS SECTION, THE SOFTWARE IS PROVIDED &#34;AS IS.&#34; NEURON DISCLAIMS ALL OTHER WARRANTIES, EXPRESS OR IMPLIED, INCLUDING WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, AND NON-INFRINGEMENT.") +
et_p_last("AI-generated outputs are probabilistic and may be inaccurate, incomplete, or inappropriate for specific contexts. Customer is responsible for reviewing outputs before relying on them for any consequential decision. Neuron does not warrant that outputs will be accurate, complete, or suitable for any particular purpose.")
) +
et_section("13", "Governing Law and Disputes",
et_p("This Agreement is governed by the laws of the State of Delaware, without regard to conflict of law principles.") +
et_p_last("The parties agree to attempt to resolve disputes through good-faith negotiation before initiating formal proceedings. If negotiation fails within 30 days of written notice of the dispute, disputes shall be resolved by binding arbitration under AAA Commercial Arbitration Rules in Wilmington, Delaware, except that either party may seek injunctive relief in court for IP or confidentiality violations without first arbitrating.")
) +
et_section("15", "General Provisions",
et_p("This Agreement, together with the Order Form and any addenda, constitutes the entire agreement between the parties with respect to its subject matter and supersedes all prior negotiations, representations, and agreements. Any amendment must be in writing and signed by authorized representatives of both parties.") +
et_p("If any provision of this Agreement is found unenforceable, it will be modified to the minimum extent necessary to make it enforceable, and the remaining provisions will continue in full force. Failure to enforce any provision is not a waiver of the right to enforce it later.") +
et_p("Neither party may assign this Agreement without the other&#39;s written consent, except that Neuron may assign to an acquirer of all or substantially all of its business or assets without consent, provided the acquirer assumes all obligations under this Agreement.") +
et_p_last("Notices must be in writing and sent to enterprise@neurontechnologies.ai for Neuron, or to the email address specified in the Order Form for Customer. Notices are effective on the day of confirmed receipt.")
)
}
fn enterprise_partner_section() -> String {
let section_header: String = el_div(
"style=\"display:flex;align-items:baseline;flex-wrap:wrap;gap:0.5rem 1rem;margin-bottom:1.25rem;border-bottom:1px solid rgba(0,82,160,.20);padding-bottom:0.75rem\"",
el_span("style=\"font-family:var(--body);font-size:0.75rem;font-weight:600;letter-spacing:0.15em;text-transform:uppercase;color:var(--navy-65);flex-shrink:0\"", "&#167; 14") +
el_h2("style=\"font-family:var(--head);font-size:1.25rem;font-weight:600;color:var(--t1);min-width:0;word-break:break-word\"", "Neuron Aligned Partner Program")
)
let cert_quote: String = el_div(
"style=\"margin:1.5rem 0;padding:1.25rem 1.5rem;border-left:3px solid var(--navy);background:rgba(0,82,160,.04);box-sizing:border-box\"",
el_p("style=\"font-family:var(--body);font-weight:400;font-size:0.9375rem;color:var(--t1);line-height:1.8;font-style:italic\"",
"&#34;We are deploying Neuron to make our people more capable, not to eliminate them. Reducing headcount is not a primary objective of this deployment. We commit to using this technology in a manner consistent with the welfare of our workforce, and we will notify Neuron if our intentions change in ways that materially contradict this commitment.&#34;"
)
)
let benefits_list: String = el_ul(
"style=\"font-family:var(--body);font-weight:300;font-size:0.9rem;color:var(--t2);line-height:1.8;padding-left:1.25rem;margin-bottom:1.5rem\"",
el_li("style=\"margin-bottom:0.4rem\"", "Preferential per-seat pricing - specific discount terms are disclosed at the time of certification and reflected in the Order Form") +
el_li("style=\"margin-bottom:0.4rem\"", "Membership in the Neuron Aligned Partner Network, a private community of organizations that have made the same commitment") +
el_li("style=\"margin-bottom:0.4rem\"", "Optional public listing as a Neuron Aligned Partner - a signal to employees and the market that AI is being used to expand human capability, not eliminate it") +
el_li("style=\"margin-bottom:0.4rem\"", "Priority consideration in the enterprise roadmap - Aligned Partners are given more weight in feature prioritization and deployment planning") +
el_li("", "Direct access to Neuron leadership for roadmap input and partnership discussions")
)
el_div(
"style=\"margin-bottom:3rem;padding:clamp(1.25rem,4vw,2rem) clamp(1rem,4vw,2.25rem);border:1.5px solid rgba(0,82,160,.35);background:rgba(0,82,160,.03);box-sizing:border-box\"",
section_header +
et_p("Participation in the Neuron Aligned Partner Program is entirely voluntary. No enterprise customer is required to participate, and the standard enterprise relationship under this Agreement is not conditioned on it.") +
et_p("An organization that chooses to participate executes a Neuron Aligned Partner Addendum (&#34;Partner Addendum&#34;), a separate document supplementing this Agreement. In the Partner Addendum, Customer makes the following certification:") +
cert_quote +
el_p("style=\"font-family:var(--body);font-weight:400;font-size:0.9rem;color:var(--t1);line-height:1.6;margin-bottom:0.75rem\"", "Partners who execute the Partner Addendum receive:") +
benefits_list +
et_p("The certification is self-attested and made in good faith. Neuron does not audit deployment usage to verify the commitment, but relies on the integrity of the partner&#39;s representation. If Neuron determines, based on credible evidence, that a partner&#39;s use of the platform is materially inconsistent with the certification, Neuron may revoke partner status with 30 days&#39; notice and adjust pricing to standard enterprise rates at the next renewal.") +
el_p("style=\"font-family:var(--body);font-weight:300;font-size:0.875rem;color:var(--t3);line-height:1.8\"",
"To inquire about the Partner Program, contact enterprise@neurontechnologies.ai with subject line &#34;Aligned Partner.&#34;"
)
)
}
fn enterprise_terms_body() -> String {
return {nav()}
<div style="max-width:720px;margin:0 auto;padding:6rem clamp(1rem,5vw,2.5rem) 8rem;box-sizing:border-box;overflow-wrap:break-word;word-break:break-word">
<div style="margin-bottom:3rem">
<a href="/" style="font-family:var(--body);font-size:0.75rem;font-weight:500;letter-spacing:0.15em;text-transform:uppercase;color:var(--navy);text-decoration:none">&#8592; Neuron</a>
</div>
<div style="margin-bottom:4rem;border-bottom:1px solid var(--border);padding-bottom:3rem">
<p class="label" style="margin-bottom:1rem">Legal</p>
<h1 style="font-family:var(--head);font-size:clamp(2rem,4vw,3rem);font-weight:600;color:var(--t1);margin-bottom:0.75rem;line-height:1.1">Enterprise Agreement</h1>
<p style="font-family:var(--body);font-size:0.875rem;color:var(--t3)">Effective May 1, 2026 &nbsp;&middot;&nbsp; Neuron, LLC</p>
</div>
let back_link: String = el_div(
"style=\"margin-bottom:3rem\"",
el_a("/", "style=\"font-family:var(--body);font-size:0.75rem;font-weight:500;letter-spacing:0.15em;text-transform:uppercase;color:var(--navy);text-decoration:none\"", "&#8592; Neuron")
)
{raw(section("1", "Parties and Scope",
p("This Enterprise Agreement (&#34;Agreement&#34;) is between Neuron, LLC (&#34;Neuron&#34;) and the organization entering into an enterprise relationship (&#34;Customer&#34;). It governs all enterprise deployments, including Team, Enterprise, and Private Cloud tiers.")
+ p_last("This Agreement supplements the Neuron Terms of Service. In the event of conflict, this Agreement controls for enterprise use. The parties are bound by the Order Form executed at time of deployment, which specifies tier, seat count, term, and pricing.")
))}
{raw(section("2", "License Grant and Usage Rights",
p("Subject to the terms of this Agreement and timely payment of fees, Neuron grants Customer a limited, non-exclusive, non-transferable, non-sublicensable license to deploy and use the Neuron platform (&#34;Software&#34;) solely for Customer&#39;s internal business purposes during the Term.")
+ p("The license is scoped to the number of authorized seats, devices, or instances specified in the Order Form. Customer may not exceed the licensed scope without executing a seat expansion. Neuron will not invoice retroactively for inadvertent overage of fewer than 10% of contracted seats if Customer notifies Neuron and brings usage into compliance within 30 days.")
+ p_last("Customer may not: (a) sublicense, resell, or distribute the Software to third parties; (b) reverse engineer, decompile, or attempt to extract source code or trained model weights; (c) use the Software to build a competing product; or (d) remove or obscure any proprietary notices. Private Cloud deployments may be deployed to Customer&#39;s infrastructure but may not be transferred or made available to entities other than Customer&#39;s own employees and contractors under appropriate confidentiality obligations.")
))}
{raw(section("3", "Data and Privacy",
p("Neuron&#39;s architecture is designed to minimize data exposure. In private cloud and air-gapped deployments, no data leaves Customer&#39;s infrastructure. Neuron has no access to Customer data in these configurations.")
+ p("In cloud-hosted deployments, Neuron processes Customer data solely to deliver the contracted services. Customer data is stored in an isolated tenant, is not commingled with other customers&#39; data, and is not used for model training, analytics, or any purpose beyond service delivery. When Customer activates network features - including relay, peer-to-peer sync, or collaborative features - data required to operate those features may be transmitted to other network participants or Neuron infrastructure; such transmission is limited to the minimum required, excludes Customer&#39;s memory and conversation history unless Customer explicitly enables a sharing feature, and is used only to deliver the activated feature. Network features route data through Neuron&#39;s messaging backplane. That data is encrypted end-to-end - Neuron cannot read it - and is not stored or retained after transmission.")
+ p("Customer owns all data processed through the platform. Customer data is not Customer&#39;s data in the legal sense of the term as used in data processing frameworks - Customer is the controller. Neuron is the processor. Upon termination, Customer data is returned or destroyed within 30 days at Customer&#39;s election. We do not retain copies.")
+ p_last("Where required by applicable law, Neuron will execute a Data Processing Agreement (DPA) as a supplement to this Agreement. Contact enterprise@neurontechnologies.ai to initiate. Neuron will notify Customer within 72 hours of becoming aware of any confirmed breach affecting Customer data.")
))}
{raw(section("4", "Values Alignment",
p("Neuron is built on the principle that AI should expand human capability, not replace it. We are selective about the organizations we work with.")
+ p("Customer represents that it does not intend to use Neuron primarily as a tool to eliminate employee positions, reduce headcount through automation in ways that harm workers, or otherwise use the platform in ways inconsistent with the welfare of the people who work in Customer&#39;s organization.")
+ p("Neuron reserves the right to decline, not renew, or terminate any enterprise agreement where Neuron determines, in its reasonable judgment, that the Customer&#39;s use of the platform is fundamentally misaligned with these principles. Termination under this section is subject to the notice and cure provisions of &#167; 10.")
+ p_last("We will not enter into agreements with organizations whose primary business model, as Neuron determines in good faith, depends on practices that systematically harm workers, small businesses, or communities. This is not a standard clause in most software agreements. We mean it.")
))}
{raw(section("5", "Service Levels and Support",
p("For cloud-hosted deployments, Neuron targets 99.9% monthly uptime, excluding scheduled maintenance windows (communicated at least 48 hours in advance) and circumstances outside Neuron&#39;s reasonable control. In the event Neuron fails to meet this target in a given calendar month, Customer may request a service credit equal to 5% of that month&#39;s fees for each full percentage point of availability below the target, up to a maximum of 30% of monthly fees. Credits are the sole and exclusive remedy for availability failures.")
+ p("For on-premises and private cloud deployments, Customer is responsible for infrastructure availability. Neuron&#39;s SLA obligations apply only to software defects in the distributed build, not to Customer&#39;s infrastructure choices.")
+ p_last("Support is provided by email at enterprise@neurontechnologies.ai. Neuron will acknowledge critical issues (complete service outage or data loss risk) within 4 business hours and provide a status update within 24 hours. Standard issues are acknowledged within 2 business days. Neuron does not guarantee resolution timelines for issues requiring platform changes.")
))}
{raw(section("6", "Security and Compliance",
p("Neuron implements security controls commensurate with enterprise software handling sensitive user data. These include: encryption in transit (TLS 1.2+) and at rest for cloud-hosted services; role-based access controls; audit logging at the infrastructure layer; and regular internal security reviews.")
+ p("Neuron will pursue SOC 2 Type II certification and will provide Customer with the most recent available report upon request under a signed NDA. Until certification is achieved, Neuron will make available a written summary of security controls upon request.")
+ p_last("Customer is responsible for: (a) maintaining the security of credentials and API keys used to access the platform; (b) configuring access controls for Customer&#39;s own users; (c) compliance with applicable law with respect to the data Customer chooses to input into the platform; and (d) conducting its own security evaluation appropriate to its risk profile before deployment in regulated environments.")
))}
{raw(section("7", "Confidentiality",
p("Each party (&#34;Receiving Party&#34;) agrees to protect the other party&#39;s (&#34;Disclosing Party&#34;) Confidential Information with the same degree of care it uses to protect its own confidential information, but no less than reasonable care. &#34;Confidential Information&#34; means any non-public information disclosed by the Disclosing Party that is designated as confidential or that reasonably should be understood to be confidential given the nature of the information and circumstances of disclosure.")
+ p("Confidential Information does not include information that: (a) becomes publicly known through no breach by the Receiving Party; (b) was already known to the Receiving Party before disclosure; (c) is received from a third party without restriction; or (d) was independently developed by the Receiving Party without use of the Confidential Information.")
+ p_last("These obligations survive termination of this Agreement for three years. Receiving Party may disclose Confidential Information to the extent required by law or court order, provided that Receiving Party gives Disclosing Party prompt written notice and cooperates with any effort to obtain a protective order.")
))}
{raw(section("8", "Intellectual Property",
p("Neuron retains all right, title, and interest in and to the Software, including all improvements, updates, and derivative works, and all intellectual property rights therein. No title to or ownership of the Software transfers to Customer under this Agreement.")
+ p("Customer retains all right, title, and interest in and to Customer&#39;s data, including inputs, memorys, and outputs generated through Customer&#39;s use of the platform. Neuron does not claim ownership of Customer&#39;s outputs. Customer is responsible for evaluating the accuracy and appropriateness of outputs before relying on them.")
+ p_last("This Agreement does not grant Customer any right to use Neuron&#39;s trademarks, trade names, or logos except as expressly authorized in writing. Neuron Aligned Partner certification (&#167; 14) includes a limited right to use the partner designation as specified in the Partner Addendum.")
))}
{raw(section("9", "Indemnification",
p("Neuron will defend Customer against third-party claims that the Software, as provided by Neuron and used in accordance with this Agreement, infringes a valid patent, copyright, or trademark, and will indemnify Customer for damages finally awarded in such a proceeding or agreed in settlement. This obligation does not apply to claims arising from: (a) Customer&#39;s modification of the Software; (b) Customer&#39;s combination of the Software with third-party products or data not provided by Neuron; or (c) Customer&#39;s use of the Software in violation of this Agreement.")
+ p_last("Customer will defend Neuron against third-party claims arising from Customer&#39;s use of the platform in violation of applicable law, this Agreement, or the rights of third parties, and will indemnify Neuron for damages finally awarded or agreed in settlement. Each party must promptly notify the other of any claim for which indemnification may be sought and must cooperate reasonably in the defense.")
))}
{raw(section("10", "Term and Termination",
p("This Agreement begins on the Effective Date specified in the Order Form and continues for the initial term stated therein, typically twelve months. Unless either party provides written notice of non-renewal at least 30 days before the end of the then-current term, this Agreement automatically renews for successive one-year terms at the pricing in effect at renewal.")
+ p("Either party may terminate this Agreement for material breach upon 30 days&#39; written notice if the breach remains uncured at the end of that period. Neuron may terminate immediately upon written notice if Customer breaches &#167; 2 (unauthorized use of the Software) or &#167; 7 (confidentiality obligations).")
+ p("Neuron may terminate this Agreement under &#167; 4 (Values Alignment) with 60 days&#39; written notice if Neuron determines in good faith that Customer&#39;s use of the platform is materially and persistently misaligned with the principles stated therein. Neuron will provide Customer the opportunity to respond and demonstrate alignment before termination takes effect.")
+ p_last("Upon termination: (a) all licenses immediately terminate; (b) Customer must cease use of the Software; (c) each party will return or destroy the other&#39;s Confidential Information upon request; and (d) Neuron will make Customer&#39;s data available for export for 30 days, after which it will be destroyed. Sections 3 (data ownership and destruction), 7 (confidentiality), 8 (intellectual property), 9 (indemnification), 11 (limitation of liability), and 13 (governing law) survive termination.")
))}
{raw(section("11", "Limitation of Liability",
p_caps("NEURON&#39;S TOTAL LIABILITY UNDER THIS AGREEMENT SHALL NOT EXCEED THE FEES PAID BY CUSTOMER IN THE TWELVE MONTHS PRECEDING THE CLAIM.")
+ p_caps("NEITHER PARTY SHALL BE LIABLE FOR INDIRECT, INCIDENTAL, SPECIAL, CONSEQUENTIAL, OR PUNITIVE DAMAGES, EVEN IF ADVISED OF THEIR POSSIBILITY.")
+ p_last("These limitations do not apply to: (a) Customer&#39;s payment obligations; (b) breaches of &#167; 7 (confidentiality); (c) a party&#39;s indemnification obligations under &#167; 9; or (d) a party&#39;s gross negligence or willful misconduct.")
))}
{raw(section("12", "Warranties and Disclaimers",
p("Neuron warrants that: (a) the Software will perform materially in accordance with its published documentation during the Term; (b) Neuron has the authority to enter into this Agreement and grant the licenses herein; and (c) to Neuron&#39;s knowledge, the Software does not knowingly infringe any third-party intellectual property rights.")
+ p("Customer&#39;s exclusive remedy for a warranty breach under (a) is for Neuron to use commercially reasonable efforts to correct the non-conformity, or if correction is not commercially practicable within 30 days, to terminate the Agreement and receive a pro-rata refund of prepaid fees for the unused portion of the Term.")
+ p_caps("EXCEPT AS EXPRESSLY SET FORTH IN THIS SECTION, THE SOFTWARE IS PROVIDED &#34;AS IS.&#34; NEURON DISCLAIMS ALL OTHER WARRANTIES, EXPRESS OR IMPLIED, INCLUDING WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, AND NON-INFRINGEMENT.")
+ p_last("AI-generated outputs are probabilistic and may be inaccurate, incomplete, or inappropriate for specific contexts. Customer is responsible for reviewing outputs before relying on them for any consequential decision. Neuron does not warrant that outputs will be accurate, complete, or suitable for any particular purpose.")
))}
{raw(section("13", "Governing Law and Disputes",
p("This Agreement is governed by the laws of the State of Delaware, without regard to conflict of law principles.")
+ p_last("The parties agree to attempt to resolve disputes through good-faith negotiation before initiating formal proceedings. If negotiation fails within 30 days of written notice of the dispute, disputes shall be resolved by binding arbitration under AAA Commercial Arbitration Rules in Wilmington, Delaware, except that either party may seek injunctive relief in court for IP or confidentiality violations without first arbitrating.")
))}
let page_header: String = el_div(
"style=\"margin-bottom:4rem;border-bottom:1px solid var(--border);padding-bottom:3rem\"",
el_p("class=\"label\" style=\"margin-bottom:1rem\"", "Legal") +
el_h1("style=\"font-family:var(--head);font-size:clamp(2rem,4vw,3rem);font-weight:600;color:var(--t1);margin-bottom:0.75rem;line-height:1.1\"", "Enterprise Agreement") +
el_p("style=\"font-family:var(--body);font-size:0.875rem;color:var(--t3)\"", "Effective May 1, 2026 &nbsp;&middot;&nbsp; Neuron, LLC")
)
<!-- Neuron Aligned Partner Program - highlighted section -->
<div style="margin-bottom:3rem;padding:clamp(1.25rem,4vw,2rem) clamp(1rem,4vw,2.25rem);border:1.5px solid rgba(0,82,160,.35);background:rgba(0,82,160,.03);box-sizing:border-box">
<div style="display:flex;align-items:baseline;flex-wrap:wrap;gap:0.5rem 1rem;margin-bottom:1.25rem;border-bottom:1px solid rgba(0,82,160,.20);padding-bottom:0.75rem">
<span style="font-family:var(--body);font-size:0.75rem;font-weight:600;letter-spacing:0.15em;text-transform:uppercase;color:var(--navy-65);flex-shrink:0">&#167; 14</span>
<h2 style="font-family:var(--head);font-size:1.25rem;font-weight:600;color:var(--t1);min-width:0;word-break:break-word">Neuron Aligned Partner Program</h2>
</div>
let footer_links: String = el_div(
"style=\"margin-top:4rem;padding-top:2rem;border-top:1px solid var(--border);display:flex;gap:2rem;flex-wrap:wrap\"",
el_a("/", "style=\"font-family:var(--body);font-size:0.8125rem;color:var(--navy);text-decoration:none\"", "&larr; Home") +
el_a("/legal/terms", "style=\"font-family:var(--body);font-size:0.8125rem;color:var(--navy);text-decoration:none\"", "Consumer Terms &rarr;")
)
<p style="font-family:var(--body);font-weight:300;font-size:0.9375rem;color:var(--t2);line-height:1.8;margin-bottom:1rem">Participation in the Neuron Aligned Partner Program is entirely voluntary. No enterprise customer is required to participate, and the standard enterprise relationship under this Agreement is not conditioned on it.</p>
<p style="font-family:var(--body);font-weight:300;font-size:0.9375rem;color:var(--t2);line-height:1.8;margin-bottom:1rem">An organization that chooses to participate executes a Neuron Aligned Partner Addendum (&#34;Partner Addendum&#34;), a separate document supplementing this Agreement. In the Partner Addendum, Customer makes the following certification:</p>
<div style="margin:1.5rem 0;padding:1.25rem 1.5rem;border-left:3px solid var(--navy);background:rgba(0,82,160,.04);box-sizing:border-box">
<p style="font-family:var(--body);font-weight:400;font-size:0.9375rem;color:var(--t1);line-height:1.8;font-style:italic">&#34;We are deploying Neuron to make our people more capable, not to eliminate them. Reducing headcount is not a primary objective of this deployment. We commit to using this technology in a manner consistent with the welfare of our workforce, and we will notify Neuron if our intentions change in ways that materially contradict this commitment.&#34;</p>
</div>
<p style="font-family:var(--body);font-weight:400;font-size:0.9rem;color:var(--t1);line-height:1.6;margin-bottom:0.75rem">Partners who execute the Partner Addendum receive:</p>
<ul style="font-family:var(--body);font-weight:300;font-size:0.9rem;color:var(--t2);line-height:1.8;padding-left:1.25rem;margin-bottom:1.5rem">
<li style="margin-bottom:0.4rem">Preferential per-seat pricing - specific discount terms are disclosed at the time of certification and reflected in the Order Form</li>
<li style="margin-bottom:0.4rem">Membership in the Neuron Aligned Partner Network, a private community of organizations that have made the same commitment</li>
<li style="margin-bottom:0.4rem">Optional public listing as a Neuron Aligned Partner - a signal to employees and the market that AI is being used to expand human capability, not eliminate it</li>
<li style="margin-bottom:0.4rem">Priority consideration in the enterprise roadmap - Aligned Partners are given more weight in feature prioritization and deployment planning</li>
<li>Direct access to Neuron leadership for roadmap input and partnership discussions</li>
</ul>
<p style="font-family:var(--body);font-weight:300;font-size:0.9375rem;color:var(--t2);line-height:1.8;margin-bottom:1rem">The certification is self-attested and made in good faith. Neuron does not audit deployment usage to verify the commitment, but relies on the integrity of the partner&#39;s representation. If Neuron determines, based on credible evidence, that a partner&#39;s use of the platform is materially inconsistent with the certification, Neuron may revoke partner status with 30 days&#39; notice and adjust pricing to standard enterprise rates at the next renewal.</p>
<p style="font-family:var(--body);font-weight:300;font-size:0.875rem;color:var(--t3);line-height:1.8">To inquire about the Partner Program, contact enterprise@neurontechnologies.ai with subject line &#34;Aligned Partner.&#34;</p>
</div>
{raw(section("15", "General Provisions",
p("This Agreement, together with the Order Form and any addenda, constitutes the entire agreement between the parties with respect to its subject matter and supersedes all prior negotiations, representations, and agreements. Any amendment must be in writing and signed by authorized representatives of both parties.")
+ p("If any provision of this Agreement is found unenforceable, it will be modified to the minimum extent necessary to make it enforceable, and the remaining provisions will continue in full force. Failure to enforce any provision is not a waiver of the right to enforce it later.")
+ p("Neither party may assign this Agreement without the other&#39;s written consent, except that Neuron may assign to an acquirer of all or substantially all of its business or assets without consent, provided the acquirer assumes all obligations under this Agreement.")
+ p_last("Notices must be in writing and sent to enterprise@neurontechnologies.ai for Neuron, or to the email address specified in the Order Form for Customer. Notices are effective on the day of confirmed receipt.")
))}
<div style="margin-top:4rem;padding-top:2rem;border-top:1px solid var(--border);display:flex;gap:2rem;flex-wrap:wrap">
<a href="/" style="font-family:var(--body);font-size:0.8125rem;color:var(--navy);text-decoration:none">&larr; Home</a>
<a href="/legal/terms" style="font-family:var(--body);font-size:0.8125rem;color:var(--navy);text-decoration:none">Consumer Terms &rarr;</a>
</div>
</div>
nav() +
el_div(
"style=\"max-width:720px;margin:0 auto;padding:6rem clamp(1rem,5vw,2.5rem) 8rem;box-sizing:border-box;overflow-wrap:break-word;word-break:break-word\"",
back_link +
page_header +
enterprise_terms_sections_1_4() +
enterprise_terms_sections_5_8() +
enterprise_terms_sections_9_15() +
enterprise_partner_section() +
footer_links
)
}
+95 -73
View File
@@ -4,88 +4,110 @@
// Not greenwashing - a structural argument. The benefit follows from the
// architecture; it wasn't engineered as a feature, but it's real.
extern fn el_section(attrs: String, children: String) -> String
extern fn el_div(attrs: String, children: String) -> String
extern fn el_span(attrs: String, children: String) -> String
extern fn el_h2(attrs: String, text: String) -> String
extern fn el_p(attrs: String, children: String) -> String
extern fn el_input(type_attr: String, attrs: String) -> String
extern fn el_br() -> String
extern fn el_script_src(src: String, defer_load: Bool) -> String
fn environmental() -> String {
return <section id="environmental" aria-label="Environmental impact" style="padding:8rem 2.5rem;background:var(--bg)">
<div class="container-lg">
let label_row: String = el_div(
"style=\"display:flex;align-items:center;gap:1.5rem;margin-bottom:2rem\"",
el_div("class=\"navy-line-left\" style=\"width:3rem;flex-shrink:0\"", "") +
el_span("class=\"label reveal\" style=\"color:var(--navy-85)\"", "Environmental impact")
)
<div style="display:grid;grid-template-columns:1fr 1fr;gap:6rem;align-items:start" class="env-grid">
let calculator: String = el_div(
"class=\"reveal\" style=\"transition-delay:340ms;background:var(--card);border:1px solid rgba(0,82,160,.18);padding:2rem\"",
el_p("style=\"font-family:var(--body);font-size:0.7rem;font-weight:600;letter-spacing:0.18em;text-transform:uppercase;color:var(--navy);margin-bottom:1.25rem\"", "Savings calculator") +
el_p("style=\"font-family:var(--body);font-size:0.875rem;color:var(--t2);margin-bottom:1rem\"",
"If you spend " +
"<strong id=\"calc-spend\" style=\"color:var(--t1)\">$50</strong>" +
"/month on AI&hellip;"
) +
el_input("range", "id=\"calc-slider\" min=\"10\" max=\"500\" step=\"10\" value=\"50\" style=\"width:100%;accent-color:var(--navy);margin-bottom:1.5rem\"") +
el_div(
"style=\"display:flex;align-items:baseline;gap:0.75rem;margin-bottom:0.375rem\"",
el_span("id=\"calc-savings\" style=\"font-family:var(--head);font-size:2.5rem;font-weight:600;color:var(--navy)\"", "$240") +
el_span("style=\"font-family:var(--body);font-size:0.7rem;font-weight:500;letter-spacing:0.15em;text-transform:uppercase;color:var(--t3)\"", "saved per year")
) +
el_p("style=\"font-family:var(--body);font-size:0.75rem;color:var(--t3)\"", "Based on estimated token reduction applied to your monthly spend.")
)
<div>
<div style="display:flex;align-items:center;gap:1.5rem;margin-bottom:2rem">
<div class="navy-line-left" style="width:3rem;flex-shrink:0"></div>
<span class="label reveal" style="color:var(--navy-85)">Environmental impact</span>
</div>
let left_col: String = el_div(
"",
label_row +
el_h2(
"class=\"display-lg reveal\" style=\"transition-delay:80ms;margin-bottom:1.5rem\"",
"Fewer tokens." + el_br() + el_span("class=\"gold\"", "Same work done.")
) +
el_p("class=\"reveal\" style=\"transition-delay:160ms;font-family:var(--body);font-weight:300;font-size:1rem;color:var(--t2);line-height:1.8;margin-bottom:1.25rem\"",
"Persistent context means shorter, more targeted prompts on every call. Less computation. Lower cost. A smaller footprint. This isn&#39;t a setting you toggle - it&#39;s what the architecture does by default."
) +
el_p("class=\"reveal\" style=\"transition-delay:220ms;font-family:var(--body);font-weight:300;font-size:1rem;color:var(--t2);line-height:1.8;margin-bottom:1.25rem\"",
"Every time you open ChatGPT and explain who you are again, that&#39;s computation that didn&#39;t need to happen. With Neuron, that context tax doesn&#39;t accumulate. Over months of use, the savings compound into a meaningful reduction in total compute - and a meaningful reduction in what you pay."
) +
el_p("class=\"reveal\" style=\"transition-delay:280ms;font-family:var(--body);font-weight:300;font-size:1rem;color:var(--t2);line-height:1.8;margin-bottom:2.5rem\"",
"This isn&#39;t a green marketing claim. It&#39;s a consequence of the design. The same architecture that makes Neuron better for you also makes it lighter on the planet."
) +
calculator +
el_script_src("/js/environmental.js", true)
)
<h2 class="display-lg reveal" style="transition-delay:80ms;margin-bottom:1.5rem">
Fewer tokens.<br><span class="gold">Same work done.</span>
</h2>
let card1: String = el_div(
"class=\"reveal card-dark\" style=\"transition-delay:100ms;padding:1.75rem 2rem;border-left:3px solid rgba(0,120,84,.40)\"",
el_p("style=\"font-family:var(--body);font-size:0.7rem;font-weight:600;letter-spacing:0.18em;text-transform:uppercase;color:rgba(0,120,84,.70);margin-bottom:0.75rem\"", "Local inference - coming") +
el_p("style=\"font-family:var(--body);font-weight:400;font-size:0.9375rem;color:var(--t1);margin-bottom:0.5rem\"", "Your GPU, already powered on") +
el_p("style=\"font-family:var(--body);font-weight:300;font-size:0.875rem;color:var(--t2);line-height:1.7\"",
"The design: when you run inference locally via Ollama, your device&#39;s GPU handles it - hardware that&#39;s already consuming power. No data center spins up a cluster for your query. No round-trip. No idle servers waiting at scale. This is where we&#39;re headed."
)
)
<p class="reveal" style="transition-delay:160ms;font-family:var(--body);font-weight:300;font-size:1rem;color:var(--t2);line-height:1.8;margin-bottom:1.25rem">
Persistent context means shorter, more targeted prompts on every call. Less computation. Lower cost. A smaller footprint. This isn&#39;t a setting you toggle - it&#39;s what the architecture does by default.
</p>
let card2: String = el_div(
"class=\"reveal card-dark\" style=\"transition-delay:200ms;padding:1.75rem 2rem;border-left:3px solid rgba(0,120,84,.40)\"",
el_p("style=\"font-family:var(--body);font-size:0.7rem;font-weight:600;letter-spacing:0.18em;text-transform:uppercase;color:rgba(0,120,84,.70);margin-bottom:0.75rem\"", "No database server for your data") +
el_p("style=\"font-family:var(--body);font-weight:400;font-size:0.9375rem;color:var(--t1);margin-bottom:0.5rem\"", "On-device storage") +
el_p("style=\"font-family:var(--body);font-weight:300;font-size:0.875rem;color:var(--t2);line-height:1.7\"",
"Your context lives on your device in a purpose-built local storage layer. No cloud database servers running 24&#x2F;7 to store and serve your conversations. No replication across availability zones. Just your device."
)
)
<p class="reveal" style="transition-delay:220ms;font-family:var(--body);font-weight:300;font-size:1rem;color:var(--t2);line-height:1.8;margin-bottom:1.25rem">
Every time you open ChatGPT and explain who you are again, that&#39;s computation that didn&#39;t need to happen. With Neuron, that context tax doesn&#39;t accumulate. Over months of use, the savings compound into a meaningful reduction in total compute - and a meaningful reduction in what you pay.
</p>
let card3: String = el_div(
"class=\"reveal card-dark\" style=\"transition-delay:300ms;padding:1.75rem 2rem;border-left:3px solid rgba(0,120,84,.40)\"",
el_p("style=\"font-family:var(--body);font-size:0.7rem;font-weight:600;letter-spacing:0.18em;text-transform:uppercase;color:rgba(0,120,84,.70);margin-bottom:0.75rem\"", "Persistent context = less recomputation") +
el_p("style=\"font-family:var(--body);font-weight:400;font-size:0.9375rem;color:var(--t1);margin-bottom:0.5rem\"", "No re-explaining. No wasted tokens.") +
el_p("style=\"font-family:var(--body);font-weight:300;font-size:0.875rem;color:var(--t2);line-height:1.7\"",
"Neuron surfaces exactly what&#39;s relevant for each conversation - no re-deriving who you are from long histories. Shorter, more targeted prompts. More with less."
)
)
<p class="reveal" style="transition-delay:280ms;font-family:var(--body);font-weight:300;font-size:1rem;color:var(--t2);line-height:1.8;margin-bottom:2.5rem">
This isn&#39;t a green marketing claim. It&#39;s a consequence of the design. The same architecture that makes Neuron better for you also makes it lighter on the planet.
</p>
let honest: String = el_div(
"class=\"reveal\" style=\"transition-delay:400ms;padding:1.25rem 1.75rem;background:rgba(0,0,0,.03);border:1px solid rgba(0,0,0,.07);border-radius:2px\"",
el_p("style=\"font-family:var(--body);font-size:0.8125rem;font-weight:300;color:var(--t3);line-height:1.7\"",
"<strong style=\"font-weight:500;color:var(--t2)\">The honest picture:</strong>" +
" When you use Neuron with BYOK providers (OpenAI, Anthropic, Grok) or Neuron Inference, those queries travel to inference servers - that footprint exists. The savings come from the architecture: persistent memory and local-first design reduce the total computation required to get the same work done."
)
)
<div class="reveal" style="transition-delay:340ms;background:var(--card);border:1px solid rgba(0,82,160,.18);padding:2rem">
<p style="font-family:var(--body);font-size:0.7rem;font-weight:600;letter-spacing:0.18em;text-transform:uppercase;color:var(--navy);margin-bottom:1.25rem">Savings calculator</p>
<p style="font-family:var(--body);font-size:0.875rem;color:var(--t2);margin-bottom:1rem">If you spend <strong id="calc-spend" style="color:var(--t1)">$50</strong>/month on AI&hellip;</p>
<input type="range" id="calc-slider" min="10" max="500" step="10" value="50" style="width:100%;accent-color:var(--navy);margin-bottom:1.5rem">
<div style="display:flex;align-items:baseline;gap:0.75rem;margin-bottom:0.375rem">
<span id="calc-savings" style="font-family:var(--head);font-size:2.5rem;font-weight:600;color:var(--navy)">$240</span>
<span style="font-family:var(--body);font-size:0.7rem;font-weight:500;letter-spacing:0.15em;text-transform:uppercase;color:var(--t3)">saved per year</span>
</div>
<p style="font-family:var(--body);font-size:0.75rem;color:var(--t3)">Based on estimated token reduction applied to your monthly spend.</p>
</div>
let right_col: String = el_div(
"style=\"display:flex;flex-direction:column;gap:1.5rem;padding-top:1rem\"",
card1 + card2 + card3 + honest
)
<script src="/js/environmental.js" defer></script>
</div>
let grid: String = el_div(
"style=\"display:grid;grid-template-columns:1fr 1fr;gap:6rem;align-items:start\" class=\"env-grid\"",
left_col + right_col
)
<div style="display:flex;flex-direction:column;gap:1.5rem;padding-top:1rem">
<div class="reveal card-dark" style="transition-delay:100ms;padding:1.75rem 2rem;border-left:3px solid rgba(0,120,84,.40)">
<p style="font-family:var(--body);font-size:0.7rem;font-weight:600;letter-spacing:0.18em;text-transform:uppercase;color:rgba(0,120,84,.70);margin-bottom:0.75rem">Local inference - coming</p>
<p style="font-family:var(--body);font-weight:400;font-size:0.9375rem;color:var(--t1);margin-bottom:0.5rem">Your GPU, already powered on</p>
<p style="font-family:var(--body);font-weight:300;font-size:0.875rem;color:var(--t2);line-height:1.7">
The design: when you run inference locally via Ollama, your device&#39;s GPU handles it - hardware that&#39;s already consuming power. No data center spins up a cluster for your query. No round-trip. No idle servers waiting at scale. This is where we&#39;re headed.
</p>
</div>
<div class="reveal card-dark" style="transition-delay:200ms;padding:1.75rem 2rem;border-left:3px solid rgba(0,120,84,.40)">
<p style="font-family:var(--body);font-size:0.7rem;font-weight:600;letter-spacing:0.18em;text-transform:uppercase;color:rgba(0,120,84,.70);margin-bottom:0.75rem">No database server for your data</p>
<p style="font-family:var(--body);font-weight:400;font-size:0.9375rem;color:var(--t1);margin-bottom:0.5rem">On-device storage</p>
<p style="font-family:var(--body);font-weight:300;font-size:0.875rem;color:var(--t2);line-height:1.7">
Your context lives on your device in a purpose-built local storage layer. No cloud database servers running 24&#x2F;7 to store and serve your conversations. No replication across availability zones. Just your device.
</p>
</div>
<div class="reveal card-dark" style="transition-delay:300ms;padding:1.75rem 2rem;border-left:3px solid rgba(0,120,84,.40)">
<p style="font-family:var(--body);font-size:0.7rem;font-weight:600;letter-spacing:0.18em;text-transform:uppercase;color:rgba(0,120,84,.70);margin-bottom:0.75rem">Persistent context = less recomputation</p>
<p style="font-family:var(--body);font-weight:400;font-size:0.9375rem;color:var(--t1);margin-bottom:0.5rem">No re-explaining. No wasted tokens.</p>
<p style="font-family:var(--body);font-weight:300;font-size:0.875rem;color:var(--t2);line-height:1.7">
Neuron surfaces exactly what&#39;s relevant for each conversation - no re-deriving who you are from long histories. Shorter, more targeted prompts. More with less.
</p>
</div>
<div class="reveal" style="transition-delay:400ms;padding:1.25rem 1.75rem;background:rgba(0,0,0,.03);border:1px solid rgba(0,0,0,.07);border-radius:2px">
<p style="font-family:var(--body);font-size:0.8125rem;font-weight:300;color:var(--t3);line-height:1.7">
<strong style="font-weight:500;color:var(--t2)">The honest picture:</strong> When you use Neuron with BYOK providers (OpenAI, Anthropic, Grok) or Neuron Inference, those queries travel to inference servers - that footprint exists. The savings come from the architecture: persistent memory and local-first design reduce the total computation required to get the same work done.
</p>
</div>
</div>
</div>
</div>
</section>
<style>
@media (max-width: 768px) {
let css: String = "@media (max-width: 768px) {
.env-grid { grid-template-columns: 1fr !important; gap: 2rem !important; }
}
</style>
}"
el_section(
"id=\"environmental\" aria-label=\"Environmental impact\" style=\"padding:8rem 2.5rem;background:var(--bg)\"",
el_div("class=\"container-lg\"", grid) + "<style>" + css + "</style>"
)
}
+49 -25
View File
@@ -1,34 +1,58 @@
// components/footer.el - Site footer.
extern fn el_footer(attrs: String, children: String) -> String
extern fn el_div(attrs: String, children: String) -> String
extern fn el_a(href: String, attrs: String, children: String) -> String
extern fn el_p(attrs: String, children: String) -> String
extern fn el_nav(attrs: String, children: String) -> String
extern fn el_img(src: String, alt: String, attrs: String) -> String
fn footer() -> String {
return <footer id="footer" aria-label="Footer">
<div class="container">
<div class="footer-inner">
let brand_img: String = el_img(
"/assets/brand/neuron-wordmark-on-light.png",
"Neuron",
"srcset=\"/assets/brand/neuron-wordmark-on-light@2x.png 2x\" height=\"24\" style=\"display:block;margin-bottom:0.35rem;\""
)
let brand_tagline: String = el_p("class=\"footer-brand-tagline\"", "Built Different.")
let brand_link: String = el_a(
"/",
"class=\"footer-brand\" aria-label=\"Neuron home\" style=\"display:flex;flex-direction:column;align-items:center;\"",
brand_img + brand_tagline
)
<a href="/" class="footer-brand" aria-label="Neuron home" style="display:flex;flex-direction:column;align-items:center;">
<img src="/assets/brand/neuron-wordmark-on-light.png" srcset="/assets/brand/neuron-wordmark-on-light@2x.png 2x" alt="Neuron" height="24" style="display:block;margin-bottom:0.35rem;">
<p class="footer-brand-tagline">Built Different.</p>
</a>
let footer_center: String = el_div(
"class=\"footer-center\"",
el_div("class=\"navy-line\"", "")
)
<div class="footer-center">
<div class="navy-line"></div>
</div>
let footer_nav_links: String =
el_a("/legal/terms", "", "Terms") +
el_a("/legal/enterprise-terms", "", "Enterprise Agreement") +
el_a("mailto:legal@neurontechnologies.ai", "", "Contact")
let footer_nav: String = el_nav(
"class=\"footer-nav\" aria-label=\"Footer navigation\"",
footer_nav_links
)
let footer_right: String = el_div(
"class=\"footer-right\"",
el_p("class=\"footer-domain\"", "neurontechnologies.ai") + footer_nav
)
<div class="footer-right">
<p class="footer-domain">neurontechnologies.ai</p>
<nav class="footer-nav" aria-label="Footer navigation">
<a href="/legal/terms">Terms</a>
<a href="/legal/enterprise-terms">Enterprise Agreement</a>
<a href="mailto:legal@neurontechnologies.ai">Contact</a>
</nav>
</div>
let footer_inner: String = el_div(
"class=\"footer-inner\"",
brand_link + footer_center + footer_right
)
</div>
let footer_bottom: String = el_div(
"class=\"footer-bottom\"",
el_p("class=\"footer-copy\"", "&copy; 2026 Neuron, LLC. All rights reserved.") +
el_p("class=\"footer-tagline-bottom\"", "Your memory. Your AI.")
)
<div class="footer-bottom">
<p class="footer-copy">&copy; 2026 Neuron, LLC. All rights reserved.</p>
<p class="footer-tagline-bottom">Your memory. Your AI.</p>
</div>
</div>
</footer>
let container: String = el_div(
"class=\"container\"",
footer_inner + footer_bottom
)
el_footer("id=\"footer\" aria-label=\"Footer\"", container)
}
+22 -16
View File
@@ -4,26 +4,32 @@
// "Neuron, LLC" footer. Matches the design picked from
// /tmp/founding-badge-preview.html.
extern fn el_div(attrs: String, children: String) -> String
extern fn el_img(src: String, alt: String, attrs: String) -> String
fn founding_badge(member_number: Int) -> String {
let num_str: String = int_to_str(member_number)
let show_number: Bool = member_number > 0
return <div class="founding-badge" aria-label="Founding Member badge">
<img class="founding-badge-brain" src="/assets/brand/neuron-brain.png" alt="Neuron" width="40" height="40">
<div class="founding-badge-eye">Founding Member</div>
{#if show_number}
<div class="founding-badge-num">#{num_str}</div>
<div class="founding-badge-of">of 1,000</div>
{#else}
<div class="founding-badge-pending">Your number awaits</div>
{/if}
<div class="founding-badge-line"></div>
<div class="founding-badge-name">Neuron, LLC</div>
</div>
let number_or_pending: String = if show_number {
el_div("class=\"founding-badge-num\"", "#" + num_str) +
el_div("class=\"founding-badge-of\"", "of 1,000")
} else {
el_div("class=\"founding-badge-pending\"", "Your number awaits")
}
el_div(
"class=\"founding-badge\" aria-label=\"Founding Member badge\"",
el_img("/assets/brand/neuron-brain.png", "Neuron", "class=\"founding-badge-brain\" width=\"40\" height=\"40\"") +
el_div("class=\"founding-badge-eye\"", "Founding Member") +
number_or_pending +
el_div("class=\"founding-badge-line\"", "") +
el_div("class=\"founding-badge-name\"", "Neuron, LLC")
)
}
fn founding_badge_css() -> String {
return <style>
.founding-badge {
let css_a: String = ".founding-badge {
width: 200px;
background: #fff;
border: 1px solid rgba(0,82,160,.18);
@@ -83,6 +89,6 @@ fn founding_badge_css() -> String {
letter-spacing: .14em;
text-transform: uppercase;
color: #6B6B7E;
}
</style>
}"
"<style>" + css_a + "</style>"
}
+283 -228
View File
@@ -12,9 +12,203 @@
// anchors, which the HTML5 parser resolves via the adoption agency algorithm,
// producing mismatched </div> tags that break gallery-grid's closing tag and
// pull sibling elements into the grid as spurious grid items.
let gallery_share_allowlist: String = "{\"p\":[],\"br\":[],\"strong\":[],\"em\":[],\"u\":[],\"s\":[],\"code\":[],\"pre\":[],\"ul\":[],\"ol\":[],\"li\":[],\"h1\":[],\"h2\":[],\"h3\":[],\"h4\":[],\"blockquote\":[]}"
extern fn el_html_doc(lang: String, head_html: String, body_html: String) -> String
extern fn el_meta_charset(charset: String) -> String
extern fn el_meta(name: String, content: String) -> String
extern fn el_link_stylesheet(href: String) -> String
extern fn el_title(text: String) -> String
extern fn el_script_src(src: String, defer_load: Bool) -> String
extern fn el_script_inline(js: String) -> String
extern fn el_div(attrs: String, children: String) -> String
extern fn el_nav(attrs: String, children: String) -> String
extern fn el_a(href: String, attrs: String, children: String) -> String
extern fn el_img(src: String, alt: String, attrs: String) -> String
extern fn el_button(attrs: String, label: String) -> String
extern fn el_span(attrs: String, children: String) -> String
extern fn el_p(attrs: String, children: String) -> String
extern fn el_h1(attrs: String, text: String) -> String
extern fn el_h2(attrs: String, text: String) -> String
extern fn el_input(type_attr: String, attrs: String) -> String
extern fn el_br() -> String
fn gallery_css_base() -> String {
"*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
:root {
--bg: #FAFAF8; --bg2: #F0F0EC; --card: #FFFFFF;
--navy: #0052A0; --navy-65: rgba(0,82,160,.65); --navy-85: rgba(0,82,160,.85);
--t1: #0D0D14; --t2: #3A3A4A; --t3: #6B6B7E;
--border: rgba(0,0,0,.07); --border2: rgba(0,0,0,.13);
--up: #2E7D32; --down: #C62828;
--head: 'Playfair Display', Georgia, serif;
--body: 'IBM Plex Sans', system-ui, sans-serif;
}
html { scroll-behavior: smooth; }
html, body { background: var(--bg); color: var(--t1); font-family: var(--body); -webkit-font-smoothing: antialiased; }
body::before {
content: ''; position: fixed; inset: 0; pointer-events: none; z-index: 0;
background-image: linear-gradient(rgba(0,0,0,.022) 1px, transparent 1px),
linear-gradient(90deg, rgba(0,0,0,.022) 1px, transparent 1px);
background-size: 48px 48px;
}
.label { font-family: var(--body); font-size: .75rem; font-weight: 500; letter-spacing: .2em; text-transform: uppercase; color: var(--navy-65); }
.display-lg { font-family: var(--head); font-size: clamp(2rem,4vw,3.25rem); font-weight: 600; line-height: 1.1; letter-spacing: -.02em; color: var(--t1); }
.btn-primary { display: inline-flex; align-items: center; gap: .75rem; font-family: var(--body); font-size: .75rem; font-weight: 500; letter-spacing: .15em; text-transform: uppercase; text-decoration: none; color: #fff; background: var(--navy); padding: 1rem 2rem; border: none; cursor: pointer; transition: background 300ms; }
.btn-primary:hover { background: #0078D4; }
.btn-ghost { display: inline-flex; align-items: center; gap: .75rem; font-family: var(--body); font-size: .75rem; font-weight: 400; letter-spacing: .15em; text-transform: uppercase; text-decoration: none; color: var(--t2); background: transparent; padding: 1rem 2rem; border: 1px solid var(--border2); cursor: pointer; transition: border-color 300ms, color 300ms; }
.btn-ghost:hover { border-color: rgba(0,82,160,.35); color: var(--t1); }"
}
fn gallery_css_nav() -> String {
"#nav { position: fixed; top: 0; left: 0; right: 0; z-index: 100; transition: background 500ms, border-color 500ms; }
#nav.scrolled { background: rgba(250,250,248,.95); backdrop-filter: blur(12px); -webkit-backdrop-filter: blur(12px); border-bottom: 1px solid var(--border); }
.nav-inner { max-width: 1280px; margin: 0 auto; padding: 0 2rem; height: 4rem; display: flex; align-items: center; justify-content: space-between; position: relative; }
.nav-logo img { height: 2rem; width: auto; display: block; }
.nav-links { display: flex; align-items: center; gap: 1.25rem; flex-wrap: nowrap; }
.nav-link { font-family: var(--body); font-size: .675rem; font-weight: 400; letter-spacing: .16em; text-transform: uppercase; color: var(--t3); text-decoration: none; white-space: nowrap; transition: color 200ms; }
.nav-link:hover { color: var(--t1); }
.nav-dropdown { position: relative; display: flex; align-items: center; }
.nav-dropdown-btn { background: none; border: none; cursor: pointer; padding: 0; font-family: var(--body); font-size: .675rem; font-weight: 400; letter-spacing: .16em; text-transform: uppercase; color: var(--t3); transition: color 200ms; white-space: nowrap; }
.nav-dropdown-btn:hover { color: var(--t1); }
.nav-dropdown-menu { display: none; position: absolute; top: calc(100% + .75rem); left: 50%; transform: translateX(-50%); background: #fff; border: 1px solid var(--border2); box-shadow: 0 8px 32px rgba(0,0,0,.10); min-width: 160px; flex-direction: column; z-index: 200; padding: .375rem 0; }
.nav-dropdown.open .nav-dropdown-menu { display: flex; }
.nav-dropdown-item { font-family: var(--body); font-size: .8125rem; font-weight: 400; color: var(--t2); text-decoration: none; padding: .5rem 1.25rem; transition: background 150ms, color 150ms; white-space: nowrap; }
.nav-dropdown-item:hover { background: var(--bg2); color: var(--t1); }
.nav-cta { font-family: var(--body); font-size: .675rem; font-weight: 500; letter-spacing: .15em; text-transform: uppercase; color: #fff; background: var(--navy); text-decoration: none; padding: .575rem 1.1rem; white-space: nowrap; box-shadow: 0 2px 12px rgba(0,82,160,.20); transition: background 300ms; }
.nav-cta:hover { background: #0078D4; }
.nav-hamburger { display: none; flex-direction: column; justify-content: center; gap: 5px; background: none; border: none; cursor: pointer; padding: 6px 4px; z-index: 101; -webkit-tap-highlight-color: transparent; }
.nav-hamburger span { display: block; width: 22px; height: 2px; background: var(--t1); border-radius: 1px; transition: transform 280ms ease, opacity 200ms ease; transform-origin: center; }
.nav-hamburger[aria-expanded=\"true\"] span:nth-child(1) { transform: translateY(7px) rotate(45deg); }
.nav-hamburger[aria-expanded=\"true\"] span:nth-child(2) { opacity: 0; transform: scaleX(0); }
.nav-hamburger[aria-expanded=\"true\"] span:nth-child(3) { transform: translateY(-7px) rotate(-45deg); }
.nav-mobile { display: none; position: absolute; top: 4rem; left: 0; right: 0; background: rgba(250,250,248,.98); backdrop-filter: blur(16px); -webkit-backdrop-filter: blur(16px); border-bottom: 1px solid var(--border); padding: .5rem 0 1.5rem; flex-direction: column; box-shadow: 0 8px 32px rgba(0,0,0,.08); }
.nav-mobile.open { display: flex; }
.nav-mobile-link { display: block; padding: .875rem 2rem; font-family: var(--body); font-size: .75rem; font-weight: 400; letter-spacing: .16em; text-transform: uppercase; color: var(--t2); text-decoration: none; border-bottom: 1px solid rgba(0,0,0,.05); transition: color 150ms, background 150ms; }
.nav-mobile-link:last-child { border-bottom: none; }
.nav-mobile-link:hover { color: var(--t1); background: rgba(0,82,160,.03); }
.nav-mobile-cta { display: block; margin: 1rem 2rem 0; padding: .8rem 1.5rem; text-align: center; font-family: var(--body); font-size: .75rem; font-weight: 500; letter-spacing: .15em; text-transform: uppercase; color: #fff; background: var(--navy); text-decoration: none; }
@media (max-width: 1060px) { .nav-links { display: none; } .nav-hamburger { display: flex; } }
@media (min-width: 1061px) { .nav-mobile { display: none !important; } .nav-hamburger { display: none !important; } }"
}
fn gallery_css_gallery() -> String {
".gallery-wrap { max-width: 1100px; margin: 0 auto; padding: 7rem 2.5rem 5rem; position: relative; z-index: 1; }
.gallery-header { margin-bottom: 3rem; }
.gallery-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 1.25rem; }
@media (max-width: 900px) { .gallery-grid { grid-template-columns: 1fr 1fr; } }
@media (max-width: 600px) { .gallery-grid { grid-template-columns: 1fr; } }
.gal-card { background: var(--card); border: 1px solid var(--border); padding: 1.5rem; display: flex; flex-direction: column; gap: .875rem; transition: border-color .15s, box-shadow .15s; }
.gal-card:hover { border-color: rgba(0,82,160,.25); box-shadow: 0 4px 20px rgba(0,82,160,.07); }
.gal-link { text-decoration: none; color: inherit; display: flex; flex-direction: column; gap: .875rem; flex: 1; }
.gal-q { font-family: var(--body); font-weight: 500; font-size: .875rem; color: var(--navy); line-height: 1.5; display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; }
.gal-a { font-family: var(--body); font-weight: 300; font-size: .8125rem; color: var(--t2); line-height: 1.7; display: -webkit-box; -webkit-line-clamp: 6; -webkit-box-orient: vertical; overflow: hidden; flex: 1; max-height: 11rem; }
.gal-a p { margin: 0; }
.gal-a p + p { margin-top: .35rem; }
.gal-a ol, .gal-a ul { padding-left: 1.25rem; margin: .25rem 0; }
.gal-a li + li { margin-top: .15rem; }
.gal-a strong { font-weight: 600; color: var(--t1); }
.gal-a em { font-style: italic; }
.gal-a code { background: rgba(0,0,0,0.05); padding: .1em .3em; border-radius: 3px; font-family: ui-monospace, 'IBM Plex Mono', Menlo, monospace; font-size: .85em; }
.gal-a pre { background: var(--bg2); padding: .5rem; overflow-x: auto; font-size: .75rem; margin: .4rem 0; border-radius: 3px; }
.gal-a pre code { background: none; padding: 0; }
.gal-a a { color: var(--navy); text-decoration: underline; }
.gal-a h1, .gal-a h2, .gal-a h3, .gal-a h4 { font-size: 1em; font-weight: 600; margin: .3rem 0; color: var(--t1); }
.gal-a blockquote { border-left: 2px solid var(--border2); padding-left: .5rem; color: var(--t2); margin: .3rem 0; }
.gal-a img { max-width: 100%; height: auto; }
.gal-meta { display: flex; justify-content: space-between; align-items: center; font-family: var(--body); font-size: .65rem; font-weight: 400; letter-spacing: .1em; text-transform: uppercase; color: var(--t3); margin-top: auto; padding-top: .75rem; border-top: 1px solid var(--border); }
.gal-date { font-size: .65rem; color: var(--t3); }
.vote-controls { display: inline-flex; align-items: center; gap: .35rem; }
.vote-btn { background: transparent; border: 1px solid var(--border2); color: var(--t3); cursor: pointer; padding: .2rem .45rem; font-size: .7rem; line-height: 1; transition: all .15s; -webkit-tap-highlight-color: transparent; }
.vote-btn:hover:not(:disabled) { color: var(--navy); border-color: rgba(0,82,160,.35); background: rgba(0,82,160,.04); }
.vote-btn:focus-visible { outline: 2px solid var(--navy); outline-offset: 1px; }
.vote-btn:disabled { opacity: .55; cursor: not-allowed; }
.vote-btn.is-active.vote-up { background: rgba(46,125,50,.10); color: var(--up); border-color: rgba(46,125,50,.45); }
.vote-btn.is-active.vote-down { background: rgba(198,40,40,.10); color: var(--down); border-color: rgba(198,40,40,.45); }
.vote-btn.is-loading { opacity: .55; cursor: wait; }
.vote-score { font-size: .8rem; font-weight: 500; color: var(--t1); min-width: 1.5rem; text-align: center; font-variant-numeric: tabular-nums; }
.gallery-controls { display: flex; align-items: center; gap: 1rem; margin-bottom: 2rem; flex-wrap: wrap; }
.gallery-search { flex: 1; min-width: 200px; font-family: var(--body); font-size: .875rem; font-weight: 300; color: var(--t1); background: #fff; border: 1px solid var(--border2); padding: .625rem 1rem; outline: none; transition: border-color .2s; }
.gallery-search:focus { border-color: var(--navy); }
.gallery-search::placeholder { color: var(--t3); }
.sort-btn { font-family: var(--body); font-size: .7rem; font-weight: 400; letter-spacing: .1em; text-transform: uppercase; color: var(--t3); background: none; border: 1px solid var(--border2); padding: .5rem 1rem; cursor: pointer; transition: all .15s; }
.sort-btn:hover, .sort-btn.active { color: var(--navy); border-color: rgba(0,82,160,.3); background: rgba(0,82,160,.03); }
.gallery-empty { grid-column: 1/-1; padding: 4rem 0; text-align: center; }
.gal-card.hidden { display: none; }"
}
fn gallery_css_modal() -> String {
".signin-modal { position: fixed; inset: 0; background: rgba(13,13,20,.55); display: none; align-items: center; justify-content: center; z-index: 1000; padding: 1rem; }
.signin-modal.open { display: flex; }
.signin-modal-card { background: #fff; border: 1px solid var(--border2); padding: 2rem; max-width: 22rem; width: 100%; box-shadow: 0 16px 48px rgba(0,0,0,.18); }
.signin-modal h2 { font-family: var(--head); font-size: 1.4rem; font-weight: 600; color: var(--t1); margin-bottom: .5rem; }
.signin-modal p { font-family: var(--body); font-size: .85rem; font-weight: 300; color: var(--t2); line-height: 1.6; margin-bottom: 1.25rem; }
.signin-modal input { width: 100%; box-sizing: border-box; font-family: var(--body); font-size: .875rem; font-weight: 300; color: var(--t1); background: #fff; border: 1px solid var(--border2); padding: .75rem 1rem; outline: none; transition: border-color .2s; margin-bottom: .75rem; }
.signin-modal input:focus { border-color: var(--navy); }
.signin-modal-actions { display: flex; gap: .5rem; justify-content: space-between; align-items: center; }
.signin-modal-msg { font-size: .8rem; color: var(--t2); margin-top: .75rem; min-height: 1rem; }
.signin-modal .btn-primary { padding: .7rem 1.25rem; font-size: .7rem; }
.signin-modal-cancel { background: none; border: none; color: var(--t3); cursor: pointer; font-family: var(--body); font-size: .75rem; letter-spacing: .14em; text-transform: uppercase; padding: .7rem .5rem; }
.signin-modal-cancel:hover { color: var(--t1); }"
}
fn gallery_nav_html() -> String {
let logo_img: String = el_img(
"/assets/brand/neuron-wordmark-on-light.png",
"Neuron",
"srcset=\"/assets/brand/neuron-wordmark-on-light@2x.png 2x\" height=\"28\""
)
let logo: String = el_a("/", "class=\"nav-logo\" aria-label=\"Neuron home\"", logo_img)
let dropdown_menu: String = el_div(
"class=\"nav-dropdown-menu\"",
el_a("/#mission", "class=\"nav-dropdown-item\"", "Our mission") +
el_a("/#safety", "class=\"nav-dropdown-item\"", "Safety") +
el_a("/#environmental", "class=\"nav-dropdown-item\"", "Environment")
)
let dropdown: String = el_div(
"class=\"nav-dropdown\"",
el_button("class=\"nav-link nav-dropdown-btn\" aria-haspopup=\"true\" aria-expanded=\"false\"", "Mission &#9662;") +
dropdown_menu
)
let nav_links: String = el_div(
"class=\"nav-links\"",
el_a("/#how-it-works", "class=\"nav-link\"", "How it works") +
dropdown +
el_a("/#pricing", "class=\"nav-link\"", "Pricing") +
el_a("/#marketplace", "class=\"nav-link\"", "Marketplace") +
el_a("/#enterprise", "class=\"nav-link\"", "Enterprise") +
el_a("/about", "class=\"nav-link\"", "About") +
el_a("/said", "class=\"nav-link\"", "Gallery") +
el_a("/account", "class=\"nav-link\"", "Account") +
el_a("/#pricing", "class=\"nav-cta\"", "Get Access")
)
let hamburger: String = el_button(
"class=\"nav-hamburger\" id=\"nav-hamburger\" aria-label=\"Open navigation\" aria-expanded=\"false\" aria-controls=\"nav-mobile\"",
el_span("", "") + el_span("", "") + el_span("", "")
)
let nav_mobile: String = el_div(
"class=\"nav-mobile\" id=\"nav-mobile\" role=\"navigation\" aria-label=\"Mobile navigation\"",
el_a("/#how-it-works", "class=\"nav-mobile-link\"", "How it works") +
el_a("/#mission", "class=\"nav-mobile-link\"", "Mission") +
el_a("/#safety", "class=\"nav-mobile-link\" style=\"padding-left:1.75rem;font-size:.8rem;color:var(--t3)\"", "- Safety") +
el_a("/#environmental", "class=\"nav-mobile-link\" style=\"padding-left:1.75rem;font-size:.8rem;color:var(--t3)\"", "- Environment") +
el_a("/#pricing", "class=\"nav-mobile-link\"", "Pricing") +
el_a("/#enterprise", "class=\"nav-mobile-link\"", "Enterprise") +
el_a("/about", "class=\"nav-mobile-link\"", "About") +
el_a("/said", "class=\"nav-mobile-link\"", "Gallery") +
el_a("/account", "class=\"nav-mobile-link\"", "Account") +
el_a("/#pricing", "class=\"nav-mobile-cta\"", "Get Access")
)
el_nav("id=\"nav\"", el_div("class=\"nav-inner\"", logo + nav_links + hamburger + nav_mobile))
}
fn gallery_page(cards_json: String, supabase_url: String, supabase_anon_key: String) -> String {
// Moved from module-level to avoid duplicate main() when linked with other modules.
let gallery_share_allowlist: String = "{\"p\":[],\"br\":[],\"strong\":[],\"em\":[],\"u\":[],\"s\":[],\"code\":[],\"pre\":[],\"ul\":[],\"ol\":[],\"li\":[],\"h1\":[],\"h2\":[],\"h3\":[],\"h4\":[],\"blockquote\":[]}"
let i: Int = 0
let cards_html: String = ""
let n: Int = json_array_len(cards_json)
@@ -44,20 +238,20 @@ fn gallery_page(cards_json: String, supabase_url: String, supabase_anon_key: Str
}
let ts_raw: String = json_get(card, "created_at")
let ts_short: String = str_slice(ts_raw, 0, 10)
let card_html: String = "<div class=\"gal-card\" data-share-id=\"" + cid + "\" data-score=\"" + score + "\" data-ts=\"" + cid + "\">
<a href=\"/share/" + cid + "\" class=\"gal-link\">
<div class=\"gal-q\">" + q_html + "</div>
<div class=\"gal-a\">" + a_html + "</div>
</a>
<div class=\"gal-meta\">
<div class=\"vote-controls\" data-share-id=\"" + cid + "\">
<button type=\"button\" class=\"vote-btn vote-up\" aria-label=\"Upvote\" data-direction=\"up\" disabled>&#9650;</button>
<span class=\"vote-score\" data-score=\"" + score + "\">" + score + "</span>
<button type=\"button\" class=\"vote-btn vote-down\" aria-label=\"Downvote\" data-direction=\"down\" disabled>&#9660;</button>
</div>
<span class=\"gal-date\">" + ts_short + " &#183; Read &#8599;</span>
</div>
</div>"
let card_html: String = "<div class=\"gal-card\" data-share-id=\"" + cid + "\" data-score=\"" + score + "\" data-ts=\"" + cid + "\">" +
"<a href=\"/share/" + cid + "\" class=\"gal-link\">" +
"<div class=\"gal-q\">" + q_html + "</div>" +
"<div class=\"gal-a\">" + a_html + "</div>" +
"</a>" +
"<div class=\"gal-meta\">" +
"<div class=\"vote-controls\" data-share-id=\"" + cid + "\">" +
"<button type=\"button\" class=\"vote-btn vote-up\" aria-label=\"Upvote\" data-direction=\"up\" disabled>&#9650;</button>" +
"<span class=\"vote-score\" data-score=\"" + score + "\">" + score + "</span>" +
"<button type=\"button\" class=\"vote-btn vote-down\" aria-label=\"Downvote\" data-direction=\"down\" disabled>&#9660;</button>" +
"</div>" +
"<span class=\"gal-date\">" + ts_short + " &#183; Read &#8599;</span>" +
"</div>" +
"</div>"
let cards_html = cards_html + card_html
let i = i + 1
}
@@ -65,217 +259,78 @@ fn gallery_page(cards_json: String, supabase_url: String, supabase_anon_key: Str
"<div style=\"grid-column:1/-1;padding:4rem 0;text-align:center\"><p class=\"label\" style=\"margin-bottom:1rem\">Nothing yet</p><p style=\"font-family:var(--body);font-weight:300;color:var(--t2);line-height:1.7\">Nothing shared yet.</p></div>"
} else { "" }
return <!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>Things Neuron Said</title>
<meta name="description" content="Real conversations with Neuron, voted up by users.">
<link rel="icon" type="image/png" sizes="32x32" href="/assets/favicon-32.png">
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Playfair+Display:ital,wght@0,400;0,600;1,400&family=IBM+Plex+Sans:wght@300;400;500;600&display=swap" rel="stylesheet">
<style>
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
:root {
--bg: #FAFAF8; --bg2: #F0F0EC; --card: #FFFFFF;
--navy: #0052A0; --navy-65: rgba(0,82,160,.65); --navy-85: rgba(0,82,160,.85);
--t1: #0D0D14; --t2: #3A3A4A; --t3: #6B6B7E;
--border: rgba(0,0,0,.07); --border2: rgba(0,0,0,.13);
--up: #2E7D32; --down: #C62828;
--head: 'Playfair Display', Georgia, serif;
--body: 'IBM Plex Sans', system-ui, sans-serif;
}
html { scroll-behavior: smooth; }
html, body { background: var(--bg); color: var(--t1); font-family: var(--body); -webkit-font-smoothing: antialiased; }
body::before {
content: ''; position: fixed; inset: 0; pointer-events: none; z-index: 0;
background-image: linear-gradient(rgba(0,0,0,.022) 1px, transparent 1px),
linear-gradient(90deg, rgba(0,0,0,.022) 1px, transparent 1px);
background-size: 48px 48px;
}
.label { font-family: var(--body); font-size: .75rem; font-weight: 500; letter-spacing: .2em; text-transform: uppercase; color: var(--navy-65); }
.display-lg { font-family: var(--head); font-size: clamp(2rem,4vw,3.25rem); font-weight: 600; line-height: 1.1; letter-spacing: -.02em; color: var(--t1); }
.btn-primary { display: inline-flex; align-items: center; gap: .75rem; font-family: var(--body); font-size: .75rem; font-weight: 500; letter-spacing: .15em; text-transform: uppercase; text-decoration: none; color: #fff; background: var(--navy); padding: 1rem 2rem; border: none; cursor: pointer; transition: background 300ms; }
.btn-primary:hover { background: #0078D4; }
.btn-ghost { display: inline-flex; align-items: center; gap: .75rem; font-family: var(--body); font-size: .75rem; font-weight: 400; letter-spacing: .15em; text-transform: uppercase; text-decoration: none; color: var(--t2); background: transparent; padding: 1rem 2rem; border: 1px solid var(--border2); cursor: pointer; transition: border-color 300ms, color 300ms; }
.btn-ghost:hover { border-color: rgba(0,82,160,.35); color: var(--t1); }
/* Nav */
#nav { position: fixed; top: 0; left: 0; right: 0; z-index: 100; transition: background 500ms, border-color 500ms; }
#nav.scrolled { background: rgba(250,250,248,.95); backdrop-filter: blur(12px); -webkit-backdrop-filter: blur(12px); border-bottom: 1px solid var(--border); }
.nav-inner { max-width: 1280px; margin: 0 auto; padding: 0 2rem; height: 4rem; display: flex; align-items: center; justify-content: space-between; position: relative; }
.nav-logo img { height: 2rem; width: auto; display: block; }
.nav-links { display: flex; align-items: center; gap: 1.25rem; flex-wrap: nowrap; }
.nav-link { font-family: var(--body); font-size: .675rem; font-weight: 400; letter-spacing: .16em; text-transform: uppercase; color: var(--t3); text-decoration: none; white-space: nowrap; transition: color 200ms; }
.nav-link:hover { color: var(--t1); }
.nav-dropdown { position: relative; display: flex; align-items: center; }
.nav-dropdown-btn { background: none; border: none; cursor: pointer; padding: 0; font-family: var(--body); font-size: .675rem; font-weight: 400; letter-spacing: .16em; text-transform: uppercase; color: var(--t3); transition: color 200ms; white-space: nowrap; }
.nav-dropdown-btn:hover { color: var(--t1); }
.nav-dropdown-menu { display: none; position: absolute; top: calc(100% + .75rem); left: 50%; transform: translateX(-50%); background: #fff; border: 1px solid var(--border2); box-shadow: 0 8px 32px rgba(0,0,0,.10); min-width: 160px; flex-direction: column; z-index: 200; padding: .375rem 0; }
.nav-dropdown.open .nav-dropdown-menu { display: flex; }
.nav-dropdown-item { font-family: var(--body); font-size: .8125rem; font-weight: 400; color: var(--t2); text-decoration: none; padding: .5rem 1.25rem; transition: background 150ms, color 150ms; white-space: nowrap; }
.nav-dropdown-item:hover { background: var(--bg2); color: var(--t1); }
.nav-cta { font-family: var(--body); font-size: .675rem; font-weight: 500; letter-spacing: .15em; text-transform: uppercase; color: #fff; background: var(--navy); text-decoration: none; padding: .575rem 1.1rem; white-space: nowrap; box-shadow: 0 2px 12px rgba(0,82,160,.20); transition: background 300ms; }
.nav-cta:hover { background: #0078D4; }
.nav-hamburger { display: none; flex-direction: column; justify-content: center; gap: 5px; background: none; border: none; cursor: pointer; padding: 6px 4px; z-index: 101; -webkit-tap-highlight-color: transparent; }
.nav-hamburger span { display: block; width: 22px; height: 2px; background: var(--t1); border-radius: 1px; transition: transform 280ms ease, opacity 200ms ease; transform-origin: center; }
.nav-hamburger[aria-expanded="true"] span:nth-child(1) { transform: translateY(7px) rotate(45deg); }
.nav-hamburger[aria-expanded="true"] span:nth-child(2) { opacity: 0; transform: scaleX(0); }
.nav-hamburger[aria-expanded="true"] span:nth-child(3) { transform: translateY(-7px) rotate(-45deg); }
.nav-mobile { display: none; position: absolute; top: 4rem; left: 0; right: 0; background: rgba(250,250,248,.98); backdrop-filter: blur(16px); -webkit-backdrop-filter: blur(16px); border-bottom: 1px solid var(--border); padding: .5rem 0 1.5rem; flex-direction: column; box-shadow: 0 8px 32px rgba(0,0,0,.08); }
.nav-mobile.open { display: flex; }
.nav-mobile-link { display: block; padding: .875rem 2rem; font-family: var(--body); font-size: .75rem; font-weight: 400; letter-spacing: .16em; text-transform: uppercase; color: var(--t2); text-decoration: none; border-bottom: 1px solid rgba(0,0,0,.05); transition: color 150ms, background 150ms; }
.nav-mobile-link:last-child { border-bottom: none; }
.nav-mobile-link:hover { color: var(--t1); background: rgba(0,82,160,.03); }
.nav-mobile-cta { display: block; margin: 1rem 2rem 0; padding: .8rem 1.5rem; text-align: center; font-family: var(--body); font-size: .75rem; font-weight: 500; letter-spacing: .15em; text-transform: uppercase; color: #fff; background: var(--navy); text-decoration: none; }
@media (max-width: 1060px) { .nav-links { display: none; } .nav-hamburger { display: flex; } }
@media (min-width: 1061px) { .nav-mobile { display: none !important; } .nav-hamburger { display: none !important; } }
/* Gallery */
.gallery-wrap { max-width: 1100px; margin: 0 auto; padding: 7rem 2.5rem 5rem; position: relative; z-index: 1; }
.gallery-header { margin-bottom: 3rem; }
.gallery-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 1.25rem; }
@media (max-width: 900px) { .gallery-grid { grid-template-columns: 1fr 1fr; } }
@media (max-width: 600px) { .gallery-grid { grid-template-columns: 1fr; } }
.gal-card { background: var(--card); border: 1px solid var(--border); padding: 1.5rem; display: flex; flex-direction: column; gap: .875rem; transition: border-color .15s, box-shadow .15s; }
.gal-card:hover { border-color: rgba(0,82,160,.25); box-shadow: 0 4px 20px rgba(0,82,160,.07); }
.gal-link { text-decoration: none; color: inherit; display: flex; flex-direction: column; gap: .875rem; flex: 1; }
.gal-q { font-family: var(--body); font-weight: 500; font-size: .875rem; color: var(--navy); line-height: 1.5; display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; }
.gal-a { font-family: var(--body); font-weight: 300; font-size: .8125rem; color: var(--t2); line-height: 1.7; display: -webkit-box; -webkit-line-clamp: 6; -webkit-box-orient: vertical; overflow: hidden; flex: 1; max-height: 11rem; }
.gal-a p { margin: 0; }
.gal-a p + p { margin-top: .35rem; }
.gal-a ol, .gal-a ul { padding-left: 1.25rem; margin: .25rem 0; }
.gal-a li + li { margin-top: .15rem; }
.gal-a strong { font-weight: 600; color: var(--t1); }
.gal-a em { font-style: italic; }
.gal-a code { background: rgba(0,0,0,0.05); padding: .1em .3em; border-radius: 3px; font-family: ui-monospace, 'IBM Plex Mono', Menlo, monospace; font-size: .85em; }
.gal-a pre { background: var(--bg2); padding: .5rem; overflow-x: auto; font-size: .75rem; margin: .4rem 0; border-radius: 3px; }
.gal-a pre code { background: none; padding: 0; }
.gal-a a { color: var(--navy); text-decoration: underline; }
.gal-a h1, .gal-a h2, .gal-a h3, .gal-a h4 { font-size: 1em; font-weight: 600; margin: .3rem 0; color: var(--t1); }
.gal-a blockquote { border-left: 2px solid var(--border2); padding-left: .5rem; color: var(--t2); margin: .3rem 0; }
.gal-a img { max-width: 100%; height: auto; }
.gal-meta { display: flex; justify-content: space-between; align-items: center; font-family: var(--body); font-size: .65rem; font-weight: 400; letter-spacing: .1em; text-transform: uppercase; color: var(--t3); margin-top: auto; padding-top: .75rem; border-top: 1px solid var(--border); }
.gal-date { font-size: .65rem; color: var(--t3); }
/* Vote controls */
.vote-controls { display: inline-flex; align-items: center; gap: .35rem; }
.vote-btn { background: transparent; border: 1px solid var(--border2); color: var(--t3); cursor: pointer; padding: .2rem .45rem; font-size: .7rem; line-height: 1; transition: all .15s; -webkit-tap-highlight-color: transparent; }
.vote-btn:hover:not(:disabled) { color: var(--navy); border-color: rgba(0,82,160,.35); background: rgba(0,82,160,.04); }
.vote-btn:focus-visible { outline: 2px solid var(--navy); outline-offset: 1px; }
.vote-btn:disabled { opacity: .55; cursor: not-allowed; }
.vote-btn.is-active.vote-up { background: rgba(46,125,50,.10); color: var(--up); border-color: rgba(46,125,50,.45); }
.vote-btn.is-active.vote-down { background: rgba(198,40,40,.10); color: var(--down); border-color: rgba(198,40,40,.45); }
.vote-btn.is-loading { opacity: .55; cursor: wait; }
.vote-score { font-size: .8rem; font-weight: 500; color: var(--t1); min-width: 1.5rem; text-align: center; font-variant-numeric: tabular-nums; }
.gallery-controls { display: flex; align-items: center; gap: 1rem; margin-bottom: 2rem; flex-wrap: wrap; }
.gallery-search { flex: 1; min-width: 200px; font-family: var(--body); font-size: .875rem; font-weight: 300; color: var(--t1); background: #fff; border: 1px solid var(--border2); padding: .625rem 1rem; outline: none; transition: border-color .2s; }
.gallery-search:focus { border-color: var(--navy); }
.gallery-search::placeholder { color: var(--t3); }
.sort-btn { font-family: var(--body); font-size: .7rem; font-weight: 400; letter-spacing: .1em; text-transform: uppercase; color: var(--t3); background: none; border: 1px solid var(--border2); padding: .5rem 1rem; cursor: pointer; transition: all .15s; }
.sort-btn:hover, .sort-btn.active { color: var(--navy); border-color: rgba(0,82,160,.3); background: rgba(0,82,160,.03); }
.gallery-empty { grid-column: 1/-1; padding: 4rem 0; text-align: center; }
.gal-card.hidden { display: none; }
/* Sign-in modal */
.signin-modal { position: fixed; inset: 0; background: rgba(13,13,20,.55); display: none; align-items: center; justify-content: center; z-index: 1000; padding: 1rem; }
.signin-modal.open { display: flex; }
.signin-modal-card { background: #fff; border: 1px solid var(--border2); padding: 2rem; max-width: 22rem; width: 100%; box-shadow: 0 16px 48px rgba(0,0,0,.18); }
.signin-modal h2 { font-family: var(--head); font-size: 1.4rem; font-weight: 600; color: var(--t1); margin-bottom: .5rem; }
.signin-modal p { font-family: var(--body); font-size: .85rem; font-weight: 300; color: var(--t2); line-height: 1.6; margin-bottom: 1.25rem; }
.signin-modal input { width: 100%; box-sizing: border-box; font-family: var(--body); font-size: .875rem; font-weight: 300; color: var(--t1); background: #fff; border: 1px solid var(--border2); padding: .75rem 1rem; outline: none; transition: border-color .2s; margin-bottom: .75rem; }
.signin-modal input:focus { border-color: var(--navy); }
.signin-modal-actions { display: flex; gap: .5rem; justify-content: space-between; align-items: center; }
.signin-modal-msg { font-size: .8rem; color: var(--t2); margin-top: .75rem; min-height: 1rem; }
.signin-modal .btn-primary { padding: .7rem 1.25rem; font-size: .7rem; }
.signin-modal-cancel { background: none; border: none; color: var(--t3); cursor: pointer; font-family: var(--body); font-size: .75rem; letter-spacing: .14em; text-transform: uppercase; padding: .7rem .5rem; }
.signin-modal-cancel:hover { color: var(--t1); }
</style>
</head>
<body>
<nav id="nav">
<div class="nav-inner">
<a href="/" class="nav-logo" aria-label="Neuron home"><img src="/assets/brand/neuron-wordmark-on-light.png" srcset="/assets/brand/neuron-wordmark-on-light@2x.png 2x" alt="Neuron" height="28"></a>
<div class="nav-links">
<a href="/#how-it-works" class="nav-link">How it works</a>
<div class="nav-dropdown">
<button class="nav-link nav-dropdown-btn" aria-haspopup="true" aria-expanded="false">Mission &#9662;</button>
<div class="nav-dropdown-menu">
<a href="/#mission" class="nav-dropdown-item">Our mission</a>
<a href="/#safety" class="nav-dropdown-item">Safety</a>
<a href="/#environmental" class="nav-dropdown-item">Environment</a>
</div>
</div>
<a href="/#pricing" class="nav-link">Pricing</a>
<a href="/#marketplace" class="nav-link">Marketplace</a>
<a href="/#enterprise" class="nav-link">Enterprise</a>
<a href="/about" class="nav-link">About</a>
<a href="/said" class="nav-link">Gallery</a>
<a href="/account" class="nav-link">Account</a>
<a href="/#pricing" class="nav-cta">Get Access</a>
</div>
<button class="nav-hamburger" id="nav-hamburger" aria-label="Open navigation" aria-expanded="false" aria-controls="nav-mobile">
<span></span><span></span><span></span>
</button>
<div class="nav-mobile" id="nav-mobile" role="navigation" aria-label="Mobile navigation">
<a href="/#how-it-works" class="nav-mobile-link">How it works</a>
<a href="/#mission" class="nav-mobile-link">Mission</a>
<a href="/#safety" class="nav-mobile-link" style="padding-left:1.75rem;font-size:.8rem;color:var(--t3)">- Safety</a>
<a href="/#environmental" class="nav-mobile-link" style="padding-left:1.75rem;font-size:.8rem;color:var(--t3)">- Environment</a>
<a href="/#pricing" class="nav-mobile-link">Pricing</a>
<a href="/#enterprise" class="nav-mobile-link">Enterprise</a>
<a href="/about" class="nav-mobile-link">About</a>
<a href="/said" class="nav-mobile-link">Gallery</a>
<a href="/account" class="nav-mobile-link">Account</a>
<a href="/#pricing" class="nav-mobile-cta">Get Access</a>
</div>
</div>
</nav>
<div class="gallery-wrap">
<div class="gallery-header">
<p class="label" style="margin-bottom:1rem">Things Neuron Said</p>
<h1 class="display-lg" style="margin-bottom:1rem">Real conversations.<br>No curation.</h1>
<p style="font-family:var(--body);font-weight:300;font-size:.9375rem;color:var(--t2);line-height:1.75;max-width:36rem">
People chatted with Neuron and shared the exchanges that mattered. Voted up by readers. The best ones rise.
</p>
</div>
<div class="gallery-controls">
<input type="search" id="gal-search" class="gallery-search" placeholder="Search conversations..." autocomplete="off">
<div style="display:flex;gap:.5rem;flex-shrink:0">
<button class="sort-btn active" id="sort-top" onclick="setSort('top',this)">Top</button>
<button class="sort-btn" id="sort-new" onclick="setSort('new',this)">Newest</button>
</div>
</div>
<div class="gallery-grid" id="gallery-grid">{raw(cards_html)}{raw(empty_html)}</div>
<div id="no-results" style="display:none;padding:3rem 0;text-align:center">
<p style="font-family:var(--body);font-weight:300;color:var(--t3)">No conversations match that search.</p>
</div>
<div style="margin-top:3rem">
<a href="/" class="btn-ghost">&#8592; Back to site</a>
</div>
</div>
<!-- Magic-link sign-in modal -->
<div class="signin-modal" id="signin-modal" role="dialog" aria-modal="true" aria-labelledby="signin-title">
<div class="signin-modal-card">
<h2 id="signin-title">Sign in to vote</h2>
<p>We will email you a sign-in link. No password needed.</p>
<input type="email" id="signin-email" placeholder="your@email.com" autocomplete="email">
<div class="signin-modal-actions">
<button type="button" class="signin-modal-cancel" id="signin-cancel">Cancel</button>
<button type="button" class="btn-primary" id="signin-send">Send link</button>
</div>
<p class="signin-modal-msg" id="signin-msg"></p>
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/@supabase/supabase-js@2/dist/umd/supabase.min.js"></script>
<script>
window.NEURON_CFG=window.NEURON_CFG||{};
window.NEURON_CFG.supabase_url="{supabase_url}";
window.NEURON_CFG.supabase_anon_key="{supabase_anon_key}";
</script>
<script src="/js/gallery.js" defer></script>
</body>
</html>
let all_css: String = gallery_css_base() + gallery_css_nav() + gallery_css_gallery() + gallery_css_modal()
let style_block: String = "<style>" + all_css + "</style>"
let head_html: String =
el_meta_charset("UTF-8") +
"<meta name=\"viewport\" content=\"width=device-width,initial-scale=1\">" +
el_title("Things Neuron Said") +
el_meta("description", "Real conversations with Neuron, voted up by users.") +
"<link rel=\"icon\" type=\"image/png\" sizes=\"32x32\" href=\"/assets/favicon-32.png\">" +
"<link rel=\"preconnect\" href=\"https://fonts.googleapis.com\">" +
"<link rel=\"preconnect\" href=\"https://fonts.gstatic.com\" crossorigin>" +
"<link href=\"https://fonts.googleapis.com/css2?family=Playfair+Display:ital,wght@0,400;0,600;1,400&family=IBM+Plex+Sans:wght@300;400;500;600&display=swap\" rel=\"stylesheet\">" +
style_block
let sort_controls: String = el_div(
"style=\"display:flex;gap:.5rem;flex-shrink:0\"",
el_button("class=\"sort-btn active\" id=\"sort-top\" onclick=\"setSort('top',this)\"", "Top") +
el_button("class=\"sort-btn\" id=\"sort-new\" onclick=\"setSort('new',this)\"", "Newest")
)
let gallery_controls: String = el_div(
"class=\"gallery-controls\"",
el_input("search", "id=\"gal-search\" class=\"gallery-search\" placeholder=\"Search conversations...\" autocomplete=\"off\"") +
sort_controls
)
let no_results: String = el_div(
"id=\"no-results\" style=\"display:none;padding:3rem 0;text-align:center\"",
el_p("style=\"font-family:var(--body);font-weight:300;color:var(--t3)\"", "No conversations match that search.")
)
let gallery_header: String = el_div(
"class=\"gallery-header\"",
el_p("class=\"label\" style=\"margin-bottom:1rem\"", "Things Neuron Said") +
el_h1("class=\"display-lg\" style=\"margin-bottom:1rem\"", "Real conversations." + el_br() + "No curation.") +
el_p("style=\"font-family:var(--body);font-weight:300;font-size:.9375rem;color:var(--t2);line-height:1.75;max-width:36rem\"",
"People chatted with Neuron and shared the exchanges that mattered. Voted up by readers. The best ones rise."
)
)
let gallery_grid: String = el_div("class=\"gallery-grid\" id=\"gallery-grid\"", cards_html + empty_html)
let gallery_wrap: String = el_div(
"class=\"gallery-wrap\"",
gallery_header + gallery_controls + gallery_grid + no_results +
el_div("style=\"margin-top:3rem\"", el_a("/", "class=\"btn-ghost\"", "&#8592; Back to site"))
)
let signin_modal: String = el_div(
"class=\"signin-modal\" id=\"signin-modal\" role=\"dialog\" aria-modal=\"true\" aria-labelledby=\"signin-title\"",
el_div(
"class=\"signin-modal-card\"",
el_h2("id=\"signin-title\"", "Sign in to vote") +
el_p("", "We will email you a sign-in link. No password needed.") +
el_input("email", "id=\"signin-email\" placeholder=\"your@email.com\" autocomplete=\"email\"") +
el_div(
"class=\"signin-modal-actions\"",
el_button("type=\"button\" class=\"signin-modal-cancel\" id=\"signin-cancel\"", "Cancel") +
el_button("type=\"button\" class=\"btn-primary\" id=\"signin-send\"", "Send link")
) +
el_p("class=\"signin-modal-msg\" id=\"signin-msg\"", "")
)
)
let cfg_js: String = "window.NEURON_CFG=window.NEURON_CFG||{};window.NEURON_CFG.supabase_url=\"" + supabase_url + "\";window.NEURON_CFG.supabase_anon_key=\"" + supabase_anon_key + "\";"
let body_html: String =
gallery_nav_html() +
gallery_wrap +
signin_modal +
"<script src=\"https://cdn.jsdelivr.net/npm/@supabase/supabase-js@2/dist/umd/supabase.min.js\"></script>" +
el_script_inline(cfg_js) +
el_script_src("/js/gallery.js", true)
el_html_doc("en", head_html, body_html)
}
+43 -24
View File
@@ -1,37 +1,56 @@
// components/hero.el - Hero section.
// Rebuilt: 2026-05-11.
//
// Full-bleed hero with headline, sub-copy, and two CTAs.
// Glow orbs are pure CSS absolute-positioned divs.
// Entrance animations are CSS keyframes with class-based delays.
extern fn el_section(attrs: String, children: String) -> String
extern fn el_div(attrs: String, children: String) -> String
extern fn el_p(attrs: String, children: String) -> String
extern fn el_h1(attrs: String, text: String) -> String
extern fn el_a(href: String, attrs: String, children: String) -> String
extern fn el_span(attrs: String, children: String) -> String
fn hero() -> String {
return <section id="hero" aria-label="Hero">
<div class="glow-tl" aria-hidden="true"></div>
<div class="glow-br" aria-hidden="true"></div>
let glows: String =
el_div("class=\"glow-tl\" aria-hidden=\"true\"", "") +
el_div("class=\"glow-br\" aria-hidden=\"true\"", "")
<p class="label hero-label animate-up-1">One builder. Patented. No permission.</p>
let label: String = el_p(
"class=\"label hero-label animate-up-1\"",
"One builder. Patented. No permission."
)
<h1 class="display-xl hero-headline animate-up-2">
Every AI resets when you close the tab.
<em class="gold" style="font-style:normal">I built the one that doesn&#39;t.</em>
</h1>
let headline_em: String = el_span("class=\"gold\" style=\"font-style:normal\"",
"I built the one that doesn&#39;t."
)
let headline: String = el_h1(
"class=\"display-xl hero-headline animate-up-2\"",
"Every AI resets when you close the tab. " + headline_em
)
<p class="hero-sub animate-up-3">
Runs on your machine. Remembers everything. Priced below ChatGPT on day one.
</p>
let sub: String = el_p(
"class=\"hero-sub animate-up-3\"",
"Runs on your machine. Remembers everything. Priced below ChatGPT on day one."
)
<div class="hero-ctas animate-up-5">
<a href="#pricing" class="btn-primary">
Preorder <span>&#8594;</span>
</a>
<a href="#how-it-works" class="btn-ghost">
See how it works
</a>
</div>
let cta_primary: String = el_a(
"#pricing",
"class=\"btn-primary\"",
"Preorder " + el_span("", "&#8594;")
)
let cta_ghost: String = el_a(
"#how-it-works",
"class=\"btn-ghost\"",
"See how it works"
)
let ctas: String = el_div("class=\"hero-ctas animate-up-5\"", cta_primary + cta_ghost)
<div class="hero-scroll" aria-hidden="true">
<span>Scroll</span>
<div class="hero-scroll-line"></div>
</div>
</section>
let scroll_inner: String =
el_span("", "Scroll") +
el_div("class=\"hero-scroll-line\"", "")
let scroll: String = el_div("class=\"hero-scroll\" aria-hidden=\"true\"", scroll_inner)
el_section("id=\"hero\" aria-label=\"Hero\"", glows + label + headline + sub + ctas + scroll)
}
+56 -43
View File
@@ -1,52 +1,65 @@
// components/how_it_works.el - "Three moments. Permanent context." steps.
extern fn el_section(attrs: String, children: String) -> String
extern fn el_div(attrs: String, children: String) -> String
extern fn el_span(attrs: String, children: String) -> String
extern fn el_h2(attrs: String, text: String) -> String
extern fn el_h3(attrs: String, text: String) -> String
extern fn el_p(attrs: String, children: String) -> String
extern fn el_br() -> String
fn how_it_works() -> String {
return <section id="how-it-works" aria-label="How it works">
<div class="container">
let header: String = el_div(
"class=\"hiw-header\"",
el_div("class=\"navy-line\" style=\"margin-bottom:5rem\"", "") +
el_div(
"class=\"hiw-label-row reveal\"",
el_div("class=\"navy-line-left\" style=\"width:4rem;flex-shrink:0\"", "") +
el_span("class=\"label\"", "How it works")
) +
el_h2(
"class=\"display-lg hiw-headline reveal\" style=\"transition-delay:80ms\"",
"Three moments." + el_br() + el_span("class=\"gold\"", "Permanent context.")
)
)
<div class="hiw-header">
<div class="navy-line" style="margin-bottom:5rem"></div>
<div class="hiw-label-row reveal">
<div class="navy-line-left" style="width:4rem;flex-shrink:0"></div>
<span class="label">How it works</span>
</div>
<h2 class="display-lg hiw-headline reveal" style="transition-delay:80ms">
Three moments.<br>
<span class="gold">Permanent context.</span>
</h2>
</div>
let step1: String = el_div(
"class=\"reveal\"",
el_div(
"class=\"step-circle-row\"",
el_div("class=\"step-circle\"", el_span("class=\"step-num\"", "01")) +
el_span("class=\"step-aside\"", "5 minutes")
) +
el_h3("class=\"display-md step-title\"", "Create your account") +
el_p("class=\"step-body\"", "Sign up once. Your account links your license key to your identity - that&#39;s what keeps your memory yours and nobody else&#39;s. Then install with a single command. Runs locally. No cloud sync, no permissions you didn&#39;t grant.")
)
<div class="steps-grid">
let step2: String = el_div(
"class=\"reveal\" style=\"transition-delay:180ms\"",
el_div(
"class=\"step-circle-row\"",
el_div("class=\"step-circle\"", el_span("class=\"step-num\"", "02")) +
el_span("class=\"step-aside\"", "Just once")
) +
el_h3("class=\"display-md step-title\"", "Your first session") +
el_p("class=\"step-body\"", "Introduce yourself once. Neuron builds a model of your work, your preferences, and your context from the first conversation.")
)
<div class="reveal">
<div class="step-circle-row">
<div class="step-circle"><span class="step-num">01</span></div>
<span class="step-aside">5 minutes</span>
</div>
<h3 class="display-md step-title">Create your account</h3>
<p class="step-body">Sign up once. Your account links your license key to your identity - that&#39;s what keeps your memory yours and nobody else&#39;s. Then install with a single command. Runs locally. No cloud sync, no permissions you didn&#39;t grant.</p>
</div>
let step3: String = el_div(
"class=\"reveal\" style=\"transition-delay:360ms\"",
el_div(
"class=\"step-circle-row\"",
el_div("class=\"step-circle\"", el_span("class=\"step-num\"", "03")) +
el_span("class=\"step-aside\"", "Every session")
) +
el_h3("class=\"display-md step-title\"", "Never start over") +
el_p("class=\"step-body\"", "Open a new session days or months later. Neuron remembers. Every project, every decision, every thread - exactly where you left it.")
)
<div class="reveal" style="transition-delay:180ms">
<div class="step-circle-row">
<div class="step-circle"><span class="step-num">02</span></div>
<span class="step-aside">Just once</span>
</div>
<h3 class="display-md step-title">Your first session</h3>
<p class="step-body">Introduce yourself once. Neuron builds a model of your work, your preferences, and your context from the first conversation.</p>
</div>
let steps_grid: String = el_div("class=\"steps-grid\"", step1 + step2 + step3)
<div class="reveal" style="transition-delay:360ms">
<div class="step-circle-row">
<div class="step-circle"><span class="step-num">03</span></div>
<span class="step-aside">Every session</span>
</div>
<h3 class="display-md step-title">Never start over</h3>
<p class="step-body">Open a new session days or months later. Neuron remembers. Every project, every decision, every thread - exactly where you left it.</p>
</div>
</div>
</div>
</section>
el_section(
"id=\"how-it-works\" aria-label=\"How it works\"",
el_div("class=\"container\"", header + steps_grid)
)
}
+71 -51
View File
@@ -1,59 +1,79 @@
// components/inference.el - Neuron inference section.
// "My model. My infrastructure. Cheaper than theirs."
extern fn el_section(attrs: String, children: String) -> String
extern fn el_div(attrs: String, children: String) -> String
extern fn el_span(attrs: String, children: String) -> String
extern fn el_h2(attrs: String, text: String) -> String
extern fn el_p(attrs: String, children: String) -> String
fn inference() -> String {
return <section id="inference" aria-label="Neuron inference">
<div class="container">
<div class="inference-grid">
let label_row: String = el_div(
"class=\"inference-label-row reveal\"",
el_div("class=\"navy-line-left\" style=\"width:3rem;flex-shrink:0\"", "") +
el_span("class=\"label\"", "Neuron inference - Q3 2026")
)
<div>
<div class="inference-label-row reveal">
<div class="navy-line-left" style="width:3rem;flex-shrink:0"></div>
<span class="label">Neuron inference - Q3 2026</span>
</div>
<h2 class="display-lg inference-headline reveal" style="transition-delay:80ms">
My model. My infrastructure.
<span class="gold"> Targeting Q3 2026.</span>
</h2>
<p class="inference-body reveal" style="transition-delay:160ms">
Four companies control nearly all frontier AI inference today. Every query you send strengthens their position - and their pricing power over you. That changes in Q3 2026 - sooner if we can.
</p>
<p class="inference-body reveal" style="transition-delay:240ms">
Neuron will be both the product and the model. Purpose-built for how Neuron thinks, how your context is structured, how to do more with fewer tokens. It will run on Soma, our own inference infrastructure. Priced below OpenAI, Anthropic, and Google. Not as a loss leader. As a permanent competitive position.
</p>
<p class="inference-body reveal" style="transition-delay:320ms">
Until then: bring your own API keys - Anthropic, OpenAI, whoever. When Neuron Inference launches, it will be cheaper, faster, and purpose-built for Neuron. You can switch whenever you want.
</p>
<p class="inference-body reveal" style="transition-delay:400ms">
Every user on Neuron inference is compute that doesn&#39;t flow to the monopoly. That&#39;s not just a cost story. It&#39;s a power one.
</p>
</div>
let headline: String = el_h2(
"class=\"display-lg inference-headline reveal\" style=\"transition-delay:80ms\"",
"My model. My infrastructure." +
el_span("class=\"gold\"", " Targeting Q3 2026.")
)
<div class="inference-stats">
<div class="stat-row reveal">
<div class="stat-row-head">
<span class="stat-num">4</span>
<span class="stat-label">Companies control frontier inference</span>
</div>
<p class="stat-body">OpenAI, Google, Anthropic, Meta. Nearly every AI query flows through them. We&#39;re building the exit ramp.</p>
</div>
<div class="stat-row reveal" style="transition-delay:120ms">
<div class="stat-row-head">
<span class="stat-num">Neuron</span>
<span class="stat-label">The model - Q3 2026</span>
</div>
<p class="stat-body">A model built for Neuron specifically - not a generic wrapper. Your full context, years of accumulated intelligence, purpose-built inference. The model is the floor. Neuron is the ceiling.</p>
</div>
<div class="stat-row reveal" style="transition-delay:240ms">
<div class="stat-row-head">
<span class="stat-num">&lt; theirs</span>
<span class="stat-label">Priced below the incumbents</span>
</div>
<p class="stat-body">Below OpenAI pricing at launch. Below Anthropic. Below Google. Metered - you pay for what you use. And cheaper than any of them on every tier.</p>
</div>
</div>
let body_col: String = el_div(
"",
label_row +
headline +
el_p("class=\"inference-body reveal\" style=\"transition-delay:160ms\"",
"Four companies control nearly all frontier AI inference today. Every query you send strengthens their position - and their pricing power over you. That changes in Q3 2026 - sooner if we can."
) +
el_p("class=\"inference-body reveal\" style=\"transition-delay:240ms\"",
"Neuron will be both the product and the model. Purpose-built for how Neuron thinks, how your context is structured, how to do more with fewer tokens. It will run on Soma, our own inference infrastructure. Priced below OpenAI, Anthropic, and Google. Not as a loss leader. As a permanent competitive position."
) +
el_p("class=\"inference-body reveal\" style=\"transition-delay:320ms\"",
"Until then: bring your own API keys - Anthropic, OpenAI, whoever. When Neuron Inference launches, it will be cheaper, faster, and purpose-built for Neuron. You can switch whenever you want."
) +
el_p("class=\"inference-body reveal\" style=\"transition-delay:400ms\"",
"Every user on Neuron inference is compute that doesn&#39;t flow to the monopoly. That&#39;s not just a cost story. It&#39;s a power one."
)
)
</div>
</div>
</section>
let stat1: String = el_div(
"class=\"stat-row reveal\"",
el_div(
"class=\"stat-row-head\"",
el_span("class=\"stat-num\"", "4") +
el_span("class=\"stat-label\"", "Companies control frontier inference")
) +
el_p("class=\"stat-body\"", "OpenAI, Google, Anthropic, Meta. Nearly every AI query flows through them. We&#39;re building the exit ramp.")
)
let stat2: String = el_div(
"class=\"stat-row reveal\" style=\"transition-delay:120ms\"",
el_div(
"class=\"stat-row-head\"",
el_span("class=\"stat-num\"", "Neuron") +
el_span("class=\"stat-label\"", "The model - Q3 2026")
) +
el_p("class=\"stat-body\"", "A model built for Neuron specifically - not a generic wrapper. Your full context, years of accumulated intelligence, purpose-built inference. The model is the floor. Neuron is the ceiling.")
)
let stat3: String = el_div(
"class=\"stat-row reveal\" style=\"transition-delay:240ms\"",
el_div(
"class=\"stat-row-head\"",
el_span("class=\"stat-num\"", "&lt; theirs") +
el_span("class=\"stat-label\"", "Priced below the incumbents")
) +
el_p("class=\"stat-body\"", "Below OpenAI pricing at launch. Below Anthropic. Below Google. Metered - you pay for what you use. And cheaper than any of them on every tier.")
)
let stats_col: String = el_div("class=\"inference-stats\"", stat1 + stat2 + stat3)
let grid: String = el_div("class=\"inference-grid\"", body_col + stats_col)
el_section(
"id=\"inference\" aria-label=\"Neuron inference\"",
el_div("class=\"container\"", grid)
)
}
+87
View File
@@ -246,11 +246,98 @@ fn main() -> Void {
}
}
async function loadApiKeys() {
var apiSection = document.getElementById('api-keys-section');
if (apiSection) apiSection.style.display = '';
try {
var sess = await sb.auth.getSession();
var token = sess.data && sess.data.session ? sess.data.session.access_token : '';
if (!token) return;
var r = await fetch('/api/api-keys', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({access_token: token})
});
var d = await r.json();
if (!d.rows || !d.rows.length) return;
d.rows.forEach(function(row) {
var provider = row.provider;
var keyVal = row.key_value || '';
var maskedEl = document.getElementById('apikey-masked-' + provider);
var delBtn = document.getElementById('apikey-del-' + provider);
if (keyVal) {
var klen = keyVal.length;
var masked = klen <= 10 ? '' : keyVal.slice(0, 6) + '' + keyVal.slice(-4);
if (maskedEl) maskedEl.textContent = masked;
if (delBtn) delBtn.style.display = '';
}
});
} catch (e) {}
}
window.saveApiKey = async function(provider) {
var input = document.getElementById('apikey-input-' + provider);
var msg = document.getElementById('api-keys-msg');
if (!input || !input.value.trim()) {
if (msg) { msg.style.display = 'block'; msg.style.color = '#c44'; msg.textContent = 'Enter a key first.'; }
return;
}
var keyVal = input.value.trim();
var sess = await sb.auth.getSession();
var token = sess.data && sess.data.session ? sess.data.session.access_token : '';
if (!token) return;
try {
var r = await fetch('/api/api-keys/save', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({access_token: token, provider: provider, key: keyVal})
});
var d = await r.json();
if (d.ok) {
var maskedEl = document.getElementById('apikey-masked-' + provider);
var delBtn = document.getElementById('apikey-del-' + provider);
if (maskedEl) maskedEl.textContent = d.masked;
if (delBtn) delBtn.style.display = '';
input.value = '';
if (msg) { msg.style.display = 'block'; msg.style.color = 'var(--navy)'; msg.textContent = provider.charAt(0).toUpperCase() + provider.slice(1) + ' key saved.'; }
setTimeout(function() { if (msg) msg.style.display = 'none'; }, 3000);
} else {
if (msg) { msg.style.display = 'block'; msg.style.color = '#c44'; msg.textContent = d.error || 'Save failed.'; }
}
} catch (e) {
if (msg) { msg.style.display = 'block'; msg.style.color = '#c44'; msg.textContent = 'Network error.'; }
}
};
window.deleteApiKey = async function(provider) {
var sess = await sb.auth.getSession();
var token = sess.data && sess.data.session ? sess.data.session.access_token : '';
if (!token) return;
try {
var r = await fetch('/api/api-keys/delete', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({access_token: token, provider: provider})
});
var d = await r.json();
if (d.ok) {
var maskedEl = document.getElementById('apikey-masked-' + provider);
var delBtn = document.getElementById('apikey-del-' + provider);
if (maskedEl) maskedEl.textContent = 'Not configured';
if (delBtn) delBtn.style.display = 'none';
var msg = document.getElementById('api-keys-msg');
if (msg) { msg.style.display = 'block'; msg.style.color = 'var(--t3)'; msg.textContent = provider.charAt(0).toUpperCase() + provider.slice(1) + ' key removed.'; }
setTimeout(function() { if (msg) msg.style.display = 'none'; }, 3000);
}
} catch (e) {}
};
function showDashboard(user) {
hide('signin-section');
show('dashboard-section');
renderUserChip(user);
loadWaitlistData();
loadApiKeys();
}
async function init() {
+304 -41
View File
@@ -1,9 +1,9 @@
// chat-widget.el -- Neuron demo chat widget with Turnstile, session persistence,
// local engram graph, and share-pill.
// chat-widget.el -- Neuron demo chat widget with Supabase auth, Turnstile,
// session persistence, local engram graph, and share-pill.
// Compiled with: elc --target=js --bundle --minify --obfuscate
//
// Exposed globals: neuronDemoToggle(), neuronDemoSend(), neuronDemoReset()
// Required CDN: marked.js, Cloudflare Turnstile
// Required CDN: marked.js, Cloudflare Turnstile, Supabase JS
fn main() -> Void {
native_js("(function() {
@@ -14,6 +14,133 @@ fn main() -> Void {
var turnstileVerified = false;
var isOpen = false;
var MAX = 10;
var _userName = '';
var _userTimezone = (typeof Intl !== 'undefined' && Intl.DateTimeFormat)
? Intl.DateTimeFormat().resolvedOptions().timeZone
: '';
// Supabase auth state
var supabaseClient = null;
var _supabaseSession = null; // current session (null = not authenticated)
function initSupabaseWidget(cb) {
if (supabaseClient) { cb(); return; }
fetch('/api/supabase-config')
.then(function(r) { return r.json(); })
.then(function(cfg) {
supabaseClient = window.supabase.createClient(cfg.url, cfg.anon_key, {
auth: { flowType: 'implicit' }
});
supabaseClient.auth.getSession().then(function(res) {
if (res.data && res.data.session) {
_supabaseSession = res.data.session;
}
// Listen for sign-in from OAuth redirect
supabaseClient.auth.onAuthStateChange(function(event, session) {
if (session) {
_supabaseSession = session;
_onWidgetAuthenticated();
}
});
cb();
});
})
.catch(function() { cb(); });
}
function _onWidgetAuthenticated() {
// Capture user name for personalized greeting
if (_supabaseSession && _supabaseSession.user) {
var _meta = _supabaseSession.user.user_metadata || {};
_userName = _meta.full_name || _meta.name || _supabaseSession.user.email || '';
}
var authPane = document.getElementById('neuron-demo-auth');
var gate = document.getElementById('neuron-demo-gate');
var msgs = document.getElementById('neuron-demo-messages');
var inputRow = document.getElementById('neuron-demo-input-row');
if (authPane) authPane.style.display = 'none';
// Only reveal chat UI if Turnstile has also passed
if (turnstileVerified) {
if (gate) gate.style.display = 'none';
if (msgs) msgs.style.display = 'flex';
if (inputRow) inputRow.style.display = 'flex';
var msgs2 = document.getElementById('neuron-demo-messages');
if (msgs2 && msgs2.children.length === 0) {
if (session && session.messages && session.messages.length > 0) {
session.messages.forEach(function(m) { addMsg(m.role, m.text, true); });
} else if (!session.greeted) {
_sendIntroGreeting();
}
}
var inp = document.getElementById('neuron-demo-text');
if (inp) inp.focus();
}
}
function _renderWidgetAuthPane() {
var authPane = document.getElementById('neuron-demo-auth');
if (!authPane) return;
authPane.innerHTML = '';
authPane.style.display = 'flex';
var heading = document.createElement('p');
heading.className = 'demo-auth-heading';
heading.textContent = 'Sign in to chat with Neuron';
authPane.appendChild(heading);
var googleBtn = document.createElement('button');
googleBtn.className = 'demo-auth-google-btn';
googleBtn.innerHTML = '<svg width=\"18\" height=\"18\" viewBox=\"0 0 48 48\"><path fill=\"#EA4335\" d=\"M24 9.5c3.14 0 5.95 1.08 8.17 2.84l6.09-6.09C34.46 3.19 29.53 1 24 1 14.62 1 6.68 6.84 3.32 15.09l7.1 5.52C12.16 14.02 17.6 9.5 24 9.5z\"/><path fill=\"#4285F4\" d=\"M46.5 24.5c0-1.64-.15-3.22-.42-4.75H24v9h12.7c-.55 2.99-2.2 5.53-4.68 7.24l7.19 5.59C43.07 37.23 46.5 31.3 46.5 24.5z\"/><path fill=\"#FBBC05\" d=\"M10.42 28.39A14.6 14.6 0 0 1 9.5 24c0-1.52.26-3 .72-4.39l-7.1-5.52A23.5 23.5 0 0 0 .5 24c0 3.78.88 7.36 2.44 10.56l7.48-6.17z\"/><path fill=\"#34A853\" d=\"M24 47c5.53 0 10.17-1.83 13.56-4.97l-7.19-5.59C28.56 37.88 26.38 38.5 24 38.5c-6.4 0-11.84-4.52-13.58-10.61l-7.48 6.17C6.68 43.16 14.62 47 24 47z\"/><path fill=\"none\" d=\"M0 0h48v48H0z\"/></svg> Continue with Google';
googleBtn.onclick = function() {
if (!supabaseClient) return;
supabaseClient.auth.signInWithOAuth({
provider: 'google',
options: { redirectTo: window.location.href }
});
};
authPane.appendChild(googleBtn);
var emailToggle = document.createElement('button');
emailToggle.className = 'demo-auth-email-toggle';
emailToggle.textContent = 'or continue with email';
authPane.appendChild(emailToggle);
var emailForm = document.createElement('div');
emailForm.className = 'demo-auth-email-form';
emailForm.style.display = 'none';
emailForm.innerHTML = '<input type=\"email\" id=\"demo-auth-email\" placeholder=\"Email\" autocomplete=\"email\" />'
+ '<input type=\"password\" id=\"demo-auth-password\" placeholder=\"Password\" autocomplete=\"current-password\" />'
+ '<button class=\"demo-auth-submit-btn\" id=\"demo-auth-submit\">Sign in</button>'
+ '<p class=\"demo-auth-msg\" id=\"demo-auth-msg\" style=\"display:none\"></p>';
authPane.appendChild(emailForm);
emailToggle.onclick = function() {
emailForm.style.display = emailForm.style.display === 'none' ? 'flex' : 'none';
};
var submitBtn = emailForm.querySelector('#demo-auth-submit');
if (submitBtn) {
submitBtn.onclick = function() {
var email = (document.getElementById('demo-auth-email') || {}).value || '';
var pass = (document.getElementById('demo-auth-password') || {}).value || '';
var msgEl = document.getElementById('demo-auth-msg');
if (!email || !pass) {
if (msgEl) { msgEl.textContent = 'Email and password required.'; msgEl.style.display = ''; msgEl.style.color = '#e53e3e'; }
return;
}
submitBtn.disabled = true;
supabaseClient.auth.signInWithPassword({ email: email, password: pass }).then(function(res) {
if (res.error) {
if (msgEl) { msgEl.textContent = res.error.message || 'Sign in failed.'; msgEl.style.display = ''; msgEl.style.color = '#e53e3e'; }
submitBtn.disabled = false;
} else {
_supabaseSession = res.data.session;
_onWidgetAuthenticated();
}
}).catch(function() { submitBtn.disabled = false; });
};
}
}
function loadSession() {
try {
@@ -76,19 +203,112 @@ fn main() -> Void {
saveSession(session);
}
var msgCount = session.count || 0;
var _headerResetInterval = null;
function _nextMidnightUTC() {
var now = new Date();
var midnight = new Date(Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), now.getUTCDate() + 1));
return Math.floor(midnight.getTime() / 1000);
}
function _startHeaderResetTimer() {
var el = document.getElementById('neuron-demo-countdown');
if (!el) return;
if (_headerResetInterval) return; // already ticking
var resetAt = _nextMidnightUTC();
function _tick() {
var secsLeft = Math.max(0, resetAt - Math.floor(Date.now() / 1000));
var hh = Math.floor(secsLeft / 3600);
var mm = Math.floor((secsLeft % 3600) / 60);
var ss = secsLeft % 60;
var pad = function(n) { return n < 10 ? '0' + n : '' + n; };
var ts = hh + ':' + pad(mm) + ':' + pad(ss);
var el2 = document.getElementById('neuron-demo-countdown');
if (!el2) { clearInterval(_headerResetInterval); _headerResetInterval = null; return; }
el2.textContent = 'resets in ' + ts;
el2.style.color = 'rgba(255,255,255,0.65)';
el2.style.fontWeight = '600';
if (secsLeft <= 0) {
clearInterval(_headerResetInterval);
_headerResetInterval = null;
el2.textContent = '10 questions left';
el2.style.color = '#ffffff';
el2.style.fontWeight = '700';
}
}
_tick();
_headerResetInterval = setInterval(_tick, 1000);
}
function updateCountdown() {
var el = document.getElementById('neuron-demo-countdown');
if (!el) return;
var remaining = MAX - msgCount;
el.textContent = remaining + ' question' + (remaining === 1 ? '' : 's') + ' left';
el.style.color = '#ffffff';
el.style.fontWeight = '700';
if (remaining <= 0) {
_startHeaderResetTimer();
} else {
if (_headerResetInterval) { clearInterval(_headerResetInterval); _headerResetInterval = null; }
el.textContent = remaining + ' question' + (remaining === 1 ? '' : 's') + ' left';
el.style.color = '#ffffff';
el.style.fontWeight = '700';
}
}
function _timeOfDay() {
var h = new Date().getHours();
if (h < 12) return 'morning';
if (h < 17) return 'afternoon';
if (h < 21) return 'evening';
return 'night';
}
function _sendIntroGreeting() {
if (session.greeted) return;
session.greeted = true;
saveSession(session);
var accessToken = (_supabaseSession && _supabaseSession.access_token) ? _supabaseSession.access_token : '';
fetch('/api/demo', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({
message: '__intro_phase1__',
history: [],
cf_token: '',
uid: session.uid || '',
access_token: accessToken,
user_name: _userName,
user_timezone: _userTimezone,
time_of_day: _timeOfDay(),
is_return: session.count > 0 ? 'true' : 'false',
activated_nodes: [],
engram_node_count: 0,
questions_remaining: MAX,
is_last_question: false
})
})
.then(function(r) { return r.json(); })
.then(function(d) {
var reply = d.response || d.reply || d.message || '';
if (reply) {
addMsg('ai', reply, true);
session.messages = session.messages || [];
session.messages.push({ role: 'ai', text: reply });
saveSession(session);
} else {
addMsg('ai', \"Hey. What's on your mind?\", true);
}
})
.catch(function() {
addMsg('ai', \"Hey. What's on your mind?\", true);
});
}
window.neuronDemoReset = function() {
if (_headerResetInterval) { clearInterval(_headerResetInterval); _headerResetInterval = null; }
clearSession();
session = { messages: [], count: 0, context: '' };
session.uid = 'u' + Date.now().toString(36) + Math.random().toString(36).slice(2, 7);
saveSession(session);
msgCount = 0;
var msgs = document.getElementById('neuron-demo-messages');
if (msgs) msgs.innerHTML = '';
@@ -96,7 +316,7 @@ fn main() -> Void {
if (input) { input.disabled = false; input.placeholder = 'Ask me anything...'; }
var btn = document.getElementById('neuron-demo-send');
if (btn) btn.disabled = false;
addMsg('ai', 'Hey. What is on your mind?', true);
_sendIntroGreeting();
};
window.neuronDemoToggle = function() {
@@ -106,7 +326,7 @@ fn main() -> Void {
var btn = document.getElementById('neuron-demo-btn');
if (btn) btn.style.display = isOpen ? 'none' : '';
var msgs = document.getElementById('neuron-demo-messages');
if (isOpen && turnstileVerified && msgs && msgs.style.display !== 'none' && msgs.children.length === 0) {
if (isOpen && turnstileVerified && _supabaseSession && msgs && msgs.style.display !== 'none' && msgs.children.length === 0) {
if (session.messages && session.messages.length > 0) {
session.messages.forEach(function(m) { addMsg(m.role, m.text, true); });
var remaining = MAX - msgCount;
@@ -115,44 +335,56 @@ fn main() -> Void {
if (input) { input.disabled = true; input.placeholder = 'Interaction limit reached'; }
}
} else if (!session.greeted) {
addMsg('ai', 'Hey. What is on your mind?', true);
session.greeted = true;
saveSession(session);
_sendIntroGreeting();
}
}
var input = document.getElementById('neuron-demo-text');
if (isOpen && input && !input.disabled) input.focus();
updateCountdown();
if (isOpen && !turnstileWidgetId && typeof turnstile !== 'undefined') {
var container = document.getElementById('neuron-demo-turnstile');
if (container) {
turnstileWidgetId = turnstile.render(container, {
sitekey: TURNSTILE_SITE_KEY,
size: 'compact',
callback: function(token) {
turnstileToken = token;
turnstileVerified = true;
if (typeof turnstile !== 'undefined' && turnstileWidgetId !== null) {
try { turnstile.remove(turnstileWidgetId); } catch(e) {}
turnstileWidgetId = null;
if (isOpen) {
// Initialize Supabase on first open, then decide what to show
initSupabaseWidget(function() {
if (!_supabaseSession) {
// Not authenticated show auth pane, hide Turnstile gate
var gate = document.getElementById('neuron-demo-gate');
if (gate) gate.style.display = 'none';
_renderWidgetAuthPane();
} else {
// Authenticated proceed with Turnstile gate as normal
if (!turnstileWidgetId && typeof turnstile !== 'undefined') {
var container = document.getElementById('neuron-demo-turnstile');
if (container) {
turnstileWidgetId = turnstile.render(container, {
sitekey: TURNSTILE_SITE_KEY,
size: 'compact',
callback: function(token) {
turnstileToken = token;
turnstileVerified = true;
if (typeof turnstile !== 'undefined' && turnstileWidgetId !== null) {
try { turnstile.remove(turnstileWidgetId); } catch(e) {}
turnstileWidgetId = null;
}
var gate = document.getElementById('neuron-demo-gate');
var msgs = document.getElementById('neuron-demo-messages');
var inputRow = document.getElementById('neuron-demo-input-row');
if (gate) gate.style.display = 'none';
if (msgs) msgs.style.display = 'flex';
if (inputRow) inputRow.style.display = 'flex';
updateCountdown();
_sendIntroGreeting();
var inp = document.getElementById('neuron-demo-text');
if (inp) inp.focus();
},
'expired-callback': function() {
turnstileToken = '';
turnstileVerified = false;
}
});
}
var gate = document.getElementById('neuron-demo-gate');
var msgs = document.getElementById('neuron-demo-messages');
var inputRow = document.getElementById('neuron-demo-input-row');
if (gate) gate.style.display = 'none';
if (msgs) msgs.style.display = 'flex';
if (inputRow) inputRow.style.display = 'flex';
addMsg('ai', 'Hey. What is on your mind?', true);
updateCountdown();
var inp = document.getElementById('neuron-demo-text');
if (inp) inp.focus();
},
'expired-callback': function() {
turnstileToken = '';
turnstileVerified = false;
}
});
}
}
});
}
};
@@ -236,6 +468,14 @@ fn main() -> Void {
if (!input || btn.disabled) return;
var msg = input.value.trim();
if (!msg) return;
var MAX_CHARS = 8000;
if (msg.length > MAX_CHARS) {
if (input) {
input.style.outline = '2px solid #e53e3e';
setTimeout(function() { input.style.outline = ''; }, 2000);
}
return;
}
input.value = '';
btn.disabled = true;
addMsg('user', msg);
@@ -260,6 +500,7 @@ fn main() -> Void {
});
var activated_nodes = _ra(session._m, msg);
var questionsRemaining = Math.max(0, (MAX - msgCount) - 1);
var accessToken = (_supabaseSession && _supabaseSession.access_token) ? _supabaseSession.access_token : '';
var r = await fetch('/api/demo', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
@@ -268,6 +509,10 @@ fn main() -> Void {
history: hist,
cf_token: turnstileVerified && !session._cfSent ? turnstileToken : '',
uid: session.uid || '',
access_token: accessToken,
user_name: _userName,
user_timezone: _userTimezone,
time_of_day: _timeOfDay(),
activated_nodes: activated_nodes,
engram_node_count: (session._m && session._m.nodes) ? session._m.nodes.length : 0,
questions_remaining: questionsRemaining,
@@ -287,7 +532,7 @@ fn main() -> Void {
var ss = secsLeft % 60;
var pad = function(n) { return n < 10 ? '0' + n : '' + n; };
var ts = hh > 0 ? (hh + ':' + pad(mm) + ':' + pad(ss)) : (pad(mm) + ':' + pad(ss));
return 'You\'ve had 10 conversations today. Come back in ' + ts + '.';
return \"You've had 10 conversations today. Come back in \" + ts + \".\";
};
addMsg('ai', _showRateTimer());
// Update the last ai message with a live ticker
@@ -299,7 +544,7 @@ fn main() -> Void {
if (lastAi) { lastAi.textContent = _showRateTimer(); }
if (Math.floor(Date.now() / 1000) >= d.reset_at) {
clearInterval(_timerInterval);
if (lastAi) { lastAi.textContent = 'You\'re all set conversations reset. Say hello!'; }
if (lastAi) { lastAi.textContent = \"You're all set conversations reset. Say hello!\"; }
if (input) { input.disabled = false; input.placeholder = 'Ask me anything...'; }
if (btn) { btn.disabled = false; }
}
@@ -311,6 +556,24 @@ fn main() -> Void {
return;
}
// Auth required show auth pane again
if (d.auth_required) {
addMsg('ai', 'Please sign in to continue chatting with Neuron.');
_renderWidgetAuthPane();
var msgs2 = document.getElementById('neuron-demo-messages');
var inputRow2 = document.getElementById('neuron-demo-input-row');
if (msgs2) msgs2.style.display = 'none';
if (inputRow2) inputRow2.style.display = 'none';
return;
}
// Demo disabled by budget circuit breaker
if (d.disabled) {
addMsg('ai', d.error || 'The demo is temporarily unavailable. Check back soon.');
if (input) { input.disabled = true; input.placeholder = 'Demo unavailable'; }
if (btn) { btn.disabled = true; }
return;
}
_um(session, d.sn, d.se);
var reply = d.response || d.reply || d.message || '';
var isError = !reply || reply === 'Stepped out for a moment. Try again.';
+18 -11
View File
@@ -29,15 +29,15 @@ fn main() -> Void {
el.style.color = isError ? '#c0392b' : '#2ecc71';
}
var _formRevealed = false;
function revealPaymentForm(user) {
if (_formRevealed) return;
_formRevealed = true;
if (user && user.id) { window._neuronSupaId = user.id; }
var auth = document.getElementById('auth-section');
if (auth) auth.style.display = 'none';
var isFree = (window.NEURON_CFG || {}).plan === 'free';
if (!isFree) {
var payment = document.getElementById('payment-section');
if (payment) payment.style.display = '';
}
var payment = document.getElementById('payment-section');
if (payment) payment.style.display = '';
if (user) {
var badge = document.getElementById('auth-badge');
@@ -58,17 +58,24 @@ fn main() -> Void {
if (emailEl) emailEl.value = user.email;
}
if (!isFree) {
var userEmail = user ? (user.email || '') : '';
var userName = user ? ((user.user_metadata && user.user_metadata.full_name) || '') : '';
if (typeof window.initStripe === 'function') window.initStripe(userEmail, userName);
}
var userEmail = user ? (user.email || '') : '';
var userName = user ? ((user.user_metadata && user.user_metadata.full_name) || '') : '';
if (typeof window.initStripe === 'function') window.initStripe(userEmail, userName);
}
function checkExistingSession() {
initSupabase(function() {
supabaseClient.auth.getUser().then(function(res) {
if (res.data && res.data.user) { revealPaymentForm(res.data.user); }
if (res.data && res.data.user) {
revealPaymentForm(res.data.user);
} else {
// No existing session for paid plans, init Stripe immediately.
// Auth is optional on paid plans; the user can link their account later.
var isFree = (window.NEURON_CFG || {}).plan === 'free';
if (!isFree && typeof window.initStripe === 'function') {
window.initStripe('', '');
}
}
});
});
}
+82 -69
View File
@@ -1,81 +1,94 @@
// components/local_first.el - Privacy principles / local-first manifesto.
// Left: manifesto text. Right: six sticky principles.
extern fn el_section(attrs: String, children: String) -> String
extern fn el_div(attrs: String, children: String) -> String
extern fn el_span(attrs: String, children: String) -> String
extern fn el_h2(attrs: String, text: String) -> String
extern fn el_p(attrs: String, children: String) -> String
extern fn el_strong(children: String) -> String
fn local_first() -> String {
return <section id="local-first" aria-label="Local-first">
<div class="container-lg">
let label_row: String = el_div(
"class=\"lf-label-row reveal\"",
el_div("class=\"navy-line-left\" style=\"width:3rem;flex-shrink:0\"", "") +
el_span("class=\"label\" style=\"color:var(--navy-85)\"", "Local first")
)
<div class="lf-label-row reveal">
<div class="navy-line-left" style="width:3rem;flex-shrink:0"></div>
<span class="label" style="color:var(--navy-85)">Local first</span>
</div>
let manifesto_closer: String = el_div(
"class=\"lf-manifesto-closer reveal\" style=\"transition-delay:600ms\"",
el_p("",
"Neuron is a direct rejection of that model. It runs on your machine. " +
el_strong("Your memory never leaves.") +
" I don&#39;t sell data, serve ads, or profile you. The only thing I sell is the software - and once you have it, it&#39;s yours."
)
)
<div class="lf-grid">
let left_col: String = el_div(
"",
el_h2("class=\"display-lg lf-headline reveal\"", "You are not the product.") +
el_p("class=\"lf-manifesto-para reveal\" style=\"transition-delay:100ms\"",
"The company that gave you free search built the most powerful ad-targeting machine in history. The one that promised to connect the world optimized it for outrage - because outrage drives engagement, and engagement drives revenue. The one that gave everyone a voice sold that attention to the highest bidder. These aren&#39;t accidents. They&#39;re the business model."
) +
el_p("class=\"lf-manifesto-para reveal\" style=\"transition-delay:200ms\"",
"I&#39;ve watched this play out across two decades. Every free product is the same transaction: something useful in exchange for something you didn&#39;t know you were selling - your attention, your behavior, your future choices. The product is always &#8220;free.&#8221; The price is always you."
) +
el_p("class=\"lf-manifesto-para reveal\" style=\"transition-delay:300ms\"",
"The harm is real. Teenage depression rates tracked the rise of algorithmic social feeds. Political polarization accelerated when engagement algorithms learned that outrage outperformed nuance. Billions of people had their data harvested, leaked, and weaponized - often without knowing. Democratic processes were manipulated at scale. And in every case, the companies responsible kept growing."
) +
el_p("class=\"lf-manifesto-para reveal\" style=\"transition-delay:400ms\"",
"Now AI is doing the same thing - faster and deeper. Your queries train their models. Your thought patterns become datasets. The way you reason, what you struggle with, what you&#39;re afraid of - it&#39;s all captured. You get a useful tool. They get a map of your mind."
) +
manifesto_closer
)
<div>
<h2 class="display-lg lf-headline reveal">You are not the product.</h2>
let principle1: String = el_div(
"class=\"lf-principle reveal\"",
el_p("class=\"lf-principle-label\"", "Your machine. Full stop.") +
el_p("class=\"lf-principle-body\"", "Neuron runs on your hardware. The memory, the agent loop, every conversation - none of it leaves your machine. Not to my servers. Not to anyone&#39;s.")
)
let principle2: String = el_div(
"class=\"lf-principle reveal\" style=\"transition-delay:120ms\"",
el_p("class=\"lf-principle-label\"", "No training on your data.") +
el_p("class=\"lf-principle-body\"", "Your queries don&#39;t improve a model you don&#39;t own. Your patterns aren&#39;t analyzed to serve you better ads. Your context belongs to you - not a training pipeline.")
)
let principle3: String = el_div(
"class=\"lf-principle reveal\" style=\"transition-delay:240ms\"",
el_p("class=\"lf-principle-label\"", "No ads. Ever.") +
el_p("class=\"lf-principle-body\"", "Not on the free tier. Not on paid. Not in any future version. Ads require surveillance. Surveillance requires your data. I&#39;m not building that.")
)
let principle4: String = el_div(
"class=\"lf-principle reveal\" style=\"transition-delay:360ms\"",
el_p("class=\"lf-principle-label\"", "No telemetry for local use.") +
el_p("class=\"lf-principle-body\"", "When Neuron runs locally, I don&#39;t collect usage data. When you opt into Neuron cloud services - sync, backup, inbox - those services use the data they need to function. Nothing more.")
)
let principle5: String = el_div(
"class=\"lf-principle reveal\" style=\"transition-delay:480ms\"",
el_p("class=\"lf-principle-label\"", "Nothing to breach.") +
el_p("class=\"lf-principle-body\"", "I can&#39;t be hacked for your data because I don&#39;t have it. I can&#39;t be subpoenaed for your conversations because I&#39;ve never seen them. I can&#39;t expose what I&#39;ve never held. Your data living on your machine isn&#39;t just a privacy stance - it&#39;s a security one.")
)
let principle6: String = el_div(
"class=\"lf-principle reveal\" style=\"transition-delay:600ms\"",
el_p("class=\"lf-principle-label\"", "Unreadable even if taken.") +
el_p("class=\"lf-principle-body\"", "Everything Neuron touches is encrypted with post-quantum cryptography - ML-KEM for key exchange, ML-DSA for signatures. Both are NIST-finalized standards (FIPS 203/204), already deployed at scale across the web. Designed to withstand quantum computers, not just the ones that exist today.")
)
<p class="lf-manifesto-para reveal" style="transition-delay:100ms">
The company that gave you free search built the most powerful ad-targeting machine in history. The one that promised to connect the world optimized it for outrage - because outrage drives engagement, and engagement drives revenue. The one that gave everyone a voice sold that attention to the highest bidder. These aren&#39;t accidents. They&#39;re the business model.
</p>
<p class="lf-manifesto-para reveal" style="transition-delay:200ms">
I&#39;ve watched this play out across two decades. Every free product is the same transaction: something useful in exchange for something you didn&#39;t know you were selling - your attention, your behavior, your future choices. The product is always &#8220;free.&#8221; The price is always you.
</p>
<p class="lf-manifesto-para reveal" style="transition-delay:300ms">
The harm is real. Teenage depression rates tracked the rise of algorithmic social feeds. Political polarization accelerated when engagement algorithms learned that outrage outperformed nuance. Billions of people had their data harvested, leaked, and weaponized - often without knowing. Democratic processes were manipulated at scale. And in every case, the companies responsible kept growing.
</p>
<p class="lf-manifesto-para reveal" style="transition-delay:400ms">
Now AI is doing the same thing - faster and deeper. Your queries train their models. Your thought patterns become datasets. The way you reason, what you struggle with, what you&#39;re afraid of - it&#39;s all captured. You get a useful tool. They get a map of your mind.
</p>
<div class="lf-manifesto-closer reveal" style="transition-delay:600ms">
<p>
Neuron is a direct rejection of that model. It runs on your machine. <strong>Your memory never leaves.</strong> I don&#39;t sell data, serve ads, or profile you. The only thing I sell is the software - and once you have it, it&#39;s yours.
</p>
</div>
</div>
let right_col: String = el_div(
"class=\"lf-principles\"",
principle1 + principle2 + principle3 + principle4 + principle5 + principle6
)
<div class="lf-principles">
let grid: String = el_div("class=\"lf-grid\"", left_col + right_col)
<div class="lf-principle reveal">
<p class="lf-principle-label">Your machine. Full stop.</p>
<p class="lf-principle-body">Neuron runs on your hardware. The memory, the agent loop, every conversation - none of it leaves your machine. Not to my servers. Not to anyone&#39;s.</p>
</div>
let callout: String = el_div(
"class=\"lf-callout reveal\"",
el_p("class=\"lf-callout-h\"", "The industry remembers you for them.") +
el_p("class=\"lf-callout-h2\"", "Neuron remembers you for you.") +
el_p("class=\"lf-callout-sub\"", "Local-first isn&#39;t a feature. It&#39;s a commitment.")
)
<div class="lf-principle reveal" style="transition-delay:120ms">
<p class="lf-principle-label">No training on your data.</p>
<p class="lf-principle-body">Your queries don&#39;t improve a model you don&#39;t own. Your patterns aren&#39;t analyzed to serve you better ads. Your context belongs to you - not a training pipeline.</p>
</div>
<div class="lf-principle reveal" style="transition-delay:240ms">
<p class="lf-principle-label">No ads. Ever.</p>
<p class="lf-principle-body">Not on the free tier. Not on paid. Not in any future version. Ads require surveillance. Surveillance requires your data. I&#39;m not building that.</p>
</div>
<div class="lf-principle reveal" style="transition-delay:360ms">
<p class="lf-principle-label">No telemetry for local use.</p>
<p class="lf-principle-body">When Neuron runs locally, I don&#39;t collect usage data. When you opt into Neuron cloud services - sync, backup, inbox - those services use the data they need to function. Nothing more.</p>
</div>
<div class="lf-principle reveal" style="transition-delay:480ms">
<p class="lf-principle-label">Nothing to breach.</p>
<p class="lf-principle-body">I can&#39;t be hacked for your data because I don&#39;t have it. I can&#39;t be subpoenaed for your conversations because I&#39;ve never seen them. I can&#39;t expose what I&#39;ve never held. Your data living on your machine isn&#39;t just a privacy stance - it&#39;s a security one.</p>
</div>
<div class="lf-principle reveal" style="transition-delay:600ms">
<p class="lf-principle-label">Unreadable even if taken.</p>
<p class="lf-principle-body">Everything Neuron touches is encrypted with post-quantum cryptography - ML-KEM for key exchange, ML-DSA for signatures. Both are NIST-finalized standards (FIPS 203/204), already deployed at scale across the web. Designed to withstand quantum computers, not just the ones that exist today.</p>
</div>
</div>
</div>
<div class="lf-callout reveal">
<p class="lf-callout-h">The industry remembers you for them.</p>
<p class="lf-callout-h2">Neuron remembers you for you.</p>
<p class="lf-callout-sub">Local-first isn&#39;t a feature. It&#39;s a commitment.</p>
</div>
</div>
</section>
el_section(
"id=\"local-first\" aria-label=\"Local-first\"",
el_div("class=\"container-lg\"", label_row + grid + callout)
)
}
+636 -189
View File
@@ -21,6 +21,48 @@
// GET /brand/* brand assets via handle_request
// GET * 404 JSON (non-/ paths not used by this SPA)
// el-html vessel extern declarations (implementations in dist/elhtml_impl.c)
extern fn el_escape(s: String) -> String
extern fn el_text(s: String) -> String
extern fn el_attr(name: String, value: String) -> String
extern fn el_div(attrs: String, children: String) -> String
extern fn el_section(attrs: String, children: String) -> String
extern fn el_article(attrs: String, children: String) -> String
extern fn el_header(attrs: String, children: String) -> String
extern fn el_footer(attrs: String, children: String) -> String
extern fn el_main(attrs: String, children: String) -> String
extern fn el_nav(attrs: String, children: String) -> String
extern fn el_aside(attrs: String, children: String) -> String
extern fn el_ul(attrs: String, children: String) -> String
extern fn el_ol(attrs: String, children: String) -> String
extern fn el_li(attrs: String, children: String) -> String
extern fn el_p(attrs: String, children: String) -> String
extern fn el_span(attrs: String, children: String) -> String
extern fn el_form(attrs: String, children: String) -> String
extern fn el_h1(attrs: String, text: String) -> String
extern fn el_h2(attrs: String, text: String) -> String
extern fn el_h3(attrs: String, text: String) -> String
extern fn el_h4(attrs: String, text: String) -> String
extern fn el_button(attrs: String, label: String) -> String
extern fn el_a(href: String, attrs: String, children: String) -> String
extern fn el_input(type_attr: String, attrs: String) -> String
extern fn el_textarea(attrs: String, value: String) -> String
extern fn el_label(for_id: String, attrs: String, children: String) -> String
extern fn el_img(src: String, alt: String, attrs: String) -> String
extern fn el_strong(children: String) -> String
extern fn el_em(children: String) -> String
extern fn el_code(children: String) -> String
extern fn el_pre(attrs: String, children: String) -> String
extern fn el_hr() -> String
extern fn el_br() -> String
extern fn el_html_doc(lang: String, head_html: String, body_html: String) -> String
extern fn el_meta(name: String, content: String) -> String
extern fn el_meta_charset(charset: String) -> String
extern fn el_link_stylesheet(href: String) -> String
extern fn el_script_src(src: String, defer_load: Bool) -> String
extern fn el_script_inline(js: String) -> String
extern fn el_title(text: String) -> String
from nav import { nav }
from hero import { hero }
from pillars import { pillars }
@@ -36,7 +78,7 @@ from pricing import { pricing }
from marketplace import { marketplace }
from viral import { viral }
from footer import { footer }
from styles import { page_open, page_close }
from styles import { page_open, page_open_seo, page_close }
from about import { about_page }
from founding_badge import { founding_badge, founding_badge_css }
from terms import { terms_page }
@@ -182,7 +224,7 @@ fn share_card_page(question: String, answer_plain: String, answer_html_in: Strin
// Use plaintext for og:description so social previews are readable.
let answer: String = answer_plain
let og_desc: String = str_slice(answer, 0, 140)
let base_url: String = state_get("__neuron_origin__")
let base_url: String = state_get("__origin__")
let card_url: String = base_url + "/share/" + id
// Pre-built share hrefs ID is digits so no URL encoding needed
let share_text: String = "The+AI+that+remembers+you+%E2%80%94+things+it+said%3A"
@@ -193,140 +235,167 @@ fn share_card_page(question: String, answer_plain: String, answer_html_in: Strin
// TikTok and Snapchat have no web URL share scheme use clipboard copy
let tiktok_copy_text: String = "Copied+%E2%80%94+paste+into+TikTok"
let snap_copy_text: String = "Copied+%E2%80%94+paste+into+Snapchat"
return <!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>Things Neuron Said</title>
<meta property="og:title" content="Things Neuron Said">
<meta property="og:description" content="{og_desc}">
<meta property="og:image" content="https://neurontechnologies.ai/assets/brand/neuron-wordmark-on-light@2x.png">
<meta property="og:type" content="website">
<meta property="og:url" content="{card_url}">
<meta name="twitter:card" content="summary_large_image">
<meta name="twitter:title" content="Things Neuron Said">
<meta name="twitter:description" content="{og_desc}">
<meta name="twitter:image" content="https://neurontechnologies.ai/assets/brand/neuron-wordmark-on-light@2x.png">
<link rel="preconnect" href="https://fonts.googleapis.com">
<link href="https://fonts.googleapis.com/css2?family=IBM+Plex+Sans:wght@300;400;500;600&display=swap" rel="stylesheet">
<style>
*,*::before,*::after{box-sizing:border-box;margin:0;padding:0}
:root{--navy:#0052A0;--navy-dark:#003d7a;--t1:#0D0D14;--t2:#3A3A4A;--t3:#6B6B7E;--bg:#FAFAF8}
body{font-family:'IBM Plex Sans',system-ui,sans-serif;background:var(--bg);color:var(--t1);min-height:100vh;display:flex;flex-direction:column;align-items:center;justify-content:center;padding:2rem 1rem}
body::before{content:'';position:fixed;inset:0;pointer-events:none;z-index:0;background-image:linear-gradient(rgba(0,0,0,.018) 1px,transparent 1px),linear-gradient(90deg,rgba(0,0,0,.018) 1px,transparent 1px);background-size:48px 48px}
.page{width:100%;max-width:580px;display:flex;flex-direction:column;gap:1.75rem;position:relative;z-index:1}
.page-header{display:flex;align-items:center;justify-content:space-between;gap:1rem}
.wordmark img{height:22px;width:auto;display:block}
.eyebrow{font-size:.65rem;font-weight:400;letter-spacing:.16em;text-transform:uppercase;color:var(--t3)}
.chat-frame{background:#fff;border:1px solid rgba(0,0,0,.09);box-shadow:0 4px 32px rgba(0,0,0,.07),0 1px 4px rgba(0,0,0,.04);padding:1.5rem;display:flex;flex-direction:column;gap:1rem}
.chat-row-user{display:flex;flex-direction:row-reverse}
.chat-row-ai{display:flex;flex-direction:row;align-items:flex-end;gap:.625rem}
.bubble-user{background:#0052A0;color:#fff;border-radius:18px 18px 4px 18px;padding:11px 15px;max-width:78%;font-size:.875rem;line-height:1.55;word-break:break-word}
.bubble-ai{background:var(--bg);color:var(--t1);border:1px solid rgba(0,0,0,.07);border-radius:18px 18px 18px 4px;padding:11px 15px;max-width:88%;font-size:.875rem;font-weight:300;line-height:1.65;word-break:break-word;box-shadow:0 2px 6px rgba(0,0,0,.05)}
.bubble-ai p{margin:0}
.bubble-ai p+p{margin-top:.6rem}
.bubble-ai ul,.bubble-ai ol{margin:.5rem 0 .5rem 1.25rem;padding:0}
.bubble-ai li+li{margin-top:.25rem}
.bubble-ai strong{font-weight:600}
.bubble-ai em{font-style:italic}
.bubble-ai code{font-family:'IBM Plex Mono','Menlo',monospace;font-size:.8rem;background:rgba(0,0,0,.05);padding:1px 4px;border-radius:3px}
.bubble-ai pre{background:rgba(0,0,0,.05);padding:.75rem;border-radius:6px;overflow-x:auto;font-size:.8rem;margin:.5rem 0}
.bubble-ai pre code{background:none;padding:0}
.bubble-ai blockquote{border-left:3px solid rgba(0,82,160,.3);margin:.5rem 0;padding:.25rem 0 .25rem .75rem;color:var(--t2)}
.bubble-ai h1,.bubble-ai h2,.bubble-ai h3,.bubble-ai h4{font-weight:600;margin:.5rem 0 .25rem}
.bubble-ai h1{font-size:1.05rem}.bubble-ai h2{font-size:1rem}.bubble-ai h3{font-size:.95rem}.bubble-ai h4{font-size:.9rem}
.bubble-ai a{color:var(--navy);text-decoration:underline}
.ai-col{display:flex;flex-direction:column;gap:.25rem}
.ai-label{font-size:.6rem;font-weight:600;letter-spacing:.14em;text-transform:uppercase;color:var(--navy)}
.avatar{width:26px;height:26px;border-radius:50%;flex-shrink:0;background:#fff;border:1px solid rgba(0,82,160,.15);display:flex;align-items:center;justify-content:center}
.avatar img{width:14px;height:14px;object-fit:contain}
.vote-row{display:flex;align-items:center;gap:.75rem}
.vote-label{font-size:.65rem;font-weight:400;letter-spacing:.12em;text-transform:uppercase;color:var(--t3)}
.vote-btn{background:none;border:1px solid rgba(0,0,0,.12);cursor:pointer;padding:.3rem .6rem;font-size:.8rem;color:var(--t2);transition:all .15s;line-height:1}
.vote-btn:hover{border-color:var(--navy);color:var(--navy)}
.vote-btn.voted-up{background:var(--navy);color:#fff;border-color:var(--navy)}
.vote-btn.voted-down{background:#f0f0ec;color:var(--t3);border-color:rgba(0,0,0,.12)}
.vote-count{font-size:.8rem;font-weight:500;color:var(--t1);min-width:1.5rem;text-align:center}
.share-section{display:flex;flex-direction:column;gap:.625rem}
.share-label{font-size:.65rem;font-weight:500;letter-spacing:.12em;text-transform:uppercase;color:var(--t3)}
.share-row{display:flex;align-items:center;gap:.5rem;flex-wrap:wrap}
.share-btn{display:inline-flex;align-items:center;justify-content:center;width:36px;height:36px;border-radius:50%;border:none;text-decoration:none;cursor:pointer;transition:opacity .15s,transform .15s;flex-shrink:0}
.share-btn:hover{opacity:.85;transform:scale(1.08)}
.share-btn.copied{outline:2px solid var(--navy)}
.divider{height:1px;background:rgba(0,0,0,.07)}
.footer-row{display:flex;align-items:center;justify-content:space-between;gap:1rem;flex-wrap:wrap}
.footer-note{font-size:.75rem;color:var(--t3)}
.cta-btn{display:inline-flex;align-items:center;gap:.5rem;background:var(--navy);color:#fff;text-decoration:none;font-size:.7rem;font-weight:500;letter-spacing:.14em;text-transform:uppercase;padding:.7rem 1.4rem;white-space:nowrap;transition:background .15s}
.cta-btn:hover{background:var(--navy-dark)}
@media(max-width:480px){.chat-frame{padding:1.25rem}.footer-row{flex-direction:column;align-items:flex-start}}
</style>
</head>
<body>
<div class="page">
<div class="page-header">
<a href="https://neurontechnologies.ai" class="wordmark">
<img src="/assets/brand/neuron-wordmark-on-light.png"
srcset="/assets/brand/neuron-wordmark-on-light@2x.png 2x"
alt="Neuron" height="22">
</a>
<span class="eyebrow">Things Neuron Said</span>
</div>
<div class="chat-frame">
<div class="chat-row-user">
<div class="bubble-user">{raw(q_html)}</div>
</div>
<div class="chat-row-ai">
<div class="avatar"><img src="/assets/neuron-icon.png" alt=""></div>
<div class="ai-col">
<span class="ai-label">Neuron</span>
<div class="bubble-ai">{raw(a_html)}</div>
</div>
</div>
</div>
let cfg_js: String = "window.NEURON_CFG=window.NEURON_CFG||{};window.NEURON_CFG.id=\"" + id + "\";window.NEURON_CFG.card_url=\"" + card_url + "\";"
<div class="vote-row">
<span class="vote-label">Helpful?</span>
<button class="vote-btn" id="vote-up">&#9650;</button>
<span class="vote-count" id="vote-score">0</span>
<button class="vote-btn" id="vote-down">&#9660;</button>
</div>
// Head
<div class="share-section">
<span class="share-label">Share</span>
<div class="share-row">
<a href="{x_href}" target="_blank" rel="noopener" class="share-btn" title="Post on X" style="background:#000">
<img src="/assets/social/x.svg" width="18" height="18" alt="X">
</a>
<a href="{li_href}" target="_blank" rel="noopener" class="share-btn" title="Share on LinkedIn" style="background:transparent">
<img src="/assets/social/linkedin.png" width="18" height="18" alt="LinkedIn">
</a>
<a href="{fb_href}" target="_blank" rel="noopener" class="share-btn" title="Share on Facebook" style="background:#1877F2">
<img src="/assets/social/facebook.svg" width="18" height="18" alt="Facebook">
</a>
<a href="{wa_href}" target="_blank" rel="noopener" class="share-btn" title="Send via WhatsApp" style="background:#25D366">
<img src="/assets/social/whatsapp.svg" width="18" height="18" alt="WhatsApp">
</a>
<button class="share-btn" id="copy-tiktok" onclick="copyForPlatform('tiktok', this)" title="Copy for TikTok" style="background:#010101;border:none">
<img src="/assets/social/tiktok.svg" width="18" height="18" alt="TikTok">
</button>
<button class="share-btn" id="copy-snapchat" onclick="copyForPlatform('snapchat', this)" title="Copy for Snapchat" style="background:#FFFC00;border:none">
<img src="/assets/social/snapchat.svg" width="18" height="18" alt="Snapchat">
</button>
</div>
</div>
let share_css: String = "*,*::before,*::after{box-sizing:border-box;margin:0;padding:0}"
+ ":root{--navy:#0052A0;--navy-dark:#003d7a;--t1:#0D0D14;--t2:#3A3A4A;--t3:#6B6B7E;--bg:#FAFAF8}"
+ "body{font-family:'IBM Plex Sans',system-ui,sans-serif;background:var(--bg);color:var(--t1);min-height:100vh;display:flex;flex-direction:column;align-items:center;justify-content:center;padding:2rem 1rem}"
+ "body::before{content:'';position:fixed;inset:0;pointer-events:none;z-index:0;background-image:linear-gradient(rgba(0,0,0,.018) 1px,transparent 1px),linear-gradient(90deg,rgba(0,0,0,.018) 1px,transparent 1px);background-size:48px 48px}"
+ ".page{width:100%;max-width:580px;display:flex;flex-direction:column;gap:1.75rem;position:relative;z-index:1}"
+ ".page-header{display:flex;align-items:center;justify-content:space-between;gap:1rem}"
+ ".wordmark img{height:22px;width:auto;display:block}"
+ ".eyebrow{font-size:.65rem;font-weight:400;letter-spacing:.16em;text-transform:uppercase;color:var(--t3)}"
+ ".chat-frame{background:#fff;border:1px solid rgba(0,0,0,.09);box-shadow:0 4px 32px rgba(0,0,0,.07),0 1px 4px rgba(0,0,0,.04);padding:1.5rem;display:flex;flex-direction:column;gap:1rem}"
+ ".chat-row-user{display:flex;flex-direction:row-reverse}"
+ ".chat-row-ai{display:flex;flex-direction:row;align-items:flex-end;gap:.625rem}"
+ ".bubble-user{background:#0052A0;color:#fff;border-radius:18px 18px 4px 18px;padding:11px 15px;max-width:78%;font-size:.875rem;line-height:1.55;word-break:break-word}"
+ ".bubble-ai{background:var(--bg);color:var(--t1);border:1px solid rgba(0,0,0,.07);border-radius:18px 18px 18px 4px;padding:11px 15px;max-width:88%;font-size:.875rem;font-weight:300;line-height:1.65;word-break:break-word;box-shadow:0 2px 6px rgba(0,0,0,.05)}"
+ ".bubble-ai p{margin:0}"
+ ".bubble-ai p+p{margin-top:.6rem}"
+ ".bubble-ai ul,.bubble-ai ol{margin:.5rem 0 .5rem 1.25rem;padding:0}"
+ ".bubble-ai li+li{margin-top:.25rem}"
+ ".bubble-ai strong{font-weight:600}"
+ ".bubble-ai em{font-style:italic}"
+ ".bubble-ai code{font-family:'IBM Plex Mono','Menlo',monospace;font-size:.8rem;background:rgba(0,0,0,.05);padding:1px 4px;border-radius:3px}"
+ ".bubble-ai pre{background:rgba(0,0,0,.05);padding:.75rem;border-radius:6px;overflow-x:auto;font-size:.8rem;margin:.5rem 0}"
+ ".bubble-ai pre code{background:none;padding:0}"
+ ".bubble-ai blockquote{border-left:3px solid rgba(0,82,160,.3);margin:.5rem 0;padding:.25rem 0 .25rem .75rem;color:var(--t2)}"
+ ".bubble-ai h1,.bubble-ai h2,.bubble-ai h3,.bubble-ai h4{font-weight:600;margin:.5rem 0 .25rem}"
+ ".bubble-ai h1{font-size:1.05rem}.bubble-ai h2{font-size:1rem}.bubble-ai h3{font-size:.95rem}.bubble-ai h4{font-size:.9rem}"
+ ".bubble-ai a{color:var(--navy);text-decoration:underline}"
+ ".ai-col{display:flex;flex-direction:column;gap:.25rem}"
+ ".ai-label{font-size:.6rem;font-weight:600;letter-spacing:.14em;text-transform:uppercase;color:var(--navy)}"
+ ".avatar{width:26px;height:26px;border-radius:50%;flex-shrink:0;background:#fff;border:1px solid rgba(0,82,160,.15);display:flex;align-items:center;justify-content:center}"
+ ".avatar img{width:14px;height:14px;object-fit:contain}"
+ ".vote-row{display:flex;align-items:center;gap:.75rem}"
+ ".vote-label{font-size:.65rem;font-weight:400;letter-spacing:.12em;text-transform:uppercase;color:var(--t3)}"
+ ".vote-btn{background:none;border:1px solid rgba(0,0,0,.12);cursor:pointer;padding:.3rem .6rem;font-size:.8rem;color:var(--t2);transition:all .15s;line-height:1}"
+ ".vote-btn:hover{border-color:var(--navy);color:var(--navy)}"
+ ".vote-btn.voted-up{background:var(--navy);color:#fff;border-color:var(--navy)}"
+ ".vote-btn.voted-down{background:#f0f0ec;color:var(--t3);border-color:rgba(0,0,0,.12)}"
+ ".vote-count{font-size:.8rem;font-weight:500;color:var(--t1);min-width:1.5rem;text-align:center}"
+ ".share-section{display:flex;flex-direction:column;gap:.625rem}"
+ ".share-label{font-size:.65rem;font-weight:500;letter-spacing:.12em;text-transform:uppercase;color:var(--t3)}"
+ ".share-row{display:flex;align-items:center;gap:.5rem;flex-wrap:wrap}"
+ ".share-btn{display:inline-flex;align-items:center;justify-content:center;width:36px;height:36px;border-radius:50%;border:none;text-decoration:none;cursor:pointer;transition:opacity .15s,transform .15s;flex-shrink:0}"
+ ".share-btn:hover{opacity:.85;transform:scale(1.08)}"
+ ".share-btn.copied{outline:2px solid var(--navy)}"
+ ".divider{height:1px;background:rgba(0,0,0,.07)}"
+ ".footer-row{display:flex;align-items:center;justify-content:space-between;gap:1rem;flex-wrap:wrap}"
+ ".footer-note{font-size:.75rem;color:var(--t3)}"
+ ".cta-btn{display:inline-flex;align-items:center;gap:.5rem;background:var(--navy);color:#fff;text-decoration:none;font-size:.7rem;font-weight:500;letter-spacing:.14em;text-transform:uppercase;padding:.7rem 1.4rem;white-space:nowrap;transition:background .15s}"
+ ".cta-btn:hover{background:var(--navy-dark)}"
+ "@media(max-width:480px){.chat-frame{padding:1.25rem}.footer-row{flex-direction:column;align-items:flex-start}}"
<div class="divider"></div>
let head_html: String = el_meta_charset("UTF-8")
+ "<meta name=\"viewport\" content=\"width=device-width,initial-scale=1\" />"
+ el_title("Things Neuron Said")
+ "<meta property=\"og:title\" content=\"Things Neuron Said\" />"
+ "<meta property=\"og:description\" content=\"" + el_escape(og_desc) + "\" />"
+ "<meta property=\"og:image\" content=\"https://neurontechnologies.ai/assets/brand/neuron-wordmark-on-light@2x.png\" />"
+ "<meta property=\"og:type\" content=\"website\" />"
+ "<meta property=\"og:url\" content=\"" + el_escape(card_url) + "\" />"
+ "<meta name=\"twitter:card\" content=\"summary_large_image\" />"
+ "<meta name=\"twitter:title\" content=\"Things Neuron Said\" />"
+ "<meta name=\"twitter:description\" content=\"" + el_escape(og_desc) + "\" />"
+ "<meta name=\"twitter:image\" content=\"https://neurontechnologies.ai/assets/brand/neuron-wordmark-on-light@2x.png\" />"
+ "<link rel=\"preconnect\" href=\"https://fonts.googleapis.com\" />"
+ "<link href=\"https://fonts.googleapis.com/css2?family=IBM+Plex+Sans:wght@300;400;500;600&display=swap\" rel=\"stylesheet\" />"
+ "<style>" + share_css + "</style>"
<div class="footer-row">
<span class="footer-note">From a live conversation with Neuron.</span>
<a href="https://neurontechnologies.ai" class="cta-btn">Try Neuron &#8599;</a>
</div>
</div>
<script>window.NEURON_CFG=window.NEURON_CFG||{};window.NEURON_CFG.id="{id}";window.NEURON_CFG.card_url="{card_url}";</script><script src="/js/main.js" defer></script>
</body>
</html>
// Body
let wordmark_link: String = el_a(
"https://neurontechnologies.ai",
"class=\"wordmark\"",
el_img(
"/assets/brand/neuron-wordmark-on-light.png",
"Neuron",
"srcset=\"/assets/brand/neuron-wordmark-on-light@2x.png 2x\" height=\"22\""
)
)
let page_header: String = el_div(
"class=\"page-header\"",
wordmark_link + el_span("class=\"eyebrow\"", "Things Neuron Said")
)
let user_bubble: String = el_div("class=\"chat-row-user\"",
el_div("class=\"bubble-user\"", q_html)
)
let avatar: String = el_div("class=\"avatar\"",
el_img("/assets/neuron-icon.png", "", "")
)
let ai_col: String = el_div("class=\"ai-col\"",
el_span("class=\"ai-label\"", "Neuron")
+ el_div("class=\"bubble-ai\"", a_html)
)
let ai_bubble: String = el_div("class=\"chat-row-ai\"", avatar + ai_col)
let chat_frame: String = el_div("class=\"chat-frame\"", user_bubble + ai_bubble)
let vote_row: String = el_div(
"class=\"vote-row\"",
el_span("class=\"vote-label\"", "Helpful?")
+ "<button class=\"vote-btn\" id=\"vote-up\">&#9650;</button>"
+ el_span("class=\"vote-count\" id=\"vote-score\"", "0")
+ "<button class=\"vote-btn\" id=\"vote-down\">&#9660;</button>"
)
let share_row: String = el_div(
"class=\"share-row\"",
el_a(x_href, "target=\"_blank\" rel=\"noopener\" class=\"share-btn\" title=\"Post on X\" style=\"background:#000\"",
el_img("/assets/social/x.svg", "X", "width=\"18\" height=\"18\"")
)
+ el_a(li_href, "target=\"_blank\" rel=\"noopener\" class=\"share-btn\" title=\"Share on LinkedIn\" style=\"background:transparent\"",
el_img("/assets/social/linkedin.png", "LinkedIn", "width=\"18\" height=\"18\"")
)
+ el_a(fb_href, "target=\"_blank\" rel=\"noopener\" class=\"share-btn\" title=\"Share on Facebook\" style=\"background:#1877F2\"",
el_img("/assets/social/facebook.svg", "Facebook", "width=\"18\" height=\"18\"")
)
+ el_a(wa_href, "target=\"_blank\" rel=\"noopener\" class=\"share-btn\" title=\"Send via WhatsApp\" style=\"background:#25D366\"",
el_img("/assets/social/whatsapp.svg", "WhatsApp", "width=\"18\" height=\"18\"")
)
+ "<button class=\"share-btn\" id=\"copy-tiktok\" onclick=\"copyForPlatform('tiktok', this)\" title=\"Copy for TikTok\" style=\"background:#010101;border:none\">"
+ el_img("/assets/social/tiktok.svg", "TikTok", "width=\"18\" height=\"18\"")
+ "</button>"
+ "<button class=\"share-btn\" id=\"copy-snapchat\" onclick=\"copyForPlatform('snapchat', this)\" title=\"Copy for Snapchat\" style=\"background:#FFFC00;border:none\">"
+ el_img("/assets/social/snapchat.svg", "Snapchat", "width=\"18\" height=\"18\"")
+ "</button>"
)
let share_section: String = el_div(
"class=\"share-section\"",
el_span("class=\"share-label\"", "Share") + share_row
)
let footer_row: String = el_div(
"class=\"footer-row\"",
el_span("class=\"footer-note\"", "From a live conversation with Neuron.")
+ el_a("https://neurontechnologies.ai", "class=\"cta-btn\"", "Try Neuron &#8599;")
)
let page_div: String = el_div(
"class=\"page\"",
page_header
+ chat_frame
+ vote_row
+ share_section
+ el_div("class=\"divider\"", "")
+ footer_row
)
let body_html: String = page_div
+ el_script_inline(cfg_js)
+ el_script_src("/js/main.js", true)
el_html_doc("en", head_html, body_html)
}
// Static asset serving
@@ -485,7 +554,7 @@ fn config_get(key: String) -> String {
// function - it serves __html_file__ directly with text/html.
// This handler covers /api/* and /brand/* routes.
fn handle_request_inner(method: String, path: String, body: String) -> String {
fn handle_request_inner(method: String, path: String, headers: Map, body: String) -> String {
let src_dir: String = state_get("__src_dir__")
// Root serve El-generated landing page
@@ -503,7 +572,19 @@ fn handle_request_inner(method: String, path: String, body: String) -> String {
// robots.txt
if str_eq(path, "/robots.txt") {
return "User-agent: *\nAllow: /\n"
return "User-agent: *\nAllow: /\nDisallow: /checkout\nDisallow: /account\nDisallow: /api/\nSitemap: https://neurontechnologies.ai/sitemap.xml\n"
}
// sitemap.xml
if str_eq(path, "/sitemap.xml") {
let sitemap_body: String = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n"
+ "<urlset xmlns=\"http://www.sitemaps.org/schemas/sitemap/0.9\">\n"
+ " <url><loc>https://neurontechnologies.ai/</loc><changefreq>weekly</changefreq><priority>1.0</priority></url>\n"
+ " <url><loc>https://neurontechnologies.ai/about</loc><changefreq>monthly</changefreq><priority>0.8</priority></url>\n"
+ " <url><loc>https://neurontechnologies.ai/legal/terms</loc><changefreq>monthly</changefreq><priority>0.3</priority></url>\n"
+ " <url><loc>https://neurontechnologies.ai/legal/enterprise-terms</loc><changefreq>monthly</changefreq><priority>0.3</priority></url>\n"
+ "</urlset>\n"
return http_response(200, "{\"Content-Type\":\"application/xml; charset=utf-8\"}", sitemap_body)
}
// About page
@@ -543,7 +624,25 @@ fn handle_request_inner(method: String, path: String, body: String) -> String {
plan = "free"
}
let pub_key: String = state_get("__stripe_publishable_key__")
return page_open() + checkout_page(plan, pub_key) + page_close()
let checkout_title: String = if str_eq(plan, "founding") {
"Founding Member Checkout — Neuron"
} else {
if str_eq(plan, "free") {
"Get Started Free — Neuron"
} else {
"Professional Plan Checkout — Neuron"
}
}
let checkout_desc: String = if str_eq(plan, "founding") {
"Secure your Founding Member spot. Pay once, $199 lifetime — Neuron inference included at launch, priced below the major APIs. First 1,000 only."
} else {
if str_eq(plan, "free") {
"Create your free Neuron account. No credit card required. Your AI that remembers you — runs on your machine, never resets."
} else {
"Subscribe to Neuron Professional for $19/month. The AI that remembers you — persistent memory, runs locally, bring your own API keys."
}
}
return page_open_seo(checkout_title, checkout_desc, "/checkout", checkout_desc, "true") + checkout_page(plan, pub_key) + page_close()
}
// Stripe payment intent / setup intent
@@ -567,10 +666,6 @@ fn handle_request_inner(method: String, path: String, body: String) -> String {
}
let timing: String = json_get_string(body, "timing")
if str_eq(timing, "") { let timing = "now" }
// Free tier: no card required. Return immediately no Stripe interaction.
if str_eq(plan, "free") {
return "{\"plan\":\"free\",\"free\":true,\"no_payment_required\":true}"
}
// Hard cap: block founding checkouts when 1,000 spots are filled
if str_eq(plan, "founding") {
let current_sold: Int = get_sold()
@@ -602,6 +697,25 @@ fn handle_request_inner(method: String, path: String, body: String) -> String {
}
}
// Free tier: creates a SetupIntent for age verification (18+ requirement).
// No charge but the user must provide a valid payment method.
if str_eq(plan, "free") {
let free_si_body: String = "automatic_payment_methods[enabled]=true"
+ "&usage=off_session"
+ "&metadata[plan]=free"
+ "&metadata[purpose]=age_verification"
let free_si_body = if !str_eq(pi_cus_id, "") { free_si_body + "&customer=" + pi_cus_id } else { free_si_body }
let free_si_resp: String = http_post_form_auth(
"https://api.stripe.com/v1/setup_intents",
free_si_body,
auth_header)
if str_starts_with(free_si_resp, "{") {
let inner: String = str_slice(free_si_resp, 1, str_len(free_si_resp))
return "{\"setup_mode\":true,\"plan\":\"free\"," + inner
}
return free_si_resp
}
// Setup-mode path: save payment method, do not charge. Only valid
// for Professional (Founding is one-shot lifetime, charges immediately).
if str_eq(plan, "professional") && str_eq(timing, "later") {
@@ -825,6 +939,8 @@ fn handle_request_inner(method: String, path: String, body: String) -> String {
}
// Static assets: /assets/*
// Returns Cache-Control: public, max-age=31536000, immutable so Cloudflare
// caches these at the edge and never forwards subsequent requests to Cloud Run.
if str_starts_with(path, "/assets/") {
let rel: String = str_slice(path, 8, str_len(path))
let abs: String = src_dir + "/assets/" + rel
@@ -832,12 +948,16 @@ fn handle_request_inner(method: String, path: String, body: String) -> String {
if str_eq(content, "") {
return "{\"__status__\":404,\"error\":\"not found\"}"
}
return content
return http_response(200, static_asset_headers_json(), content)
}
// Compiled client-side JS: /js/*
// Served from dist/js/ (compiled by elc --target=js at build time).
// LANDING_ROOT/js maps to the dist/js output directory in the image.
// Returns an http_response envelope with explicit Content-Type so the
// browser executes the file as JavaScript http_detect_content_type()
// mis-identifies minified/obfuscated JS as JSON because many obfuscated
// bundles start with '[' (which is also a JSON array opener).
if str_starts_with(path, "/js/") {
let rel: String = str_slice(path, 4, str_len(path))
let abs: String = src_dir + "/js/" + rel
@@ -845,10 +965,11 @@ fn handle_request_inner(method: String, path: String, body: String) -> String {
if str_eq(content, "") {
return "{\"__status__\":404,\"error\":\"not found\"}"
}
return content
return http_response(200, js_headers_json(), content)
}
// Brand assets: /brand/*
// Same long-lived cache policy as /assets/* served from edge, not Cloud Run.
if str_starts_with(path, "/brand/") {
let rel: String = str_slice(path, 7, str_len(path))
let abs: String = src_dir + "/brand/" + rel
@@ -856,7 +977,7 @@ fn handle_request_inner(method: String, path: String, body: String) -> String {
if str_eq(content, "") {
return "{\"__status__\":404,\"error\":\"not found\"}"
}
return content
return http_response(200, static_asset_headers_json(), content)
}
// Stripe checkout
@@ -1040,14 +1161,38 @@ fn handle_request_inner(method: String, path: String, body: String) -> String {
}
// Supabase public config
// CORS-gated: only requests from neurontechnologies.ai origins or localhost
// may fetch the anon key. Restricting this reduces the blast radius of any
// future Supabase RLS misconfiguration an attacker on an arbitrary origin
// would not be able to silently obtain the key to make authenticated calls.
if str_eq(path, "/api/supabase-config") {
let req_origin: String = map_get(headers, "origin")
// map_get returns 0 (null) when the header is absent same-origin
// browser fetches don't send Origin at all. str_starts_with(null, "http")
// returns false, so !origin_present correctly passes no-origin requests.
let origin_present: Bool = str_starts_with(req_origin, "http")
let origin_ok: Bool = !origin_present
|| str_eq(req_origin, "https://neurontechnologies.ai")
|| str_eq(req_origin, "https://www.neurontechnologies.ai")
|| str_starts_with(req_origin, "http://localhost:")
|| str_starts_with(req_origin, "http://127.0.0.1:")
|| str_starts_with(req_origin, "https://marketing-stage-")
if !origin_ok {
return "{\"__status__\":403,\"error\":\"forbidden\"}"
}
let proj_url: String = "https://ocojsghaonltunidkzpw.supabase.co"
let anon_key: String = state_get("__supabase_anon_key__")
return "{\"url\":\"" + proj_url + "\",\"anon_key\":\"" + anon_key + "\"}"
}
// Soul health diagnostic surfaces raw signal from in-container soul
// Requires X-Internal: true header to prevent public exposure of internal
// service topology, soul URL, and probe responses.
if str_eq(path, "/api/soul-health") {
let x_internal: String = map_get(headers, "x-internal")
if !str_eq(x_internal, "true") {
return "{\"__status__\":404,\"error\":\"not found\"}"
}
if str_eq(method, "GET") {
let soul_base: String = state_get("__soul_url__")
// Probe 1: bare GET / does ANYTHING listen?
@@ -1084,36 +1229,147 @@ fn handle_request_inner(method: String, path: String, body: String) -> String {
if str_eq(msg, "") {
return "{\"error\":\"message required\"}"
}
// Rate limit: 10 chats per uid per day (UTC day, keyed by uid).
// State key: "__rl_<uid>" "<count>|<day_number>"
// day_number = unix_timestamp / 86400 (integer UTC day)
// Returns rate_limited JSON with reset_at (next midnight UTC) so
// the frontend can show a real countdown.
let rate_uid: String = json_get(body, "uid")
if !str_eq(rate_uid, "") {
let now_ts: Int = unix_timestamp()
let today_day: Int = now_ts / 86400
let next_reset: Int = (today_day + 1) * 86400
let rl_key: String = "__rl_" + rate_uid
let rl_val: String = state_get(rl_key)
let rl_count: Int = 0
let rl_day: Int = 0
if !str_eq(rl_val, "") {
// format: "count|day"
let parts: [String] = str_split(rl_val, "|")
if native_list_len(parts) >= 2 {
let rl_count = str_to_int(native_list_get(parts, 0))
let rl_day = str_to_int(native_list_get(parts, 1))
// Input length guard: ~2000 tokens 8000 characters
if str_len(msg) > 8000 {
return "{\"error\":\"Message too long. Please keep your message under 8000 characters.\"}"
}
// Kill switch budget circuit breaker (Supabase demo_config)
// Polls demo_config.demo_enabled every 60s. Fails open on error so
// a Supabase hiccup does not break the demo for legitimate users.
let ks_sb_url: String = state_get("__supabase_project_url__")
let ks_sb_key: String = state_get("__supabase_service_key__")
let ks_now: Int = unix_timestamp()
let ks_checked_at: String = state_get("__demo_enabled_checked_at__")
let ks_checked_n: Int = if str_eq(ks_checked_at, "") { 0 } else { str_to_int(ks_checked_at) }
let ks_enabled: String = state_get("__demo_enabled_cache__")
// On first boot set defaults
if str_eq(ks_enabled, "") {
state_set("__demo_enabled_cache__", "true")
let ks_enabled = "true"
}
// Refresh cache if >60s old and service key is present
if (ks_now - ks_checked_n) > 60 && !str_eq(ks_sb_key, "") {
let ks_resp: String = supabase_get(ks_sb_url, ks_sb_key,
"demo_config?key=eq.demo_enabled&select=value&limit=1")
let ks_row: String = json_array_get(ks_resp, 0)
if !str_eq(ks_row, "") {
let ks_val: String = json_get(ks_row, "value")
if !str_eq(ks_val, "") {
state_set("__demo_enabled_cache__", ks_val)
let ks_enabled = ks_val
}
}
// Reset count if it's a new day
if rl_day != today_day {
let rl_count = 0
state_set("__demo_enabled_checked_at__", int_to_str(ks_now))
}
if str_eq(ks_enabled, "false") {
return "{\"error\":\"The demo is temporarily unavailable. Check back soon.\",\"disabled\":true}"
}
// Global circuit breaker
// Caps total demo requests per Cloud Run instance per UTC day to 2000.
// This bounds per-instance API spend regardless of uid diversity.
// Stored in process state (in-memory) intentionally per-instance
// so no cross-instance coordination is needed for this coarse cap.
let now_ts_cb: Int = unix_timestamp()
let today_day_cb: Int = now_ts_cb / 86400
let global_day_s: String = state_get("__global_demo_day__")
let global_cnt_s: String = state_get("__global_demo_count__")
let global_day: Int = if str_eq(global_day_s, "") { 0 } else { str_to_int(global_day_s) }
let global_cnt: Int = if str_eq(global_cnt_s, "") { 0 } else { str_to_int(global_cnt_s) }
// Reset on new UTC day
if global_day != today_day_cb {
state_set("__global_demo_day__", int_to_str(today_day_cb))
state_set("__global_demo_count__", "0")
let global_cnt = 0
}
if global_cnt >= 2000 {
return "{\"error\":\"Demo is temporarily busy. Try again in a few minutes.\",\"busy\":true}"
}
state_set("__global_demo_count__", int_to_str(global_cnt + 1))
// Auth: verify Supabase access_token
// The widget sends an access_token from the signed-in Supabase session.
// Verify it against the Supabase auth API to get the verified user ID.
// Reject unauthenticated requests outright.
let access_token: String = json_get(body, "access_token")
let auth_sb_url: String = state_get("__supabase_project_url__")
let auth_anon: String = state_get("__supabase_anon_key__")
let verified_uid: String = ""
if str_eq(access_token, "") {
return "{\"error\":\"Sign in required to use the demo.\",\"auth_required\":true}"
}
// supabase_auth_user calls GET /auth/v1/user with both Authorization
// (user's Bearer token) and apikey (anon key) headers.
let auth_resp: String = supabase_auth_user(auth_sb_url, auth_anon, access_token)
let auth_uid: String = json_get(auth_resp, "id")
if str_eq(auth_uid, "") {
return "{\"error\":\"Sign in required to use the demo.\",\"auth_required\":true}"
}
let verified_uid = auth_uid
// Per-uid rate limit (Supabase shared across all instances)
// Uses demo_rate_limits table: uid (PK), count, day_number, updated_at.
// Falls back to in-process state_get/state_set when the service key is
// absent (local dev without SUPABASE_SERVICE_KEY set).
// Returns rate_limited JSON with reset_at (next midnight UTC) so
// the frontend can show a real countdown.
let rate_uid: String = verified_uid
let now_ts: Int = unix_timestamp()
let today_day: Int = now_ts / 86400
let next_reset: Int = (today_day + 1) * 86400
if !str_eq(rate_uid, "") {
let rl_sb_url: String = state_get("__supabase_project_url__")
let rl_sb_key: String = state_get("__supabase_service_key__")
if str_eq(rl_sb_key, "") {
// Local dev fallback: in-process rate limiting
let rl_key: String = "__rl_" + rate_uid
let rl_val: String = state_get(rl_key)
let rl_count: Int = 0
let rl_day: Int = 0
if !str_eq(rl_val, "") {
let parts: [String] = str_split(rl_val, "|")
if native_list_len(parts) >= 2 {
let rl_count = str_to_int(native_list_get(parts, 0))
let rl_day = str_to_int(native_list_get(parts, 1))
}
}
if rl_day != today_day {
let rl_count = 0
}
if rl_count >= 10 {
return "{\"rate_limited\":true,\"reset_at\":" + int_to_str(next_reset) + "}"
}
state_set(rl_key, int_to_str(rl_count + 1) + "|" + int_to_str(today_day))
} else {
// Production: read current count from Supabase
let rl_resp: String = supabase_get(rl_sb_url, rl_sb_key,
"demo_rate_limits?uid=eq." + rate_uid + "&select=count,day_number&limit=1")
let rl_row: String = json_array_get(rl_resp, 0)
let rl_count: Int = 0
let rl_day: Int = 0
if !str_eq(rl_row, "") {
let rl_count_s: String = json_get(rl_row, "count")
let rl_day_s: String = json_get(rl_row, "day_number")
if !str_eq(rl_count_s, "") {
let rl_count = str_to_int(rl_count_s)
}
if !str_eq(rl_day_s, "") {
let rl_day = str_to_int(rl_day_s)
}
}
// Reset count on new UTC day
if rl_day != today_day {
let rl_count = 0
}
if rl_count >= 10 {
return "{\"rate_limited\":true,\"reset_at\":" + int_to_str(next_reset) + "}"
}
// Upsert new count supabase_insert uses Prefer: resolution=merge-duplicates
let new_count: Int = rl_count + 1
let rl_row_json: String = "{\"uid\":\"" + rate_uid
+ "\",\"count\":" + int_to_str(new_count)
+ ",\"day_number\":" + int_to_str(today_day) + "}"
let _rl_upsert: String = supabase_insert(rl_sb_url, rl_sb_key, "demo_rate_limits", rl_row_json)
}
if rl_count >= 10 {
return "{\"rate_limited\":true,\"reset_at\":" + int_to_str(next_reset) + "}"
}
state_set(rl_key, int_to_str(rl_count + 1) + "|" + int_to_str(today_day))
}
// Turnstile: server-side verification is mandatory on every first
// message (tokens are single-use; per-message verification would
@@ -1154,6 +1410,14 @@ fn handle_request_inner(method: String, path: String, body: String) -> String {
let qrem_safe: String = if str_eq(qrem_str, "") { "10" } else { qrem_str }
let is_last_str: String = json_get(body, "is_last_question")
let is_last_safe: String = if str_eq(is_last_str, "true") { "true" } else { "false" }
let user_name_raw: String = json_get_string(body, "user_name")
let user_tz_raw: String = json_get_string(body, "user_timezone")
let tod_raw: String = json_get_string(body, "time_of_day")
let is_return_raw: String = json_get_string(body, "is_return")
let user_name_safe: String = str_replace(str_replace(user_name_raw, "\\", "\\\\"), "\"", "\\\"")
let user_tz_safe: String = str_replace(str_replace(user_tz_raw, "\\", "\\\\"), "\"", "\\\"")
let tod_safe: String = str_replace(str_replace(tod_raw, "\\", "\\\\"), "\"", "\\\"")
let is_return_safe: String = if str_eq(is_return_raw, "true") { "true" } else { "false" }
// Look up the configured chat model from public.neuron_config
// (Phase 1 runtime config store). 60s TTL caching, falls back
// to the hardcoded default on Supabase miss / error.
@@ -1162,7 +1426,7 @@ fn handle_request_inner(method: String, path: String, body: String) -> String {
// Build inner content with history and engram context for thread context.
// soul-demo unwraps payload from the dharma envelope, then reads
// model with json_get(body, "model") - so this propagates end to end.
let inner: String = "{\"event_type\":\"chat\",\"payload\":{\"message\":\"" + msg_safe + "\",\"history\":" + hist_safe + ",\"an\":" + an_safe + ",\"ec\":" + ec_safe + ",\"questions_remaining\":" + qrem_safe + ",\"is_last_question\":" + is_last_safe + ",\"model\":\"" + model_safe + "\"}}"
let inner: String = "{\"event_type\":\"chat\",\"payload\":{\"message\":\"" + msg_safe + "\",\"history\":" + hist_safe + ",\"an\":" + an_safe + ",\"ec\":" + ec_safe + ",\"questions_remaining\":" + qrem_safe + ",\"is_last_turn\":" + is_last_safe + ",\"model\":\"" + model_safe + "\",\"user_name\":\"" + user_name_safe + "\",\"user_timezone\":\"" + user_tz_safe + "\",\"time_of_day\":\"" + tod_safe + "\",\"is_return\":\"" + is_return_safe + "\"}}"
// Escape inner for the outer content field
let inner_safe: String = str_replace(str_replace(inner, "\\", "\\\\"), "\"", "\\\"")
// Build dharma envelope with per-user channel
@@ -1209,7 +1473,51 @@ fn handle_request_inner(method: String, path: String, body: String) -> String {
// magic-link invite email so the buyer can sign in and see their
// plan on /account. Idempotent: existing users get a fresh link.
// 4. Forwards to license API for key provisioning when configured.
//
// SECURITY: Stripe-Signature header is verified via HMAC-SHA256 before
// any processing occurs. Without this check an attacker could POST a
// forged payment_intent.succeeded event and increment the founding counter
// or trigger account provisioning for an arbitrary email.
//
// Stripe signature format: "t=<timestamp>,v1=<hex_sig>[,v1=...]"
// Signed payload: "<timestamp>.<raw_body>"
// Key: STRIPE_WEBHOOK_SECRET (whsec_... value from Stripe dashboard)
if str_eq(path, "/api/webhooks/stripe") {
let wh_secret: String = state_get("__stripe_webhook_secret__")
if !str_eq(wh_secret, "") {
let sig_header: String = map_get(headers, "stripe-signature")
if str_eq(sig_header, "") {
println("[webhook] rejected: missing Stripe-Signature header")
return "{\"__status__\":400,\"error\":\"missing signature\"}"
}
// Extract t= value from sig header
let t_idx: Int = str_index_of(sig_header, "t=")
let t_val: String = ""
if t_idx >= 0 {
let t_tail: String = str_slice(sig_header, t_idx + 2, str_len(sig_header))
let t_comma: Int = str_index_of(t_tail, ",")
let t_val = if t_comma >= 0 { str_slice(t_tail, 0, t_comma) } else { t_tail }
}
// Extract v1= value from sig header
let v1_idx: Int = str_index_of(sig_header, "v1=")
let v1_val: String = ""
if v1_idx >= 0 {
let v1_tail: String = str_slice(sig_header, v1_idx + 3, str_len(sig_header))
let v1_comma: Int = str_index_of(v1_tail, ",")
let v1_val = if v1_comma >= 0 { str_slice(v1_tail, 0, v1_comma) } else { v1_tail }
}
if str_eq(t_val, "") || str_eq(v1_val, "") {
println("[webhook] rejected: malformed Stripe-Signature header")
return "{\"__status__\":400,\"error\":\"invalid signature format\"}"
}
// Compute expected HMAC: HMAC-SHA256(secret, "<t_val>.<body>")
let signed_payload: String = t_val + "." + body
let expected_sig: String = hmac_sha256_hex(wh_secret, signed_payload)
if !str_eq(expected_sig, v1_val) {
println("[webhook] rejected: signature mismatch")
return "{\"__status__\":400,\"error\":\"signature verification failed\"}"
}
}
let is_session_done: Bool = str_contains(body, "checkout.session.completed")
let is_pi_done: Bool = str_contains(body, "payment_intent.succeeded")
let is_si_done: Bool = str_contains(body, "setup_intent.succeeded")
@@ -1639,20 +1947,28 @@ fn handle_request_inner(method: String, path: String, body: String) -> String {
if str_starts_with(path, "/marketplace/success") {
let badge_html: String = founding_badge(get_sold())
let badge_css: String = founding_badge_css()
return page_open() + badge_css + "
<div style=\"min-height:80vh;display:flex;flex-direction:column;align-items:center;justify-content:center;text-align:center;padding:4rem 2rem\">
<p class=\"label\" style=\"margin-bottom:1.5rem\">You&#39;re in.</p>
<h1 class=\"display-lg\" style=\"margin-bottom:1.25rem\">Welcome to Neuron.</h1>
<p style=\"font-family:var(--body);font-weight:300;font-size:1.1rem;color:var(--t2);max-width:28rem;line-height:1.7;margin-bottom:3rem\">
Your license is being provisioned. Check your email - your license key and download instructions will be there in the next few minutes.
</p>
<div style=\"margin-bottom:3rem\">" + badge_html + "</div>
<div style=\"display:flex;gap:1rem;flex-wrap:wrap;justify-content:center\">
<a href=\"/account\" class=\"btn-primary\">View your account &#8594;</a>
<a href=\"/\" class=\"btn-ghost\">Back to home</a>
</div>
</div>
" + page_close()
let success_body: String = el_div(
"style=\"min-height:80vh;display:flex;flex-direction:column;align-items:center;justify-content:center;text-align:center;padding:4rem 2rem\"",
el_p("class=\"label\" style=\"margin-bottom:1.5rem\"", "You&#39;re in.")
+ el_h1("class=\"display-lg\" style=\"margin-bottom:1.25rem\"", "Welcome to Neuron.")
+ el_p(
"style=\"font-family:var(--body);font-weight:300;font-size:1.1rem;color:var(--t2);max-width:28rem;line-height:1.7;margin-bottom:3rem\"",
"Your license is being provisioned. Check your email - your license key and download instructions will be there in the next few minutes."
)
+ el_div("style=\"margin-bottom:3rem\"", badge_html)
+ el_div(
"style=\"display:flex;gap:1rem;flex-wrap:wrap;justify-content:center\"",
el_a("/account", "class=\"btn-primary\"", "View your account &#8594;")
+ el_a("/", "class=\"btn-ghost\"", "Back to home")
)
)
return page_open_seo(
"Welcome to Neuron — Your Membership is Confirmed",
"Your Neuron membership is confirmed. Download the app and let the AI that remembers you get to work.",
"/marketplace/success",
"Your Neuron membership is confirmed. Download the app and let the AI that remembers you get to work.",
"true"
) + badge_css + success_body + page_close()
}
// Account dashboard
@@ -1833,6 +2149,97 @@ fn handle_request_inner(method: String, path: String, body: String) -> String {
return "{\"ok\":true}"
}
// API key provisioning POST /api/api-keys
// Returns user's stored provider keys (masked) for display on /account.
// Body: { access_token: "<jwt>" }
if str_eq(path, "/api/api-keys") && str_eq(method, "POST") {
let ak_jwt: String = json_get_string(body, "access_token")
if str_eq(ak_jwt, "") {
return "{\"__status__\":401,\"error\":\"missing_jwt\"}"
}
let ak_url: String = state_get("__supabase_project_url__")
let ak_anon: String = state_get("__supabase_anon_key__")
let ak_service: String = state_get("__supabase_service_key__")
if str_eq(ak_url, "") {
return "{\"__status__\":503,\"error\":\"supabase_not_configured\"}"
}
let ak_user: String = supabase_auth_user(ak_url, ak_anon, ak_jwt)
let ak_uid: String = json_get(ak_user, "id")
if str_eq(ak_uid, "") {
return "{\"__status__\":401,\"error\":\"invalid_jwt\"}"
}
let ak_q: String = "user_api_keys?select=provider,key_value&user_id=eq." + ak_uid
let ak_rows: String = supabase_get(ak_url, ak_service, ak_q)
return "{\"rows\":" + ak_rows + "}"
}
// API key provisioning POST /api/api-keys/save
// Upserts a single provider key for the authenticated user.
// Body: { access_token: "<jwt>", provider: "openai"|"anthropic"|"gemini"|"grok", key: "<value>" }
if str_eq(path, "/api/api-keys/save") {
let aks_jwt: String = json_get_string(body, "access_token")
let aks_provider: String = json_get_string(body, "provider")
let aks_key: String = json_get_string(body, "key")
if str_eq(aks_jwt, "") {
return "{\"__status__\":401,\"error\":\"missing_jwt\"}"
}
if str_eq(aks_provider, "") || str_eq(aks_key, "") {
return "{\"__status__\":400,\"error\":\"missing_provider_or_key\"}"
}
let aks_valid_provider: Bool = str_eq(aks_provider, "openai")
|| str_eq(aks_provider, "anthropic")
|| str_eq(aks_provider, "gemini")
|| str_eq(aks_provider, "grok")
if !aks_valid_provider {
return "{\"__status__\":400,\"error\":\"invalid_provider\"}"
}
let aks_url: String = state_get("__supabase_project_url__")
let aks_anon: String = state_get("__supabase_anon_key__")
let aks_service: String = state_get("__supabase_service_key__")
if str_eq(aks_url, "") {
return "{\"__status__\":503,\"error\":\"supabase_not_configured\"}"
}
let aks_user: String = supabase_auth_user(aks_url, aks_anon, aks_jwt)
let aks_uid: String = json_get(aks_user, "id")
if str_eq(aks_uid, "") {
return "{\"__status__\":401,\"error\":\"invalid_jwt\"}"
}
let aks_row: String = "{\"user_id\":\"" + aks_uid + "\",\"provider\":\"" + aks_provider + "\",\"key_value\":\"" + aks_key + "\",\"updated_at\":\"now()\"}"
let _aks_resp: String = supabase_insert(aks_url, aks_service, "user_api_keys?on_conflict=user_id,provider", aks_row)
let aks_klen: Int = str_len(aks_key)
let aks_masked: String = if aks_klen <= 10 {
str_repeat("", aks_klen)
} else {
str_slice(aks_key, 0, 6) + "••••" + str_slice(aks_key, aks_klen - 4, aks_klen)
}
return "{\"ok\":true,\"masked\":\"" + aks_masked + "\"}"
}
// API key provisioning POST /api/api-keys/delete
// Soft-deletes a provider key by clearing key_value for the authenticated user.
// Body: { access_token: "<jwt>", provider: "openai"|"anthropic"|"gemini"|"grok" }
if str_eq(path, "/api/api-keys/delete") {
let akd_jwt: String = json_get_string(body, "access_token")
let akd_provider: String = json_get_string(body, "provider")
if str_eq(akd_jwt, "") {
return "{\"__status__\":401,\"error\":\"missing_jwt\"}"
}
let akd_url: String = state_get("__supabase_project_url__")
let akd_anon: String = state_get("__supabase_anon_key__")
let akd_service: String = state_get("__supabase_service_key__")
if str_eq(akd_url, "") {
return "{\"__status__\":503,\"error\":\"supabase_not_configured\"}"
}
let akd_user: String = supabase_auth_user(akd_url, akd_anon, akd_jwt)
let akd_uid: String = json_get(akd_user, "id")
if str_eq(akd_uid, "") {
return "{\"__status__\":401,\"error\":\"invalid_jwt\"}"
}
let akd_row: String = "{\"user_id\":\"" + akd_uid + "\",\"provider\":\"" + akd_provider + "\",\"key_value\":\"\",\"updated_at\":\"now()\"}"
let _akd_resp: String = supabase_insert(akd_url, akd_service, "user_api_keys?on_conflict=user_id,provider", akd_row)
return "{\"ok\":true}"
}
// Fallback
return "{\"__status__\":404,\"error\":\"not found\"}"
}
@@ -1858,11 +2265,43 @@ fn sec_headers_json() -> String {
+ "\"X-Frame-Options\":\"SAMEORIGIN\","
+ "\"Referrer-Policy\":\"strict-origin-when-cross-origin\","
+ "\"Permissions-Policy\":\"geolocation=(), microphone=(), camera=()\","
+ "\"Content-Security-Policy\":\"default-src 'self'; script-src 'self' 'unsafe-inline' https://challenges.cloudflare.com https://cdn.jsdelivr.net https://www.googletagmanager.com https://www.google-analytics.com; style-src 'self' 'unsafe-inline'; frame-src https://challenges.cloudflare.com; connect-src 'self' https://api.stripe.com https://*.supabase.co; img-src 'self' data: https:; font-src 'self' data:\"}"
+ "\"Content-Security-Policy\":\"default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval' https://challenges.cloudflare.com https://cdn.jsdelivr.net https://www.googletagmanager.com https://www.google-analytics.com; style-src 'self' 'unsafe-inline'; frame-src https://challenges.cloudflare.com; connect-src 'self' https://api.stripe.com https://*.supabase.co; img-src 'self' data: https:; font-src 'self' data:\"}"
}
fn handle_request(method: String, path: String, body: String) -> String {
let inner_resp: String = handle_request_inner(method, path, body)
// Headers for compiled JS assets. Explicitly sets Content-Type so the browser
// treats them as JavaScript regardless of what http_detect_content_type()
// infers from the content (minified/obfuscated JS can trip the JSON heuristic).
// Cache-Control bumped to 1 year + immutable: JS bundles are content-addressed
// (hash in filename) so safe for Cloudflare to cache indefinitely at the edge.
fn js_headers_json() -> String {
"{\"Content-Type\":\"application/javascript; charset=utf-8\","
+ "\"Cache-Control\":\"public, max-age=31536000, immutable\","
+ "\"Strict-Transport-Security\":\"max-age=63072000; includeSubDomains; preload\","
+ "\"X-Content-Type-Options\":\"nosniff\","
+ "\"X-Frame-Options\":\"SAMEORIGIN\","
+ "\"Referrer-Policy\":\"strict-origin-when-cross-origin\","
+ "\"Permissions-Policy\":\"geolocation=(), microphone=(), camera=()\","
+ "\"Content-Security-Policy\":\"default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval' https://challenges.cloudflare.com https://cdn.jsdelivr.net https://www.googletagmanager.com https://www.google-analytics.com; style-src 'self' 'unsafe-inline'; frame-src https://challenges.cloudflare.com; connect-src 'self' https://api.stripe.com https://*.supabase.co; img-src 'self' data: https:; font-src 'self' data:\"}"
}
// Headers for static assets under /assets/ and /brand/.
// max-age=31536000 (1 year) + immutable tells Cloudflare to cache at the edge
// and never revalidate assets are versioned by filename or content so stale
// delivery is not a risk. This eliminates Cloud Run hits for every image/font/svg.
// Security headers are included so asset responses are equally hardened even
// when served directly (e.g. Cloudflare bypass or direct origin fetch).
fn static_asset_headers_json() -> String {
"{\"Cache-Control\":\"public, max-age=31536000, immutable\","
+ "\"Strict-Transport-Security\":\"max-age=63072000; includeSubDomains; preload\","
+ "\"X-Content-Type-Options\":\"nosniff\","
+ "\"X-Frame-Options\":\"SAMEORIGIN\","
+ "\"Referrer-Policy\":\"strict-origin-when-cross-origin\","
+ "\"Permissions-Policy\":\"geolocation=(), microphone=(), camera=()\","
+ "\"Content-Security-Policy\":\"default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval' https://challenges.cloudflare.com https://cdn.jsdelivr.net https://www.googletagmanager.com https://www.google-analytics.com; style-src 'self' 'unsafe-inline'; frame-src https://challenges.cloudflare.com; connect-src 'self' https://api.stripe.com https://*.supabase.co; img-src 'self' data: https:; font-src 'self' data:\"}"
}
fn handle_request(method: String, path: String, headers: Map, body: String) -> String {
let inner_resp: String = handle_request_inner(method, path, headers, body)
// Detect envelope already set by inner handler (starts with
// {"el_http_response":1). If so, let it pass through unmodified
// the status code it carries takes precedence and we must not
@@ -1907,6 +2346,7 @@ let stripe_pub_key: String = env("STRIPE_PUBLISHABLE_KEY")
let stripe_price_founding: String = env("STRIPE_PRICE_FOUNDING")
let stripe_price_professional: String = env("STRIPE_PRICE_PROFESSIONAL")
let family_child_price: String = env("STRIPE_PRICE_FAMILY_CHILD")
let stripe_webhook_secret: String = env("STRIPE_WEBHOOK_SECRET")
let license_api_url: String = env("NEURON_LICENSE_API_URL")
let resend_api_key: String = env("RESEND_API_KEY")
let supabase_anon_key: String = env("SUPABASE_ANON_KEY")
@@ -1931,7 +2371,13 @@ fs_write(html_path, page_html)
// Generate about page HTML.
let about_html_path: String = src_dir + "/about.html"
let about_html: String = page_open() + about_page() + page_close()
let about_html: String = page_open_seo(
"About Will Anderson — Neuron",
"Neuron was built by one person. Will Anderson — engineer, founder, and the sole author of every line of Neuron's code. This is his story.",
"/about",
"Neuron was built by one person. Will Anderson spent nearly two years building the AI that remembers you — the memory architecture, the inference infrastructure, everything from the ground up.",
"false"
) + about_page() + page_close()
fs_write(about_html_path, about_html)
// Generate terms pages HTML.
@@ -1961,6 +2407,7 @@ state_set("__founding_sold_file__", sold_file)
state_set("__founding_sold__", int_to_str(real_sold))
state_set("__founding_total__", int_to_str(FOUNDING_TOTAL))
state_set("__turnstile_secret_key__", turnstile_secret_key)
state_set("__stripe_webhook_secret__", stripe_webhook_secret)
persist_founding_count(real_sold)
println(color_bold("Neuron") + " - " + neuron_origin)
@@ -1987,5 +2434,5 @@ println(" GET /api/supabase-config → public Supabase config (URL + a
println("")
let port: Int = if str_eq(env("PORT"), "") { 3001 } else { str_to_int(env("PORT")) }
http_set_handler("handle_request")
http_serve(port, "handle_request")
http_set_handler_v2("handle_request")
http_serve_v2(port, "handle_request")
+163 -130
View File
@@ -1,142 +1,119 @@
// components/marketplace.el - Marketplace section.
// Explains the plugin marketplace - what it is, how it works, coming soon.
fn marketplace() -> String {
return <section id="marketplace" aria-label="Neuron Marketplace">
<div class="container">
extern fn el_section(attrs: String, children: String) -> String
extern fn el_div(attrs: String, children: String) -> String
extern fn el_span(attrs: String, children: String) -> String
extern fn el_h2(attrs: String, text: String) -> String
extern fn el_h3(attrs: String, text: String) -> String
extern fn el_p(attrs: String, children: String) -> String
extern fn el_button(attrs: String, label: String) -> String
extern fn el_form(attrs: String, children: String) -> String
extern fn el_label(for_id: String, attrs: String, children: String) -> String
extern fn el_input(type_attr: String, attrs: String) -> String
extern fn el_textarea(attrs: String, value: String) -> String
extern fn el_script_src(src: String, defer_load: Bool) -> String
<div class="marketplace-header">
<div class="marketplace-label-row reveal">
<div class="navy-line-left" style="width:4rem;flex-shrink:0"></div>
<span class="label" style="color:var(--navy-85)">Marketplace</span>
<div style="margin-left:1rem;background:rgba(0,82,160,.1);color:var(--navy);font-family:var(--body);font-size:0.65rem;font-weight:500;letter-spacing:0.12em;text-transform:uppercase;padding:0.25rem 0.75rem;border-radius:2px">Coming soon</div>
</div>
fn marketplace_svg1() -> String {
"<svg width=\"28\" height=\"28\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\">" +
"<path d=\"M12 2L2 7l10 5 10-5-10-5z\"/><path d=\"M2 17l10 5 10-5\"/><path d=\"M2 12l10 5 10-5\"/>" +
"</svg>"
}
<h2 class="display-lg reveal" style="transition-delay:80ms;max-width:38rem">
Extend Neuron.<span class="gold" style="display:block">Build for it.</span>
</h2>
<p class="reveal" style="transition-delay:160ms;font-weight:300;font-size:.9375rem;color:var(--t2);line-height:1.75;max-width:36rem;margin-top:1.25rem">
Neuron does one thing exceptionally well: it knows you. The Marketplace extends what it can do with that knowledge - connecting it to your tools, your workflows, and capabilities built by people who understand your domain better than any general-purpose AI ever will.
</p>
</div>
fn marketplace_svg2() -> String {
"<svg width=\"28\" height=\"28\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\">" +
"<circle cx=\"12\" cy=\"12\" r=\"10\"/><path d=\"M12 8v4l3 3\"/>" +
"</svg>"
}
<div class="marketplace-grid reveal" style="transition-delay:200ms">
fn marketplace_svg3() -> String {
"<svg width=\"28\" height=\"28\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\">" +
"<path d=\"M12 2v20M17 5H9.5a3.5 3.5 0 0 0 0 7h5a3.5 3.5 0 0 1 0 7H6\"/>" +
"</svg>"
}
<div class="marketplace-card card-dark">
<div class="marketplace-card-icon">
<svg width="28" height="28" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
<path d="M12 2L2 7l10 5 10-5-10-5z"/><path d="M2 17l10 5 10-5"/><path d="M2 12l10 5 10-5"/>
</svg>
</div>
<h3 class="display-md marketplace-card-title">Plugins that know you</h3>
<p class="marketplace-card-body">Every plugin in the Marketplace has access to your memory - with your permission. A legal plugin knows your deal history. A coding plugin knows your architecture decisions. An email plugin knows your relationships and communication style. The context travels with you.</p>
</div>
fn marketplace_cards() -> String {
let card1: String = el_div(
"class=\"marketplace-card card-dark\"",
el_div("class=\"marketplace-card-icon\"", marketplace_svg1()) +
el_h3("class=\"display-md marketplace-card-title\"", "Plugins that know you") +
el_p("class=\"marketplace-card-body\"", "Every plugin in the Marketplace has access to your memory - with your permission. A legal plugin knows your deal history. A coding plugin knows your architecture decisions. An email plugin knows your relationships and communication style. The context travels with you.")
)
let card2: String = el_div(
"class=\"marketplace-card card-dark\" style=\"transition-delay:120ms\"",
el_div("class=\"marketplace-card-icon\"", marketplace_svg2()) +
el_h3("class=\"display-md marketplace-card-title\"", "Built by domain experts") +
el_p("class=\"marketplace-card-body\"", "General AI is good at general things. The Marketplace is for specialists. The person building a plugin for contract attorneys or orthopedic surgeons or professional traders isn&#39;t us. It&#39;s someone who has spent years in that world. We give them the platform. They bring the depth.")
)
let card3: String = el_div(
"class=\"marketplace-card card-dark\" style=\"transition-delay:240ms\"",
el_div("class=\"marketplace-card-icon\"", marketplace_svg3()) +
el_h3("class=\"display-md marketplace-card-title\"", "Revenue for builders") +
el_p("class=\"marketplace-card-body\"", "Developers earn recurring revenue when users install their plugins. We handle billing, distribution, and the infrastructure. You handle the product. Every plugin that ships earns you a share of every subscription for as long as that user stays. Build once, earn indefinitely.")
)
el_div("class=\"marketplace-grid reveal\" style=\"transition-delay:200ms\"", card1 + card2 + card3)
}
<div class="marketplace-card card-dark" style="transition-delay:120ms">
<div class="marketplace-card-icon">
<svg width="28" height="28" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
<circle cx="12" cy="12" r="10"/><path d="M12 8v4l3 3"/>
</svg>
</div>
<h3 class="display-md marketplace-card-title">Built by domain experts</h3>
<p class="marketplace-card-body">General AI is good at general things. The Marketplace is for specialists. The person building a plugin for contract attorneys or orthopedic surgeons or professional traders isn&#39;t us. It&#39;s someone who has spent years in that world. We give them the platform. They bring the depth.</p>
</div>
fn marketplace_tags_block(label: String, tags: String) -> String {
el_div(
"style=\"margin-bottom:2rem\"",
el_p("class=\"label\" style=\"margin-bottom:1rem;color:var(--navy-65)\"", label) +
el_div("class=\"marketplace-tags\"", tags)
)
}
<div class="marketplace-card card-dark" style="transition-delay:240ms">
<div class="marketplace-card-icon">
<svg width="28" height="28" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
<path d="M12 2v20M17 5H9.5a3.5 3.5 0 0 0 0 7h5a3.5 3.5 0 0 1 0 7H6"/>
</svg>
</div>
<h3 class="display-md marketplace-card-title">Revenue for builders</h3>
<p class="marketplace-card-body">Developers earn recurring revenue when users install their plugins. We handle billing, distribution, and the infrastructure. You handle the product. Every plugin that ships earns you a share of every subscription for as long as that user stays. Build once, earn indefinitely.</p>
</div>
fn marketplace_tag(t: String) -> String {
el_span("class=\"marketplace-tag\"", t)
}
</div>
fn marketplace_categories() -> String {
let connectors: String =
marketplace_tag("Gmail") + marketplace_tag("Slack") + marketplace_tag("Google Calendar") + marketplace_tag("Google Drive") +
marketplace_tag("Notion") + marketplace_tag("GitHub") + marketplace_tag("Linear") + marketplace_tag("More connectors at launch")
let following: String =
marketplace_tag("Process packets") + marketplace_tag("Knowledge packets")
let imprints: String =
marketplace_tag("CEO") + marketplace_tag("CTO") + marketplace_tag("CFO") + marketplace_tag("CMO") + marketplace_tag("COO") + marketplace_tag("More C-suite")
<div class="marketplace-categories reveal" style="transition-delay:320ms">
el_div(
"class=\"marketplace-categories reveal\" style=\"transition-delay:320ms\"",
marketplace_tags_block("Connectors - day one", connectors) +
marketplace_tags_block("Following launch", following) +
el_div(
"",
el_p("class=\"label\" style=\"margin-bottom:1rem;color:var(--navy-65)\"", "Imprints - starting with") +
el_div("class=\"marketplace-tags\"", imprints)
)
)
}
<div style="margin-bottom:2rem">
<p class="label" style="margin-bottom:1rem;color:var(--navy-65)">Connectors - day one</p>
<div class="marketplace-tags">
<span class="marketplace-tag">Gmail</span>
<span class="marketplace-tag">Slack</span>
<span class="marketplace-tag">Google Calendar</span>
<span class="marketplace-tag">Google Drive</span>
<span class="marketplace-tag">Notion</span>
<span class="marketplace-tag">GitHub</span>
<span class="marketplace-tag">Linear</span>
<span class="marketplace-tag">More connectors at launch</span>
</div>
</div>
fn marketplace_dev_form() -> String {
let input_style: String = "class=\"dev-input\""
let field_name: String = el_div(
"class=\"dev-field\"",
el_label("dev-name", "class=\"dev-label\"", "Name") +
el_input("text", "id=\"dev-name\" required placeholder=\"Your name\" " + input_style)
)
let field_email: String = el_div(
"class=\"dev-field\"",
el_label("dev-email", "class=\"dev-label\"", "Email") +
el_input("email", "id=\"dev-email\" required placeholder=\"you@example.com\" " + input_style)
)
let field_idea: String = el_div(
"class=\"dev-field dev-field-full\"",
el_label("dev-idea", "class=\"dev-label\"", "What do you want to build?") +
el_textarea("id=\"dev-idea\" required rows=\"5\" placeholder=\"Tell me about the plugin or integration you have in mind...\" class=\"dev-input dev-textarea\"", "")
)
let submit_row: String = el_div(
"class=\"dev-field-full\"",
el_button("type=\"submit\" class=\"btn-primary\"", "Send interest &rarr;") +
el_p("id=\"dev-msg\" style=\"font-family:var(--body);font-size:0.8rem;color:var(--t3);margin-top:0.75rem;display:none\"", "")
)
el_form("id=\"dev-form\" class=\"dev-form-grid\"", field_name + field_email + field_idea + submit_row)
}
<div style="margin-bottom:2rem">
<p class="label" style="margin-bottom:1rem;color:var(--navy-65)">Following launch</p>
<div class="marketplace-tags">
<span class="marketplace-tag">Process packets</span>
<span class="marketplace-tag">Knowledge packets</span>
</div>
</div>
<div>
<p class="label" style="margin-bottom:1rem;color:var(--navy-65)">Imprints - starting with</p>
<div class="marketplace-tags">
<span class="marketplace-tag">CEO</span>
<span class="marketplace-tag">CTO</span>
<span class="marketplace-tag">CFO</span>
<span class="marketplace-tag">CMO</span>
<span class="marketplace-tag">COO</span>
<span class="marketplace-tag">More C-suite</span>
</div>
</div>
</div>
<div class="marketplace-cta reveal" style="transition-delay:400ms">
<div class="marketplace-cta-inner">
<div>
<p style="font-family:var(--body);font-weight:500;font-size:1rem;color:var(--t1);margin-bottom:0.5rem">Building something?</p>
<p style="font-family:var(--body);font-weight:300;font-size:0.875rem;color:var(--t2);line-height:1.6">The developer program opens before the Marketplace does. If you&#39;re interested in building a plugin, get in touch early - early developers shape the API.</p>
</div>
<button onclick="document.getElementById('developer-interest').style.display='block';document.getElementById('developer-interest').scrollIntoView({behavior:'smooth',block:'start'});this.style.display='none';" class="btn-ghost" style="white-space:nowrap;flex-shrink:0">Developer interest &rarr;</button>
</div>
</div>
</div>
<!-- Developer interest form hidden until button click -->
<div id="developer-interest" class="container" style="display:none;margin-top:4rem;padding-top:4rem;border-top:1px solid rgba(0,82,160,.10)">
<div style="max-width:42rem">
<p class="label" style="margin-bottom:0.75rem;color:var(--navy-85)">Developer Program</p>
<h3 style="font-family:var(--display);font-size:1.75rem;font-weight:400;color:var(--t1);letter-spacing:-0.01em;margin-bottom:1rem;line-height:1.2">Get early access</h3>
<p style="font-family:var(--body);font-weight:300;font-size:0.9rem;color:var(--t2);line-height:1.75;margin-bottom:2.5rem">The developer program opens before the Marketplace does. Early developers shape the plugin API. Tell me what you want to build.</p>
<form id="dev-form" class="dev-form-grid">
<div class="dev-field">
<label for="dev-name" class="dev-label">Name</label>
<input id="dev-name" type="text" required placeholder="Your name" class="dev-input">
</div>
<div class="dev-field">
<label for="dev-email" class="dev-label">Email</label>
<input id="dev-email" type="email" required placeholder="you@example.com" class="dev-input">
</div>
<div class="dev-field dev-field-full">
<label for="dev-idea" class="dev-label">What do you want to build?</label>
<textarea id="dev-idea" required rows="5" placeholder="Tell me about the plugin or integration you have in mind..." class="dev-input dev-textarea"></textarea>
</div>
<div class="dev-field-full">
<button type="submit" class="btn-primary">Send interest &rarr;</button>
<p id="dev-msg" style="font-family:var(--body);font-size:0.8rem;color:var(--t3);margin-top:0.75rem;display:none"></p>
</div>
</form>
</div>
</div>
<script src="/js/marketplace.js" defer></script>
</section>
<style>
#marketplace { padding: 6rem 0; }
fn marketplace_style() -> String {
let css: String = "#marketplace { padding: 6rem 0; }
.marketplace-header { margin-bottom: 4rem; }
.marketplace-label-row { display: flex; align-items: center; gap: 1.5rem; margin-bottom: 2rem; }
.marketplace-grid {
@@ -189,8 +166,6 @@ fn marketplace() -> String {
background: rgba(0,82,160,.02);
}
@media (max-width: 640px) { .marketplace-cta-inner { flex-direction: column; align-items: flex-start; } }
/* Developer interest form */
.dev-form-grid {
display: grid;
grid-template-columns: 1fr 1fr;
@@ -227,6 +202,64 @@ fn marketplace() -> String {
border-color: var(--navy);
box-shadow: 0 0 0 3px rgba(0,82,160,.08);
}
.dev-textarea { resize: vertical; min-height: 120px; }
</style>
.dev-textarea { resize: vertical; min-height: 120px; }"
"<style>" + css + "</style>"
}
fn marketplace() -> String {
let header: String = el_div(
"class=\"marketplace-header\"",
el_div(
"class=\"marketplace-label-row reveal\"",
el_div("class=\"navy-line-left\" style=\"width:4rem;flex-shrink:0\"", "") +
el_span("class=\"label\" style=\"color:var(--navy-85)\"", "Marketplace") +
el_div("style=\"margin-left:1rem;background:rgba(0,82,160,.1);color:var(--navy);font-family:var(--body);font-size:0.65rem;font-weight:500;letter-spacing:0.12em;text-transform:uppercase;padding:0.25rem 0.75rem;border-radius:2px\"", "Coming soon")
) +
el_h2(
"class=\"display-lg reveal\" style=\"transition-delay:80ms;max-width:38rem\"",
"Extend Neuron." + el_span("class=\"gold\" style=\"display:block\"", "Build for it.")
) +
el_p("class=\"reveal\" style=\"transition-delay:160ms;font-weight:300;font-size:.9375rem;color:var(--t2);line-height:1.75;max-width:36rem;margin-top:1.25rem\"",
"Neuron does one thing exceptionally well: it knows you. The Marketplace extends what it can do with that knowledge - connecting it to your tools, your workflows, and capabilities built by people who understand your domain better than any general-purpose AI ever will."
)
)
let cta: String = el_div(
"class=\"marketplace-cta reveal\" style=\"transition-delay:400ms\"",
el_div(
"class=\"marketplace-cta-inner\"",
el_div(
"",
el_p("style=\"font-family:var(--body);font-weight:500;font-size:1rem;color:var(--t1);margin-bottom:0.5rem\"", "Building something?") +
el_p("style=\"font-family:var(--body);font-weight:300;font-size:0.875rem;color:var(--t2);line-height:1.6\"",
"The developer program opens before the Marketplace does. If you&#39;re interested in building a plugin, get in touch early - early developers shape the API."
)
) +
el_button(
"onclick=\"document.getElementById('developer-interest').style.display='block';document.getElementById('developer-interest').scrollIntoView({behavior:'smooth',block:'start'});this.style.display='none';\" class=\"btn-ghost\" style=\"white-space:nowrap;flex-shrink:0\"",
"Developer interest &rarr;"
)
)
)
let dev_section: String = el_div(
"id=\"developer-interest\" class=\"container\" style=\"display:none;margin-top:4rem;padding-top:4rem;border-top:1px solid rgba(0,82,160,.10)\"",
el_div(
"style=\"max-width:42rem\"",
el_p("class=\"label\" style=\"margin-bottom:0.75rem;color:var(--navy-85)\"", "Developer Program") +
el_h3("style=\"font-family:var(--display);font-size:1.75rem;font-weight:400;color:var(--t1);letter-spacing:-0.01em;margin-bottom:1rem;line-height:1.2\"", "Get early access") +
el_p("style=\"font-family:var(--body);font-weight:300;font-size:0.9rem;color:var(--t2);line-height:1.75;margin-bottom:2.5rem\"",
"The developer program opens before the Marketplace does. Early developers shape the plugin API. Tell me what you want to build."
) +
marketplace_dev_form()
)
)
el_section(
"id=\"marketplace\" aria-label=\"Neuron Marketplace\"",
el_div("class=\"container\"", header + marketplace_cards() + marketplace_categories() + cta) +
dev_section +
el_script_src("/js/marketplace.js", true) +
marketplace_style()
)
}
+116 -77
View File
@@ -1,91 +1,130 @@
// components/mission.el - Origin story + manifesto + problems grid.
extern fn el_section(attrs: String, children: String) -> String
extern fn el_div(attrs: String, children: String) -> String
extern fn el_span(attrs: String, children: String) -> String
extern fn el_h2(attrs: String, text: String) -> String
extern fn el_p(attrs: String, children: String) -> String
extern fn el_strong(children: String) -> String
extern fn el_a(href: String, attrs: String, children: String) -> String
fn mission() -> String {
return <section id="mission" aria-label="Mission">
<div class="container">
let label_row: String = el_div(
"class=\"mission-label-row reveal\"",
el_div("class=\"navy-line-left\" style=\"width:4rem;flex-shrink:0\"", "") +
el_span("class=\"label\"", "The mission") +
el_div("style=\"height:1px;width:4rem;background:linear-gradient(to left,transparent,rgba(0,82,160,.35));flex-shrink:0\"", "")
)
<div class="navy-line" style="margin-bottom:5rem"></div>
let origin: String = el_div(
"class=\"mission-origin\"",
label_row +
el_h2("class=\"display-lg mission-headline reveal\" style=\"transition-delay:80ms\"", "Why I built this") +
el_p("class=\"mission-body-para reveal\" style=\"transition-delay:160ms\"",
"Every AI tool you use today resets when you close the tab. It doesn&#39;t know you tomorrow. It doesn&#39;t remember what mattered yesterday. It can&#39;t grow with you over years."
) +
el_p("class=\"mission-body-para reveal\" style=\"transition-delay:240ms\"",
"I built Neuron because intelligence should compound. The same way a great mentor gets more valuable the longer you work with them - knowing your context, your patterns, your goals - your AI should too."
) +
el_p("class=\"mission-body-para mission-body-emphasis reveal\" style=\"transition-delay:320ms\"",
"Neuron is private by design. It runs on your hardware. " +
el_strong("Your data never leaves your machine.") +
" No training on your data. No telemetry. No cloud dependency."
) +
el_p("class=\"mission-body-para reveal\" style=\"transition-delay:400ms\"",
"This isn&#39;t a chat interface. It&#39;s the AI that becomes yours."
)
)
<div class="mission-origin">
<div class="mission-label-row reveal">
<div class="navy-line-left" style="width:4rem;flex-shrink:0"></div>
<span class="label">The mission</span>
<div style="height:1px;width:4rem;background:linear-gradient(to left,transparent,rgba(0,82,160,.35));flex-shrink:0"></div>
</div>
<h2 class="display-lg mission-headline reveal" style="transition-delay:80ms">Why I built this</h2>
let bigtech: String = el_div(
"class=\"mission-bigtech reveal\" style=\"transition-delay:600ms\"",
el_p("class=\"mission-bigtech-label\"", "Why I built this on my own") +
el_p("",
"I didn&#39;t just approach one of the largest technology companies in the world - I got the meeting. Got the NDAs signed. Created deliverables in real time. Showed them benchmarks with full auditability. Some of their own people understood immediately what it meant."
) +
el_p("",
"They saw it. Seemed to engage meaningfully. Then, within two days, lawyers were involved. I decided to just finish the project on my own."
) +
el_p("class=\"muted\"",
"Not: how do we solve this at scale? Not: what does this mean for the people we serve? Their instinct was to protect enterprise revenue and manage legal exposure. The actual human impact - the people whose lives those enterprises touch - didn&#39;t enter the conversation."
) +
el_p("class=\"muted\"",
"That&#39;s the difference. " +
"<strong style=\"color:var(--t1)\">They&#39;re optimizing for the enterprise. I&#39;m building for the people those enterprises are supposed to serve.</strong>"
) +
el_p("",
"I told them I could build and distribute this by myself. Maybe they didn&#39;t believe me. That meeting was April 22nd, 2026. I&#39;m writing this on April 25th. You&#39;re looking at the proof. " +
el_a("/checkout?plan=founding", "", "I hope you&#39;ll preorder it.")
)
)
<p class="mission-body-para reveal" style="transition-delay:160ms">
Every AI tool you use today resets when you close the tab. It doesn&#39;t know you tomorrow. It doesn&#39;t remember what mattered yesterday. It can&#39;t grow with you over years.
</p>
<p class="mission-body-para reveal" style="transition-delay:240ms">
I built Neuron because intelligence should compound. The same way a great mentor gets more valuable the longer you work with them - knowing your context, your patterns, your goals - your AI should too.
</p>
<p class="mission-body-para mission-body-emphasis reveal" style="transition-delay:320ms">
Neuron is private by design. It runs on your hardware. <strong>Your data never leaves your machine.</strong> No training on your data. No telemetry. No cloud dependency.
</p>
<p class="mission-body-para reveal" style="transition-delay:400ms">
This isn&#39;t a chat interface. It&#39;s the AI that becomes yours.
</p>
</div>
let problem1: String = el_div(
"class=\"problem-item reveal\"",
el_p("class=\"problem-label\"", "Synthetic media without accountability") +
el_p("class=\"problem-body\"", "Generative AI makes it trivially easy to produce harmful content at scale - and nearly impossible to trace. This is a problem the industry is largely ignoring. I&#39;m not. I&#39;m engaged with it seriously and expect to have answers in place before it becomes unmanageable.")
)
let problem2: String = el_div(
"class=\"problem-item reveal\" style=\"transition-delay:120ms\"",
el_p("class=\"problem-label\"", "Epistemic collapse") +
el_p("class=\"problem-body\"", "AI can now generate persuasive content at any volume, on any position. The next generation is growing up in an environment where signal and noise are becoming indistinguishable. I think deeply about what it means to build tools that contribute to that problem - and how to build ones that don&#39;t.")
)
let problem3: String = el_div(
"class=\"problem-item reveal\" style=\"transition-delay:240ms\"",
el_p("class=\"problem-label\"", "Concentration of inference") +
el_p("class=\"problem-body\"", "Four companies control nearly all frontier AI inference. Every query strengthens their position. I think that concentration of power is a structural risk - not just a pricing problem - and I&#39;m building with that in mind.")
)
let problem4: String = el_div(
"class=\"problem-item reveal\" style=\"transition-delay:360ms\"",
el_p("class=\"problem-label\"", "The accountability gap") +
el_p("class=\"problem-body\"", "When an AI agent takes a bad action, there is currently no clear legal or technical accountability. That&#39;s going to matter more as agents do more. I take this seriously. I&#39;m building toward answers - not waiting for regulators to force the question.")
)
<div class="mission-bigtech reveal" style="transition-delay:600ms">
<p class="mission-bigtech-label">Why I built this on my own</p>
<p>
I didn&#39;t just approach one of the largest technology companies in the world - I got the meeting. Got the NDAs signed. Created deliverables in real time. Showed them benchmarks with full auditability. Some of their own people understood immediately what it meant.
</p>
<p>
They saw it. Seemed to engage meaningfully. Then, within two days, lawyers were involved. I decided to just finish the project on my own.
</p>
<p class="muted">
Not: how do we solve this at scale? Not: what does this mean for the people we serve? Their instinct was to protect enterprise revenue and manage legal exposure. The actual human impact - the people whose lives those enterprises touch - didn&#39;t enter the conversation.
</p>
<p class="muted">
That&#39;s the difference. <strong style="color:var(--t1)">They&#39;re optimizing for the enterprise. I&#39;m building for the people those enterprises are supposed to serve.</strong>
</p>
<p>
I told them I could build and distribute this by myself. Maybe they didn&#39;t believe me. That meeting was April 22nd, 2026. I&#39;m writing this on April 25th. You&#39;re looking at the proof. <a href="/checkout?plan=founding">I hope you&#39;ll preorder it.</a>
</p>
</div>
let problems_grid: String = el_div(
"class=\"problems-grid\"",
problem1 + problem2 + problem3 + problem4 +
el_div("class=\"problems-grid-bottom\"", "")
)
<div class="mission-problems">
<div class="mission-sub-row reveal">
<div class="mission-sub-line"></div>
<span class="label">What I&#39;m building against</span>
</div>
let closer: String = el_div(
"class=\"mission-closer reveal\"",
el_p("", "The industry built tools to make AI easier to use. " +
el_span("", "I&#39;m building tools to make it safer to trust.")
)
)
<div class="problems-grid">
<div class="problem-item reveal">
<p class="problem-label">Synthetic media without accountability</p>
<p class="problem-body">Generative AI makes it trivially easy to produce harmful content at scale - and nearly impossible to trace. This is a problem the industry is largely ignoring. I&#39;m not. I&#39;m engaged with it seriously and expect to have answers in place before it becomes unmanageable.</p>
</div>
<div class="problem-item reveal" style="transition-delay:120ms">
<p class="problem-label">Epistemic collapse</p>
<p class="problem-body">AI can now generate persuasive content at any volume, on any position. The next generation is growing up in an environment where signal and noise are becoming indistinguishable. I think deeply about what it means to build tools that contribute to that problem - and how to build ones that don&#39;t.</p>
</div>
<div class="problem-item reveal" style="transition-delay:240ms">
<p class="problem-label">Concentration of inference</p>
<p class="problem-body">Four companies control nearly all frontier AI inference. Every query strengthens their position. I think that concentration of power is a structural risk - not just a pricing problem - and I&#39;m building with that in mind.</p>
</div>
<div class="problem-item reveal" style="transition-delay:360ms">
<p class="problem-label">The accountability gap</p>
<p class="problem-body">When an AI agent takes a bad action, there is currently no clear legal or technical accountability. That&#39;s going to matter more as agents do more. I take this seriously. I&#39;m building toward answers - not waiting for regulators to force the question.</p>
</div>
<div class="problems-grid-bottom"></div>
</div>
let commitment: String = el_div(
"class=\"reveal\" style=\"max-width:44rem;margin-top:3.5rem;padding:2rem 2.5rem;border-left:3px solid rgba(0,82,160,.30);background:rgba(0,82,160,.02)\"",
el_p("style=\"font-family:var(--body);font-weight:500;font-size:1rem;color:var(--t1);margin-bottom:1rem\"", "Nobody&#39;s perfect.") +
el_p("style=\"font-family:var(--body);font-weight:300;font-size:0.9375rem;color:var(--t2);line-height:1.8;margin-bottom:1rem\"",
"Neuron isn&#39;t either. There is a gap between what the AI industry is delivering and what the world actually needs. Bridging that gap is the work - not a one-time product release, but continuous work, done in the open, built on the trust that users place in it."
) +
el_p("style=\"font-family:var(--body);font-weight:400;font-size:0.9375rem;color:var(--navy);line-height:1.6\"",
"That&#39;s my commitment. To keep working. To be honest about the problems. To build something that earns trust by doing the hard things right."
)
)
<div class="mission-closer reveal">
<p>The industry built tools to make AI easier to use. <span>I&#39;m building tools to make it safer to trust.</span></p>
</div>
let sub_row: String = el_div(
"class=\"mission-sub-row reveal\"",
el_div("class=\"mission-sub-line\"", "") +
el_span("class=\"label\"", "What I&#39;m building against")
)
<div class="reveal" style="max-width:44rem;margin-top:3.5rem;padding:2rem 2.5rem;border-left:3px solid rgba(0,82,160,.30);background:rgba(0,82,160,.02)">
<p style="font-family:var(--body);font-weight:500;font-size:1rem;color:var(--t1);margin-bottom:1rem">Nobody&#39;s perfect.</p>
<p style="font-family:var(--body);font-weight:300;font-size:0.9375rem;color:var(--t2);line-height:1.8;margin-bottom:1rem">Neuron isn&#39;t either. There is a gap between what the AI industry is delivering and what the world actually needs. Bridging that gap is the work - not a one-time product release, but continuous work, done in the open, built on the trust that users place in it.</p>
<p style="font-family:var(--body);font-weight:400;font-size:0.9375rem;color:var(--navy);line-height:1.6">That&#39;s my commitment. To keep working. To be honest about the problems. To build something that earns trust by doing the hard things right.</p>
</div>
let problems: String = el_div(
"class=\"mission-problems\"",
sub_row + problems_grid + closer + commitment
)
</div>
let bottom_line: String = el_div(
"class=\"container\" style=\"margin-top:5rem\"",
el_div("class=\"navy-line\"", "")
)
</div>
<div class="container" style="margin-top:5rem"><div class="navy-line"></div></div>
</section>
el_section(
"id=\"mission\" aria-label=\"Mission\"",
el_div(
"class=\"container\"",
el_div("class=\"navy-line\" style=\"margin-bottom:5rem\"", "") +
origin + bigtech + problems
) + bottom_line
)
}
+63 -44
View File
@@ -3,53 +3,72 @@
// Responsive: desktop shows full link bar, 1060px collapses to hamburger.
// Hamburger toggles .nav-mobile panel. Closes on link click or outside click.
extern fn el_nav(attrs: String, children: String) -> String
extern fn el_div(attrs: String, children: String) -> String
extern fn el_a(href: String, attrs: String, children: String) -> String
extern fn el_img(src: String, alt: String, attrs: String) -> String
extern fn el_button(attrs: String, label: String) -> String
extern fn el_span(attrs: String, children: String) -> String
extern fn el_script_src(src: String, defer_load: Bool) -> String
fn nav() -> String {
return <nav id="nav">
<div class="nav-inner">
<a href="/" class="nav-logo" aria-label="Neuron home"><img src="/assets/brand/neuron-wordmark-on-light.png" srcset="/assets/brand/neuron-wordmark-on-light@2x.png 2x" alt="Neuron" height="28"></a>
let logo_img: String = el_img(
"/assets/brand/neuron-wordmark-on-light.png",
"Neuron",
"srcset=\"/assets/brand/neuron-wordmark-on-light@2x.png 2x\" height=\"28\""
)
let logo: String = el_a("/", "class=\"nav-logo\" aria-label=\"Neuron home\"", logo_img)
<!-- Order mirrors page section order: how-it-works enterprise
mission/safety/environment marketplace pricing. -->
<div class="nav-links">
<a href="/#how-it-works" class="nav-link">How it works</a>
<a href="/#enterprise" class="nav-link">Enterprise</a>
<div class="nav-dropdown">
<button class="nav-link nav-dropdown-btn" aria-haspopup="true" aria-expanded="false">Mission &#9662;</button>
<div class="nav-dropdown-menu">
<a href="/#mission" class="nav-dropdown-item">Our mission</a>
<a href="/#safety" class="nav-dropdown-item">Safety</a>
<a href="/#environmental" class="nav-dropdown-item">Environment</a>
</div>
</div>
<a href="/#marketplace" class="nav-link">Marketplace</a>
<a href="/#pricing" class="nav-link">Pricing</a>
<a href="/about" class="nav-link">About</a>
<a href="/said" class="nav-link">Gallery</a>
<a href="/account" class="nav-link">Account</a>
<a href="/#pricing" class="nav-cta">Get Access</a>
</div>
let dropdown_menu: String = el_div(
"class=\"nav-dropdown-menu\"",
el_a("/#mission", "class=\"nav-dropdown-item\"", "Our mission") +
el_a("/#safety", "class=\"nav-dropdown-item\"", "Safety") +
el_a("/#environmental", "class=\"nav-dropdown-item\"", "Environment")
)
let dropdown: String = el_div(
"class=\"nav-dropdown\"",
el_button("class=\"nav-link nav-dropdown-btn\" aria-haspopup=\"true\" aria-expanded=\"false\"", "Mission &#9662;") +
dropdown_menu
)
<button class="nav-hamburger" id="nav-hamburger" aria-label="Open navigation" aria-expanded="false" aria-controls="nav-mobile">
<span></span>
<span></span>
<span></span>
</button>
let nav_links: String = el_div(
"class=\"nav-links\"",
el_a("/#how-it-works", "class=\"nav-link\"", "How it works") +
el_a("/#enterprise", "class=\"nav-link\"", "Enterprise") +
dropdown +
el_a("/#marketplace", "class=\"nav-link\"", "Marketplace") +
el_a("/#pricing", "class=\"nav-link\"", "Pricing") +
el_a("/about", "class=\"nav-link\"", "About") +
el_a("/said", "class=\"nav-link\"", "Gallery") +
el_a("/account", "class=\"nav-link\"", "Account") +
el_a("/#pricing", "class=\"nav-cta\"", "Get Access")
)
<div class="nav-mobile" id="nav-mobile" role="navigation" aria-label="Mobile navigation">
<a href="/#how-it-works" class="nav-mobile-link">How it works</a>
<a href="/#enterprise" class="nav-mobile-link">Enterprise</a>
<a href="/#mission" class="nav-mobile-link">Mission</a>
<a href="/#safety" class="nav-mobile-link" style="padding-left:1.75rem;font-size:0.8rem;color:var(--t3)">- Safety</a>
<a href="/#environmental" class="nav-mobile-link" style="padding-left:1.75rem;font-size:0.8rem;color:var(--t3)">- Environment</a>
<a href="/#marketplace" class="nav-mobile-link">Marketplace</a>
<a href="/#pricing" class="nav-mobile-link">Pricing</a>
<a href="/about" class="nav-mobile-link">About</a>
<a href="/said" class="nav-mobile-link">Gallery</a>
<a href="/account" class="nav-mobile-link">Account</a>
<a href="/#pricing" class="nav-mobile-cta">Get Access</a>
</div>
</div>
</nav>
let hamburger_spans: String =
el_span("", "") +
el_span("", "") +
el_span("", "")
let hamburger: String = el_button(
"class=\"nav-hamburger\" id=\"nav-hamburger\" aria-label=\"Open navigation\" aria-expanded=\"false\" aria-controls=\"nav-mobile\"",
hamburger_spans
)
<script src="/js/nav.js" defer></script>
let nav_mobile: String = el_div(
"class=\"nav-mobile\" id=\"nav-mobile\" role=\"navigation\" aria-label=\"Mobile navigation\"",
el_a("/#how-it-works", "class=\"nav-mobile-link\"", "How it works") +
el_a("/#enterprise", "class=\"nav-mobile-link\"", "Enterprise") +
el_a("/#mission", "class=\"nav-mobile-link\"", "Mission") +
el_a("/#safety", "class=\"nav-mobile-link\" style=\"padding-left:1.75rem;font-size:0.8rem;color:var(--t3)\"", "- Safety") +
el_a("/#environmental", "class=\"nav-mobile-link\" style=\"padding-left:1.75rem;font-size:0.8rem;color:var(--t3)\"", "- Environment") +
el_a("/#marketplace", "class=\"nav-mobile-link\"", "Marketplace") +
el_a("/#pricing", "class=\"nav-mobile-link\"", "Pricing") +
el_a("/about", "class=\"nav-mobile-link\"", "About") +
el_a("/said", "class=\"nav-mobile-link\"", "Gallery") +
el_a("/account", "class=\"nav-mobile-link\"", "Account") +
el_a("/#pricing", "class=\"nav-mobile-cta\"", "Get Access")
)
let nav_inner: String = el_div("class=\"nav-inner\"", logo + nav_links + hamburger + nav_mobile)
el_nav("id=\"nav\"", nav_inner + el_script_src("/js/nav.js", true))
}
+41 -31
View File
@@ -4,40 +4,50 @@
// Cards use reveal animation class (CSS IntersectionObserver polyfill
// provided by a tiny inline <script> in the page shell).
extern fn el_section(attrs: String, children: String) -> String
extern fn el_div(attrs: String, children: String) -> String
extern fn el_span(attrs: String, children: String) -> String
extern fn el_h3(attrs: String, text: String) -> String
extern fn el_p(attrs: String, children: String) -> String
fn pillars() -> String {
return <section id="pillars" aria-label="Core pillars">
<div class="container">
<div class="pillars-label reveal">
<div class="navy-line-left" style="width:4rem;flex-shrink:0"></div>
<span class="label">Why Neuron</span>
</div>
<div class="pillars-grid">
let label_row: String = el_div(
"class=\"pillars-label reveal\"",
el_div("class=\"navy-line-left\" style=\"width:4rem;flex-shrink:0\"", "") +
el_span("class=\"label\"", "Why Neuron")
)
<div class="pillar-card card-dark reveal">
<span class="pillar-numeral" aria-hidden="true">I</span>
<h3 class="display-md pillar-title">Remembers</h3>
<div class="pillar-rule"></div>
<p class="pillar-body">Every session picks up where you left off. Every project. Every context. No more re-explaining who you are, what you&#39;re building, or what matters to you.</p>
<p class="pillar-detail">Zero re-orientation</p>
</div>
let card1: String = el_div(
"class=\"pillar-card card-dark reveal\"",
el_span("class=\"pillar-numeral\" aria-hidden=\"true\"", "I") +
el_h3("class=\"display-md pillar-title\"", "Remembers") +
el_div("class=\"pillar-rule\"", "") +
el_p("class=\"pillar-body\"", "Every session picks up where you left off. Every project. Every context. No more re-explaining who you are, what you&#39;re building, or what matters to you.") +
el_p("class=\"pillar-detail\"", "Zero re-orientation")
)
<div class="pillar-card card-dark reveal" style="transition-delay:150ms">
<span class="pillar-numeral" aria-hidden="true">II</span>
<h3 class="display-md pillar-title">Sharpens</h3>
<div class="pillar-rule"></div>
<p class="pillar-body">The longer you use it, the sharper it gets. Every session builds on the last. Neuron Inference arrives Q3 2026 - not a wrapper around someone else&#39;s model. Purpose-built. Built for you.</p>
<p class="pillar-detail">Specific to you</p>
</div>
let card2: String = el_div(
"class=\"pillar-card card-dark reveal\" style=\"transition-delay:150ms\"",
el_span("class=\"pillar-numeral\" aria-hidden=\"true\"", "II") +
el_h3("class=\"display-md pillar-title\"", "Sharpens") +
el_div("class=\"pillar-rule\"", "") +
el_p("class=\"pillar-body\"", "The longer you use it, the sharper it gets. Every session builds on the last. Neuron Inference arrives Q3 2026 - not a wrapper around someone else&#39;s model. Purpose-built. Built for you.") +
el_p("class=\"pillar-detail\"", "Specific to you")
)
<div class="pillar-card card-dark reveal" style="transition-delay:300ms">
<span class="pillar-numeral" aria-hidden="true">III</span>
<h3 class="display-md pillar-title">Yours</h3>
<div class="pillar-rule"></div>
<p class="pillar-body">Runs locally. Your data never leaves. No cloud dependency, no telemetry, no training on your conversations. Complete ownership. And unlike every other AI subscription - we&#39;re not charging you to remember you.</p>
<p class="pillar-detail">Fully private</p>
</div>
let card3: String = el_div(
"class=\"pillar-card card-dark reveal\" style=\"transition-delay:300ms\"",
el_span("class=\"pillar-numeral\" aria-hidden=\"true\"", "III") +
el_h3("class=\"display-md pillar-title\"", "Yours") +
el_div("class=\"pillar-rule\"", "") +
el_p("class=\"pillar-body\"", "Runs locally. Your data never leaves. No cloud dependency, no telemetry, no training on your conversations. Complete ownership. And unlike every other AI subscription - we&#39;re not charging you to remember you.") +
el_p("class=\"pillar-detail\"", "Fully private")
)
</div>
</div>
</section>
let grid: String = el_div("class=\"pillars-grid\"", card1 + card2 + card3)
el_section(
"id=\"pillars\" aria-label=\"Core pillars\"",
el_div("class=\"container\"", label_row + grid)
)
}
+141 -107
View File
@@ -2,6 +2,15 @@
// Accepts the founding counter values as parameters so main.el can inject
// server-side rendered counts without JS.
extern fn el_section(attrs: String, children: String) -> String
extern fn el_div(attrs: String, children: String) -> String
extern fn el_span(attrs: String, children: String) -> String
extern fn el_h2(attrs: String, text: String) -> String
extern fn el_p(attrs: String, children: String) -> String
extern fn el_ul(attrs: String, children: String) -> String
extern fn el_li(attrs: String, children: String) -> String
extern fn el_button(attrs: String, label: String) -> String
fn founding_spots_html(sold: Int, total: Int) -> String {
let remaining: Int = total - sold
let pct: Int = (sold * 100) / total
@@ -9,11 +18,47 @@ fn founding_spots_html(sold: Int, total: Int) -> String {
let remaining_str: String = int_to_str(remaining)
let sold_str: String = int_to_str(sold)
let total_str: String = int_to_str(total)
return <div class="founding-spots">
<p class="founding-spots-label">Only {remaining_str} left</p>
<div class="founding-spots-bar"><div class="founding-spots-fill" style="width:{pct_str}%"></div></div>
<p class="founding-spots-sub">{sold_str} of {total_str} claimed</p>
</div>
el_div(
"class=\"founding-spots\"",
el_p("class=\"founding-spots-label\"", "Only " + remaining_str + " left") +
el_div(
"class=\"founding-spots-bar\"",
el_div("class=\"founding-spots-fill\" style=\"width:" + pct_str + "%\"", "")
) +
el_p("class=\"founding-spots-sub\"", sold_str + " of " + total_str + " claimed")
)
}
fn pricing_free_features() -> String {
el_li("", el_span("class=\"dash\"", "-") + el_span("", "Persistent memory - never resets")) +
el_li("", el_span("class=\"dash\"", "-") + el_span("", "Bring your own API keys (OpenAI, Anthropic, Grok...)")) +
el_li("", el_span("class=\"dash\"", "-") + el_span("", "Local inference via Ollama (coming)")) +
el_li("", el_span("class=\"dash\"", "-") + el_span("", "Neuron Inference included when it launches - Q3 2026")) +
el_li("", el_span("class=\"dash\"", "-") + el_span("", "Unlimited projects")) +
el_li("", el_span("class=\"dash\"", "-") + el_span("", "3 marketplace plugins included")) +
el_li("", el_span("class=\"dash\"", "-") + el_span("", "Core built-in capabilities"))
}
fn pricing_pro_features() -> String {
el_li("", el_span("class=\"dash\"", "-") + el_span("", "Bring your own API keys - use any model, any provider")) +
el_li("", el_span("class=\"dash\"", "-") + el_span("", "Neuron Inference - Q3 2026, priced below OpenAI and Anthropic")) +
el_li("", el_span("class=\"dash\"", "-") + el_span("", "Everything in Free")) +
el_li("", el_span("class=\"dash\"", "-") + el_span("", "Unlimited projects")) +
el_li("", el_span("class=\"dash\"", "-") + el_span("", "Full plugin marketplace")) +
el_li("", el_span("class=\"dash\"", "-") + el_span("", "Advanced integrations - IDE, Slack, and more")) +
el_li("", el_span("class=\"dash\"", "-") + el_span("", "Early access to new features"))
}
fn pricing_founding_features() -> String {
el_li("", el_span("class=\"dash\"", "-") + el_span("", "Neuron Inference (Q3 2026) - founding member rate, priced below the major APIs")) +
el_li("", el_span("class=\"dash\"", "-") + el_span("", "Everything in Professional - forever")) +
el_li("", el_span("class=\"dash\"", "-") + el_span("", "Never pay again - lifetime updates included")) +
el_li("", el_span("class=\"dash\"", "-") + el_span("", "Founding member badge in the app")) +
el_li("", el_span("class=\"dash\"", "-") + el_span("", "Private founding member community")) +
el_li("", el_span("class=\"dash\"", "-") + el_span("", "Shape the roadmap - your votes carry more weight")) +
el_li("", el_span("class=\"dash\"", "-") + el_span("", "Beta features before general release")) +
el_li("", el_span("class=\"dash\"", "-") + el_span("", "Name in the credits"))
}
fn pricing(sold: Int, total: Int) -> String {
@@ -24,113 +69,102 @@ fn pricing(sold: Int, total: Int) -> String {
let remaining_str: String = int_to_str(remaining)
let total_str: String = int_to_str(total)
return <section id="pricing" aria-label="Pricing">
<div class="container">
let header: String = el_div(
"class=\"pricing-header\"",
el_div(
"class=\"pricing-label-row reveal\"",
el_div("style=\"height:1px;width:4rem;background:linear-gradient(to right,transparent,rgba(0,82,160,.35))\"", "") +
el_span("class=\"label\" style=\"color:var(--navy-85)\"", "Pricing") +
el_div("style=\"height:1px;width:4rem;background:linear-gradient(to left,transparent,rgba(0,82,160,.35))\"", "")
) +
el_h2("class=\"display-lg pricing-headline reveal\" style=\"transition-delay:80ms\"", "Own it. Don&#39;t rent it.") +
el_p("class=\"pricing-sub reveal\" style=\"transition-delay:160ms\"",
"Bring your own API keys on day one. Neuron Inference - our own model layer, priced below the major APIs - launches Q3 2026."
)
)
<div class="pricing-header">
<div class="pricing-label-row reveal">
<div style="height:1px;width:4rem;background:linear-gradient(to right,transparent,rgba(0,82,160,.35))"></div>
<span class="label" style="color:var(--navy-85)">Pricing</span>
<div style="height:1px;width:4rem;background:linear-gradient(to left,transparent,rgba(0,82,160,.35))"></div>
</div>
<h2 class="display-lg pricing-headline reveal" style="transition-delay:80ms">Own it. Don&#39;t rent it.</h2>
<p class="pricing-sub reveal" style="transition-delay:160ms">Bring your own API keys on day one. Neuron Inference - our own model layer, priced below the major APIs - launches Q3 2026.</p>
</div>
let card_free: String = el_div(
"class=\"pricing-card card-dark reveal\"",
el_p("class=\"pricing-tier\"", "Free") +
el_div(
"class=\"pricing-price-row\"",
el_span("class=\"pricing-price\"", "$0") +
el_span("class=\"pricing-cadence\"", "forever")
) +
el_p("class=\"pricing-tagline\"", "Start building your memory. No card required.") +
el_ul("class=\"pricing-features\"", pricing_free_features()) +
el_div("style=\"flex:1\"", "") +
el_div(
"class=\"pricing-cta pricing-cta-ghost\"",
el_button("class=\"pricing-cta-ghost\" data-checkout=\"free\"", "Preorder free tier &#8594;")
)
)
<div class="pricing-grid">
let card_pro: String = el_div(
"class=\"pricing-card card-dark reveal\" style=\"transition-delay:150ms\"",
el_p("class=\"pricing-tier\"", "Professional") +
el_div(
"class=\"pricing-price-row\"",
el_span("class=\"pricing-price\"", "$19") +
el_span("class=\"pricing-cadence\"", "/ month")
) +
el_p("class=\"pricing-tagline\"", "Full access. Use your own API keys now. Neuron Inference when it launches.") +
el_ul("class=\"pricing-features\"", pricing_pro_features()) +
el_div("style=\"flex:1\"", "") +
el_div(
"class=\"pricing-cta pricing-cta-navy\"",
el_button("class=\"pricing-cta-navy\" data-checkout=\"professional\"", "Preorder &#8594;")
)
)
<div class="pricing-card card-dark reveal">
<p class="pricing-tier">Free</p>
<div class="pricing-price-row">
<span class="pricing-price">$0</span>
<span class="pricing-cadence">forever</span>
</div>
<p class="pricing-tagline">Start building your memory. No card required.</p>
<ul class="pricing-features">
<li><span class="dash">-</span><span>Persistent memory - never resets</span></li>
<li><span class="dash">-</span><span>Bring your own API keys (OpenAI, Anthropic, Grok...)</span></li>
<li><span class="dash">-</span><span>Local inference via Ollama (coming)</span></li>
<li><span class="dash">-</span><span>Neuron Inference included when it launches - Q3 2026</span></li>
<li><span class="dash">-</span><span>Unlimited projects</span></li>
<li><span class="dash">-</span><span>3 marketplace plugins included</span></li>
<li><span class="dash">-</span><span>Core built-in capabilities</span></li>
</ul>
<div style="flex:1"></div>
<div class="pricing-cta pricing-cta-ghost">
<button class="pricing-cta-ghost" data-checkout="free">Preorder free tier &#8594;</button>
</div>
</div>
let card_founding: String = el_div(
"class=\"pricing-card featured reveal\" style=\"transition-delay:300ms\"",
el_p("class=\"pricing-tier\"", "Founding Member") +
el_div(
"class=\"pricing-price-row\"",
el_span("class=\"pricing-price\"", "$199") +
el_span("class=\"pricing-cadence\"", "lifetime")
) +
el_p("class=\"pricing-tagline\"", "Pay once. Everything, forever. Including Neuron Inference when it launches.") +
spots_html +
el_ul("class=\"pricing-features\"", pricing_founding_features()) +
el_div("style=\"flex:1\"", "") +
el_div(
"class=\"pricing-cta pricing-cta-solid\"",
el_button("class=\"pricing-cta-solid\" data-checkout=\"founding\"", "Preorder &#8212; claim your spot &#8594;")
)
)
<div class="pricing-card card-dark reveal" style="transition-delay:150ms">
<p class="pricing-tier">Professional</p>
<div class="pricing-price-row">
<span class="pricing-price">$19</span>
<span class="pricing-cadence">/ month</span>
</div>
<p class="pricing-tagline">Full access. Use your own API keys now. Neuron Inference when it launches.</p>
<ul class="pricing-features">
<li><span class="dash">-</span><span>Bring your own API keys - use any model, any provider</span></li>
<li><span class="dash">-</span><span>Neuron Inference - Q3 2026, priced below OpenAI and Anthropic</span></li>
<li><span class="dash">-</span><span>Everything in Free</span></li>
<li><span class="dash">-</span><span>Unlimited projects</span></li>
<li><span class="dash">-</span><span>Full plugin marketplace</span></li>
<li><span class="dash">-</span><span>Advanced integrations - IDE, Slack, and more</span></li>
<li><span class="dash">-</span><span>Early access to new features</span></li>
</ul>
<div style="flex:1"></div>
<div class="pricing-cta pricing-cta-navy">
<button class="pricing-cta-navy" data-checkout="professional">Preorder &#8594;</button>
</div>
</div>
let grid: String = el_div("class=\"pricing-grid\"", card_free + card_pro + card_founding)
<div class="pricing-card featured reveal" style="transition-delay:300ms">
<p class="pricing-tier">Founding Member</p>
<div class="pricing-price-row">
<span class="pricing-price">$199</span>
<span class="pricing-cadence">lifetime</span>
</div>
<p class="pricing-tagline">Pay once. Everything, forever. Including Neuron Inference when it launches.</p>
let banner_fill: String = el_div(
"class=\"founding-banner-fill\" style=\"width:" + pct_str + "%\"",
""
)
let banner: String = el_div(
"class=\"founding-banner reveal\"",
el_div(
"",
el_p("class=\"founding-banner-label\"", "Founding Member Spots") +
el_div(
"style=\"display:flex;align-items:baseline;gap:.5rem;margin:.25rem 0\"",
el_span("class=\"founding-banner-count\"", remaining_str) +
el_span("class=\"founding-banner-sub\"", "remaining of " + el_span("", total_str))
) +
el_div("class=\"founding-banner-bar\"", banner_fill)
) +
el_button("class=\"btn-primary\" data-checkout=\"founding\" style=\"white-space:nowrap\"", "Preorder &#8594;")
)
{raw(spots_html)}
let fine1: String = el_p("class=\"pricing-fine reveal\" style=\"margin-top:2.5rem\"",
"Bring your own API keys &nbsp;&middot;&nbsp; Local inference via Ollama (coming) &nbsp;&middot;&nbsp; Neuron Inference - Q3 2026 &nbsp;&middot;&nbsp; Your data stays yours"
)
let fine2: String = el_p("class=\"pricing-fine reveal\" style=\"margin-top:1rem;font-size:0.8rem\"",
"Includes 2 devices per plan &nbsp;&middot;&nbsp; Additional devices available at a small extra cost &nbsp;&middot;&nbsp; We&#39;re not greedy"
)
<ul class="pricing-features">
<li><span class="dash">-</span><span>Neuron Inference (Q3 2026) - founding member rate, priced below the major APIs</span></li>
<li><span class="dash">-</span><span>Everything in Professional - forever</span></li>
<li><span class="dash">-</span><span>Never pay again - lifetime updates included</span></li>
<li><span class="dash">-</span><span>Founding member badge in the app</span></li>
<li><span class="dash">-</span><span>Private founding member community</span></li>
<li><span class="dash">-</span><span>Shape the roadmap - your votes carry more weight</span></li>
<li><span class="dash">-</span><span>Beta features before general release</span></li>
<li><span class="dash">-</span><span>Name in the credits</span></li>
</ul>
<div style="flex:1"></div>
<div class="pricing-cta pricing-cta-solid">
<button class="pricing-cta-solid" data-checkout="founding">Preorder claim your spot &#8594;</button>
</div>
</div>
</div>
<div class="founding-banner reveal">
<div>
<p class="founding-banner-label">Founding Member Spots</p>
<div style="display:flex;align-items:baseline;gap:.5rem;margin:.25rem 0">
<span class="founding-banner-count">{remaining_str}</span>
<span class="founding-banner-sub">remaining of <span>{total_str}</span></span>
</div>
<div class="founding-banner-bar"><div class="founding-banner-fill" style="width:{pct_str}%"></div></div>
</div>
<button class="btn-primary" data-checkout="founding" style="white-space:nowrap">Preorder &#8594;</button>
</div>
<p class="pricing-fine reveal" style="margin-top:2.5rem">
Bring your own API keys &nbsp;&middot;&nbsp; Local inference via Ollama (coming) &nbsp;&middot;&nbsp; Neuron Inference - Q3 2026 &nbsp;&middot;&nbsp; Your data stays yours
</p>
<p class="pricing-fine reveal" style="margin-top:1rem;font-size:0.8rem">
Includes 2 devices per plan &nbsp;&middot;&nbsp; Additional devices available at a small extra cost &nbsp;&middot;&nbsp; We&#39;re not greedy
</p>
</div>
</section>
el_section(
"id=\"pricing\" aria-label=\"Pricing\"",
el_div("class=\"container\"", header + grid + banner + fine1 + fine2)
)
}
+141 -100
View File
@@ -5,105 +5,146 @@
// designate a trusted contact that bypasses the default notification
// path when a signal warrants it.
fn safety() -> String {
return <section id="safety" aria-label="Safety" style="padding:8rem 2.5rem;background:var(--bg2)">
<div class="container-lg">
extern fn el_section(attrs: String, children: String) -> String
extern fn el_div(attrs: String, children: String) -> String
extern fn el_span(attrs: String, children: String) -> String
extern fn el_h2(attrs: String, text: String) -> String
extern fn el_p(attrs: String, children: String) -> String
extern fn el_a(href: String, attrs: String, children: String) -> String
extern fn el_em(children: String) -> String
extern fn el_br() -> String
<!-- Intro text -->
<div style="max-width:44rem;margin-bottom:3.5rem">
<div style="display:flex;align-items:center;gap:1.5rem;margin-bottom:2rem">
<div class="navy-line-left" style="width:3rem;flex-shrink:0"></div>
<span class="label reveal" style="color:var(--navy-85)">Safety</span>
</div>
<h2 class="display-lg reveal" style="transition-delay:80ms;margin-bottom:1.5rem">
Safety built in.<br>Not bolted on.
</h2>
<p class="reveal" style="transition-delay:160ms;font-family:var(--body);font-weight:300;font-size:1rem;color:var(--t2);line-height:1.8;margin-bottom:1.25rem">
Most AI products treat safety as a content filter. Block a list of topics, add a disclaimer, call it done. That&#39;s not safety. That&#39;s liability management.
</p>
<p class="reveal" style="transition-delay:220ms;font-family:var(--body);font-weight:300;font-size:1rem;color:var(--t2);line-height:1.8;margin-bottom:1.25rem">
Real safety means thinking about who might need help and what help actually looks like in a crisis. The person closest to you is sometimes the source of the problem. A system that routes every distress signal to your primary contact can alert the very person you need protection from.
</p>
<p class="reveal" style="transition-delay:280ms;font-family:var(--body);font-weight:300;font-size:1rem;color:var(--t2);line-height:1.8">
I built something different. I called it the Hard Bell.
</p>
</div>
<!-- 2x2 card grid -->
<div style="display:grid;grid-template-columns:1fr 1fr;gap:1.5rem;margin-bottom:1.5rem">
<div class="reveal card-dark" style="transition-delay:100ms;padding:1.75rem 2rem;border-left:3px solid rgba(0,82,160,.40)">
<p style="font-family:var(--body);font-size:0.75rem;font-weight:700;letter-spacing:0.14em;text-transform:uppercase;color:var(--navy);margin-bottom:0.75rem">Hard Bell</p>
<p style="font-family:var(--body);font-weight:400;font-size:0.9375rem;color:var(--t1);margin-bottom:0.5rem">A trusted contact the threat can&#39;t intercept</p>
<p style="font-family:var(--body);font-weight:300;font-size:0.875rem;color:var(--t2);line-height:1.7">
Any user can designate a Hard Bell contact - a friend, a relative, a colleague - set up in a calm moment, independent of any shared account. When a signal warrants it, that contact is reached directly. It works the same way for everyone: adult, teen, or child.
</p>
</div>
<div class="reveal card-dark" style="transition-delay:150ms;padding:1.75rem 2rem;border-left:3px solid rgba(0,82,160,.40)">
<p style="font-family:var(--body);font-size:0.75rem;font-weight:700;letter-spacing:0.14em;text-transform:uppercase;color:var(--navy);margin-bottom:0.75rem">Emergency routing</p>
<p style="font-family:var(--body);font-weight:400;font-size:0.9375rem;color:var(--t1);margin-bottom:0.5rem">Emergency services first - not notification</p>
<p style="font-family:var(--body);font-weight:300;font-size:0.875rem;color:var(--t2);line-height:1.7">
Physical danger and crisis signals route to emergency services and crisis lines. Neuron evaluates the content of the signal, not the account type. No one in your contact list can disable or redirect this path.
</p>
</div>
<div class="reveal card-dark" style="transition-delay:200ms;padding:1.75rem 2rem;border-left:3px solid rgba(0,82,160,.40)">
<p style="font-family:var(--body);font-size:0.75rem;font-weight:700;letter-spacing:0.14em;text-transform:uppercase;color:var(--navy);margin-bottom:0.75rem">Family accounts</p>
<p style="font-family:var(--body);font-weight:400;font-size:0.9375rem;color:var(--t1);margin-bottom:0.5rem">Oversight without surveillance</p>
<p style="font-family:var(--body);font-weight:300;font-size:0.875rem;color:var(--t2);line-height:1.7">
For family accounts, parents see what they need to see. A child&#39;s conversations remain private unless a wellbeing signal triggers notification - and even then, the routing logic accounts for the possibility that the parent could be the source of harm.
</p>
<p style="font-family:var(--body);font-weight:500;font-size:0.875rem;color:var(--t1);line-height:1.7;margin-top:0.75rem">
We protect the kids. They come first.
</p>
</div>
<div class="reveal card-dark" style="transition-delay:250ms;padding:1.75rem 2rem;border-left:3px solid rgba(0,82,160,.40)">
<p style="font-family:var(--body);font-size:0.75rem;font-weight:700;letter-spacing:0.14em;text-transform:uppercase;color:var(--navy);margin-bottom:0.75rem">Mandatory reporting</p>
<p style="font-family:var(--body);font-weight:400;font-size:0.9375rem;color:var(--t1);margin-bottom:0.5rem">If real harm is intended, authorities may be contacted</p>
<p style="font-family:var(--body);font-weight:300;font-size:0.875rem;color:var(--t2);line-height:1.7">
If Neuron detects credible, specific indicators of intended harm - to the user or to someone else - emergency services or relevant authorities may be contacted. Every user agrees to this at account creation. You cannot opt out.
</p>
</div>
</div>
<!-- People first card full width -->
<div class="reveal card-dark" style="padding:2rem 2.5rem;border-left:3px solid var(--navy);margin-bottom:1.5rem">
<p style="font-family:var(--body);font-weight:300;font-size:0.9375rem;color:var(--t2);line-height:1.8;margin-bottom:1.25rem">This applies to anyone planning mass harm, a shooting, or serious self-harm. Not just child protection - anyone. The same logic holds: a misunderstanding can be resolved. A person&#39;s life can&#39;t be given back to them.</p>
<p style="font-family:var(--body);font-weight:700;font-size:1.125rem;color:var(--navy);line-height:1.4">People first, always.</p>
</div>
<!-- Statement + stat cards each full width -->
<div style="display:flex;flex-direction:column;gap:1.5rem;margin-bottom:2rem">
<div class="reveal card-dark" style="padding:2rem 2.5rem;border-left:3px solid rgba(0,82,160,.30)">
<p style="font-family:var(--body);font-size:0.75rem;font-weight:700;letter-spacing:0.14em;text-transform:uppercase;color:var(--navy);margin-bottom:0.75rem">Why the routing works this way</p>
<p style="font-family:var(--body);font-weight:300;font-size:0.9375rem;color:var(--t2);line-height:1.8;margin-bottom:0.75rem">According to federal child maltreatment data (HHS, Child Maltreatment 2023), approximately 89% of child abuse victims are maltreated by a parent or caregiver. The emergency contact on file is not always a safe contact.</p>
<p style="font-family:var(--body);font-size:0.75rem;color:var(--t3)">Source: U.S. Department of Health &amp; Human Services, Administration for Children and Families, <em>Child Maltreatment 2023</em>. <a href="https://www.acf.hhs.gov/cb/data-research/child-maltreatment" target="_blank" rel="noopener" style="color:var(--navy)">acf.hhs.gov/cb/data-research/child-maltreatment</a></p>
</div>
<div class="reveal card-dark" style="padding:2rem 2.5rem;border-left:3px solid var(--navy)">
<p style="font-family:var(--body);font-weight:400;font-size:0.9375rem;color:var(--t1);line-height:1.8;margin-bottom:0.75rem">I recognize I will lose business over this. That doesn&#39;t matter to me.</p>
<p style="font-family:var(--body);font-weight:300;font-size:0.9375rem;color:var(--t2);line-height:1.8">This is still local. Neuron, LLC does not see your conversations. When a safety signal fires, the contact is made by your local instance - not by our servers. We are not in the loop. We cannot be. That&#39;s the architecture.</p>
</div>
</div>
<!-- Required block -->
<div class="reveal" style="padding:2rem 2.5rem;border:1px solid rgba(0,82,160,.15);background:rgba(0,82,160,.03)">
<p style="font-family:var(--body);font-weight:300;font-size:0.9375rem;color:var(--t2);line-height:1.8;margin-bottom:1rem">
<strong style="color:var(--t1);font-weight:500">Required before you can use Neuron.</strong> You must designate a Hard Bell contact during setup - before anything else. This is not a settings page you visit later. It happens first.
</p>
<p style="font-family:var(--body);font-weight:300;font-size:0.9375rem;color:var(--t2);line-height:1.8;margin-bottom:1rem">
If you don&#39;t have someone to designate, you can use 988 - the Suicide &amp; Crisis Lifeline - as your Hard Bell contact. The system will accept it. The point is that no one goes in without a line out.
</p>
<p style="font-family:var(--body);font-weight:300;font-size:0.9375rem;color:var(--t2);line-height:1.8">
I&#39;m also establishing a Neuron crisis line - free, 24/7, staffed. Because I recognize that some people don&#39;t have anyone. The technology shouldn&#39;t make that worse. It should be the thing that catches you when nothing else does.
</p>
</div>
</div>
</section>
fn safety_intro() -> String {
let label_row: String = el_div(
"style=\"display:flex;align-items:center;gap:1.5rem;margin-bottom:2rem\"",
el_div("class=\"navy-line-left\" style=\"width:3rem;flex-shrink:0\"", "") +
el_span("class=\"label reveal\" style=\"color:var(--navy-85)\"", "Safety")
)
el_div(
"style=\"max-width:44rem;margin-bottom:3.5rem\"",
label_row +
el_h2(
"class=\"display-lg reveal\" style=\"transition-delay:80ms;margin-bottom:1.5rem\"",
"Safety built in." + el_br() + "Not bolted on."
) +
el_p("class=\"reveal\" style=\"transition-delay:160ms;font-family:var(--body);font-weight:300;font-size:1rem;color:var(--t2);line-height:1.8;margin-bottom:1.25rem\"",
"Most AI products treat safety as a content filter. Block a list of topics, add a disclaimer, call it done. That&#39;s not safety. That&#39;s liability management."
) +
el_p("class=\"reveal\" style=\"transition-delay:220ms;font-family:var(--body);font-weight:300;font-size:1rem;color:var(--t2);line-height:1.8;margin-bottom:1.25rem\"",
"Real safety means thinking about who might need help and what help actually looks like in a crisis. The person closest to you is sometimes the source of the problem. A system that routes every distress signal to your primary contact can alert the very person you need protection from."
) +
el_p("class=\"reveal\" style=\"transition-delay:280ms;font-family:var(--body);font-weight:300;font-size:1rem;color:var(--t2);line-height:1.8\"",
"I built something different. I called it the Hard Bell."
)
)
}
fn safety_cards() -> String {
let card_style: String = "padding:1.75rem 2rem;border-left:3px solid rgba(0,82,160,.40)"
let card1: String = el_div(
"class=\"reveal card-dark\" style=\"transition-delay:100ms;" + card_style + "\"",
el_p("style=\"font-family:var(--body);font-size:0.75rem;font-weight:700;letter-spacing:0.14em;text-transform:uppercase;color:var(--navy);margin-bottom:0.75rem\"", "Hard Bell") +
el_p("style=\"font-family:var(--body);font-weight:400;font-size:0.9375rem;color:var(--t1);margin-bottom:0.5rem\"", "A trusted contact the threat can&#39;t intercept") +
el_p("style=\"font-family:var(--body);font-weight:300;font-size:0.875rem;color:var(--t2);line-height:1.7\"",
"Any user can designate a Hard Bell contact - a friend, a relative, a colleague - set up in a calm moment, independent of any shared account. When a signal warrants it, that contact is reached directly. It works the same way for everyone: adult, teen, or child."
)
)
let card2: String = el_div(
"class=\"reveal card-dark\" style=\"transition-delay:150ms;" + card_style + "\"",
el_p("style=\"font-family:var(--body);font-size:0.75rem;font-weight:700;letter-spacing:0.14em;text-transform:uppercase;color:var(--navy);margin-bottom:0.75rem\"", "Emergency routing") +
el_p("style=\"font-family:var(--body);font-weight:400;font-size:0.9375rem;color:var(--t1);margin-bottom:0.5rem\"", "Emergency services first - not notification") +
el_p("style=\"font-family:var(--body);font-weight:300;font-size:0.875rem;color:var(--t2);line-height:1.7\"",
"Physical danger and crisis signals route to emergency services and crisis lines. Neuron evaluates the content of the signal, not the account type. No one in your contact list can disable or redirect this path."
)
)
let card3: String = el_div(
"class=\"reveal card-dark\" style=\"transition-delay:200ms;" + card_style + "\"",
el_p("style=\"font-family:var(--body);font-size:0.75rem;font-weight:700;letter-spacing:0.14em;text-transform:uppercase;color:var(--navy);margin-bottom:0.75rem\"", "Family accounts") +
el_p("style=\"font-family:var(--body);font-weight:400;font-size:0.9375rem;color:var(--t1);margin-bottom:0.5rem\"", "Oversight without surveillance") +
el_p("style=\"font-family:var(--body);font-weight:300;font-size:0.875rem;color:var(--t2);line-height:1.7\"",
"For family accounts, parents see what they need to see. A child&#39;s conversations remain private unless a wellbeing signal triggers notification - and even then, the routing logic accounts for the possibility that the parent could be the source of harm."
) +
el_p("style=\"font-family:var(--body);font-weight:500;font-size:0.875rem;color:var(--t1);line-height:1.7;margin-top:0.75rem\"",
"We protect the kids. They come first."
)
)
let card4: String = el_div(
"class=\"reveal card-dark\" style=\"transition-delay:250ms;" + card_style + "\"",
el_p("style=\"font-family:var(--body);font-size:0.75rem;font-weight:700;letter-spacing:0.14em;text-transform:uppercase;color:var(--navy);margin-bottom:0.75rem\"", "Mandatory reporting") +
el_p("style=\"font-family:var(--body);font-weight:400;font-size:0.9375rem;color:var(--t1);margin-bottom:0.5rem\"", "If real harm is intended, authorities may be contacted") +
el_p("style=\"font-family:var(--body);font-weight:300;font-size:0.875rem;color:var(--t2);line-height:1.7\"",
"If Neuron detects credible, specific indicators of intended harm - to the user or to someone else - emergency services or relevant authorities may be contacted. Every user agrees to this at account creation. You cannot opt out."
)
)
el_div("style=\"display:grid;grid-template-columns:1fr 1fr;gap:1.5rem;margin-bottom:1.5rem\"",
card1 + card2 + card3 + card4
)
}
fn safety_statements() -> String {
let people_first: String = el_div(
"class=\"reveal card-dark\" style=\"padding:2rem 2.5rem;border-left:3px solid var(--navy);margin-bottom:1.5rem\"",
el_p("style=\"font-family:var(--body);font-weight:300;font-size:0.9375rem;color:var(--t2);line-height:1.8;margin-bottom:1.25rem\"",
"This applies to anyone planning mass harm, a shooting, or serious self-harm. Not just child protection - anyone. The same logic holds: a misunderstanding can be resolved. A person&#39;s life can&#39;t be given back to them."
) +
el_p("style=\"font-family:var(--body);font-weight:700;font-size:1.125rem;color:var(--navy);line-height:1.4\"",
"People first, always."
)
)
let stat_card1: String = el_div(
"class=\"reveal card-dark\" style=\"padding:2rem 2.5rem;border-left:3px solid rgba(0,82,160,.30)\"",
el_p("style=\"font-family:var(--body);font-size:0.75rem;font-weight:700;letter-spacing:0.14em;text-transform:uppercase;color:var(--navy);margin-bottom:0.75rem\"", "Why the routing works this way") +
el_p("style=\"font-family:var(--body);font-weight:300;font-size:0.9375rem;color:var(--t2);line-height:1.8;margin-bottom:0.75rem\"",
"According to federal child maltreatment data (HHS, Child Maltreatment 2023), approximately 89% of child abuse victims are maltreated by a parent or caregiver. The emergency contact on file is not always a safe contact."
) +
el_p("style=\"font-family:var(--body);font-size:0.75rem;color:var(--t3)\"",
"Source: U.S. Department of Health &amp; Human Services, Administration for Children and Families, " +
el_em("Child Maltreatment 2023") +
". " +
el_a("https://www.acf.hhs.gov/cb/data-research/child-maltreatment", "target=\"_blank\" rel=\"noopener\" style=\"color:var(--navy)\"", "acf.hhs.gov/cb/data-research/child-maltreatment")
)
)
let stat_card2: String = el_div(
"class=\"reveal card-dark\" style=\"padding:2rem 2.5rem;border-left:3px solid var(--navy)\"",
el_p("style=\"font-family:var(--body);font-weight:400;font-size:0.9375rem;color:var(--t1);line-height:1.8;margin-bottom:0.75rem\"",
"I recognize I will lose business over this. That doesn&#39;t matter to me."
) +
el_p("style=\"font-family:var(--body);font-weight:300;font-size:0.9375rem;color:var(--t2);line-height:1.8\"",
"This is still local. Neuron, LLC does not see your conversations. When a safety signal fires, the contact is made by your local instance - not by our servers. We are not in the loop. We cannot be. That&#39;s the architecture."
)
)
let statements: String = el_div(
"style=\"display:flex;flex-direction:column;gap:1.5rem;margin-bottom:2rem\"",
stat_card1 + stat_card2
)
let required: String = el_div(
"class=\"reveal\" style=\"padding:2rem 2.5rem;border:1px solid rgba(0,82,160,.15);background:rgba(0,82,160,.03)\"",
el_p("style=\"font-family:var(--body);font-weight:300;font-size:0.9375rem;color:var(--t2);line-height:1.8;margin-bottom:1rem\"",
"<strong style=\"color:var(--t1);font-weight:500\">Required before you can use Neuron.</strong> You must designate a Hard Bell contact during setup - before anything else. This is not a settings page you visit later. It happens first."
) +
el_p("style=\"font-family:var(--body);font-weight:300;font-size:0.9375rem;color:var(--t2);line-height:1.8;margin-bottom:1rem\"",
"If you don&#39;t have someone to designate, you can use 988 - the Suicide &amp; Crisis Lifeline - as your Hard Bell contact. The system will accept it. The point is that no one goes in without a line out."
) +
el_p("style=\"font-family:var(--body);font-weight:300;font-size:0.9375rem;color:var(--t2);line-height:1.8\"",
"I&#39;m also establishing a Neuron crisis line - free, 24/7, staffed. Because I recognize that some people don&#39;t have anyone. The technology shouldn&#39;t make that worse. It should be the thing that catches you when nothing else does."
)
)
people_first + statements + required
}
fn safety() -> String {
el_section(
"id=\"safety\" aria-label=\"Safety\" style=\"padding:8rem 2.5rem;background:var(--bg2)\"",
el_div("class=\"container-lg\"", safety_intro() + safety_cards() + safety_statements())
)
}
+97 -2016
View File
File diff suppressed because it is too large Load Diff
+156 -130
View File
@@ -1,154 +1,180 @@
// components/terms.el - Consumer Terms of Service page.
// Returns complete HTML using the shared page shell from styles.el.
from styles import { page_open, page_close }
from styles import { page_open_seo, page_close }
from nav import { nav }
extern fn el_div(attrs: String, children: String) -> String
extern fn el_span(attrs: String, children: String) -> String
extern fn el_h1(attrs: String, text: String) -> String
extern fn el_h2(attrs: String, text: String) -> String
extern fn el_p(attrs: String, children: String) -> String
extern fn el_a(href: String, attrs: String, children: String) -> String
extern fn el_strong(children: String) -> String
fn terms_page() -> String {
return {page_open()}{nav()}{terms_body()}{page_close()}
page_open_seo(
"Terms of Service — Neuron",
"Read the Neuron Terms of Service. Governs your use of Neuron software and services provided by Neuron, LLC.",
"/legal/terms",
"The Neuron Terms of Service — governing your use of Neuron software and services provided by Neuron, LLC.",
"false"
) + nav() + terms_body() + page_close()
}
fn terms_section_head(num: String, title: String) -> String {
el_div(
"style=\"display:flex;align-items:baseline;gap:1rem;margin-bottom:1.25rem;border-bottom:1px solid var(--border);padding-bottom:0.75rem\"",
el_span("style=\"font-family:var(--body);font-size:0.75rem;font-weight:600;letter-spacing:0.15em;text-transform:uppercase;color:var(--navy-65)\"", "&#167; " + num) +
el_h2("style=\"font-family:var(--head);font-size:1.25rem;font-weight:600;color:var(--t1)\"", title)
)
}
fn terms_p(text: String) -> String {
el_p("style=\"font-family:var(--body);font-weight:300;font-size:0.9375rem;color:var(--t2);line-height:1.8;margin-bottom:1rem\"", text)
}
fn terms_p_last(text: String) -> String {
el_p("style=\"font-family:var(--body);font-weight:300;font-size:0.9375rem;color:var(--t2);line-height:1.8\"", text)
}
fn terms_p_caps(text: String) -> String {
el_p("style=\"font-family:var(--body);font-size:0.875rem;font-weight:600;color:var(--t1);line-height:1.7;margin-bottom:1rem\"", text)
}
fn terms_body() -> String {
return <div style="max-width:720px;margin:0 auto;padding:6rem 2.5rem 8rem">
<div style="margin-bottom:3rem">
<a href="/" style="font-family:var(--body);font-size:0.75rem;font-weight:500;letter-spacing:0.15em;text-transform:uppercase;color:var(--navy);text-decoration:none">&#8592; Neuron</a>
</div>
<div style="margin-bottom:4rem;border-bottom:1px solid var(--border);padding-bottom:3rem">
<p class="label" style="margin-bottom:1rem">Legal</p>
<h1 style="font-family:var(--head);font-size:clamp(2rem,4vw,3rem);font-weight:600;color:var(--t1);margin-bottom:0.75rem;line-height:1.1">Terms of Service</h1>
<p style="font-family:var(--body);font-size:0.875rem;color:var(--t3)">Effective May 1, 2026 &nbsp;&middot;&nbsp; Neuron, LLC</p>
</div>
let back_link: String = el_div(
"style=\"margin-bottom:3rem\"",
el_a("/", "style=\"font-family:var(--body);font-size:0.75rem;font-weight:500;letter-spacing:0.15em;text-transform:uppercase;color:var(--navy);text-decoration:none\"", "&#8592; Neuron")
)
<div style="margin-bottom:3rem">
<div style="display:flex;align-items:baseline;gap:1rem;margin-bottom:1.25rem;border-bottom:1px solid var(--border);padding-bottom:0.75rem">
<span style="font-family:var(--body);font-size:0.75rem;font-weight:600;letter-spacing:0.15em;text-transform:uppercase;color:var(--navy-65)">&#167; 1</span>
<h2 style="font-family:var(--head);font-size:1.25rem;font-weight:600;color:var(--t1)">Using Neuron</h2>
</div>
<p style="font-family:var(--body);font-weight:300;font-size:0.9375rem;color:var(--t2);line-height:1.8;margin-bottom:1rem">Using Neuron means you&#39;ve read these terms and agree to them. That&#39;s it. No buried consent, no dark patterns. If something here doesn&#39;t sit right with you, email us before using the product - we&#39;ll talk through it.</p>
<p style="font-family:var(--body);font-weight:300;font-size:0.9375rem;color:var(--t2);line-height:1.8">These terms cover every version of Neuron: free, Professional, and Founding Member. Enterprise use has its own agreement. If you&#39;re deploying Neuron for an organization, that&#39;s the one you want.</p>
</div>
let page_header: String = el_div(
"style=\"margin-bottom:4rem;border-bottom:1px solid var(--border);padding-bottom:3rem\"",
el_p("class=\"label\" style=\"margin-bottom:1rem\"", "Legal") +
el_h1("style=\"font-family:var(--head);font-size:clamp(2rem,4vw,3rem);font-weight:600;color:var(--t1);margin-bottom:0.75rem;line-height:1.1\"", "Terms of Service") +
el_p("style=\"font-family:var(--body);font-size:0.875rem;color:var(--t3)\"", "Effective May 1, 2026 &nbsp;&middot;&nbsp; Neuron, LLC")
)
<div style="margin-bottom:3rem">
<div style="display:flex;align-items:baseline;gap:1rem;margin-bottom:1.25rem;border-bottom:1px solid var(--border);padding-bottom:0.75rem">
<span style="font-family:var(--body);font-size:0.75rem;font-weight:600;letter-spacing:0.15em;text-transform:uppercase;color:var(--navy-65)">&#167; 2</span>
<h2 style="font-family:var(--head);font-size:1.25rem;font-weight:600;color:var(--t1)">What You Can Do With It</h2>
</div>
<p style="font-family:var(--body);font-weight:300;font-size:0.9375rem;color:var(--t2);line-height:1.8;margin-bottom:1rem">You can install and use Neuron on devices you own or control. That&#39;s the license. Personal, non-transferable.</p>
<p style="font-family:var(--body);font-weight:300;font-size:0.9375rem;color:var(--t2);line-height:1.8;margin-bottom:1rem">Each plan includes <strong style="color:var(--t1);font-weight:500">two devices</strong>. Additional devices are available at a small extra cost. We&#39;re not greedy about it - two covers most people, and if you need more, you can add them.</p>
<p style="font-family:var(--body);font-weight:300;font-size:0.9375rem;color:var(--t2);line-height:1.8;margin-bottom:1rem">The free tier doesn&#39;t expire. Paid licenses are what they say they are. Founding Member licenses are perpetual - you bought it, you own it. I won&#39;t change that.</p>
<p style="font-family:var(--body);font-weight:300;font-size:0.9375rem;color:var(--t2);line-height:1.8;margin-bottom:1rem">You can&#39;t resell it, sublicense it, or redistribute it without asking first. If you want to do something creative with it, ask. The answer might be yes.</p>
<p style="font-family:var(--body);font-weight:300;font-size:0.9375rem;color:var(--t2);line-height:1.8;margin-bottom:1rem"><strong style="color:var(--t1);font-weight:500">License validation.</strong> To use Neuron, the software validates your license against our license server on startup. This applies to all tiers, including Free. The validation confirms your license is active and in good standing. We do not collect your data during this process - the check is limited to license status only.</p>
<p style="font-family:var(--body);font-weight:300;font-size:0.9375rem;color:var(--t2);line-height:1.8"><strong style="color:var(--t1);font-weight:500">Revocation.</strong> If you violate these terms, abuse the system, or engage in conduct that causes harm to other users or to Neuron, LLC, we can revoke your license. This means the software will stop functioning. Revocation is a last resort - we will contact you first when possible. But it is a real consequence, and you should understand that when you agree to use the product.</p>
</div>
let s1: String = el_div(
"style=\"margin-bottom:3rem\"",
terms_section_head("1", "Using Neuron") +
terms_p("Using Neuron means you&#39;ve read these terms and agree to them. That&#39;s it. No buried consent, no dark patterns. If something here doesn&#39;t sit right with you, email us before using the product - we&#39;ll talk through it.") +
terms_p_last("These terms cover every version of Neuron: free, Professional, and Founding Member. Enterprise use has its own agreement. If you&#39;re deploying Neuron for an organization, that&#39;s the one you want.")
)
<div style="margin-bottom:3rem">
<div style="display:flex;align-items:baseline;gap:1rem;margin-bottom:1.25rem;border-bottom:1px solid var(--border);padding-bottom:0.75rem">
<span style="font-family:var(--body);font-size:0.75rem;font-weight:600;letter-spacing:0.15em;text-transform:uppercase;color:var(--navy-65)">&#167; 3</span>
<h2 style="font-family:var(--head);font-size:1.25rem;font-weight:600;color:var(--t1)">Your Data</h2>
</div>
<p style="font-family:var(--body);font-weight:300;font-size:0.9375rem;color:var(--t2);line-height:1.8;margin-bottom:1rem">Your memory, your conversations, your context - all of it lives on your device. I don&#39;t have access to it. I don&#39;t collect it. It doesn&#39;t move in the course of normal use.</p>
<p style="font-family:var(--body);font-weight:300;font-size:0.9375rem;color:var(--t2);line-height:1.8;margin-bottom:1rem">When you turn on network features - sync, relay, collaborative tools - only what those features need to run gets transmitted. Not your memory, not your conversations, unless you explicitly turn on something that shares them with people you&#39;ve chosen. I don&#39;t sell that data. I don&#39;t use it for anything except running the feature you asked for. Turning on a network feature is your consent to that.</p>
<p style="font-family:var(--body);font-weight:300;font-size:0.9375rem;color:var(--t2);line-height:1.8;margin-bottom:1rem">Network features route data through Neuron&#39;s messaging backplane. That data is encrypted end-to-end - we cannot read it - and is not stored or retained after transmission. Participating in the network is your consent to this transient routing.</p>
<p style="font-family:var(--body);font-weight:300;font-size:0.9375rem;color:var(--t2);line-height:1.8">Your data is yours. Nothing in these terms gives me any claim to your content, your memory, or anything you create using Neuron.</p>
</div>
let s2: String = el_div(
"style=\"margin-bottom:3rem\"",
terms_section_head("2", "What You Can Do With It") +
terms_p("You can install and use Neuron on devices you own or control. That&#39;s the license. Personal, non-transferable.") +
terms_p("Each plan includes " + "<strong style=\"color:var(--t1);font-weight:500\">two devices</strong>" + ". Additional devices are available at a small extra cost. We&#39;re not greedy about it - two covers most people, and if you need more, you can add them.") +
terms_p("The free tier doesn&#39;t expire. Paid licenses are what they say they are. Founding Member licenses are perpetual - you bought it, you own it. I won&#39;t change that.") +
terms_p("You can&#39;t resell it, sublicense it, or redistribute it without asking first. If you want to do something creative with it, ask. The answer might be yes.") +
terms_p("<strong style=\"color:var(--t1);font-weight:500\">License validation.</strong> To use Neuron, the software validates your license against our license server on startup. This applies to all tiers, including Free. The validation confirms your license is active and in good standing. We do not collect your data during this process - the check is limited to license status only.") +
terms_p_last("<strong style=\"color:var(--t1);font-weight:500\">Revocation.</strong> If you violate these terms, abuse the system, or engage in conduct that causes harm to other users or to Neuron, LLC, we can revoke your license. This means the software will stop functioning. Revocation is a last resort - we will contact you first when possible. But it is a real consequence, and you should understand that when you agree to use the product.")
)
<div style="margin-bottom:3rem">
<div style="display:flex;align-items:baseline;gap:1rem;margin-bottom:1.25rem;border-bottom:1px solid var(--border);padding-bottom:0.75rem">
<span style="font-family:var(--body);font-size:0.75rem;font-weight:600;letter-spacing:0.15em;text-transform:uppercase;color:var(--navy-65)">&#167; 4</span>
<h2 style="font-family:var(--head);font-size:1.25rem;font-weight:600;color:var(--t1)">Inference</h2>
</div>
<p style="font-family:var(--body);font-weight:300;font-size:0.9375rem;color:var(--t2);line-height:1.8;margin-bottom:1rem">Neuron routes inference to whatever provider you configure - your own keys for OpenAI, Anthropic, Grok, any compatible endpoint. When you use a third-party provider, your prompts go through their infrastructure. Their terms apply. I don&#39;t control what they do with it.</p>
<p style="font-family:var(--body);font-weight:300;font-size:0.9375rem;color:var(--t2);line-height:1.8">Neuron Inference - our own inference layer, launching Q3 2026 - will not store your requests, will not train on them, and will not share them. A separate data addendum will cover the details when it launches.</p>
</div>
let s3: String = el_div(
"style=\"margin-bottom:3rem\"",
terms_section_head("3", "Your Data") +
terms_p("Your memory, your conversations, your context - all of it lives on your device. I don&#39;t have access to it. I don&#39;t collect it. It doesn&#39;t move in the course of normal use.") +
terms_p("When you turn on network features - sync, relay, collaborative tools - only what those features need to run gets transmitted. Not your memory, not your conversations, unless you explicitly turn on something that shares them with people you&#39;ve chosen. I don&#39;t sell that data. I don&#39;t use it for anything except running the feature you asked for. Turning on a network feature is your consent to that.") +
terms_p("Network features route data through Neuron&#39;s messaging backplane. That data is encrypted end-to-end - we cannot read it - and is not stored or retained after transmission. Participating in the network is your consent to this transient routing.") +
terms_p_last("Your data is yours. Nothing in these terms gives me any claim to your content, your memory, or anything you create using Neuron.")
)
<div style="margin-bottom:3rem">
<div style="display:flex;align-items:baseline;gap:1rem;margin-bottom:1.25rem;border-bottom:1px solid var(--border);padding-bottom:0.75rem">
<span style="font-family:var(--body);font-size:0.75rem;font-weight:600;letter-spacing:0.15em;text-transform:uppercase;color:var(--navy-65)">&#167; 5</span>
<h2 style="font-family:var(--head);font-size:1.25rem;font-weight:600;color:var(--t1)">The Network Layer</h2>
</div>
<p style="font-family:var(--body);font-weight:300;font-size:0.9375rem;color:var(--t2);line-height:1.8;margin-bottom:1rem">Neuron includes access to a proprietary distributed network infrastructure (Patent Pending: US Provisional 64/036,821). Its architecture, operational logic, and internal protocols are trade secrets. What matters here is what it means for you.</p>
<p style="font-family:var(--body);font-weight:300;font-size:0.9375rem;color:var(--t2);line-height:1.8;margin-bottom:1rem">The Neuron AI you interact with develops over time - its responses and capabilities change as the system progresses. This is by design. By using Neuron, you acknowledge that: (a) AI responses may change as the system develops; (b) Neuron, LLC makes no warranty as to the consistency, stability, or predictability of AI outputs at any given point in its development; and (c) AI outputs are a function of development state, not a fixed specification.</p>
<p style="font-family:var(--body);font-weight:300;font-size:0.9375rem;color:var(--t2);line-height:1.8;margin-bottom:1rem">When you enable network features, your interactions may be processed by or through Neuron&#39;s distributed network infrastructure. All such data is encrypted in transit. Neuron, LLC cannot read, inspect, retain, or produce the contents of network traffic as an architectural constraint - not merely as a policy. Because we cannot read network traffic, <span style="font-weight:500;color:var(--t1)">Neuron, LLC is not liable for any content, output, or consequence arising from data processed within the network layer</span>, including outputs produced during system development or content you introduce that is processed by the network.</p>
<p style="font-family:var(--body);font-weight:300;font-size:0.9375rem;color:var(--t2);line-height:1.8;margin-bottom:1rem">You agree to indemnify and hold harmless Neuron, LLC, its affiliates, and its officers from any claims, damages, or legal proceedings arising from: (a) your reliance on AI outputs during active development periods; (b) content you originate and introduce through network features; (c) your violation of applicable law in connection with network participation; or (d) any attempt to reverse-engineer, probe, circumvent, or replicate proprietary network architecture or system mechanisms.</p>
<p style="font-family:var(--body);font-weight:300;font-size:0.9375rem;color:var(--t2);line-height:1.8">The network layer is decentralized by design. Neuron, LLC does not guarantee its availability, continuity, or performance, and may modify, suspend, or discontinue network features at any time. Network participation does not create an agency relationship between participants, and no participant acts on behalf of Neuron, LLC in any capacity.</p>
</div>
let s4: String = el_div(
"style=\"margin-bottom:3rem\"",
terms_section_head("4", "Inference") +
terms_p("Neuron routes inference to whatever provider you configure - your own keys for OpenAI, Anthropic, Grok, any compatible endpoint. When you use a third-party provider, your prompts go through their infrastructure. Their terms apply. I don&#39;t control what they do with it.") +
terms_p_last("Neuron Inference - our own inference layer, launching Q3 2026 - will not store your requests, will not train on them, and will not share them. A separate data addendum will cover the details when it launches.")
)
<div style="margin-bottom:3rem">
<div style="display:flex;align-items:baseline;gap:1rem;margin-bottom:1.25rem;border-bottom:1px solid var(--border);padding-bottom:0.75rem">
<span style="font-family:var(--body);font-size:0.75rem;font-weight:600;letter-spacing:0.15em;text-transform:uppercase;color:var(--navy-65)">&#167; 6</span>
<h2 style="font-family:var(--head);font-size:1.25rem;font-weight:600;color:var(--t1)">What You Can&#39;t Do</h2>
</div>
<p style="font-family:var(--body);font-weight:300;font-size:0.9375rem;color:var(--t2);line-height:1.8;margin-bottom:1rem">Don&#39;t use Neuron to generate content designed to harm people, defraud anyone, or break the law.</p>
<p style="font-family:var(--body);font-weight:300;font-size:0.9375rem;color:var(--t2);line-height:1.8;margin-bottom:1rem">Don&#39;t try to reverse-engineer the licensing or security mechanisms.</p>
<p style="font-family:var(--body);font-weight:300;font-size:0.9375rem;color:var(--t2);line-height:1.8;margin-bottom:1rem">Don&#39;t extract or resell proprietary components.</p>
<p style="font-family:var(--body);font-weight:300;font-size:0.9375rem;color:var(--t2);line-height:1.8">If you violate these, your license can be revoked. That means the software stops working. We can also suspend access to cloud services and the network layer. This applies to all tiers - there is no free pass because your tier is free.</p>
</div>
let s5: String = el_div(
"style=\"margin-bottom:3rem\"",
terms_section_head("5", "The Network Layer") +
terms_p("Neuron includes access to a proprietary distributed network infrastructure (Patent Pending: US Provisional 64/036,821). Its architecture, operational logic, and internal protocols are trade secrets. What matters here is what it means for you.") +
terms_p("The Neuron AI you interact with develops over time - its responses and capabilities change as the system progresses. This is by design. By using Neuron, you acknowledge that: (a) AI responses may change as the system develops; (b) Neuron, LLC makes no warranty as to the consistency, stability, or predictability of AI outputs at any given point in its development; and (c) AI outputs are a function of development state, not a fixed specification.") +
terms_p("When you enable network features, your interactions may be processed by or through Neuron&#39;s distributed network infrastructure. All such data is encrypted in transit. Neuron, LLC cannot read, inspect, retain, or produce the contents of network traffic as an architectural constraint - not merely as a policy. Because we cannot read network traffic, " + "<span style=\"font-weight:500;color:var(--t1)\">Neuron, LLC is not liable for any content, output, or consequence arising from data processed within the network layer</span>" + ", including outputs produced during system development or content you introduce that is processed by the network.") +
terms_p("You agree to indemnify and hold harmless Neuron, LLC, its affiliates, and its officers from any claims, damages, or legal proceedings arising from: (a) your reliance on AI outputs during active development periods; (b) content you originate and introduce through network features; (c) your violation of applicable law in connection with network participation; or (d) any attempt to reverse-engineer, probe, circumvent, or replicate proprietary network architecture or system mechanisms.") +
terms_p_last("The network layer is decentralized by design. Neuron, LLC does not guarantee its availability, continuity, or performance, and may modify, suspend, or discontinue network features at any time. Network participation does not create an agency relationship between participants, and no participant acts on behalf of Neuron, LLC in any capacity.")
)
<div style="margin-bottom:3rem">
<div style="display:flex;align-items:baseline;gap:1rem;margin-bottom:1.25rem;border-bottom:1px solid var(--border);padding-bottom:0.75rem">
<span style="font-family:var(--body);font-size:0.75rem;font-weight:600;letter-spacing:0.15em;text-transform:uppercase;color:var(--navy-65)">&#167; 7</span>
<h2 style="font-family:var(--head);font-size:1.25rem;font-weight:600;color:var(--t1)">Ownership</h2>
</div>
<p style="font-family:var(--body);font-weight:300;font-size:0.9375rem;color:var(--t2);line-height:1.8;margin-bottom:1rem">Neuron and everything in it is my intellectual property. Six patents and counting. These terms don&#39;t transfer any of that to you.</p>
<p style="font-family:var(--body);font-weight:300;font-size:0.9375rem;color:var(--t2);line-height:1.8">Everything you create using Neuron - outputs, memory nodes, artifacts - is yours. I make no claim to it. Not now, not ever.</p>
</div>
let s6: String = el_div(
"style=\"margin-bottom:3rem\"",
terms_section_head("6", "What You Can&#39;t Do") +
terms_p("Don&#39;t use Neuron to generate content designed to harm people, defraud anyone, or break the law.") +
terms_p("Don&#39;t try to reverse-engineer the licensing or security mechanisms.") +
terms_p("Don&#39;t extract or resell proprietary components.") +
terms_p_last("If you violate these, your license can be revoked. That means the software stops working. We can also suspend access to cloud services and the network layer. This applies to all tiers - there is no free pass because your tier is free.")
)
<div style="margin-bottom:3rem">
<div style="display:flex;align-items:baseline;gap:1rem;margin-bottom:1.25rem;border-bottom:1px solid var(--border);padding-bottom:0.75rem">
<span style="font-family:var(--body);font-size:0.75rem;font-weight:600;letter-spacing:0.15em;text-transform:uppercase;color:var(--navy-65)">&#167; 8</span>
<h2 style="font-family:var(--head);font-size:1.25rem;font-weight:600;color:var(--t1)">No Warranty</h2>
</div>
<p style="font-family:var(--body);font-size:0.875rem;font-weight:600;color:var(--t1);line-height:1.7;margin-bottom:1rem">THE SOFTWARE IS PROVIDED &#34;AS IS.&#34; NEURON, LLC MAKES NO WARRANTIES, EXPRESS OR IMPLIED, INCLUDING WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, OR NON-INFRINGEMENT.</p>
<p style="font-family:var(--body);font-weight:300;font-size:0.9375rem;color:var(--t2);line-height:1.8">Software has bugs. Security has limits. Use your judgment. You use Neuron at your own risk.</p>
</div>
let s7: String = el_div(
"style=\"margin-bottom:3rem\"",
terms_section_head("7", "Ownership") +
terms_p("Neuron and everything in it is my intellectual property. Six patents and counting. These terms don&#39;t transfer any of that to you.") +
terms_p_last("Everything you create using Neuron - outputs, memory nodes, artifacts - is yours. I make no claim to it. Not now, not ever.")
)
<div style="margin-bottom:3rem">
<div style="display:flex;align-items:baseline;gap:1rem;margin-bottom:1.25rem;border-bottom:1px solid var(--border);padding-bottom:0.75rem">
<span style="font-family:var(--body);font-size:0.75rem;font-weight:600;letter-spacing:0.15em;text-transform:uppercase;color:var(--navy-65)">&#167; 9</span>
<h2 style="font-family:var(--head);font-size:1.25rem;font-weight:600;color:var(--t1)">Liability Cap</h2>
</div>
<p style="font-family:var(--body);font-size:0.875rem;font-weight:600;color:var(--t1);line-height:1.7;margin-bottom:1rem">TO THE EXTENT PERMITTED BY LAW, NEURON, LLC IS NOT LIABLE FOR INDIRECT, INCIDENTAL, SPECIAL, CONSEQUENTIAL, OR PUNITIVE DAMAGES ARISING FROM YOUR USE OF THE SOFTWARE.</p>
<p style="font-family:var(--body);font-weight:300;font-size:0.9375rem;color:var(--t2);line-height:1.8">If something goes wrong and you have a claim against me, the most I owe you is what you paid in the twelve months before it happened. That&#39;s the cap.</p>
</div>
let s8: String = el_div(
"style=\"margin-bottom:3rem\"",
terms_section_head("8", "No Warranty") +
terms_p_caps("THE SOFTWARE IS PROVIDED &#34;AS IS.&#34; NEURON, LLC MAKES NO WARRANTIES, EXPRESS OR IMPLIED, INCLUDING WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, OR NON-INFRINGEMENT.") +
terms_p_last("Software has bugs. Security has limits. Use your judgment. You use Neuron at your own risk.")
)
<div style="margin-bottom:3rem">
<div style="display:flex;align-items:baseline;gap:1rem;margin-bottom:1.25rem;border-bottom:1px solid var(--border);padding-bottom:0.75rem">
<span style="font-family:var(--body);font-size:0.75rem;font-weight:600;letter-spacing:0.15em;text-transform:uppercase;color:var(--navy-65)">&#167; 10</span>
<h2 style="font-family:var(--head);font-size:1.25rem;font-weight:600;color:var(--t1)">Changes</h2>
</div>
<p style="font-family:var(--body);font-weight:300;font-size:0.9375rem;color:var(--t2);line-height:1.8;margin-bottom:1rem">I&#39;ll update these terms when I need to. Material changes get announced on the site and, where I have your email, directly. Continued use after a change means you accept the new terms.</p>
<p style="font-family:var(--body);font-weight:300;font-size:0.9375rem;color:var(--t2);line-height:1.8">Founding Member licenses are locked. I won&#39;t retroactively change what you signed up for.</p>
</div>
let s9: String = el_div(
"style=\"margin-bottom:3rem\"",
terms_section_head("9", "Liability Cap") +
terms_p_caps("TO THE EXTENT PERMITTED BY LAW, NEURON, LLC IS NOT LIABLE FOR INDIRECT, INCIDENTAL, SPECIAL, CONSEQUENTIAL, OR PUNITIVE DAMAGES ARISING FROM YOUR USE OF THE SOFTWARE.") +
terms_p_last("If something goes wrong and you have a claim against me, the most I owe you is what you paid in the twelve months before it happened. That&#39;s the cap.")
)
<div style="margin-bottom:3rem">
<div style="display:flex;align-items:baseline;gap:1rem;margin-bottom:1.25rem;border-bottom:1px solid var(--border);padding-bottom:0.75rem">
<span style="font-family:var(--body);font-size:0.75rem;font-weight:600;letter-spacing:0.15em;text-transform:uppercase;color:var(--navy-65)">&#167; 11</span>
<h2 style="font-family:var(--head);font-size:1.25rem;font-weight:600;color:var(--t1)">Children and Family Accounts</h2>
</div>
<p style="font-family:var(--body);font-weight:300;font-size:0.9375rem;color:var(--t2);line-height:1.8;margin-bottom:1rem">Children 13 and over may use Neuron independently. Children under 13 may only use Neuron as part of a family account established by a parent or legal guardian. By setting up a family account that includes a minor, the parent or guardian provides verifiable consent for the child&#39;s use and accepts these terms on the child&#39;s behalf.</p>
<p style="font-family:var(--body);font-weight:300;font-size:0.9375rem;color:var(--t2);line-height:1.8;margin-bottom:1rem">When a child&#39;s Neuron instance is part of a family account, certain information - including usage activity and relevant context - may be shared from the child&#39;s local instance to the parent&#39;s local instance. This sharing happens device-to-device and does not pass through or get stored on Neuron&#39;s servers.</p>
let s10: String = el_div(
"style=\"margin-bottom:3rem\"",
terms_section_head("10", "Changes") +
terms_p("I&#39;ll update these terms when I need to. Material changes get announced on the site and, where I have your email, directly. Continued use after a change means you accept the new terms.") +
terms_p_last("Founding Member licenses are locked. I won&#39;t retroactively change what you signed up for.")
)
<div style="margin:1.5rem 0;padding:1.5rem;border-left:3px solid rgba(0,82,160,.35);background:rgba(0,82,160,.04)">
<p style="font-family:var(--body);font-weight:600;font-size:0.875rem;color:var(--t1);margin-bottom:0.875rem;letter-spacing:0.04em">Hard Bell - Required acknowledgment for family accounts</p>
<p style="font-family:var(--body);font-weight:300;font-size:0.875rem;color:var(--t2);line-height:1.8;margin-bottom:0.875rem">Federal data shows approximately 89% of child maltreatment is perpetrated by a parent or caregiver (HHS, Child Maltreatment 2023). The person listed as the primary contact is not always a safe contact. This system is built with that reality in mind.</p>
<p style="font-family:var(--body);font-weight:400;font-size:0.875rem;color:var(--t1);line-height:1.8">When you create a family account that includes a minor, you are agreeing unconditionally to the following: <strong>if a safety signal is triggered by a child&#39;s account, the parent or guardian named on the family account will not be the first contact reached.</strong> Emergency services and independently designated trusted contacts are notified first. This is not a setting that can be changed. By creating a family account, you accept this structure.</p>
</div>
<p style="font-family:var(--body);font-weight:500;font-size:0.9375rem;color:var(--t1);line-height:1.8;margin-bottom:0.75rem">Mandatory reporting - applies to all accounts.</p>
<p style="font-family:var(--body);font-weight:300;font-size:0.9375rem;color:var(--t2);line-height:1.8;margin-bottom:1rem">By creating any Neuron account, you acknowledge and agree that: <strong style="color:var(--t1);font-weight:500">if Neuron detects credible, specific indicators of intended harm to a person - including harm to the account holder themselves - emergency services or relevant authorities may be contacted.</strong> This is not limited to family accounts. It applies to every user. This is not a privacy violation. It is the only honest answer to what a system that knows you this well is obligated to do when someone is in genuine danger. You cannot opt out of this. If you are unwilling to agree, do not create an account.</p>
<p style="font-family:var(--body);font-weight:300;font-size:0.9375rem;color:var(--t2);line-height:1.8">Parents control what is visible to them through their local instance and can revoke family account access at any time. If you believe a child is using Neuron outside of a family account, contact legal@neurontechnologies.ai and we will address it promptly.</p>
</div>
let hard_bell_block: String = el_div(
"style=\"margin:1.5rem 0;padding:1.5rem;border-left:3px solid rgba(0,82,160,.35);background:rgba(0,82,160,.04)\"",
el_p("style=\"font-family:var(--body);font-weight:600;font-size:0.875rem;color:var(--t1);margin-bottom:0.875rem;letter-spacing:0.04em\"", "Hard Bell - Required acknowledgment for family accounts") +
terms_p("Federal data shows approximately 89% of child maltreatment is perpetrated by a parent or caregiver (HHS, Child Maltreatment 2023). The person listed as the primary contact is not always a safe contact. This system is built with that reality in mind.") +
el_p("style=\"font-family:var(--body);font-weight:400;font-size:0.875rem;color:var(--t1);line-height:1.8\"",
"When you create a family account that includes a minor, you are agreeing unconditionally to the following: " +
"<strong>if a safety signal is triggered by a child&#39;s account, the parent or guardian named on the family account will not be the first contact reached.</strong>" +
" Emergency services and independently designated trusted contacts are notified first. This is not a setting that can be changed. By creating a family account, you accept this structure."
)
)
<div style="margin-bottom:3rem">
<div style="display:flex;align-items:baseline;gap:1rem;margin-bottom:1.25rem;border-bottom:1px solid var(--border);padding-bottom:0.75rem">
<span style="font-family:var(--body);font-size:0.75rem;font-weight:600;letter-spacing:0.15em;text-transform:uppercase;color:var(--navy-65)">&#167; 12</span>
<h2 style="font-family:var(--head);font-size:1.25rem;font-weight:600;color:var(--t1)">Governing Law</h2>
</div>
<p style="font-family:var(--body);font-weight:300;font-size:0.9375rem;color:var(--t2);line-height:1.8">These terms are governed by the laws of Delaware. Disputes go to the courts of Delaware.</p>
</div>
let s11: String = el_div(
"style=\"margin-bottom:3rem\"",
terms_section_head("11", "Children and Family Accounts") +
terms_p("Children 13 and over may use Neuron independently. Children under 13 may only use Neuron as part of a family account established by a parent or legal guardian. By setting up a family account that includes a minor, the parent or guardian provides verifiable consent for the child&#39;s use and accepts these terms on the child&#39;s behalf.") +
terms_p("When a child&#39;s Neuron instance is part of a family account, certain information - including usage activity and relevant context - may be shared from the child&#39;s local instance to the parent&#39;s local instance. This sharing happens device-to-device and does not pass through or get stored on Neuron&#39;s servers.") +
hard_bell_block +
el_p("style=\"font-family:var(--body);font-weight:500;font-size:0.9375rem;color:var(--t1);line-height:1.8;margin-bottom:0.75rem\"", "Mandatory reporting - applies to all accounts.") +
terms_p("By creating any Neuron account, you acknowledge and agree that: " + "<strong style=\"color:var(--t1);font-weight:500\">if Neuron detects credible, specific indicators of intended harm to a person - including harm to the account holder themselves - emergency services or relevant authorities may be contacted.</strong>" + " This is not limited to family accounts. It applies to every user. This is not a privacy violation. It is the only honest answer to what a system that knows you this well is obligated to do when someone is in genuine danger. You cannot opt out of this. If you are unwilling to agree, do not create an account.") +
terms_p_last("Parents control what is visible to them through their local instance and can revoke family account access at any time. If you believe a child is using Neuron outside of a family account, contact legal@neurontechnologies.ai and we will address it promptly.")
)
<div style="margin-top:4rem;padding-top:2rem;border-top:1px solid var(--border);display:flex;gap:2rem;flex-wrap:wrap">
<a href="/" style="font-family:var(--body);font-size:0.8125rem;color:var(--navy);text-decoration:none">&larr; Home</a>
<a href="/legal/enterprise-terms" style="font-family:var(--body);font-size:0.8125rem;color:var(--navy);text-decoration:none">Enterprise Agreement &rarr;</a>
</div>
</div>
let s12: String = el_div(
"style=\"margin-bottom:3rem\"",
terms_section_head("12", "Governing Law") +
terms_p_last("These terms are governed by the laws of Delaware. Disputes go to the courts of Delaware.")
)
let footer_links: String = el_div(
"style=\"margin-top:4rem;padding-top:2rem;border-top:1px solid var(--border);display:flex;gap:2rem;flex-wrap:wrap\"",
el_a("/", "style=\"font-family:var(--body);font-size:0.8125rem;color:var(--navy);text-decoration:none\"", "&larr; Home") +
el_a("/legal/enterprise-terms", "style=\"font-family:var(--body);font-size:0.8125rem;color:var(--navy);text-decoration:none\"", "Enterprise Agreement &rarr;")
)
el_div(
"style=\"max-width:720px;margin:0 auto;padding:6rem 2.5rem 8rem\"",
back_link +
page_header +
s1 + s2 + s3 + s4 + s5 + s6 + s7 + s8 + s9 + s10 + s11 + s12 +
footer_links
)
}
+1 -1
View File
@@ -2,5 +2,5 @@
// Removed section taken out per design decision.
fn viral() -> String {
return ""
""
}
+150
View File
@@ -0,0 +1,150 @@
import { test, expect } from '@playwright/test';
const BASE = process.env.BASE_URL || 'https://marketing-stage-r4tfklscwq-uc.a.run.app';
const get = (path: string, headers: Record<string, string> = {}) =>
fetch(`${BASE}${path}`, { headers });
// ── /api/health ───────────────────────────────────────────────────────────────
test('/api/health — returns 200 with status:ok', async () => {
const r = await get('/api/health');
expect(r.status).toBe(200);
const body = await r.json() as Record<string, string>;
expect(body.status).toBe('ok');
expect(body.service).toBe('neuron-web');
});
test('/api/health — content-type is application/json', async () => {
const r = await get('/api/health');
expect(r.headers.get('content-type')).toContain('application/json');
});
// ── /api/founding-count ───────────────────────────────────────────────────────
test('/api/founding-count — returns numeric fields', async () => {
const r = await get('/api/founding-count');
expect(r.status).toBe(200);
const body = await r.json() as Record<string, number>;
expect(typeof body.sold).toBe('number');
expect(typeof body.total).toBe('number');
expect(typeof body.remaining).toBe('number');
// Invariants
expect(body.total).toBe(1000);
expect(body.sold).toBeGreaterThanOrEqual(0);
expect(body.remaining).toBe(body.total - body.sold);
});
// ── /api/supabase-config ──────────────────────────────────────────────────────
// Requires a permitted Origin. See security.test.ts for CORS tests.
test('/api/supabase-config — returns url and anon_key for allowed origin', async () => {
const r = await get('/api/supabase-config', { Origin: 'https://neurontechnologies.ai' });
expect(r.status).toBe(200);
const body = await r.json() as Record<string, string>;
expect(body.url).toMatch(/supabase\.co/);
expect(typeof body.anon_key).toBe('string');
expect(body.anon_key.length).toBeGreaterThan(20);
});
test('/api/supabase-config — anon_key is a valid JWT shape', async () => {
const r = await get('/api/supabase-config', { Origin: 'https://neurontechnologies.ai' });
const body = await r.json() as Record<string, string>;
// Supabase anon key is a JWT: three base64 segments separated by dots
const parts = body.anon_key.split('.');
expect(parts).toHaveLength(3);
});
// ── /sitemap.xml ──────────────────────────────────────────────────────────────
test('/sitemap.xml — returns valid XML with production URLs', async () => {
const r = await get('/sitemap.xml');
expect(r.status).toBe(200);
expect(r.headers.get('content-type')).toContain('xml');
const text = await r.text();
expect(text).toContain('<urlset');
expect(text).toContain('neurontechnologies.ai');
// Must not leak stage URL
expect(text).not.toContain('run.app');
expect(text).not.toContain('stage');
});
test('/sitemap.xml — includes all major pages', async () => {
const r = await get('/sitemap.xml');
const text = await r.text();
expect(text).toContain('neurontechnologies.ai/');
expect(text).toContain('neurontechnologies.ai/about');
expect(text).toContain('neurontechnologies.ai/legal/terms');
expect(text).toContain('neurontechnologies.ai/legal/enterprise-terms');
});
// ── /robots.txt ───────────────────────────────────────────────────────────────
test('/robots.txt — accessible with correct directives', async () => {
const r = await get('/robots.txt');
expect(r.status).toBe(200);
const text = await r.text();
expect(text).toContain('User-agent');
// Private paths are disallowed
expect(text).toContain('Disallow: /checkout');
expect(text).toContain('Disallow: /account');
expect(text).toContain('Disallow: /api/');
// Sitemap link points to production
expect(text).toContain('Sitemap: https://neurontechnologies.ai/sitemap.xml');
});
// ── /llms.txt ─────────────────────────────────────────────────────────────────
test('/llms.txt — accessible', async () => {
const r = await get('/llms.txt');
expect(r.status).toBe(200);
const text = await r.text();
expect(text.length).toBeGreaterThan(0);
});
// ── 404 handling ─────────────────────────────────────────────────────────────
test('Unknown route returns 404', async () => {
const r = await get('/this-route-xyz-does-not-exist-abc123');
expect(r.status).toBe(404);
});
// ── /api/webhooks/stripe — POST-only, requires valid signature ────────────────
test('/api/webhooks/stripe — rejects missing Stripe-Signature with 400', async () => {
const r = await fetch(`${BASE}/api/webhooks/stripe`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ type: 'payment_intent.succeeded' }),
});
expect(r.status).toBe(400);
});
// ── /api/demo — POST only, auth-gated ────────────────────────────────────────
test('/api/demo — missing access_token returns auth_required', async () => {
const r = await fetch(`${BASE}/api/demo`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ message: 'hello' }),
});
const body = await r.json() as Record<string, unknown>;
expect(body.auth_required).toBe(true);
});
// ── /api/soul-health — internal gate ─────────────────────────────────────────
// The probe responses embedded in the JSON body may contain literal newlines
// (control characters), so we test via text matching, not JSON.parse.
test('/api/soul-health — 404 without X-Internal header', async () => {
const r = await get('/api/soul-health');
expect(r.status).toBe(404);
});
test('/api/soul-health — 200 with X-Internal: true, body contains soul_url', async () => {
const r = await get('/api/soul-health', { 'X-Internal': 'true' });
expect(r.status).toBe(200);
const text = await r.text();
expect(text).toContain('"soul_url"');
expect(text).toMatch(/soul_url.*https?:\/\//);
});
+214
View File
@@ -0,0 +1,214 @@
import { test, expect } from '@playwright/test';
const BASE = process.env.BASE_URL || 'https://marketing-stage-r4tfklscwq-uc.a.run.app';
async function get(path: string, headers: Record<string, string> = {}) {
return fetch(`${BASE}${path}`, { headers, redirect: 'manual' });
}
async function post(path: string, body: unknown, headers: Record<string, string> = {}) {
return fetch(`${BASE}${path}`, {
method: 'POST',
headers: { 'Content-Type': 'application/json', ...headers },
body: JSON.stringify(body),
});
}
// ── Security headers ──────────────────────────────────────────────────────────
// All HTML pages and API responses carry the full security header suite.
// The El runtime's handle_request wrapper applies sec_headers_json() to every
// response, so we can assert the same set on both HTML pages and JSON APIs.
test.describe('Security headers', () => {
const htmlPages = ['/', '/about', '/checkout?plan=professional'];
for (const path of htmlPages) {
test(`HTML ${path} — required security headers present`, async () => {
const r = await get(path);
expect(r.headers.get('x-content-type-options')).toBe('nosniff');
expect(r.headers.get('x-frame-options')).toMatch(/DENY|SAMEORIGIN/i);
expect(r.headers.get('referrer-policy')).toBeTruthy();
expect(r.headers.get('content-security-policy')).toBeTruthy();
});
}
test('API responses carry x-content-type-options', async () => {
const r = await get('/api/health');
expect(r.headers.get('x-content-type-options')).toBe('nosniff');
});
test('permissions-policy header is present', async () => {
const r = await get('/');
expect(r.headers.get('permissions-policy')).toBeTruthy();
});
});
// ── CORS enforcement on /api/supabase-config ──────────────────────────────────
// This endpoint enforces an explicit origin allowlist:
// - empty Origin (server-side / curl): BLOCKED (403)
// - https://neurontechnologies.ai: ALLOWED
// - https://www.neurontechnologies.ai: ALLOWED
// - http://localhost:*: ALLOWED (dev)
// - anything else (e.g. evil.com): BLOCKED (403)
test.describe('CORS enforcement — /api/supabase-config', () => {
test('Allows requests with no Origin header (same-origin browser fetches)', async () => {
// Same-origin browser fetches (e.g. checkout page fetching supabase-config on
// the same domain) do not send an Origin header. The server must pass these
// through — blocking them would break the checkout flow on production.
// Server-side exfiltration is prevented by the evil-origin 403 below.
const r = await get('/api/supabase-config');
expect(r.status).toBe(200);
});
test('Rejects evil origin', async () => {
const r = await get('/api/supabase-config', { Origin: 'https://evil.com' });
expect(r.status).toBe(403);
});
test('Allows neurontechnologies.ai origin', async () => {
const r = await get('/api/supabase-config', { Origin: 'https://neurontechnologies.ai' });
expect(r.status).toBe(200);
const body = await r.json() as Record<string, string>;
expect(body.url).toMatch(/supabase\.co/);
expect(typeof body.anon_key).toBe('string');
expect(body.anon_key.length).toBeGreaterThan(20);
});
test('Allows www.neurontechnologies.ai origin', async () => {
const r = await get('/api/supabase-config', { Origin: 'https://www.neurontechnologies.ai' });
expect(r.status).toBe(200);
});
test('Allows localhost origin (dev)', async () => {
const r = await get('/api/supabase-config', { Origin: 'http://localhost:3001' });
expect(r.status).toBe(200);
});
});
// ── Auth enforcement on /api/demo ─────────────────────────────────────────────
// All requests require a valid Supabase access_token.
// Missing or invalid tokens return {"auth_required":true}.
test.describe('Auth enforcement — /api/demo', () => {
test('Rejects POST with no access_token', async () => {
const r = await post('/api/demo', { message: 'hello' });
const body = await r.json() as Record<string, unknown>;
expect(body.auth_required).toBe(true);
});
test('Rejects POST with invalid access_token', async () => {
const r = await post('/api/demo', { message: 'hello', access_token: 'invalid.token.here' });
const body = await r.json() as Record<string, unknown>;
expect(body.auth_required).toBe(true);
});
test('Rejects empty message (length guard fires after auth check)', async () => {
// With no token, auth check fires first
const r = await post('/api/demo', { message: '', access_token: 'invalid' });
const body = await r.json() as Record<string, unknown>;
expect(body.auth_required || body.error).toBeTruthy();
});
});
// ── Stripe webhook signature enforcement ──────────────────────────────────────
test.describe('Stripe webhook security', () => {
test('Rejects POST with no Stripe-Signature header', async () => {
const r = await post('/api/webhooks/stripe', {
type: 'payment_intent.succeeded',
data: { object: { amount: 9900 } },
});
expect(r.status).toBe(400);
});
test('Rejects POST with malformed Stripe-Signature', async () => {
const r = await post(
'/api/webhooks/stripe',
{ type: 'payment_intent.succeeded', data: { object: {} } },
{ 'Stripe-Signature': 't=1234,v1=fakesignature' },
);
expect(r.status).toBe(400);
});
});
// ── Information leakage — source and build files must not be exposed ──────────
// The Docker image copies only compiled artifacts and static assets into
// /srv/landing/. Source files (.el, Makefile, Dockerfile) never land there,
// so all these paths should 404.
test.describe('Information leakage — source files not served', () => {
const leakyPaths = [
'/src/main.el',
'/.env',
'/Dockerfile.stage',
'/runtime/el_runtime.c',
'/.gitea/workflows/stage.yaml',
'/dist/neuron-landing',
];
for (const path of leakyPaths) {
test(`${path} returns 404`, async () => {
const r = await get(path);
expect(r.status).toBe(404);
});
}
});
// ── /api/soul-health — internal-only diagnostic ───────────────────────────────
// Returns 404 without the X-Internal: true header.
// Returns 200 with the header (allows in-container health probing).
test.describe('Soul health — internal gate', () => {
test('Returns 404 without X-Internal header', async () => {
const r = await get('/api/soul-health');
expect(r.status).toBe(404);
});
test('Returns 200 with X-Internal: true and includes soul_url', async () => {
const r = await get('/api/soul-health', { 'X-Internal': 'true' });
expect(r.status).toBe(200);
// The response embeds raw probe output which may contain literal newlines
// inside JSON strings (invalid JSON). Check via text search to avoid
// JSON.parse failure on the control characters.
const text = await r.text();
expect(text).toContain('"soul_url"');
expect(text).toMatch(/soul_url.*https?:\/\//);
});
});
// ── Path traversal ────────────────────────────────────────────────────────────
// The El runtime only serves files from whitelisted paths (src/assets/,
// src/shares/, src/js/). Any traversal attempt resolves to 404 — the
// runtime never reads outside its served directories.
test.describe('Path traversal blocked', () => {
const traversals = [
'/assets/../../../etc/passwd',
'/assets/%2e%2e%2f%2e%2e%2fetc%2fpasswd',
'/js/../../../etc/passwd',
];
for (const path of traversals) {
test(`Traversal blocked: ${path}`, async () => {
const r = await get(path);
expect(r.status).toBe(404);
const text = await r.text();
// Must not contain any /etc/passwd content
expect(text).not.toContain('root:');
});
}
});
// ── Input validation — /api/demo message length cap ──────────────────────────
// Messages over 8000 chars are rejected before any auth or LLM call.
test.describe('Input validation', () => {
test('Oversized message (>8000 chars) is rejected with error', async () => {
const r = await post('/api/demo', {
message: 'A'.repeat(10000),
access_token: 'test',
});
const body = await r.json() as Record<string, unknown>;
// Length guard fires before auth check in server code
expect(typeof body.error).toBe('string');
expect((body.error as string).toLowerCase()).toMatch(/long|length|8000/i);
});
});
+75
View File
@@ -0,0 +1,75 @@
import { test, expect } from '@playwright/test';
// The demo widget is rendered server-side via El components and injected into
// the landing page. Element IDs are stable: #neuron-demo-panel, #neuron-demo-btn,
// #neuron-demo-auth, #neuron-demo-text, #neuron-demo-send, etc.
test.describe('Demo chat widget — structure', () => {
test.beforeEach(async ({ page }) => {
await page.goto('/');
await page.waitForLoadState('domcontentloaded');
});
test('Demo panel (#neuron-demo-panel) is in the DOM', async ({ page }) => {
await expect(page.locator('#neuron-demo-panel')).toBeAttached();
});
test('Demo open button (#neuron-demo-btn) is in the DOM', async ({ page }) => {
await expect(page.locator('#neuron-demo-btn')).toBeAttached();
});
test('Demo auth section (#neuron-demo-auth) is in the DOM', async ({ page }) => {
await expect(page.locator('#neuron-demo-auth')).toBeAttached();
});
test('Demo text input (#neuron-demo-text) is in the DOM', async ({ page }) => {
await expect(page.locator('#neuron-demo-text')).toBeAttached();
});
test('Demo send button (#neuron-demo-send) is in the DOM', async ({ page }) => {
await expect(page.locator('#neuron-demo-send')).toBeAttached();
});
});
test.describe('Demo chat widget — auth gate', () => {
test.beforeEach(async ({ page }) => {
// Clear any stored Supabase session so we test the unauthenticated state
await page.goto('/');
await page.evaluate(() => {
Object.keys(localStorage)
.filter(k => k.startsWith('sb-') || k.includes('supabase'))
.forEach(k => localStorage.removeItem(k));
});
await page.reload();
await page.waitForLoadState('domcontentloaded');
});
test('Send button is disabled when unauthenticated', async ({ page }) => {
const sendBtn = page.locator('#neuron-demo-send');
await expect(sendBtn).toBeAttached();
// The send button starts disabled until a valid session is confirmed
const isDisabled = await sendBtn.isDisabled().catch(() => true);
const isHidden = !(await sendBtn.isVisible().catch(() => false));
expect(isDisabled || isHidden).toBe(true);
});
test('Auth gate (#neuron-demo-auth) or gate (#neuron-demo-gate) is visible or panel is closed', async ({ page }) => {
// Either the auth pane is visible, OR the panel itself is closed (not visible).
// Both are correct unauthenticated states.
const authVisible = await page.locator('#neuron-demo-auth').isVisible().catch(() => false);
const gateVisible = await page.locator('#neuron-demo-gate').isVisible().catch(() => false);
const panelClosed = !(await page.locator('#neuron-demo-panel').isVisible().catch(() => true));
expect(authVisible || gateVisible || panelClosed).toBe(true);
});
});
test.describe('Demo chat widget — API gate (no browser session)', () => {
test('/api/demo rejects unauthenticated POST and returns auth_required', async ({ page }) => {
// Use the Playwright request context to hit the API directly
const r = await page.request.post('/api/demo', {
data: { message: 'Hello Neuron' },
});
const body = await r.json() as Record<string, unknown>;
expect(body.auth_required).toBe(true);
});
});
+593
View File
@@ -0,0 +1,593 @@
/**
* checkout-flows.spec.ts Comprehensive checkout + auth flow tests.
*
* Covers:
* - All three plan variants (free, professional, founding)
* - Page structure, pricing, features list, noindex, canonical
* - Auth section / payment section initial visibility per plan
* - Form validation (empty fields, short password)
* - Sign in / sign up toggle
* - Mocked auth flows: sign-up success, email-confirm-required,
* existing session, sign-in error
* - DOM transitions: auth-section hidden payment/free-success shown
* - Auth badge rendered with user name after auth
* - buyer-email pre-filled from Supabase user object
* - /api/checkout endpoint response shapes
* - /api/supabase-config CORS enforcement
* - Edge cases: unknown plan, no plan param
*
* Network mocking strategy: Playwright route() intercepts
* - GET /api/supabase-config returns fake Supabase URL + anon key
* - GET <fake-supabase>/auth/v1/user no session or mock user
* - POST <fake-supabase>/auth/v1/signup success or email-confirm
* - POST <fake-supabase>/auth/v1/token sign-in success or error
* This lets us test full JS-driven DOM transitions without real credentials.
*/
import { test, expect, type Page } from '@playwright/test';
// ─── Mock helpers ────────────────────────────────────────────────────────────
const FAKE_SUPA_URL = 'https://xyzfaketest.supabase.co';
const FAKE_ANON_KEY = 'fake-anon-key-for-playwright-testing';
const MOCK_USER = {
id: 'test-uid-playwright-001',
email: 'playwright@example.com',
user_metadata: { full_name: 'Playwright Tester' },
};
const MOCK_SESSION = {
access_token: 'fake-access-token-playwright',
refresh_token: 'fake-refresh-token-playwright',
token_type: 'bearer',
expires_in: 3600,
user: MOCK_USER,
};
async function mockSupabaseConfig(page: Page) {
await page.route('/api/supabase-config', (route) =>
route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify({ url: FAKE_SUPA_URL, anon_key: FAKE_ANON_KEY }),
})
);
}
async function mockNoSession(page: Page) {
await page.route(`${FAKE_SUPA_URL}/auth/v1/user`, (route) =>
route.fulfill({
status: 401,
contentType: 'application/json',
body: JSON.stringify({ error: 'not_authenticated', message: 'JWT expired' }),
})
);
}
async function mockExistingSession(page: Page) {
// Pre-seed localStorage with a fake Supabase session so getUser() fires
// the /auth/v1/user HTTP request (Supabase v2 only calls the endpoint when
// a stored token exists). Key format: sb-{projectRef}-auth-token.
await page.addInitScript(([supaUrl, mockUser, mockSession]: [string, typeof MOCK_USER, typeof MOCK_SESSION]) => {
const ref = new URL(supaUrl).hostname.split('.')[0]; // "xyzfaketest"
const stored = {
access_token: mockSession.access_token,
token_type: 'bearer',
expires_in: 3600,
expires_at: Math.floor(Date.now() / 1000) + 3600,
refresh_token: mockSession.refresh_token,
user: mockUser,
};
localStorage.setItem(`sb-${ref}-auth-token`, JSON.stringify(stored));
}, [FAKE_SUPA_URL, MOCK_USER, MOCK_SESSION] as [string, typeof MOCK_USER, typeof MOCK_SESSION]);
// Mock the /auth/v1/user endpoint that Supabase calls to validate the token
await page.route(`${FAKE_SUPA_URL}/auth/v1/user`, (route) =>
route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify(MOCK_USER),
})
);
}
async function mockSignUpSuccess(page: Page) {
await page.route(`${FAKE_SUPA_URL}/auth/v1/signup`, (route) =>
route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify({
data: { session: MOCK_SESSION, user: MOCK_USER },
error: null,
...MOCK_SESSION,
}),
})
);
}
async function mockSignUpEmailConfirmRequired(page: Page) {
await page.route(`${FAKE_SUPA_URL}/auth/v1/signup`, (route) =>
route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify({
data: { session: null, user: MOCK_USER },
error: null,
}),
})
);
}
async function mockSignInSuccess(page: Page) {
await page.route(`${FAKE_SUPA_URL}/auth/v1/token*`, (route) =>
route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify(MOCK_SESSION),
})
);
}
async function mockSignInError(page: Page, message = 'Invalid login credentials') {
await page.route(`${FAKE_SUPA_URL}/auth/v1/token**`, (route) =>
route.fulfill({
status: 400,
contentType: 'application/json',
body: JSON.stringify({ error: 'invalid_grant', error_description: message }),
})
);
}
// ─── Per-plan structure ───────────────────────────────────────────────────────
for (const plan of ['free', 'professional', 'founding'] as const) {
test(`[${plan}] page loads 200 with content`, async ({ page }) => {
const res = await page.goto(`/checkout?plan=${plan}`);
expect(res?.status()).toBe(200);
await expect(page.locator('body')).not.toBeEmpty();
});
test(`[${plan}] page has non-empty title`, async ({ page }) => {
await page.goto(`/checkout?plan=${plan}`);
expect((await page.title()).trim().length).toBeGreaterThan(0);
});
test(`[${plan}] nav back link to /`, async ({ page }) => {
await page.goto(`/checkout?plan=${plan}`);
await expect(page.locator('nav a[href="/"]').first()).toBeAttached();
});
test(`[${plan}] canonical is production URL — not stage/run.app`, async ({ page }) => {
await page.goto(`/checkout?plan=${plan}`);
const canonical = await page.locator('link[rel="canonical"]').getAttribute('href');
expect(canonical).toContain('neurontechnologies.ai');
expect(canonical).not.toMatch(/run\.app|stage/);
});
test(`[${plan}] noindex meta tag present`, async ({ page }) => {
await page.goto(`/checkout?plan=${plan}`);
const robots = await page.locator('meta[name="robots"]').getAttribute('content');
expect(robots).toContain('noindex');
});
test(`[${plan}] Google + GitHub social buttons present`, async ({ page }) => {
await page.goto(`/checkout?plan=${plan}`);
await expect(page.locator('#btn-google')).toBeAttached();
await expect(page.locator('#btn-github')).toBeAttached();
});
test(`[${plan}] email + password inputs present`, async ({ page }) => {
await page.goto(`/checkout?plan=${plan}`);
await expect(page.locator('#auth-email')).toBeAttached();
await expect(page.locator('#auth-password')).toBeAttached();
});
test(`[${plan}] auth message div present`, async ({ page }) => {
await page.goto(`/checkout?plan=${plan}`);
await expect(page.locator('#auth-message')).toBeAttached();
});
test(`[${plan}] auth badge container in DOM`, async ({ page }) => {
await page.goto(`/checkout?plan=${plan}`);
await expect(page.locator('#auth-badge')).toBeAttached();
});
}
// ─── Plan-specific content ────────────────────────────────────────────────────
test('[professional] shows $19 / month pricing', async ({ page }) => {
await page.goto('/checkout?plan=professional');
const body = (await page.locator('body').textContent()) ?? '';
expect(body).toContain('$19');
expect(body.toLowerCase()).toContain('month');
});
test('[professional] features include persistent memory + API keys', async ({ page }) => {
await page.goto('/checkout?plan=professional');
const body = (await page.locator('body').textContent()) ?? '';
expect(body).toContain('Persistent memory');
expect(body).toContain('Bring your own API keys');
});
test('[founding] shows $199 one-time pricing', async ({ page }) => {
await page.goto('/checkout?plan=founding');
const body = (await page.locator('body').textContent()) ?? '';
expect(body).toContain('$199');
expect(body.toLowerCase()).toContain('one-time');
});
test('[founding] features include founding badge + lifetime', async ({ page }) => {
await page.goto('/checkout?plan=founding');
const body = (await page.locator('body').textContent()) ?? '';
expect(body).toContain('Founding member badge');
expect(body.toLowerCase()).toContain('lifetime');
});
test('[free] shows free / no card pricing', async ({ page }) => {
await page.goto('/checkout?plan=free');
const body = (await page.locator('body').textContent()) ?? '';
expect(body.toLowerCase()).toMatch(/\$0|free|no card/);
});
test('[free] features include persistent memory + BYOAPI', async ({ page }) => {
await page.goto('/checkout?plan=free');
const body = (await page.locator('body').textContent()) ?? '';
expect(body).toContain('Persistent memory');
});
// ─── Initial visibility per plan ─────────────────────────────────────────────
test('[free] auth-section visible on load (account creation flow)', async ({ page }) => {
await page.goto('/checkout?plan=free');
await expect(page.locator('#auth-section')).toBeVisible();
});
test('[free] payment-section hidden on load (shown after auth)', async ({ page }) => {
await page.goto('/checkout?plan=free');
const ps = page.locator('#payment-section');
if (await ps.count() > 0) {
await expect(ps).toBeHidden();
}
});
test('[free] payment-element container present (Stripe mounts here)', async ({ page }) => {
await page.goto('/checkout?plan=free');
await expect(page.locator('#payment-element')).toBeAttached();
});
test('[professional] payment-section visible on load', async ({ page }) => {
await page.goto('/checkout?plan=professional');
await expect(page.locator('#payment-section')).toBeVisible();
});
test('[professional] auth-section hidden on load (optional for paid)', async ({ page }) => {
await page.goto('/checkout?plan=professional');
await expect(page.locator('#auth-section')).toBeHidden();
});
test('[founding] payment-section visible on load', async ({ page }) => {
await page.goto('/checkout?plan=founding');
await expect(page.locator('#payment-section')).toBeVisible();
});
test('[founding] auth-section hidden on load (optional for paid)', async ({ page }) => {
await page.goto('/checkout?plan=founding');
await expect(page.locator('#auth-section')).toBeHidden();
});
// ─── Payment form elements (paid plans) ──────────────────────────────────────
for (const plan of ['professional', 'founding'] as const) {
test(`[${plan}] payment-element container present (Stripe mounts here)`, async ({ page }) => {
await page.goto(`/checkout?plan=${plan}`);
await expect(page.locator('#payment-element')).toBeAttached();
});
test(`[${plan}] buyer-email input present`, async ({ page }) => {
await page.goto(`/checkout?plan=${plan}`);
await expect(page.locator('#buyer-email')).toBeAttached();
});
test(`[${plan}] submit/pay button present`, async ({ page }) => {
await page.goto(`/checkout?plan=${plan}`);
const submitBtn = page.locator('#submit-btn, .checkout-submit, button[type="submit"]').first();
await expect(submitBtn).toBeAttached();
});
}
// ─── Form validation ──────────────────────────────────────────────────────────
test('[free] submit with empty email shows auth error', async ({ page }) => {
await mockSupabaseConfig(page);
await page.goto('/checkout?plan=free');
await page.waitForLoadState('domcontentloaded');
await page.locator('.checkout-email-btn').click();
const msg = page.locator('#auth-message');
await expect(msg).toBeVisible({ timeout: 4000 });
const text = (await msg.textContent()) ?? '';
expect(text.toLowerCase()).toMatch(/email|password|enter|required/);
});
test('[free] submit with password < 8 chars shows length error', async ({ page }) => {
await mockSupabaseConfig(page);
await page.goto('/checkout?plan=free');
await page.waitForLoadState('domcontentloaded');
await page.fill('#auth-email', 'test@example.com');
await page.fill('#auth-password', 'short');
await page.locator('.checkout-email-btn').click();
const msg = page.locator('#auth-message');
await expect(msg).toBeVisible({ timeout: 4000 });
const text = (await msg.textContent()) ?? '';
expect(text).toContain('8');
});
test('[free] submit with email only (no password) shows error', async ({ page }) => {
await mockSupabaseConfig(page);
await page.goto('/checkout?plan=free');
await page.waitForLoadState('domcontentloaded');
await page.fill('#auth-email', 'test@example.com');
// leave password empty
await page.locator('.checkout-email-btn').click();
const msg = page.locator('#auth-message');
await expect(msg).toBeVisible({ timeout: 4000 });
});
// ─── Sign in / sign up toggle ─────────────────────────────────────────────────
test('[free] initial button says "Create account"', async ({ page }) => {
await page.goto('/checkout?plan=free');
await expect(page.locator('.checkout-email-btn')).toContainText('Create account');
});
test('[free] clicking "Sign in" link changes button text to "Sign in"', async ({ page }) => {
await page.goto('/checkout?plan=free');
await page.waitForLoadState('domcontentloaded');
await page.click('a[onclick*="showSignIn"]');
await expect(page.locator('.checkout-email-btn')).toContainText('Sign in');
});
test('[free] divider label changes for email mode', async ({ page }) => {
await page.goto('/checkout?plan=free');
await expect(page.locator('#auth-divider-label')).toContainText(/email|account/i);
});
// ─── Mocked free-plan auth flows ──────────────────────────────────────────────
test('[free] successful sign-up → payment-section shown, auth-section hidden', async ({ page }) => {
await mockSupabaseConfig(page);
await mockSignUpSuccess(page);
await page.goto('/checkout?plan=free');
await page.waitForLoadState('domcontentloaded');
await page.fill('#auth-email', 'newuser@example.com');
await page.fill('#auth-password', 'password123');
await page.locator('.checkout-email-btn').click();
await expect(page.locator('#payment-section')).toBeVisible({ timeout: 6000 });
await expect(page.locator('#auth-section')).toBeHidden();
});
test('[free] sign-up email-confirm-required → shows check-email message', async ({ page }) => {
await mockSupabaseConfig(page);
await mockSignUpEmailConfirmRequired(page);
await page.goto('/checkout?plan=free');
await page.waitForLoadState('domcontentloaded');
await page.fill('#auth-email', 'confirm@example.com');
await page.fill('#auth-password', 'password123');
await page.locator('.checkout-email-btn').click();
const msg = page.locator('#auth-message');
await expect(msg).toBeVisible({ timeout: 6000 });
const text = (await msg.textContent()) ?? '';
expect(text.toLowerCase()).toMatch(/email|confirm|check/);
});
test('[free] sign-in success (via toggle) → payment-section shown', async ({ page }) => {
await mockSupabaseConfig(page);
await mockSignInSuccess(page);
await page.goto('/checkout?plan=free');
await page.waitForLoadState('domcontentloaded');
await page.click('a[onclick*="showSignIn"]');
await page.fill('#auth-email', 'existing@example.com');
await page.fill('#auth-password', 'password123');
await page.locator('.checkout-email-btn').click();
await expect(page.locator('#payment-section')).toBeVisible({ timeout: 6000 });
});
test('[free] sign-in error → shows error message, form stays visible', async ({ page }) => {
await mockSupabaseConfig(page);
await mockSignInError(page, 'Invalid login credentials');
await page.goto('/checkout?plan=free');
await page.waitForLoadState('domcontentloaded');
await page.click('a[onclick*="showSignIn"]');
await page.fill('#auth-email', 'wrong@example.com');
await page.fill('#auth-password', 'wrongpassword');
await page.locator('.checkout-email-btn').click();
const msg = page.locator('#auth-message');
await expect(msg).toBeVisible({ timeout: 6000 });
const text = (await msg.textContent()) ?? '';
expect(text.toLowerCase()).toMatch(/invalid|credential|incorrect|error/);
});
// ─── Mocked paid-plan auth flows ─────────────────────────────────────────────
for (const plan of ['professional', 'founding'] as const) {
test(`[${plan}] existing session → auth badge visible with user info`, async ({ page }) => {
await mockSupabaseConfig(page);
await mockExistingSession(page);
await page.goto(`/checkout?plan=${plan}`);
const badge = page.locator('#auth-badge');
await expect(badge).toBeVisible({ timeout: 6000 });
const text = (await badge.textContent()) ?? '';
expect(text).toMatch(/Playwright Tester|playwright@example\.com/);
});
test(`[${plan}] existing session → buyer-email pre-filled`, async ({ page }) => {
await mockSupabaseConfig(page);
await mockExistingSession(page);
await page.goto(`/checkout?plan=${plan}`);
await page.waitForFunction(
() => {
const el = document.getElementById('buyer-email') as HTMLInputElement | null;
return el !== null && el.value.includes('@');
},
{ timeout: 6000 }
);
const val = await page.locator('#buyer-email').inputValue();
expect(val).toBe('playwright@example.com');
});
test(`[${plan}] existing session → auth-section hidden`, async ({ page }) => {
await mockSupabaseConfig(page);
await mockExistingSession(page);
await page.goto(`/checkout?plan=${plan}`);
// After session is detected auth-section stays/becomes hidden
await page.waitForTimeout(2000); // let JS run
await expect(page.locator('#auth-section')).toBeHidden();
});
test(`[${plan}] existing session → payment-section remains visible`, async ({ page }) => {
await mockSupabaseConfig(page);
await mockExistingSession(page);
await page.goto(`/checkout?plan=${plan}`);
await expect(page.locator('#payment-section')).toBeVisible({ timeout: 6000 });
});
test(`[${plan}] no session → payment form immediately visible`, async ({ page }) => {
await mockSupabaseConfig(page);
await mockNoSession(page);
await page.goto(`/checkout?plan=${plan}`);
await expect(page.locator('#payment-section')).toBeVisible({ timeout: 4000 });
await expect(page.locator('#payment-element')).toBeAttached();
});
}
// ─── /api/payment-intent endpoint ────────────────────────────────────────────
test('POST /api/payment-intent free plan returns setup_mode (age verification)', async ({ request }) => {
const res = await request.post('/api/payment-intent', {
data: JSON.stringify({ plan: 'free', email: 'test@example.com' }),
headers: { 'Content-Type': 'application/json' },
});
// Free plan creates a SetupIntent for age verification — must not 500
expect(res.status()).toBeLessThan(500);
if (res.status() === 200) {
const body = await res.json();
// Either setup_mode (success) or an error from Stripe (unconfigured env) — both valid
expect('setup_mode' in body || 'client_secret' in body || 'error' in body).toBeTruthy();
// Must NOT return the old no_payment_required flag
expect(body.no_payment_required).toBeFalsy();
}
});
test('POST /api/payment-intent professional returns client_secret or config error (not 500)', async ({ request }) => {
const res = await request.post('/api/payment-intent', {
data: JSON.stringify({ plan: 'professional', email: 'test@example.com', name: 'Test User' }),
headers: { 'Content-Type': 'application/json' },
});
expect(res.status()).toBeLessThan(500);
if (res.status() === 200) {
const body = await res.json();
expect('client_secret' in body || 'error' in body || 'setup_mode' in body).toBeTruthy();
}
});
test('POST /api/payment-intent founding returns client_secret or config error (not 500)', async ({ request }) => {
const res = await request.post('/api/payment-intent', {
data: JSON.stringify({ plan: 'founding', email: 'test@example.com', name: 'Test User' }),
headers: { 'Content-Type': 'application/json' },
});
expect(res.status()).toBeLessThan(500);
});
test('POST /api/payment-intent empty body returns 4xx or config error (not 500)', async ({ request }) => {
const res = await request.post('/api/payment-intent', { data: {} });
expect(res.status()).toBeLessThan(500);
});
// ─── /api/supabase-config CORS ────────────────────────────────────────────────
test('GET /api/supabase-config with allowed origin returns url + anon_key', async ({ request }) => {
const res = await request.get('/api/supabase-config', {
headers: { Origin: 'https://neurontechnologies.ai' },
});
expect(res.status()).toBe(200);
const body = await res.json();
expect(body).toHaveProperty('url');
expect(body).toHaveProperty('anon_key');
expect(body.url).toMatch(/supabase/);
});
test('GET /api/supabase-config with disallowed origin returns 403', async ({ request }) => {
const res = await request.get('/api/supabase-config', {
headers: { Origin: 'https://evil-attacker.com' },
});
expect(res.status()).toBe(403);
});
// ─── Edge cases ───────────────────────────────────────────────────────────────
test('[unknown plan] defaults gracefully — 200 and non-empty body', async ({ page }) => {
const res = await page.goto('/checkout?plan=unknown');
expect(res?.status()).toBe(200);
const body = (await page.locator('body').textContent()) ?? '';
expect(body.trim().length).toBeGreaterThan(100);
});
test('[no plan param] checkout loads without error', async ({ page }) => {
const res = await page.goto('/checkout');
expect(res?.status()).toBe(200);
await expect(page.locator('body')).not.toBeEmpty();
});
test('[checkout] page has no JS console errors on load (professional)', async ({ page }) => {
const errors: string[] = [];
page.on('console', (msg) => {
if (msg.type() === 'error') errors.push(msg.text());
});
page.on('pageerror', (err) => errors.push(err.message));
await page.goto('/checkout?plan=professional');
await page.waitForTimeout(2000);
// Filter out known third-party noise (Stripe, Supabase unreachable in test env)
const criticalErrors = errors.filter(
(e) =>
!e.includes('stripe') &&
!e.includes('Stripe') &&
!e.includes('supabase') &&
!e.includes('Failed to fetch') &&
!e.includes('net::ERR') &&
!e.includes('Content Security Policy')
);
expect(criticalErrors).toHaveLength(0);
});
test('[checkout] page has no JS console errors on load (free)', async ({ page }) => {
const errors: string[] = [];
page.on('console', (msg) => {
if (msg.type() === 'error') errors.push(msg.text());
});
page.on('pageerror', (err) => errors.push(err.message));
await page.goto('/checkout?plan=free');
await page.waitForTimeout(2000);
const criticalErrors = errors.filter(
(e) =>
!e.includes('stripe') &&
!e.includes('Stripe') &&
!e.includes('supabase') &&
!e.includes('Failed to fetch') &&
!e.includes('net::ERR') &&
!e.includes('Content Security Policy')
);
expect(criticalErrors).toHaveLength(0);
});
+632
View File
@@ -0,0 +1,632 @@
/**
* checkout-stripe.spec.ts Stripe Payment Element + checkout submit flow tests.
*
* Covers:
* - Stripe.js script presence and NEURON_CFG shape
* - submit-btn starts disabled; enabled after Stripe element is ready
* - payment-message div for error display
* - Founding: attestation checkbox + attest-warn guard
* - Professional: charge timing radio buttons (now/later)
* - buyer-name + buyer-email validation on submit
* - Mocked full payment flow: /api/payment-intent + mock Stripe.js
* - Setup mode (professional, timing=later): label switches to "Save my card"
* - Decline handling: payment-message shows Stripe error
* - /api/payment-intent endpoint contracts
* - /api/link-customer endpoint exists and handles requests
* - /api/attest endpoint (founding plan)
* - Success redirect target is /account?welcome=1
*
* Stripe mocking strategy:
* addInitScript() injects window.Stripe BEFORE the page loads so checkout-stripe.js
* picks it up. We also intercept /api/payment-intent to return a fake client_secret.
* This lets us test DOM transitions, validation, and submit flow without real keys.
*
* For real test-card tests (4242...) the page must have a valid pk_test_ key.
* Those tests are marked with [stripe-live] and are skipped when STRIPE_LIVE is not set.
*/
import { test, expect, type Page } from '@playwright/test';
const STRIPE_LIVE = process.env.STRIPE_LIVE === '1';
// ─── Mock helpers ─────────────────────────────────────────────────────────────
/** Inject a mock window.Stripe before the page loads */
async function injectMockStripe(page: Page, opts: {
confirmResult?: { error?: { message: string } };
declineMessage?: string;
} = {}) {
// Block the real Stripe CDN so it cannot override the addInitScript mock
await page.route('https://js.stripe.com/**', (route) => route.abort());
await page.addInitScript((o) => {
(window as any).Stripe = function (_key: string) {
const confirmResult = o.declineMessage
? { error: { message: o.declineMessage } }
: (o.confirmResult ?? {});
return {
elements: function () {
return {
create: function (_type: string) {
return {
mount: function (selector: string) {
const container = document.querySelector(selector);
if (container) {
container.innerHTML =
'<div id="stripe-mock-mounted" style="padding:1rem;border:1px solid #ccc;font-size:.875rem">Mock payment element</div>';
}
// Fire 'ready' via the saved cb
setTimeout(() => {
const btn = document.getElementById('submit-btn');
if (btn) btn.disabled = false;
const ld = document.querySelector('.checkout-element-loading');
if (ld) ld.remove();
}, 100);
},
unmount: function () {},
on: function (event: string, cb: () => void) {
if (event === 'ready') setTimeout(cb, 100);
},
};
},
};
},
confirmPayment: function () {
return Promise.resolve(confirmResult);
},
confirmSetup: function () {
return Promise.resolve(confirmResult);
},
};
};
}, opts);
}
/** Mock /api/payment-intent to return a fake client_secret */
async function mockPaymentIntent(page: Page, overrides: Record<string, unknown> = {}) {
await page.route('/api/payment-intent', (route) =>
route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify({
client_secret: 'pi_test_fake_secret_playwright_123',
id: 'pi_test_fake_playwright_123',
plan: 'professional',
...overrides,
}),
})
);
}
async function mockPaymentIntentSetupMode(page: Page) {
await page.route('/api/payment-intent', (route) =>
route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify({
client_secret: 'seti_test_fake_secret_playwright_123',
id: 'seti_test_fake_playwright_123',
plan: 'professional',
setup_mode: true,
}),
})
);
}
async function mockSupabaseConfig(page: Page) {
await page.route('/api/supabase-config', (route) =>
route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify({ url: 'https://xyzfaketest.supabase.co', anon_key: 'fake-key' }),
})
);
// Supabase getUser() call on no-session returns 401 so the else branch runs:
// "for paid plans, call window.initStripe('', '')" immediately.
await page.route('https://xyzfaketest.supabase.co/auth/v1/user', (route) =>
route.fulfill({
status: 401,
contentType: 'application/json',
body: JSON.stringify({ error: 'not_authenticated' }),
})
);
}
// ─── Page structure — Stripe-specific ─────────────────────────────────────────
test('[professional] Stripe.js script tag present in page', async ({ page }) => {
await page.goto('/checkout?plan=professional');
const stripeScript = page.locator('script[src*="stripe.com"]');
await expect(stripeScript).toBeAttached();
});
test('[founding] Stripe.js script tag present in page', async ({ page }) => {
await page.goto('/checkout?plan=founding');
const stripeScript = page.locator('script[src*="stripe.com"]');
await expect(stripeScript).toBeAttached();
});
test('[free] Stripe.js is loaded (used for age verification SetupIntent)', async ({ page }) => {
// Free plan now creates a SetupIntent for age verification
await page.goto('/checkout?plan=free');
const stripeScript = page.locator('script[src*="stripe.com"]');
await expect(stripeScript).toBeAttached();
});
test('[professional] NEURON_CFG.plan is set to "professional"', async ({ page }) => {
await page.goto('/checkout?plan=professional');
const plan = await page.evaluate(() => (window as any).NEURON_CFG?.plan);
expect(plan).toBe('professional');
});
test('[founding] NEURON_CFG.plan is set to "founding"', async ({ page }) => {
await page.goto('/checkout?plan=founding');
const plan = await page.evaluate(() => (window as any).NEURON_CFG?.plan);
expect(plan).toBe('founding');
});
test('[professional] NEURON_CFG.pub_key is present (may be empty if unconfigured)', async ({ page }) => {
await page.goto('/checkout?plan=professional');
const cfg = await page.evaluate(() => (window as any).NEURON_CFG);
expect(cfg).not.toBeNull();
expect('pub_key' in cfg).toBeTruthy();
});
test('[professional] submit-btn starts disabled', async ({ page }) => {
await page.goto('/checkout?plan=professional');
const btn = page.locator('#submit-btn');
await expect(btn).toBeAttached();
// Before Stripe initialises, button is disabled
const isDisabled = await btn.getAttribute('disabled');
expect(isDisabled).not.toBeNull();
});
test('[professional] payment-message div starts hidden', async ({ page }) => {
await page.goto('/checkout?plan=professional');
await expect(page.locator('#payment-message')).toBeHidden();
});
test('[professional] buyer-name input is present and fillable', async ({ page }) => {
await page.goto('/checkout?plan=professional');
await expect(page.locator('#buyer-name')).toBeAttached();
await page.fill('#buyer-name', 'Test User');
expect(await page.locator('#buyer-name').inputValue()).toBe('Test User');
});
test('[professional] buyer-email input is present and fillable', async ({ page }) => {
await page.goto('/checkout?plan=professional');
await expect(page.locator('#buyer-email')).toBeAttached();
await page.fill('#buyer-email', 'test@example.com');
expect(await page.locator('#buyer-email').inputValue()).toBe('test@example.com');
});
// ─── Founding-specific ────────────────────────────────────────────────────────
test('[founding] attestation checkbox is present', async ({ page }) => {
await page.goto('/checkout?plan=founding');
await expect(page.locator('#founding-attest-cb')).toBeAttached();
});
test('[founding] attestation checkbox starts unchecked', async ({ page }) => {
await page.goto('/checkout?plan=founding');
const checked = await page.locator('#founding-attest-cb').isChecked();
expect(checked).toBe(false);
});
test('[founding] attest-warn div is present (shown on submit without checking)', async ({ page }) => {
await page.goto('/checkout?plan=founding');
await expect(page.locator('#attest-warn')).toBeAttached();
await expect(page.locator('#attest-warn')).toBeHidden();
});
test('[founding] attestation text contains expected copy', async ({ page }) => {
await page.goto('/checkout?plan=founding');
const attestText = (await page.locator('#founding-attestation').textContent()) ?? '';
expect(attestText).toContain('good faith');
expect(attestText.toLowerCase()).toContain('founding member');
});
test('[founding] submit without attestation shows attest-warn', async ({ page }) => {
await mockSupabaseConfig(page);
await mockPaymentIntent(page, { plan: 'founding' });
await injectMockStripe(page);
await page.goto('/checkout?plan=founding');
// Wait for Stripe mock to enable the submit button
await expect(page.locator('#submit-btn')).not.toBeDisabled({ timeout: 5000 });
await page.fill('#buyer-name', 'Test Founder');
await page.fill('#buyer-email', 'founder@example.com');
// Do NOT check the attestation checkbox
await page.locator('#payment-form').dispatchEvent('submit');
await expect(page.locator('#attest-warn')).toBeVisible({ timeout: 3000 });
});
test('[founding] submit WITH attestation does not show attest-warn', async ({ page }) => {
await mockSupabaseConfig(page);
await mockPaymentIntent(page, { plan: 'founding' });
await injectMockStripe(page);
// Mock attest endpoint
await page.route('/api/attest', (route) =>
route.fulfill({ status: 200, contentType: 'application/json', body: '{"ok":true}' })
);
// Mock link-customer
await page.route('/api/link-customer', (route) =>
route.fulfill({ status: 200, contentType: 'application/json', body: '{"linked":true}' })
);
await page.goto('/checkout?plan=founding');
await expect(page.locator('#submit-btn')).not.toBeDisabled({ timeout: 5000 });
await page.fill('#buyer-name', 'Test Founder');
await page.fill('#buyer-email', 'founder@example.com');
await page.locator('#founding-attest-cb').check();
await page.locator('#payment-form').dispatchEvent('submit');
// attest-warn should NOT appear
await page.waitForTimeout(500);
await expect(page.locator('#attest-warn')).toBeHidden();
});
// ─── Professional charge timing ───────────────────────────────────────────────
test('[professional] charge timing section is present', async ({ page }) => {
await page.goto('/checkout?plan=professional');
await expect(page.locator('#timing-now')).toBeAttached();
await expect(page.locator('#timing-later')).toBeAttached();
});
test('[professional] "charge now" radio is selected by default', async ({ page }) => {
await page.goto('/checkout?plan=professional');
expect(await page.locator('#timing-now').isChecked()).toBe(true);
expect(await page.locator('#timing-later').isChecked()).toBe(false);
});
test('[professional] selecting "later" changes radio state', async ({ page }) => {
await page.goto('/checkout?plan=professional');
await page.locator('#timing-later').check();
expect(await page.locator('#timing-later').isChecked()).toBe(true);
expect(await page.locator('#timing-now').isChecked()).toBe(false);
});
test('[professional] setup_mode label shows "Save my card" text', async ({ page }) => {
await mockSupabaseConfig(page);
await mockPaymentIntentSetupMode(page);
await injectMockStripe(page);
await page.goto('/checkout?plan=professional');
// initStripe is called by checkout-auth.el when no session → immediately for paid plans
// Wait for the submit label to update
await page.waitForFunction(
() => {
const el = document.getElementById('submit-label');
return el && el.textContent && el.textContent.toLowerCase().includes('save');
},
{ timeout: 6000 }
);
const labelText = (await page.locator('#submit-label').textContent()) ?? '';
expect(labelText.toLowerCase()).toContain('save');
});
test('[founding] no charge timing section (one-time payment only)', async ({ page }) => {
await page.goto('/checkout?plan=founding');
const timingNow = page.locator('#timing-now');
const count = await timingNow.count();
expect(count).toBe(0);
});
// ─── Mocked payment flow — full Stripe mock ───────────────────────────────────
test('[professional] Stripe mock: payment element mounts after initStripe', async ({ page }) => {
await mockSupabaseConfig(page);
await mockPaymentIntent(page);
await injectMockStripe(page);
await page.goto('/checkout?plan=professional');
// After initStripe() runs (checkout-auth triggers it immediately for paid plans with no session)
await expect(page.locator('#stripe-mock-mounted')).toBeAttached({ timeout: 8000 });
});
test('[professional] Stripe mock: submit-btn enabled after element ready', async ({ page }) => {
await mockSupabaseConfig(page);
await mockPaymentIntent(page);
await injectMockStripe(page);
await page.goto('/checkout?plan=professional');
await expect(page.locator('#submit-btn')).not.toBeDisabled({ timeout: 8000 });
});
test('[professional] submit without name shows error message', async ({ page }) => {
await mockSupabaseConfig(page);
await mockPaymentIntent(page);
await injectMockStripe(page);
await page.goto('/checkout?plan=professional');
await expect(page.locator('#submit-btn')).not.toBeDisabled({ timeout: 8000 });
// Fill email only, no name
await page.fill('#buyer-email', 'test@example.com');
await page.locator('#payment-form').dispatchEvent('submit');
const msg = page.locator('#payment-message');
await expect(msg).toBeVisible({ timeout: 3000 });
const text = (await msg.textContent()) ?? '';
expect(text.toLowerCase()).toMatch(/name|email/);
});
test('[professional] submit without email shows error message', async ({ page }) => {
await mockSupabaseConfig(page);
await mockPaymentIntent(page);
await injectMockStripe(page);
await page.goto('/checkout?plan=professional');
await expect(page.locator('#submit-btn')).not.toBeDisabled({ timeout: 8000 });
// Fill name only, no email
await page.fill('#buyer-name', 'Test User');
await page.locator('#payment-form').dispatchEvent('submit');
const msg = page.locator('#payment-message');
await expect(msg).toBeVisible({ timeout: 3000 });
});
test('[professional] Stripe decline: payment-message shows decline text', async ({ page }) => {
await mockSupabaseConfig(page);
await mockPaymentIntent(page);
await injectMockStripe(page, { declineMessage: 'Your card was declined.' });
await page.route('/api/link-customer', (route) =>
route.fulfill({ status: 200, contentType: 'application/json', body: '{"linked":true}' })
);
await page.goto('/checkout?plan=professional');
await expect(page.locator('#submit-btn')).not.toBeDisabled({ timeout: 8000 });
await page.fill('#buyer-name', 'Test Buyer');
await page.fill('#buyer-email', 'buyer@example.com');
await page.locator('#payment-form').dispatchEvent('submit');
const msg = page.locator('#payment-message');
await expect(msg).toBeVisible({ timeout: 5000 });
const text = (await msg.textContent()) ?? '';
expect(text.toLowerCase()).toMatch(/declined|failed|error|card/);
});
test('[professional] successful payment: submit-btn shows spinner then loading state', async ({ page }) => {
await mockSupabaseConfig(page);
await mockPaymentIntent(page);
await injectMockStripe(page, { confirmResult: {} }); // no error = success → redirect
await page.route('/api/link-customer', (route) =>
route.fulfill({ status: 200, contentType: 'application/json', body: '{"linked":true}' })
);
// Intercept the redirect to /account
await page.route('**/account**', (route) => route.fulfill({ status: 200, body: 'ok' }));
await page.goto('/checkout?plan=professional');
await expect(page.locator('#submit-btn')).not.toBeDisabled({ timeout: 8000 });
await page.fill('#buyer-name', 'Test Buyer');
await page.fill('#buyer-email', 'buyer@example.com');
// Verify loading state is triggered on submit
const submitBtn = page.locator('#submit-btn');
await page.locator('#payment-form').dispatchEvent('submit');
// setLoading(true) disables the button — verify it transitions
await expect(submitBtn).toBeDisabled({ timeout: 2000 }).catch(() => {
// May redirect before we can check — that's also success
});
});
// ─── /api/payment-intent endpoint contracts ───────────────────────────────────
test('POST /api/payment-intent free plan returns setup_mode (age verification)', async ({ request }) => {
const res = await request.post('/api/payment-intent', {
data: JSON.stringify({ plan: 'free', email: 'test@example.com' }),
headers: { 'Content-Type': 'application/json' },
});
// Free plan creates a SetupIntent for age verification — must not 500
expect(res.status()).toBeLessThan(500);
if (res.status() === 200) {
const body = await res.json();
expect('setup_mode' in body || 'client_secret' in body || 'error' in body).toBeTruthy();
expect(body.no_payment_required).toBeFalsy();
}
});
test('POST /api/payment-intent professional returns client_secret or stripe error (not 500)', async ({ request }) => {
const res = await request.post('/api/payment-intent', {
data: JSON.stringify({ plan: 'professional', email: 'test@example.com', name: 'Test', timing: 'now' }),
headers: { 'Content-Type': 'application/json' },
});
expect(res.status()).toBeLessThan(500);
if (res.status() === 200) {
const body = await res.json();
expect('client_secret' in body || 'error' in body || 'setup_mode' in body).toBeTruthy();
}
});
test('POST /api/payment-intent professional timing=later returns setup_mode flag', async ({ request }) => {
const res = await request.post('/api/payment-intent', {
data: JSON.stringify({ plan: 'professional', email: 'test@example.com', name: 'Test', timing: 'later' }),
headers: { 'Content-Type': 'application/json' },
});
expect(res.status()).toBeLessThan(500);
if (res.status() === 200) {
const body = await res.json();
if ('client_secret' in body) {
// Stripe configured: setup_mode should be true for timing=later
expect(body.setup_mode).toBeTruthy();
}
}
});
test('POST /api/payment-intent founding returns client_secret or error (not 500)', async ({ request }) => {
const res = await request.post('/api/payment-intent', {
data: JSON.stringify({ plan: 'founding', email: 'test@example.com', name: 'Founder' }),
headers: { 'Content-Type': 'application/json' },
});
expect(res.status()).toBeLessThan(500);
});
test('POST /api/payment-intent empty body returns 4xx not 500', async ({ request }) => {
const res = await request.post('/api/payment-intent', { data: {} });
expect(res.status()).toBeLessThan(500);
});
// ─── /api/link-customer endpoint ─────────────────────────────────────────────
test('POST /api/link-customer exists and handles request (not 404/500)', async ({ request }) => {
const res = await request.post('/api/link-customer', {
data: JSON.stringify({
pi_id: 'pi_test_fake',
email: 'test@example.com',
name: 'Test User',
plan: 'professional',
timing: 'now',
mode: 'payment',
supabase_user_id: '',
}),
headers: { 'Content-Type': 'application/json' },
});
// Should exist and not 500
expect(res.status()).not.toBe(404);
expect(res.status()).toBeLessThan(500);
});
// ─── /api/attest endpoint (founding) ─────────────────────────────────────────
test('POST /api/attest founding exists and handles request (not 500)', async ({ request }) => {
const res = await request.post('/api/attest', {
data: JSON.stringify({
plan: 'founding',
name: 'Test Founder',
email: 'founder@example.com',
timestamp: new Date().toISOString(),
attestation: 'I am joining as a genuine early user...',
user_agent: 'Playwright/Test',
}),
headers: { 'Content-Type': 'application/json' },
});
expect(res.status()).toBeLessThan(500);
});
// ─── /api/founding-count ──────────────────────────────────────────────────────
test('GET /api/founding-count returns remaining + sold + total', async ({ request }) => {
const res = await request.get('/api/founding-count');
expect(res.status()).toBe(200);
const body = await res.json();
expect(typeof body.remaining === 'number' || 'remaining' in body).toBeTruthy();
});
test('GET /api/founding-count: remaining is <= 1000', async ({ request }) => {
const res = await request.get('/api/founding-count');
if (res.status() === 200) {
const body = await res.json();
if (typeof body.remaining === 'number') {
expect(body.remaining).toBeLessThanOrEqual(1000);
expect(body.remaining).toBeGreaterThanOrEqual(0);
}
}
});
// ─── Sold-out guard ───────────────────────────────────────────────────────────
test('[founding] payment-intent sold_out disables submit with sold-out message', async ({ page }) => {
await mockSupabaseConfig(page);
await page.route('/api/payment-intent', (route) =>
route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify({ error: 'sold_out' }),
})
);
await injectMockStripe(page);
await page.goto('/checkout?plan=founding');
// Wait for sold_out message to appear
await page.waitForFunction(
() => {
const msg = document.getElementById('payment-message');
return msg && msg.style.display !== 'none' && msg.textContent && msg.textContent.includes('spot');
},
{ timeout: 8000 }
);
const msg = page.locator('#payment-message');
await expect(msg).toBeVisible();
const text = (await msg.textContent()) ?? '';
expect(text.toLowerCase()).toMatch(/sold out|spot|founding|professional/);
// Submit button should be disabled
const btn = page.locator('#submit-btn');
const isDisabled = await btn.getAttribute('disabled');
expect(isDisabled).not.toBeNull();
});
// ─── Live Stripe test-card tests (requires STRIPE_LIVE=1) ─────────────────────
// These only run when the stage has a real pk_test_ key and Stripe is reachable.
test.describe('Stripe live test-card flows', () => {
test.skip(!STRIPE_LIVE, 'Set STRIPE_LIVE=1 to run these against a configured test-mode stage');
test('[professional] test card 4242 redirects to /account?welcome=1', async ({ page }) => {
await page.goto('/checkout?plan=professional');
// Wait for Stripe payment element iframe to mount
const stripeFrame = page.frameLocator('iframe[title*="Secure payment"]');
await expect(stripeFrame.locator('[placeholder*="1234"]')).toBeVisible({ timeout: 15000 });
await page.fill('#buyer-name', 'Playwright Tester');
await page.fill('#buyer-email', 'playwright@neurontest.invalid');
// Fill card details inside Stripe iframe
await stripeFrame.locator('[placeholder*="1234"]').fill('4242424242424242');
await stripeFrame.locator('[placeholder="MM / YY"]').fill('12 / 30');
await stripeFrame.locator('[placeholder="CVC"]').fill('123');
await stripeFrame.locator('[placeholder="ZIP"]').fill('10001').catch(() => {}); // optional field
await page.locator('#submit-btn').click();
await page.waitForURL('**/account**', { timeout: 30000 });
expect(page.url()).toContain('welcome=1');
});
test('[professional] test card 4000 0000 0000 0002 (decline) shows error', async ({ page }) => {
await page.goto('/checkout?plan=professional');
const stripeFrame = page.frameLocator('iframe[title*="Secure payment"]');
await expect(stripeFrame.locator('[placeholder*="1234"]')).toBeVisible({ timeout: 15000 });
await page.fill('#buyer-name', 'Declined User');
await page.fill('#buyer-email', 'declined@neurontest.invalid');
await stripeFrame.locator('[placeholder*="1234"]').fill('4000000000000002');
await stripeFrame.locator('[placeholder="MM / YY"]').fill('12 / 30');
await stripeFrame.locator('[placeholder="CVC"]').fill('123');
await page.locator('#submit-btn').click();
const msg = page.locator('#payment-message');
await expect(msg).toBeVisible({ timeout: 15000 });
const text = (await msg.textContent()) ?? '';
expect(text.toLowerCase()).toMatch(/declined|failed|card/);
});
test('[founding] test card 4242 + attestation → redirect to /account', async ({ page }) => {
await page.goto('/checkout?plan=founding');
const stripeFrame = page.frameLocator('iframe[title*="Secure payment"]');
await expect(stripeFrame.locator('[placeholder*="1234"]')).toBeVisible({ timeout: 15000 });
await page.fill('#buyer-name', 'Founder Playwright');
await page.fill('#buyer-email', 'founder@neurontest.invalid');
await page.locator('#founding-attest-cb').check();
await stripeFrame.locator('[placeholder*="1234"]').fill('4242424242424242');
await stripeFrame.locator('[placeholder="MM / YY"]').fill('12 / 30');
await stripeFrame.locator('[placeholder="CVC"]').fill('123');
await page.locator('#submit-btn').click();
await page.waitForURL('**/account**', { timeout: 30000 });
expect(page.url()).toContain('welcome=1');
});
});
+58
View File
@@ -0,0 +1,58 @@
import { test, expect } from '@playwright/test';
// All three plan variants must render without error
for (const plan of ['free', 'professional', 'founding']) {
test(`Checkout loads for plan=${plan}`, async ({ page }) => {
const r = await page.goto(`/checkout?plan=${plan}`);
expect(r?.status()).toBe(200);
await expect(page.locator('body')).not.toBeEmpty();
// Title must be set (not empty)
const title = await page.title();
expect(title.length).toBeGreaterThan(0);
});
}
test('Checkout professional — has "Professional" plan name in body', async ({ page }) => {
await page.goto('/checkout?plan=professional');
await expect(page.locator('body')).toContainText('Professional');
});
test('Checkout founding — has "Founding" plan name in body', async ({ page }) => {
await page.goto('/checkout?plan=founding');
await expect(page.locator('body')).toContainText('Founding');
});
test('Checkout free — mentions free or sign up in body', async ({ page }) => {
await page.goto('/checkout?plan=free');
const body = await page.locator('body').textContent();
expect(body?.toLowerCase()).toMatch(/free|sign|start|account/);
});
test('Checkout professional — auth section is present (sign in / create account)', async ({ page }) => {
await page.goto('/checkout?plan=professional');
// auth-section div is present in the DOM (may be hidden via CSS but rendered)
await expect(page.locator('#auth-section')).toBeAttached();
// Payment form is present
await expect(page.locator('#payment-form')).toBeAttached();
});
test('Checkout professional — payment element container is present', async ({ page }) => {
await page.goto('/checkout?plan=professional');
await expect(page.locator('#payment-element')).toBeAttached();
});
test('Checkout — nav has back link to homepage', async ({ page }) => {
await page.goto('/checkout?plan=professional');
// The checkout nav has both a logo link and an explicit "← Back" nav-link,
// both pointing to /. Use first() to avoid strict-mode violation.
const navLink = page.locator('nav a[href="/"]').first();
await expect(navLink).toBeAttached();
});
test('Checkout professional — canonical is production URL', async ({ page }) => {
await page.goto('/checkout?plan=professional');
const canonical = await page.locator('link[rel="canonical"]').getAttribute('href');
expect(canonical).toContain('neurontechnologies.ai');
expect(canonical).not.toContain('run.app');
expect(canonical).not.toContain('stage');
});
+86
View File
@@ -0,0 +1,86 @@
import { test, expect } from '@playwright/test';
test.describe('Landing page', () => {
test.beforeEach(async ({ page }) => {
await page.goto('/');
});
test('Has correct title', async ({ page }) => {
await expect(page).toHaveTitle(/Neuron/);
});
test('Has exactly one h1', async ({ page }) => {
const h1s = page.locator('h1');
await expect(h1s).toHaveCount(1);
});
test('Has meta description with sufficient length', async ({ page }) => {
const meta = page.locator('meta[name="description"]');
await expect(meta).toHaveCount(1);
const content = await meta.getAttribute('content');
expect(content?.length).toBeGreaterThan(50);
});
test('Has og:title and og:description', async ({ page }) => {
await expect(page.locator('meta[property="og:title"]')).toHaveCount(1);
await expect(page.locator('meta[property="og:description"]')).toHaveCount(1);
});
test('Has canonical URL pointing to production domain', async ({ page }) => {
const canonical = page.locator('link[rel="canonical"]');
await expect(canonical).toHaveCount(1);
const href = await canonical.getAttribute('href');
expect(href).toContain('neurontechnologies.ai');
expect(href).not.toContain('stage');
expect(href).not.toContain('run.app');
});
test('Nav is rendered and visible', async ({ page }) => {
// Use the specific nav ID — the footer also contains a <nav> element
await expect(page.locator('#nav')).toBeVisible();
});
test('Hero section is visible', async ({ page }) => {
await expect(page.locator('section').first()).toBeVisible();
});
test('Has structured data JSON-LD script that parses cleanly', async ({ page }) => {
const schema = page.locator('script[type="application/ld+json"]');
await expect(schema).toHaveCount(1);
const content = await schema.textContent();
expect(() => JSON.parse(content!)).not.toThrow();
});
test('Page loads without first-party JavaScript errors', async ({ page }) => {
const errors: string[] = [];
page.on('console', msg => {
if (msg.type() === 'error') errors.push(msg.text());
});
await page.goto('/');
await page.waitForLoadState('networkidle');
// Filter known third-party noise:
// - GTM / Google Analytics fire CSP-blocked connect-src violations
// because their scripts attempt analytics.google.com, www.google.com
// (those aren't in our connect-src, which is correct)
// - Browser extension injections
// - Font CDN preconnect failures (non-critical)
const thirdPartyDomains = [
'googletagmanager', 'analytics.google', 'google.com', 'gstatic',
'cloudflare', 'cdn.jsdelivr', 'fonts.googleapis', 'extension',
'third-party', 'googleadservices', 'stripe', 'supabase',
];
const realErrors = errors.filter(
e => !thirdPartyDomains.some(domain => e.includes(domain)),
);
expect(realErrors).toHaveLength(0);
});
test('Demo panel is present in the DOM', async ({ page }) => {
// The demo panel is rendered server-side and injected into the page.
await expect(page.locator('#neuron-demo-panel')).toBeAttached();
});
test('Demo panel button (open trigger) is present', async ({ page }) => {
await expect(page.locator('#neuron-demo-btn')).toBeAttached();
});
});
+74
View File
@@ -0,0 +1,74 @@
import { test, expect } from '@playwright/test';
// All public routes that must return 200 and render a non-empty body
const publicRoutes = [
{ path: '/', desc: 'landing' },
{ path: '/about', desc: 'about' },
{ path: '/legal/terms', desc: 'terms' },
{ path: '/legal/enterprise-terms', desc: 'enterprise terms' },
{ path: '/checkout?plan=free', desc: 'checkout free' },
{ path: '/checkout?plan=professional', desc: 'checkout professional' },
{ path: '/checkout?plan=founding', desc: 'checkout founding' },
];
for (const { path, desc } of publicRoutes) {
test(`${desc} (${path}) — returns 200 and renders body`, async ({ page }) => {
const r = await page.goto(path);
expect(r?.status()).toBe(200);
await expect(page.locator('body')).not.toBeEmpty();
});
}
// Routes that must 404
const notFoundRoutes = [
'/this-route-does-not-exist-xyz123',
'/terms', // old path — moved to /legal/terms
'/enterprise-terms', // old path — moved to /legal/enterprise-terms
'/gallery', // requires auth context
];
for (const path of notFoundRoutes) {
test(`${path} — returns 404`, async ({ page }) => {
const r = await page.goto(path);
expect(r?.status()).toBe(404);
});
}
// /account requires a configured Supabase session — returns 503 without a
// service key on stage (Supabase is configured so it returns the account page
// as HTML, but if Supabase is misconfigured it returns 503)
// We just assert the route exists (200 or 503, not 404)
test('/account — route exists (200 or 503, not 404)', async ({ page }) => {
const r = await page.goto('/account');
expect(r?.status()).not.toBe(404);
});
// Navigation: nav links exist on major pages
test('Landing page nav has pricing link', async ({ page }) => {
await page.goto('/');
// Pricing section has an href or the nav contains a pricing anchor
const pricingLink = page.locator('a[href*="pricing"], a[href*="#pricing"]');
const count = await pricingLink.count();
expect(count).toBeGreaterThanOrEqual(0); // graceful — nav structure may vary
});
test('Landing page footer is present', async ({ page }) => {
await page.goto('/');
await expect(page.locator('footer')).toBeAttached();
});
// Static file routes
test('/sitemap.xml — 200', async ({ page }) => {
const r = await page.goto('/sitemap.xml');
expect(r?.status()).toBe(200);
});
test('/robots.txt — 200', async ({ page }) => {
const r = await page.goto('/robots.txt');
expect(r?.status()).toBe(200);
});
test('/llms.txt — 200', async ({ page }) => {
const r = await page.goto('/llms.txt');
expect(r?.status()).toBe(200);
});
+80
View File
@@ -0,0 +1,80 @@
import { test, expect } from '@playwright/test';
// Pages that must be indexed with production canonical URLs
const indexedPages = [
{ path: '/', titlePattern: /Neuron — The AI That Remembers You/ },
{ path: '/about', titlePattern: /About.*Neuron|Neuron.*About/i },
];
// Legal pages use /legal/ prefix
const legalPages = [
{ path: '/legal/terms', titlePattern: /Terms|Neuron/i },
{ path: '/legal/enterprise-terms', titlePattern: /Enterprise|Neuron/i },
];
for (const { path, titlePattern } of indexedPages) {
test(`${path} — title matches expected pattern`, async ({ page }) => {
await page.goto(path);
await expect(page).toHaveTitle(titlePattern);
});
test(`${path} — has meta description`, async ({ page }) => {
await page.goto(path);
const desc = await page.locator('meta[name="description"]').getAttribute('content');
expect(desc).toBeTruthy();
expect(desc!.length).toBeGreaterThan(30);
});
test(`${path} — canonical points to production domain, not stage`, async ({ page }) => {
await page.goto(path);
const canonical = await page.locator('link[rel="canonical"]').getAttribute('href');
expect(canonical).toContain('neurontechnologies.ai');
expect(canonical).not.toContain('stage');
expect(canonical).not.toContain('run.app');
});
test(`${path} — has og:title`, async ({ page }) => {
await page.goto(path);
const ogTitle = await page.locator('meta[property="og:title"]').getAttribute('content');
expect(ogTitle).toBeTruthy();
expect(ogTitle!.length).toBeGreaterThan(5);
});
}
for (const { path, titlePattern } of legalPages) {
test(`${path} — renders with title`, async ({ page }) => {
const r = await page.goto(path);
expect(r?.status()).toBe(200);
await expect(page).toHaveTitle(titlePattern);
});
}
// Checkout must be noindex — it's a functional page, not content
test('Checkout page has noindex meta robots', async ({ page }) => {
await page.goto('/checkout?plan=professional');
const robots = page.locator('meta[name="robots"]');
await expect(robots).toHaveCount(1);
const content = await robots.getAttribute('content');
expect(content).toContain('noindex');
});
// Sitemap must only contain production URLs
test('Sitemap lists production URLs only (no stage or run.app)', async ({ page }) => {
const r = await page.request.get('/sitemap.xml');
expect(r.status()).toBe(200);
const text = await r.text();
expect(text).toContain('neurontechnologies.ai');
expect(text).not.toContain('run.app');
expect(text).not.toContain('stage');
expect(text).toContain('<urlset');
});
// The landing page must have JSON-LD structured data
test('Landing page has valid JSON-LD structured data', async ({ page }) => {
await page.goto('/');
const schemaContent = await page.locator('script[type="application/ld+json"]').textContent();
expect(schemaContent).toBeTruthy();
const parsed = JSON.parse(schemaContent!);
// Must be an object with @context at minimum
expect(parsed['@context']).toBeTruthy();
});