//! 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>>, } 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 = { 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::(&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) -> axum::Json { axum::Json(gather(&state).await) }