The Suspense fallback on a question is the whole served page until the question resolves - no header, no wordmark - so the word "loading…" was the first and only thing a visitor or a crawler read about the site. True, and not about them. It holds its space and stays quiet now; `aria-busy` says what is happening to a reader that would otherwise find an empty document. Same for the two resource slots inside a page, which keep a line's worth of room. Also gone: the `gitea_org_repos` resource kind, which no site ever named. `gitea_starred` and `gitea_releases` stay. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
17 KiB
17 KiB
Changelog
One line per change, grouped by the release that shipped it. Newest
first. A release is a Cargo.toml bump plus a v<version> tag; the
content repos pin the tag they run (see the end of this file).
Unreleased
- A submitter stays in dialogue with their case for as long as it runs.
self_transitiontakesfrom: [state, ...], and the access policy compiles one rule per state named; it defaulted to the initial state alone and was written that way in the compiler, so an applicant written to for a missing drawing could not send it, and nobody could withdraw once anything had happened. Omitted, it still means the initial state alone. - A record can belong to another record.
type: recordwithof: <bucket>stores the parent's id, which arrives on the link rather than being typed; a submission naming a record that is not there is refused. The other half iswhere: { field, equals }on a Kv resource, so an application's page can list the objections that name it. - A public view need not publish the whole bucket.
states: [...]on a Kv resource shows only those, for one record as well as a listing - a register that is public by law is public about what was decided, not about what was withdrawn. The desk over the same bucket is a different spec and still sees all of it. - A state can grant a group:
grants: <group>beside itsevent, so an approval that means membership does not also need somebody to remember to invite them afterwards. The lint requires the forms that fill such a bucket to collect a required name and email, because by the time a desk presses approve there is nobody left to ask. - A state can be moved by a clock:
after: { days, to }, swept hourly, measured from the record's last move (src/deadlines.rs). A deadline may only do what a person could have done -after.tois held to the same declared edge as a desk button - and the move fires the same mail and publishes the same event, stampeddeadline. - Documentation moved out of the README into
docs/: architecture, content reference, state machines, access, operations. - A page says nothing until it has something to say. The Suspense fallback on a question was the whole served page - no header, no wordmark - so the first and only thing a visitor or a crawler read was the word "loading…", which is true and is not about them. It now holds the space and stays quiet, with
aria-busyfor a reader that would otherwise find an empty document. - Gone: the
gitea_org_reposresource kind, which no site ever named.gitea_starredandgitea_releasesstay.
0.4.0 (2026-09-24)
- A field can be a place:
type: locationputs a pin on a map and stores{lat, lon, zoom}; a record holding one renders as a still map on its desk and in any resource card. - A question can be about a place:
place: { lat, lon, zoom, name }is the counterpart toevent:- event makes a page an occasion with a time, place makes it somewhere, draws the map under the question, and opens any location field on that page there instead of on the globe. - Maps are served from this origin. Portal fetches every tile and every still itself (
src/maps.rs, MapLibre vendored), so a page with a map on it makes no third-party request, needs no consent banner, and theMAPBOX_TOKENnever leaves the server. Unset, the feature is simply off. - A reply to a case mail becomes a note on its record: whatever reads the mailbox publishes on
portal.mail.receivedand portal appends the note, after checking the record exists and the sender is the address the record itself holds. A note never moves a case.
0.3.47 (2026-09-24)
- iris: needs.yaml takes
stage_words, each state in the site's own words, carried into the simulation model for the trainer's desk score. No change to portal itself.
0.3.46 (2026-09-23)
- An invite first looks the email up in Kanidm (
/v1/raw/searchonmail, when the service account may read people:idm_people_pii_read) and only adds an existing account, under whatever username, to the group; no second account, no reset link.
0.3.45 (2026-09-23)
- A mail stream that cannot be created at boot (subjects owned by an older stream) is a warning, not a failed start; 0.3.44 took the kasse sites down for two minutes on the rename.
0.3.44 (2026-09-23)
- The mail stream is named
mail(wasWORMHOLE); gdo 0.1.2 matches. Delete the old stream on each NATS after upgrading both.
0.3.43 (2026-09-23)
- Mail is content:
mail: { to, subject, body }on a state in aggregates.yaml sends when a record enters that state (to: submitteror one address;{field},{site},{chain},{email}in the templates), and an invite mails its one-time link built in;site.yamlgainsmail: { from, reply_to, invite }. Portal renders and publishes onportal.mail.sendin theWORMHOLEstream; uhhm/gdo delivers. The lint requires a requiredemailfield on every form recording into a bucket that mails the submitter.
0.3.42 (2026-09-23)
- people: the Kanidm person lookup reads the body, since Kanidm 1.11 answers a missing person with 200 and
nullrather than 404; before this every invite skipped account creation and failed on the group step
0.3.41 (2026-09-23)
- publish: the iris release step retries on a fresh clone when its push loses a race against a push to iris main (the v0.3.40 tag never published)
- iris:
check --accessprints the access matrix, and every check validates the compiled policy against the schema
0.3.40 (2026-09-23)
- One access policy, compiled from content into Cedar on every load (
src/access.rs): pages, resource reads, desk transitions, self transitions and the automation endpoint all decide through it; decisions carry rule ids, refusals and mutations publish onportal.access.decided, andportal.access.<site>.mayanswers the same question for other apps over NATS invite: {group}on an alternative onboards a person into Kanidm from a desk (src/people.rs,KANIDM_API_TOKEN), recording the one-time credential link inportal_invitesand onportal.people.invited
0.3.39 (2026-09-23)
- publish: releases the matching uhhm/iris by proxy - re-pins, builds, tests and tags it as this version (needs the
IRIS_PUSH_USERvariable andIRIS_PUSH_TOKENsecret); iris carries portal's version number from here on
0.3.38 (2026-09-22)
- publish: the iris build step gets a writable cargo home; the v0.3.37 tag never published
0.3.37 (2026-09-22)
- The lint moves to its own repo, uhhm/iris:
question_lintbecomesiris check,needs_replaybecomesiris replay; the tarball shipsiris(pinned in publish.yml) instead ofquestion_lint; the needs module and local loaders leave portal
0.3.36 (2026-09-22)
- Question nav: a followup is offered only once earned - the verified chain answered the alternative leading there, or a link that records nothing leads on from an earned page. Standing on a page no longer unlocks the pages behind its forms; the chain index now records which alternative was answered
- One bounded HTTP client for every server-side fetch: 20 s budget, size caps per kind of fetch (
src/http.rs) - Uploads are refused past 20 MB while streaming, and the route body limit that had capped them at axum's 2 MB default is raised to match
tests/jetstream.rs: the store, the event log and the projection tested against a real JetStream (CI starts a throwaway one)contentsplit intogitea,validate,markdown,handlers; every path stillcontent::x- This changelog, and the table of what each site runs
- Business needs as a checkable spec: needs.yaml + question_lint pass
- needs:
successstates and a resolved simulation model - needs:
stages- every stage the business named must be a state - needs_replay: make simulated cases real aggregates, report buckets by state
- needs: also_moved_by - a need carried by more than one group's desk
0.3.35 (2026-09-12)
- Item cards: no paragraph margin under the description
0.3.34 (2026-09-11)
- Question nav: offer followups by reach, not by carrying any chain
0.3.33 (2026-09-02)
- Head preloads: latin font, wordmark, content-host preconnect
0.3.32 (2026-09-01)
- Per-site favicon via site.yaml
0.3.31 (2026-09-01)
- Markdown links allow tel:
0.3.30 (2026-09-01)
- Single alt-image shows natural height
0.3.29 (2026-09-01)
- Markdown image layout hints + AVIF assets
0.3.28 (2026-09-01)
- Hero description renders inline markdown
0.3.27 (2026-09-01)
- wordmark_invert: explicit, not inferred from .svg
0.3.26 (2026-09-01)
- Select fields: inline static options
0.3.25 (2026-09-01)
- Content-shipped stylesheet override (site.yaml stylesheet)
0.3.24 (2026-09-01)
- Gateway submit-as-link loses the underline
0.3.23 (2026-09-01)
- Chrome speaks the site's language: site.yaml lang + i18n table
0.3.22 (2026-09-01)
- site.yaml wordmark_height; raster logos keep their colors
0.3.21 (2026-09-01)
- Hero paragraph centers its measure box
0.3.20 (2026-09-01)
- Sign-in becomes optional: KANIDM_URL unset disables auth cleanly
0.3.19 (2026-09-01)
- Markdown images in descriptions, gated and framed
0.3.18 (2026-09-01)
- Item cards render inline markdown descriptions
- Convergence: ink converges onto the decoded key (WebGL glow)
- Gesture results overlay the canvas - the page never jumps
0.3.16 (2026-08-30)
- Announce sweeper: refresh open records from content
0.3.15 (2026-08-30)
- Hidden fields: carrier value without a label row
0.3.14 (2026-08-30)
- Feature cards: icon column, one text edge
0.3.13 (2026-08-30)
- Voice field: nothing leaves the browser until Keep it here
0.3.12 (2026-08-30)
- Voice preview stays hidden until there is something to play
0.3.11 (2026-08-30)
- README: gesture and voice fields
- Validate templated actions against the page pattern
0.3.10 (2026-08-30)
- type: voice, Requirement.value, playable resource cards
0.3.9 (2026-08-30)
- Templated actions; places are pickable in the gesture input
0.3.8 (2026-08-30)
- Descriptions are inline markdown; Feature.link withdrawn
0.3.7 (2026-08-30)
- Feature.link: a card's name may point elsewhere
0.3.6 (2026-08-30)
- Announced pages: event windows, header announcements, summary tasks
0.3.5 (2026-08-30)
- Gesture widget: stop drawing the decoded ghost path
- Gesture widget: ghost path back, for v2 keys only
0.3.4 (2026-08-25)
- Alternative.disabled: announced but not yet takeable
0.3.3 (2026-08-25)
- Empty list says so before it can parse as zero answers
0.3.2 (2026-08-25)
- README: hashed pkg assets in the release flow
- Resource empty text: content-declared, and null counts as empty
0.3.1 (2026-08-25)
- Content-hashed pkg assets (hash-files) so stale bundles can't pair with new wasm
0.3.0 (2026-08-25)
- Hero becomes a content-owned module; site asset proxy; gesture growth fix
0.2.4 (2026-08-25)
- Gates keep the question nav
0.2.3 (2026-08-25)
- Attended-bucket lint rule; app-lifetime resources fix first-nav corruption
0.2.2 (2026-08-24)
- One app-level Title fed by a shared site resource
0.2.1 (2026-08-24)
- Docs: routing bullet + design doc marked implemented
- Revalidating cache on app assets; site title survives SPA navigation
0.2.0 (2026-08-24)
- Initial commit: content-driven onboarding portal
- Load content from Gitea directly, drop the local clone; add deploy workflow
- Share sccache between manual dev builds and CI
- Give CI its own SCCACHE_SERVER_PORT
- Erase view types at every list/component boundary, not just leaves
- Redirect cargo-leptos's tool cache out of act-runner-bare's StateDirectory
- Fix Ship release: site-root is project-relative, not CARGO_TARGET_DIR
- Drop sudo from the deploy workflow - NoNewPrivileges blocks it outright
- Caddy snippet: use multi-line log block
- Fix static asset 404s and build-time SITE_NAME
- Hot-reload content on a NATS trigger instead of requiring a restart
- Add prosekit rich-text field, Gitea repo embeds, automation KV read endpoint; fix apex/www session-cookie mismatch on /auth/callback
- Fix prosekit-editor.js: remove bare CSS imports that aborted the whole module
- Redirect back to the originating page after sign-in, not always /
- Fix hero-canvas navigation race; style the prosekit editor and add a toolbar
- yes.js: guard against the canvas not being in the DOM yet
- Fix get_resource/transition_answer: scope feature lookup to its own alternative
- Add self-service transitions, authorized by item possession not group membership
- Event-sourced applicant/subscriber/project aggregates, generalized resources
- deploy.yml: write GITEA_API_TOKEN for the new GiteaStarred/OrgRepos resource sources
- Cargo.toml: disambiguate bin-target for cargo-leptos
- Add manually-triggered backfill workflow, no terminal/sudo needed
- Trigger redeploy to pick up PORTAL_GITEA_API_TOKEN secret
- Render generic resource lists as cards, not raw JSON dump
- Fix recursion-limit build failure in the new item-card renderer
- Resource-backed multi/single-select requirement
- Add Organization aggregate (client-as-status), wired but inert
- Fix favicon (real icon, not an unrelated orange circle) + stack encouragements with the submit button
- Fix bucket-404 on empty resources, redesign review actions as select-then-confirm, CSS polish
- Inline encouragements with the submit button, vertically centered
- Use the real institutional logo for the header wordmark, not plain text
- Use the actual UHHM-letters wordmark, not the institutional mark
- Responsive base font size via svmin, not fixed 17px
- Add plain-unit fallbacks for svmin/svh in case of unsupported engines
- Size the YES canvas from its container, not window.innerHeight
- Debounce the YES canvas resize handler and skip no-op resizes
- Revert canvas sizing to window.innerWidth/innerHeight, gate resize on width change
- Stop blocking native scroll on the YES canvas's touch handlers
- Quicksand: font-display optional, not swap, to stop the text-jump on scroll
- Use lvh/lvmin, not svh/svmin - svh was the wrong end of the viewport
- Update style/main.css
- Update style/main.css
- YES canvas: size off its own container rect, not window.innerHeight
- yes.js: freeze .hero-yes's height via inline style, don't trust svh alone
- add responsive container width
- deploy.yml: build and publish question_lint alongside the app binary
- Make aggregate state graphs content-driven, not compiled Rust
- One shared, content-labeled Confirm button per alternative instead of one per row
- get_resource/get_requirement_options: #[server(default)] on params
- Add optional Alternative.image and Feature.color/icon for richer layouts
- Drop the feature list's left-padding
- Drop question-report entirely, obfuscate the mailto link instead
- Transition.from: state graphs deeper than one decision
- Alternative.images: array of urls, rendered as a Swiper card deck
- Reseed lost event history from the KV projection on transition
- Lighten the comment load
- Validate action targets; format timestamps client-side; clippy cleanup
- Question nav + gateway alternatives
- Responsible note under the title; followup pages out of the nav until a chain exists
- README: what the engine provides
- Feature icons: masked span painted by currentColor, not
- Responsible note: one line, same hue and size as the hero description
- Responsible note as : meta information, dimmer than the description
- Responsible note back to the footer, above the question nav
- Bound inputs: a field that loads its value from a sibling-parameterized resource
- add some more width to our alternatives
- Accent to a dusty press-cyan, a muted echo of the canvas's cyan ink
- Editor type in rem: prosekit's px typography ignored the responsive root
- Ban px from stylesheets: every length rem/em off the responsive root
- No translucent input chrome: solid ink-dim placeholders and embed meta
- Editor type scales for real: drop prosekit typography.css, fix selectors
- Stable server fn endpoints: open tabs survive deploys
- Drop stray local tool temp file, ignore .claude/
- Noise-driven YES, sticky hero with frosted cards, full light theme
- increase translucency of laternative cards
- Showcase jq test tracks Gitea repo shape and website-first links
- Gesture input type, redoal-relay client, gitea_releases, content-driven branding
- Echo thumbnails fade in via timeout, not rAF
- Trigger deploy: redoal OAuth2 credentials now configured
- Deploy becomes publish: portal ships as a versioned release artifact
- Semver releases cut with cargo-release; publish on v* tags only
- Docs: release/rollout flow after the instance-ownership refactor
- README: frame portal as a generic multi-site question engine
- Design doc: filesystem routes, sections, dynamic segments
- Filesystem routes, sections, dynamic segments; instant YES hero
What each site runs
Read from the hosts on 2026-09-23; a rollout is a pin bump in the
site's content repo (PORTAL_RELEASE in its deploy.yml), or a manual
tarball deploy on kasse.
| Site | Host | Instance | Version |
|---|---|---|---|
| uhhm.no | ergo | app@uhhm-portal |
v0.3.36 |
| redoal.com | ergo | app@redoal-portal |
v0.3.36 |
| westra.klingenbergbygg.no | kasse | app@westra-portal |
v0.3.41 |
| portal.klingenbergbygg.no | kasse | app@klingenberg-portal |
v0.3.41 |
| vel.klingenbergbygg.no | kasse | app@tomtervel-portal |
v0.3.41 |