1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
|
use crate::{log::LOG, Check, Config, Service, Success, STATUS};
use anyhow::Result;
use axum::response::Html;
use markup::{doctype, Render};
use std::{collections::BTreeMap, ops::Deref, sync::Arc, time::Duration};
pub async fn send_html_page(config: Arc<Config>) -> Html<String> {
let mut out = String::new();
let status = STATUS.read().await;
let status = status.deref();
let log = LOG.read().await;
let log = log.deref();
#[cfg(not(debug_assertions))]
let css = include_str!("style.css");
#[cfg(debug_assertions)]
let css = tokio::fs::read_to_string("src/style.css").await.unwrap();
markup::new! {
@doctype()
html {
head {
meta[charset="UTF-8"];
meta[name="viewport", content="width=device-width, initial-scale=1.0"];
title { "Status Page" }
style { @css }
}
body {
main {
h1 { @config.title }
@for (i, service) in config.services.iter().enumerate() {
@ServiceCard { i, status, service }
}
div.log {
h2 { "Past Events" }
ul {
@for event in log.iter() {
li.{if event.error.is_some() { "error" } else { "ok" }} {
@let service = &config.services[event.service];
b { @event.time.to_rfc2822() ": " }
@service.title " "
@if let Some(error) = &event.error {
" failed. " @service.checks[event.check].display() " reported " @error
} else {
" is working again."
}
}
}
}
}
}
}
}
}
.render(&mut out)
.unwrap();
Html(out)
}
markup::define!(
ServiceCard<'a>(status: &'a BTreeMap<(usize, usize), Result<Success>>, service: &'a Service, i: usize) {
@let any_err = status.range((*i,usize::MIN)..(*i,usize::MAX)).any(|(_,v)|v.is_err());
div.service.{if any_err { "error" } else { "ok" }} {
h2 { @service.title }
@if let Some(url) = &service.url { a[href=url] { "Link" } }
div.checks {
@for (j, check) in service.checks.iter().enumerate() {
@if let Some(status) = status.get(&(*i, j)) {
span.name { @check.display() }
@match status {
Ok(Success { latency, updated }) => {
span.status.ok { "Ok" }
span.details.ok {
"Checked " @format_duration(updated.elapsed().unwrap_or_default()) " ago"
@if let Some(latency) = latency { ", took " @format_duration(*latency) } "."
}
}
Err(e) => {
span.status.error { "Error" }
span.details { @format!("{e}") }
}
}
}
}
}
}
}
);
impl Check {
pub fn display(&self) -> String {
match self {
Check::Systemd(_) => "Service".to_string(),
Check::Http { title, .. } => title.clone().unwrap_or("HTTP".to_string()),
Check::Shell { title, .. } => title.to_owned(),
}
}
}
fn format_duration(d: Duration) -> String {
if d.as_millis() < 1500 {
format!("{}ms", d.as_millis())
} else {
format!("{}s", d.as_secs())
}
}
|