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>
This commit is contained in:
Bendik Aagaard Lynghaug
2026-09-23 15:09:48 +02:00
co-authored by Claude Fable 5.1
parent 58225386b0
commit 5b657828e1
29 changed files with 950 additions and 539 deletions
+1 -1
View File
@@ -18,7 +18,7 @@ on:
- .gitea/workflows/deploy.yml - .gitea/workflows/deploy.yml
env: env:
PORTAL_RELEASE: v0.3.36 PORTAL_RELEASE: v0.3.42
INSTANCE: uhhm-portal INSTANCE: uhhm-portal
jobs: jobs:
+1 -1
View File
@@ -20,7 +20,7 @@ jobs:
# this site runs (PORTAL_RELEASE in deploy.yml). # this site runs (PORTAL_RELEASE in deploy.yml).
- name: Lint questions - name: Lint questions
env: env:
IRIS_RELEASE: v0.3.39 IRIS_RELEASE: v0.3.42
run: | run: |
# The checkout is on a noexec mount for this runner; the cargo # The checkout is on a noexec mount for this runner; the cargo
# target dir is where it may run what it fetched. # target dir is where it may run what it fetched.
+112 -67
View File
@@ -1,68 +1,113 @@
# 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: aggregates:
# One continuous graph per person, applicant through employee - the - bucket: applicants
# bucket name stays "applicants" even as a record progresses past the initial: open
# applying stage (a deliberate trade: one chain of events per person, states:
# no identity-linking between separate buckets). active:
- bucket: applicants event: onboarded
initial: open declined:
states: event: declined
open: { event: applied } departed:
invited: { event: invited } event: departed
declined: { event: declined } invited:
active: { event: onboarded } event: invited
departed: { event: departed } open:
transitions: event: applied
open: [invited, declined] transitions:
invited: [active, declined] active:
active: [departed] - departed
- declined
- bucket: subscribers invited:
initial: open - active
states: - declined
open: { event: subscribed } open:
unsubscribed: { event: unsubscribed } - invited
transitions: - declined
open: [unsubscribed] - bucket: custody_requests
initial: open
- bucket: projects states:
initial: open declined:
states: event: declined
open: { event: submitted } in_dialogue:
accepted: { event: accepted } event: in_dialogue
declined: { event: declined } open:
transitions: event: received
open: [accepted, declined] resolved:
event: resolved
- bucket: organizations transitions:
initial: prospect in_dialogue:
states: - resolved
prospect: { event: identified } - declined
client: { event: became_client } open:
past_client: { event: churned } - in_dialogue
transitions: - declined
prospect: [client, past_client] - bucket: development_proposals
client: [past_client] initial: open
states:
# Proposals to change this very repo - the question architecture approved:
# dogfooding its own development. Approval hands off to an n8n event: approved
# automation that opens a PR against this repo (see the infrastructure open:
# repo's development-commit workflow); the lint CI gates the merge. event: proposed
- bucket: development_proposals rejected:
initial: open event: rejected
states: transitions:
open: { event: proposed } open:
approved: { event: approved } - approved
rejected: { event: rejected } - rejected
transitions: - bucket: organizations
open: [approved, rejected] 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
declined:
event: declined
open:
event: submitted
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
declined:
event: declined
open:
event: received
previewed:
event: previewed
transitions:
approved:
- previewed
- declined
open:
- approved
- declined
+173
View File
@@ -0,0 +1,173 @@
needs:
- done_when:
- accepted
- declined
enters: /
handled_by: owners
id: commission
lands_in: projects
max_fields: 4
max_steps: 2
stages:
- open
- accepted
- declined
success:
- accepted
via: You run an organization that needs software built
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: 2
stages:
- open
- approved
- previewed
- declined
success:
- previewed
via: You run an organization that needs software built
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: 2
stages:
- open
- invited
- active
- departed
- declined
success:
- departed
via: You want to join us, follow us, or fix a page on this site
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: 2
stages:
- open
- unsubscribed
success: []
via: You want to join us, follow us, or fix a page on this site
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: 2
stages:
- open
- approved
- rejected
success:
- approved
via: You want to join us, follow us, or fix a page on this site
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
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
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.
+94
View File
@@ -0,0 +1,94 @@
alternatives:
- 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
description: Have a problem software should solve, or want to see your own way of working as a site before you pay anyone?
name: Do you have a brief, or do you want to see your business as a site first?
responsible:
contact: bl@uhhm.no
name: Bendik Aagaard Lynghaug
+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: 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.
+62 -112
View File
@@ -1,113 +1,63 @@
name: What are you here for?
description: >
Strategies that address problems — applied in software.
responsible:
name: Bendik Aagaard Lynghaug
contact: bl@uhhm.no
alternatives: alternatives:
- name: Good software begins with a clear argument - action: /build
description: > consequence:
Infrastructure, tools, or a product — bring the problem, and we - Start here
read the whole brief before answering. description: Send us a brief, or first describe how work flows through your business and see it as a working site before you pay for anything.
action: /project features:
consequence: [Tell us what you're after] - description: ''
record_as: projects icon: lucide:hammer
encouragements: name: For a business or an organization with a job for us
- We read every word before breaking ground. name: You run an organization that needs software built
features: - action: /join
- name: What we've built consequence:
icon: lucide:layers - Start here
description: A few things already out in the world. description: A developer, designer or student who wants in; someone who wants a note now and then; or a reader who would reword a page on uhhm.no.
resource: features:
source: - description: ''
kind: gitea_starred icon: lucide:handshake
username: bl name: For a person, on their own behalf
public: true name: You want to join us, follow us, or fix a page on this site
# The link prefers the repo's Website setting so private - action: /received/custody
# repos can point at something the public can actually consequence:
# open; a private repo with no website gets no link at all - Send to a custodian
# (the card renders an unlinked heading) instead of a 404. 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.
# TODO: star counts (Gitea's .stars_count) are left out until encouragements:
# they're worth showing - add `stars: .stars_count` back here. - A custodian reads every message that comes in here.
jq: > features:
.[] | {name: .name, description: .description, - 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.
url: (if .website != null and .website != "" then .website icon: lucide:route
elif .private then null name: What happens next
else .html_url end)} - description: A custodian reads it directly; if it's urgent, write to bl@uhhm.no in the meantime.
- name: Your brief icon: lucide:user-check
icon: lucide:file-text name: Who reads this
description: > - description: Say which of the four it is, and what you need, in a few lines.
A few lines are enough to begin a conversation — we need a icon: lucide:shield
name and an address because the reply is personal, and the name: Reach a custodian
organization because software always lands somewhere. requirements:
requirements: - label: Name
- name: name name: name
label: Name placeholder: Your name
- name: email type: text
label: Email - label: Email
type: email name: email
- name: project placeholder: you@yourcompany.com
label: The build type: email
type: textarea - label: What's going on
placeholder: Just the shape of it — we'll ask the right questions. name: about
- name: organization options:
label: Organization - Running a node of my own
- name: org_contact - Building on the network
label: Their contact, if not you - Something the network keeps about me
optional: true - Something else
type: select
- name: Attention is earned - label: In short
description: > name: what
Considered notes on what we're building, and why — sent only when placeholder: A relay node is misbehaving
there's something worth saying. type: textarea
action: /subscribed name: 'You are on the redoal network: a node, an app, or a recording that concerns you'
consequence: [Subscribe] record_as: custody_requests
record_as: subscribers description: UHHM builds software for organisations that can say what problem they have, and keeps the redoal network underneath.
encouragements: name: Who are you, and what do you need from UHHM?
- Consider yourself posted. responsible:
features: contact: bl@uhhm.no
- name: Where to send it name: Bendik Aagaard Lynghaug
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
+110
View File
@@ -0,0 +1,110 @@
alternatives:
- 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
description: 'Three ways to be part of what UHHM builds: work with us, hear from us, or fix something on this site.'
name: How do you want to be part of this?
responsible:
contact: bl@uhhm.no
name: Bendik Aagaard Lynghaug
+1
View File
@@ -0,0 +1 @@
qualifies: owners
+212
View File
@@ -0,0 +1,212 @@
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: active
label: End collaboration
to: declined
- from: invited
label: Confirm active
to: active
- from: invited
label: Withdraw 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: Mark not pursued
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