From daee7e627774c81eb901ef79dde2ae606e0799ec Mon Sep 17 00:00:00 2001 From: 4cuck <40539855+4cuck@users.noreply.github.com> Date: Thu, 31 Aug 2023 03:28:12 +0200 Subject: [PATCH] uptime-graph details, hover, history and % uptime overall --- entities/src/state/mod.rs | 5 ++ scanner/src/cache_update.rs | 60 +++++++++++++++++++++ scanner/src/lib.rs | 24 +++++++++ server/src/lib.rs | 2 +- server/templates/instances.html.j2 | 84 +++++++++++++----------------- 5 files changed, 127 insertions(+), 48 deletions(-) diff --git a/entities/src/state/mod.rs b/entities/src/state/mod.rs index 8690894..a1adf2c 100644 --- a/entities/src/state/mod.rs +++ b/entities/src/state/mod.rs @@ -47,5 +47,10 @@ pub struct CacheHost { pub is_latest_version: bool, /// Whether this host is known to be bad (ip blocking) pub is_bad_host: bool, + /// Country from the wiki pub country: String, + /// Last health checks time formatted, healthy + pub recent_checks: Vec<(String,bool)>, + /// Percentage of healthy checks since first seen + pub healthy_percentage_overall: u8, } diff --git a/scanner/src/cache_update.rs b/scanner/src/cache_update.rs index 908bfe7..7718705 100644 --- a/scanner/src/cache_update.rs +++ b/scanner/src/cache_update.rs @@ -24,6 +24,13 @@ pub struct HostStats { total: u32, } +#[derive(Debug, FromQueryResult)] +pub struct HostHealthyPercentage { + host: i32, + /// Percentage + healthy: u8, +} + #[derive(Debug, Default)] struct LastPings { avg: Option, @@ -84,6 +91,8 @@ impl Scanner { .query_pings(time_now - self.inner.config.ping_range) .await?; + let mut healthy_percentage_total = self.query_healthy_percentage().await?; + let mut host_statistics = Vec::with_capacity(hosts.len()); let default_health_check = LatestCheck::default(); for host in hosts { @@ -145,6 +154,8 @@ impl Scanner { version_url: host.version_url, is_bad_host, country: host.country, + healthy_percentage_overall: healthy_percentage_total.remove(&host.id).unwrap_or(0), + recent_checks: self.query_latest_health_checks(22,host.id).await?, }) } host_statistics.sort_unstable_by_key(|k| k.points); @@ -304,4 +315,53 @@ impl Scanner { let stats: HashMap<_, _> = stats.into_iter().map(|v| (v.host, v)).collect(); Ok(stats) } + + /// Query total up percentage for all hosts + async fn query_healthy_percentage( + &self, + ) -> Result> { + let stats: Vec = HostHealthyPercentage::find_by_statement(Statement::from_sql_and_values( + DbBackend::Sqlite, + r#"SELECT u.host, CAST(AVG(healthy) * 100 as INT) as healthy FROM health_check u + JOIN host h ON h.id = u.host + WHERE h.enabled = true + GROUP BY u.host"#, + [], + )) + .all(&self.inner.db) + .await?; + let stats: HashMap<_, _> = stats.into_iter().map(|v| (v.host, v.healthy)).collect(); + Ok(stats) + } + + /// Query latest health checks for the red/green only graph. Returns latest $amount in ascending order and formatted time. + async fn query_latest_health_checks( + &self, + // How many to retrieve + amount: i32, + host: i32, + ) -> Result> { + #[derive(Debug, FromQueryResult)] + pub struct HostHealthCheck { + healthy: bool, + time: i64, + } + let health_checks: Vec = HostHealthCheck::find_by_statement(Statement::from_sql_and_values( + DbBackend::Sqlite, + r#"SELECT healthy, time FROM health_check u + JOIN host h ON h.id = u.host + WHERE h.enabled = true AND host = $1 + ORDER BY time DESC + LIMIT $2"#, + [host.into(), amount.into()], + )) + .all(&self.inner.db) + .await?; + // transform to correct time format + let health_checks: Vec<_> = health_checks.into_iter().rev().map(|entry|{ + let time = Utc.timestamp_opt(entry.time, 0).unwrap(); + (time.format("%Y.%m.%d %H:%M").to_string(),entry.healthy) + }).collect(); + Ok(health_checks) + } } diff --git a/scanner/src/lib.rs b/scanner/src/lib.rs index a96bcaa..5ac2fbc 100644 --- a/scanner/src/lib.rs +++ b/scanner/src/lib.rs @@ -412,10 +412,13 @@ impl Scanner { #[cfg(test)] mod test { + use super::*; + use chrono::Duration; use entities::state::scanner::Config; use sea_orm::{ConnectOptions, Database}; use tokio::{fs::File, io::AsyncWriteExt}; + use entities::health_check; async fn db_init() -> DatabaseConnection { Database::connect(ConnectOptions::new( @@ -425,6 +428,27 @@ mod test { .unwrap() } + // only for generating fake data + // still requires copying over the DB for running on it + #[tokio::test(flavor = "multi_thread", worker_threads = 1)] + #[ignore] + async fn setup_test_data() { + let db = db_init().await; + let time: chrono::DateTime = Utc::now(); + for v in 0..365 { + let entry_time = time.checked_sub_signed(Duration::minutes(15*v)).unwrap(); + health_check::ActiveModel { + time: ActiveValue::Set(entry_time.timestamp()), + host: ActiveValue::Set(208692), + resp_time: ActiveValue::Set(Some(12)), + healthy: ActiveValue::Set(v % 2 == 0), + response_code: ActiveValue::Set(Some(200)), + } + .insert(&db) + .await.unwrap(); + } + } + #[tokio::test(flavor = "multi_thread", worker_threads = 1)] #[ignore] async fn update_instacelist() { diff --git a/server/src/lib.rs b/server/src/lib.rs index db48b94..9fcf997 100644 --- a/server/src/lib.rs +++ b/server/src/lib.rs @@ -64,7 +64,7 @@ pub async fn start( .layer(cors_policy(&config.site_url)) .layer(SetResponseHeaderLayer::overriding( header::CONTENT_SECURITY_POLICY, - "default-src 'self'; child-src 'none'; script-src 'self'; style-src 'self';" + "default-src 'self'; child-src 'none'; script-src 'self'; style-src 'self' 'unsafe-inline';" .parse::() .expect("Invalid CSP header value"), )), diff --git a/server/templates/instances.html.j2 b/server/templates/instances.html.j2 index aa0ef37..6024487 100644 --- a/server/templates/instances.html.j2 +++ b/server/templates/instances.html.j2 @@ -4,6 +4,14 @@ + Nitter Instance Health @@ -20,8 +28,9 @@ Instance Country Healthy - Response Times + Health History Average (ms) + All Healthy % RSS Version Points @@ -40,57 +49,38 @@ {%- else -%} ❌ {%- endif -%} - - {%- if host.recent_pings|length > 0 -%} - {%- set height = 34 -%} - {%- set width = 100 -%} - {%- set amount = host.recent_pings|length -%} - {%- set width_bar = 5 -%} - {%- set offset = 5 -%} - {%- set error_size = height / 3 %} - {%- if host.ping_max and host.ping_min -%} - {# only one entry, so max = min => step_size would be 0 #} - {%- if host.ping_max == host.ping_min -%} - {%- set step_size = 5/(height-offset) -%} - {%- else -%} - {%- set step_size = (host.ping_max - host.ping_min)/(height-offset) -%} - {%- endif -%} - {%- else %} - {# no valid entries, only nulls #} - {%- set step_size = 5/(height-offset) -%} - {%- endif -%} - {# - - #} - {%- if host.ping_min -%} - - {%- for ping in host.recent_pings -%} - {%- if ping -%} - {%- set title = "Healthy" -%} - {%- set color_bar = "#2fcc66" -%} - {%- else -%} - {%- set title = "Unhealthy" -%} - {%- set color_bar = "#ff6225" -%} - {%- endif -%} - {{title}} - {%- endfor -%} - - {%- else -%} - {# #} - {%- if host.last_healthy -%} - Last seen healthy: {{host.last_healthy | truncate(length=16, end="") | replace(from="-", to=".") | replace(from="T", to=" ")}} - UTC - {%- else -%} - Never seen healthy. - {%- endif -%} - {%- endif -%} + + {%- set height = 28 -%} + {%- set width = 110 -%} + {%- set width_bar = 5 -%} + {%- set offset = 5 -%} + {%- if host.recent_checks -%} + + {%- for check in host.recent_checks -%} + {%- if check.1 -%} + {%- set title = "Healthy " ~ check.0 -%} + {%- set color_bar = "#2fcc66" -%} + {%- else -%} + {%- set title = "Unhealthy " ~ check.0 -%} + {%- set color_bar = "#ff6225" -%} + {%- endif -%} + {{title}} + {%- endfor -%} + {%- else -%} - No recent data! + {# #} + {%- if host.last_healthy -%} + Last seen healthy: {{host.last_healthy | truncate(length=16, end="") | replace(from="-", to=".") | replace(from="T", to=" ")}} + UTC + {%- else -%} + Never seen healthy. + {%- endif -%} {%- endif -%} - {%- if host.ping_min -%} + {%- if host.recent_checks -%} {{host.ping_avg}} {% endif %} + {{host.healthy_percentage_overall}} {% if host.rss -%} ✅ {%- else -%} ❌ {%- endif -%} {%- if host.healthy and not host.version -%} {%- set version = "Dmissing" -%}