mirror of
https://github.com/Noratrieb/uptime.git
synced 2026-01-14 16:45:06 +01:00
bars!!!
This commit is contained in:
parent
8af7829b0e
commit
f17454ae67
3 changed files with 125 additions and 4 deletions
|
|
@ -18,7 +18,7 @@ pub struct CheckResult {
|
||||||
pub state: CheckState,
|
pub state: CheckState,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(sqlx::Type)]
|
#[derive(Debug, PartialEq, sqlx::Type)]
|
||||||
#[sqlx(rename_all = "snake_case")]
|
#[sqlx(rename_all = "snake_case")]
|
||||||
pub enum CheckState {
|
pub enum CheckState {
|
||||||
Ok,
|
Ok,
|
||||||
|
|
|
||||||
89
src/web.rs
89
src/web.rs
|
|
@ -7,6 +7,7 @@ use axum::{
|
||||||
routing::get,
|
routing::get,
|
||||||
Router,
|
Router,
|
||||||
};
|
};
|
||||||
|
use chrono::{DateTime, Utc};
|
||||||
use eyre::{Context, Result};
|
use eyre::{Context, Result};
|
||||||
use http::StatusCode;
|
use http::StatusCode;
|
||||||
use sqlx::{Pool, Sqlite};
|
use sqlx::{Pool, Sqlite};
|
||||||
|
|
@ -58,14 +59,19 @@ fn compute_status(checks: Vec<Check>) -> Vec<WebsiteStatus> {
|
||||||
|
|
||||||
websites
|
websites
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|(website, checks)| {
|
.map(|(website, mut checks)| {
|
||||||
|
checks.sort_by_key(|check| check.0);
|
||||||
|
|
||||||
let mut last_ok = None;
|
let mut last_ok = None;
|
||||||
let mut count_ok = 0;
|
let mut count_ok = 0;
|
||||||
|
|
||||||
|
const BAR_ELEMS: usize = 100;
|
||||||
|
let bar_classes = checks_to_classes(&checks, BAR_ELEMS);
|
||||||
|
|
||||||
let len = checks.len();
|
let len = checks.len();
|
||||||
checks.into_iter().for_each(|(time, result)| {
|
checks.into_iter().for_each(|(time, result)| {
|
||||||
last_ok = std::cmp::max(last_ok, Some(time));
|
|
||||||
if let CheckState::Ok = result {
|
if let CheckState::Ok = result {
|
||||||
|
last_ok = std::cmp::max(last_ok, Some(time));
|
||||||
count_ok += 1;
|
count_ok += 1;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
@ -81,6 +87,84 @@ fn compute_status(checks: Vec<Check>) -> Vec<WebsiteStatus> {
|
||||||
ok_ratio,
|
ok_ratio,
|
||||||
count_ok,
|
count_ok,
|
||||||
total_requests: len,
|
total_requests: len,
|
||||||
|
bar_classes,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
enum BarClass {
|
||||||
|
Green,
|
||||||
|
Orange,
|
||||||
|
Red,
|
||||||
|
Unknown,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl BarClass {
|
||||||
|
fn as_class(&self) -> &'static str {
|
||||||
|
match self {
|
||||||
|
Self::Green => "check-result-green",
|
||||||
|
Self::Orange => "check-result-orange",
|
||||||
|
Self::Red => "check-result-red",
|
||||||
|
Self::Unknown => "check-result-unknown",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Converts a list of (sorted by time) checks at arbitrary dates into a list of boxes for the
|
||||||
|
/// frontend, in a fixed sensical timeline.
|
||||||
|
/// We slice the time from the first check to the last check (maybe something like last check-30d
|
||||||
|
/// in the future) into slices and aggregate all checks from these times into these slices.
|
||||||
|
fn checks_to_classes(checks: &[(DateTime<Utc>, CheckState)], classes: usize) -> Vec<BarClass> {
|
||||||
|
assert_ne!(classes, 0);
|
||||||
|
let Some(first) = checks.first() else {
|
||||||
|
return vec![];
|
||||||
|
};
|
||||||
|
let last = checks.last().unwrap();
|
||||||
|
|
||||||
|
let mut bins = vec![vec![]; classes];
|
||||||
|
|
||||||
|
let first = first.0.timestamp_millis();
|
||||||
|
let last = last.0.timestamp_millis();
|
||||||
|
|
||||||
|
let last_rel = last - first;
|
||||||
|
assert!(last.is_positive(), "checks not ordered correctly");
|
||||||
|
|
||||||
|
for check in checks {
|
||||||
|
let time_rel = check.0.timestamp_millis() - first;
|
||||||
|
assert!(first.is_positive(), "checks not ordered correctly");
|
||||||
|
|
||||||
|
/*
|
||||||
|
5 bins:
|
||||||
|
| | | | | |
|
||||||
|
0.0 0.2 0.4 0.6 0.8 1.0 division
|
||||||
|
0.0 1.0 2.0 3.0 4.0 5.0 after multiply
|
||||||
|
*/
|
||||||
|
|
||||||
|
let bin = (time_rel as f64) / (last_rel as f64) * ((classes) as f64);
|
||||||
|
let bin = bin as usize; // flooring on purpose
|
||||||
|
let bin = if bin == classes { bin - 1 } else { bin };
|
||||||
|
bins[bin].push(check);
|
||||||
|
}
|
||||||
|
|
||||||
|
bins.iter()
|
||||||
|
.map(|checks| {
|
||||||
|
let ok = checks
|
||||||
|
.iter()
|
||||||
|
.filter(|check| check.1 == CheckState::Ok)
|
||||||
|
.count();
|
||||||
|
let all = checks.len();
|
||||||
|
|
||||||
|
if all == 0 {
|
||||||
|
BarClass::Unknown
|
||||||
|
} else if all == ok {
|
||||||
|
BarClass::Green
|
||||||
|
} else if ok == 0 {
|
||||||
|
BarClass::Red
|
||||||
|
} else if ok > 0 && ok < all {
|
||||||
|
BarClass::Orange
|
||||||
|
} else {
|
||||||
|
unreachable!("i dont think logic works like this")
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.collect()
|
.collect()
|
||||||
|
|
@ -92,6 +176,7 @@ struct WebsiteStatus {
|
||||||
ok_ratio: String,
|
ok_ratio: String,
|
||||||
total_requests: usize,
|
total_requests: usize,
|
||||||
count_ok: usize,
|
count_ok: usize,
|
||||||
|
bar_classes: Vec<BarClass>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Template)]
|
#[derive(Template)]
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,36 @@
|
||||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<title>Uptime</title>
|
<title>Uptime</title>
|
||||||
|
<style>
|
||||||
|
html {
|
||||||
|
font-family: Arial, Helvetica, sans-serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
.check-result-bar {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
.check-result {
|
||||||
|
height: 10px;
|
||||||
|
width: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.check-result-red {
|
||||||
|
background-color: red;
|
||||||
|
}
|
||||||
|
|
||||||
|
.check-result-orange {
|
||||||
|
background-color: orange;
|
||||||
|
}
|
||||||
|
|
||||||
|
.check-result-green {
|
||||||
|
background-color: green;
|
||||||
|
}
|
||||||
|
|
||||||
|
.check-result-unknown {
|
||||||
|
background-color: grey;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<main>
|
<main>
|
||||||
|
|
@ -16,6 +46,12 @@
|
||||||
<p>Last OK: <span class="utc-timestamp">{{ check.last_ok.as_deref().unwrap() }}</span></p>
|
<p>Last OK: <span class="utc-timestamp">{{ check.last_ok.as_deref().unwrap() }}</span></p>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
|
<div class="check-result-bar">
|
||||||
|
{% for result in check.bar_classes %}
|
||||||
|
<div class="check-result {{ result.as_class() }}"></div>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue