Compare commits

..

53 Commits

Author SHA1 Message Date
will.anderson 3e230e52e5 ci: touch dist/ to trigger workflow rebuild 2026-05-11 18:27:31 -05:00
will.anderson f06850eb1a Merge pull request 'ci: re-trigger #3' (#117) from ci/trigger-rebuild into stage 2026-05-11 23:25:53 +00:00
will.anderson 0a4d454765 ci: re-trigger #3 2026-05-11 18:25:41 -05:00
will.anderson 7bc2a8e8f6 Merge pull request 'ci: re-trigger build after runner restart' (#116) from ci/trigger-rebuild into stage 2026-05-11 22:59:51 +00:00
will.anderson c4c30f1b33 ci: re-trigger after runner restart 2026-05-11 17:59:40 -05:00
will.anderson ae1a87de98 Merge pull request 'ci: rebuild after registry cleanup' (#115) from ci/trigger-rebuild into stage 2026-05-11 22:34:23 +00:00
will.anderson f5cbc15b43 Merge pull request 'dev → stage: fix HTML escaping in headings and button' (#114) from dev into stage
Stage — Build, push & deploy to marketing-stage / deploy-stage (push) Successful in 8m5s
2026-05-11 22:27:28 +00:00
will.anderson e148e6987d Merge pull request 'Force full El rebuild — strip CGI content' (#111) from dev into stage
Stage — Build, push & deploy to marketing-stage / deploy-stage (push) Successful in 7m44s
2026-05-11 21:43:20 +00:00
will.anderson 9554430b7e Merge pull request 'Also skip El rebuild for workflow-only changes' (#108) from dev into stage
Stage — Build, push & deploy to marketing-stage / deploy-stage (push) Successful in 8m23s
2026-05-11 20:47:04 +00:00
will.anderson 9685a42c7d Merge pull request 'Skip El rebuild for migration/script/test-only changes' (#106) from dev into stage
Stage — Build, push & deploy to marketing-stage / deploy-stage (push) Successful in 11m48s
2026-05-11 20:45:32 +00:00
will.anderson 9650dad951 Merge pull request 'Update CORS test: no-Origin requests are allowed' (#104) from dev into stage
Stage — Build, push & deploy to marketing-stage / deploy-stage (push) Successful in 8m44s
2026-05-11 20:22:39 +00:00
will.anderson c3aec8947a Merge pull request 'Fix stage source guard: fetch origin/dev before ancestry check' (#102) from dev into stage
Stage — Build, push & deploy to marketing-stage / deploy-stage (push) Failing after 9m28s
2026-05-11 19:09:52 +00:00
will.anderson 441d6d7cb5 Fix: idempotent migration policy creation
Stage — Build, push & deploy to marketing-stage / deploy-stage (push) Failing after 12s
2026-05-11 18:56:50 +00:00
will.anderson de9bf25437 Merge pull request 'dev → stage: fix CI migration heredoc YAML parse error' (#98) from dev into stage
Stage — Build, push & deploy to marketing-stage / deploy-stage (push) Failing after 23s
Merge PR #98 from dev into stage
2026-05-11 18:34:15 +00:00
will.anderson a59fdf4baa Merge pull request 'dev → stage: fix supabase-config null-origin CORS bug' (#96) from dev into stage
Merge PR #96 from dev into stage
2026-05-11 18:31:00 +00:00
will.anderson ae633d3f71 Merge pull request 'dev → stage: wire Supabase migrations into CI/CD' (#94) from dev into stage
Merge PR #94 from dev into stage
2026-05-11 18:22:17 +00:00
will.anderson 43b5286fd5 Merge pull request 'dev → stage: pricing buttons, API keys, enterprise contacts' (#92) from dev into stage
Stage — Build, push & deploy to marketing-stage / deploy-stage (push) Failing after 7m55s
Merge dev into stage
2026-05-11 18:06:02 +00:00
will.anderson 04641ed1a3 Merge pull request 'Stage: CI fixes, pricing buttons, API key provisioning' (#90) from fix/stage-ci-paths into stage
Stage — Build, push & deploy to marketing-stage / deploy-stage (push) Failing after 10s
2026-05-11 17:49:37 +00:00
will.anderson f4a202e220 Merge pull request 'dev → stage: CSP unsafe-eval fix' (#89) from dev into stage
Stage — Build, push & deploy to marketing-stage / deploy-stage (push) Failing after 7m59s
Merge dev into stage
2026-05-11 16:45:12 +00:00
will.anderson 3482e7e0f5 Merge pull request 'dev → stage: remove --obfuscate (CSP/eval fix)' (#87) from dev into stage
Stage — Build, push & deploy to marketing-stage / deploy-stage (push) Failing after 2m31s
2026-05-11 16:11:58 +00:00
will.anderson beee0f99a7 Merge pull request 'Stage: fix Stripe CDN mock + free-plan waitForLoadState sync' (#85) from dev into stage
Stage: fix Stripe CDN mock + free-plan waitForLoadState sync
2026-05-11 15:36:35 +00:00
will.anderson 4b70e8c186 Merge pull request 'Fix Stripe CDN mock override and free-plan sync guards in E2E tests' (#83) from fix/stage-ci-paths into stage
Stage — Build, push & deploy to marketing-stage / deploy-stage (push) Failing after 11s
Fix Stripe CDN mock override and free-plan sync guards in E2E tests
2026-05-11 14:55:22 +00:00
will.anderson f9a5f93070 Merge pull request 'Stage: fix CI JS corruption from obfuscator stdout + flaky test guards' (#82) from dev into stage
Stage — Build, push & deploy to marketing-stage / deploy-stage (push) Failing after 20m46s
Stage: fix CI JS corruption + flaky test guards
2026-05-11 14:16:48 +00:00
will.anderson 8e2deab5cb Merge pull request 'Stage: free plan age verification + soul demo personalization' (#80) from dev into stage
Stage — Build, push & deploy to marketing-stage / deploy-stage (push) Failing after 23m41s
Stage: free plan age verification + soul demo personalization
2026-05-11 07:05:52 +00:00
will.anderson ddeca2250e Merge pull request 'dev → stage: CI paths + comprehensive checkout tests' (#78) from dev into stage
Stage — Build, push & deploy to marketing-stage / deploy-stage (push) Failing after 20m10s
2026-05-11 06:21:21 +00:00
will.anderson d228701828 Merge pull request 'dev → stage: comprehensive checkout + Stripe tests' (#76) from dev into stage
Merge: dev into stage — comprehensive checkout + Stripe tests
2026-05-11 06:19:33 +00:00
will.anderson 41f27e83aa Merge pull request 'test: full Playwright + API test suite for stage' (#74) from dev into stage
Stage — Build, push & deploy to marketing-stage / deploy-stage (push) Successful in 6m43s
Merge dev into stage
2026-05-11 05:29:33 +00:00
will.anderson 533436e2c2 Merge pull request 'security: pentest fixes — deploy to stage' (#70) from dev into stage
Stage — Build, push & deploy to marketing-stage / deploy-stage (push) Successful in 3m32s
security: pentest fixes — deploy to stage
2026-05-11 04:57:20 +00:00
will.anderson aeea037e6f Merge pull request 'feat: auth-gate demo chat + budget circuit breaker' (#68) from dev into stage
Stage — Build, push & deploy to marketing-stage / deploy-stage (push) Successful in 3m53s
feat: auth-gate demo chat + budget circuit breaker
2026-05-11 04:45:56 +00:00
will.anderson 41bad94368 Merge pull request 'feat: scale fixes — max-instances, asset caching, shared rate limits, global cap' (#66) from dev into stage
Stage — Build, push & deploy to marketing-stage / deploy-stage (push) Successful in 4m15s
feat: scale fixes — max-instances, asset caching, shared rate limits, global cap
2026-05-11 03:12:54 +00:00
will.anderson 3020b4e902 Merge pull request 'feat: extract soul-demo into standalone Cloud Run service' (#64) from dev into stage
Stage — Build, push & deploy to marketing-stage / deploy-stage (push) Successful in 4m5s
feat: extract soul-demo into standalone Cloud Run service
2026-05-11 02:09:27 +00:00
will.anderson e82425a829 Merge pull request 'deploy: fix HAVE_CURL verification' (#62) from dev into stage
Stage — Build, push & deploy to marketing-stage / deploy-stage (push) Successful in 3m14s
2026-05-11 01:07:49 +00:00
will.anderson c4cdb31529 Merge pull request 'deploy: fix HAVE_CURL — enable chat proxy to soul-demo' (#60) from dev into stage
Stage — Build, push & deploy to marketing-stage / deploy-stage (push) Failing after 57s
deploy: fix HAVE_CURL — enable chat proxy to soul-demo
2026-05-11 01:03:41 +00:00
will.anderson a1c0cc090d Merge pull request 'Deploy: replace k3s with direct soul-demo watchdog' (#58) from dev into stage
Stage — Build, push & deploy to marketing-stage / deploy-stage (push) Successful in 2m54s
Merge dev into stage
2026-05-11 00:47:16 +00:00
will.anderson 7df96a2273 Merge pull request 'Deploy: fix envelope truncation' (#56) from dev into stage
Stage — Build, push & deploy to marketing-stage / deploy-stage (push) Successful in 3m50s
2026-05-11 00:23:38 +00:00
will.anderson d3b890b739 Merge pull request 'Deploy: fix JS served as JSON envelope' (#54) from dev into stage
Stage — Build, push & deploy to marketing-stage / deploy-stage (push) Successful in 3m11s
Deploy: fix JS served as JSON envelope
2026-05-10 22:34:57 +00:00
will.anderson 3f069eeb79 Merge pull request 'Fix checkout auth (dev → stage)' (#52) from dev into stage
Stage — Build, push & deploy to marketing-stage / deploy-stage (push) Successful in 3m14s
Fix checkout auth (dev → stage)
2026-05-10 22:01:17 +00:00
will.anderson 8676751ed6 Merge pull request 'Fix http handler registration (dev → stage)' (#50) from dev into stage
Stage — Build, push & deploy to marketing-stage / deploy-stage (push) Successful in 3m40s
Merge PR #50: Fix http handler registration (dev → stage)
2026-05-10 18:37:16 +00:00
will.anderson a4f5312069 Merge pull request 'Fix GLIBC_2.38 mismatch: switch base image to ubuntu:24.04' (#48) from dev into stage
Stage — Build, push & deploy to marketing-stage / deploy-stage (push) Successful in 3m37s
Fix GLIBC_2.38 mismatch: switch base image to ubuntu:24.04
2026-05-10 18:02:14 +00:00
will.anderson c76e5a19eb Merge pull request 'Non-blocking entrypoint + k3s --flannel-iface fix' (#46) from dev into stage
Stage — Build, push & deploy to marketing-stage / deploy-stage (push) Failing after 4m8s
Non-blocking entrypoint + k3s flannel-iface fix
2026-05-10 17:55:12 +00:00
will.anderson 58b7b32cdd Single-stage Dockerfile.stage: pre-download k3s on host runner
Stage — Build, push & deploy to marketing-stage / deploy-stage (push) Failing after 12s
2026-05-10 16:27:07 +00:00
will.anderson 0fdabcce86 Merge pull request 'promote: dev → stage' (#42) from dev into stage
Stage — Build, push & deploy to marketing-stage / deploy-stage (push) Failing after 4m46s
promote: dev → stage
2026-05-10 15:57:36 +00:00
will.anderson 79de47de2c Merge pull request 'promote: dev → stage' (#40) from dev into stage
Stage — Build, push & deploy to marketing-stage / deploy-stage (push) Failing after 10m59s
2026-05-10 02:26:35 +00:00
will.anderson 45963154d9 Merge pull request 'promote: dev → stage' (#35) from dev into stage
Stage — Build, push & deploy to marketing-stage / deploy-stage (push) Failing after 14m42s
2026-05-10 01:32:52 +00:00
will.anderson aabaa2ffb0 Merge pull request 'promote: dev → stage' (#33) from dev into stage
Stage — Build, push & deploy to marketing-stage / deploy-stage (push) Failing after 15m11s
2026-05-10 01:07:20 +00:00
will.anderson d5dcb08ec6 Merge pull request 'promote: dev → stage (soul-demo image tar fix)' (#31) from dev into stage
Stage — Build, push & deploy to marketing-stage / deploy-stage (push) Failing after 24s
2026-05-10 01:01:01 +00:00
will.anderson 20a36eeb9e Merge pull request 'promote: dev → stage' (#29) from dev into stage
Stage — Build, push & deploy to marketing-stage / deploy-stage (push) Failing after 3m11s
2026-05-10 00:34:47 +00:00
will.anderson 32a179c24a Merge pull request 'promote: dev → stage' (#27) from dev into stage
Stage — Build, push & deploy to marketing-stage / deploy-stage (push) Failing after 37s
2026-05-10 00:12:47 +00:00
will.anderson 6bc026de19 Merge pull request 'promote: dev → stage' (#25) from dev into stage
Stage — Build, push & deploy to marketing-stage / deploy-stage (push) Failing after 21s
2026-05-09 23:44:30 +00:00
will.anderson 0ae526b72e Merge pull request 'promote: dev → stage' (#23) from dev into stage
Stage — Build, push & deploy to marketing-stage / deploy-stage (push) Failing after 22s
2026-05-09 23:35:56 +00:00
will.anderson 8221aef605 promote: dev → stage
Stage — Build, push & deploy to marketing-stage / deploy-stage (push) Failing after 37s
2026-05-09 18:34:59 +00:00
will.anderson f8487c43a0 Merge branch 'dev' into stage
Stage — Build, push & deploy to marketing-stage / deploy-stage (push) Failing after 14m16s
2026-05-09 17:41:09 +00:00
will.anderson 36b99dd9e2 Merge branch 'dev' into stage
Stage — Build, push & deploy to marketing-stage / deploy-stage (push) Failing after 6s
2026-05-09 17:32:23 +00:00
9 changed files with 40 additions and 115 deletions
+1 -6
View File
@@ -82,12 +82,7 @@ jobs:
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 '^migrations/' | grep -v '^scripts/' | grep -v '^tests/' | grep -v '^\.gitea/' | grep -v '^$' || true)
if [ -z "$CHANGED" ] || [ "$CHANGED" = "unknown" ]; then
# No diff (workflow_dispatch with no new commits, or git error).
# Registry may not have a stage-latest base image, so force full build.
echo "asset_only=false" >> "$GITHUB_OUTPUT"
echo "=> No changed files detected (workflow_dispatch?), forcing full build"
elif [ -z "$NON_ASSET" ]; then
if [ -z "$NON_ASSET" ] && [ "$CHANGED" != "unknown" ]; then
echo "asset_only=true" >> "$GITHUB_OUTPUT"
echo "=> Asset-only change detected, will use fast path"
else
-1
View File
@@ -9,7 +9,6 @@
#
# CI pre-build steps (in stage.yaml):
# - neuron-web: built by `elb build` → dist/neuron-landing
# Last rebuilt: 2026-05-11
FROM ubuntu:24.04
+2 -63
View File
@@ -77,23 +77,6 @@ static _Thread_local int _tl_arena_active = 0;
* Allows serving PNGs and other binary files without strlen truncation. */
static _Thread_local size_t _tl_fs_read_len = 0;
/* Binary body side-channel for http_response().
*
* http_response() normally JSON-encodes the body via jb_emit_escaped(), which
* stops at the first null byte (C-string semantics). Binary files like PNGs
* contain null bytes as early as byte 8 (IHDR chunk length), causing truncation.
*
* When _tl_fs_read_len > 0 at the time http_response() is called, we skip
* JSON-encoding and instead:
* 1. malloc-copy the raw bytes here
* 2. write the sentinel string "__el_binary__" into the envelope body field
* 3. In http_send_response(), detect the sentinel and use these raw bytes
*
* Thread-local so each worker thread has independent storage.
* Lifecycle: set by http_response(), consumed (and freed) by http_send_response(). */
static _Thread_local char* _tl_binary_body = NULL;
static _Thread_local size_t _tl_binary_size = 0;
static void el_arena_track(char* p) {
if (!_tl_arena_active || !p) return;
if (_tl_arena.count >= _tl_arena.cap) {
@@ -1553,22 +1536,10 @@ static void http_send_response(int fd, const char* body) {
}
const char* eff_body = is_envelope ? env_body : body;
int binary_side_channel = 0;
/* Binary side-channel: if the envelope body is the sentinel "__el_binary__",
* http_response() stored the real bytes in _tl_binary_body/_tl_binary_size.
* Substitute them here so http_send_all() sends the correct binary payload. */
if (is_envelope && env_body && strcmp(env_body, "__el_binary__") == 0
&& _tl_binary_body && _tl_binary_size > 0) {
eff_body = _tl_binary_body;
binary_side_channel = 1;
}
/* Use the real byte count from fs_read if available (handles binary files
* with embedded null bytes PNG, WOFF2, etc.). Fall back to strlen for
* normal text/JSON responses where _tl_fs_read_len is 0. */
size_t blen = binary_side_channel ? _tl_binary_size
: (_tl_fs_read_len > 0) ? _tl_fs_read_len : strlen(eff_body);
size_t blen = (_tl_fs_read_len > 0) ? _tl_fs_read_len : strlen(eff_body);
_tl_fs_read_len = 0; /* consume — one-shot per response */
int head_only = _tl_http_head_only;
@@ -1616,13 +1587,6 @@ static void http_send_response(int fd, const char* body) {
if (env_parsed_root) el_release(env_parsed_root);
free(env_body);
free(hdrs.buf);
/* Release binary side-channel if it was used (or left over from an error). */
if (_tl_binary_body) {
free(_tl_binary_body);
_tl_binary_body = NULL;
_tl_binary_size = 0;
}
}
typedef struct {
@@ -1997,14 +1961,6 @@ el_val_t http_response(el_val_t status, el_val_t headers_json, el_val_t body) {
const char* b = EL_CSTR(body);
if (!b) b = "";
/* Capture binary length BEFORE clearing _tl_fs_read_len.
* If the body came from fs_read(), _tl_fs_read_len holds the real byte
* count. jb_emit_escaped() stops at the first NUL byte, so we cannot
* JSON-encode binary data directly. Instead we copy it to a thread-local
* side-channel and write the sentinel "__el_binary__" into the envelope.
* http_send_response() detects the sentinel and uses the side-channel. */
size_t binary_len = _tl_fs_read_len;
/* Clear the fs_read binary-length hint: the envelope we're about to build
* is a fresh JSON string, not the raw file bytes. Without this reset,
* http_worker would use the stale _tl_fs_read_len (= original file size)
@@ -2012,18 +1968,6 @@ el_val_t http_response(el_val_t status, el_val_t headers_json, el_val_t body) {
* http_send_response and http_parse_envelope. */
_tl_fs_read_len = 0;
if (binary_len > 0) {
/* Binary body path: store raw bytes in thread-local, emit sentinel. */
free(_tl_binary_body); /* discard any stale binary from a prior error path */
_tl_binary_body = malloc(binary_len);
if (_tl_binary_body) {
memcpy(_tl_binary_body, b, binary_len);
_tl_binary_size = binary_len;
} else {
_tl_binary_size = 0; /* malloc failed — fall through to empty body */
}
}
JsonBuf out; jb_init(&out);
jb_puts(&out, EL_HTTP_RESPONSE_TAG); /* {"el_http_response":1 */
jb_puts(&out, ",\"status\":");
@@ -2033,12 +1977,7 @@ el_val_t http_response(el_val_t status, el_val_t headers_json, el_val_t body) {
jb_puts(&out, ",\"headers\":");
jb_puts(&out, hj);
jb_puts(&out, ",\"body\":");
if (binary_len > 0 && _tl_binary_body) {
/* Sentinel: http_send_response() will substitute the real bytes. */
jb_puts(&out, "\"__el_binary__\"");
} else {
jb_emit_escaped(&out, b);
}
jb_emit_escaped(&out, b);
jb_putc(&out, '}');
return el_wrap_str(out.buf);
}
+1 -25
View File
@@ -345,31 +345,7 @@ fn checkout_page(plan: String, pub_key: String) -> String {
}
fn checkout_style_html() -> String {
let css: String = ".checkout-shell {
max-width: 980px;
margin: 0 auto;
display: grid;
grid-template-columns: 1fr 1fr;
gap: 4rem;
align-items: start;
}
.checkout-summary {
position: sticky;
top: 2rem;
}
.checkout-form-wrap {
min-width: 0;
}
@media (max-width: 860px) {
.checkout-shell {
grid-template-columns: 1fr;
gap: 2.5rem;
}
.checkout-summary {
position: static;
}
}
.checkout-plan-name {
let css: String = ".checkout-plan-name {
font-family: var(--head);
font-size: clamp(1.5rem, 3vw, 2rem);
font-weight: 600;
+17 -3
View File
@@ -1,6 +1,20 @@
// checkout-free.el -- RETIRED. Free plan now uses the standard Stripe
// payment flow (checkout-stripe.el) with a $0 PaymentIntent for age
// verification. This file is no longer compiled or loaded.
// checkout-free.el -- Free plan: show success panel after auth completes.
// Watches the auth-badge element; when it becomes visible, hides the auth
// section and shows the free-success panel. No card required for free tier.
// Compiled with: elc --target=js --bundle --minify --obfuscate
fn main() -> Void {
native_js("(function() {
var success = document.getElementById('free-success');
var auth = document.getElementById('auth-section');
if (!success) return;
var timer = setInterval(function() {
var badge = document.getElementById('auth-badge');
if (badge && badge.offsetParent !== null) {
if (auth) auth.style.display = 'none';
success.style.display = '';
clearInterval(timer);
}
}, 150);
})()")
}
+3 -3
View File
@@ -31,8 +31,8 @@ fn main() -> Void {
if (spinner) spinner.style.display = loading ? '' : 'none';
}
// Free plan: Stripe SetupIntent for age verification (card saved, never charged).
// Falls through to the same Stripe init path — server returns setup_mode=true for free.
// Free plan has no payment form — bail out entirely.
if (str_eq(PLAN, 'free')) return;
window._neuronMode = 'payment';
var paymentEl = null;
@@ -101,7 +101,7 @@ fn main() -> Void {
if (submitLabel) {
submitLabel.textContent = window._neuronMode === 'setup'
? 'Save my card - no charge today →'
: PLAN === 'free' ? 'Verify age & get started →' : 'Complete purchase →';
: 'Complete purchase →';
}
waitForStripe(function() {
if (!stripe) stripe = Stripe(STRIPE_PK);
+14 -12
View File
@@ -637,7 +637,7 @@ fn handle_request_inner(method: String, path: String, headers: Map, body: String
"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. A card verifies you're 18+ — you won't be charged. Your AI that remembers you, runs on your machine, never resets."
"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."
}
@@ -697,21 +697,23 @@ fn handle_request_inner(method: String, path: String, headers: Map, body: String
}
}
// Free tier: $0 PaymentIntent for age verification (18+ requirement).
// Verifies card is valid. No charge, no capture.
// Note: setup_future_usage cannot be used with amount=0.
// 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_pi_body: String = "amount=0"
+ "&currency=usd"
+ "&payment_method_types[]=card"
let free_si_body: String = "automatic_payment_methods[enabled]=true"
+ "&usage=off_session"
+ "&metadata[plan]=free"
+ "&metadata[purpose]=age_verification"
let free_pi_body = if !str_eq(pi_cus_id, "") { free_pi_body + "&customer=" + pi_cus_id } else { free_pi_body }
let free_pi_resp: String = http_post_form_auth(
"https://api.stripe.com/v1/payment_intents",
free_pi_body,
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)
return free_pi_resp
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
+1 -1
View File
@@ -91,7 +91,7 @@ fn pricing(sold: Int, total: Int) -> String {
el_span("class=\"pricing-price\"", "$0") +
el_span("class=\"pricing-cadence\"", "forever")
) +
el_p("class=\"pricing-tagline\"", "Start building your memory. Card required for age verification — you won't be charged.") +
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(
+1 -1
View File
@@ -78,7 +78,7 @@ fn page_head() -> String {
return page_head_base()
+ page_seo_block(
"Neuron — The AI That Remembers You",
"Every AI resets when you close the tab. Neuron doesn't. Runs on your machine. Remembers everything. Start free.",
"Every AI resets when you close the tab. Neuron doesn't. Runs on your machine. Remembers everything. Start free — no credit card required.",
"/",
"Every other AI forgets you. Neuron doesn't. Runs on your machine, builds a persistent memory over time, and gets sharper the longer you use it. Free tier available."
)