Commit Graph
86 Commits
Author SHA1 Message Date
Bendik Aagaard Lynghaug 71613b5ef5 Debounce the YES canvas resize handler and skip no-op resizes
Deploy / deploy (push) Successful in 33s
Even with dimensions now sourced from the stable CSS container
(previous commit), assigning canvas.width/height unconditionally
clears the canvas buffer regardless of whether the value actually
changed. Mobile Safari fires `resize` repeatedly *during* the address
-bar hide/show animation, not just once at the end, so the handler
was still clearing+redrawing on every one of those events. Debounce
to let the animation settle, then skip the redraw entirely if the
resolved size didn't change.
2026-08-06 16:24:11 +02:00
Bendik Aagaard Lynghaug f0022cd037 Size the YES canvas from its container, not window.innerHeight
Deploy / deploy (push) Successful in 34s
The rasterized "YES" text (and everything downstream: fontSize, line
rendering) was recomputed from window.innerWidth/innerHeight on every
`resize` event. Mobile Safari fires `resize` continuously as the
address bar hides/shows while scrolling, so the text visibly rescaled
mid-scroll even after .hero-yes's own height was already stabilized
via 100svh. Reading the containing .hero-canvas box's own rendered
size instead ties canvas sizing to that already-stable CSS layout, so
a toolbar-only resize recomputes to the same numbers.
2026-08-06 15:46:41 +02:00
Bendik Aagaard Lynghaug adaaf7f3e3 Add plain-unit fallbacks for svmin/svh in case of unsupported engines
Deploy / deploy (push) Successful in 33s
Couldn't reproduce the reported "hero-yes doesn't show" in a live
WebKitGTK check (svmin/svh both resolved correctly there), but an
engine that doesn't support these newer viewport units would treat
the whole declaration as invalid and drop it - for .hero-yes's height
that means falling through to auto, which collapses the section to
its in-flow content height and clips the canvas via overflow:hidden.
Declaring a plain px/vh value first means an unsupported engine keeps
that instead of falling through further.
2026-08-06 15:37:49 +02:00
Bendik Aagaard Lynghaug a6dfa30f71 Responsive base font size via svmin, not fixed 17px
Deploy / deploy (push) Successful in 34s
Scales with viewport instead of capping at a fixed px value.
svmin (small-viewport min) rather than plain vmin - plain viewport
units recalculate as mobile Safari's address bar hides/shows on
scroll, which would reflow every text size mid-scroll; svmin stays
pinned to the smallest viewport state, matching the stability
.hero-yes already gets from 100svh.
2026-08-06 15:28:32 +02:00
Bendik Aagaard LynghaugandClaude Sonnet 5 2d424d70ed Use the actual UHHM-letters wordmark, not the institutional mark
Deploy / deploy (push) Successful in 37s
The institutional (triple-arch) mark is project.uhhm.no's identity,
not uhhm.no's - swapped the header wordmark for the real letterform
asset (matching infrastructure/assets/brand/wordmark-*.svg).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-06 14:09:20 +02:00
Bendik Aagaard LynghaugandClaude Sonnet 5 bdab23d0f6 Use the real institutional logo for the header wordmark, not plain text
Deploy / deploy (push) Successful in 35s
The header link was just styled "UHHM" text, unrelated to the actual
brand mark - swapped in the institutional logo (the three-arch
"building" mark, matching infrastructure/assets/brand/institutional-*.svg
and Gitea's own homepage logo) instead. Dark-stroke variant only -
portal is a single permanent dark theme, no light mode to switch for.
Same explicit-route treatment the favicon files needed (the /*any
wildcard route would otherwise swallow the request before the static
file handler gets a chance).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-06 13:59:03 +02:00
Bendik Aagaard LynghaugandClaude Sonnet 5 400316f622 Inline encouragements with the submit button, vertically centered
Deploy / deploy (push) Successful in 33s
Was stacked vertically; now a row (encouragements left, button right,
center-aligned), falling back to the stacked layout under 640px where
a row would get cramped.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-06 13:03:43 +02:00
Bendik Aagaard LynghaugandClaude Sonnet 5 a200161334 Fix bucket-404 on empty resources, redesign review actions as select-then-confirm, CSS polish
Deploy / deploy (push) Successful in 35s
get_resource's Kv path treated a not-yet-created bucket (nothing
submitted there yet) as a hard error instead of an empty list -
projects bucket never got created since the backfill found nothing to
migrate, so /review's Projects alternative 404'd outright. Now matches
store_answer's own "doesn't exist yet is normal" posture; a specific
key request still errors, only listing degrades gracefully.

AnswerRow's Invite/Decline buttons fired transition_answer immediately
on click, with no staging step and no visible confirmation once it
landed (the resource list never refetched, so a click barely looked
like it did anything). Redesigned as toggle-select (reusing the
.select-option pattern from the resource-backed select requirement)
plus one explicit Confirm button; a successful transition now refetches
the parent Resource so the row actually reflects the change.

CSS: soften the hard cut where the hero canvas meets the page
background below it (gradient fade over the last few ems, matching
--paper); center the "Asked by X" responsible note with the em-dash
starting its own line; give it and the report button real styling
(previously unstyled default text).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-06 12:42:54 +02:00
Bendik Aagaard LynghaugandClaude Sonnet 5 11c6f3e8f7 Fix favicon (real icon, not an unrelated orange circle) + stack encouragements with the submit button
Deploy / deploy (push) Successful in 35s
favicon.svg was a plain solid circle, unrelated to the actual brand
mark - replaced with the real single-arch icon (matching
infrastructure/assets/icon-{light,dark}.svg), split into two
prefers-color-scheme-scoped files rather than one SVG with an embedded
@media query - the latter doesn't reliably re-evaluate via <link> on
Safari/iOS (confirmed by prior real-device testing on the Gitea side,
see gitea-head.tmpl).

Encouragements used to render right after the description, separated
from the actual submit button by the whole features/requirements
block - moved them into the same wrapper as the form, directly above
the button, so reassurance text and the call to action read together.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-06 12:11:17 +02:00
Bendik Aagaard LynghaugandClaude Sonnet 5 aa18c69be4 Add Organization aggregate (client-as-status), wired but inert
Deploy / deploy (push) Successful in 34s
Prospect -> Client -> PastClient (Prospect -> PastClient allowed
directly too - a prospect that never converted). "Client" is a status
on Organization, not a separate aggregate, per the event-sourcing
plan's design decision. Wired into the same dispatch points as the
other three aggregates (content.rs's bucket map, answers.rs's
create/transition dispatch) so it's ready the moment content
references an "organizations" bucket - nothing does yet, so this lands
compiled and tested but with zero production surface.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-06 12:01:17 +02:00
Bendik Aagaard LynghaugandClaude Sonnet 5 755b796ad1 Resource-backed multi/single-select requirement
Deploy / deploy (push) Successful in 34s
Requirement gains an optional `resource` (reuses ResourceSpec/
ResourceSource/jq wholesale - a resource is a resource whether it's
displayed read-only or offered as choices to pick from) and `id_field`
(which field in each item is its stable id, defaults to _id then id).
`type: select` + `multiple` (already-existing field, previously file
-only) picks single vs multi. New get_requirement_options server fn
shares its auth/fetch/jq logic with get_resource via two extracted
helpers rather than duplicating it.

Submitted value is the selected id (single) or a JSON array of ids
(multi) - a new select_field_map (RwSignal<Vec<String>>, alongside the
existing field_map/file_refs maps, since a multi-select's value is a
set, not a string) threaded through the same nested <For> structure
the other requirement kinds already use.

Content validation extended: a `type: select` requirement declaring no
resource now fails at load time instead of rendering a dead field.
Caught a real bug in my own first version of that check while testing
it - it was nested inside a feature-level resource guard, so it never
ran unless the *feature* also happened to have its own resource.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-06 11:57:15 +02:00
Bendik Aagaard LynghaugandClaude Sonnet 5 43fa496778 Fix recursion-limit build failure in the new item-card renderer
Deploy / deploy (push) Successful in 33s
Same class of bug as the earlier ResourceValue/AnswerRow one: impl
IntoView doesn't erase a component's concrete type within the same
crate, so nested <For> children closures need their own explicit
.into_any() at the call site, not just at the outer return - missing
it on the two new <For>s (item list, item-card-fields) overflowed the
query depth limit on release build. Verified locally with a full
`cargo build --release` this time, not just cargo check.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-06 11:34:18 +02:00
Bendik Aagaard LynghaugandClaude Sonnet 5 bd702bfbae Render generic resource lists as cards, not raw JSON dump
Deploy / deploy (push) Failing after 32s
A jq-shaped resource (e.g. the GiteaStarred showcase) previously fell
through to the raw <pre> JSON dump - same fallback path meant for
truly unstructured data. Added a card renderer for the common case
(array of plain objects): name/title as a linked heading, description
as body text, everything else as small key/value chips - same visual
language as AnswerRow's answer-fields, just for non-Answer data.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-06 11:31:42 +02:00
Bendik Aagaard LynghaugandClaude Sonnet 5 1db558974e Trigger redeploy to pick up PORTAL_GITEA_API_TOKEN secret
Deploy / deploy (push) Successful in 31s
No code change - the secret was just set (Settings -> Actions ->
Secrets), and the running service's env file only gets written at
deploy time, not live.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-06 10:32:39 +02:00
Bendik Aagaard LynghaugandClaude Sonnet 5 3a2a19b79b Add manually-triggered backfill workflow, no terminal/sudo needed
Deploy / deploy (push) Successful in 31s
Runs backfill_events using secrets.PORTAL_NATS_URL directly from the
CI job's own env (same source deploy.yml's env-write step already
uses) - avoids needing a sudo'd terminal session to read
/etc/app/uhhm-portal.env's secrets off disk just to run a one-time
migration tool. workflow_dispatch only, defaults to dry run.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-06 10:09:08 +02:00
Bendik Aagaard LynghaugandClaude Sonnet 5 8f0eead8da Cargo.toml: disambiguate bin-target for cargo-leptos
Deploy / deploy (push) Successful in 1m13s
The new question_lint/backfill_events [[bin]] targets left cargo-leptos
unable to tell which of the three binaries is the actual app server -
broke the last two deploy runs ("Several bin targets found for member
portal"). bin-target = "portal" fixes it; confirmed locally that
`cargo leptos build` gets past config resolution now.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-06 08:57:58 +02:00
Bendik Aagaard LynghaugandClaude Sonnet 5 f6c5d8e05c deploy.yml: write GITEA_API_TOKEN for the new GiteaStarred/OrgRepos resource sources
Deploy / deploy (push) Failing after 3s
Needs the PORTAL_GITEA_API_TOKEN repo secret set (Settings -> Actions ->
Secrets) - a fresh read-only token, not yet wired in as a value.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-06 08:55:27 +02:00
Bendik Aagaard LynghaugandClaude Sonnet 5 ebf4bf91b3 Event-sourced applicant/subscriber/project aggregates, generalized resources
Deploy / deploy (push) Failing after 3s
Replaces the free-string, direct-KV-mutate state model in answers.rs
with a proper event log (events/store.rs, JetStream-backed, CAS via
expected_last_subject_sequence) and three pure state machines
(aggregates/{applicant,subscriber,project}.rs). Closes a real
lost-update race in the old transition_answer (concurrent decisions on
the same item could both win, publishing contradictory events). KV
buckets become best-effort read-model projections, not the source of
truth. Content-declared transition targets are now validated at
load/reload time against the real compiled transition tables, not
accepted as arbitrary strings.

Buckets renamed to describe their content, not their relation to the
app (portal_applicants -> applicants, etc); "inquiry" folded into a
richer "project" concept.

ResourceSpec generalized beyond a single KV bucket: Kv | GiteaStarred |
GiteaOrgRepos | Url sources, with an optional jq filter (via the jaq
crate) to shape live data for the frontend. Url source is SSRF-guarded
(https-only, rejects loopback/private/link-local, real DNS resolve).

New headless question_lint binary (validates content against compiled
transition tables with no NATS/OIDC/server involved) and a one-time
backfill_events binary (dry-run by default) for migrating existing KV
data onto the new event log.

Questions get an optional `responsible` contact plus a lightweight
"report this question" action.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-06 08:52:29 +02:00
Bendik Aagaard Lynghaug 213b1130bb Add self-service transitions, authorized by item possession not group membership
Deploy / deploy (push) Successful in 33s
New Alternative.self_transition: like ResourceSpec.transitions, but for
an anonymous visitor holding one specific item's own chain hash (from
a ?chain= link, the same reference /subscribed?chain=... already
carries) rather than a signed-in owner browsing a whole bucket. email
is a second factor checked against the stored item, not the lookup
key - defense in depth against a leaked/guessed chain hash alone.

AlternativeCard renders it as a single confirm button when both
?chain= and ?email= are present; QuestionView hides the alternative
entirely otherwise, rather than showing a dead card with nothing to
click.

Powers /subscribed's new "Unsubscribe" alternative, which the
newsletter's own recipient links now carry - no email lookup needed,
just the same chain_hash the subscription itself already produced.
2026-08-05 18:44:28 +02:00
Bendik Aagaard Lynghaug 6df895b1d3 Fix get_resource/transition_answer: scope feature lookup to its own alternative
Deploy / deploy (push) Successful in 28s
Both resolved a feature by name flattened across every alternative on
the question, not scoped to the one the caller actually meant - fine
when every question had exactly one alternative with a resource
feature, silently wrong now that /review has three (Applicants,
Inquiries, Subscribers all use the same empty feature name). Every
lookup always resolved to the first alternative's feature - Subscribers
was reading the (empty) Applicants bucket instead of its own, and
Invite/Decline/Mark-handled buttons would have had the same problem
had two of those transitions ever been clicked side by side.

Threaded a new `alternative` parameter through get_resource,
transition_answer, and their client-side callers
(ResourceFeature/ResourceValue/AnswerRow) - the fix lives entirely in
the already-generic resource-fetching machinery, no per-alternative
special-casing.
2026-08-05 15:04:02 +02:00
Bendik Aagaard Lynghaug c73981da2d yes.js: guard against the canvas not being in the DOM yet
Deploy / deploy (push) Successful in 28s
The Rust-side NodeRef gate (previous commit) didn't actually close the
race on fast client-side re-navigation back to / - reproduced the
same crash again after that fix shipped. Harden the actual failure
point directly instead of chasing the exact Leptos/wasm-bindgen
timing: skip setup (not throw) if either canvas is missing.
2026-08-05 14:55:55 +02:00
Bendik Aagaard Lynghaug 00d2a95625 Fix hero-canvas navigation race; style the prosekit editor and add a toolbar
Deploy / deploy (push) Successful in 28s
- Hero: gate RasterizedYES construction on a NodeRef resolving, not
  just "the Effect ran". Root cause of the reported "loading..." stall
  + "RefCell already borrowed" panic - navigating back to / client-side
  could run the Effect before the new <canvas> was actually in the
  DOM, yes.js did an unchecked getElementById(...).getContext() on
  null and threw mid-reactive-update, corrupting wasm_bindgen_futures'
  executor badly enough to panic on the next tick. Same NodeRef-gating
  pattern the prosekit editor's own mount already used.
- style/main.css: .prosekit-wrap/.prosekit-toolbar/.prosekit-editor to
  match the existing input/textarea look (border, background, focus
  ring).
- prosekit-editor.js: a real toolbar (Bold, Italic, H1, H2, Link, and
  a dedicated Gitea-repo-embed button reusing the paste rule's own
  insert logic) with active-state highlighting via the standard
  ProseMirror markActive/nodeActive idiom.
2026-08-05 14:52:08 +02:00
Bendik Aagaard Lynghaug 3242482c8e Redirect back to the originating page after sign-in, not always /
Deploy / deploy (push) Successful in 29s
Sign in link now carries ?redirect=<question_id>; /auth/login stashes
it in the session (validated same-site-path-only to avoid an open
redirect via a crafted query param), /auth/callback reads it back for
the final redirect instead of a hardcoded "/".
2026-08-05 14:40:44 +02:00
Bendik Aagaard Lynghaug d5bbe799ce Fix prosekit-editor.js: remove bare CSS imports that aborted the whole module
Deploy / deploy (push) Successful in 28s
import 'x.css' with a real text/css response isn't a valid JS module
without a `with { type: 'css' }` assertion browsers don't uniformly
support - it threw and prevented mountEditor from ever being defined,
so the prosekit container just stayed empty. ensureStylesheet() (a
<link> tag, already written) is the actual CSS-loading path; the
top-level imports were dead-wrong duplicates of it.
2026-08-05 14:23:46 +02:00
Bendik Aagaard Lynghaug 44c8c9d410 Add prosekit rich-text field, Gitea repo embeds, automation KV read endpoint; fix apex/www session-cookie mismatch on /auth/callback
Deploy / deploy (push) Successful in 29s
- type: prosekit requirement kind, backed by public/prosekit-editor.js
  (ProseMirror via prosekit, loaded from esm.sh, no bundler) - mirrors
  its HTML into a paired hidden input so it reuses the existing
  RwSignal/on:input wiring.
- Pasting a project.uhhm.no/<owner>/<repo> URL in the editor embeds a
  repo card, resolved server-side via a new /gitea-repo handler
  (content::gitea_repo_handler) so the browser never needs Gitea API
  CORS.
- New /automation/kv/{bucket} handler, bearer-token gated
  (AUTOMATION_READ_TOKEN), for backing automations (n8n) to read a
  NATS KV bucket without a browser session.
- Fix: a login started on one of apex/www set its session cookie
  there, but Kanidm's redirect_uri is fixed to PUBLIC_URL - landing
  the callback on a different, empty session ("no login in
  progress"). Caddy now redirects www -> apex so every visit stays on
  one canonical host.
2026-08-05 13:45:56 +02:00
Bendik Aagaard Lynghaug 18025bf870 Hot-reload content on a NATS trigger instead of requiring a restart
Deploy / deploy (push) Successful in 29s
questions is now Arc<ArcSwap<HashMap<...>>> - readers do a lock-free
atomic load (state.questions.load().get(&id).cloned()), never blocking
on or blocked by a reload. content::watch_for_reload subscribes to
portal.content.reload (published by the questions repo's own CI after
it lints a push - see that repo's lint-and-reload.yml) and swaps in a
freshly re-fetched HashMap on each message. A fetch/parse failure logs
and keeps serving the last-good content rather than clearing it.
2026-08-05 07:20:40 +02:00
Bendik Aagaard Lynghaug b0086b2ed9 Fix static asset 404s and build-time SITE_NAME
Deploy / deploy (push) Successful in 27s
Two runtime gaps found via hwatu after the first successful deploy:
- /pkg/*.{js,wasm,css} 404'd because the running binary looked for
  target/site (Cargo.toml's site-root, a build-time path) but the
  deploy step copies the bundle to releases/<sha>/site with no
  target/ prefix - LEPTOS_SITE_ROOT=site in the runtime env corrects
  it.
- SITE_NAME is read via option_env! (compile-time), so writing it
  only into the runtime env file never reached the binary - the
  wordmark stayed "portal". Now exported for the Build step too.

Verified live: applied both fixes directly to the running deployment
first (no CI round-trip), confirmed via hwatu (no console errors,
hero renders, all three alternatives' forms present) before folding
them back into the workflow.
2026-08-04 20:46:00 +02:00
Bendik Aagaard Lynghaug 6b1af34d57 Caddy snippet: use multi-line log block
Deploy / deploy (push) Successful in 27s
Single-line "log { output file X }" parses fine directly in the main
Caddyfile but not through import services.d/*.caddy - Caddy rejects it
with "Unexpected next token after '{' on same line". Verified the
multi-line form validates cleanly (caddy validate) against the real
Caddyfile.
2026-08-04 20:39:12 +02:00
Bendik Aagaard Lynghaug 70a7586471 Drop sudo from the deploy workflow - NoNewPrivileges blocks it outright
Deploy / deploy (push) Failing after 27s
sudo needs setuid escalation, which the runner's own unit disables
entirely via NoNewPrivileges=yes (unconditionally, independent of any
sudoers config - the earlier sudoers.d/deploy-runner grant was never
going to work). Caddy reload already works via the runner's existing
docker group membership; the service restart now goes through plain
systemctl (talks to PID1 over D-Bus, no escalation needed), authorized
by a new polkit rule scoped to deploy-runner + app@*.service.
2026-08-04 20:36:54 +02:00
Bendik Aagaard Lynghaug 634e8dcdfa Fix Ship release: site-root is project-relative, not CARGO_TARGET_DIR
Deploy / deploy (push) Failing after 26s
cp was looking for the site bundle under $CARGO_TARGET_DIR/site, but
site-root = "target/site" in Cargo.toml is resolved relative to the
project directory regardless of CARGO_TARGET_DIR - only the plain
cargo build outputs (release/, front/) actually move when that's
overridden.
2026-08-04 20:31:58 +02:00
Bendik Aagaard Lynghaug 6ac64eeb8f Redirect cargo-leptos's tool cache out of act-runner-bare's StateDirectory
Deploy / deploy (push) Failing after 27s
wasm-bindgen/wasm-opt binaries cargo-leptos downloads under
XDG_CACHE_HOME ended up owned by the kernel's overflow "nobody" uid
inside systemd's StateDirectory for this unit - a quirk of that
specific path, not anything about our shared-cache setup (CARGO_TARGET_DIR
elsewhere is owned correctly). Pointing XDG_CACHE_HOME at our own
shared /var/local/leptos-cache sidesteps it; verified with a clean
checkout + a from-scratch env matching the workflow's exactly.
2026-08-04 20:30:09 +02:00
Bendik Aagaard Lynghaug 44c2fa8f82 Erase view types at every list/component boundary, not just leaves
Deploy / deploy (push) Failing after 27s
Release builds were hitting rustc's recursion limit compiling the
hydrate target: impl IntoView doesn't erase a component's concrete
type within the same crate, so calling a component (or a <For>/<Show>
children closure) without wrapping the result in .into_any() lets the
caller's own type keep growing to include everything nested inside it.
AlternativeCard's requirement fields already did this correctly at
their own leaves; QuestionView's <For alternatives> call into
AlternativeCard, and AlternativeCard's own <For features> closure,
plus ResourceValue/AnswerRow's nested <For>s, didn't - so the depth
compounded across all of them. Verified with a real
`cargo leptos build --release` (was previously untested - only dev
builds had been run against this code).
2026-08-04 19:58:55 +02:00
Bendik Aagaard Lynghaug abe13c139c Give CI its own SCCACHE_SERVER_PORT
Deploy / deploy (push) Failing after 1m21s
Sharing the default port meant whichever context's server happened to
be running (usually the interactive shell's, via rust-analyzer's
background cargo checks) silently served the other's build requests
too - and since sccache's server writes outputs under its own user,
that meant permission-denied for whichever context didn't own it.
Separate ports keep the server processes apart; SCCACHE_DIR is what's
actually shared, giving real cache hits across both.
2026-08-04 13:30:39 +02:00
Bendik Aagaard Lynghaug 76d101b7a8 Share sccache between manual dev builds and CI
Deploy / deploy (push) Failing after 4s
SCCACHE_DIR/CARGO_TARGET_DIR now point at /var/local/sccache and
/var/local/cargo-target, shared with the interactive dev shell via a
new `builders` group, instead of the CI job hitting a leftover
personal sccache server it had no permission to write build output for.
2026-08-04 13:27:22 +02:00
Bendik Aagaard LynghaugandClaude Sonnet 5 286fdbf67f Load content from Gitea directly, drop the local clone; add deploy workflow
Deploy / deploy (push) Failing after 16s
load_questions_from_gitea (content.rs) fetches question YAML straight
from the questions repo's public Gitea contents API at startup instead
of scanning a local directory - one fewer moving part in production
(no git clone to keep in sync, no separate questions-repo deploy
workflow). Still just an in-memory startup load, same as before -
served from RAM for every request, no per-request network call.
Verified against the real repo (all 5 questions fetch correctly).

CONTENT_DIR is replaced by CONTENT_REPO/CONTENT_BRANCH, defaulting to
the real questions repo so local dev needs no env override.

Also adds .gitea/workflows/deploy.yml: builds with cargo-leptos,
ships the release under /srv/app/uhhm-portal (the generic app@.service
deploy layout), writes /etc/app/uhhm-portal.env from this repo's own
Actions Variables/Secrets, restarts the service, and drops this app's
Caddy routing snippet into services.d/.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-31 06:10:06 +02:00
Bendik Aagaard Lynghaug aa1a7fa572 Initial commit: content-driven onboarding portal
Leptos/Axum app that renders a Question/Alternative/Feature schema
loaded from a sibling content repo (portal-content). Kanidm OIDC login,
content-driven authorization (Question.qualifies), a generic NATS
KV-backed resource + state-transition mechanism (no bespoke "applicant"
concept baked into the runtime - it's all content), a SHA-256 DAG chain
tying submissions and decisions together, and the "YES - Rasterized
Lines" piece (ported from the live uhhm.no site) as the landing hero.
2026-07-29 19:38:40 +02:00