Commit Graph
100 Commits
Author SHA1 Message Date
Bendik Aagaard LynghaugandClaude Fable 5 de6a90bbf5 Gateway submit-as-link loses the underline
A no-requirements gateway alternative renders .alt-submit as an <a>
for plain navigation; strip the link underline so it reads as the
button it's styled to be.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Y42TyF8Zu7NGRR2893vNcZ
2026-09-01 20:01:28 +02:00
Bendik Aagaard LynghaugandClaude Fable 5 88b1b96322 Release 0.3.23
Test / test (push) Successful in 28s
Publish release / publish (push) Successful in 1m44s
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Y42TyF8Zu7NGRR2893vNcZ
2026-09-01 17:23:33 +02:00
Bendik Aagaard LynghaugandClaude Fable 5 b259bf39bb Chrome speaks the site's language: site.yaml lang + i18n table
The strings the portal itself says - Asked by, Also worth asking,
Sign in, the not-found page, defaults - translate via a small en/no
table selected by site.yaml's lang, which also sets the html lang
attribute. Content keeps speaking for itself; unknown languages fall
back to English per key.

Also: a scheme-gated markdown image now swallows its alt text instead
of leaking it as stray text - this is the fix for the test that has
been red since 0.3.19 (publish is tag-triggered and does not gate on
the test workflow; caught late).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Y42TyF8Zu7NGRR2893vNcZ
2026-09-01 17:23:28 +02:00
Bendik Aagaard LynghaugandClaude Fable 5 528e2badaf Release 0.3.22
Test / test (push) Failing after 25s
Publish release / publish (push) Successful in 1m39s
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Y42TyF8Zu7NGRR2893vNcZ
2026-09-01 17:17:26 +02:00
Bendik Aagaard LynghaugandClaude Fable 5 f4e9f6d0ba site.yaml wordmark_height; raster logos keep their colors
Custom wordmark sizing per site (0.5-6 rem, clamped at render), and
the light-theme invert now applies only to .svg wordmarks - the
white-stroke house style - so a client's raster logo is never
recolored.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Y42TyF8Zu7NGRR2893vNcZ
2026-09-01 17:17:11 +02:00
Bendik Aagaard LynghaugandClaude Fable 5 3b2cdc07b7 Release 0.3.21
Test / test (push) Failing after 29s
Publish release / publish (push) Successful in 1m42s
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Y42TyF8Zu7NGRR2893vNcZ
2026-09-01 17:10:49 +02:00
Bendik Aagaard LynghaugandClaude Fable 5 2de936d995 Hero paragraph centers its measure box
max-width: 46ch without auto margins left-anchored the box inside the
centered column; the text centered in the wrong frame.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Y42TyF8Zu7NGRR2893vNcZ
2026-09-01 17:10:44 +02:00
Bendik Aagaard LynghaugandClaude Fable 5 9fca79b8ee Release 0.3.20
Test / test (push) Failing after 25s
Publish release / publish (push) Successful in 1m36s
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Y42TyF8Zu7NGRR2893vNcZ
2026-09-01 16:54:40 +02:00
Bendik Aagaard LynghaugandClaude Fable 5 3d51aa1e6a Sign-in becomes optional: KANIDM_URL unset disables auth cleanly
A content-only instance (westra preview) has no review desk and no
Kanidm client; booting no longer demands one. Auth routes answer 503
'sign-in is not configured on this instance'; everything public
renders as usual.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Y42TyF8Zu7NGRR2893vNcZ
2026-09-01 16:54:35 +02:00
Bendik Aagaard LynghaugandClaude Fable 5 d5eb362c87 Release 0.3.19
Test / test (push) Failing after 25s
Publish release / publish (push) Successful in 1m40s
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Y42TyF8Zu7NGRR2893vNcZ
2026-09-01 16:47:01 +02:00
Bendik Aagaard LynghaugandClaude Fable 5 7f10ba20d3 Markdown images in descriptions, gated and framed
Images already flowed through render_inline_markdown ungated; they
now take the same scheme gate as links (https or same-origin only -
no data:, no plain http) and render as full-width framed figures in
alternative, feature, and item-card descriptions. Carries whole-site
imagery for content-driven instances (westra).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Y42TyF8Zu7NGRR2893vNcZ
2026-09-01 16:46:56 +02:00
Bendik Aagaard LynghaugandClaude Fable 5 756818cacd Release 0.3.18
Test / test (push) Successful in 26s
Publish release / publish (push) Successful in 1m42s
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Y42TyF8Zu7NGRR2893vNcZ
2026-09-01 15:58:45 +02:00
Bendik Aagaard LynghaugandClaude Fable 5 0423e9c368 Merge convergence: ink-to-key animation + overlay results
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Y42TyF8Zu7NGRR2893vNcZ
2026-09-01 15:58:41 +02:00
Bendik Aagaard LynghaugandClaude Fable 5 ea7acf4d7d Gesture results overlay the canvas - the page never jumps
Echo thumbnails float centered along the canvas's bottom edge and the
empty-state line sits in the same band; both are out of flow, so the
widget's height is fixed by the canvas alone whether results come
back or not. The strip is pointer-inert except the thumbnails
themselves, so drawing near the bottom edge still works.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Y42TyF8Zu7NGRR2893vNcZ
2026-09-01 15:55:58 +02:00
Bendik Aagaard LynghaugandClaude Fable 5 19f8fbaf03 Convergence: ink converges onto the decoded key (WebGL glow)
On ack (and on picking a place) a glowing copy of the stroke peels
off and converges point-by-point onto the key's decoded path - a
staggered wave from stroke start to end, comet trails, additive on
dark / ink on light, ghost deposited where the light settles. WebGL
point sprites on an overlay canvas; no WebGL or reduced-motion means
exactly the previous behavior. ?relay= query override points widgets
at a local relay for dev.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Y42TyF8Zu7NGRR2893vNcZ
2026-09-01 15:47:09 +02:00
Bendik Aagaard LynghaugandClaude Fable 5 ee295be6a7 Item cards render inline markdown descriptions
Test / test (push) Successful in 25s
Publish release / publish (push) Successful in 1m39s
Resource-fed cards follow the same convention as every other
description: links live there, sanitized by render_inline_markdown.
Carries the per-recording report link on place pages.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Y42TyF8Zu7NGRR2893vNcZ
2026-08-31 17:18:40 +02:00
Bendik Aagaard LynghaugandClaude Fable 5 d161677155 Release 0.3.16
Test / test (push) Successful in 25s
Publish release / publish (push) Successful in 1m33s
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Y42TyF8Zu7NGRR2893vNcZ
2026-08-30 23:13:33 +02:00
Bendik Aagaard LynghaugandClaude Fable 5 8229dbf4c2 Announce sweeper: refresh open records from content
A date or place corrected after first announce never reached the
portal_events record, so the followup fired on the stale schedule.
While a record is still in its initial state, content is the source
of truth: differing responses are written back on each sweep.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Y42TyF8Zu7NGRR2893vNcZ
2026-08-30 23:13:29 +02:00
Bendik Aagaard LynghaugandClaude Fable 5 3de069f41d Release 0.3.15
Test / test (push) Successful in 24s
Publish release / publish (push) Successful in 1m33s
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Y42TyF8Zu7NGRR2893vNcZ
2026-08-30 23:09:11 +02:00
Bendik Aagaard LynghaugandClaude Fable 5 ab3a649646 Hidden fields: carrier value without a label row
A type: hidden requirement rendered through the fallback branch, whose
label wrapper shows the field name — so a preset key listed as a second
visible field under the email. Bare hidden input, no row.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Y42TyF8Zu7NGRR2893vNcZ
2026-08-30 23:08:59 +02:00
Bendik Aagaard Lynghaug f1e7573b36 Release 0.3.14
Test / test (push) Successful in 24s
Publish release / publish (push) Successful in 1m33s
2026-08-30 16:56:19 +02:00
Bendik Aagaard LynghaugandClaude Fable 5 2082e8ae48 Feature cards: icon column, one text edge
The icon floated, so a description's second line wrapped back under
it. With an icon the feature is a two-column grid: icon left, every
other child in the text column.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-30 16:56:18 +02:00
Bendik Aagaard Lynghaug 313126768b Release 0.3.13
Test / test (push) Successful in 25s
Publish release / publish (push) Successful in 1m33s
2026-08-30 16:36:15 +02:00
Bendik Aagaard LynghaugandClaude Fable 5 78953a4633 Voice field: nothing leaves the browser until Keep it here
The widget holds the alternative's submit: on the first press it
uploads, sets its value on the relay's confirmation, then lets the
submit through. A relay error keeps the recording for another try and
submits nothing. Status copy says what to do at each step.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-30 16:36:14 +02:00
Bendik Aagaard Lynghaug 703e6b6356 Release 0.3.12
Test / test (push) Successful in 24s
Publish release / publish (push) Successful in 1m32s
2026-08-30 16:06:58 +02:00
Bendik Aagaard Lynghaug 2c545cd1b0 Voice preview stays hidden until there is something to play 2026-08-30 16:06:57 +02:00
Bendik Aagaard Lynghaug 47c894cc1d Release 0.3.11
Test / test (push) Successful in 25s
Publish release / publish (push) Successful in 1m32s
2026-08-30 16:02:42 +02:00
Bendik Aagaard LynghaugandClaude Fable 5 7bc59a65e0 Validate templated actions against the page pattern
resolve_question returns a clone with a concrete id, so is_dynamic()
on it was always false and every templated action failed lint.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-30 16:02:41 +02:00
Bendik Aagaard Lynghaug 243b6a5704 README: gesture and voice fields
Test / test (push) Successful in 24s
2026-08-30 16:00:25 +02:00
Bendik Aagaard Lynghaug 3a20870762 Release 0.3.10
Test / test (push) Successful in 24s
Publish release / publish (push) Successful in 1m34s
2026-08-30 15:58:05 +02:00
Bendik Aagaard LynghaugandClaude Fable 5 07776be2bb type: voice, Requirement.value, playable resource cards
A voice requirement records in the browser and ships PCM to the relay
as one binary frame (voice.js, same mount/stop contract as gesture);
its value becomes {key, digest, duration_ms}. Requirement.value
presets a field and, on a dynamic page, takes the URL segment - so
value: "{key}" tells the voice field where to record. A url resource
source takes the segment too. Resource items with an https `audio`
field render an <audio> player. The gesture widget reports picks to
the relay for ranking.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-30 15:58:04 +02:00
Bendik Aagaard Lynghaug 39fdb8e0e9 Release 0.3.9
Test / test (push) Successful in 25s
Publish release / publish (push) Successful in 3m8s
2026-08-30 15:38:24 +02:00
Bendik Aagaard LynghaugandClaude Fable 5 b5ee79da8e Templated actions; places are pickable in the gesture input
action: /shape/{curve.key} fills placeholders from the submitted
responses and must land on a dynamic page (validated). The gesture
widget now treats relay 'place' (kept) and 'echo' (live presence)
frames as pickable options: picking one re-derives the input's key -
that place's decode becomes the ghost under the stroke and the answer
records {key: picked, own_key, selected_from, selected_distance}, the
labelled pair that later ranks places and calibrates the key. Dedupe
by key; an empty state when nothing is kept at the shape.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-30 15:38:23 +02:00
Bendik Aagaard Lynghaug ff2f94e549 Release 0.3.8
Publish release / publish (push) Successful in 1m35s
Test / test (push) Successful in 25s
2026-08-30 14:44:43 +02:00
Bendik Aagaard LynghaugandClaude Fable 5 be954bc43e Descriptions are inline markdown; Feature.link withdrawn
Alternative and feature descriptions render links, emphasis and code
(pulldown-cmark, html feature only). Block structure flattens to one
paragraph, raw HTML is dropped, link targets are limited to https,
mailto and site-relative paths. A link belongs in the prose, so the
title-link field shipped in 0.3.7 goes before anyone uses it.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-30 14:44:42 +02:00
Bendik Aagaard Lynghaug 7fb0afcdc5 Release 0.3.7
Test / test (push) Successful in 25s
Publish release / publish (push) Successful in 1m30s
2026-08-30 14:39:59 +02:00
Bendik Aagaard LynghaugandClaude Fable 5 73efa6a4eb Feature.link: a card's name may point elsewhere
An https URL on a feature renders its name as an outbound link -
an announcement's programme page, a venue. Validated on load.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-30 14:39:58 +02:00
Bendik Aagaard Lynghaug 7a0724af7f Release 0.3.6
Test / test (push) Successful in 24s
Publish release / publish (push) Successful in 1m31s
2026-08-30 12:34:35 +02:00
Bendik Aagaard LynghaugandClaude Fable 5 0154f6c8c0 Announced pages: event windows, header announcements, summary tasks
A question may carry event: {starts, duration, place}. While the
window is open the page is announced in a strip at the top of every
header (name, when, 'in 3 days'), soonest first, and kept out of the
footer nav; when it closes the page becomes a followup - only a
visitor carrying an answer chain still sees it.

announce.rs keeps one record per event page in the runtime-owned
portal_events bucket (built-in state graph: announced ->
awaiting_summary -> summarized, content may override) and, on a
one-minute idempotent sweep, moves ended windows to awaiting_summary,
publishing the transition on portal.answers.submitted as 'Summary
due' - the post-what-happened task a review desk picks up. Lint
warns when event pages exist but nothing reads portal_events.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-30 12:34:34 +02:00
Bendik Aagaard Lynghaug 47d7f9d2c3 Release 0.3.5
Publish release / publish (push) Successful in 1m27s
Test / test (push) Successful in 22s
2026-08-30 11:40:47 +02:00
Bendik Aagaard LynghaugandClaude Fable 5 12e9fcd609 Gesture widget: ghost path back, for v2 keys only
Test / test (push) Successful in 23s
The relay's ack path is drawn under the stroke when the key's version
nibble is >= 2 (ADR-0014's ordered turning chain decodes to the
stroke itself); a v1 key's blob stays hidden. Safe to ship ahead of
the relay: nothing shows until the relay encodes v2.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-30 11:38:48 +02:00
Bendik Aagaard LynghaugandClaude Fable 5 72674afe05 Gesture widget: stop drawing the decoded ghost path
Test / test (push) Successful in 25s
decode_key's reconstruction is too rough to have value on screen;
the ack's key is kept, its path ignored. Revisit only once the
reconstruction is fixed at the core in redoal.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-30 11:21:20 +02:00
Bendik Aagaard Lynghaug 7ba9b08d76 Release 0.3.4
Test / test (push) Successful in 24s
Publish release / publish (push) Successful in 1m27s
2026-08-25 20:25:52 +02:00
Bendik Aagaard LynghaugandClaude Fable 5 1a45839699 Alternative.disabled: announced but not yet takeable
Test / test (push) Successful in 24s
The button renders disabled (title 'Not yet') and submit_answer
refuses the alternative server-side - lysbue's disabled flag, back as
content, for advertising a path before it works.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-25 20:25:49 +02:00
Bendik Aagaard Lynghaug be54f54804 Release 0.3.3
Test / test (push) Successful in 24s
Publish release / publish (push) Successful in 1m26s
2026-08-25 20:09:00 +02:00
Bendik Aagaard LynghaugandClaude Fable 5 8212477377 Empty list says so before it can parse as zero answers
Test / test (push) Successful in 24s
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-25 20:08:59 +02:00
Bendik Aagaard Lynghaug e97470c2f6 Release 0.3.2
Publish release / publish (push) Successful in 1m35s
Test / test (push) Successful in 40s
2026-08-25 20:04:38 +02:00
Bendik Aagaard LynghaugandClaude Fable 5 9f26203ed5 Resource empty text: content-declared, and null counts as empty
Test / test (push) Successful in 25s
A `.[] | {...}` jq over an empty list yields no output, which arrived
as null and rendered nothing - redoal's Releases feature was a bare
heading. Null now reads as the same silence as [], and ResourceSpec
gains `empty:` so content can word it ("Nothing released yet");
default stays "Nothing here yet."

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-25 20:04:35 +02:00
Bendik Aagaard LynghaugandClaude Fable 5 d6fdcd0bce README: hashed pkg assets in the release flow
Test / test (push) Successful in 23s
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-25 19:52:31 +02:00
Bendik Aagaard Lynghaug d65c5593b3 Release 0.3.1
Test / test (push) Successful in 25s
Publish release / publish (push) Successful in 1m25s
2026-08-25 19:48:49 +02:00
Bendik Aagaard LynghaugandClaude Fable 5 330ab11fe9 Content-hashed pkg assets (hash-files) so stale bundles can't pair with new wasm
Test / test (push) Successful in 26s
An iPhone kept a heuristically-cached portal.js across three releases
(cached before Cache-Control: no-cache existed) and loaded it against
fresh wasm - its snippet imports 404'd, hydration never started, and
the gesture field never mounted. With hash-files = true every pkg
file is content-named and the freshly served page references exactly
its own bundle; hash.txt ships beside the binary (leptos resolves it
next to current_exe), the shell links the stylesheet through
HashedStylesheet, and instances set LEPTOS_HASH_FILES=true.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-25 19:48:47 +02:00
Bendik Aagaard Lynghaug d403eda5da Release 0.3.0
Test / test (push) Successful in 24s
Publish release / publish (push) Successful in 1m5s
2026-08-25 17:26:31 +02:00
Bendik Aagaard LynghaugandClaude Fable 5 1afd34c22e Hero becomes a content-owned module; site asset proxy; gesture growth fix
Test / test (push) Successful in 24s
site.yaml's hero is now plain | module, where module names a
JavaScript file the content repo ships (mount(container) -> handle
with stop()). Portal serves content assets same-origin at
/site/<path> (Gitea raw sends no CORS headers), starts the module at
HTML parse time, adopts it on hydration, mounts fresh via the inline
script's __mountHero on client-side navigation, and stops it on
leave. The YES canvas (yes.js) and the gesture hero mode leave the
engine - uhhm/questions ships YES as its hero.js, redoal/questions
ships a sine-swings band. Gesture form canvas no longer balloons after
a stroke: the wrap's aspect-ratio reservation is scoped to :empty
(pre-mount) so it can't turn echo-strip height into width inside the
flex field.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-25 17:26:29 +02:00
Bendik Aagaard Lynghaug 307fd7e753 Release 0.2.4
Test / test (push) Successful in 24s
Publish release / publish (push) Successful in 1m16s
2026-08-25 17:05:12 +02:00
Bendik Aagaard LynghaugandClaude Fable 5 30c2b6bea9 Gates keep the question nav
Test / test (push) Successful in 24s
The qualifies and requires_chain gates replaced the whole page,
footer nav included - a visitor hitting one had only the wordmark as
a way out.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-25 17:05:09 +02:00
Bendik Aagaard Lynghaug b875045bbc Release 0.2.3
Test / test (push) Successful in 24s
Publish release / publish (push) Successful in 1m8s
2026-08-25 15:36:10 +02:00
Bendik Aagaard LynghaugandClaude Fable 5 a1fc333079 Attended-bucket lint rule; app-lifetime resources fix first-nav corruption
Test / test (push) Successful in 25s
validate_questions now rejects content where a record_as bucket is
read by nothing: every bucket must be listed by some Kv resource in
the same repo (a desk) or carry aggregates.yaml's new attended_by
annotation naming the automation that consumes it - no publicly
collected answer may land where nothing reads.

Separately, all server-fn resources (question/user/nav) move to a
PortalShell above the routes, created once and provided via context.
Per-page Resources broke on the first client-side navigation: the
remounted component's fresh Resource consumed a stale SSR hydration
buffer - the nav list [[id, name], ..] deserialized as a Page (serde
fills structs from sequences in field order), so uhhm.no's landing
question rendered chain-gated behind its own nav entry instead of
the /develop/proposal form.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-25 15:36:07 +02:00
Bendik Aagaard Lynghaug 0d6cdb8a00 Release 0.2.2
Test / test (push) Successful in 23s
Publish release / publish (push) Successful in 1m5s
2026-08-24 22:47:12 +02:00
Bendik Aagaard LynghaugandClaude Fable 5 b2342f074b One app-level Title fed by a shared site resource
Test / test (push) Successful in 24s
The per-page override remounted on every SPA navigation and lost the
leptos_meta race to App's static SITE_NAME fallback regardless of
hoisting - single source of truth instead: App owns the site resource
(provided via context, pages reuse it) and the only Title, wrapped in
Suspense so SSR still serves the resolved name.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-24 22:47:09 +02:00
Bendik Aagaard Lynghaug 7429f1b9e1 Release 0.2.1
Test / test (push) Successful in 23s
Publish release / publish (push) Successful in 1m23s
2026-08-24 22:40:30 +02:00
Bendik Aagaard LynghaugandClaude Fable 5 a0305282ef Revalidating cache on app assets; site title survives SPA navigation
Test / test (push) Successful in 24s
pkg files keep stable names across releases, so an uncontrolled
browser cache could pair last release's wasm with the new server's
server-fn wire format - every page then renders its error branch
(redoal.com's 'Nothing here' after v0.2.0). Cache-Control: no-cache
on /pkg and /yes.js makes clients revalidate (a 304 per load) instead
of guessing. The content-declared site title also moves out of the
question Suspense: remounting with each navigation lost the
leptos_meta race to the compile-time fallback, flipping redoal.com's
tab to 'uhhm' on the first client-side nav.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-24 22:40:29 +02:00
Bendik Aagaard LynghaugandClaude Fable 5 c67f6f1a59 Docs: routing bullet + design doc marked implemented
Test / test (push) Successful in 23s
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-24 22:33:37 +02:00
Bendik Aagaard Lynghaug 4e69f26fcc Release 0.2.0
Test / test (push) Successful in 24s
Publish release / publish (push) Successful in 1m8s
2026-08-24 22:30:35 +02:00
Bendik Aagaard LynghaugandClaude Fable 5 452ea88fbf Filesystem routes, sections, dynamic segments; instant YES hero
Test / test (push) Successful in 23s
The questions/ tree is the router now: ids derive from file paths
(index.yaml names its directory; explicit id still wins for legacy
content), actions and requires_chain accept relative refs, nested
non-index files infer followup, and _section.yaml applies qualifies/
requires_chain/responsible to everything under its directory. Dynamic
[name].yaml pages serve any /dir/<value> with the segment substituted
into {name} resource-key placeholders; submissions index their chain
node in a portal_chains KV so requires_chain pages can verify a
visitor's ?chain= lineage actually ends at the required question.
Loading uses one recursive git-trees call; question_lint walks
subdirectories the same way. Implements docs/design/filesystem-routes.md.

Also: the YES hero now starts at HTML parse time via an inline module
script (yes.js moved to public/ for a stable /yes.js the wasm binding
raw_module-imports too - snippet paths are per-build-hashed), with
hydration adopting the running instance; and both gesture containers
reserve their box in CSS so mounting doesn't shift content.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-24 22:30:28 +02:00
Bendik Aagaard LynghaugandClaude Fable 5 b737e0a7e7 Design doc: filesystem routes, sections, dynamic segments
Test / test (push) Successful in 24s
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-24 21:58:40 +02:00
Bendik Aagaard LynghaugandClaude Fable 5 b354db2e76 README: frame portal as a generic multi-site question engine
Test / test (push) Successful in 24s
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-24 21:53:17 +02:00
Bendik Aagaard LynghaugandClaude Fable 5 74965376bd Docs: release/rollout flow after the instance-ownership refactor
Test / test (push) Successful in 23s
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-24 21:49:31 +02:00
Bendik Aagaard LynghaugandClaude Fable 5 8858336ecc Semver releases cut with cargo-release; publish on v* tags only
Test / test (push) Successful in 23s
Publish release / publish (push) Successful in 1m8s
cargo release <level> bumps, tags v<semver>, and pushes; publish.yml
reacts to the tag and attaches the artifact to that release. Plain
main pushes now run tests (test.yml) instead of publishing build-<sha>
artifacts on every push.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-24 21:44:31 +02:00
Bendik Aagaard LynghaugandClaude Fable 5 2b593ba53f Deploy becomes publish: portal ships as a versioned release artifact
Publish release / publish (push) Successful in 1m7s
The portal repo no longer deploys instances. CI builds once, packages
portal + question_lint + site/ into a tarball, and publishes it as a
Gitea release tagged build-<shortsha> using the run's ephemeral token.
Content repos (uhhm/questions, redoal/questions) now own their instance
deploys - env, unit restart, Caddy route - pinned to a release tag.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-24 21:21:06 +02:00
Bendik Aagaard LynghaugandClaude Fable 5 e0094f2e97 Trigger deploy: redoal OAuth2 credentials now configured
Deploy / deploy (push) Successful in 1m8s
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-23 20:39:16 +02:00
Bendik Aagaard LynghaugandClaude Fable 5 0def91f8ab Echo thumbnails fade in via timeout, not rAF
Deploy / deploy (push) Successful in 1m5s
The 'shown' class only needs a later tick for the opacity transition
to run - requestAnimationFrame never fires in headless engines
(caught in hwatu verification), and a timeout is equivalent in real
browsers.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-23 13:16:53 +02:00
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
Bendik Aagaard LynghaugandClaude Fable 5 6cd8cc6ff5 Showcase jq test tracks Gitea repo shape and website-first links
Deploy / deploy (push) Successful in 1m2s
Mirrors the index.yaml filter change in uhhm/questions: fixture now
carries Gitea 1.27's actual fields (stars_count, website, private)
and the test covers all three link outcomes - public repo without a
website links to the repo, private with a website links there, and
private without one gets url: null so the card renders unlinked
instead of 404ing for visitors.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-23 11:27:49 +02:00
Bendik Aagaard Lynghaug 394133b687 increase translucency of laternative cards
Deploy / deploy (push) Successful in 1m4s
2026-08-17 18:50:09 +02:00
Bendik Aagaard LynghaugandClaude Fable 5 1b7ffe0f43 Noise-driven YES, sticky hero with frosted cards, full light theme
Deploy / deploy (push) Successful in 1m0s
Three changes that belong together, all verified against a real
mounted page in headless WebKit:

- yes.js's two behavior dials (containment/wiggle) now drift on a
  smooth two-octave value-noise field at cloud pace (~25s per weather
  change) instead of following the mouse. Includes a real bug fix
  found in verification: the hash's final XOR yields a SIGNED 32-bit
  value in JS, so the "0..1" noise dipped to -0.36 without a
  reinterpreting >>> 0.

- The landing hero is position: sticky, so the piece keeps animating
  behind the whole page. Cards go translucent with backdrop blur and
  a soft shadow so the piece reads faintly through and around them
  (near-opaque fallback where backdrop-filter is unsupported). The
  hero copy fades out over the first half-screen of scroll - pinned,
  it ghosted through the cards. The bottom fade gradient is gone: its
  hard-cut reason disappeared with the canvas behind everything.

- Full prefers-color-scheme light theme: warm paper, near-black ink,
  accent deepened from dusty cyan to teal ink (#8ec2c0 washes out on
  white), wordmark inverted via filter. yes.js mirrors the palette
  itself (canvas can't read CSS vars): CMYK process-ink strokes dark
  enough to carry on paper, raster ghost repainted as multiply-blended
  gray on white, live re-theme on scheme flip with a trail clear.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-16 15:53:55 +02:00
Bendik Aagaard LynghaugandClaude Fable 5 ee91b092d1 Drop stray local tool temp file, ignore .claude/
Deploy / deploy (push) Successful in 1m1s
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-15 12:18:24 +02:00
Bendik Aagaard LynghaugandClaude Fable 5 c9c5caf3ff Stable server fn endpoints: open tabs survive deploys
Deploy / deploy (push) Successful in 1m3s
The 'previous question's resources until refresh' mystery: leptos's
auto-generated server fn routes embed a hash that changes across
builds. Every deploy therefore breaks every already-open tab - its
wasm keeps calling /api/get_question<oldhash>, the new server answers
400 'Could not find a server function at the route', and client-side
navigation quietly leaves the previous question's data on screen.
Refresh loads the new wasm with matching hashes, which is why it
always fixed it. Confirmed live: a pre-deploy client 400ed on
get_question2970801986613442004 while the freshly served wasm calls
get_question13103328088426240960, same source on both builds.

Explicit endpoint names decouple the URL from the build. This deploy
is the last breaking one; after it, old clients keep working.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-15 12:18:15 +02:00
Bendik Aagaard LynghaugandClaude Fable 5 5b3ffa62e8 Editor type scales for real: drop prosekit typography.css, fix selectors
Deploy / deploy (push) Successful in 1m16s
The earlier rem override never matched: editor.mount() turns the
.prosekit-editor div itself into the .ProseMirror root, so
'.prosekit-editor .ProseMirror p' selected nothing and prosekit
typography.css's fixed 16px kept winning. Verified by mounting the
editor standalone and reading the mount div's classes.

Rather than out-specificity a stylesheet we don't control, stop
loading it: prosekit's typography.css is optional px-based sugar. Our
own rem scale (now selected as direct children of .prosekit-editor)
plus UA em defaults for lists/blockquote/code is the whole editor
typography, all tracking the responsive root.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-14 23:16:16 +02:00
Bendik Aagaard LynghaugandClaude Fable 5 7b5d4dbf52 No translucent input chrome: solid ink-dim placeholders and embed meta
Deploy / deploy (push) Successful in 1m15s
One placeholder rule for native inputs and the prosekit editor alike;
kills ProseKit's 0.3-opacity placeholder and Firefox's UA dimming. The
embed description drops inline opacity for a fixed mid-gray that stays
dim on both the newsletter's light background and our dark paper.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-14 23:05:43 +02:00
Bendik Aagaard LynghaugandClaude Fable 5 117cd3216d Ban px from stylesheets: every length rem/em off the responsive root
Deploy / deploy (push) Successful in 1m15s
Only the :root font-size definition itself stays absolute - it is the
base everything derives from. Embed-card inline styles in the editor
follow the same rule; already-stored embeds keep their old px inline
styles until re-edited.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-14 22:46:14 +02:00
Bendik Aagaard LynghaugandClaude Fable 5 428716e479 Editor type in rem: prosekit's px typography ignored the responsive root
Deploy / deploy (push) Successful in 1m5s
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-14 19:45:58 +02:00
Bendik Aagaard LynghaugandClaude Fable 5 5571d21b21 Accent to a dusty press-cyan, a muted echo of the canvas's cyan ink
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-14 19:45:58 +02:00
Bendik Aagaard Lynghaug f3ca83b9f9 add some more width to our alternatives
Deploy / deploy (push) Successful in 1m0s
2026-08-13 14:46:30 +02:00
Bendik Aagaard LynghaugandClaude Sonnet 5 357d755b8c Bound inputs: a field that loads its value from a sibling-parameterized resource
Deploy / deploy (push) Successful in 1m7s
Requirement.bind {field, param, resource}: when the watched sibling
changes (a file select, say), the bind's resource is fetched with the
sibling's value as a parameter and the result becomes this field's
value - the on-site editing flow for /develop-proposal, and a generic
select-plus-detail primitive anywhere else. Url sources gain {name}
path templating (percent-encoded, / preserved, SSRF check runs after
substitution); an empty sibling never fetches and never clears an
edit. Bound textareas render monospace.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-13 13:49:42 +02:00
Bendik Aagaard LynghaugandClaude Sonnet 5 3d8af23167 Responsible note back to the footer, above the question nav
Deploy / deploy (push) Successful in 1m2s
Under the title it read as asking for feedback before the visitor had
seen anything - it's meta, it closes the page rather than opening it.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-12 23:48:35 +02:00
Bendik Aagaard LynghaugandClaude Sonnet 5 f1d835d107 Responsible note as <small>: meta information, dimmer than the description
Deploy / deploy (push) Successful in 1m4s
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-12 23:47:14 +02:00
Bendik Aagaard LynghaugandClaude Sonnet 5 ab2b3ec128 Responsible note: one line, same hue and size as the hero description
Deploy / deploy (push) Successful in 1m3s
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-12 23:39:08 +02:00
Bendik Aagaard LynghaugandClaude Sonnet 5 ca09dce90c Feature icons: masked span painted by currentColor, not <img>
Deploy / deploy (push) Successful in 1m1s
An image can't take the text color; a mask-image painted by
background-color can. Icons now follow --feature-accent when the
feature declares a color, and the surrounding text color otherwise.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-12 23:36:14 +02:00
Bendik Aagaard LynghaugandClaude Sonnet 5 34a027e17b README: what the engine provides
Deploy / deploy (push) Successful in 1m0s
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-12 23:30:29 +02:00
Bendik Aagaard LynghaugandClaude Sonnet 5 811c48be77 Responsible note under the title; followup pages out of the nav until a chain exists
Deploy / deploy (push) Successful in 1m2s
- The "Asked by X" line moves from the page footer to directly below
  the hero, where the question it belongs to is.
- New Question.followup flag: a post-submission page only appears in
  the question nav once the visitor's context actually carries an
  answer chain - nobody qualifies for "what happens now" before
  something happened.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-12 23:25:06 +02:00
Bendik Aagaard LynghaugandClaude Sonnet 5 e237a4b3c0 Question nav + gateway alternatives
Deploy / deploy (push) Successful in 1m1s
- QuestionNav: every other question the visitor currently qualifies
  for, rendered under each page's alternatives - a concern reaches
  the people it speaks to without claiming front-page space, and an
  owner sees the gated desks in the same nav. Context-dependent by
  session (list_qualifying_questions filters via is_qualified).
- An alternative with an action but nothing to submit, confirm, or
  record is a gateway: its button now renders as a real link instead
  of a form submit that did nothing.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-12 23:09:55 +02:00
Bendik Aagaard LynghaugandClaude Sonnet 5 4d71b6ccd0 Validate action targets; format timestamps client-side; clippy cleanup
Deploy / deploy (push) Successful in 1m0s
- A dangling alternative.action (navigating to "Nothing here") is now
  a lint/load-time rejection like any other dead reference.
- Rows re-rendered after a client-side refetch showed raw milliseconds
  - format_ms now formats via js_sys::Date under hydrate, matching the
  server's rendering.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-12 23:05:23 +02:00
Bendik Aagaard LynghaugandClaude Sonnet 5 6932816c42 Lighten the comment load
Deploy / deploy (push) Successful in 1m1s
Drops porting-history narratives (dodrenett), superseded-behavior
explanations, and restatements of what the next line does. Constraint
notes (fail-closed policies, CAS semantics, cascade behavior, id
uniqueness) stay, just shorter. No code changes.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-12 22:55:36 +02:00
Bendik Aagaard LynghaugandClaude Sonnet 5 20b24f08c2 Reseed lost event history from the KV projection on transition
Deploy / deploy (push) Successful in 1m0s
A record whose event log is empty (the EVENTS stream was purged, or
the record predates event-sourcing) but whose projection exists used
to be stranded - replay found nothing, every transition failed with
"unknown item". transition_or_reseed reconstructs a minimal one-event
history from the projection's own current state and retries, making
"zap the whole event stream" a recoverable operation by design instead
of a slow-motion data loss. Reseed races are guarded by the same
expected-sequence-0 CAS create uses.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-12 22:27:57 +02:00
Bendik Aagaard LynghaugandClaude Sonnet 5 3233d9a3db Alternative.images: array of urls, rendered as a Swiper card deck
Deploy / deploy (push) Successful in 1m0s
image: Option<String> becomes images: Vec<String> (nothing in live
content used the old field). One url renders as the plain banner it
was; several become a swipeable cards-effect deck via Swiper Element
12.2.0, vendored into public/ (MIT) like prosekit-editor.js/yes.js
rather than pulled from a CDN - the bundle only loads on pages where
some alternative actually declares more than one image.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-12 22:11:07 +02:00
Bendik Aagaard LynghaugandClaude Sonnet 5 40694a8f3f Transition.from: state graphs deeper than one decision
Deploy / deploy (push) Successful in 1m0s
Transitions now declare which state they fire from (default "open",
today's implicit behavior - existing content needs no edits). AnswerRow
renders only the buttons legal from a row's current state; two declared
transitions may share a target (open -> declined, in_dialogue ->
declined), so apply_transition matches on (from, to) against the row's
actual state instead of the old blanket "already decided" check.
validate_questions/question_lint reject a from naming an undeclared
state or a from -> to pair the bucket's graph never declared.

store_answer's KV projection now starts a record at its schema's
declared initial state, not a hardcoded "open" - organizations begin
at "prospect", and the read model has to agree with the event log
about that.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-12 22:03:43 +02:00
Bendik Aagaard LynghaugandClaude Sonnet 5 35663a28c1 Drop question-report entirely, obfuscate the mailto link instead
Deploy / deploy (push) Successful in 1m2s
The report button logged to an event stream nothing ever read - no
notification, no dashboard, just a durable no-op. A browser back
button already covers "this page wasn't helpful" better than a button
that silently does nothing visible to anyone but the visitor who
clicked it.

The mailto address is now assembled from data-user/data-domain on a
real mouse event instead of baked into the server-rendered href -
keeps a plain mailto: string (what bulk scrapers regex HTML for) out
of what a generic bot crawl sees, without hiding the contact option
from an actual visitor.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-12 20:48:27 +02:00
Bendik Aagaard LynghaugandClaude Sonnet 5 27dac946a3 Drop the feature list's left-padding
Deploy / deploy (push) Successful in 1m1s
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-12 20:32:53 +02:00
Bendik Aagaard LynghaugandClaude Sonnet 5 3ccc50248f Add optional Alternative.image and Feature.color/icon for richer layouts
Deploy / deploy (push) Successful in 1m6s
image: a banner url rendered as <img>, client-fetched directly (never
server-side, so none of ResourceSource::Url's SSRF concern applies).
color: any CSS color, set as the feature's own --feature-accent custom
property (never interpolated into a stylesheet) - draws a left-border
accent; unset means no border, not a fallback to the global --accent.
icon: an Iconify name (lucide:star etc.), rendered via Iconify's public
SVG API - no icon library bundled, matching this app's zero-JS-
dependency content otherwise keeps to.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-12 17:34:07 +02:00
Bendik Aagaard LynghaugandClaude Sonnet 5 a29c025a85 get_resource/get_requirement_options: #[server(default)] on params
Deploy / deploy (push) Successful in 1m1s
An empty params map serializes to no `params` key at all on a real
client-side call, so every Resource::refetch() after an empty-params
fetch failed with "missing field params" - only ever masked before
because a resource's *first* load is always server-resolved during
SSR, never round-tripping through serialization at all. Surfaced by
the new shared batch-confirm button's post-confirm refetch, but this
already affected the old per-row confirm's refetch identically -
pre-existing, not new.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-12 16:53:12 +02:00