Compare commits
37
Commits
87d004cc05
...
main
@@ -18,7 +18,7 @@ on:
|
||||
- .gitea/workflows/deploy.yml
|
||||
|
||||
env:
|
||||
PORTAL_RELEASE: v0.3.0
|
||||
PORTAL_RELEASE: v0.3.32
|
||||
INSTANCE: uhhm-portal
|
||||
|
||||
jobs:
|
||||
@@ -68,6 +68,10 @@ jobs:
|
||||
# The release tarball carries the site bundle at site/ (no
|
||||
# target/ prefix), so override Cargo.toml's build-time path.
|
||||
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 }}
|
||||
# Read-only (read:user,read:repository,read:organization),
|
||||
# used only by the Gitea resource sources (portal
|
||||
@@ -81,9 +85,16 @@ jobs:
|
||||
# No sudo: the runner's unit sets NoNewPrivileges=yes - systemctl
|
||||
# talks to PID1 over D-Bus, authorized by the polkit rule scoped
|
||||
# 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
|
||||
run: |
|
||||
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
|
||||
|
||||
# Apex and www are separate cookie scopes (no shared Domain
|
||||
|
||||
@@ -133,13 +133,18 @@ entirely in this repo, in this order:
|
||||
and its workflow must be updated in lockstep.
|
||||
|
||||
5. **Change the system through itself.** `/develop/proposal` takes a
|
||||
proposed replacement for any content file (`questions/*.yaml` or
|
||||
`aggregates.yaml` — nothing else is in scope) — from a person, or
|
||||
eventually a locally-run model, through the same public form. An
|
||||
owner approves it on `/develop`; approval triggers the automation
|
||||
that branches, commits, opens a PR, and merges only when the same
|
||||
lint that gates every human push passes. Nothing merges on
|
||||
autopilot.
|
||||
proposed replacement for any content file (the `questions/` tree,
|
||||
`aggregates.yaml`, or `site.yaml` — nothing else is in scope) —
|
||||
from a person, or eventually a locally-run model, through the same
|
||||
public form. The submission itself becomes a branch and a draft
|
||||
pull request at once (n8n "Portal: proposal received"), so lint
|
||||
reports on it immediately and the submitter gets one
|
||||
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
|
||||
|
||||
@@ -175,9 +180,11 @@ Question id (derived from the file's path - see routing
|
||||
requires_chain (question ref the visitor's ?chain=
|
||||
lineage must end at), followup (nav-hidden until
|
||||
the visitor carries a chain; inferred from the
|
||||
tree when unset), responsible {name, contact},
|
||||
alternatives[]
|
||||
Alternative name, description, action, consequence[label],
|
||||
tree when unset), event {starts, duration, place}
|
||||
(announced page - see below), responsible
|
||||
{name, contact}, alternatives[]
|
||||
Alternative name, description, action, disabled (announced,
|
||||
not yet takeable), consequence[label],
|
||||
encouragements[], images[] (1 = banner, 2+ = card deck),
|
||||
record_as (bucket), self_transition {bucket, to, label},
|
||||
features[]
|
||||
@@ -189,20 +196,38 @@ Question id (derived from the file's path - see routing
|
||||
bind {field, param, resource} (load this field's
|
||||
value from a resource whenever the named sibling
|
||||
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 |
|
||||
gitea_releases | url),
|
||||
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`,
|
||||
`select`, `file`, `prosekit` (rich text), `gesture` (draw a stroke on
|
||||
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
|
||||
`select`, `file`, `prosekit` (rich text), `gesture`, `voice`, or any
|
||||
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
|
||||
`aggregates.yaml`) declaring instance branding: `title`, `wordmark`
|
||||
(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
|
||||
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
|
||||
read by some `kv` resource in this repo (a desk or listing) — or its
|
||||
`aggregates.yaml` entry must carry `attended_by: <who/what consumes
|
||||
|
||||
@@ -15,6 +15,9 @@ alternatives:
|
||||
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:
|
||||
@@ -60,9 +63,12 @@ alternatives:
|
||||
public: true
|
||||
source:
|
||||
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: >
|
||||
.[] | 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
|
||||
label: Or a new file's path
|
||||
placeholder: questions/example.yaml
|
||||
|
||||
@@ -27,11 +27,10 @@ alternatives:
|
||||
# repos can point at something the public can actually
|
||||
# open; a private repo with no website gets no link at all
|
||||
# (the card renders an unlinked heading) instead of a 404.
|
||||
# Gitea calls the star count stars_count, not GitHub's
|
||||
# stargazers_count.
|
||||
# TODO: star counts (Gitea's .stars_count) are left out until
|
||||
# they're worth showing - add `stars: .stars_count` back here.
|
||||
jq: >
|
||||
.[] | {name: .name, description: .description,
|
||||
stars: .stars_count,
|
||||
url: (if .website != null and .website != "" then .website
|
||||
elif .private then null
|
||||
else .html_url end)}
|
||||
|
||||
@@ -116,3 +116,26 @@ alternatives:
|
||||
- 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
|
||||
|
||||
Reference in New Issue
Block a user