Compare commits

...
2 Commits
Author SHA1 Message Date
Bendik Aagaard LynghaugandClaude Fable 5 f243494499 Pin portal v0.2.0; document tree routing in the schema reference
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-24 22:33:16 +02:00
Bendik Aagaard LynghaugandClaude Fable 5 ad9dd1762b The tree is the router: nest the develop flow, section-gate review
develop/ holds the whole proposal flow (index = owners desk,
proposal = public form, proposed = chain-gated receipt) with relative
actions; review/ gains _section.yaml (one owners gate for the
directory) and a [record].yaml dynamic page - /review/<chain-hash>
shows a single proposal. Top-level files drop their now-derived id:
and fossil route: fields. URLs /develop-proposal and /proposed become
/develop/proposal and /develop/proposed (no automation references
them - n8n filters checked).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-24 22:32:01 +02:00
12 changed files with 88 additions and 27 deletions
+1 -1
View File
@@ -18,7 +18,7 @@ on:
- .gitea/workflows/deploy.yml
env:
PORTAL_RELEASE: v0.1.0
PORTAL_RELEASE: v0.2.0
INSTANCE: uhhm-portal
jobs:
+57 -8
View File
@@ -167,10 +167,14 @@ The rules every page here follows:
The exact structs live in portal's `src/content.rs`; the shape:
```
Question id (doubles as the URL path), name, description,
qualifies (Kanidm group gate), followup (nav-hidden
until the visitor carries a chain),
responsible {name, contact}, alternatives[]
Question id (derived from the file's path - see routing
below - declare only to override), name,
description, qualifies (Kanidm group gate),
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],
encouragements[], images[] (1 = banner, 2+ = card deck),
record_as (bucket), self_transition {bucket, to, label},
@@ -208,9 +212,54 @@ review resource's transitions — fireable by whoever holds one specific
record's `?chain=` link plus its matching email (the unsubscribe
pattern).
## Routing: the tree is the router
The `questions/` directory tree is the URL tree — `index.yaml` names
its directory, everything else appends its stem:
```
questions/
index.yaml /
applied.yaml /applied
develop/
index.yaml /develop
proposal.yaml /develop/proposal
proposed.yaml /develop/proposed
review/
_section.yaml (not a page - defaults for the directory)
index.yaml /review
[record].yaml /review/<any value>
```
- `id:` is derived from the path; declaring it still wins (legacy),
with a lint warning when it disagrees.
- `action:` and `requires_chain:` take relative refs — `proposed`
names a sibling, `../x` climbs, `/x` is absolute. A directory is a
self-contained flow: `git mv` renames every internal edge with it.
- Files nested in a subdirectory infer `followup: true` unless
they're the directory's `index.yaml` — declare `followup: false`
on a nested page that should stay in the nav. Top-level files keep
the flat-repo default (not a followup).
- `_section.yaml` applies `qualifies`, `requires_chain`, and
`responsible` to every page at or below its directory (nearest
ancestor wins; a page's own declaration always overrides). A URL
prefix is a trust boundary.
- Any other `_`-prefixed file is skipped entirely — drafts live in
the tree without being served.
- `[name].yaml` is a dynamic page: it serves every `/dir/<value>`,
with the segment substituted into `{name}` placeholders in the
page's resource `key`s (`/review/<chain-hash>` shows that one
record). One per directory; never in the nav; not a valid `action`
target.
- `requires_chain: <ref>` gates a page on provenance instead of
identity: the visitor's `?chain=` lineage must verifiably end at an
answer to the referenced question, otherwise the page renders a
pointer there instead of its alternatives.
## Adding a page
Drop a `questions/<name>.yaml` with a unique `id`, reference that `id`
from some alternative's `action`, push. Lint runs, portal hot-reloads,
the page is live — no registration, no deploy. Or propose it through
`/develop-proposal` and let the loop do the pushing.
Drop a YAML file where its URL should live (`questions/foo.yaml`
`/foo`, `questions/flow/step.yaml` → `/flow/step`), reference it from
some alternative's `action`, push. Lint runs, portal hot-reloads, the
page is live — no registration, no deploy. Or propose it through
`/develop/proposal` and let the loop do the pushing.
-2
View File
@@ -1,5 +1,3 @@
id: /applied
route: ^/applied
followup: true
name: What happens now that you've applied?
description: >
@@ -1,5 +1,3 @@
id: /develop
route: ^/develop
name: Development proposals
qualifies: owners
description: >
@@ -1,5 +1,6 @@
id: /develop-proposal
route: ^/develop-proposal
# 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
@@ -13,7 +14,7 @@ alternatives:
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
action: proposed
consequence: [Propose it]
record_as: development_proposals
encouragements:
@@ -1,6 +1,6 @@
id: /proposed
route: ^/proposed
followup: true
# 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: Proposal received
description: >
It's in the queue. An owner reads every proposal in full — approved
-1
View File
@@ -1,4 +1,3 @@
id: /
name: What are you here for?
description: >
Strategies that address problems — applied in software.
-2
View File
@@ -1,5 +1,3 @@
id: /project
route: ^/project
followup: true
name: So, what happens now?
description: >
+20
View File
@@ -0,0 +1,20 @@
# 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: One proposal
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
@@ -0,0 +1,3 @@
# Everything under /review is the owners' area - one gate for the
# whole directory instead of a qualifies flag per file.
qualifies: owners
@@ -1,7 +1,4 @@
id: /review
route: ^/review
name: The review desk
qualifies: owners
description: >
Sign in as an organizational owner to see what's come in.
alternatives:
-2
View File
@@ -1,5 +1,3 @@
id: /subscribed
route: ^/subscribed
followup: true
name: What should you expect?
description: >