Three changes that belong together, all verified against a real mounted page in headless WebKit: - yes.js's two behavior dials (containment/wiggle) now drift on a smooth two-octave value-noise field at cloud pace (~25s per weather change) instead of following the mouse. Includes a real bug fix found in verification: the hash's final XOR yields a SIGNED 32-bit value in JS, so the "0..1" noise dipped to -0.36 without a reinterpreting >>> 0. - The landing hero is position: sticky, so the piece keeps animating behind the whole page. Cards go translucent with backdrop blur and a soft shadow so the piece reads faintly through and around them (near-opaque fallback where backdrop-filter is unsupported). The hero copy fades out over the first half-screen of scroll - pinned, it ghosted through the cards. The bottom fade gradient is gone: its hard-cut reason disappeared with the canvas behind everything. - Full prefers-color-scheme light theme: warm paper, near-black ink, accent deepened from dusty cyan to teal ink (#8ec2c0 washes out on white), wordmark inverted via filter. yes.js mirrors the palette itself (canvas can't read CSS vars): CMYK process-ink strokes dark enough to carry on paper, raster ghost repainted as multiply-blended gray on white, live re-theme on scheme flip with a trail clear. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
portal
The question runtime behind uhhm.no. Every page,
form, review desk, and state machine it serves is declared in the
questions content repo —
this codebase is the engine that renders, enforces, and records, and
it special-cases none of it.
What the engine provides
- Content-driven pages (
src/content.rs,src/app.rs): YAML loaded from Gitea at boot and hot-swapped on a NATS reload signal; a bad push keeps the last-good content serving. A page'sidis its URL;qualifiesgates it to a Kanidm group. - State machines as content (
src/aggregates/):aggregates.yamldeclares each bucket's states and legal transitions; the engine replays a record's event history and refuses undeclared moves, with optimistic concurrency (CAS on the event log's sequence) against racing decisions. An empty history reseeds from the KV projection, so wiping the event stream strands nothing. - Durable events + projections (
src/events/,src/answers.rs): every submission and decision appends to a JetStream event log and projects into a NATS KV bucket pages read back; every one also publishes onportal.answers.submittedfor automations (n8n) to react to. - Answer chains (
src/chain.rs): each submission hashes its parent(s), so a visitor's path through the questions is a verifiable lineage;?chain=links carry it, and self-service transitions (unsubscribe) authorize by holding one. - Live resources (
src/resource.rs): content can pull a KV bucket, Gitea starred/org repos, or any public JSON URL (SSRF fail-closed), reshaped by a content-declaredjqfilter. - Review desks: any Kv resource with
transitionsrenders rows with per-state action buttons and one shared confirm per alternative — owners walk records through their graphs without bespoke UI per bucket.
Binaries
portal— the server (Leptos SSR + hydrate, Axum underneath).question_lint— headless content validation, run by thequestionsrepo's CI against a prebuilt copy this repo's deploy publishes; also works offline:question_lint --path <dir>.
Development
cargo leptos build # full app (server + wasm)
cargo test --features ssr # engine tests
cargo build --features ssr --bin question_lint
Runtime configuration is env vars (see src/main.rs and
.gitea/workflows/deploy.yml): NATS_URL, CONTENT_REPO/
CONTENT_BRANCH, Kanidm OIDC (KANIDM_URL, OAUTH2_CLIENT_*),
optional GARAGE_* for uploads, GITEA_API_TOKEN for authenticated
resource pulls, AUTOMATION_READ_TOKEN for the automation KV read
endpoint.
Deploy
Pushing main triggers .gitea/workflows/deploy.yml on the
bare-metal runner: release build, ship to
/srv/app/uhhm-portal/releases/<sha>, flip the current symlink,
restart app@uhhm-portal, reload Caddy. Content changes never come
through here — they hot-reload live from the questions repo.