Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9266699003 |
@@ -18,7 +18,7 @@ on:
|
|||||||
- .gitea/workflows/deploy.yml
|
- .gitea/workflows/deploy.yml
|
||||||
|
|
||||||
env:
|
env:
|
||||||
PORTAL_RELEASE: v0.6.23
|
PORTAL_RELEASE: v0.3.35
|
||||||
INSTANCE: uhhm-portal
|
INSTANCE: uhhm-portal
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
@@ -58,10 +58,6 @@ jobs:
|
|||||||
KANIDM_URL=${{ vars.KANIDM_URL }}
|
KANIDM_URL=${{ vars.KANIDM_URL }}
|
||||||
OAUTH2_CLIENT_ID=${{ vars.OAUTH2_CLIENT_ID }}
|
OAUTH2_CLIENT_ID=${{ vars.OAUTH2_CLIENT_ID }}
|
||||||
OAUTH2_CLIENT_SECRET=${{ secrets.OAUTH2_CLIENT_SECRET }}
|
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 }}
|
PUBLIC_URL=${{ vars.PUBLIC_URL }}
|
||||||
COOKIE_SECURE=true
|
COOKIE_SECURE=true
|
||||||
# This repo is its own instance's content source.
|
# This repo is its own instance's content source.
|
||||||
@@ -114,60 +110,7 @@ jobs:
|
|||||||
}
|
}
|
||||||
|
|
||||||
{$DOMAIN} {
|
{$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 {
|
log {
|
||||||
output file /var/log/caddy/www.log
|
output file /var/log/caddy/www.log
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,18 +15,13 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
# The lint is portal itself (`portal lint`, since portal 0.6.0;
|
# Same bare-metal runner/host as this repo's own Deploy workflow,
|
||||||
# iris is no longer released), at the version this site runs
|
# which ships question_lint alongside the portal binary in every
|
||||||
# (PORTAL_RELEASE in deploy.yml).
|
# 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
|
- name: Lint questions
|
||||||
env:
|
run: /srv/app/uhhm-portal/current/question_lint --path questions
|
||||||
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
|
|
||||||
|
|
||||||
reload:
|
reload:
|
||||||
runs-on: bare
|
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
|
lint, so no publicly collected answer can land where no one will ever
|
||||||
see it.
|
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
|
## Routing: the tree is the router
|
||||||
|
|
||||||
The `questions/` directory tree is the URL tree — `index.yaml` names
|
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
|
type: textarea
|
||||||
optional: true
|
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
|
- name: Projects
|
||||||
description: ""
|
description: ""
|
||||||
action: /review
|
action: /review
|
||||||
|
|||||||
@@ -4,21 +4,3 @@
|
|||||||
hero:
|
hero:
|
||||||
kind: module
|
kind: module
|
||||||
module: hero.js
|
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