Compare commits

Author SHA1 Message Date
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
13 changed files with 343 additions and 67 deletions
+1 -1
View File
@@ -18,7 +18,7 @@ on:
- .gitea/workflows/deploy.yml - .gitea/workflows/deploy.yml
env: env:
PORTAL_RELEASE: v0.3.6 PORTAL_RELEASE: v0.3.17
INSTANCE: redoal-portal INSTANCE: redoal-portal
jobs: jobs:
+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]
+12 -3
View File
@@ -18,18 +18,27 @@
// //
// prefers-reduced-motion freezes the field fully drawn either way. // prefers-reduced-motion freezes the field fully drawn either way.
import { makePaths, drawPaths } from './paths.js'; import { makePaths, drawPaths, OVERSCAN } from './paths.js';
const STYLE_ID = 'redoal-hero-style'; const STYLE_ID = 'redoal-hero-style';
const CYCLE_MS = 28000; const CYCLE_MS = 28000;
const SEED = 7; const SEED = 7;
const COUNT = 47; const COUNT = 47;
const BLEED = `${((OVERSCAN - 1) / 2) * -100}%`;
const STYLE_TEXT = ` const STYLE_TEXT = `
/* The field bleeds 25% past the hero piece on every side. Sideways
it is clipped at the header box (clip, not hidden: no scroll
container, and overflow-y stays visible) - an element wider than
the viewport, even unscrollable, makes phones re-size the layout
viewport when it appears, which read as the page jumping as the
input loaded. Vertically it may run into the copy below. */
.hero-module { overflow-x: clip; overflow-y: visible; }
.redoal-paths { .redoal-paths {
position: absolute; position: absolute;
inset: 0; inset: ${BLEED};
overflow: hidden; overflow: visible;
pointer-events: none; pointer-events: none;
--redoal-t: 0; --redoal-t: 0;
--redoal-seed: ${SEED}; --redoal-seed: ${SEED};
+6 -1
View File
@@ -8,6 +8,11 @@
// engine likes, and unseeded randomness would reshuffle the picture on // engine likes, and unseeded randomness would reshuffle the picture on
// every repaint. // 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) { export function mulberry32(seed) {
let a = seed >>> 0; let a = seed >>> 0;
return () => { return () => {
@@ -118,7 +123,7 @@ const smooth = (u) => u * u * (3 - 2 * u);
export function drawPaths(ctx, width, height, paths, t, colors) { export function drawPaths(ctx, width, height, paths, t, colors) {
ctx.clearRect(0, 0, width, height); ctx.clearRect(0, 0, width, height);
const s = Math.min(width, height) * 0.9; const s = (Math.min(width, height) / OVERSCAN) * 0.9;
ctx.save(); ctx.save();
ctx.translate(width / 2, height / 2); ctx.translate(width / 2, height / 2);
ctx.lineWidth = 1; ctx.lineWidth = 1;
+21 -13
View File
@@ -1,16 +1,17 @@
# The group exhibition at Podium, connected to the conference: opens # The group exhibition at Podium, part of Attac's «Big Tech må vekk»
# 21 October 2026 and runs for a month. Announced in every header # 2026: opens 23 October 2026 at 19:00 and runs for a month. Announced
# while it runs (portal's event: window); afterwards a followup, and # in every header while it runs (portal's event: window); afterwards a
# a "post what happened" task lands on /review. # followup, and a "post what happened" task lands on /review.
name: Where can you see it in person? name: Where can you see it in person?
description: > description: >
redoal is part of the group exhibition at Podium, opening 21 October redoal is part of the group exhibition at Podium, opening 23 October
and running for a month alongside the conference. Draw on the and running for a month — the art strand of the conference
screens, leave a voice at a shape, hear who came before you. «Big Tech må vekk». Draw on the screens, leave a voice at a shape,
hear who came before you.
event: event:
starts: 2026-10-21T18:00:00+02:00 starts: 2026-10-23T19:00:00+02:00
duration: 31d duration: 31d
place: Podium, Oslo place: Podium, Hausmanns gate 34, Oslo
alternatives: alternatives:
- name: Come draw on the wall - name: Come draw on the wall
description: > description: >
@@ -18,12 +19,19 @@ alternatives:
you can stand in front of together — the network at the scale you can stand in front of together — the network at the scale
of a room. of a room.
features: features:
- name: Opening 21 October - name: Opening 23 October, 19:00
description: > description: >
The exhibition opens on the 21st and stays up for a month, At [Podium](https://podium.enterprises/), Hausmanns gate 34,
within the conference programme. Come to the opening, or and up for a month. Come to the opening, or any Saturday
any day after. or Sunday after.
icon: lucide:calendar-days 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 - name: Draw, then listen
description: > description: >
Draw a shape on the touch screen and arrive at a place. Draw a shape on the touch screen and arrive at a place.
+21 -17
View File
@@ -13,12 +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.
# Announced, not yet open: the network answers with echoes today, action: /shape/{curve.key}
# what follows a shape is still being built. consequence: [Go to the place]
disabled: true
consequence: [Find what follows]
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.
@@ -36,10 +34,6 @@ 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
type: email
optional: true
- name: Meet a network you carry in your hand - name: Meet a network you carry in your hand
description: > description: >
@@ -83,15 +77,25 @@ alternatives:
script arrives on the same terms as everyone else — that is script arrives on the same terms as everyone else — that is
the whole entry requirement. the whole entry requirement.
icon: lucide:hand icon: lucide:hand
- name: Young, and honest about it - name: Carried by iroh, through varde
description: > description: >
A prototype and an artwork before it is a product. The What is left at a place is kept and mirrored by
gesture math, the fingerprints, the voice recordings, and the [varde](https://project.uhhm.no/uhhm/varde), a small
machine-to-machine fetching are real and running; the 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 transport that reaches across the internet is being built
now. The edges are rough, and the edges are where the now, and the rough edges are where the interesting questions
interesting questions live. live.
icon: lucide:sprout icon: lucide:anvil
- name: Follow the build as it lands - name: Follow the build as it lands
description: > description: >
+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
+10
View File
@@ -0,0 +1,10 @@
# Landing after filing a report.
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: /
+64
View File
@@ -38,6 +38,70 @@ alternatives:
- 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 - name: Announced pages
description: > description: >
Pages with an `event:` window, kept here by portal itself. When a Pages with an `event:` window, kept here by portal itself. When a
-32
View File
@@ -1,32 +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: What arrives here next
description: >
In the full network a drawing is a place that holds voices —
recordings left by whoever came before, and live talk with
whoever is standing here now. On this site it holds echoes
today; the voices follow.
- 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