People can now buy tickets, from the web UI, with capacity enforcement — the heart of the model. - Showing page (events): a 🎟 Tickets section (host/blog--showing-extras) shows capacity/sold + a Buy form per Offering (ticket type + price). host/blog--showing-capacity = the showing's override else its calendar's screen's default (via on-calendar → has-calendar reverse). - host/blog-buy-ticket (events): CAPACITY-CHECKED (sold < capacity), then POSTs to shop /ticket and records showing --sold--> ticket. Sold out → the Buy form is replaced by 'sold out'. - host/blog-ticket (shop): issues a Ticket (is-a ticket, for showing, bought-as offering, owned-by the person's email) + registers the person on the identity peer. - host/blog-person (identity): find-or-create a Person keyed by email (login-optional) → person:<id>. - IDENTITY is a new 4th fed-sx peer (sx_identity, SX_DOMAIN=identity, id.rose-ash.com-ready); shop gets SX_IDENTITY_BASE. serve.sh gains shop 'ticket' type + identity 'person' type seeds. LIVE end-to-end: events.rose-ash.com/<showing> → Buy adult (alice@example.com) → sold 0→1, a ticket on market.rose-ash.com, a person on identity. blog 218/218. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
364 lines
18 KiB
Bash
Executable File
364 lines
18 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
# host-on-sx live server launcher. Loads the kernel stdlib, the subsystem
|
|
# libraries, and the host modules into one sx_server process, then calls
|
|
# (host/serve PORT ...) which binds the native http-listen server to the
|
|
# Dream-shaped host app. Runs in the FOREGROUND (http-listen blocks), so this
|
|
# doubles as a container entrypoint and a local launcher.
|
|
#
|
|
# Usage:
|
|
# bash lib/host/serve.sh # serve on $HOST_PORT (default 8910)
|
|
# HOST_PORT=8920 bash lib/host/serve.sh # pick a port
|
|
#
|
|
# The module list is kept identical to lib/host/conformance.sh so what serves is
|
|
# exactly what the suites verify.
|
|
|
|
set -uo pipefail
|
|
# Project root: SX_PROJECT_DIR in containers (set to /app by the compose stack),
|
|
# else the git toplevel for local runs.
|
|
cd "${SX_PROJECT_DIR:-$(git rev-parse --show-toplevel 2>/dev/null || echo .)}"
|
|
|
|
SX_SERVER="${SX_SERVER:-hosts/ocaml/_build/default/bin/sx_server.exe}"
|
|
if [ ! -x "$SX_SERVER" ]; then
|
|
SX_SERVER="/root/rose-ash/hosts/ocaml/_build/default/bin/sx_server.exe"
|
|
fi
|
|
if [ ! -x "$SX_SERVER" ]; then
|
|
echo "ERROR: sx_server.exe not found." >&2
|
|
exit 1
|
|
fi
|
|
|
|
PORT="${HOST_PORT:-8910}"
|
|
|
|
# Self-warm the SERVING JIT. The FIRST HTTP request after a restart pays a one-time
|
|
# ~2.5s cost: the shared request path (session middleware + router + instrumented
|
|
# handlers + blog render) JIT-compiles under the http-listen resolver, a context a
|
|
# boot-time render can't reach. So once the server is answering, we GET the hot
|
|
# pages ONCE over real HTTP to absorb that compile before a visitor does (~2.5s ->
|
|
# ~78ms warm). The container image has no curl/wget, so we speak HTTP over bash's
|
|
# /dev/tcp. Runs detached (survives the exec below); exits after warming.
|
|
_warm_serving_jit() {
|
|
local path
|
|
for _ in $(seq 1 180); do
|
|
case "$( { exec 3<>/dev/tcp/127.0.0.1/"$PORT" && printf 'GET /health HTTP/1.0\r\nHost: x\r\n\r\n' >&3 && head -1 <&3; exec 3>&- 3<&-; } 2>/dev/null )" in
|
|
*200*) break ;;
|
|
esac
|
|
sleep 0.5
|
|
done
|
|
for path in / /welcome /nt-live-encore /otel; do
|
|
{ exec 3<>/dev/tcp/127.0.0.1/"$PORT" && printf 'GET %s HTTP/1.0\r\nHost: x\r\n\r\n' "$path" >&3 && cat <&3 >/dev/null; exec 3>&- 3<&-; } 2>/dev/null
|
|
done
|
|
}
|
|
_warm_serving_jit &
|
|
|
|
# TA-live: the background FEDERATION DELIVERY loop. Every 15s, hit /fed-tick (the container has no
|
|
# curl, so speak HTTP over /dev/tcp), which re-follows our target (idempotent — recovers if it was
|
|
# down at boot) and flushes the durable outbox (delivers backlog to followers who are now up). This
|
|
# is the delivery TIMER — federation is eventually-consistent, not best-effort-at-emit-only.
|
|
_fed_delivery_loop() {
|
|
for _ in $(seq 1 240); do
|
|
case "$( { exec 3<>/dev/tcp/127.0.0.1/"$PORT" && printf 'GET /health HTTP/1.0\r\nHost: x\r\n\r\n' >&3 && head -1 <&3; exec 3>&- 3<&-; } 2>/dev/null )" in
|
|
*200*) break ;;
|
|
esac
|
|
sleep 0.5
|
|
done
|
|
while true; do
|
|
{ exec 3<>/dev/tcp/127.0.0.1/"$PORT" && printf 'GET /fed-tick HTTP/1.0\r\nHost: x\r\n\r\n' >&3 && cat <&3 >/dev/null; exec 3>&- 3<&-; } 2>/dev/null
|
|
sleep 15
|
|
done
|
|
}
|
|
_fed_delivery_loop &
|
|
|
|
# Modules: every load line from conformance.sh's MODULES list, minus the ledger
|
|
# (not needed to serve). server.sx supplies host/serve.
|
|
MODULES=(
|
|
"spec/stdlib.sx"
|
|
"lib/r7rs.sx"
|
|
"lib/apl/runtime.sx"
|
|
"lib/datalog/tokenizer.sx"
|
|
"lib/datalog/parser.sx"
|
|
"lib/datalog/unify.sx"
|
|
"lib/datalog/db.sx"
|
|
"lib/datalog/builtins.sx"
|
|
"lib/datalog/aggregates.sx"
|
|
"lib/datalog/strata.sx"
|
|
"lib/datalog/eval.sx"
|
|
"lib/datalog/api.sx"
|
|
"lib/datalog/magic.sx"
|
|
"lib/acl/schema.sx"
|
|
"lib/acl/facts.sx"
|
|
"lib/acl/engine.sx"
|
|
"lib/acl/explain.sx"
|
|
"lib/acl/audit.sx"
|
|
"lib/acl/federation.sx"
|
|
"lib/acl/api.sx"
|
|
"lib/relations/schema.sx"
|
|
"lib/relations/engine.sx"
|
|
"lib/relations/api.sx"
|
|
"lib/relations/explain.sx"
|
|
"lib/relations/federation.sx"
|
|
"lib/relations/tree.sx"
|
|
"lib/feed/normalize.sx"
|
|
"lib/feed/stream.sx"
|
|
"lib/feed/api.sx"
|
|
"lib/persist/event.sx"
|
|
"lib/persist/backend.sx"
|
|
"lib/persist/log.sx"
|
|
"lib/persist/kv.sx"
|
|
"lib/persist/api.sx"
|
|
"lib/persist/durable.sx"
|
|
"spec/render.sx"
|
|
"web/adapter-html.sx"
|
|
"lib/dream/types.sx"
|
|
"lib/dream/json.sx"
|
|
"lib/dream/auth.sx"
|
|
"lib/dream/error.sx"
|
|
"lib/dream/form.sx"
|
|
"lib/dream/session.sx"
|
|
"lib/dream/router.sx"
|
|
"lib/host/handler.sx"
|
|
"lib/host/middleware.sx"
|
|
"lib/host/session.sx"
|
|
"lib/host/auth.sx"
|
|
"lib/host/sxtp.sx"
|
|
"lib/host/router.sx"
|
|
"lib/host/static.sx"
|
|
"lib/host/sx/relate-picker.sx"
|
|
"lib/host/sx/kg-cards.sx"
|
|
"lib/host/feed.sx"
|
|
"lib/host/relations.sx"
|
|
"lib/host/compose.sx"
|
|
"lib/host/execute.sx"
|
|
"lib/host/behavior.sx"
|
|
"lib/host/flows.sx"
|
|
"lib/host/ra.sx"
|
|
"lib/host/ta.sx"
|
|
"lib/host/htmlsx.sx"
|
|
"lib/host/blog.sx"
|
|
"lib/host/server.sx"
|
|
"lib/host/otel.sx"
|
|
)
|
|
|
|
# Admin login credentials + session signing secret. Override via the container
|
|
# env; the in-source defaults are dev-only. The blog write routes are now GUARDED
|
|
# (session login or Bearer), so these gate publishing on blog.rose-ash.com.
|
|
ADMIN_USER="${SX_ADMIN_USER:-admin}"
|
|
ADMIN_PASS="${SX_ADMIN_PASSWORD:-letmein}"
|
|
SESSION_SECRET="${SX_SESSION_SECRET:-rose-ash-host-dev-secret-change-me}"
|
|
|
|
EPOCH=1
|
|
{
|
|
for M in "${MODULES[@]}"; do
|
|
echo "(epoch $EPOCH)"; echo "(load \"$M\")"; EPOCH=$((EPOCH+1))
|
|
done
|
|
# 100% serving JIT — NO host exclude. The serving-JIT perform-in-HO-callback
|
|
# miscompile (map/rest/drop wrong args → blank pages, empty picker) is fixed by
|
|
# two composing pieces: sx-vm-extensions 81177d0e resolves a callback's IO
|
|
# inline (instead of unwinding the native HO loop) WHEN a synchronous resolver
|
|
# is installed, and sx_server.ml's http-listen now installs that resolver (it
|
|
# mirrors cek_run_with_io exactly). So the whole request path — host app +
|
|
# Dream + Datalog — runs under JIT with no exclude. Verified: ephemeral durable
|
|
# server, 100% JIT, zero fallbacks, real content, picker lists candidates.
|
|
# Point the blog at the DURABLE file backend (persists under $SX_PERSIST_DIR),
|
|
# then idempotently seed a welcome post (sx_content = SX element markup, the
|
|
# editor's content model). Re-seeding is a no-op if the slug already exists.
|
|
echo "(epoch $EPOCH)"
|
|
echo "(eval \"(host/blog-use-store! (persist/durable-backend))\")"
|
|
EPOCH=$((EPOCH+1))
|
|
# Rebuild the relations graph from the durable edge store. lib/relations holds
|
|
# the graph in memory only, so without this, related/tags/types vanish on every
|
|
# restart even though the posts persist.
|
|
echo "(epoch $EPOCH)"
|
|
echo "(eval \"(host/blog-load-edges!)\")"
|
|
EPOCH=$((EPOCH+1))
|
|
# P0.3b: rebuild the in-memory publish flow log from the durable store, so /flows
|
|
# survives a restart (the driver persists each effect record under "flowlog").
|
|
echo "(epoch $EPOCH)"
|
|
echo "(eval \"(host/blog-load-flowlog!)\")"
|
|
EPOCH=$((EPOCH+1))
|
|
# P2: rebuild the activity log (the event source) from the durable store, so /activities survives.
|
|
echo "(epoch $EPOCH)"
|
|
echo "(eval \"(host/blog-load-activitylog!)\")"
|
|
EPOCH=$((EPOCH+1))
|
|
# Sessions on the DURABLE store, LAZILY: only a logged-in session (one that
|
|
# writes a field) persists, so a login survives a restart while anonymous /
|
|
# crawler traffic leaves no rows. host/session-init! bumps the per-boot epoch
|
|
# that keeps sids unique across restarts. Then the signing secret + admin
|
|
# credentials, and grant admin "edit" on "blog" so a logged-in session passes
|
|
# the ACL gate on the write routes.
|
|
echo "(epoch $EPOCH)"
|
|
echo "(eval \"(host/session-use-store! (persist/durable-backend))\")"
|
|
EPOCH=$((EPOCH+1))
|
|
echo "(epoch $EPOCH)"
|
|
echo "(eval \"(host/session-init!)\")"
|
|
EPOCH=$((EPOCH+1))
|
|
echo "(epoch $EPOCH)"
|
|
echo "(eval \"(host/session-set-secret! \\\"$SESSION_SECRET\\\")\")"
|
|
EPOCH=$((EPOCH+1))
|
|
echo "(epoch $EPOCH)"
|
|
echo "(eval \"(host/auth-set-admin! \\\"$ADMIN_USER\\\" \\\"$ADMIN_PASS\\\")\")"
|
|
EPOCH=$((EPOCH+1))
|
|
echo "(epoch $EPOCH)"
|
|
echo "(eval \"(acl/load! (list (acl-grant \\\"$ADMIN_USER\\\" \\\"edit\\\" \\\"blog\\\")))\")"
|
|
EPOCH=$((EPOCH+1))
|
|
# Idempotently seed a welcome post (sx_content = SX element markup, the editor's
|
|
# content model). Re-seeding is a no-op if the slug already exists.
|
|
echo "(epoch $EPOCH)"
|
|
echo "(eval \"(host/blog-seed! \\\"welcome\\\" \\\"Welcome to the SX host\\\" \\\"(article (h1 \\\\\\\"Welcome to the SX host\\\\\\\") (p \\\\\\\"Rendered by lib/host via render-to-html, from the durable SX store.\\\\\\\"))\\\" \\\"published\\\")\")"
|
|
EPOCH=$((EPOCH+1))
|
|
# Seed the root type-posts (type, tag) — types ARE posts. Idempotent.
|
|
echo "(epoch $EPOCH)"
|
|
echo "(eval \"(host/blog-seed-types!)\")"
|
|
EPOCH=$((EPOCH+1))
|
|
# RA-LIVE: point at the durable-execution KERNEL, add it to the runner fleet, register the durable
|
|
# "blog-digest" DAG (needs {suspend} → routes to the kernel), and declare a DURABLE behavior on the
|
|
# article: an UPDATE (a published edit) runs the kernel flow (newsletter suspends until resumed,
|
|
# urgent completes now). create→publish stays SYNCHRONOUS (exec-fold). Then reload the registry +
|
|
# the pending log. host/blog--kernel-base is a mutable define set here.
|
|
echo "(epoch $EPOCH)"
|
|
echo "(eval \"(set! host/blog--kernel-base \\\"http://sx_kernel:8930\\\")\")"
|
|
EPOCH=$((EPOCH+1))
|
|
echo "(epoch $EPOCH)"
|
|
echo "(eval \"(host/blog--add-runner! (host/ra--make-kernel-runner host/blog--kernel-base))\")"
|
|
EPOCH=$((EPOCH+1))
|
|
echo "(epoch $EPOCH)"
|
|
echo "(eval \"(host/blog--register-dag! \\\"blog-digest\\\" {:erl-flow \\\"blog_digest\\\" :needs (list \\\"effect\\\" \\\"branch\\\" \\\"suspend\\\")})\")"
|
|
EPOCH=$((EPOCH+1))
|
|
# DOMAIN-SPECIFIC types + behaviors — behaviors are declared on TYPES; SX_DOMAIN picks which.
|
|
if [ "${SX_DOMAIN:-blog}" = "events" ]; then
|
|
# The EVENTS domain: a "calendar" type whose ON-ALLOCATE behavior links a post federated from
|
|
# blog. When a directed "allocate" activity arrives at /inbox, the calendar behavior fires the
|
|
# allocate-link DAG (an execute-fold effect). This is a cross-domain, type-declared reaction.
|
|
echo "(epoch $EPOCH)"
|
|
echo "(eval \"(host/blog-seed! \\\"calendar\\\" \\\"Calendar\\\" \\\"(article (h1 \\\\\\\"Calendar\\\\\\\") (p \\\\\\\"Posts allocated to this calendar.\\\\\\\"))\\\" \\\"published\\\")\")"
|
|
EPOCH=$((EPOCH+1))
|
|
# a concrete calendar instance ("main") posts get allocated to.
|
|
echo "(epoch $EPOCH)"
|
|
echo "(eval \"(host/blog-seed! \\\"main\\\" \\\"Main Calendar\\\" \\\"(article (h1 \\\\\\\"Main Calendar\\\\\\\"))\\\" \\\"published\\\")\")"
|
|
EPOCH=$((EPOCH+1))
|
|
echo "(epoch $EPOCH)"
|
|
echo "(eval \"(host/blog-relate! \\\"main\\\" \\\"calendar\\\" \\\"is-a\\\")\")"
|
|
EPOCH=$((EPOCH+1))
|
|
echo "(epoch $EPOCH)"
|
|
echo "(eval \"(host/blog--register-dag! \\\"allocate-link\\\" (quote (effect relate (field \\\"target\\\") \\\"allocated\\\" (field \\\"slug\\\"))))\")"
|
|
EPOCH=$((EPOCH+1))
|
|
echo "(epoch $EPOCH)"
|
|
echo "(eval \"(host/blog--set-type-behavior! \\\"calendar\\\" (list {\\\"verb\\\" \\\"allocate\\\" \\\"type\\\" \\\"article\\\" \\\"dag\\\" \\\"allocate-link\\\"}))\")"
|
|
EPOCH=$((EPOCH+1))
|
|
# an "event" type (events scheduled on a calendar); point at the shop peer for ticket orders.
|
|
echo "(epoch $EPOCH)"
|
|
echo "(eval \"(host/blog-seed! \\\"event\\\" \\\"Event\\\" \\\"(article (h1 \\\\\\\"Event\\\\\\\"))\\\" \\\"published\\\")\")"
|
|
EPOCH=$((EPOCH+1))
|
|
echo "(epoch $EPOCH)"
|
|
echo "(eval \"(host/blog--set-shop-base! \\\"${SX_SHOP_BASE:-}\\\")\")"
|
|
EPOCH=$((EPOCH+1))
|
|
# The CINEMA model: Cinema → Screen → Calendar, Film, TicketType, Showing, Offering.
|
|
echo "(epoch $EPOCH)"
|
|
echo "(eval \"(host/blog-seed-cinema!)\")"
|
|
EPOCH=$((EPOCH+1))
|
|
elif [ "${SX_DOMAIN:-blog}" = "shop" ]; then
|
|
# The SHOP domain: "order" + "ticket" types. POST /order creates an order; POST /ticket issues a
|
|
# cinema ticket (for a showing, bought-as an offering, owned-by a person) + registers the person
|
|
# on the identity peer. SX_IDENTITY_BASE points at that peer.
|
|
echo "(epoch $EPOCH)"
|
|
echo "(eval \"(host/blog-seed! \\\"order\\\" \\\"Order\\\" \\\"(article (h1 \\\\\\\"Order\\\\\\\"))\\\" \\\"published\\\")\")"
|
|
EPOCH=$((EPOCH+1))
|
|
echo "(epoch $EPOCH)"
|
|
echo "(eval \"(host/blog-seed! \\\"ticket\\\" \\\"Ticket\\\" \\\"(article (h1 \\\\\\\"Ticket\\\\\\\"))\\\" \\\"published\\\")\")"
|
|
EPOCH=$((EPOCH+1))
|
|
echo "(epoch $EPOCH)"
|
|
echo "(eval \"(host/blog--set-identity-base! \\\"${SX_IDENTITY_BASE:-}\\\")\")"
|
|
EPOCH=$((EPOCH+1))
|
|
elif [ "${SX_DOMAIN:-blog}" = "identity" ]; then
|
|
# The IDENTITY domain: a "person" type. POST /person?email= find-or-creates a login-optional
|
|
# Person keyed by a contact-id (email). The shop calls this when a ticket is bought.
|
|
echo "(epoch $EPOCH)"
|
|
echo "(eval \"(host/blog-seed! \\\"person\\\" \\\"Person\\\" \\\"(article (h1 \\\\\\\"Person\\\\\\\"))\\\" \\\"published\\\")\")"
|
|
EPOCH=$((EPOCH+1))
|
|
else
|
|
# The BLOG domain: article create→publish (sync) + update→blog-digest (durable kernel) + a
|
|
# "category" field for the edit form; and point at the events peer for cross-domain allocate.
|
|
echo "(epoch $EPOCH)"
|
|
echo "(eval \"(host/blog--set-type-behavior! \\\"article\\\" (list {\\\"verb\\\" \\\"create\\\" \\\"type\\\" \\\"article\\\" \\\"dag\\\" \\\"publish\\\"} {\\\"verb\\\" \\\"update\\\" \\\"type\\\" \\\"article\\\" \\\"dag\\\" \\\"blog-digest\\\"}))\")"
|
|
EPOCH=$((EPOCH+1))
|
|
echo "(epoch $EPOCH)"
|
|
echo "(eval \"(host/blog--set-fields! \\\"article\\\" (list {:name \\\"category\\\" :type \\\"text\\\"}))\")"
|
|
EPOCH=$((EPOCH+1))
|
|
echo "(epoch $EPOCH)"
|
|
echo "(eval \"(host/blog--set-events-base! \\\"${SX_EVENTS_BASE:-}\\\")\")"
|
|
EPOCH=$((EPOCH+1))
|
|
fi
|
|
# P1: gather the types' declared :behavior bindings into the registry the trigger match
|
|
# consults (so publishing an article fires its declared on-publish DAG, runner derived).
|
|
echo "(epoch $EPOCH)"
|
|
echo "(eval \"(host/blog--load-behaviors!)\")"
|
|
EPOCH=$((EPOCH+1))
|
|
echo "(epoch $EPOCH)"
|
|
echo "(eval \"(host/blog-load-pendinglog!)\")"
|
|
EPOCH=$((EPOCH+1))
|
|
# TA-live ACTOR MODEL: our actor id (SX_ACTOR) + base URL (SX_SELF_URL — followers POST to
|
|
# <base>/inbox). Load our persisted followers. If SX_FOLLOW is set, follow that actor's base at
|
|
# boot (POST a follow to its /inbox), so it delivers ITS activities to us. Follower-based delivery
|
|
# replaces the static peer list; the background loop re-follows (idempotent) if the target was down.
|
|
echo "(epoch $EPOCH)"
|
|
echo "(eval \"(host/blog--set-actor! \\\"${SX_ACTOR:-site}\\\" \\\"${SX_SELF_URL:-}\\\")\")"
|
|
EPOCH=$((EPOCH+1))
|
|
echo "(epoch $EPOCH)"
|
|
echo "(eval \"(host/blog-load-followers!)\")"
|
|
EPOCH=$((EPOCH+1))
|
|
echo "(epoch $EPOCH)"
|
|
echo "(eval \"(host/blog--set-follow-target! \\\"${SX_FOLLOW:-}\\\")\")"
|
|
EPOCH=$((EPOCH+1))
|
|
# TA-live: the shared federation secret (peers sign every POST; /inbox verifies). Empty = open.
|
|
echo "(epoch $EPOCH)"
|
|
echo "(eval \"(host/blog--set-fed-secret! \\\"${SX_FED_SECRET:-}\\\")\")"
|
|
EPOCH=$((EPOCH+1))
|
|
# TA-live: rebuild the durable outbox + RETRY any deliveries that were pending from before a
|
|
# restart (a peer that was down gets its backlog once it + we are back up).
|
|
echo "(epoch $EPOCH)"
|
|
echo "(eval \"(host/blog-load-outbox!)\")"
|
|
EPOCH=$((EPOCH+1))
|
|
echo "(epoch $EPOCH)"
|
|
echo "(eval \"(host/blog--flush-outbox!)\")"
|
|
EPOCH=$((EPOCH+1))
|
|
# Seed a live demo of the composition fold (plans/composition-objects.md): /compose-demo
|
|
# is one composition object rendered by host/comp-render — renders differently by context.
|
|
echo "(epoch $EPOCH)"
|
|
echo "(eval \"(host/blog-seed-compose-demo!)\")"
|
|
EPOCH=$((EPOCH+1))
|
|
# Seed the EXECUTE-fold demo (composition step 7): /workflow-demo runs ONE composition
|
|
# object through host/exec-run — the same algebra as render, folded to an effect log.
|
|
echo "(epoch $EPOCH)"
|
|
echo "(eval \"(host/blog-seed-workflow-demo!)\")"
|
|
EPOCH=$((EPOCH+1))
|
|
# Seed a REAL imported blog post (rose-ash.com/nt-live-encore) decomposed into the :body
|
|
# composition — so the import survives store wipes, reseeded on boot like the demos.
|
|
echo "(epoch $EPOCH)"
|
|
echo "(eval \"(host/blog-seed-nt-live-encore!)\")"
|
|
EPOCH=$((EPOCH+1))
|
|
# Seed the layer-2 demo: a Landing type with TWO composition fields (:body + :aside) + a
|
|
# populated instance — so the two-field composition editor + render show side by side.
|
|
echo "(epoch $EPOCH)"
|
|
echo "(eval \"(host/blog-seed-landing-demo!)\")"
|
|
EPOCH=$((EPOCH+1))
|
|
# Load relation metadata (symmetry/labels) from the relation-posts into the
|
|
# in-memory cache, so render paths read it without a (VmSuspending) durable read.
|
|
echo "(epoch $EPOCH)"
|
|
echo "(eval \"(host/blog-load-rel-kinds!)\")"
|
|
EPOCH=$((EPOCH+1))
|
|
# Index the web-stack .sxbc by content hash so /sx/h/{hash} can serve them
|
|
# immutably and the shell can emit the data-sx-manifest (content-addressed
|
|
# client module cache). Done once at boot.
|
|
echo "(epoch $EPOCH)"
|
|
echo "(eval \"(host/static-build-sxh-index!)\")"
|
|
EPOCH=$((EPOCH+1))
|
|
echo "(epoch $EPOCH)"
|
|
# Anonymous reads (feed timeline + relations container reads + blog post detail)
|
|
# plus the GUARDED blog write routes: POST /new (editor form ingest), POST/PUT/
|
|
# DELETE /posts behind host/require-user (session login OR Bearer) + ACL. make-app
|
|
# auto-mounts /login + /logout and wraps everything in the signed-session
|
|
# middleware, so a browser logs in then publishes. The bearer resolver is a stub
|
|
# (no API tokens configured) — browser session is the live auth path for now.
|
|
# blog-routes LAST — its GET /:slug catch-all must not shadow the rest. otel/routes must
|
|
# come BEFORE blog-routes, or GET /otel is swallowed by /:slug (only /otel/stream survives).
|
|
echo "(eval \"(host/serve $PORT (list host/static-routes host/feed-routes host/relations-routes otel/routes (host/blog-write-routes (fn (tok) nil)) host/blog-routes))\")"
|
|
} | exec "$SX_SERVER"
|