6 Commits
Author SHA1 Message Date
Bendik Aagaard LynghaugandClaude Opus 5 b50e406319 Pin portal and iris v0.4.0
Lint and reload / lint (push) Skipped
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-24 21:46:49 +02:00
Bendik Aagaard LynghaugandClaude Fable 5.1 f5027a2bd4 Desk buttons that say what they do, stages in words, declined only before starting
Lint and reload / lint (push) Skipped
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-24 06:01:57 +02:00
Bendik Aagaard LynghaugandClaude Fable 5.1 d0af3c88d5 The mails, worded: what happened and what you can do, in a few lines
Lint and reload / lint (push) Skipped
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-23 23:02:48 +02:00
Bendik Aagaard LynghaugandClaude Fable 5.1 77b62afaa2 Every stage that matters mails the person who sent it
Lint and reload / lint (push) Skipped
The brief, the trial and a custody case tell the submitter when they
arrive and when they are decided; the sender is portal@uhhm.no with
replies to bl@uhhm.no. Built by portal-trainer from plans/uhhm.yaml
with `mails:` on the needs; pins to portal and iris v0.3.44.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-23 21:19:10 +02:00
Bendik Aagaard LynghaugandClaude Fable 5.1 8b4e6e053b The front door opens modestly, and everything sits on it
Lint and reload / lint (push) Skipped
No doors: the page first says what UHHM does, two cards for the two
roles it exists for (a brief, and the redoal network), each headed by
what that role gets, then what there is to look at (what was built,
the relay's live count), and only then the six offers. The client can
withdraw a brief and the person on the network says when a case is
resolved, so both roles have a move after sending. Rebuilt by
portal-trainer from plans/uhhm.yaml, round 5.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01L4jrCgLiKKHAEFuZUJjckH
2026-09-23 18:15:06 +02:00
Bendik Aagaard LynghaugandClaude Fable 5.1 5b657828e1 uhhm.no rebuilt from a trainer plan: trials, and the redoal custodians
Lint and reload / lint (push) Skipped
Built by portal-trainer from plans/uhhm.yaml (uhhm/portal-trainer,
runs/uhhm-2026-09-23): the five live buckets keep their state and
event names, so every record on the owners' desk stays. New: `trials`,
a business trying the trainer on its own workflow, approved by an
owner before anything is built (the n8n preview workflow comes later,
noted as attended_by); `custody_requests`, what reaches UHHM as the
custodian of the redoal network, on a custodians' desk owners can
watch. Owners invite into both desks. needs.yaml holds the site to
its needs from here on; the lint reads it.

Gone from the live site: the starred-repos card, the proposal editor
with its file picker and /review/[record], and the /applied, /project
and /subscribed followups (now /received/<need>; the unsubscribe move
is /decide/subscribe). The pins move to v0.3.42: `invite:` and
`attended_by` need a portal newer than the v0.3.36 this site runs.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-23 15:09:48 +02:00
30 changed files with 1045 additions and 538 deletions
+1 -1
View File
@@ -18,7 +18,7 @@ on:
- .gitea/workflows/deploy.yml
env:
PORTAL_RELEASE: v0.3.36
PORTAL_RELEASE: v0.4.0
INSTANCE: uhhm-portal
jobs:
+1 -1
View File
@@ -20,7 +20,7 @@ jobs:
# this site runs (PORTAL_RELEASE in deploy.yml).
- name: Lint questions
env:
IRIS_RELEASE: v0.3.39
IRIS_RELEASE: v0.4.0
run: |
# The checkout is on a noexec mount for this runner; the cargo
# target dir is where it may run what it fetched.
+153 -66
View File
@@ -1,68 +1,155 @@
# The full state graph for every bucket that wants event-sourced,
# CAS-protected transitions - see portal's src/aggregates/mod.rs. A
# bucket with no entry here still works (plain KV mutate-in-place, no
# event log). `bucket` doubles as both the KV bucket name and the
# JetStream event-subject segment (events.<bucket>.<id>) - one name,
# not two.
#
# Pages (review.yaml, subscribed.yaml, index.yaml) reference these
# bucket names and `to:` state names by string; they never re-declare
# the graph itself. Add a new aggregate here alone - no portal code
# change needed.
aggregates:
# One continuous graph per person, applicant through employee - the
# bucket name stays "applicants" even as a record progresses past the
# applying stage (a deliberate trade: one chain of events per person,
# no identity-linking between separate buckets).
- bucket: applicants
initial: open
states:
open: { event: applied }
invited: { event: invited }
declined: { event: declined }
active: { event: onboarded }
departed: { event: departed }
transitions:
open: [invited, declined]
invited: [active, declined]
active: [departed]
- bucket: applicants
initial: open
states:
active:
event: onboarded
declined:
event: declined
departed:
event: departed
invited:
event: invited
open:
event: applied
transitions:
active:
- departed
invited:
- active
- declined
open:
- invited
- declined
- bucket: custody_requests
initial: open
states:
declined:
event: declined
mail:
body: We've read your message and decided this isn't something we can take on as custodians. If you think we've missed something, write back and tell us.
subject: On your message to the custodians
to: submitter
in_dialogue:
event: in_dialogue
open:
event: received
mail:
body: Your message reached the custodians of the first node. We read everything that comes in - expect a real reply, not an autoresponder.
subject: We have your message
to: submitter
resolved:
event: resolved
mail:
body: This is resolved. Here's what we found and what we did about it - write back if it isn't settled from where you sit.
subject: Your case is resolved
to: submitter
transitions:
in_dialogue:
- resolved
- declined
open:
- in_dialogue
- declined
- bucket: development_proposals
initial: open
states:
approved:
event: approved
open:
event: proposed
rejected:
event: rejected
transitions:
open:
- approved
- rejected
- bucket: organizations
initial: prospect
states:
client:
event: became_client
past_client:
event: churned
prospect:
event: identified
transitions:
client:
- past_client
prospect:
- client
- past_client
- bucket: projects
initial: open
states:
accepted:
event: accepted
mail:
body: We want to take this on. Next we'll be in touch to talk through scope, timing and how we'll work together.
subject: We'd like to take this on
to: submitter
declined:
event: declined
mail:
body: We've read your brief and this isn't one we're taking on. It isn't a judgment on the problem - just not a fit for us right now.
subject: On your brief
to: submitter
open:
event: submitted
mail:
body: |-
Your brief is in front of us. We read the whole thing before we reply, so it may take a few days - you'll hear from us either way.
- bucket: subscribers
initial: open
states:
open: { event: subscribed }
unsubscribed: { event: unsubscribed }
transitions:
open: [unsubscribed]
- bucket: projects
initial: open
states:
open: { event: submitted }
accepted: { event: accepted }
declined: { event: declined }
transitions:
open: [accepted, declined]
- bucket: organizations
initial: prospect
states:
prospect: { event: identified }
client: { event: became_client }
past_client: { event: churned }
transitions:
prospect: [client, past_client]
client: [past_client]
# Proposals to change this very repo - the question architecture
# dogfooding its own development. Approval hands off to an n8n
# automation that opens a PR against this repo (see the infrastructure
# repo's development-commit workflow); the lint CI gates the merge.
- bucket: development_proposals
initial: open
states:
open: { event: proposed }
approved: { event: approved }
rejected: { event: rejected }
transitions:
open: [approved, rejected]
What you can do next: {site}/decide/commission?chain={chain}&email={email}
subject: We have your brief
to: submitter
transitions:
open:
- accepted
- declined
- bucket: subscribers
initial: open
states:
open:
event: subscribed
unsubscribed:
event: unsubscribed
transitions:
open:
- unsubscribed
- attended_by: |
approved -> previewed: the n8n workflow that builds the draft site from an approved intake (docker, rauthy, portal content) and sends the preview link. Until it exists, owners run portal-trainer by hand and press the button.
bucket: trials
initial: open
states:
approved:
event: approved
mail:
body: Your draft is approved and on its way. We built it from your own words - have a look when it's ready and tell us what to change.
subject: Your draft is approved
to: submitter
declined:
event: declined
mail:
body: We've decided not to build this draft. If the situation changes, get back in touch and we'll take another look.
subject: On your draft
to: submitter
open:
event: received
mail:
body: We have what you sent. A person will read it and build your draft, or tell you why not - you'll hear from us either way.
subject: We're looking at your draft
to: submitter
previewed:
event: previewed
mail:
body: Your draft is ready to look at. It's built from your own words - the link below takes you straight to it.
subject: Your draft is ready to view
to: submitter
transitions:
approved:
- previewed
- declined
open:
- approved
- declined
+204
View File
@@ -0,0 +1,204 @@
needs:
- done_when:
- accepted
- declined
enters: /
handled_by: owners
id: commission
lands_in: projects
max_fields: 4
max_steps: 1
stage_words:
accepted: taken on
declined: not a fit for us
open: received
stages:
- open
- accepted
- declined
success:
- accepted
via: 'Have us build it: send the brief'
wants: a considered reply to a brief, from people who read the whole thing
who: an organization with a problem it wants software to solve
- done_when:
- previewed
- declined
enters: /
handled_by: owners
id: trial
lands_in: trials
max_fields: 6
max_steps: 1
stage_words:
approved: approved for a draft
declined: not a fit for us
open: received
previewed: a preview is ready
stages:
- open
- approved
- previewed
- declined
success:
- previewed
via: See your own business as a site, before you buy anything
wants: a first draft of a site for their own roles, built from their own words and approved by a person first
who: someone who runs a business where requests, approvals and handovers get lost, and wants to see it as a site before committing to anything
- done_when:
- departed
- declined
enters: /
handled_by: owners
id: collaborate
lands_in: applicants
max_fields: 3
max_steps: 1
stage_words:
active: working with us
declined: not a fit for us
departed: moved on
invited: invited in
open: received
stages:
- open
- invited
- active
- departed
- declined
success:
- departed
via: Work with us
wants: to be read as a person, and invited in if there is a fit
who: someone who wants to work with us on what we build
- done_when:
- unsubscribed
enters: /
handled_by: owners
id: subscribe
lands_in: subscribers
max_fields: 2
max_steps: 1
stage_words:
open: on the list
unsubscribed: off the list
stages:
- open
- unsubscribed
success: []
via: Hear from us now and then
wants: a note now and then, and an easy way out
who: someone who wants to hear from us when there is something to say
- done_when:
- approved
- rejected
enters: /
handled_by: owners
id: propose
lands_in: development_proposals
max_fields: 3
max_steps: 1
stage_words:
approved: approved
open: received
rejected: turned down
stages:
- open
- approved
- rejected
success:
- approved
via: Suggest a change to this website
wants: the change read by a person and, if approved, made
who: a visitor who sees how a page on this site should change
- done_when:
- resolved
- declined
enters: /
handled_by: custodians
id: custody
lands_in: custody_requests
max_fields: 4
max_steps: 1
stage_words:
declined: closed without action
in_dialogue: in dialogue with a custodian
open: received
resolved: resolved
stages:
- open
- in_dialogue
- resolved
- declined
success:
- resolved
via: 'You are on the redoal network: a node, an app, or a recording that concerns you'
wants: an answer from a custodian, and action where the network keeps something it should not
who: someone who runs, builds on, or is affected by the redoal network and needs the people who keep its first node
- done_when:
- past_client
enters: /owners/prospect
enters_as: owners
handled_by: owners
id: prospect
lands_in: organizations
max_fields: 2
max_steps: 1
stage_words:
client: a client
past_client: a past client
prospect: a prospect
stages:
- prospect
- client
- past_client
success: []
via: Keep an organization on your radar
wants: it written down and walked through its states from the desk
who: an owner who has met an organization worth following up
personas:
- id: architecture-firm
need: commission
text: We run a small architecture firm and our project files are scattered across three cloud drives. We want to pay someone to set up proper storage and backups we control ourselves.
- id: climbing-gym-founder
need: commission
text: I have an idea for a booking app for climbing gyms and I'm looking for a team that can build the first version with me.
- id: municipality-buyer
need: commission
text: Our municipality needs an internal tool for tracking building permits. We are looking for a vendor who can scope it and deliver it.
- id: exhibition-visitor
need: subscribe
text: I saw your drawing network at an exhibition and liked it. I don't want to get involved, I'd just like to know when there is news.
- id: tech-journalist
need: subscribe
text: I write about Nordic technology. Keep me posted on what you release so I can cover it.
- id: rust-developer
need: collaborate
text: I'm a Rust developer between jobs and I like that you build in the open. I would love to contribute, and maybe join you.
- id: interaction-designer
need: collaborate
text: I'm an interaction designer. I want to work with you on the drawing interface.
- id: student-intern
need: collaborate
text: I'm a computer science student looking for a summer internship doing infrastructure work.
- id: plumber-owner
need: trial
text: I run a plumbing company with four vans. Jobs come in by phone and text and some get lost. I'd like to see how this would look for us before I spend any money.
- id: dance-school
need: trial
text: We are a dance school. Enrolments, cancellations and invoices all go through one email inbox that only I understand. Can you show me a version made for us?
- id: node-runner
need: custody
text: I run a small server in Berlin and want to host a redoal node that peers with yours. Who do I talk to?
- id: recorded-name
need: custody
text: Someone left a recording on redoal that says my full name and where I live. I want it gone, and I want to know who is responsible for that network.
- id: app-builder
need: custody
text: I'm building a music app and would like to use redoal's gesture addresses in it. Is that possible, and who decides?
- id: wording-fix
need: propose
text: The subscribe page should say how often you actually write. I'd like to suggest that exact change to the page.
- id: rewrite
need: propose
text: One page on this site reads wrong to me and I know exactly how I'd rewrite it. Where do I send the new text?
-25
View File
@@ -1,25 +0,0 @@
followup: true
name: What happens now that you've applied?
description: >
We're not filling a seat — we're looking for exactly what you bring
that we don't already have. We read every one of these ourselves, in
full, and write back.
alternatives:
- name: Thanks — it's in front of us
description: ""
features:
- name: We need your specific angle
description: >
Not a generic fit — the particular thing you're good at, the
problem you'd actually notice, the take nobody else here
has. That's what we're reading for.
- name: Work happens through dialogue, not tickets
description: >
What you bring shapes what gets built, in an ongoing back
-and-forth — not a queue of pre-decided tasks handed down to
execute.
- name: You receive a personal invitation
description: >
A match earns you a real key — access, a measure of
responsibility, and eventually, the standing to invite
someone else in.
+1
View File
@@ -0,0 +1 @@
qualifies: custodians
+65
View File
@@ -0,0 +1,65 @@
alternatives:
- action: /custodians
consequence:
- Confirm
description: Cases from people who use, build on, or answer for something the redoal network touches, and need a custodian's word on it.
features:
- description: ''
name: ''
resource:
requires_group: custodians
source:
bucket: custody_requests
kind: kv
transitions:
- from: in_dialogue
label: Mark resolved
to: resolved
- from: in_dialogue
label: Close, no action
to: declined
- from: open
label: Open dialogue
to: in_dialogue
- from: open
label: Decline case
to: declined
name: Custody cases waiting on you
- action: /custodians
consequence:
- Invite custodian
description: Give someone an account and a one-time link to join as a custodian.
features:
- description: ''
name: Invite a custodian
requirements:
- label: Name
name: name
- label: Email
name: email
type: email
invite:
group: custodians
name: Bring in another custodian
- action: /custodians
consequence:
- Confirm
description: Custodians invited but not yet signed in.
features:
- description: ''
name: ''
resource:
requires_group: custodians
source:
bucket: portal_invites
kind: kv
transitions:
- from: open
label: Signed in
to: done
name: Invited custodians
description: 'The desk for custodians: open custody cases, and the people invited to answer them.'
name: What is waiting on the custodians?
responsible:
contact: bl@uhhm.no
name: Bendik Aagaard Lynghaug
+9
View File
@@ -0,0 +1,9 @@
alternatives:
- description: ''
name: Declined
self_transition:
bucket: projects
label: Declined
to: declined
description: ''
name: What would you like to do?
+9
View File
@@ -0,0 +1,9 @@
alternatives:
- description: ''
name: Resolved
self_transition:
bucket: custody_requests
label: Resolved
to: resolved
description: ''
name: What would you like to do?
+9
View File
@@ -0,0 +1,9 @@
alternatives:
- description: You'll stop hearing from us. Nothing else changes.
name: Stop getting notes from us
self_transition:
bucket: subscribers
label: Unsubscribe
to: unsubscribed
description: You're on our list. Change your mind whenever you like.
name: Do you want to keep hearing from us?
-24
View File
@@ -1,24 +0,0 @@
name: What's been proposed?
qualifies: owners
description: >
Proposed changes to the questions repo, awaiting a decision.
Approval hands off to the automation that opens the pull request —
lint gates the merge, not this page.
alternatives:
- name: Proposals
description: ""
action: /develop
consequence: [Confirm]
features:
- name: ""
description: ""
resource:
source:
kind: kv
bucket: development_proposals
requires_group: owners
transitions:
- to: approved
label: Approve
- to: rejected
label: Reject
-90
View File
@@ -1,90 +0,0 @@
# Nested for the flow, but a public entry point - without this it
# would infer followup and drop out of the nav.
followup: false
name: How can this website improve?
description: >
This site is built on the same question architecture you're using
right now — and changes to it are proposed through it. Tell us what
should change and why; an owner reads every proposal, and approved
ones become real commits.
alternatives:
- name: The change you propose
description: >
The proposed content replaces (or creates) one content file —
a page under questions/, or aggregates.yaml — verbatim, so it
has to be complete, valid YAML for that file, not a diff.
Nothing outside those files is in scope.
action: proposed
# Announced, not yet open: the automation that turns a proposal
# into a pull request is still being built.
disabled: true
consequence: [Propose it]
record_as: development_proposals
encouragements:
- Every proposal gets read by a person before anything moves.
features:
- name: The reference
icon: lucide:book-open
description: >
Every page this site serves, verbatim — check what exists
before proposing against it.
resource:
public: true
source:
kind: url
url: https://project.uhhm.no/api/v1/repos/uhhm/questions
jq: >
{name: .full_name, description: .description, url: .html_url}
- name: Sign it
icon: lucide:pen-line
description: >
A proposal is a stance — it carries a name, and an address
if you want the outcome to reach you.
requirements:
- name: submitted_by
label: Name (or what to call you)
- name: email
label: Email
type: email
optional: true
- name: The change
icon: lucide:git-branch
description: >
Pick the page you're changing and its current content loads
below, ready to edit in place — or name a new file's path if
the proposal creates one.
requirements:
- name: target_path
label: The file to change
type: select
optional: true
id_field: path
resource:
public: true
source:
kind: url
# The whole tree, not one directory: since the tree became
# the router, pages live in subdirectories too.
url: https://project.uhhm.no/api/v1/repos/uhhm/questions/git/trees/main?recursive=true
jq: >
.tree[] | select(.type == "blob" and (.path | startswith("questions/")) and (.path | endswith(".yaml")))
| {path: .path, name: .path}
- name: new_path
label: Or a new file's path
placeholder: questions/example.yaml
optional: true
- name: rationale
label: The rationale
type: textarea
- name: proposed_yaml
label: Proposed file content (complete YAML)
type: textarea
bind:
field: target_path
param: path
resource:
public: true
source:
kind: url
url: https://project.uhhm.no/api/v1/repos/uhhm/questions/contents/{path}?ref=main
jq: ".content | @base64d"
-18
View File
@@ -1,18 +0,0 @@
# Reached from the proposal form only - and provably so: the visitor
# must hold a ?chain= lineage ending at an answer to proposal.yaml.
requires_chain: proposal
name: What happens to your proposal?
description: >
It's in the queue. An owner reads every proposal in full — approved
ones open a real pull request, and the same checks every human
commit passes gate the merge.
alternatives:
- name: The path from here
description: ""
features:
- name: A person decides
description: Nothing merges on autopilot — approval is a deliberate act.
- name: The linter has no favorites
description: >
Approved or not, your YAML faces the same validation as
everything else in the repo.
+269 -112
View File
@@ -1,113 +1,270 @@
name: What are you here for?
description: >
Strategies that address problems — applied in software.
responsible:
name: Bendik Aagaard Lynghaug
contact: bl@uhhm.no
alternatives:
- name: Good software begins with a clear argument
description: >
Infrastructure, tools, or a product — bring the problem, and we
read the whole brief before answering.
action: /project
consequence: [Tell us what you're after]
record_as: projects
encouragements:
- We read every word before breaking ground.
features:
- name: What we've built
icon: lucide:layers
description: A few things already out in the world.
resource:
source:
kind: gitea_starred
username: bl
public: true
# The link prefers the repo's Website setting so private
# repos can point at something the public can actually
# open; a private repo with no website gets no link at all
# (the card renders an unlinked heading) instead of a 404.
# TODO: star counts (Gitea's .stars_count) are left out until
# they're worth showing - add `stars: .stars_count` back here.
jq: >
.[] | {name: .name, description: .description,
url: (if .website != null and .website != "" then .website
elif .private then null
else .html_url end)}
- name: Your brief
icon: lucide:file-text
description: >
A few lines are enough to begin a conversation — we need a
name and an address because the reply is personal, and the
organization because software always lands somewhere.
requirements:
- name: name
label: Name
- name: email
label: Email
type: email
- name: project
label: The build
type: textarea
placeholder: Just the shape of it — we'll ask the right questions.
- name: organization
label: Organization
- name: org_contact
label: Their contact, if not you
optional: true
- name: Attention is earned
description: >
Considered notes on what we're building, and why — sent only when
there's something worth saying.
action: /subscribed
consequence: [Subscribe]
record_as: subscribers
encouragements:
- Consider yourself posted.
features:
- name: Where to send it
icon: lucide:mail
description: >
Notes go to a person, not a list — a name and an address is
all it takes.
requirements:
- name: name
label: Name
- name: email
label: Email
type: email
- name: Work happens through dialogue, not tickets
description: >
What you bring shapes what gets built. We onboard collaborators
the same deliberate way we build — by hand, through a real
conversation.
action: /applied
consequence: [Apply]
record_as: applicants
encouragements:
- Knock — we actually answer.
features:
- name: A way to reach you
icon: lucide:at-sign
description: The reply is personal — it needs a name and an address.
requirements:
- name: name
label: Name
- name: email
label: Email
type: email
- name: Your angle
icon: lucide:compass
description: >
We build around what each person actually brings — say what
that is, in your own words.
requirements:
- name: interest
label: The work you're drawn to
type: textarea
- name: note
label: Anything else worth knowing
type: textarea
optional: true
- description: Two things, both built here and both running.
features:
- description: 'UHHM builds and carries the redoal network: the gesture math, the relay, the first node, and the portal this page runs on. Run a node, build on it, or have a recording removed, and you say when it is resolved.'
name: A network that answers to you
- description: 'Infrastructure, tools and products for organizations that can say what problem they have. Bring the brief, or first see your own way of working as a working site, before you pay for anything. The brief stays yours: you can withdraw it yourself.'
icon: lucide:layers
name: A considered reply to your brief, and software that fits the argument
name: What UHHM does
- description: Nothing to fill in here. What is already out in the world, and the network as it runs right now.
features:
- description: A few things already out in the world.
icon: lucide:hammer
name: What we have built
resource:
jq: |
.[] | {name: .name, description: .description, url: (if .website != null and .website != "" then .website elif .private then null else .html_url end)}
public: true
source:
kind: gitea_starred
username: bl
- description: Straight from the relay, the first node of the network.
icon: lucide:activity
name: The redoal network, live
resource:
empty: The first voice has not echoed yet.
jq: |
{ "name": (.voices_echoed | tostring),
"description": "voices echoed so far" }
public: true
source:
kind: url
url: https://relay.redoal.com/pulse
name: Have a look around first
- action: /received/commission
consequence:
- Send the brief
description: You know the problem and want it solved, whether it is infrastructure, a tool, or the first version of a product. Write it up for people who read the whole thing.
encouragements:
- Someone here reads every brief in full before writing back.
features:
- description: We read your brief and either take it on or tell you honestly if it's not a fit.
icon: lucide:route
name: What happens next
- description: One of the owners reads it; write to bl@uhhm.no if you'd rather reach us directly.
icon: lucide:user-check
name: Who reads this
- description: The more of the problem you put in, the better the reply.
icon: lucide:briefcase
name: Tell us the problem
requirements:
- label: Your name
name: name
placeholder: Sara Nilsen
type: text
- label: Your email
name: email
placeholder: sara@company.com
type: email
- label: The problem you want solved
name: project
placeholder: Our drivers log hours on paper and it takes a day to get them into payroll.
type: textarea
- label: Your organization
name: organization
placeholder: Nilsen Transport AS
type: text
- label: Who we should talk to
name: org_contact
optional: true
placeholder: Sara Nilsen, ops manager
type: text
name: 'Have us build it: send the brief'
record_as: projects
- action: /received/trial
consequence:
- Start the draft
description: Describe how requests, approvals and handovers move through your business today. We approve it by hand and build a working draft of a site for your own roles, for you to click through.
encouragements:
- A person looks at what you send before anything gets built.
features:
- description: We look at what you sent and, if it's a fit, build a draft from your own roles and words for you to see before anything moves further.
icon: lucide:route
name: What happens next
- description: An owner looks at what you send; write to bl@uhhm.no if you'd rather reach us directly.
icon: lucide:user-check
name: Who reads this
- description: Answer in your own words - there's no wrong way to describe your business.
icon: lucide:layers
name: Describe your business
requirements:
- label: Your business
name: business
placeholder: Nilsen Transport AS
type: text
- label: Your email
name: email
placeholder: sara@company.com
type: email
- label: What you do
name: what_we_do
placeholder: We move freight for local businesses and handle the paperwork for them.
type: textarea
- label: Who comes to you
name: who_comes_to_us
placeholder: Local businesses that need something moved on short notice.
type: textarea
- label: What you keep track of
name: what_we_keep_track_of
placeholder: Bookings, driver hours, and which loads are still waiting on a signature.
type: textarea
- label: Who's involved
name: who_is_involved
placeholder: Drivers, dispatchers, and the customers waiting on their delivery.
type: textarea
- label: What slows you down
name: what_slows_us_down
optional: true
placeholder: Approvals sit in someone's inbox for days before anyone acts on them.
type: textarea
name: See your own business as a site, before you buy anything
record_as: trials
- action: /received/collaborate
consequence:
- Get in touch
description: 'A developer, a designer, a student: say what you would want to build with us, and we read it as a person and invite you in if there is a fit.'
encouragements:
- A person reads this, not a form filter — worth the ten minutes.
features:
- description: We read what you send and get back to you. If there's a fit, we invite you in and you start working with us — and if things ever change, that's fine too.
icon: lucide:route
name: What happens next
- description: One of the owners reads this directly — for anything urgent, write to bl@uhhm.no.
icon: lucide:user-check
name: Who reads this
- description: Tell us who you are and what kind of work you're after.
icon: lucide:handshake
name: Say hello
requirements:
- label: Name
name: name
placeholder: Your name
type: text
- label: Email
name: email
placeholder: you@example.com
type: email
- label: What kind of work
name: interest
placeholder: Backend work, or something design-heavy
type: textarea
- label: Tell us more
name: note
optional: true
placeholder: A bit about what you'd want to work on, or a link to your work
type: textarea
name: Work with us
record_as: applicants
- action: /received/subscribe
consequence:
- Sign up
description: You'd rather hear from us now and then than check back on your own.
encouragements:
- One click unsubscribes you later, no questions asked.
features:
- description: You're on the list. We write when there's something worth saying, and you can leave the list whenever you like.
icon: lucide:bell
name: What happens next
- description: An owner sends these notes directly — write to bl@uhhm.no if you'd rather ask something first.
icon: lucide:user-check
name: Who reads this
- description: Just your email, or your name too if you'd like.
icon: lucide:mail
name: Stay in the loop
requirements:
- label: Name
name: name
placeholder: Your name
type: text
- label: Email
name: email
placeholder: you@example.com
type: email
name: Hear from us now and then
record_as: subscribers
- action: /received/propose
consequence:
- Send change
description: You have spotted a page on uhhm.no that should read differently, and you know roughly how.
encouragements:
- Even a rough version helps — we can tidy the wording ourselves.
features:
- description: Someone here reads your proposal and either makes the change or explains why not. Either way, you'll hear back.
icon: lucide:route
name: What happens next
- description: An owner reads every proposal before anything changes — reach us at bl@uhhm.no if it's time-sensitive.
icon: lucide:user-check
name: Who reads this
- description: Point us to the page and show us the change.
icon: lucide:pencil
name: Propose a change
requirements:
- label: Your name
name: submitted_by
placeholder: Your name
type: text
- label: Email
name: email
optional: true
placeholder: you@example.com
type: email
- label: Which page
name: page
optional: true
placeholder: questions/join/index.yaml
type: text
- label: Why this is better
name: rationale
placeholder: The current heading doesn't say what you actually get
type: textarea
- label: Your proposed text
name: proposed_yaml
placeholder: 'heading: "A clearer way to say this"'
type: textarea
name: Suggest a change to this website
record_as: development_proposals
- action: /received/custody
consequence:
- Send to a custodian
description: UHHM hosts the redoal network's first node and answers for what it keeps. Write here if you run a node, build on the network, or want something it holds about you looked at.
encouragements:
- A custodian reads every message that comes in here.
features:
- description: A custodian opens a dialogue with you. A node gets its peer, a builder gets an answer, and something the network should not keep gets removed. Either way you hear what was decided.
icon: lucide:route
name: What happens next
- description: A custodian reads it directly; if it's urgent, write to bl@uhhm.no in the meantime.
icon: lucide:user-check
name: Who reads this
- description: Say which of the four it is, and what you need, in a few lines.
icon: lucide:shield
name: Reach a custodian
requirements:
- label: Name
name: name
placeholder: Your name
type: text
- label: Email
name: email
placeholder: you@yourcompany.com
type: email
- label: What's going on
name: about
options:
- Running a node of my own
- Building on the network
- Something the network keeps about me
- Something else
type: select
- label: In short
name: what
placeholder: A relay node is misbehaving
type: textarea
name: 'You are on the redoal network: a node, an app, or a recording that concerns you'
record_as: custody_requests
description: UHHM builds software for organisations that can say what problem they have, and keeps the redoal network underneath.
name: What is UHHM, and what can you get here?
responsible:
contact: bl@uhhm.no
name: Bendik Aagaard Lynghaug
+1
View File
@@ -0,0 +1 @@
qualifies: owners
+209
View File
@@ -0,0 +1,209 @@
alternatives:
- action: /owners
consequence:
- Confirm
description: People who've asked to build with us, waiting on a decision or already working alongside us.
features:
- description: ''
name: ''
resource:
requires_group: owners
source:
bucket: applicants
kind: kv
transitions:
- from: active
label: Mark departed
to: departed
- from: invited
label: Confirm active
to: active
- from: invited
label: Withdraw the invite
to: declined
- from: open
label: Send invite
to: invited
- from: open
label: Decline applicant
to: declined
name: People who want to build with us
- action: /owners
consequence:
- Confirm
description: Questions and issues raised with the people who keep the first node - custodians handle these, but you can see where each one stands.
features:
- description: ''
name: ''
resource:
requires_group: owners
source:
bucket: custody_requests
kind: kv
transitions: []
name: What's been raised with custodians
- action: /owners
consequence:
- Confirm
description: Changes visitors have proposed for pages on this site, waiting on a yes or no.
features:
- description: ''
name: ''
resource:
requires_group: owners
source:
bucket: development_proposals
kind: kv
transitions:
- from: open
label: Approve change
to: approved
- from: open
label: Reject change
to: rejected
name: Proposed changes to the site
- action: /owners
consequence:
- Confirm
description: Organizations you've met, tracked from first contact through to client and beyond.
features:
- description: ''
name: ''
resource:
requires_group: owners
source:
bucket: organizations
kind: kv
transitions:
- from: client
label: Mark past client
to: past_client
- from: prospect
label: Mark as client
to: client
- from: prospect
label: Archive as past client
to: past_client
name: Organizations you're tracking
- action: /owners
consequence:
- Confirm
description: Briefs sent in by organizations with a problem they want solved.
features:
- description: ''
name: ''
resource:
requires_group: owners
source:
bucket: projects
kind: kv
transitions:
- from: open
label: Accept brief
to: accepted
- from: open
label: Decline brief
to: declined
name: Briefs waiting on a reply
- action: /owners
consequence:
- Confirm
description: People who want to hear from you now and then.
features:
- description: ''
name: ''
resource:
requires_group: owners
source:
bucket: subscribers
kind: kv
transitions:
- from: open
label: Unsubscribe them
to: unsubscribed
name: Who's hearing from you
- action: /owners
consequence:
- Confirm
description: Businesses who want to see their own workflow as a working site before committing to anything.
features:
- description: ''
name: ''
resource:
requires_group: owners
source:
bucket: trials
kind: kv
transitions:
- from: approved
label: Mark previewed
to: previewed
- from: approved
label: Decline trial
to: declined
- from: open
label: Approve trial
to: approved
- from: open
label: Decline trial
to: declined
name: Trial sites in progress
- action: /owners/prospect
consequence:
- Add prospect
description: Write down an organization worth following up, and track it from first contact onward.
name: Track a new organization
- action: /owners
consequence:
- Send invite
description: Bring someone in to help keep the first node and answer for the network.
features:
- description: ''
name: Invite a custodian
requirements:
- label: Their name
name: name
- label: Their email
name: email
type: email
invite:
group: custodians
name: Bring in a custodian
- action: /owners
consequence:
- Send invite
description: Bring someone in to work this desk alongside you.
features:
- description: ''
name: Invite an owner
requirements:
- label: Their name
name: name
- label: Their email
name: email
type: email
invite:
group: owners
name: Bring in another owner
- action: /owners
consequence:
- Confirm
description: People you've invited in, and whether they've taken it up yet.
features:
- description: ''
name: ''
resource:
requires_group: owners
source:
bucket: portal_invites
kind: kv
transitions:
- from: open
label: Taken up
to: done
name: Invites you've sent
description: 'Everything waiting on you: briefs, trials, applicants, subscribers, and proposals, all in one place.'
name: What's waiting on me today?
responsible:
contact: bl@uhhm.no
name: Bendik Aagaard Lynghaug
+10
View File
@@ -0,0 +1,10 @@
alternatives:
- description: An owner walks it from prospect to client, and on to past client, as the relationship moves.
name: It's on the desk now
- action: /owners
consequence:
- Back to site
description: ''
name: Back to site
description: Thanks — we've got your prospect written down, and it now moves through the desk with everything else we're tracking.
name: What happens to this now?
+39
View File
@@ -0,0 +1,39 @@
alternatives:
- action: /owners/prospect-sent
consequence:
- Add to prospects
description: One line per organization worth watching, moved through the stages as things develop.
encouragements:
- Takes a minute, and it's yours to update later.
features:
- description: It sits as a prospect until something moves - then you mark it client, and later past client if the work ends.
icon: lucide:route
name: Moves as you update it
- description: Only you see this, so keep it wherever you check next.
icon: lucide:user
name: Just for you
- description: Name the organization and where things stand.
icon: lucide:briefcase
name: New prospect
requirements:
- label: Organization
name: name
placeholder: Nordvik Bakery
type: text
- label: Contact
name: contact
placeholder: met at the bakers' conference, [email protected]
type: text
- label: Note
name: note
optional: true
placeholder: wants a booking flow, follow up after harvest season
type: textarea
name: Keep an organization on your radar
record_as: organizations
description: Write down an organization worth following up, and move it through client and past client as things develop.
followup: false
name: Is there an organization I should be keeping track of?
responsible:
contact: bl@uhhm.no
name: Bendik Aagaard Lynghaug
-15
View File
@@ -1,15 +0,0 @@
followup: true
name: So, what happens now?
description: >
That's on its way to a real inbox. Expect a considered reply within a
couple of days.
alternatives:
- name: In short
description: ""
features:
- name: We read it
description: In full, before we write back.
- name: We ask questions
description: >
Usually more than you'd expect. Good software begins with a
clear argument for what it's actually for.
+10
View File
@@ -0,0 +1,10 @@
alternatives:
- description: If it looks like a fit, we'll reach out to talk about where you could help.
name: We'll write if there's a fit
- action: /
consequence:
- Back to site
description: ''
name: Back to site
description: Your note is in. An owner reads every one and will get back to you if there's a fit worth exploring.
name: What happens to what I just sent?
+10
View File
@@ -0,0 +1,10 @@
alternatives:
- description: Expect a reply within a few working days, either way, straight to your inbox.
name: A real reply from someone who read it
- action: /
consequence:
- Back to home
description: ''
name: Back to home
description: Your brief is in. An owner reads every commission end to end before replying.
name: What happens now that we've sent our brief?
+10
View File
@@ -0,0 +1,10 @@
alternatives:
- description: Expect a reply once your report has been looked at and the network checked.
name: A custodian is on it
- action: /
consequence:
- Back to the site
description: ''
name: Back to the site
description: A custodian reads every message about the redoal network and what it keeps.
name: Who looks after what the network keeps?
+10
View File
@@ -0,0 +1,10 @@
alternatives:
- description: We'll write back once it's been read — approved and made live, or turned down with a reason.
name: A read, then a decision
- action: /
consequence:
- Back home
description: ''
name: Back home
description: Your proposal is in. Someone here will read it and decide whether it fits before anything changes on the page.
name: What happens to my proposal now?
+10
View File
@@ -0,0 +1,10 @@
alternatives:
- description: You're on the list. Every note has an unsubscribe link if it stops being useful.
name: You're on the list
- action: /
consequence:
- Back to site
description: ''
name: Back to site
description: You'll hear from us when there's something worth saying, not on a schedule.
name: What happens now that I've subscribed?
+10
View File
@@ -0,0 +1,10 @@
alternatives:
- description: If it looks like a fit, we'll draft your site from your own words and send it back for your say before anything goes live.
name: A first draft, built from your own words
- action: /
consequence:
- Back to site
description: ''
name: Back to site
description: We read every trial request ourselves before anything gets built.
name: What happens now that you've sent this?
-20
View File
@@ -1,20 +0,0 @@
# Dynamic page: /review/<chain-hash> shows one development proposal,
# addressed by its record key. The {record} placeholder receives the
# URL segment; the section's qualifies (plus the resource's own
# requires_group) keeps it owner-only.
name: Which proposal is this?
description: >
A single development proposal, addressed by its record key — link or
paste a chain hash from the desk.
alternatives:
- name: Record
description: ""
features:
- name: ""
description: ""
resource:
source:
kind: kv
bucket: development_proposals
key: "{record}"
requires_group: owners
-3
View File
@@ -1,3 +0,0 @@
# Everything under /review is the owners' area - one gate for the
# whole directory instead of a qualifies flag per file.
qualifies: owners
-141
View File
@@ -1,141 +0,0 @@
name: What came in?
description: >
Sign in as an organizational owner to see what's come in.
alternatives:
- name: Applicants
description: ""
action: /review
consequence: [Confirm]
features:
- name: ""
description: ""
resource:
source:
kind: kv
bucket: applicants
requires_group: owners
transitions:
- to: invited
label: Invite
- to: declined
label: Decline
- from: invited
to: active
label: Onboard
- from: invited
to: declined
label: Decline
- from: active
to: departed
label: Depart
- name: Organizations
description: >
Client relationships as tracked state — advanced as the
relationship becomes real.
action: /review
consequence: [Confirm]
features:
- name: ""
description: ""
resource:
source:
kind: kv
bucket: organizations
requires_group: owners
transitions:
- from: prospect
to: client
label: Becomes client
- from: prospect
to: past_client
label: Archive
- from: client
to: past_client
label: Churn
- name: Record a prospect
description: >
A relationship worth tracking before it's a project — enter it
here and walk it through its states from the desk.
action: /review
consequence: [Record]
record_as: organizations
encouragements:
- Written down is halfway to followed up.
features:
- name: ""
description: The organization and one person there to reach.
requirements:
- name: name
label: Organization
- name: contact
label: Contact
- name: note
label: Anything worth remembering
type: textarea
optional: true
- name: Projects
description: ""
action: /review
consequence: [Confirm]
features:
- name: ""
description: ""
resource:
source:
kind: kv
bucket: projects
requires_group: owners
transitions:
- to: accepted
label: Accept
- to: declined
label: Decline
- name: Subscribers
description: ""
action: /review
consequence: [Send]
features:
- name: ""
description: ""
resource:
source:
kind: kv
bucket: subscribers
requires_group: owners
- name: Compose newsletter
description: >
Sends to everyone above - picked up by an automation watching
this question, not portal itself.
requirements:
- name: subject
label: Subject
- name: body
label: Body
type: prosekit
- 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
-22
View File
@@ -1,22 +0,0 @@
followup: true
name: What should you expect?
description: >
We'll write when there's something worth your time — that's the
whole promise.
alternatives:
- name: You're on the list
description: ""
features:
- name: Rare, by design
description: A few considered notes a year — nothing more.
- name: Easy to leave
description: Every note carries a genuine, one-click unsubscribe.
- name: Unsubscribe
description: >
Using the link from one of our emails? Confirm below and we'll
stop writing.
self_transition:
bucket: subscribers
to: unsubscribed
label: Unsubscribe me
+5
View File
@@ -4,3 +4,8 @@
hero:
kind: module
module: hero.js
# Who mail from this site comes from (portal 0.3.43+, delivered by gdo).
mail:
from: UHHM <portal@uhhm.no>
reply_to: bl@uhhm.no