Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0d6cdb8a00 | ||
|
|
b2342f074b | ||
|
|
7429f1b9e1 | ||
|
|
a0305282ef | ||
|
|
c67f6f1a59 |
Generated
+1
-1
@@ -2948,7 +2948,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "portal"
|
||||
version = "0.2.0"
|
||||
version = "0.2.2"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"arc-swap",
|
||||
|
||||
+2
-2
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "portal"
|
||||
version = "0.2.0"
|
||||
version = "0.2.2"
|
||||
edition = "2021"
|
||||
|
||||
[lib]
|
||||
@@ -19,7 +19,7 @@ axum = { version = "0.8", features = ["multipart"], optional = true }
|
||||
aws-sdk-s3 = { version = "1", optional = true }
|
||||
tokio = { version = "1", features = ["rt-multi-thread", "macros", "signal"], 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 }
|
||||
async-nats = { version = "0.38", optional = true }
|
||||
arc-swap = { version = "1", optional = true }
|
||||
|
||||
@@ -29,8 +29,13 @@ stream.
|
||||
|
||||
- **Content-driven pages** (`src/content.rs`, `src/app.rs`): YAML
|
||||
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
|
||||
its URL; `qualifies` gates it to a Kanidm group.
|
||||
a bad push keeps the last-good content serving. The `questions/`
|
||||
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`
|
||||
declares each bucket's states and legal transitions; the engine
|
||||
replays a record's event history and refuses undeclared moves, with
|
||||
|
||||
@@ -1,6 +1,12 @@
|
||||
# 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
|
||||
|
||||
|
||||
+27
-9
@@ -54,9 +54,32 @@ pub fn shell(options: LeptosOptions) -> impl IntoView {
|
||||
pub fn App() -> impl IntoView {
|
||||
provide_meta_context();
|
||||
|
||||
// ONE site resource and ONE <Title> for the whole app, both living
|
||||
// outside the router. Two dueling Title components (a static
|
||||
// SITE_NAME fallback here + a per-page override) turned every SPA
|
||||
// navigation into a remount race that the compile-time fallback
|
||||
// kept winning - redoal.com's tab flipped to "uhhm" on the first
|
||||
// client-side nav. App never remounts, so this Title never
|
||||
// unmounts; the Suspense makes SSR await the resolved title so the
|
||||
// served <head> is right too. Pages read the same resource via
|
||||
// context instead of fetching their own copy.
|
||||
let site = Resource::new(|| (), |_| get_site());
|
||||
provide_context(site);
|
||||
|
||||
view! {
|
||||
<Stylesheet id="leptos" href="/pkg/portal.css"/>
|
||||
<Title text=SITE_NAME/>
|
||||
<Suspense fallback=|| ()>
|
||||
{move || {
|
||||
site.get()
|
||||
.map(|res| {
|
||||
let title = res
|
||||
.ok()
|
||||
.and_then(|s| s.title)
|
||||
.unwrap_or_else(|| SITE_NAME.to_string());
|
||||
view! { <Title text=title/> }
|
||||
})
|
||||
}}
|
||||
</Suspense>
|
||||
<Router>
|
||||
<Routes fallback=|| view! { <NotFound/> }>
|
||||
<Route path=path!("") view=QuestionPage/>
|
||||
@@ -86,7 +109,9 @@ fn QuestionPage() -> impl IntoView {
|
||||
|(path, chain)| get_question(path, chain),
|
||||
);
|
||||
let user = Resource::new(|| (), |_| current_user());
|
||||
let site = Resource::new(|| (), |_| get_site());
|
||||
// App's shared site resource (see App) - one fetch per session,
|
||||
// and the Title tied to it survives navigation.
|
||||
let site = expect_context::<Resource<Result<SiteConfig, ServerFnError>>>();
|
||||
|
||||
view! {
|
||||
<Suspense fallback=|| {
|
||||
@@ -109,13 +134,6 @@ fn QuestionPage() -> impl IntoView {
|
||||
Ok(Some(page)) => {
|
||||
let current = user_res.and_then(|r| r.ok()).flatten();
|
||||
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
|
||||
question=page.question
|
||||
chain_gate=page.chain_gate
|
||||
|
||||
+27
-2
@@ -143,7 +143,22 @@ async fn main() -> anyhow::Result<()> {
|
||||
.route("/upload", post(upload::upload))
|
||||
.route("/gitea-repo", get(content::gitea_repo_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))
|
||||
.route_service("/favicon-light.svg", ServeFile::new(favicon_light_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",
|
||||
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(
|
||||
&state,
|
||||
routes,
|
||||
|
||||
Reference in New Issue
Block a user