Mercurial > lbo > hg > analyrics
changeset 15:d5724ebeefd3
Enable basic plot interface
author | Lewin Bormann <lbo@spheniscida.de> |
---|---|
date | Mon, 11 Jul 2022 18:22:59 -0700 |
parents | 195d92c0e106 |
children | be0b8268c936 |
files | assets/index.html.hbs src/main.rs |
diffstat | 2 files changed, 148 insertions(+), 39 deletions(-) [+] |
line wrap: on
line diff
--- a/assets/index.html.hbs Mon Jul 11 12:44:26 2022 -0700 +++ b/assets/index.html.hbs Mon Jul 11 18:22:59 2022 -0700 @@ -2,6 +2,7 @@ <html> <head> <title>Analyrics Login</title> + <script src="/static/chart.min.js" type="application/javascript"></script> <style> #logo { color: #22bb22; font-size: 20pt; font-style: bold; } @@ -10,9 +11,14 @@ #logout { float: right; padding: 3pt; } #flashtext { } #flash { text-align: center; border-style: solid; border-color: #22aa22; margin-left: 30%; margin-right: 30%; } + #errortext { } + #error { text-align: center; border-style: solid; border-color: #aa2222; margin-left: 30%; margin-right: 30%; } - .plotrow { border-style: solid; border-color: blue; } - .plotframe { border-style: solid; border-color: green; height: 10em; display: inline-block; margin: 5pt; } + .plotrow { border-style: solid; border-color: blue; text-align: center; } + .plotframe { border-style: solid; border-color: green; display: inline-block; margin: 5pt; } + + .fullwidth { width: 95%; } + .halfwidth { width: 47%; } </style> </head> @@ -29,12 +35,27 @@ {{/if}} </div> {{#if flash}}<div id="flash"><span id="flashtext">{{flash}}<span></div>{{/if}} + {{#if error}}<div id="error"><span id="errortext">{{error}}</span></div>{{/if}} <!-- Plots --> - <div class="plotrow"> - <div class="plotframe"> </div> - <div class="plotframe"> </div> + <div class="plotrow row1"> + <div class="plotframe fullwidth"> + <canvas id="visitsAndSessions" height="100"></canvas> + </div> + </div> + <div class="plotrow row2"> + <div class="plotframe halfwidth"> </div> + <div class="plotframe halfwidth"> </div> </div> + <script> + let plots = {"visitsAndSessions": {{{ chartconfig.visitsAndSessions }}} }; + + Object.keys(plots).forEach((cv) => { + const ctx = document.getElementById(cv).getContext('2d'); + const myChart = new Chart(ctx, plots[cv]); + }); + </script> + </body> </html>
--- a/src/main.rs Mon Jul 11 12:44:26 2022 -0700 +++ b/src/main.rs Mon Jul 11 18:22:59 2022 -0700 @@ -1,9 +1,11 @@ -use anyhow::{self, Error}; +use anyhow::{self, Context, Error}; use either::Either; use log::{debug, error, info, warn, Level}; +use time::{Duration, OffsetDateTime}; use rocket::form::Form; use rocket::fs::NamedFile; +use rocket::futures::StreamExt; use rocket::http::{Cookie, CookieJar, HeaderMap, Status}; use rocket::request::{self, FlashMessage, FromRequest, Outcome, Request}; use rocket::response::{self, Flash, Redirect, Responder}; @@ -15,8 +17,10 @@ use rocket_dyn_templates::{context, Template}; +use std::collections::HashMap; use std::net::IpAddr; use std::path::Path; +use std::time::Instant; #[cfg(feature = "sqlite")] type DBType = Sqlite; @@ -130,6 +134,50 @@ .await?; Ok(()) } + + async fn query_visits_sessions_counts( + &mut self, + from: OffsetDateTime, + to: OffsetDateTime, + ) -> Result<(Vec<String>, Vec<u32>, Vec<u32>), Error> { + let mut results = sqlx::query( + r#" +SELECT DATE(atime, 'unixepoch') AS rqdate, COUNT(requestlog.id) AS rqcount, sesscount +FROM requestlog +JOIN ( + SELECT DATE(start, 'unixepoch') AS sessdate, COUNT(*) AS sesscount + FROM sessions GROUP BY sessdate) +AS sc ON (rqdate = sessdate) +WHERE atime > ? AND atime < ? +GROUP BY rqdate +ORDER BY rqdate ASC;"#, + ) + .bind(from.unix_timestamp()) + .bind(to.unix_timestamp()) + .fetch(&mut *self.0); + + // Result table: date / visit count / session count + let mut dates = vec![]; //Vec::<String>::with_capacity(results.len()); + let mut visits = vec![]; //Vec::<u32>::with_capacity(results.len()); + let mut sessions = vec![]; //Vec::<u32>::with_capacity(results.len()); + + loop { + match results.next().await { + Some(Ok(row)) => { + dates.push(row.get(0)); + visits.push(row.get(1)); + sessions.push(row.get(2)); + } + None => break, + Some(Err(e)) => { + error!("Error querying visits/sessions: {}", e); + return Err(e).context("query visits/session counts"); + } + } + } + + Ok((dates, visits, sessions)) + } } const USER_ID_COOKIE_KEY: &str = "user_id"; @@ -203,7 +251,10 @@ if let Some(cookie) = cookies.get_private(USER_ID_COOKIE_KEY) { cookies.remove_private(cookie); } - Flash::success(Redirect::to(rocket::uri!("/")), format!("Logged out. Goodbye!")) + Flash::success( + Redirect::to(rocket::uri!("/")), + format!("Logged out. Goodbye!"), + ) } #[derive(rocket::FromForm)] @@ -240,15 +291,82 @@ } } +fn create_chart<S1: Serialize, S2: Serialize>( + labels: Vec<S1>, + values: Vec<(String, Vec<S2>)>, + typ: String, +) -> Value { + let colors = vec![ + "red", "blue", "orange", "green", "gray", "purple", "black", "brown", + ]; + if values.len() > colors.len() { + error!("Not enough colors for line chart!"); + } + let datasets: Vec<Value> = values + .iter() + .zip(colors) + .map(|((name, val), col)| json!({"label": name, "data": val, "borderColor": col})) + .collect(); + let inner = json!({ + "type": typ, + "data": { + "labels": labels, + "datasets": datasets, + }, + "options": { + "scales": { "y": { "beginAtZero": true }}, + }, + }); + inner +} + #[rocket::get("/", rank = 1)] -async fn route_index_loggedin(lig: LoggedInGuard, flash: Option<FlashMessage<'_>>) -> Template { +async fn route_index_loggedin( + mut conn: Connection<LogsDB>, + lig: LoggedInGuard, + flash: Option<FlashMessage<'_>>, +) -> Template { let f; if let Some(ref flash) = flash { f = Some(format!("{}: {}", flash.kind(), flash.message())); } else { f = None; } - Template::render("index", context![loggedin: true, username: lig.0, flash: f]) + + let mut charts = HashMap::<String, String>::new(); + + // TODO: Make configurable + let begin = OffsetDateTime::now_utc() - Duration::days(30); + let end = OffsetDateTime::now_utc(); + match LogsDBSession(&mut conn) + .query_visits_sessions_counts(begin, end) + .await + { + Ok((dates, visits, sessions)) => { + info!( + "Successfully queried visits/sessions: {:?}, {:?}", + visits, sessions + ); + let vissess = create_chart( + dates, + vec![("Visits".into(), visits), ("Sessions".into(), sessions)], + "line".into(), + ) + .to_string(); + Template::render( + "index", + context![ + loggedin: true, + username: lig.0, + flash: f, + chartconfig: context![ visitsAndSessions: vissess ]], + ) + } + Err(e) => Template::render( + "index", + context![loggedin: true, username: lig.0, flash: f, chartconfig: context![], error: e.to_string()], + ), + } } #[rocket::get("/", rank = 2)] @@ -367,36 +485,6 @@ } } } - -fn create_chart<S1: Serialize, S2: Serialize>( - labels: Vec<S1>, - values: Vec<(String, Vec<S2>)>, - typ: String, -) -> Value { - let colors = vec![ - "red", "blue", "orange", "green", "gray", "purple", "black", "brown", - ]; - if values.len() > colors.len() { - error!("Not enough colors for line chart!"); - } - let datasets: Vec<Value> = values - .iter() - .zip(colors) - .map(|((name, val), col)| json!({"label": name, "data": val, "borderColor": col})) - .collect(); - let inner = json!({ - "type": typ, - "data": { - "labels": labels, - "datasets": datasets, - }, - "options": { - "scales": { "y": { "beginAtZero": true }}, - }, - }); - inner -} - #[derive(rocket::serde::Deserialize)] #[serde(crate = "rocket::serde")] struct CustomConfig {