mirror of
https://github.com/4cuck/nitter-status.git
synced 2026-10-27 00:57:39 +00:00
uptime-graph details, hover, history and % uptime overall
This commit is contained in:
1 parent
df316e8fdd
commit
daee7e6277
5 files changed
+127
-48
No files matched your search
@@ -47,5 +47,10 @@ pub struct CacheHost {
|
|||||||
pub is_latest_version: bool,
|
pub is_latest_version: bool,
|
||||||
/// Whether this host is known to be bad (ip blocking)
|
/// Whether this host is known to be bad (ip blocking)
|
||||||
pub is_bad_host: bool,
|
pub is_bad_host: bool,
|
||||||
|
/// Country from the wiki
|
||||||
pub country: String,
|
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,
|
||||||
}
|
}
|
||||||
@@ -24,6 +24,13 @@ pub struct HostStats {
|
|||||||
total: u32,
|
total: u32,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, FromQueryResult)]
|
||||||
|
pub struct HostHealthyPercentage {
|
||||||
|
host: i32,
|
||||||
|
/// Percentage
|
||||||
|
healthy: u8,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, Default)]
|
#[derive(Debug, Default)]
|
||||||
struct LastPings {
|
struct LastPings {
|
||||||
avg: Option<i32>,
|
avg: Option<i32>,
|
||||||
@@ -84,6 +91,8 @@ impl Scanner {
|
|||||||
.query_pings(time_now - self.inner.config.ping_range)
|
.query_pings(time_now - self.inner.config.ping_range)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
|
let mut healthy_percentage_total = self.query_healthy_percentage().await?;
|
||||||
|
|
||||||
let mut host_statistics = Vec::with_capacity(hosts.len());
|
let mut host_statistics = Vec::with_capacity(hosts.len());
|
||||||
let default_health_check = LatestCheck::default();
|
let default_health_check = LatestCheck::default();
|
||||||
for host in hosts {
|
for host in hosts {
|
||||||
@@ -145,6 +154,8 @@ impl Scanner {
|
|||||||
version_url: host.version_url,
|
version_url: host.version_url,
|
||||||
is_bad_host,
|
is_bad_host,
|
||||||
country: host.country,
|
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);
|
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();
|
let stats: HashMap<_, _> = stats.into_iter().map(|v| (v.host, v)).collect();
|
||||||
Ok(stats)
|
Ok(stats)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Query total up percentage for all hosts
|
||||||
|
async fn query_healthy_percentage(
|
||||||
|
&self,
|
||||||
|
) -> Result<HashMap<i32, u8>> {
|
||||||
|
let stats: Vec<HostHealthyPercentage> = 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<Vec<(String,bool)>> {
|
||||||
|
#[derive(Debug, FromQueryResult)]
|
||||||
|
pub struct HostHealthCheck {
|
||||||
|
healthy: bool,
|
||||||
|
time: i64,
|
||||||
|
}
|
||||||
|
let health_checks: Vec<HostHealthCheck> = 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)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -412,10 +412,13 @@ impl Scanner {
|
|||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod test {
|
mod test {
|
||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
|
use chrono::Duration;
|
||||||
use entities::state::scanner::Config;
|
use entities::state::scanner::Config;
|
||||||
use sea_orm::{ConnectOptions, Database};
|
use sea_orm::{ConnectOptions, Database};
|
||||||
use tokio::{fs::File, io::AsyncWriteExt};
|
use tokio::{fs::File, io::AsyncWriteExt};
|
||||||
|
use entities::health_check;
|
||||||
|
|
||||||
async fn db_init() -> DatabaseConnection {
|
async fn db_init() -> DatabaseConnection {
|
||||||
Database::connect(ConnectOptions::new(
|
Database::connect(ConnectOptions::new(
|
||||||
@@ -425,6 +428,27 @@ mod test {
|
|||||||
.unwrap()
|
.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> = 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)]
|
#[tokio::test(flavor = "multi_thread", worker_threads = 1)]
|
||||||
#[ignore]
|
#[ignore]
|
||||||
async fn update_instacelist() {
|
async fn update_instacelist() {
|
||||||
|
|||||||
+1
-1
@@ -64,7 +64,7 @@ pub async fn start(
|
|||||||
.layer(cors_policy(&config.site_url))
|
.layer(cors_policy(&config.site_url))
|
||||||
.layer(SetResponseHeaderLayer::overriding(
|
.layer(SetResponseHeaderLayer::overriding(
|
||||||
header::CONTENT_SECURITY_POLICY,
|
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::<HeaderValue>()
|
.parse::<HeaderValue>()
|
||||||
.expect("Invalid CSP header value"),
|
.expect("Invalid CSP header value"),
|
||||||
)),
|
)),
|
||||||
|
|||||||
@@ -4,6 +4,14 @@
|
|||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
<link href="/static/bootstrap.min.css" rel="stylesheet">
|
<link href="/static/bootstrap.min.css" rel="stylesheet">
|
||||||
|
<style>
|
||||||
|
.uptime-graph rect {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
.uptime-graph rect:hover {
|
||||||
|
fill: #5e6c84;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
<title>Nitter Instance Health</title>
|
<title>Nitter Instance Health</title>
|
||||||
</head>
|
</head>
|
||||||
@@ -20,8 +28,9 @@
|
|||||||
<th scope="col">Instance</th>
|
<th scope="col">Instance</th>
|
||||||
<th scope="col">Country</th>
|
<th scope="col">Country</th>
|
||||||
<th scope="col">Healthy</th>
|
<th scope="col">Healthy</th>
|
||||||
<th scope="col">Response Times</th>
|
<th scope="col">Health History</th>
|
||||||
<th scope="col">Average (ms)</th>
|
<th scope="col">Average (ms)</th>
|
||||||
|
<th scope="col">All Healthy %</th>
|
||||||
<th scope="col">RSS</th>
|
<th scope="col">RSS</th>
|
||||||
<th scope="col">Version</th>
|
<th scope="col">Version</th>
|
||||||
<th scope="col">Points</th>
|
<th scope="col">Points</th>
|
||||||
@@ -40,57 +49,38 @@
|
|||||||
{%- else -%}
|
{%- else -%}
|
||||||
❌
|
❌
|
||||||
{%- endif -%}</td>
|
{%- endif -%}</td>
|
||||||
<td {%- if not host.ping_min %} colspan="2" {% endif -%}>
|
<td {%- if not host.recent_checks %} colspan="2" {% endif -%}>
|
||||||
{%- if host.recent_pings|length > 0 -%}
|
{%- set height = 28 -%}
|
||||||
{%- set height = 34 -%}
|
{%- set width = 110 -%}
|
||||||
{%- set width = 100 -%}
|
{%- set width_bar = 5 -%}
|
||||||
{%- set amount = host.recent_pings|length -%}
|
{%- set offset = 5 -%}
|
||||||
{%- set width_bar = 5 -%}
|
{%- if host.recent_checks -%}
|
||||||
{%- set offset = 5 -%}
|
<svg class="uptime-graph" viewBox="0 0 {{width}} {{height}}" height="{{height}}">
|
||||||
{%- set error_size = height / 3 %}
|
{%- for check in host.recent_checks -%}
|
||||||
{%- if host.ping_max and host.ping_min -%}
|
{%- if check.1 -%}
|
||||||
{# only one entry, so max = min => step_size would be 0 #}
|
{%- set title = "Healthy " ~ check.0 -%}
|
||||||
{%- if host.ping_max == host.ping_min -%}
|
{%- set color_bar = "#2fcc66" -%}
|
||||||
{%- set step_size = 5/(height-offset) -%}
|
{%- else -%}
|
||||||
{%- else -%}
|
{%- set title = "Unhealthy " ~ check.0 -%}
|
||||||
{%- set step_size = (host.ping_max - host.ping_min)/(height-offset) -%}
|
{%- set color_bar = "#ff6225" -%}
|
||||||
{%- endif -%}
|
{%- endif -%}
|
||||||
{%- else %}
|
<rect x="{{(loop.index-1) * width_bar}}" y="0" width="{{width_bar - 1}}" height="{{height}}" fill="{{color_bar}}" fill-opacity="0.95" shape-rendering="auto" ><title>{{title}}</title></rect>
|
||||||
{# no valid entries, only nulls #}
|
{%- endfor -%}
|
||||||
{%- set step_size = 5/(height-offset) -%}
|
</svg>
|
||||||
{%- endif -%}
|
|
||||||
{# <!-- step size {{step_size}} -->
|
|
||||||
<!-- ping_max {{host.ping_max}} -->
|
|
||||||
<!-- ping_min {{host.ping_min}} --> #}
|
|
||||||
{%- if host.ping_min -%}
|
|
||||||
<svg viewBox="0 0 {{width}} {{height}}" height="{{height - 1}}">
|
|
||||||
{%- for ping in host.recent_pings -%}
|
|
||||||
{%- if ping -%}
|
|
||||||
{%- set title = "Healthy" -%}
|
|
||||||
{%- set color_bar = "#2fcc66" -%}
|
|
||||||
{%- else -%}
|
|
||||||
{%- set title = "Unhealthy" -%}
|
|
||||||
{%- set color_bar = "#ff6225" -%}
|
|
||||||
{%- endif -%}
|
|
||||||
<rect x="{{(loop.index-1) * width_bar}}" y="0" width="{{width_bar - 2}}" height="{{height}}" fill="{{color_bar}}" fill-opacity="0.95"><title>{{title}}</title></rect>
|
|
||||||
{%- endfor -%}
|
|
||||||
</svg>
|
|
||||||
{%- else -%}
|
|
||||||
{# <!-- no healthy entries, show last time host was seen healthy --> #}
|
|
||||||
{%- if host.last_healthy -%}
|
|
||||||
Last seen healthy: {{host.last_healthy | truncate(length=16, end="") | replace(from="-", to=".") | replace(from="T", to=" ")}}
|
|
||||||
<a rel="nofollow" href="https://time.is/UTC">UTC</a>
|
|
||||||
{%- else -%}
|
|
||||||
Never seen healthy.
|
|
||||||
{%- endif -%}
|
|
||||||
{%- endif -%}
|
|
||||||
{%- else -%}
|
{%- else -%}
|
||||||
No recent data!
|
{# <!-- no healthy entries, show last time host was seen healthy --> #}
|
||||||
|
{%- if host.last_healthy -%}
|
||||||
|
Last seen healthy: {{host.last_healthy | truncate(length=16, end="") | replace(from="-", to=".") | replace(from="T", to=" ")}}
|
||||||
|
<a rel="nofollow" href="https://time.is/UTC">UTC</a>
|
||||||
|
{%- else -%}
|
||||||
|
Never seen healthy.
|
||||||
|
{%- endif -%}
|
||||||
{%- endif -%}
|
{%- endif -%}
|
||||||
</td>
|
</td>
|
||||||
{%- if host.ping_min -%}
|
{%- if host.recent_checks -%}
|
||||||
<td>{{host.ping_avg}}</td>
|
<td>{{host.ping_avg}}</td>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
<td>{{host.healthy_percentage_overall}}</td>
|
||||||
<td>{% if host.rss -%} ✅ {%- else -%} ❌ {%- endif -%}</td>
|
<td>{% if host.rss -%} ✅ {%- else -%} ❌ {%- endif -%}</td>
|
||||||
{%- if host.healthy and not host.version -%}
|
{%- if host.healthy and not host.version -%}
|
||||||
{%- set version = "Dmissing" -%}
|
{%- set version = "Dmissing" -%}
|
||||||
|
|||||||
Reference in new issue
Block a user