Bendik Aagaard LynghaugandClaude Fable 5 0221253cba Gesture input type, redoal-relay client, gitea_releases, content-driven branding
Four coupled additions that let one portal build serve a second face
(redoal.com) next to uhhm.no:

- type: gesture requirement - gesture.js draws a single stroke on a
  DPR-aware canvas (pointer events, touch-action: none), mirrors
  {points, key} into the paired hidden input prosekit-style, and -
  when content declares relay: wss://... - speaks the redoal-relay
  protocol: announce on stroke end, ghost the ack's decoded key path,
  show echoes of similar strokes as thumbnails. Offline/broken relay
  degrades to a plain drawing input; the widget handle's stop()
  closes the socket on SPA navigation (yes.js lifecycle, not
  prosekit's fire-and-forget). Submit re-parses gesture values so the
  bucket stores a real object, not double-encoded JSON.
- gitea_releases resource source - token-authenticated
  /repos/{owner}/{repo}/releases, for advertising a private repo's
  releases (content pins url: null - private html_urls 404 publicly).
- site.yaml branding - optional, at the content repo root: title,
  wordmark, hero {kind: yes|gesture|plain, relay}. Absent file means
  the historical uhhm look, so uhhm changes nothing without a content
  edit. Hot-swapped with questions/aggregates on content reload;
  question_lint validates it in both --path and --repo modes.
- deploy.yml ships the same build twice: uhhm-portal (3010) as
  before, redoal-portal (3020, CONTENT_REPO=redoal/questions,
  redoal.com vhost). Needs host prep + REDOAL_OAUTH2_* repo
  secrets/vars before the new steps succeed.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-23 13:03:29 +02:00
2026-08-12 23:30:29 +02:00

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's id is its URL; qualifies gates it to a Kanidm group.
  • State machines as content (src/aggregates/): aggregates.yaml declares 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 on portal.answers.submitted for 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-declared jq filter.
  • Review desks: any Kv resource with transitions renders 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 the questions repo'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.

S
Description
A Leptos/Axum onboarding portal — question/alternative content engine with a crypto-chain audit trail, Kanidm-gated content, and NATS-backed resources.
Readme
1.4 MiB
Languages
Rust 83.5%
JavaScript 10.3%
CSS 6.2%