# portal The question runtime behind [uhhm.no](https://uhhm.no). Every page, form, review desk, and state machine it serves is declared in the [`questions`](https://project.uhhm.no/uhhm/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 `. ## Development ```sh 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/`, flip the `current` symlink, restart `app@uhhm-portal`, reload Caddy. Content changes never come through here — they hot-reload live from the `questions` repo.