Filesystem routes, sections, dynamic segments; instant YES hero
Test / test (push) Successful in 23s
Test / test (push) Successful in 23s
The questions/ tree is the router now: ids derive from file paths
(index.yaml names its directory; explicit id still wins for legacy
content), actions and requires_chain accept relative refs, nested
non-index files infer followup, and _section.yaml applies qualifies/
requires_chain/responsible to everything under its directory. Dynamic
[name].yaml pages serve any /dir/<value> with the segment substituted
into {name} resource-key placeholders; submissions index their chain
node in a portal_chains KV so requires_chain pages can verify a
visitor's ?chain= lineage actually ends at the required question.
Loading uses one recursive git-trees call; question_lint walks
subdirectories the same way. Implements docs/design/filesystem-routes.md.
Also: the YES hero now starts at HTML parse time via an inline module
script (yes.js moved to public/ for a stable /yes.js the wasm binding
raw_module-imports too - snippet paths are per-build-hashed), with
hydration adopting the running instance; and both gesture containers
reserve their box in CSS so mounting doesn't shift content.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5
parent
b737e0a7e7
commit
452ea88fbf
@@ -9,6 +9,60 @@ use sha2::{Digest, Sha256};
|
||||
/// submitted responses, and a timestamp. Parents are sorted first so the
|
||||
/// hash doesn't depend on the order multiple parents happened to arrive
|
||||
/// in.
|
||||
/// Where submitted chain nodes are indexed - hash → which question was
|
||||
/// answered. Small on purpose (no responses), and shared by every
|
||||
/// portal instance on the JetStream (hashes are globally unique, so
|
||||
/// cross-site collisions can't happen). This is what lets
|
||||
/// `requires_chain` pages verify a visitor's `?chain=` actually ends
|
||||
/// at the question they claim to have answered.
|
||||
pub const CHAIN_BUCKET: &str = "portal_chains";
|
||||
|
||||
#[derive(serde::Serialize, serde::Deserialize)]
|
||||
pub struct ChainNode {
|
||||
pub question_id: String,
|
||||
pub timestamp_ms: i64,
|
||||
}
|
||||
|
||||
/// Indexes one submitted node. Best-effort by design (the caller logs
|
||||
/// and continues): the NATS event is the durable record, this is a
|
||||
/// lookup convenience.
|
||||
pub async fn record_node(
|
||||
js: &async_nats::jetstream::Context,
|
||||
chain_hash: &str,
|
||||
question_id: &str,
|
||||
timestamp_ms: i64,
|
||||
) -> anyhow::Result<()> {
|
||||
let store = match js.get_key_value(CHAIN_BUCKET).await {
|
||||
Ok(store) => store,
|
||||
Err(_) => {
|
||||
js.create_key_value(async_nats::jetstream::kv::Config {
|
||||
bucket: CHAIN_BUCKET.to_string(),
|
||||
..Default::default()
|
||||
})
|
||||
.await?
|
||||
}
|
||||
};
|
||||
let node = ChainNode {
|
||||
question_id: question_id.to_string(),
|
||||
timestamp_ms,
|
||||
};
|
||||
store.put(chain_hash, serde_json::to_vec(&node)?.into()).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Looks a chain hash up - `None` covers both "no such node" and
|
||||
/// "bucket not created yet" (no submissions anywhere), which read the
|
||||
/// same to a `requires_chain` check: the claimed lineage can't be
|
||||
/// verified, so the gate stays shut.
|
||||
pub async fn lookup_node(
|
||||
js: &async_nats::jetstream::Context,
|
||||
chain_hash: &str,
|
||||
) -> Option<ChainNode> {
|
||||
let store = js.get_key_value(CHAIN_BUCKET).await.ok()?;
|
||||
let bytes = store.get(chain_hash).await.ok()??;
|
||||
serde_json::from_slice(&bytes).ok()
|
||||
}
|
||||
|
||||
pub fn hash_node(
|
||||
question_id: &str,
|
||||
parent_hashes: &[String],
|
||||
|
||||
Reference in New Issue
Block a user