Compare commits

..

22 Commits

Author SHA1 Message Date
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
21 changed files with 2848 additions and 273 deletions
+52 -8
View File
@@ -62,10 +62,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
@@ -102,12 +98,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"
+106 -35
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,63 +55,116 @@ 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
# ── 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=/opt/el/el-compiler/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 ─────────────────────────────────────────
- 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.
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
- 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}" \
.
@@ -107,8 +181,6 @@ jobs:
-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"
@@ -125,4 +197,3 @@ jobs:
docker stop dev-smoke && docker rm dev-smoke || true
echo "Dev smoke test FAILED"
exit 1
+66 -16
View File
@@ -33,19 +33,23 @@ jobs:
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.
# 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).
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)"
set -euo pipefail
COMMIT_MSG=$(git log -1 --pretty=format:"%s" 2>/dev/null || true)
echo "Merge commit: $COMMIT_MSG"
# Gitea merge commits: "Merge pull request '...' (#N) from dev into stage"
# Direct branch merges: "Merge branch 'dev' into stage"
if echo "$COMMIT_MSG" | grep -qE " from dev into stage$| 'dev' into stage$"; 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: Checkout
uses: actions/checkout@v4
@@ -68,10 +72,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:
@@ -100,10 +100,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'
run: |
./build-stage.sh "${{ steps.tag.outputs.tag }}"
set -euo pipefail
docker pull us-central1-docker.pkg.dev/neuron-785695/neuron-ci/ci-base:stage
CID=$(docker create us-central1-docker.pkg.dev/neuron-785695/neuron-ci/ci-base:stage)
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:stage-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:stage-latest"
BIN
View File
Binary file not shown.
Binary file not shown.
+15 -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 \
+1 -1
View File
@@ -703,7 +703,7 @@ 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 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- 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("\\\""));
+6
View File
@@ -325,3 +325,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; }
+3 -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,6 @@ build {
target "release"
entry "src/main.el"
output "dist/"
c_source "dist/web_stubs.c"
c_source "dist/vessel_stubs.c"
}
+1325 -66
View File
File diff suppressed because it is too large Load Diff
+134 -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,108 @@ 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);
#ifdef __cplusplus
}
#endif
File diff suppressed because it is too large Load Diff
+19 -33
View File
@@ -11,6 +11,17 @@
// 4. User fills name, email, card - submits
// 5. stripe.confirmPayment() redirects to /marketplace/success
fn checkout_nav_html() -> String {
<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>
}
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")
@@ -53,17 +64,9 @@ fn checkout_page(plan: String, pub_key: String) -> String {
<li>2 devices included</li>"
} }
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;">
let main_html: String = <main style="min-height: 100vh; padding: clamp(6rem, 14vh, 9rem) 2rem 4rem;">
<div class="checkout-shell">
<!-- Left: order summary -->
@@ -236,23 +239,14 @@ fn checkout_page(plan: String, pub_key: String) -> String {
</div>
</main>
<!-- 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 style_html: String = checkout_style_html()
<style>
.checkout-shell {
max-width: 960px;
margin: 0 auto;
display: grid;
grid-template-columns: 1fr 1fr;
gap: 5rem;
align-items: start;
}
@media (max-width: 720px) {
.checkout-shell { grid-template-columns: 1fr; gap: 3rem; }
let free_init_script: String = if is_free { "<script>document.addEventListener('DOMContentLoaded',function(){window.neuronCheckoutFree&&window.neuronCheckoutFree()});</script>" } else { "" }
return nav_html + main_html + "<!-- 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>" + style_html + "<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>" + free_init_script
}
fn checkout_style_html() -> String {
<style>
.checkout-plan-name {
font-family: var(--head);
font-size: clamp(1.5rem, 3vw, 2rem);
@@ -494,12 +488,4 @@ fn checkout_page(plan: String, pub_key: String) -> String {
}
.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}
}
+4 -6
View File
@@ -166,9 +166,7 @@ fn enterprise() -> String {
</div>
</div>
</section>
<style>
<style>
.ent-inquiry-form {
display: grid;
grid-template-columns: 1fr 1fr;
@@ -179,7 +177,7 @@ fn enterprise() -> String {
.ent-inquiry-form > div[style*="grid-column:1/-1"],
.ent-inquiry-form > div[style*="grid-column: 1 / -1"] { grid-column: 1; }
}
</style>
<script src="/js/enterprise.js" defer></script>
</style>
<script src="/js/enterprise.js" defer></script>
</section>
}
+1 -1
View File
@@ -82,10 +82,10 @@ fn environmental() -> String {
</div>
</div>
</section>
<style>
@media (max-width: 768px) {
.env-grid { grid-template-columns: 1fr !important; gap: 2rem !important; }
}
</style>
</section>
}
+2 -2
View File
@@ -12,9 +12,9 @@
// 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\":[]}"
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)
+53 -3
View File
@@ -76,17 +76,59 @@ 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';
}
}
window.neuronDemoReset = function() {
if (_headerResetInterval) { clearInterval(_headerResetInterval); _headerResetInterval = null; }
clearSession();
session = { messages: [], count: 0, context: '' };
msgCount = 0;
@@ -236,6 +278,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);
+4
View File
@@ -1084,6 +1084,10 @@ fn handle_request_inner(method: String, path: String, body: String) -> String {
if str_eq(msg, "") {
return "{\"error\":\"message required\"}"
}
// Input length guard: ~2000 tokens 8000 characters
if str_len(msg) > 8000 {
return "{\"error\":\"Message too long. Please keep your message under 8000 characters.\"}"
}
// 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)
+2 -2
View File
@@ -133,8 +133,6 @@ fn marketplace() -> String {
<script src="/js/marketplace.js" defer></script>
</section>
<style>
#marketplace { padding: 6rem 0; }
.marketplace-header { margin-bottom: 4rem; }
@@ -229,4 +227,6 @@ fn marketplace() -> String {
}
.dev-textarea { resize: vertical; min-height: 120px; }
</style>
</section>
}
+1 -2
View File
@@ -49,7 +49,6 @@ fn nav() -> String {
<a href="/#pricing" class="nav-mobile-cta">Get Access</a>
</div>
</div>
<script src="/js/nav.js" defer></script>
</nav>
<script src="/js/nav.js" defer></script>
}
+5 -4
View File
@@ -1966,7 +1966,8 @@ fn page_open() -> String {
}
fn page_close() -> String {
return <script src="/js/chat-widget.js" defer></script>
let widgets: String = <div id="page-widgets">
<script src="/js/chat-widget.js" defer></script>
<!-- Neuron Demo Chat Widget -->
<div id="neuron-demo-btn">
@@ -1997,7 +1998,7 @@ fn page_close() -> String {
<div id="neuron-demo-messages"></div>
<div id="neuron-demo-turnstile" style="padding:0.75rem 1rem 0;transition:opacity 0.6s,max-height 0.6s;overflow:hidden;max-height:80px"></div>
<div id="neuron-demo-input-row" style="display:none">
<input type="text" id="neuron-demo-text" placeholder="Ask me anything..." autocomplete="off">
<input type="text" id="neuron-demo-text" placeholder="Ask me anything..." autocomplete="off" maxlength="8000">
<button id="neuron-demo-send" onclick="neuronDemoSend()">Send</button>
</div>
</div>
@@ -2023,6 +2024,6 @@ fn page_close() -> String {
</div>
<script src="/js/styles.js" defer></script>
</body>
</html>
</div>
return widgets + "</body></html>"
}