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>
Six hand-maintained copies of the bucket->aggregate-type table
(4 per-type State enums, content::aggregate_type_for_bucket +
is_valid_transition_target, answers.rs's two dispatch matches,
backfill_events.rs's own migration table) collapse into one:
questions/aggregates.yaml, loaded and hot-swapped in AppState.aggregates
the same way AppState.questions already is. aggregates/mod.rs's
replay/create/transition now run off a runtime-loaded AggregateSchema
instead of a compile-time AggregateKind trait impl per type - the
CAS/JetStream mechanics underneath are unchanged. A new aggregate type
is now a content-only change, no portal deploy required.
backfill_events.rs is deleted outright rather than ported - its
migration job was already done and production data here is minimal
and expendable (the EVENTS stream gets purged by hand post-deploy).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
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>