3 Commits
Author SHA1 Message Date
Bendik Aagaard Lynghaug 7429f1b9e1 Release 0.2.1
Test / test (push) Successful in 23s
Publish release / publish (push) Successful in 1m23s
2026-08-24 22:40:30 +02:00
Bendik Aagaard LynghaugandClaude Fable 5 a0305282ef Revalidating cache on app assets; site title survives SPA navigation
Test / test (push) Successful in 24s
pkg files keep stable names across releases, so an uncontrolled
browser cache could pair last release's wasm with the new server's
server-fn wire format - every page then renders its error branch
(redoal.com's 'Nothing here' after v0.2.0). Cache-Control: no-cache
on /pkg and /yes.js makes clients revalidate (a 304 per load) instead
of guessing. The content-declared site title also moves out of the
question Suspense: remounting with each navigation lost the
leptos_meta race to the compile-time fallback, flipping redoal.com's
tab to 'uhhm' on the first client-side nav.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-24 22:40:29 +02:00
Bendik Aagaard LynghaugandClaude Fable 5 c67f6f1a59 Docs: routing bullet + design doc marked implemented
Test / test (push) Successful in 23s
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-24 22:33:37 +02:00
6 changed files with 56 additions and 15 deletions
Generated
+1 -1
View File
@@ -2948,7 +2948,7 @@ dependencies = [
[[package]] [[package]]
name = "portal" name = "portal"
version = "0.2.0" version = "0.2.1"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"arc-swap", "arc-swap",
+2 -2
View File
@@ -1,6 +1,6 @@
[package] [package]
name = "portal" name = "portal"
version = "0.2.0" version = "0.2.1"
edition = "2021" edition = "2021"
[lib] [lib]
@@ -19,7 +19,7 @@ axum = { version = "0.8", features = ["multipart"], optional = true }
aws-sdk-s3 = { version = "1", optional = true } aws-sdk-s3 = { version = "1", optional = true }
tokio = { version = "1", features = ["rt-multi-thread", "macros", "signal"], optional = true } tokio = { version = "1", features = ["rt-multi-thread", "macros", "signal"], optional = true }
tower = { version = "0.5", optional = true } tower = { version = "0.5", optional = true }
tower-http = { version = "0.6", features = ["fs"], optional = true } tower-http = { version = "0.6", features = ["fs", "set-header"], optional = true }
tower-sessions = { version = "0.14", optional = true } tower-sessions = { version = "0.14", optional = true }
async-nats = { version = "0.38", optional = true } async-nats = { version = "0.38", optional = true }
arc-swap = { version = "1", optional = true } arc-swap = { version = "1", optional = true }
+7 -2
View File
@@ -29,8 +29,13 @@ stream.
- **Content-driven pages** (`src/content.rs`, `src/app.rs`): YAML - **Content-driven pages** (`src/content.rs`, `src/app.rs`): YAML
loaded from Gitea at boot and hot-swapped on a NATS reload signal; loaded from Gitea at boot and hot-swapped on a NATS reload signal;
a bad push keeps the last-good content serving. A page's `id` is a bad push keeps the last-good content serving. The `questions/`
its URL; `qualifies` gates it to a Kanidm group. tree IS the router — file paths become URLs, `_section.yaml`
applies criteria to a whole directory, `[name].yaml` pages serve
any `/dir/<value>` with the segment fed into resource keys, and
`requires_chain` gates a page on verifiable answer provenance next
to `qualifies`' Kanidm-group identity gate (see
`docs/design/filesystem-routes.md`).
- **State machines as content** (`src/aggregates/`): `aggregates.yaml` - **State machines as content** (`src/aggregates/`): `aggregates.yaml`
declares each bucket's states and legal transitions; the engine declares each bucket's states and legal transitions; the engine
replays a record's event history and refuses undeclared moves, with replays a record's event history and refuses undeclared moves, with
+7 -1
View File
@@ -1,6 +1,12 @@
# Filesystem routes, sections, and where chains fit # Filesystem routes, sections, and where chains fit
Status: proposal (researched 2026-08-24, nothing implemented). Status: implemented in v0.2.0 (2026-08-24) — all three phases, with
one deviation: dynamic-page params substitute into resource keys via
server-side `resolve_question` at lookup time (get_question,
find_feature, submit_answer all resolve concrete paths), so no param
threading exists client-side. The user-facing routing contract is
documented in uhhm/questions' README ("Routing: the tree is the
router"); this file stays as the design rationale.
## What exists today, precisely ## What exists today, precisely
+12 -7
View File
@@ -89,6 +89,18 @@ fn QuestionPage() -> impl IntoView {
let site = Resource::new(|| (), |_| get_site()); let site = Resource::new(|| (), |_| get_site());
view! { view! {
// The content-declared site title lives OUTSIDE the question
// Suspense: tied to the per-page resource it unmounted on every
// SPA navigation and lost the leptos_meta race to App's
// compile-time SITE_NAME fallback (redoal.com flashing to
// "uhhm" on nav - and staying there). Out here it mounts once
// and covers every branch, NotFound included.
{move || {
site.get()
.and_then(|r| r.ok())
.and_then(|s| s.title)
.map(|t| view! { <Title text=t/> })
}}
<Suspense fallback=|| { <Suspense fallback=|| {
view! { view! {
<main class="loading"> <main class="loading">
@@ -109,13 +121,6 @@ fn QuestionPage() -> impl IntoView {
Ok(Some(page)) => { Ok(Some(page)) => {
let current = user_res.and_then(|r| r.ok()).flatten(); let current = user_res.and_then(|r| r.ok()).flatten();
view! { view! {
// A second <Title> outranks App's
// SITE_NAME fallback only when content
// actually declares one.
{site_cfg
.title
.clone()
.map(|t| view! { <Title text=t/> })}
<QuestionView <QuestionView
question=page.question question=page.question
chain_gate=page.chain_gate chain_gate=page.chain_gate
+27 -2
View File
@@ -143,7 +143,22 @@ async fn main() -> anyhow::Result<()> {
.route("/upload", post(upload::upload)) .route("/upload", post(upload::upload))
.route("/gitea-repo", get(content::gitea_repo_handler)) .route("/gitea-repo", get(content::gitea_repo_handler))
.route("/automation/kv/{bucket}", get(content::automation_kv_handler)) .route("/automation/kv/{bucket}", get(content::automation_kv_handler))
.nest_service("/pkg", ServeDir::new(pkg_dir)) // no-cache = "revalidate before reuse", not "don't cache":
// pkg files keep the same names across releases (portal.js,
// portal.wasm), and without this browsers heuristically cache
// them - a stale wasm from the previous release then talks to
// a server whose server-fn wire format has moved on and every
// page renders as its error branch. A 304 per load is the
// price of never shipping that skew again.
.nest_service(
"/pkg",
tower::ServiceBuilder::new()
.layer(tower_http::set_header::SetResponseHeaderLayer::overriding(
axum::http::header::CACHE_CONTROL,
axum::http::HeaderValue::from_static("no-cache"),
))
.service(ServeDir::new(pkg_dir)),
)
.nest_service("/fonts", ServeDir::new(fonts_dir)) .nest_service("/fonts", ServeDir::new(fonts_dir))
.route_service("/favicon-light.svg", ServeFile::new(favicon_light_path)) .route_service("/favicon-light.svg", ServeFile::new(favicon_light_path))
.route_service("/favicon-dark.svg", ServeFile::new(favicon_dark_path)) .route_service("/favicon-dark.svg", ServeFile::new(favicon_dark_path))
@@ -152,7 +167,17 @@ async fn main() -> anyhow::Result<()> {
"/swiper-element-bundle.min.js", "/swiper-element-bundle.min.js",
ServeFile::new(swiper_path), ServeFile::new(swiper_path),
) )
.route_service("/yes.js", ServeFile::new(yes_path)) // Same skew concern as /pkg: the wasm's raw_module import and
// the hero's inline script both load this by fixed name.
.route_service(
"/yes.js",
tower::ServiceBuilder::new()
.layer(tower_http::set_header::SetResponseHeaderLayer::overriding(
axum::http::header::CACHE_CONTROL,
axum::http::HeaderValue::from_static("no-cache"),
))
.service(ServeFile::new(yes_path)),
)
.leptos_routes_with_context( .leptos_routes_with_context(
&state, &state,
routes, routes,