···55 routing::get,
66};
77use color_eyre::eyre::Result;
88-use serde_json::json;
88+use serde_json::{Value, json};
99use tokio::net::TcpListener;
10101111struct AppError(color_eyre::eyre::Error);
···2222 }
2323}
24242525-fn get_counts(db: &DB) -> Result<Json<serde_json::Value>> {
2525+fn get_counts(db: &DB) -> Result<Json<Value>> {
2626 Ok(db
2727 .handle()?
2828 .prepare("SELECT count(DISTINCT did) as dids, count(*) as operations FROM operations")?
···3434 })?)
3535}
36363737+/// Returns ~100 cumulative datapoints spanning the entire recorded history.
3838+///
3939+/// The full time range is divided into 100 equal-width buckets; each point
4040+/// reports the cumulative distinct-DID count and total operation count up to
4141+/// that moment. When the table is empty the endpoint returns an empty array.
4242+///
4343+/// (this SQL is unreviewed clanker output, this type of data work is above my pay grade rn)
4444+fn get_counts_history(db: &DB) -> Result<Json<Vec<Value>>> {
4545+ let conn = db.handle()?;
4646+ let mut stmt = conn.prepare(
4747+ r#"
4848+ WITH
4949+ time_range AS (
5050+ SELECT
5151+ MIN(created_at) AS start_time,
5252+ MAX(created_at) AS end_time,
5353+ (epoch(MAX(created_at)) - epoch(MIN(created_at)))
5454+ / 99.0 * INTERVAL '1' SECOND AS bucket_interval
5555+ FROM operations
5656+ HAVING COUNT(*) > 0
5757+ ),
5858+ -- Bucket every operation; flag the first occurrence of each DID
5959+ bucketed AS (
6060+ SELECT
6161+ time_bucket(tr.bucket_interval, created_at, tr.start_time) AS bucket_time,
6262+ ROW_NUMBER() OVER (PARTITION BY did ORDER BY created_at) = 1 AS is_new_did
6363+ FROM operations, time_range tr
6464+ ),
6565+ -- Per-bucket deltas for both metrics in one pass
6666+ bucket_stats AS (
6767+ SELECT
6868+ bucket_time,
6969+ COUNT(*) FILTER (WHERE is_new_did) AS new_dids,
7070+ COUNT(*) AS new_ops
7171+ FROM bucketed
7272+ GROUP BY ALL
7373+ ),
7474+ -- Full 100-point series so empty buckets are preserved
7575+ all_buckets AS (
7676+ SELECT range AS bucket_time
7777+ FROM time_range,
7878+ range(time_range.start_time,
7979+ time_range.end_time + time_range.bucket_interval,
8080+ time_range.bucket_interval)
8181+ LIMIT 100
8282+ )
8383+ SELECT
8484+ strftime(bt.bucket_time, '%Y-%m-%dT%H:%M:%SZ') AS date,
8585+ SUM(COALESCE(bs.new_dids, 0)) OVER (
8686+ ORDER BY bt.bucket_time
8787+ ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW
8888+ ) AS dids,
8989+ SUM(COALESCE(bs.new_ops, 0)) OVER (
9090+ ORDER BY bt.bucket_time
9191+ ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW
9292+ ) AS operations
9393+ FROM all_buckets bt
9494+ LEFT JOIN bucket_stats bs ON bs.bucket_time = bt.bucket_time
9595+ ORDER BY bt.bucket_time
9696+ "#,
9797+ )?;
9898+9999+ let rows = stmt.query_map([], |row| {
100100+ let date: String = row.get(0)?;
101101+ let dids: i64 = row.get(1)?;
102102+ let operations: i64 = row.get(2)?;
103103+ Ok((date, dids, operations))
104104+ })?;
105105+106106+ let mut results: Vec<Value> = Vec::with_capacity(100);
107107+ for row in rows {
108108+ let (date, dids, operations) = row?;
109109+ results.push(json!({
110110+ "date": date,
111111+ "dids": dids,
112112+ "operations": operations,
113113+ }));
114114+ }
115115+116116+ Ok(Json(results))
117117+}
118118+37119pub async fn run_api(db: DB) -> Result<()> {
3838- let app = Router::new().route(
3939- "/v0/counts",
4040- get({
4141- let db = db.clone();
4242- move || async move { get_counts(&db).map_err(AppError::from) }
4343- }),
4444- );
120120+ let app = Router::new()
121121+ .route(
122122+ "/v0/counts",
123123+ get({
124124+ let db = db.clone();
125125+ move || async move { get_counts(&db).map_err(AppError::from) }
126126+ }),
127127+ )
128128+ .route(
129129+ "/v0/counts/history",
130130+ get({
131131+ let db = db.clone();
132132+ move || async move { get_counts_history(&db).map_err(AppError::from) }
133133+ }),
134134+ );
4513546136 let listener = TcpListener::bind("127.0.0.1:7712").await?;
47137 println!("HTTP server listening on http://127.0.0.1:7712");