1use chrono::Utc;
6use itertools::Itertools;
7
8use crate::{Duration, Enst, FqdnToStatuses, Status, VERSION};
9
10#[derive(Debug)]
11struct DashServiceRow {
12 computed_status: String,
13 fqdn: String,
14 ipaddr: String,
15 node_statuses: Vec<(String, f32)>,
16}
17
18#[allow(clippy::too_many_lines)]
19pub fn render_html_dashboard(service_statuses: &FqdnToStatuses) -> String {
20 const JS: &str = r#"
21 async function fetchHtmlAsText(url) {
22 return await (await fetch(url)).text();
23 }
24 async function loadHome() {
25 setTimeout(loadHome, 1000);
26 const contentDiv = document.getElementById("content");
27 contentDiv.innerHTML = await fetchHtmlAsText("/dash");
28 }
29 setTimeout(loadHome, 1000);
30 "#;
31
32 const CSS: &str = r"
33 table {
34 margin: 2em;
35 padding: 1em;
36 font-family: sans-serif;
37 min-width: 400px;
38 box-shadow: 0 0 20px rgba(0, 0, 0, 0.15);
39 border-spacing: 10px;
40 }
41 p#footer {
42 padding-left: 5em;
43 color: #888;
44 }
45 td.dot {
46 text-shadow: 1px 1px 3px;
47 }
48 ";
49
50 fn circle(s: Status) -> String {
51 match s {
52 Status::Up => "🟢",
53 Status::Down => "🔴",
54 Status::Unknown => "â—¯",
55 }
56 .to_string()
57 }
58
59 fn opacity(ens: &Enst) -> f32 {
60 let delay_s = ens
61 .last_update_time
62 .elapsed()
63 .unwrap_or(Duration::new(0, 0))
64 .as_secs_f32();
65 if delay_s < 10.0 {
66 let d = 1.0 - delay_s / 15.0;
67 (d * 100.0).round() / 100.0
68 } else {
69 0.33
70 }
71 }
72
73 let now = Utc::now().format("%Y-%m-%d %H:%M:%S").to_string();
74
75 let service_tbl: Vec<DashServiceRow> = service_statuses
77 .iter()
78 .sorted_by_key(|item| item.0)
79 .flat_map(|(fqdn, srvs)| {
80 srvs.endpoint_statuses
81 .iter()
82 .sorted_by_key(|item| item.0)
83 .map(|(srv_ipaddr, ep)| {
84 let computed_status = circle(ep.computed_status);
86 let node_statuses = ep
87 .node_to_ens
88 .iter()
89 .sorted_by_key(|item| item.0)
90 .map(|(_, ens)| {
91 (circle(ens.status), opacity(ens))
93 })
94 .collect::<Vec<_>>();
95
96 DashServiceRow {
97 computed_status,
98 fqdn: fqdn.to_string(),
99 ipaddr: srv_ipaddr.to_string(),
100 node_statuses,
101 }
102 })
103 })
104 .collect();
105
106 markup::new! {
107 html[lang="en", id="content"] {
108 head {
109 meta[charset="utf-8"];
110 title { "rrdnsd" }
111 script[type="text/javascript"] { @markup::raw(JS) }
112 style { @CSS }
113 }
114 body {
115 table {
116 tr {
117 th { "FQDN" }
118 th { "IP address" }
119 th { "Status" }
120 }
121 @for sv in &service_tbl {
122 tr {
123 td { @sv.fqdn }
124 td { @sv.ipaddr }
125 td[class="dot"] { @sv.computed_status }
126 td {}
127 @for (sym, opacity) in &sv.node_statuses {
128 td[class="dot", style=format!("opacity: {opacity}" )] { @sym }
129 }
130 }
131 }
132 }
133 p[id="footer"] {
134 "rrdnsd v. " @VERSION " " @now
135 }
136 }
137 }
138 }
139 .to_string()
140}