Author SHA1 Message Date
Bendik Aagaard LynghaugandClaude Fable 5.1 9266699003 needs.yaml: what UHHM needs this site to do, as a checkable spec
Lint and reload / lint (push) Skipped
Five needs (commission, follow, collaborate, improve-site as planned,
track-prospect) and nine personas in their own words, for portal's
question_lint needs pass (uhhm/portal 188f9bf). The runtime never
reads this file; older lint binaries ignore it.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01L4jrCgLiKKHAEFuZUJjckH
2026-09-21 16:29:14 +02:00
30 changed files with 667 additions and 1056 deletions
+1 -5
View File
@@ -18,7 +18,7 @@ on:
- .gitea/workflows/deploy.yml
env:
PORTAL_RELEASE: v0.4.0
PORTAL_RELEASE: v0.3.35
INSTANCE: uhhm-portal
jobs:
@@ -58,10 +58,6 @@ jobs:
KANIDM_URL=${{ vars.KANIDM_URL }}
OAUTH2_CLIENT_ID=${{ vars.OAUTH2_CLIENT_ID }}
OAUTH2_CLIENT_SECRET=${{ secrets.OAUTH2_CLIENT_SECRET }}
# Onboarding from a desk (portal 0.3.40+, `invite:` alternatives): a
# Kanidm service-account token in idm_people_on_boarding. Empty
# means invites are not configured on this instance.
KANIDM_API_TOKEN=${{ secrets.KANIDM_API_TOKEN }}
PUBLIC_URL=${{ vars.PUBLIC_URL }}
COOKIE_SECURE=true
# This repo is its own instance's content source.
+6 -11
View File
@@ -15,18 +15,13 @@ jobs:
steps:
- uses: actions/checkout@v4
# iris (uhhm/iris) is the lint: it reads content with the portal
# release it is pinned to, so IRIS_RELEASE should match the portal
# this site runs (PORTAL_RELEASE in deploy.yml).
# Same bare-metal runner/host as this repo's own Deploy workflow,
# which ships question_lint alongside the portal binary in every
# pinned release - so content is validated by the exact portal
# version this instance runs, not a hand-maintained yq/jq subset
# of the same rules.
- name: Lint questions
env:
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.
bin="/var/local/cargo-target/iris-$IRIS_RELEASE"
[ -x "$bin" ] || { curl -sfL "${{ github.server_url }}/uhhm/iris/releases/download/$IRIS_RELEASE/iris" -o "$bin" && chmod +x "$bin"; }
"$bin" check --path questions
run: /srv/app/uhhm-portal/current/question_lint --path questions
reload:
runs-on: bare
+66 -153
View File
@@ -1,155 +1,68 @@
# 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:
- 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.
# 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]
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
- 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]
+125 -202
View File
@@ -1,204 +1,127 @@
# What UHHM, as a business, needs this site to do - stated apart from
# how the pages word it. `question_lint` checks every need against the
# pages and aggregates.yaml (portal `src/needs.rs`): a path of
# submissions gets the visitor's answer into `lands_in` within the
# effort budget, `handled_by` has a desk over that bucket, and every
# `done_when` state is reachable through the moves the desk offers.
#
# The personas are visitors in their own words - never the site's.
# `question_lint --needs-tasks` turns each page on their path into a
# typed choice for an answering engine; `--needs-score` grades its
# picks. A misroute means the page said one thing and they heard
# another. The runtime never reads this file.
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
- id: commission
who: someone with a problem software could solve, and a budget
wants: UHHM to take the work on
via: Good software begins with a clear argument
lands_in: projects
handled_by: owners
done_when: [accepted, declined]
max_steps: 1
# name, email, the build, the organization - the page argues for
# each of the four; a fifth would need the same argument.
max_fields: 4
- id: follow
who: someone curious who isn't ready to get involved
wants: to hear when something ships
via: Attention is earned
lands_in: subscribers
handled_by: owners
# Being on the list is the finished state; leaving is theirs to do.
done_when: [open, unsubscribed]
max_steps: 1
max_fields: 2
- id: collaborate
who: a developer, designer or researcher
wants: to work with UHHM
via: Work happens through dialogue, not tickets
lands_in: applicants
handled_by: owners
done_when: [active, declined]
max_steps: 1
max_fields: 3
- id: improve-site
who: anyone using this site
wants: the site itself to change
enters: /develop/proposal
# Announced, not open: the automation that turns a proposal into a
# pull request is still being built, so the alternative is disabled.
planned: true
lands_in: development_proposals
handled_by: owners
done_when: [approved, rejected]
max_steps: 1
max_fields: 3
- id: track-prospect
who: an owner who met an organization worth following up
wants: the relationship written down and walked to an outcome
enters: /review
enters_as: owners
via: Record a prospect
lands_in: organizations
handled_by: owners
done_when: [client, past_client]
max_steps: 1
max_fields: 2
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?
- 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: follow
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: follow
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: site-critic
need: improve-site
text: >
Your contact form never says how long a reply takes. You should
add that.
+25
View File
@@ -0,0 +1,25 @@
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
@@ -1 +0,0 @@
qualifies: custodians
-65
View File
@@ -1,65 +0,0 @@
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
@@ -1,9 +0,0 @@
alternatives:
- description: ''
name: Declined
self_transition:
bucket: projects
label: Declined
to: declined
description: ''
name: What would you like to do?
-9
View File
@@ -1,9 +0,0 @@
alternatives:
- description: ''
name: Resolved
self_transition:
bucket: custody_requests
label: Resolved
to: resolved
description: ''
name: What would you like to do?
-9
View File
@@ -1,9 +0,0 @@
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
@@ -0,0 +1,24 @@
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
@@ -0,0 +1,90 @@
# 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
@@ -0,0 +1,18 @@
# 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.
+111 -268
View File
@@ -1,270 +1,113 @@
alternatives:
- 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?
name: What are you here for?
description: >
Strategies that address problems — applied in software.
responsible:
contact: bl@uhhm.no
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
-1
View File
@@ -1 +0,0 @@
qualifies: owners
-209
View File
@@ -1,209 +0,0 @@
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
@@ -1,10 +0,0 @@
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
@@ -1,39 +0,0 @@
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
@@ -0,0 +1,15 @@
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
@@ -1,10 +0,0 @@
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
@@ -1,10 +0,0 @@
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
@@ -1,10 +0,0 @@
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
@@ -1,10 +0,0 @@
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
@@ -1,10 +0,0 @@
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
@@ -1,10 +0,0 @@
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
@@ -0,0 +1,20 @@
# 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
@@ -0,0 +1,3 @@
# 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
@@ -0,0 +1,141 @@
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
@@ -0,0 +1,22 @@
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,8 +4,3 @@
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