Any text a person reads may be a list, one entry per storyline, every list the same length. A visitor is given one storyline for the whole site and reads it on every page and in every mail about their case. Records and events carry the storyline; GET /report gives counts per bucket, storyline and state, and nothing else. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GT37Z1Xtfd9pUuQtMTg6Yt
54 lines
2.2 KiB
Rust
54 lines
2.2 KiB
Rust
//! What the storylines did, as numbers and nothing else.
|
|
//!
|
|
//! `GET /report` answers with a count per bucket, per storyline, per
|
|
//! state: how many cases each storyline brought in and where they got
|
|
//! to. That is the whole of what the storylines are compared on. No
|
|
//! record, no field, no address, no time leaves the site: a storyline
|
|
//! is assigned evenly, so the counts are the comparison, and the
|
|
//! trainer (`portal-trainer report`) reads them to say which words
|
|
//! worked. Public, since a count is what a public list already shows.
|
|
#![cfg(feature = "ssr")]
|
|
|
|
use std::collections::BTreeMap;
|
|
|
|
use serde::{Deserialize, Serialize};
|
|
|
|
/// The report: the site's storylines, and per bucket the count of
|
|
/// records by storyline and state.
|
|
#[derive(Clone, Debug, Default, Serialize, Deserialize)]
|
|
pub struct Report {
|
|
pub storylines: usize,
|
|
/// bucket -> storyline (as a string, JSON's way) -> state -> count
|
|
pub buckets: BTreeMap<String, BTreeMap<String, BTreeMap<String, u64>>>,
|
|
}
|
|
|
|
pub async fn gather(state: &crate::server::AppState) -> Report {
|
|
let mut report = Report { storylines: state.questions.load().arity(), buckets: BTreeMap::new() };
|
|
let aggregates = state.aggregates.load();
|
|
for bucket in aggregates.keys() {
|
|
// The runtime's own buckets are not the site's offers.
|
|
if bucket.starts_with("portal_") {
|
|
continue;
|
|
}
|
|
let Ok(store) = state.jetstream.get_key_value(bucket).await else { continue };
|
|
let keys: Vec<String> = {
|
|
use futures::TryStreamExt;
|
|
match store.keys().await {
|
|
Ok(keys) => keys.try_collect().await.unwrap_or_default(),
|
|
Err(_) => continue,
|
|
}
|
|
};
|
|
let counts = report.buckets.entry(bucket.clone()).or_default();
|
|
for key in keys {
|
|
let Ok(Some(bytes)) = store.get(&key).await else { continue };
|
|
let Ok(answer) = serde_json::from_slice::<crate::answers::Answer>(&bytes) else { continue };
|
|
*counts.entry(answer.storyline.to_string()).or_default().entry(answer.state).or_default() += 1;
|
|
}
|
|
}
|
|
report
|
|
}
|
|
|
|
pub async fn handler(axum::extract::State(state): axum::extract::State<crate::server::AppState>) -> axum::Json<Report> {
|
|
axum::Json(gather(&state).await)
|
|
}
|