Compare commits

...
37 Commits
Author SHA1 Message Date
Bendik Aagaard Lynghaug ff57b15f59 Pin portal v0.3.32
Deploy instance / deploy (push) Successful in 0s
Lint and reload / lint (push) Successful in 2s
Lint and reload / reload (push) Successful in 0s
2026-09-01 22:15:11 +02:00
Bendik Aagaard Lynghaug 0d20770c50 Pin portal v0.3.31
Deploy instance / deploy (push) Successful in 1s
Lint and reload / lint (push) Successful in 2s
Lint and reload / reload (push) Successful in 0s
2026-09-01 21:51:02 +02:00
Bendik Aagaard Lynghaug fb73011d0c Pin portal v0.3.30
Deploy instance / deploy (push) Successful in 1s
Lint and reload / lint (push) Successful in 2s
Lint and reload / reload (push) Successful in 0s
2026-09-01 21:45:07 +02:00
Bendik Aagaard Lynghaug 9684146dd8 Pin portal v0.3.29
Deploy instance / deploy (push) Successful in 1s
Lint and reload / lint (push) Successful in 2s
Lint and reload / reload (push) Successful in 0s
2026-09-01 21:39:39 +02:00
Bendik Aagaard Lynghaug b8fc2ade6c Pin portal v0.3.28
Deploy instance / deploy (push) Successful in 1s
Lint and reload / lint (push) Successful in 3s
Lint and reload / reload (push) Successful in 1s
2026-09-01 20:33:31 +02:00
Bendik Aagaard Lynghaug 479bc72b6e Pin portal v0.3.27
Deploy instance / deploy (push) Successful in 1s
Lint and reload / lint (push) Successful in 3s
Lint and reload / reload (push) Successful in 0s
2026-09-01 20:29:11 +02:00
Bendik Aagaard LynghaugandClaude Fable 5 cb2d9760a1 Pin portal v0.3.24
Deploy instance / deploy (push) Successful in 1s
Lint and reload / lint (push) Successful in 2s
Lint and reload / reload (push) Successful in 0s
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Y42TyF8Zu7NGRR2893vNcZ
2026-09-01 20:03:57 +02:00
Bendik Aagaard LynghaugandClaude Fable 5 0bfdc0b45e Pin portal v0.3.23
Deploy instance / deploy (push) Successful in 1s
Lint and reload / lint (push) Successful in 2s
Lint and reload / reload (push) Successful in 0s
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Y42TyF8Zu7NGRR2893vNcZ
2026-09-01 17:26:12 +02:00
Bendik Aagaard LynghaugandClaude Fable 5 c74fc6d708 Pin portal v0.3.21
Deploy instance / deploy (push) Successful in 1s
Lint and reload / lint (push) Successful in 2s
Lint and reload / reload (push) Successful in 0s
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Y42TyF8Zu7NGRR2893vNcZ
2026-09-01 17:13:52 +02:00
Bendik Aagaard LynghaugandClaude Fable 5 b4f12a1a6a Pin portal v0.3.18
Deploy instance / deploy (push) Successful in 1s
Lint and reload / lint (push) Successful in 2s
Lint and reload / reload (push) Successful in 0s
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Y42TyF8Zu7NGRR2893vNcZ
2026-09-01 16:01:04 +02:00
Bendik Aagaard LynghaugandClaude Fable 5 b34be2ada9 Pin portal v0.3.17
Deploy instance / deploy (push) Successful in 1s
Lint and reload / lint (push) Successful in 3s
Lint and reload / reload (push) Successful in 0s
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Y42TyF8Zu7NGRR2893vNcZ
2026-08-31 17:22:49 +02:00
Bendik Aagaard LynghaugandClaude Fable 5 f2dbd7aabd Pin portal v0.3.16
Deploy instance / deploy (push) Successful in 1s
Lint and reload / lint (push) Successful in 2s
Lint and reload / reload (push) Successful in 1s
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Y42TyF8Zu7NGRR2893vNcZ
2026-08-30 23:16:01 +02:00
Bendik Aagaard Lynghaug d770d8a137 Pin portal v0.3.14 - icon column
Deploy instance / deploy (push) Successful in 1s
Lint and reload / lint (push) Successful in 2s
Lint and reload / reload (push) Successful in 0s
2026-08-30 16:58:20 +02:00
Bendik Aagaard Lynghaug 3dab10d6e4 Pin portal v0.3.13 - voice keeps on submit
Deploy instance / deploy (push) Successful in 0s
Lint and reload / lint (push) Successful in 2s
Lint and reload / reload (push) Successful in 0s
2026-08-30 16:38:16 +02:00
Bendik Aagaard Lynghaug cba804adca Pin portal v0.3.12
Deploy instance / deploy (push) Successful in 1s
Lint and reload / lint (push) Successful in 3s
Lint and reload / reload (push) Successful in 0s
2026-08-30 16:08:59 +02:00
Bendik Aagaard Lynghaug eabde0c42c Pin portal v0.3.11 - templated action lint fix
Deploy instance / deploy (push) Successful in 1s
Lint and reload / lint (push) Successful in 3s
Lint and reload / reload (push) Successful in 0s
2026-08-30 16:04:58 +02:00
Bendik Aagaard Lynghaug 4a1bfd06c2 Starred repos: leave the star counts out for now
Lint and reload / lint (push) Successful in 3s
Lint and reload / reload (push) Successful in 1s
2026-08-30 16:00:53 +02:00
Bendik Aagaard Lynghaug 7859be90f5 Pin portal v0.3.10 - voice, places
Lint and reload / lint (push) Successful in 3s
Deploy instance / deploy (push) Successful in 1s
Lint and reload / reload (push) Successful in 0s
2026-08-30 16:00:06 +02:00
Bendik Aagaard Lynghaug 8a291fb574 README: voice, value, url substitution, audio cards
Lint and reload / lint (push) Successful in 2s
Lint and reload / reload (push) Successful in 0s
2026-08-30 15:58:24 +02:00
Bendik Aagaard Lynghaug f64d62f20e Pin portal v0.3.9 - templated actions, pickable places
Lint and reload / reload (push) Successful in 0s
Deploy instance / deploy (push) Successful in 1s
Lint and reload / lint (push) Successful in 3s
2026-08-30 15:42:11 +02:00
Bendik Aagaard Lynghaug dd858e4ee6 README: markdown descriptions
Lint and reload / lint (push) Successful in 2s
Lint and reload / reload (push) Successful in 1s
2026-08-30 14:47:22 +02:00
Bendik Aagaard Lynghaug 51fae8b86c Pin portal v0.3.8 - markdown descriptions
Lint and reload / lint (push) Successful in 3s
Lint and reload / reload (push) Successful in 0s
Deploy instance / deploy (push) Successful in 1s
2026-08-30 14:46:59 +02:00
Bendik Aagaard Lynghaug 83af980ab1 Pin portal v0.3.7 - feature links
Lint and reload / lint (push) Successful in 2s
Deploy instance / deploy (push) Successful in 1s
Lint and reload / reload (push) Successful in 0s
2026-08-30 14:42:00 +02:00
Bendik Aagaard Lynghaug e8a04a2783 README: Feature.link
Lint and reload / lint (push) Successful in 3s
Lint and reload / reload (push) Successful in 1s
2026-08-30 14:39:43 +02:00
Bendik Aagaard LynghaugandClaude Fable 5 799e2b3c8c Review desk: announced pages (portal_events)
Lint and reload / lint (push) Successful in 2s
Lint and reload / reload (push) Successful in 0s
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-30 12:37:17 +02:00
Bendik Aagaard Lynghaug 16e5a27bde Pin portal v0.3.6 - announced pages
Lint and reload / lint (push) Successful in 2s
Deploy instance / deploy (push) Successful in 1s
Lint and reload / reload (push) Successful in 0s
2026-08-30 12:36:48 +02:00
Bendik Aagaard LynghaugandClaude Fable 5 1bb73aa37a Deploy enables the instance unit
Deploy instance / deploy (push) Successful in 1s
Lint and reload / lint (push) Successful in 3s
Lint and reload / reload (push) Successful in 1s
A host reboot on 2026-08-30 left the instance down: deploys only ever
started it. enable is best-effort until the host's polkit rule grants
manage-unit-files to deploy-runner.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-30 12:09:14 +02:00
Bendik Aagaard Lynghaug 1dcf8118d2 Pin portal v0.3.5 - gesture ghost for v2 keys
Deploy instance / deploy (push) Successful in 1s
Lint and reload / lint (push) Successful in 3s
Lint and reload / reload (push) Successful in 0s
2026-08-30 11:43:10 +02:00
Bendik Aagaard LynghaugandClaude Fable 5 717c8ee365 README: the proposal loop as it now runs (draft PR on submission)
Lint and reload / lint (push) Successful in 3s
Lint and reload / reload (push) Successful in 0s
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-25 22:52:57 +02:00
Bendik Aagaard LynghaugandClaude Fable 5 17112452b8 Proposal file select lists the whole questions tree
Lint and reload / reload (push) Successful in 0s
Lint and reload / lint (push) Skipped
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-25 22:48:35 +02:00
Bendik Aagaard LynghaugandClaude Fable 5 17341c170e Propose it: announced, not yet open (disabled: true)
Lint and reload / lint (push) Successful in 2s
Lint and reload / reload (push) Successful in 0s
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-25 22:45:45 +02:00
Bendik Aagaard LynghaugandClaude Fable 5 569a155015 Pin portal v0.3.4 - disabled alternatives
Deploy instance / deploy (push) Successful in 1s
Lint and reload / lint (push) Successful in 3s
Lint and reload / reload (push) Successful in 1s
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-25 20:28:13 +02:00
Bendik Aagaard LynghaugandClaude Fable 5 a5125e191a README: Alternative.disabled
Lint and reload / lint (push) Successful in 2s
Lint and reload / reload (push) Successful in 0s
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-25 20:25:49 +02:00
Bendik Aagaard LynghaugandClaude Fable 5 951e85db33 Pin portal v0.3.3 - empty-list notice
Lint and reload / lint (push) Successful in 3s
Deploy instance / deploy (push) Successful in 1s
Lint and reload / reload (push) Successful in 0s
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-25 20:11:22 +02:00
Bendik Aagaard LynghaugandClaude Fable 5 564929db00 Pin portal v0.3.2 - resource empty text
Deploy instance / deploy (push) Successful in 1s
Lint and reload / lint (push) Successful in 2s
Lint and reload / reload (push) Successful in 0s
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-25 20:07:25 +02:00
Bendik Aagaard LynghaugandClaude Fable 5 36e86c7787 README: ResourceSpec.empty
Lint and reload / lint (push) Successful in 2s
Lint and reload / reload (push) Successful in 0s
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-25 20:05:22 +02:00
Bendik Aagaard LynghaugandClaude Fable 5 58b03a222e Pin portal v0.3.1 - content-hashed assets (LEPTOS_HASH_FILES=true)
Deploy instance / deploy (push) Successful in 1s
Lint and reload / lint (push) Successful in 2s
Lint and reload / reload (push) Successful in 0s
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-25 19:51:29 +02:00
5 changed files with 114 additions and 22 deletions
+12 -1
View File
@@ -18,7 +18,7 @@ on:
- .gitea/workflows/deploy.yml - .gitea/workflows/deploy.yml
env: env:
PORTAL_RELEASE: v0.3.0 PORTAL_RELEASE: v0.3.32
INSTANCE: uhhm-portal INSTANCE: uhhm-portal
jobs: jobs:
@@ -68,6 +68,10 @@ jobs:
# The release tarball carries the site bundle at site/ (no # The release tarball carries the site bundle at site/ (no
# target/ prefix), so override Cargo.toml's build-time path. # target/ prefix), so override Cargo.toml's build-time path.
LEPTOS_SITE_ROOT=site LEPTOS_SITE_ROOT=site
# Portal ships content-hashed pkg files (portal.<hash>.js) with
# a hash.txt in the site root; this makes the server reference
# them, so a stale cached bundle can never pair with new wasm.
LEPTOS_HASH_FILES=true
AUTOMATION_READ_TOKEN=${{ secrets.AUTOMATION_READ_TOKEN }} AUTOMATION_READ_TOKEN=${{ secrets.AUTOMATION_READ_TOKEN }}
# Read-only (read:user,read:repository,read:organization), # Read-only (read:user,read:repository,read:organization),
# used only by the Gitea resource sources (portal # used only by the Gitea resource sources (portal
@@ -81,9 +85,16 @@ jobs:
# No sudo: the runner's unit sets NoNewPrivileges=yes - systemctl # No sudo: the runner's unit sets NoNewPrivileges=yes - systemctl
# talks to PID1 over D-Bus, authorized by the polkit rule scoped # talks to PID1 over D-Bus, authorized by the polkit rule scoped
# to deploy-runner + the app@* unit pattern. # to deploy-runner + the app@* unit pattern.
# enable: the unit must come back after a host reboot (2026-08-30 a
# reboot left both portal instances down - deploys had only ever
# started them). Needs the manage-unit-files polkit grant; until
# that's on the host the enable is reported and skipped, never a
# failed deploy.
- name: Activate and restart - name: Activate and restart
run: | run: |
ln -sfn "/srv/app/$INSTANCE/releases/$PORTAL_RELEASE" /srv/app/$INSTANCE/current ln -sfn "/srv/app/$INSTANCE/releases/$PORTAL_RELEASE" /srv/app/$INSTANCE/current
systemctl enable app@$INSTANCE.service \
|| echo "::warning::could not enable app@$INSTANCE (polkit) - unit will not survive a reboot"
systemctl restart app@$INSTANCE.service systemctl restart app@$INSTANCE.service
# Apex and www are separate cookie scopes (no shared Domain # Apex and www are separate cookie scopes (no shared Domain
+69 -16
View File
@@ -133,13 +133,18 @@ entirely in this repo, in this order:
and its workflow must be updated in lockstep. and its workflow must be updated in lockstep.
5. **Change the system through itself.** `/develop/proposal` takes a 5. **Change the system through itself.** `/develop/proposal` takes a
proposed replacement for any content file (`questions/*.yaml` or proposed replacement for any content file (the `questions/` tree,
`aggregates.yaml` — nothing else is in scope) — from a person, or `aggregates.yaml`, or `site.yaml` — nothing else is in scope) —
eventually a locally-run model, through the same public form. An from a person, or eventually a locally-run model, through the same
owner approves it on `/develop`; approval triggers the automation public form. The submission itself becomes a branch and a draft
that branches, commits, opens a PR, and merges only when the same pull request at once (n8n "Portal: proposal received"), so lint
lint that gates every human push passes. Nothing merges on reports on it immediately and the submitter gets one
autopilot. acknowledgement mail if they left an address. An owner approves it
on `/develop`; approval un-drafts the PR and merges it once the
same lint that gates every human push passes ("Portal: commit
approved development proposal"). Nothing merges on autopilot. The
alternative is `disabled: true` while this loop is being finished —
announced, not yet open.
## Voice ## Voice
@@ -175,9 +180,11 @@ Question id (derived from the file's path - see routing
requires_chain (question ref the visitor's ?chain= requires_chain (question ref the visitor's ?chain=
lineage must end at), followup (nav-hidden until lineage must end at), followup (nav-hidden until
the visitor carries a chain; inferred from the the visitor carries a chain; inferred from the
tree when unset), responsible {name, contact}, tree when unset), event {starts, duration, place}
alternatives[] (announced page - see below), responsible
Alternative name, description, action, consequence[label], {name, contact}, alternatives[]
Alternative name, description, action, disabled (announced,
not yet takeable), consequence[label],
encouragements[], images[] (1 = banner, 2+ = card deck), encouragements[], images[] (1 = banner, 2+ = card deck),
record_as (bucket), self_transition {bucket, to, label}, record_as (bucket), self_transition {bucket, to, label},
features[] features[]
@@ -189,20 +196,38 @@ Question id (derived from the file's path - see routing
bind {field, param, resource} (load this field's bind {field, param, resource} (load this field's
value from a resource whenever the named sibling value from a resource whenever the named sibling
changes - {param} templates into a url source's path), changes - {param} templates into a url source's path),
relay (gesture only - ws(s):// redoal-relay URL) value (preset; on a dynamic page {name} takes the
URL segment - value: "{key}" hands a voice field
its address), relay (gesture/voice - ws(s)://
redoal-relay URL)
ResourceSpec source (kv | gitea_starred | gitea_org_repos | ResourceSpec source (kv | gitea_starred | gitea_org_repos |
gitea_releases | url), gitea_releases | url),
key, public, requires_group, transitions[{from, to, label}], key, public, requires_group, transitions[{from, to, label}],
jq (reshape filter) jq (reshape filter), empty (text when the
resource yields nothing; default "Nothing here yet.")
``` ```
Requirement `type`: `text` (default), `textarea`, `email`, `tel`, Requirement `type`: `text` (default), `textarea`, `email`, `tel`,
`select`, `file`, `prosekit` (rich text), `gesture` (draw a stroke on `select`, `file`, `prosekit` (rich text), `gesture`, `voice`, or any
a canvas; submits `{points, key}` - with `relay` set it announces the
stroke to a redoal-relay and shows echoes of similar strokes; mark it
`optional: true`, hidden inputs skip HTML required-validation), or any
HTML input type. HTML input type.
- `gesture` draws a stroke on a canvas and submits `{points, key}`.
With `relay` set it announces the stroke to a redoal-relay: the
relay answers with the key and its decode (drawn under the stroke),
with who is at a similar shape right now, and with *places* - keys
that hold recordings. Picking a place makes its key the field's:
the value becomes `{points, key, own_key, selected_from,
selected_distance}`. Mark it `optional: true` (hidden inputs skip
HTML required-validation).
- `voice` records in the browser and sends the audio to the relay,
which keeps it at the field's `value` key (`value: "{key}"` on a
`/shape/[key]` page). The value becomes `{key, digest, duration_ms}`.
Needs `relay`; `optional: true` for the same reason.
A `[name].yaml` page's segment also substitutes into a `url` resource
source (`https://relay.redoal.com/place/{key}`), and a resource item
with an https `audio` field renders an `<audio>` player.
A repo may also carry an optional `site.yaml` at its root (sibling of A repo may also carry an optional `site.yaml` at its root (sibling of
`aggregates.yaml`) declaring instance branding: `title`, `wordmark` `aggregates.yaml`) declaring instance branding: `title`, `wordmark`
(image URL), and the landing page's `hero`: (image URL), and the landing page's `hero`:
@@ -229,6 +254,34 @@ review resource's transitions — fireable by whoever holds one specific
record's `?chain=` link plus its matching email (the unsubscribe record's `?chain=` link plus its matching email (the unsubscribe
pattern). pattern).
**Descriptions are inline markdown** (alternative and feature):
`[text](https://…)` links, `*emphasis*`, `**strong**`, `` `code` ``.
One paragraph - block structure flattens; raw HTML is dropped; link
targets may be https, mailto or a site-relative path, anything else
renders as plain text.
**Announced pages.** A question carrying an `event:` block
```yaml
event:
starts: 2026-09-12T18:00:00+02:00 # RFC 3339, with offset
duration: 3h # m / h / d, e.g. "1d 6h"
place: Galleri X, Oslo # optional, shown verbatim
```
is announced in the header of every page (name, when, "in 3 days")
while its window is open, instead of listed in the footer nav. The
page itself is an ordinary question - RSVP, directions, whatever its
alternatives say. When the window closes the page turns into a
followup (only visitors carrying an answer chain still see it) and
portal moves the page's record in the runtime-owned `portal_events`
bucket from `announced` to `awaiting_summary`, publishing that on
`portal.answers.submitted` like any decision (alternative "Summary
due") - the "post what happened" task. `/review`'s "Announced pages"
desk reads that bucket; **Posted what happened** closes the task.
Lint warns when pages carry `event:` but nothing reads
`portal_events`.
**Attended buckets** (lint-enforced): every `record_as` bucket must be **Attended buckets** (lint-enforced): every `record_as` bucket must be
read by some `kv` resource in this repo (a desk or listing) — or its read by some `kv` resource in this repo (a desk or listing) — or its
`aggregates.yaml` entry must carry `attended_by: <who/what consumes `aggregates.yaml` entry must carry `attended_by: <who/what consumes
+8 -2
View File
@@ -15,6 +15,9 @@ alternatives:
has to be complete, valid YAML for that file, not a diff. has to be complete, valid YAML for that file, not a diff.
Nothing outside those files is in scope. Nothing outside those files is in scope.
action: proposed 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] consequence: [Propose it]
record_as: development_proposals record_as: development_proposals
encouragements: encouragements:
@@ -60,9 +63,12 @@ alternatives:
public: true public: true
source: source:
kind: url kind: url
url: https://project.uhhm.no/api/v1/repos/uhhm/questions/contents/questions?ref=main # 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: > jq: >
.[] | select(.type == "file") | {path: .path, name: .name} .tree[] | select(.type == "blob" and (.path | startswith("questions/")) and (.path | endswith(".yaml")))
| {path: .path, name: .path}
- name: new_path - name: new_path
label: Or a new file's path label: Or a new file's path
placeholder: questions/example.yaml placeholder: questions/example.yaml
+2 -3
View File
@@ -27,11 +27,10 @@ alternatives:
# repos can point at something the public can actually # repos can point at something the public can actually
# open; a private repo with no website gets no link at all # open; a private repo with no website gets no link at all
# (the card renders an unlinked heading) instead of a 404. # (the card renders an unlinked heading) instead of a 404.
# Gitea calls the star count stars_count, not GitHub's # TODO: star counts (Gitea's .stars_count) are left out until
# stargazers_count. # they're worth showing - add `stars: .stars_count` back here.
jq: > jq: >
.[] | {name: .name, description: .description, .[] | {name: .name, description: .description,
stars: .stars_count,
url: (if .website != null and .website != "" then .website url: (if .website != null and .website != "" then .website
elif .private then null elif .private then null
else .html_url end)} else .html_url end)}
+23
View File
@@ -116,3 +116,26 @@ alternatives:
- name: body - name: body
label: Body label: Body
type: prosekit 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