Compare commits

53 Commits
Author SHA1 Message Date
bl d96a020d0b Update questions/reported.yaml
Lint and reload / lint (push) Successful in 3s
Lint and reload / reload (push) Successful in 0s
2026-09-01 21:13:59 +00:00
Bendik Aagaard Lynghaug f1855d3f8b Pin portal v0.3.32
Deploy instance / deploy (push) Successful in 1s
Lint and reload / lint (push) Successful in 3s
Lint and reload / reload (push) Successful in 0s
2026-09-01 22:15:10 +02:00
Bendik Aagaard Lynghaug 86ca5228de Pin portal v0.3.31
Deploy instance / deploy (push) Successful in 1s
Lint and reload / lint (push) Successful in 3s
Lint and reload / reload (push) Successful in 0s
2026-09-01 21:51:02 +02:00
Bendik Aagaard Lynghaug e68e5c0c37 Pin portal v0.3.30
Deploy instance / deploy (push) Successful in 1s
Lint and reload / lint (push) Successful in 3s
Lint and reload / reload (push) Successful in 0s
2026-09-01 21:45:06 +02:00
Bendik Aagaard Lynghaug a732d1eb1b Pin portal v0.3.29
Deploy instance / deploy (push) Successful in 1s
Lint and reload / lint (push) Successful in 3s
Lint and reload / reload (push) Successful in 1s
2026-09-01 21:39:39 +02:00
Bendik Aagaard Lynghaug ec78a76ae9 Pin portal v0.3.28
Deploy instance / deploy (push) Successful in 1s
Lint and reload / lint (push) Successful in 3s
Lint and reload / reload (push) Successful in 0s
2026-09-01 20:33:30 +02:00
Bendik Aagaard Lynghaug 5db9e42a6b Pin portal v0.3.27
Deploy instance / deploy (push) Successful in 0s
Lint and reload / lint (push) Successful in 2s
Lint and reload / reload (push) Successful in 0s
2026-09-01 20:29:11 +02:00
Bendik Aagaard LynghaugandClaude Fable 5 10f220b928 wordmark_invert: true (white-stroke mark)
Lint and reload / lint (push) Successful in 2s
Lint and reload / reload (push) Successful in 1s
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Y42TyF8Zu7NGRR2893vNcZ
2026-09-01 20:26:44 +02:00
Bendik Aagaard LynghaugandClaude Fable 5 16bd2e41eb Pin portal v0.3.24
Deploy instance / deploy (push) Successful in 1s
Lint and reload / lint (push) Successful in 3s
Lint and reload / reload (push) Successful in 0s
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Y42TyF8Zu7NGRR2893vNcZ
2026-09-01 20:03:57 +02:00
Bendik Aagaard LynghaugandClaude Fable 5 e58aa9d43c Pin portal v0.3.23
Deploy instance / deploy (push) Successful in 1s
Lint and reload / lint (push) Successful in 2s
Lint and reload / reload (push) Successful in 0s
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Y42TyF8Zu7NGRR2893vNcZ
2026-09-01 17:26:12 +02:00
Bendik Aagaard LynghaugandClaude Fable 5 7f4058901b Pin portal v0.3.21: markdown images, optional sign-in, hero centering
Deploy instance / deploy (push) Successful in 1s
Lint and reload / lint (push) Successful in 3s
Lint and reload / reload (push) Successful in 1s
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Y42TyF8Zu7NGRR2893vNcZ
2026-09-01 17:13:51 +02:00
Bendik Aagaard LynghaugandClaude Fable 5 19eccfd212 Pin portal v0.3.18: convergence animation, overlay results
Deploy instance / deploy (push) Successful in 1s
Lint and reload / lint (push) Successful in 2s
Lint and reload / reload (push) Successful in 0s
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Y42TyF8Zu7NGRR2893vNcZ
2026-09-01 16:01:03 +02:00
Bendik Aagaard LynghaugandClaude Fable 5 c2f2fdb546 Report a recording: per-voice review link, desk, and lifecycle
Deploy instance / deploy (push) Successful in 1s
Lint and reload / lint (push) Successful in 3s
Lint and reload / reload (push) Successful in 1s
Every recording card links to /report/<digest>. Filing opens a
redoal_reports record; the Reports desk decides — Remove it acts for
real (the relay deletes the recording everywhere), Keep it closes
with the voice in place. Pins portal v0.3.17 for markdown in cards.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Y42TyF8Zu7NGRR2893vNcZ
2026-08-31 17:22:48 +02:00
Bendik Aagaard LynghaugandClaude Fable 5 02d3901610 One card: Built in the open at UHHM
Lint and reload / lint (push) Successful in 3s
Lint and reload / reload (push) Successful in 0s
Merges the facilitator credit and the young-and-honest card.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Y42TyF8Zu7NGRR2893vNcZ
2026-08-31 13:51:29 +02:00
Bendik Aagaard LynghaugandClaude Fable 5 bc96601001 Credit UHHM as facilitator; varde link follows its repo home
Lint and reload / lint (push) Successful in 4s
Lint and reload / reload (push) Successful in 0s
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Y42TyF8Zu7NGRR2893vNcZ
2026-08-31 13:33:40 +02:00
Bendik Aagaard LynghaugandClaude Fable 5 2007e80851 Pin portal v0.3.16
Deploy instance / deploy (push) Successful in 1s
Lint and reload / lint (push) Successful in 3s
Lint and reload / reload (push) Successful in 0s
Hidden fields lose their stray label row (the Address listing under
Email in the watcher card) and the announce sweeper refreshes open
event records from content.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Y42TyF8Zu7NGRR2893vNcZ
2026-08-30 23:16:01 +02:00
Bendik Aagaard LynghaugandClaude Fable 5 dcbeda34b7 Watchers: double opt-in
Lint and reload / lint (push) Successful in 3s
Lint and reload / reload (push) Successful in 0s
redoal_watchers starts pending; /watching (the confirmation mail's
landing page) confirms to open or drops. Only open watchers are told.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-30 19:37:10 +02:00
Bendik Aagaard Lynghaug 865c426266 Pin portal v0.3.14 - icon column
Deploy instance / deploy (push) Successful in 1s
Lint and reload / lint (push) Successful in 2s
Lint and reload / reload (push) Successful in 0s
2026-08-30 16:58:21 +02:00
Bendik Aagaard LynghaugandClaude Fable 5 bee1ba406b Hero: clip the bleed at the header, never overflow the document
Lint and reload / lint (push) Successful in 2s
Lint and reload / reload (push) Successful in 0s
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-30 16:48:33 +02:00
Bendik Aagaard Lynghaug 137415ee92 Pin portal v0.3.13 - voice keeps on submit
Deploy instance / deploy (push) Successful in 1s
Lint and reload / lint (push) Successful in 3s
Lint and reload / reload (push) Successful in 1s
2026-08-30 16:38:17 +02:00
Bendik Aagaard LynghaugandClaude Fable 5 57c7bc7528 Place page: preview-of-the-app copy, review before keeping
Lint and reload / lint (push) Successful in 2s
Lint and reload / reload (push) Successful in 0s
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-30 16:37:44 +02:00
Bendik Aagaard Lynghaug 98c1213d02 Pin portal v0.3.12
Deploy instance / deploy (push) Successful in 1s
Lint and reload / lint (push) Successful in 2s
Lint and reload / reload (push) Successful in 0s
2026-08-30 16:08:59 +02:00
Bendik Aagaard LynghaugandClaude Fable 5 66c31786ce Places: /shape/<key>, voices, address newsletter
Lint and reload / lint (push) Successful in 2s
Lint and reload / reload (push) Successful in 0s
The landing gesture leads to /shape/{curve.key} - the place page:
what was left there (the relay's /place, playable), leave your voice
(type: voice at value "{key}"), hear from exactly this address
(redoal_watchers, exact key), and the cards. Email leaves the landing
page. A card for iroh through varde. Desks for voices and watchers.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-30 16:05:26 +02:00
Bendik Aagaard Lynghaug 605eb549f8 Pin portal v0.3.11 - templated action lint fix
Deploy instance / deploy (push) Successful in 0s
Lint and reload / lint (push) Successful in 2s
Lint and reload / reload (push) Successful in 0s
2026-08-30 16:04:59 +02:00
Bendik Aagaard Lynghaug 8303626e78 Pin portal v0.3.10 - voice, places
Deploy instance / deploy (push) Successful in 1s
Lint and reload / lint (push) Successful in 2s
Lint and reload / reload (push) Successful in 0s
2026-08-30 16:00:07 +02:00
Bendik Aagaard Lynghaug aae9ea5592 Pin portal v0.3.9 - templated actions, pickable places
Lint and reload / lint (push) Successful in 2s
Deploy instance / deploy (push) Successful in 1s
Lint and reload / reload (push) Successful in 0s
2026-08-30 15:42:12 +02:00
Bendik Aagaard LynghaugandClaude Fable 5 6bac75891f Hero: the field bleeds past its box
Lint and reload / lint (push) Successful in 2s
Lint and reload / reload (push) Successful in 1s
25% overscan on every side, drawing scaled to the piece so nothing
grows; html overflow-x: clip keeps the bleed from scrolling sideways.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-30 15:17:26 +02:00
Bendik Aagaard LynghaugandClaude Fable 5 671b20b3c2 Exhibition: 23 October 19:00, «Big Tech må vekk», linked
Lint and reload / lint (push) Successful in 2s
Lint and reload / reload (push) Successful in 0s
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-30 14:47:40 +02:00
Bendik Aagaard Lynghaug 0dff54e2cd Pin portal v0.3.8 - markdown descriptions
Lint and reload / lint (push) Successful in 2s
Deploy instance / deploy (push) Successful in 1s
Lint and reload / reload (push) Successful in 0s
2026-08-30 14:46:59 +02:00
Bendik Aagaard Lynghaug bef058af8f Pin portal v0.3.7 - feature links
Deploy instance / deploy (push) Successful in 1s
Lint and reload / lint (push) Successful in 2s
Lint and reload / reload (push) Successful in 0s
2026-08-30 14:42:01 +02:00
bl b02a3c2a42 Merge pull request 'Landing page: talking points drawn from the zine' (#1) from zine-talking-points into main
Lint and reload / reload (push) Successful in 0s
Lint and reload / lint (push) Successful in 3s
Reviewed-on: #1
2026-08-30 12:35:23 +00:00
Bendik Aagaard LynghaugandClaude Fable 5 064c96f5ab Announce the group exhibition at Podium
Lint and reload / lint (pull_request) Successful in 2s
Lint and reload / reload (pull_request) Skipped
Opens 21 October 2026, runs a month. Uses portal's event: window so
it rides in the header while it runs.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-30 14:33:45 +02:00
Bendik Aagaard Lynghaug 3d229754bf Merge main
Lint and reload / lint (pull_request) Successful in 2s
Lint and reload / reload (pull_request) Skipped
2026-08-30 14:29:47 +02:00
Bendik Aagaard LynghaugandClaude Fable 5 588e35fd44 Hero: every path moves on its own, and the tip glides
Lint and reload / lint (push) Successful in 3s
Lint and reload / reload (push) Successful in 0s
The trace advanced a whole sample at a time (floor of points x
progress) - visible steps; the tip now interpolates between samples.
The single field-wide rotate is gone: each path turns at its own
seeded rate and direction about its own center, drifts on its own
small orbit, carries its own wave and traces itself at its own tempo.
Every rate is an integer number of cycles, so the loop stays
seamless. Same generator feeds the paint worklet and the canvas.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-30 14:29:39 +02:00
Bendik Aagaard LynghaugandClaude Fable 5 ecd77fae35 Trial event page retired - announced pages verified live
Lint and reload / lint (push) Successful in 3s
Lint and reload / reload (push) Successful in 0s
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-30 14:28:46 +02:00
Bendik Aagaard Lynghaug 9a56a0fe8b Trial event: second window for the header capture
Lint and reload / lint (push) Successful in 2s
Lint and reload / reload (push) Successful in 0s
2026-08-30 14:27:59 +02:00
Bendik Aagaard LynghaugandClaude Fable 5 79ab3b37b0 Review desk: announced pages; trial event page
Lint and reload / lint (push) Successful in 3s
Lint and reload / reload (push) Successful in 0s
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-30 12:37:18 +02:00
Bendik Aagaard Lynghaug f4e9db8475 Pin portal v0.3.6 - announced pages
Deploy instance / deploy (push) Successful in 1s
Lint and reload / lint (push) Successful in 2s
Lint and reload / reload (push) Successful in 0s
2026-08-30 12:36:48 +02:00
Bendik Aagaard LynghaugandClaude Fable 5 758b046049 Drop the manifest-language card and the program mention
Lint and reload / lint (pull_request) Successful in 2s
Lint and reload / reload (pull_request) Skipped
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-30 12:18:30 +02:00
Bendik Aagaard LynghaugandClaude Fable 5 039d2bf54d Deploy enables the instance unit
Deploy instance / deploy (push) Successful in 1s
Lint and reload / lint (push) Successful in 2s
Lint and reload / reload (push) Successful in 0s
A host reboot on 2026-08-30 left the instance down: deploys only ever
started it. enable is best-effort until the host's polkit rule grants
manage-unit-files to deploy-runner.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-30 12:09:14 +02:00
Bendik Aagaard Lynghaug 60050d80ae Pin portal v0.3.5 - gesture ghost for v2 keys
Deploy instance / deploy (push) Successful in 1s
Lint and reload / lint (push) Successful in 3s
Lint and reload / reload (push) Successful in 0s
2026-08-30 11:43:10 +02:00
Bendik Aagaard LynghaugandClaude Fable 5 818c56dd09 Drop the self-reconstruction claim
Lint and reload / lint (pull_request) Successful in 3s
Lint and reload / reload (pull_request) Skipped
decode_key's sketch is too rough to be worth advertising.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-30 11:21:06 +02:00
Bendik Aagaard LynghaugandClaude Fable 5 bb0eeb58b1 Say what each card enables
Lint and reload / lint (pull_request) Successful in 3s
Lint and reload / reload (pull_request) Skipped
Every talking point now leads with the capability instead of the
absence: the room answers, the seal is yours, drawing and speaking
is the entry requirement.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-30 11:13:30 +02:00
Bendik Aagaard LynghaugandClaude Fable 5 3c2f69a290 Landing page: talking points drawn from the zine
Lint and reload / lint (pull_request) Successful in 3s
Lint and reload / reload (pull_request) Skipped
A new alternative, "Meet a network with no list and no landlord",
carries seven feature cards lifted from the redoal repo's zine and
intro-video script: the drawing-as-address trick, no middleman,
voices at a place, signatures not names, the twenty-four-word
manifest language, the no-reading entry requirement, and the honest
status. The landing description opens with the cold-open line, and
/shape gains a card on what a place holds in the full network.

Nothing here overclaims: every card describes something the redoal
repo ships and tests, and the last one says what doesn't yet.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-30 11:12:01 +02:00
Bendik Aagaard LynghaugandClaude Fable 5 429d3a5bff Pin portal v0.3.4 - disabled alternatives
Deploy instance / deploy (push) Successful in 1s
Lint and reload / lint (push) Successful in 2s
Lint and reload / reload (push) Successful in 0s
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-25 20:28:14 +02:00
Bendik Aagaard LynghaugandClaude Fable 5 5b04c12ce8 Find what follows: announced, not yet open (disabled: true)
Lint and reload / lint (push) Successful in 3s
Lint and reload / reload (push) Successful in 0s
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-25 20:25:48 +02:00
Bendik Aagaard LynghaugandClaude Fable 5 f844ec02eb Pin portal v0.3.3 - empty-list notice
Deploy instance / deploy (push) Successful in 1s
Lint and reload / lint (push) Successful in 2s
Lint and reload / reload (push) Successful in 0s
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-25 20:11:22 +02:00
Bendik Aagaard LynghaugandClaude Fable 5 dbc44956d3 Pin portal v0.3.2 - resource empty text
Deploy instance / deploy (push) Successful in 0s
Lint and reload / lint (push) Successful in 2s
Lint and reload / reload (push) Successful in 0s
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-25 20:07:26 +02:00
Bendik Aagaard LynghaugandClaude Fable 5 8c79787d87 Releases: say so when nothing is released yet
Lint and reload / lint (push) Successful in 3s
Lint and reload / reload (push) Successful in 0s
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-25 20:05:21 +02:00
Bendik Aagaard LynghaugandClaude Fable 5 83f7533c21 Pin portal v0.3.1 - content-hashed assets (LEPTOS_HASH_FILES=true)
Deploy instance / deploy (push) Successful in 0s
Lint and reload / lint (push) Successful in 2s
Lint and reload / reload (push) Successful in 1s
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-25 19:51:30 +02:00
Bendik Aagaard LynghaugandClaude Fable 5 9a5ebb6715 Hero copy points below, where the canvas now is
Lint and reload / lint (push) Successful in 3s
Lint and reload / reload (push) Successful in 0s
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-25 19:28:31 +02:00
Bendik Aagaard Lynghaug e6f847b7d3 hero.js: don't shadow the global CSS object 2026-08-25 19:27:37 +02:00
Bendik Aagaard Lynghaug 683c6da6b9 Hero: lysbue's paths paint worklet, animated (WIP branch) 2026-08-25 19:26:54 +02:00
16 changed files with 779 additions and 136 deletions
+12 -1
View File
@@ -18,7 +18,7 @@ on:
- .gitea/workflows/deploy.yml - .gitea/workflows/deploy.yml
env: env:
PORTAL_RELEASE: v0.3.0 PORTAL_RELEASE: v0.3.32
INSTANCE: redoal-portal INSTANCE: redoal-portal
jobs: jobs:
@@ -67,6 +67,10 @@ jobs:
# The release tarball carries the site bundle at site/ (no # The release tarball carries the site bundle at site/ (no
# target/ prefix), so override Cargo.toml's build-time path. # target/ prefix), so override Cargo.toml's build-time path.
LEPTOS_SITE_ROOT=site LEPTOS_SITE_ROOT=site
# Portal ships content-hashed pkg files (portal.<hash>.js) with
# a hash.txt in the site root; this makes the server reference
# them, so a stale cached bundle can never pair with new wasm.
LEPTOS_HASH_FILES=true
# Read-only Gitea token - here it also backs the # Read-only Gitea token - here it also backs the
# gitea_releases source for the private redoal/redoal repo, # gitea_releases source for the private redoal/redoal repo,
# so the owning user needs read access there. # so the owning user needs read access there.
@@ -79,9 +83,16 @@ jobs:
# No sudo: the runner's unit sets NoNewPrivileges=yes - systemctl # No sudo: the runner's unit sets NoNewPrivileges=yes - systemctl
# talks to PID1 over D-Bus, authorized by the polkit rule scoped # talks to PID1 over D-Bus, authorized by the polkit rule scoped
# to deploy-runner + the app@* unit pattern. # to deploy-runner + the app@* unit pattern.
# enable: the unit must come back after a host reboot (2026-08-30 a
# reboot left both portal instances down - deploys had only ever
# started them). Needs the manage-unit-files polkit grant; until
# that's on the host the enable is reported and skipped, never a
# failed deploy.
- name: Activate and restart - name: Activate and restart
run: | run: |
ln -sfn "/srv/app/$INSTANCE/releases/$PORTAL_RELEASE" /srv/app/$INSTANCE/current ln -sfn "/srv/app/$INSTANCE/releases/$PORTAL_RELEASE" /srv/app/$INSTANCE/current
systemctl enable app@$INSTANCE.service \
|| echo "::warning::could not enable app@$INSTANCE (polkit) - unit will not survive a reboot"
systemctl restart app@$INSTANCE.service systemctl restart app@$INSTANCE.service
# www redirects to the apex for the same single-cookie-scope # www redirects to the apex for the same single-cookie-scope
+9 -5
View File
@@ -11,11 +11,15 @@ The full schema reference lives in
this repo only notes what's distinctive here: this repo only notes what's distinctive here:
- **`site.yaml`** rebrands the instance: title `redoal`, its own - **`site.yaml`** rebrands the instance: title `redoal`, its own
wordmark, and a `module` hero — `hero.js` in this repo, the drifting wordmark, and a `module` hero — `hero.js` in this repo, lysbue's
"sine swings" band grown from lysbue's logo (pure SVG + CSS, colors "paths" paint worklet brought back and animated: 47 generative
from portal's own custom properties, still under turning-function curves (`paths.js`, seeded so every repaint agrees)
prefers-reduced-motion). No input up there: the form's gesture tracing themselves in from the center. A real CSS Paint Worklet
canvas below is the one place a visitor draws. The wordmark is where the API exists (`paths-worklet.js`, driven by a registered
`--redoal-t` CSS animation), a canvas with the same generator
elsewhere; colors from portal's custom properties, frozen under
prefers-reduced-motion. No input up there: the form's gesture canvas
below is the one place a visitor draws. The wordmark is
white-stroked like uhhm's, because portal's light theme lands white-stroked like uhhm's, because portal's light theme lands
wordmarks as ink via an invert filter. wordmarks as ink via an invert filter.
- **`type: gesture`** on `index.yaml`'s "Draw a path and see who's near" — the - **`type: gesture`** on `index.yaml`'s "Draw a path and see who's near" — the
+24
View File
@@ -14,3 +14,27 @@ aggregates:
unsubscribed: { event: unsubscribed } unsubscribed: { event: unsubscribed }
transitions: transitions:
open: [unsubscribed] open: [unsubscribed]
# Double opt-in: a watcher is pending until the address confirms from
# the mail n8n sends (redoal: confirm watching); only open watchers
# are told when something lands at their shape.
- bucket: redoal_watchers
initial: pending
states:
pending: { event: requested }
open: { event: confirmed }
unsubscribed: { event: unsubscribed }
transitions:
pending: [open, unsubscribed]
open: [unsubscribed]
# Reports on recordings. The relay watches this bucket: a record
# moved to "removed" deletes the recording from its store, the varde
# pin, and discovery. "kept" closes the report with the recording in
# place. Decisions are human, never automatic.
- bucket: redoal_reports
initial: open
states:
open: { event: reported }
removed: { event: removed }
kept: { event: kept }
transitions:
open: [removed, kept]
+131 -94
View File
@@ -1,66 +1,65 @@
// redoal.com's hero: "sine swings" - the drifting layered sine paths // redoal.com's hero: lysbue's "paths" paint worklet, animated - the
// from lysbue's logo (the site this content descends from), grown from // site this content descends from painted its pages with generative
// a clipped 400px circle into a full-width band. No input: the form's // turning-function curves (static/worklet/paint/paths.js); this is
// gesture canvas below is the one place a visitor draws. Content-owned: // that field, alive. No input up here: the form's gesture canvas is
// portal knows only site.yaml's `hero: {kind: module, module: hero.js}` // the one place a visitor draws.
// and the contract - `mount(container) -> handle` with `stop()`; it
// serves this file same-origin at /site/hero.js, starts it at HTML
// parse time, adopts it on hydration, and stops it on navigation.
// //
// Pure SVG + CSS: the strokes take their color from portal's own // Content-owned: portal knows only site.yaml's `hero: {kind: module,
// custom properties (--accent, --ink-dim), so both themes come for // module: hero.js}` and the contract - `mount(container) -> handle`
// free and there's no palette copy to keep in sync. Motion is a CSS // with `stop()` - and serves this repo's files same-origin under
// keyframe on each layer's transform, disabled under // /site/. Two renderers, one generator (paths.js):
// prefers-reduced-motion. //
// - Where the CSS Paint API exists (Chromium), the real thing: the
// worklet paints the box's background and a CSS animation on the
// registered --redoal-t property drives repaints - no JS per frame.
// - Elsewhere (Safari, Firefox, WebKitGTK have no Paint API), a
// canvas with a requestAnimationFrame loop draws the identical
// field, colors read from the page's custom properties.
//
// prefers-reduced-motion freezes the field fully drawn either way.
import { makePaths, drawPaths, OVERSCAN } from './paths.js';
const STYLE_ID = 'redoal-hero-style'; const STYLE_ID = 'redoal-hero-style';
const CYCLE_MS = 28000;
const SEED = 7;
const COUNT = 47;
// One period is 400 units wide; the path is written two periods long const BLEED = `${((OVERSCAN - 1) / 2) * -100}%`;
// so translating by exactly one period loops seamlessly.
const SWING =
'M 0 200 Q 100 100 200 200 Q 300 300 400 200 ' +
'Q 500 100 600 200 Q 700 300 800 200 ' +
'Q 900 100 1000 200 Q 1100 300 1200 200';
// (y offset, stroke width, opacity, seconds per period, direction) const STYLE_TEXT = `
const LAYERS = [ /* The field bleeds 25% past the hero piece on every side. Sideways
[0, 6.5, 0.55, 9, 1], it is clipped at the header box (clip, not hidden: no scroll
[7, 3, 0.45, 11, 1], container, and overflow-y stays visible) - an element wider than
[9, 1, 0.5, 13, 1], the viewport, even unscrollable, makes phones re-size the layout
[15, 0.5, 0.6, 17, 1], viewport when it appears, which read as the page jumping as the
[-60, 1.5, 0.18, 23, -1], input loaded. Vertically it may run into the copy below. */
[70, 1, 0.14, 29, -1], .hero-module { overflow-x: clip; overflow-y: visible; }
]; .redoal-paths {
const CSS = `
.redoal-hero {
position: absolute; position: absolute;
inset: 0; inset: ${BLEED};
overflow: hidden; overflow: visible;
pointer-events: none; pointer-events: none;
--redoal-t: 0;
--redoal-seed: ${SEED};
--redoal-count: ${COUNT};
} }
.redoal-hero svg { .redoal-paths.worklet {
background: paint(redoal-paths);
animation: redoal-t ${CYCLE_MS}ms linear infinite;
}
.redoal-paths canvas {
position: absolute; position: absolute;
inset: 0; inset: 0;
width: 100%; width: 100%;
height: 100%; height: 100%;
} }
.redoal-hero .swing { @keyframes redoal-t {
fill: none; from { --redoal-t: 0; }
stroke: var(--accent); to { --redoal-t: 1; }
stroke-linecap: round;
animation: redoal-swing var(--dur) linear infinite;
animation-direction: var(--dir);
}
.redoal-hero .swing.far {
stroke: var(--ink-dim);
}
@keyframes redoal-swing {
from { transform: translateX(0); }
to { transform: translateX(-400px); }
} }
@media (prefers-reduced-motion: reduce) { @media (prefers-reduced-motion: reduce) {
.redoal-hero .swing { animation: none; } .redoal-paths.worklet { animation: none; --redoal-t: 0.999; }
} }
`; `;
@@ -68,67 +67,105 @@ function ensureStyle() {
if (document.getElementById(STYLE_ID)) return; if (document.getElementById(STYLE_ID)) return;
const style = document.createElement('style'); const style = document.createElement('style');
style.id = STYLE_ID; style.id = STYLE_ID;
style.textContent = CSS; style.textContent = STYLE_TEXT;
document.head.appendChild(style); document.head.appendChild(style);
} }
const NS = 'http://www.w3.org/2000/svg'; const reducedMotion = () => window.matchMedia('(prefers-reduced-motion: reduce)').matches;
class SineSwings { function hasPaintApi() {
return typeof CSS !== 'undefined' && 'paintWorklet' in CSS && typeof CSS.registerProperty === 'function';
}
class PathsHero {
constructor(container) { constructor(container) {
ensureStyle(); ensureStyle();
this.root = document.createElement('div'); this.root = document.createElement('div');
this.root.className = 'redoal-hero'; this.root.className = 'redoal-paths';
this.root.setAttribute('aria-hidden', 'true'); this.root.setAttribute('aria-hidden', 'true');
// viewBox 400 wide per period; preserveAspectRatio "slice" so
// the band always fills the box and the loop is seamless at
// any width - the SVG is 1200 wide but only translates by 400.
const svg = document.createElementNS(NS, 'svg');
svg.setAttribute('viewBox', '0 0 800 400');
svg.setAttribute('preserveAspectRatio', 'xMidYMid slice');
// The logo's clipped circle, centered - the near layers live
// inside it; the far layers drift across the whole band.
const defs = document.createElementNS(NS, 'defs');
const clip = document.createElementNS(NS, 'clipPath');
clip.id = 'redoal-hero-clip';
const circle = document.createElementNS(NS, 'circle');
circle.setAttribute('cx', '400');
circle.setAttribute('cy', '200');
circle.setAttribute('r', '150');
clip.appendChild(circle);
defs.appendChild(clip);
svg.appendChild(defs);
const far = document.createElementNS(NS, 'g');
const near = document.createElementNS(NS, 'g');
near.setAttribute('clip-path', 'url(#redoal-hero-clip)');
// The logo's tilt.
near.setAttribute('transform', 'rotate(-13 400 200)');
for (const [dy, width, opacity, dur, dir] of LAYERS) {
const path = document.createElementNS(NS, 'path');
path.setAttribute('d', SWING);
path.setAttribute('stroke-width', String(width));
path.setAttribute('opacity', String(opacity));
path.setAttribute('transform', `translate(-200 ${dy})`);
const isFar = Math.abs(dy) > 30;
path.setAttribute('class', isFar ? 'swing far' : 'swing');
path.style.setProperty('--dur', `${dur}s`);
path.style.setProperty('--dir', dir < 0 ? 'reverse' : 'normal');
(isFar ? far : near).appendChild(path);
}
svg.append(far, near);
this.root.appendChild(svg);
container.appendChild(this.root); container.appendChild(this.root);
this.stopped = false;
if (hasPaintApi()) {
this.startWorklet();
} else {
this.startCanvas();
}
}
startWorklet() {
// Registering makes --redoal-t a real <number> the animation
// can interpolate; a second mount in the same document sees it
// already registered, which throws and is fine.
try {
CSS.registerProperty({
name: '--redoal-t',
syntax: '<number>',
inherits: false,
initialValue: '0',
});
} catch {
/* already registered */
}
const url = new URL('./paths-worklet.js', import.meta.url).href;
CSS.paintWorklet
.addModule(url)
.then(() => {
if (!this.stopped) this.root.classList.add('worklet');
})
.catch(() => {
if (!this.stopped) this.startCanvas();
});
}
startCanvas() {
this.canvas = document.createElement('canvas');
this.root.appendChild(this.canvas);
this.ctx = this.canvas.getContext('2d');
this.paths = makePaths(SEED, COUNT);
this._onResize = () => this.size();
window.addEventListener('resize', this._onResize);
this.size();
if (reducedMotion()) {
this.frame(0.999);
return;
}
this.start = performance.now();
const loop = (now) => {
if (this.stopped) return;
this.frame(((now - this.start) % CYCLE_MS) / CYCLE_MS);
this.raf = requestAnimationFrame(loop);
};
this.raf = requestAnimationFrame(loop);
}
size() {
const rect = this.root.getBoundingClientRect();
const dpr = window.devicePixelRatio || 1;
this.w = Math.max(1, rect.width);
this.h = Math.max(1, rect.height);
this.canvas.width = Math.round(this.w * dpr);
this.canvas.height = Math.round(this.h * dpr);
this.ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
this.frame(this.lastT || 0);
}
frame(t) {
this.lastT = t;
const cs = getComputedStyle(this.root);
const accent = cs.getPropertyValue('--accent').trim() || '#47807e';
const dim = cs.getPropertyValue('--ink-dim').trim() || '#6d6c66';
drawPaths(this.ctx, this.w, this.h, this.paths, t, [accent, dim]);
} }
stop() { stop() {
this.stopped = true;
if (this.raf) cancelAnimationFrame(this.raf);
if (this._onResize) window.removeEventListener('resize', this._onResize);
this.root.remove(); this.root.remove();
} }
} }
export function mount(container) { export function mount(container) {
return new SineSwings(container); return new PathsHero(container);
} }
+32
View File
@@ -0,0 +1,32 @@
// CSS Paint Worklet entry for redoal.com's hero: `background:
// paint(redoal-paths)` on the hero box, animated by the page changing
// --redoal-t (a registered <number> custom property driven by a CSS
// animation - every change repaints). Colors arrive as computed input
// properties, so the worklet follows the theme without a palette copy.
import { makePaths, drawPaths } from './paths.js';
registerPaint(
'redoal-paths',
class {
static get inputProperties() {
return ['--redoal-t', '--redoal-seed', '--redoal-count', '--accent', '--ink-dim'];
}
paint(ctx, size, props) {
const num = (name, fallback) => {
const v = parseFloat(props.get(name).toString());
return Number.isFinite(v) ? v : fallback;
};
const seed = num('--redoal-seed', 7);
const count = num('--redoal-count', 47);
const t = num('--redoal-t', 0);
if (!this.paths || this.seed !== seed || this.count !== count) {
this.paths = makePaths(seed, count);
this.seed = seed;
this.count = count;
}
const accent = props.get('--accent').toString().trim() || '#47807e';
const dim = props.get('--ink-dim').toString().trim() || '#6d6c66';
drawPaths(ctx, size.width, size.height, this.paths, t % 1, [accent, dim]);
}
},
);
+172
View File
@@ -0,0 +1,172 @@
// The generator behind redoal.com's hero - a port of lysbue's
// "paths" CSS paint worklet (static/worklet/paint/paths.js): a path is
// a turning function, a list of (angle shift, distance) stops walked
// from the center and smoothed into quadratic arcs. Shared verbatim by
// the paint worklet (paths-worklet.js, where the browser has the CSS
// Paint API) and the canvas fallback (hero.js) so both draw the same
// field. Everything is seeded: a worklet is instantiated whenever the
// engine likes, and unseeded randomness would reshuffle the picture on
// every repaint.
// The field bleeds past its box: the painted element is this much
// larger than the hero piece on each axis (25% each side), and the
// drawing scales to the piece, not the element, so nothing grows.
export const OVERSCAN = 1.5;
export function mulberry32(seed) {
let a = seed >>> 0;
return () => {
a |= 0;
a = (a + 0x6d2b79f5) | 0;
let t = Math.imul(a ^ (a >>> 15), 1 | a);
t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t;
return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
};
}
function isInBounds(x, y) {
return x > -0.5 && x < 0.5 && y > -0.5 && y < 0.5;
}
// Flat array of (angle shift, distance) pairs; distance 1 = the
// frame's short side. The first pair is the start: absolute angle and
// distance from the center.
export function createTurningFunction(rand, stopCount) {
const turning = [rand() * Math.PI * 2, rand() * 0.3];
let radianSum = turning[0];
let x = Math.cos(turning[0]) * turning[1];
let y = Math.sin(turning[0]) * turning[1];
for (let i = 0; i < stopCount; i++) {
let shiftAngle = 0, shiftDistance = 0.1, next = radianSum;
for (let j = 0; j < 80; j++) {
shiftAngle = Math.pow(1 + rand() * 4, -2) * Math.PI * 2 * (rand() > 0.5 ? -1 : 1);
shiftDistance = Math.max(rand() * 0.25, 0.05);
next = radianSum + shiftAngle;
if (isInBounds(x + Math.cos(next) * shiftDistance, y + Math.sin(next) * shiftDistance)) break;
}
radianSum = next;
x += Math.cos(radianSum) * shiftDistance;
y += Math.sin(radianSum) * shiftDistance;
turning.push(shiftAngle, shiftDistance);
}
return turning;
}
// Smooth the stops into quadratic arcs (control point = half a step
// along the previous heading, like the original) and flatten each arc
// into `samples` points so the curve can be traced progressively.
export function turningFunctionToPoints(turning, samples = 14) {
const pts = [];
let heading = turning[0];
let x = Math.cos(turning[0]) * turning[1];
let y = Math.sin(turning[0]) * turning[1];
pts.push(x, y);
for (let i = 2; i < turning.length; i += 2) {
const cx = x + Math.cos(heading) * (turning[i + 1] / 2);
const cy = y + Math.sin(heading) * (turning[i + 1] / 2);
heading += turning[i];
const nx = x + Math.cos(heading) * turning[i + 1];
const ny = y + Math.sin(heading) * turning[i + 1];
for (let s = 1; s <= samples; s++) {
const u = s / samples, v = 1 - u;
pts.push(v * v * x + 2 * v * u * cx + u * u * nx, v * v * y + 2 * v * u * cy + u * u * ny);
}
x = nx;
y = ny;
}
return pts;
}
// A path plus the motion that is its own: where it sits, how it
// spins, how it drifts, how fast it traces itself in. Every rate is
// an integer number of cycles so the field loops seamlessly at t = 1.
export function makePaths(seed, count, stops = 10) {
const rand = mulberry32(seed);
const paths = [];
for (let i = 0; i < count; i++) {
const pts = turningFunctionToPoints(createTurningFunction(rand, stops));
let cx = 0, cy = 0;
for (let k = 0; k < pts.length; k += 2) { cx += pts[k]; cy += pts[k + 1]; }
cx /= pts.length / 2;
cy /= pts.length / 2;
const sign = rand() > 0.5 ? 1 : -1;
paths.push({
pts,
cx,
cy,
// own turns per cycle: a third sit still, the rest turn one
// or two full turns either way over the cycle
spin: rand() < 0.33 ? 0 : sign * (1 + Math.floor(rand() * 2)),
// slow orbit around its own place, radius in frame units
driftR: 0.01 + rand() * 0.03,
driftK: 1 + Math.floor(rand() * 3),
driftPhase: rand() * Math.PI * 2,
// its own wave along the line
waveF: 4 + rand() * 6,
waveK: 1 + Math.floor(rand() * 2),
waveA: 0.004 + rand() * 0.008,
// tracing tempo: how many times it redraws itself per cycle
tempo: 1 + Math.floor(rand() * 2),
phase: rand(),
});
}
return paths;
}
// t in [0, 1) is the animation phase. Each path traces itself in at
// its own tempo and phase, fades as it completes, turns at its own
// rate about its own center and drifts on its own small orbit; a wave
// with its own frequency runs along it. The tip is interpolated
// between samples so a trace glides instead of stepping. `colors` is
// [accent, dim] - the palette comes from the page's custom properties.
const smooth = (u) => u * u * (3 - 2 * u);
export function drawPaths(ctx, width, height, paths, t, colors) {
ctx.clearRect(0, 0, width, height);
const s = (Math.min(width, height) / OVERSCAN) * 0.9;
ctx.save();
ctx.translate(width / 2, height / 2);
ctx.lineWidth = 1;
ctx.lineCap = 'round';
ctx.lineJoin = 'round';
const TAU = Math.PI * 2;
for (let i = 0; i < paths.length; i++) {
const p = paths[i];
const phase = (t * p.tempo + p.phase) % 1;
// ease: quick to appear, long to complete, then fade
const progress = smooth(Math.min(1, phase * 1.35));
const alpha = phase < 0.85 ? 0.75 : 0.75 * (1 - (phase - 0.85) / 0.15);
const total = p.pts.length / 2;
const tip = 1 + (total - 1) * progress; // fractional sample index
const whole = Math.floor(tip);
const frac = tip - whole;
const angle = p.spin * TAU * t;
const dx = Math.cos(p.driftPhase + p.driftK * TAU * t) * p.driftR;
const dy = Math.sin(p.driftPhase + p.driftK * TAU * t) * p.driftR;
const wave = p.waveK * TAU * t;
ctx.save();
ctx.translate((p.cx + dx) * s, (p.cy + dy) * s);
ctx.rotate(angle);
ctx.strokeStyle = i % 3 === 0 ? colors[0] : colors[1];
ctx.globalAlpha = alpha * (i % 3 === 0 ? 1 : 0.55);
ctx.beginPath();
const at = (k) => {
const x = p.pts[k * 2] - p.cx, y = p.pts[k * 2 + 1] - p.cy;
return [x, y + Math.sin(x * p.waveF + wave) * p.waveA];
};
for (let k = 0; k <= Math.min(whole, total - 1); k++) {
const [x, y] = at(k);
if (k === 0) ctx.moveTo(x * s, y * s);
else ctx.lineTo(x * s, y * s);
}
if (whole < total - 1 && frac > 0) {
const [ax, ay] = at(whole), [bx, by] = at(whole + 1);
ctx.lineTo((ax + (bx - ax) * frac) * s, (ay + (by - ay) * frac) * s);
}
ctx.stroke();
ctx.restore();
}
ctx.restore();
}
+45
View File
@@ -0,0 +1,45 @@
# The group exhibition at Podium, part of Attac's «Big Tech må vekk»
# 2026: opens 23 October 2026 at 19:00 and runs for a month. Announced
# in every header while it runs (portal's event: window); afterwards a
# followup, and a "post what happened" task lands on /review.
name: Where can you see it in person?
description: >
redoal is part of the group exhibition at Podium, opening 23 October
and running for a month — the art strand of the conference
«Big Tech må vekk». Draw on the screens, leave a voice at a shape,
hear who came before you.
event:
starts: 2026-10-23T19:00:00+02:00
duration: 31d
place: Podium, Hausmanns gate 34, Oslo
alternatives:
- name: Come draw on the wall
description: >
The installation runs the same code as this site, on screens
you can stand in front of together — the network at the scale
of a room.
features:
- name: Opening 23 October, 19:00
description: >
At [Podium](https://podium.enterprises/), Hausmanns gate 34,
and up for a month. Come to the opening, or any Saturday
or Sunday after.
icon: lucide:calendar-days
- name: Part of «Big Tech må vekk»
description: >
Attac's conference on digital counterculture returns to
Hausmania — the same building — on 31 October; the
exhibition is its art strand. Programme and details on
[attac.no](https://attac.no/arrangement/big-tech-ma-vekk-2026/).
icon: lucide:megaphone
- name: Draw, then listen
description: >
Draw a shape on the touch screen and arrive at a place.
Listen to the voices left there, add your own, and watch
your line find the lines drawn before it.
icon: lucide:hand
- name: Take the zine with you
description: >
The printed zine sits in the room — six short pieces on what
a network like this could mean. Fold one, hand it on.
icon: lucide:book-open
+71 -10
View File
@@ -1,9 +1,10 @@
id: / id: /
name: What shapes us? name: What shapes us?
description: > description: >
redoal is a gesture-addressed network — peers find each other by redoal is a gesture-addressed network. Draw a line and it becomes
drawing similar shapes. The gesture is the address. Draw one above an address — the drawing itself, ready to use the moment your hand
and see who's near. lifts. Similar drawings land near each other, and everything else
follows from that. Draw one below and see who's near.
responsible: responsible:
name: Bendik Aagaard Lynghaug name: Bendik Aagaard Lynghaug
contact: bl@uhhm.no contact: bl@uhhm.no
@@ -12,9 +13,10 @@ alternatives:
description: > description: >
By associating similar movements while facilitating discourse we By associating similar movements while facilitating discourse we
discover signal within chaos. Draw a path — the network answers discover signal within chaos. Draw a path — the network answers
with the shapes it heard nearby. with the shape it heard, and with the places kept nearby. Pick
action: /shape one of those and it becomes your address.
consequence: [Find what follows] action: /shape/{curve.key}
consequence: [Go to the place]
record_as: redoal_gestures record_as: redoal_gestures
encouragements: encouragements:
- What you don't know — your collaborators will. - What you don't know — your collaborators will.
@@ -32,10 +34,68 @@ alternatives:
type: gesture type: gesture
relay: wss://relay.redoal.com relay: wss://relay.redoal.com
optional: true optional: true
- name: email
label: Email, if the echoes should reach you later - name: Meet a network you carry in your hand
type: email description: >
optional: true The one trick, and what it enables — the parts that run today,
said plainly.
features:
- name: A drawing is an address
description: >
The computer measures how your line curves, turns, and keeps
rhythm, and boils it down to a twenty-byte fingerprint.
Similar drawings get similar fingerprints — two spirals land
like two houses on the same street, and that closeness is
the whole network.
icon: lucide:map-pin
- name: Your machine asks the room
description: >
To find someone, your machine asks the machines around it:
"has anyone been near this shape?" Your spiral finds my
spiral directly, peer to peer. The whole directory is the
shapes people know, passed along on a napkin.
icon: lucide:users
- name: Voices stay where you left them
description: >
A drawing is a place, and places hold voices. Listen to the
recordings people left at a gesture, leave your own on top,
and when someone stands at the same drawing right now, just
talk — live, machine to machine.
icon: lucide:mic
- name: Sealed by your own hand
description: >
Every recording and every stored stroke carries a signature
from the leaver's own machine — proof that it arrived
intact and that the same hand made the same things. The key
lives only with you: keep it and you're recognized, delete
it and you were never here.
icon: lucide:stamp
- name: Draw and speak, and you're in
description: >
The primitives are the drawn line and the human voice. A
child, an elder, or a newcomer still learning the local
script arrives on the same terms as everyone else — that is
the whole entry requirement.
icon: lucide:hand
- name: Carried by iroh, through varde
description: >
What is left at a place is kept and mirrored by
[varde](https://project.uhhm.no/uhhm/varde), a small
daemon that moves bytes between consenting machines over
[iroh](https://iroh.computer) — verified, rate-limited, and
legible to the network it runs on. Recordings travel that way,
with no company carrying them.
icon: lucide:waypoints
- name: Built in the open at UHHM
description: >
[UHHM](https://uhhm.no) builds and carries the gesture math,
the relay, and the portal this page runs on — and hosts the
network's first node. It is a prototype and an artwork before
it is a product: what you can touch is real and running, the
transport that reaches across the internet is being built
now, and the rough edges are where the interesting questions
live.
icon: lucide:anvil
- name: Follow the build as it lands - name: Follow the build as it lands
description: > description: >
@@ -57,6 +117,7 @@ alternatives:
owner: redoal owner: redoal
repo: redoal repo: redoal
public: true public: true
empty: Nothing released yet — the first cut lands here.
# url pinned to null while the repo is private - a private # url pinned to null while the repo is private - a private
# release's html_url 404s for anonymous visitors, and the # release's html_url 404s for anonymous visitors, and the
# card renders an unlinked heading instead (same lesson as # card renders an unlinked heading instead (same lesson as
+47
View File
@@ -0,0 +1,47 @@
# One recording, held up for human review: /report/<digest>. Reached
# from the place page's per-recording link. Filing creates an open
# record in redoal_reports; the desk decides, and a removal decision
# deletes the recording from the relay, varde, and discovery.
name: Should this stay?
description: >
Every voice here was left by a person, and a person looks at every
report. Tell us what you heard and why it needs eyes on it — the
recording stays reachable until someone has listened and decided.
alternatives:
- name: Ask for a review
description: >
Goes straight to the review desk. If the recording breaks
something — a person's privacy, the law, basic decency — it is
removed everywhere this network keeps it.
action: /reported
consequence: [Send it to review]
record_as: redoal_reports
encouragements:
- A person will listen.
features:
- name: What you heard
description: >
A sentence or two is plenty. Leave an email if you want to
hear what was decided.
icon: lucide:flag
requirements:
- name: digest
label: Recording
type: hidden
value: "{digest}"
- name: reason
label: Why
type: textarea
placeholder: What is wrong with this recording?
- name: email
label: Email
type: email
optional: true
- name: Go back to listening
description: >
The place you came from holds the rest of its voices.
features:
- name: Nothing sent
description: >
Leaving this page files nothing — only the button does.
icon: lucide:undo-2
+11
View File
@@ -0,0 +1,11 @@
# Landing after filing a report.
followup: true
name: What happens now?
description: >
Your report is on the review desk. A person listens, decides, and
the decision acts everywhere this network keeps the recording.
alternatives:
- name: Draw another
description: >
The paper keeps listening.
action: /
+87
View File
@@ -37,3 +37,90 @@ alternatives:
transitions: transitions:
- to: unsubscribed - to: unsubscribed
label: Unsubscribe label: Unsubscribe
- name: Voices
description: >
Recordings left through the web, one answer per upload: the
address, the digest, the length. The audio itself lives in the
relay's node and in varde.
features:
- name: ""
description: ""
resource:
source:
kind: kv
bucket: redoal_voices
requires_group: owners
- name: Reports
description: >
Recordings a visitor asked a person to listen to. "Remove it"
acts for real: the relay watches this bucket and deletes the
recording from its store, the varde pin, and discovery. "Keep
it" closes the report with the voice in place.
action: /review
consequence: [Decide]
features:
- name: ""
description: ""
resource:
source:
kind: kv
bucket: redoal_reports
requires_group: owners
transitions:
- from: open
to: removed
label: Remove it
- from: open
to: kept
label: Keep it
- name: Watchers
description: >
Who asked to hear from one exact address. Unsubscribing is
theirs (the link in their receipt); the desk can retire an
address that bounces.
action: /review
consequence: [Confirm]
features:
- name: ""
description: ""
resource:
source:
kind: kv
bucket: redoal_watchers
requires_group: owners
transitions:
- from: pending
to: open
label: Confirm by hand
- from: pending
to: unsubscribed
label: Drop
- from: open
to: unsubscribed
label: Unsubscribe
- name: Announced pages
description: >
Pages with an `event:` window, kept here by portal itself. When a
window closes the record becomes a task: post what happened,
then mark it done.
action: /review
consequence: [Confirm]
features:
- name: ""
description: ""
resource:
source:
kind: kv
bucket: portal_events
requires_group: owners
transitions:
- from: awaiting_summary
to: summarized
label: Posted what happened
- from: announced
to: summarized
label: Needs no summary
-26
View File
@@ -1,26 +0,0 @@
id: /shape
route: ^/shape
followup: true
name: Where does the path lead?
description: >
Your shape is in the room now. Echoes of it reach whoever draws
nearby — and theirs reach you, whenever you come back to draw again.
alternatives:
- name: What just happened
description: ""
features:
- name: An address, not an upload
description: >
The stroke became a 20-byte gesture key — a compressed
encoding of its shape. Similar shapes land on nearby keys;
that proximity is the whole network.
- name: Nothing to undo
description: >
A drawn path is a fact, not an account. There is no profile
behind it unless you left an email.
- name: Draw another
description: >
The network gets more interesting with every shape it hears.
action: /
consequence: [Back to the canvas]
+101
View File
@@ -0,0 +1,101 @@
# The place a drawn (or picked) path leads to: /shape/<gesture key>.
# Reached through the landing alternative's templated action
# (/shape/{curve.key}); the key is the URL. What is kept here comes
# from the relay - the first redoal node - over its /place endpoint,
# and a voice left here goes back the same way (ADR-0015 in redoal).
name: Where does the path lead?
description: >
This is a place: the 20-byte address your path became — or the one
you picked from what the network keeps nearby. Voices left here play
here, for whoever draws their way to this shape. What you see on
this site is a preview of the app: the same network, reached
through a relay instead of your own machine.
alternatives:
- name: Listen to what was left here
description: ""
features:
- name: ""
description: ""
icon: lucide:ear
resource:
source:
kind: url
url: "https://relay.redoal.com/place/{key}"
public: true
empty: Nothing has been left at this address yet — yours could be the first voice here.
jq: >
.recordings[] | {name: ((.duration_ms / 1000 | round | tostring) + " seconds of voice"),
description: ("sealed by " + .author[0:8] + "… · [have it reviewed](/report/" + .digest + ")"),
audio: ("https://relay.redoal.com" + .audio)}
- name: Leave your voice here
description: >
A preview of what the app will give you in full. Record in your
browser, listen back, and press *Keep it here* — nothing leaves
your phone before that. The relay, the network's first node,
keeps it at exactly this address and mirrors it on, sealed with
its own key on your behalf; a key of your own comes with the app.
action: /shape/{voice.key}
consequence: [Keep it here]
record_as: redoal_voices
encouragements:
- It stays where you left it.
features:
- name: ""
description: >
Up to a minute. Record, listen back, record again if you like
— it is sealed as it is once you keep it.
icon: lucide:mic
requirements:
- name: voice
label: Voice
type: voice
relay: wss://relay.redoal.com
value: "{key}"
optional: true
- name: Hear from this address
description: >
The fallback for a phone without the app: one note when
something is left at exactly this shape — and nothing about
anywhere else.
action: /subscribed
consequence: [Watch this address]
record_as: redoal_watchers
encouragements:
- Your shape now has a listener.
features:
- name: Where the note goes
description: >
The note is about one exact address, so it needs one exact
place to land. One mail comes first, asking you to confirm —
nothing is sent before you do, and at most one note a day
after.
icon: lucide:mail
requirements:
- name: email
label: Email
type: email
- name: key
label: Address
type: hidden
value: "{key}"
- name: What just happened
description: ""
features:
- name: An address, not an upload
description: >
The stroke became a 20-byte gesture key — an ordered chain of
headings, drawn back for you under the stroke. Similar shapes
land on nearby keys; picking a kept place makes its key yours.
- name: Nothing to undo
description: >
A drawn path is a fact, not an account. There is no profile
behind it unless you asked to hear from an address.
- name: Draw another
description: >
The network gets more interesting with every shape it hears.
action: /
consequence: [Back to the canvas]
+9
View File
@@ -21,3 +21,12 @@ alternatives:
bucket: redoal_subscribers bucket: redoal_subscribers
to: unsubscribed to: unsubscribed
label: Unsubscribe me label: Unsubscribe me
- name: Stop watching an address
description: >
Using the link from a note about a shape? Confirm below and that
address goes quiet for you.
self_transition:
bucket: redoal_watchers
to: unsubscribed
label: Stop watching
+27
View File
@@ -0,0 +1,27 @@
# Landing page of the confirmation mail: /watching?chain=<record>&email=<address>.
# A followup - never in the nav; the record id in the link is the only
# way here, and it plus the matching email is what authorizes the
# transition (portal's self_transition).
followup: true
name: Are you watching this address?
description: >
Someone asked to hear from a shape on redoal with your address. Say
yes and you get one note when something is left there — nothing
else, ever. Say it wasn't you and the request is dropped.
alternatives:
- name: Yes, watch it
description: >
One note per day at most, only when a recording or a program
lands at exactly this shape.
self_transition:
bucket: redoal_watchers
to: open
label: Yes, watch it
- name: That wasn't me
description: >
The request is dropped and the address stays unused.
self_transition:
bucket: redoal_watchers
to: unsubscribed
label: Drop it
+1
View File
@@ -4,3 +4,4 @@ wordmark: https://project.uhhm.no/redoal/questions/raw/branch/main/wordmark.svg
hero: hero:
kind: module kind: module
module: hero.js # this repo's sine-swings piece, served at /site/hero.js module: hero.js # this repo's sine-swings piece, served at /site/hero.js
wordmark_invert: true