Compare commits
22 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 2447310367 | |||
| 2a3f998827 | |||
| 96fca7ebf7 | |||
| da669c67a1 | |||
| e3e6ec7ade | |||
| 5a8783ff0c | |||
| 032be3a058 | |||
| 6928a33685 | |||
| bb2be6398b | |||
| f7034c990a | |||
| 4ec5558517 | |||
| 0ace906823 | |||
| 067c83f8ff | |||
| 5f35ddde39 | |||
| e6d10fc3d5 | |||
| 7c4c0d9963 | |||
| aedb14f86c | |||
| c24b9b179b | |||
| 3e377e2bb6 | |||
| 9e77c3cbf0 | |||
| 042b9b2b2f | |||
| fef846e6f5 |
@@ -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
@@ -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
@@ -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"
|
||||
|
||||
|
||||
Executable
BIN
Binary file not shown.
Binary file not shown.
+15
-86
@@ -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 \
|
||||
|
||||
Vendored
+1
-1
@@ -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("\\\""));
|
||||
|
||||
Vendored
+6
@@ -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
@@ -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
File diff suppressed because it is too large
Load Diff
+134
-7
@@ -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
@@ -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">← 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">← 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
@@ -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>
|
||||
}
|
||||
|
||||
@@ -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
@@ -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
@@ -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);
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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>"
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user