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 {