Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9266699003 |
@@ -18,7 +18,7 @@ on:
|
||||
- .gitea/workflows/deploy.yml
|
||||
|
||||
env:
|
||||
PORTAL_RELEASE: v0.6.23
|
||||
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.
|
||||
@@ -114,60 +110,7 @@ jobs:
|
||||
}
|
||||
|
||||
{$DOMAIN} {
|
||||
# Mail clients look for their configuration on the bare domain -
|
||||
# Thunderbird at /.well-known/autoconfig, Outlook by POSTing to
|
||||
# /Autodiscover. Stalwart already generates both, templated on the
|
||||
# address asked about, so these two paths go to it instead of being
|
||||
# copied into this repo: one source of truth, and no second
|
||||
# hostname to certify. Host is rewritten to the name Stalwart
|
||||
# answers for; it never sees a request for the bare domain
|
||||
# otherwise. (MTA-STS cannot be served here - RFC 8461 requires
|
||||
# mta-sts.<domain> - so it keeps its own record and route.)
|
||||
# PACC, the standard that replaces guessing at hostnames: one JSON
|
||||
# document on the mail domain itself advertising every protocol,
|
||||
# which is why it belongs here and not on a subdomain. Stalwart
|
||||
# generates it, and /.well-known/mail-v1.xml beside it.
|
||||
@pacc path /.well-known/user-agent-configuration.json /.well-known/mail-v1.xml
|
||||
handle @pacc {
|
||||
reverse_proxy stalwart:8080 {
|
||||
header_up Host mail.{$DOMAIN}
|
||||
}
|
||||
}
|
||||
|
||||
handle /.well-known/autoconfig/* {
|
||||
reverse_proxy stalwart:8080 {
|
||||
header_up Host mail.{$DOMAIN}
|
||||
}
|
||||
}
|
||||
|
||||
# CalDAV and CardDAV discovery (RFC 6764): a client looks for
|
||||
# these on the domain its address is in, and Stalwart answers
|
||||
# each with a 307 to the collection. iOS uses these even though
|
||||
# Apple Mail ignores autoconfig and Autodiscover entirely.
|
||||
# The collections the two redirects above point at. Stalwart builds
|
||||
# those Location headers from the forwarded host, not from the Host
|
||||
# this route rewrites, so they land on the bare domain - which means
|
||||
# the collections have to be reachable here too, or a client follows
|
||||
# a 307 into portal's catch-all. Keeping them on one host is the
|
||||
# better answer anyway: the client never changes origin.
|
||||
@dav path /.well-known/caldav /.well-known/carddav /dav/*
|
||||
handle @dav {
|
||||
reverse_proxy stalwart:8080 {
|
||||
header_up Host mail.{$DOMAIN}
|
||||
}
|
||||
}
|
||||
|
||||
@autodiscover path /Autodiscover/* /autodiscover/* /AutoDiscover/*
|
||||
handle @autodiscover {
|
||||
reverse_proxy stalwart:8080 {
|
||||
header_up Host mail.{$DOMAIN}
|
||||
}
|
||||
}
|
||||
|
||||
handle {
|
||||
reverse_proxy host.docker.internal:3010
|
||||
}
|
||||
|
||||
reverse_proxy host.docker.internal:3010
|
||||
log {
|
||||
output file /var/log/caddy/www.log
|
||||
}
|
||||
|
||||
@@ -15,18 +15,13 @@ jobs:
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
# The lint is portal itself (`portal lint`, since portal 0.6.0;
|
||||
# iris is no longer released), at the version 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:
|
||||
PORTAL_RELEASE: v0.6.23
|
||||
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/portal-lint-$PORTAL_RELEASE"
|
||||
[ -x "$bin" ] || { curl -sfL "${{ github.server_url }}/uhhm/portal/releases/download/$PORTAL_RELEASE/portal-x86_64-linux" -o "$bin" && chmod +x "$bin"; }
|
||||
"$bin" lint --path questions
|
||||
run: /srv/app/uhhm-portal/current/question_lint --path questions
|
||||
|
||||
reload:
|
||||
runs-on: bare
|
||||
|
||||
@@ -1,6 +0,0 @@
|
||||
# How to report a security problem with uhhm.no, served from this repo's
|
||||
# .well-known/ by portal's static root overlay.
|
||||
Contact: mailto:bl@uhhm.no
|
||||
Expires: 2027-10-07T00:00:00.000Z
|
||||
Preferred-Languages: no, en
|
||||
Canonical: https://uhhm.no/.well-known/security.txt
|
||||
@@ -289,37 +289,6 @@ it>` naming the automation that does. A bucket nothing reads fails
|
||||
lint, so no publicly collected answer can land where no one will ever
|
||||
see it.
|
||||
|
||||
**Mail is content** (portal 0.3.43, delivered by
|
||||
[gdo](https://project.uhhm.no/uhhm/gdo)). A state in `aggregates.yaml`
|
||||
may carry the mail its arrival sends:
|
||||
|
||||
```yaml
|
||||
states:
|
||||
open: { event: received, mail: { to: submitter, subject: "Got it, {name}", body: "We read every one. Withdraw it here: {site}/decide/brief?chain={chain}&email={email}" } }
|
||||
fixed: { event: fixed, mail: { to: board@example.no, subject: "Fixed: {where}", body: "..." } }
|
||||
```
|
||||
|
||||
`to` is `submitter` (the record's `email` field; lint then requires
|
||||
every form recording into that bucket to collect a required `email`)
|
||||
or one fixed address. Subject and body are templates over the
|
||||
record's fields, plus `{site}` (the instance's public URL), `{chain}`
|
||||
(the record id), `{email}`, `{bucket}`, `{state}`; the body is
|
||||
markdown, sent as plain text with an HTML alternative. An invite
|
||||
(`invite:` on an alternative) mails its one-time link built in, in the
|
||||
site's `lang`. None of it is sent unless `site.yaml` says who it comes
|
||||
from:
|
||||
|
||||
```yaml
|
||||
mail:
|
||||
from: UHHM <portal@uhhm.no>
|
||||
reply_to: bl@uhhm.no # optional
|
||||
invite: { subject: "...", body: "..." } # optional rewording; {name} {username} {email} {group} {link} {site} {site_name} {expires}
|
||||
```
|
||||
|
||||
Portal renders each mail the moment the record enters the state and
|
||||
publishes it on `portal.mail.send` in the `mail` JetStream stream;
|
||||
gdo, one per host, hands it to the host's SMTP. A transition that
|
||||
fires twice sends once.
|
||||
## Routing: the tree is the router
|
||||
|
||||
The `questions/` directory tree is the URL tree — `index.yaml` names
|
||||
|
||||
-13
@@ -1,13 +0,0 @@
|
||||
/* uhhm - the skin over portal's own stylesheet
|
||||
(site.yaml `stylesheet`, loaded after main.css, so these rules win). */
|
||||
|
||||
/* Portal puts the way in beside the wordmark on every page of a site
|
||||
that has sign-in at all, and there is no setting to turn it off:
|
||||
`sign_in` follows whether OIDC is enabled on the instance. uhhm.no is
|
||||
read and answered without an account, so the whole nav goes - signed
|
||||
out, where it reads "Sign in", and signed in, where it reads a name
|
||||
and "Sign out". Whoever edits the site goes to /auth/login and
|
||||
/auth/logout directly. */
|
||||
.session {
|
||||
display: none;
|
||||
}
|
||||
+127
@@ -0,0 +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:
|
||||
- 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: 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.
|
||||
@@ -76,77 +76,6 @@ alternatives:
|
||||
type: textarea
|
||||
optional: true
|
||||
|
||||
# A guest for a conversation on chat.uhhm.no: an account at id.uhhm.no
|
||||
# with a passkey of their own, a place in cnats_users (which is what
|
||||
# lets someone into the lobby, where the video calls are), and a mail
|
||||
# saying when to turn up. The link lasts 24 hours.
|
||||
- name: Invite to a conversation
|
||||
description: >
|
||||
Someone you want to talk to on chat.uhhm.no. They get a mail from
|
||||
bl@uhhm.no with a link to make a passkey with us (valid for 24
|
||||
hours) and the time to turn up in the lobby, where the video is.
|
||||
action: /review
|
||||
consequence: [Send the invite]
|
||||
invite:
|
||||
group: cnats_users
|
||||
mail:
|
||||
subject: "A conversation with {site_name}, {tidspunkt}"
|
||||
body: |-
|
||||
Hello {name},
|
||||
|
||||
You are invited to a conversation on chat.uhhm.no, {tidspunkt}.
|
||||
|
||||
First, make a passkey with us. Open the link below, add a
|
||||
passkey, and press "Save" at the end: without Save it is not
|
||||
kept. The link works until {expires}:
|
||||
|
||||
{link}
|
||||
|
||||
Your username is {username}.
|
||||
|
||||
When the time comes, go to https://chat.uhhm.no, sign in with
|
||||
the passkey, and you land in the lobby. Press the video button
|
||||
there, and we are in.
|
||||
|
||||
{notat}
|
||||
features:
|
||||
- name: ""
|
||||
description: Who, and when to meet in the lobby.
|
||||
requirements:
|
||||
- name: name
|
||||
label: Name
|
||||
- name: email
|
||||
label: Email
|
||||
type: email
|
||||
- name: tidspunkt
|
||||
label: When
|
||||
type: datetime-local
|
||||
- name: notat
|
||||
label: A line for them (optional)
|
||||
type: textarea
|
||||
optional: true
|
||||
|
||||
# Every invite sent from here, from portal's own invites bucket. The
|
||||
# row carries the one-time link too, should the mail not reach them.
|
||||
- name: Invited
|
||||
description: >
|
||||
Everyone invited from this desk, with their one-time link and when
|
||||
it runs out. The mail went out by itself; the link is here should
|
||||
it not reach them. A link that ran out can be sent again; mark an
|
||||
invite done once they have signed in.
|
||||
action: /review
|
||||
consequence: [Confirm]
|
||||
features:
|
||||
- name: ""
|
||||
description: One row per invite.
|
||||
resource:
|
||||
source: { kind: kv, bucket: portal_invites }
|
||||
requires_group: owners
|
||||
transitions:
|
||||
- { from: open, to: done, label: Done }
|
||||
- { from: expired, to: open, label: Send again }
|
||||
- { from: expired, to: done, label: Done }
|
||||
|
||||
- name: Projects
|
||||
description: ""
|
||||
action: /review
|
||||
|
||||
@@ -4,21 +4,3 @@
|
||||
hero:
|
||||
kind: module
|
||||
module: hero.js
|
||||
|
||||
# The skin over portal's own stylesheet.
|
||||
stylesheet: custom.css
|
||||
|
||||
# The iOS configuration profile at /profile.mobileconfig. Everything in it -
|
||||
# hosts, ports, the DAV collection paths - comes from Stalwart's own PACC
|
||||
# document, so there is nothing here to keep in step with the mail server.
|
||||
# The profile carries no address or password; iOS asks at install.
|
||||
profile:
|
||||
pacc: https://mail.uhhm.no/.well-known/user-agent-configuration.json
|
||||
name: uhhm
|
||||
organization: uhhm
|
||||
|
||||
# Mail goes out through gdo on this host, which sends over JMAP as
|
||||
# bl@uhhm.no; replies come back to the same address.
|
||||
mail:
|
||||
from: "Bendik Lynghaug <bl@uhhm.no>"
|
||||
reply_to: bl@uhhm.no
|
||||
|
||||
Reference in New Issue
Block a user