➕ Add "last seen" to server list
parent
a3e76cf120
commit
7aafbba4d9
|
@ -13,6 +13,7 @@ license = "AGPL-3.0"
|
|||
aho-corasick = "0.7.14"
|
||||
base64 = "0.12.3"
|
||||
blake3 = "0.3.7"
|
||||
chrono = "0.4.19"
|
||||
ctrlc = { version = "3.1.7", features = [ "termination" ] }
|
||||
dashmap = "3.11.10"
|
||||
futures = "0.3.7"
|
||||
|
|
|
@ -6,12 +6,25 @@
|
|||
body {
|
||||
font-family: sans-serif;
|
||||
}
|
||||
.entry {
|
||||
display: inline-block;
|
||||
padding: 20px;
|
||||
min-width: 50%;
|
||||
td {
|
||||
padding: 0px;
|
||||
}
|
||||
.entry_list div:nth-child(odd) {
|
||||
td > * {
|
||||
padding: 20px;
|
||||
display: block;
|
||||
}
|
||||
.entry {
|
||||
|
||||
}
|
||||
.grey {
|
||||
color: #888;
|
||||
text-align: right;
|
||||
}
|
||||
.entry_list {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
}
|
||||
tbody tr:nth-child(odd) {
|
||||
background-color: #ddd;
|
||||
}
|
||||
</style>
|
||||
|
@ -21,19 +34,28 @@
|
|||
|
||||
<h1>Server list</h1>
|
||||
|
||||
<div class="entry_list">
|
||||
|
||||
{{#if servers}}
|
||||
{{#each servers}}
|
||||
<div>
|
||||
<a class="entry" href="{{this.path}}/files/">{{this.name}}</a>
|
||||
</div>
|
||||
{{/each}}
|
||||
{{else}}
|
||||
(No servers are running)
|
||||
{{/if}}
|
||||
<table class="entry_list">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Name</th>
|
||||
<th>Last seen</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
|
||||
</div>
|
||||
{{#each servers}}
|
||||
<tr>
|
||||
<td><a class="entry" href="{{this.path}}/files/">{{this.name}}</a></td>
|
||||
<td><span class="grey">{{this.last_seen}}</span></td>
|
||||
</tr>
|
||||
{{/each}}
|
||||
|
||||
</tbody>
|
||||
</table>
|
||||
{{else}}
|
||||
(No servers have reported since this relay started)
|
||||
{{/if}}
|
||||
|
||||
</body>
|
||||
</html>
|
||||
|
|
|
@ -6,19 +6,25 @@
|
|||
body {
|
||||
font-family: sans-serif;
|
||||
}
|
||||
.entry {
|
||||
display: inline-block;
|
||||
td {
|
||||
padding: 0;
|
||||
}
|
||||
td > * {
|
||||
padding: 10px;
|
||||
width: 100%;
|
||||
display: block;
|
||||
}
|
||||
.entry {
|
||||
text-decoration: none;
|
||||
}
|
||||
.grey {
|
||||
color: #888;
|
||||
text-align: right;
|
||||
}
|
||||
.entry_list {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
}
|
||||
.entry_list tr:nth-child(even) {
|
||||
tbody tr:nth-child(odd) {
|
||||
background-color: #ddd;
|
||||
}
|
||||
</style>
|
||||
|
|
178
src/relay/mod.rs
178
src/relay/mod.rs
|
@ -127,12 +127,32 @@ impl From <&ConfigFile> for Config {
|
|||
}
|
||||
}
|
||||
|
||||
use chrono::{
|
||||
DateTime,
|
||||
SecondsFormat,
|
||||
Utc
|
||||
};
|
||||
|
||||
#[derive (Clone)]
|
||||
pub struct ServerStatus {
|
||||
last_seen: DateTime <Utc>,
|
||||
}
|
||||
|
||||
impl Default for ServerStatus {
|
||||
fn default () -> Self {
|
||||
Self {
|
||||
last_seen: Utc::now (),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct RelayState {
|
||||
config: Config,
|
||||
handlebars: Arc <Handlebars <'static>>,
|
||||
|
||||
// Key: Server ID
|
||||
request_rendezvous: Mutex <HashMap <String, RequestRendezvous>>,
|
||||
server_status: Mutex <HashMap <String, ServerStatus>>,
|
||||
|
||||
// Key: Request ID
|
||||
response_rendezvous: RwLock <DashMap <String, ResponseRendezvous>>,
|
||||
|
@ -149,6 +169,7 @@ impl From <&ConfigFile> for RelayState {
|
|||
config: Config::from (config_file),
|
||||
handlebars: Arc::new (load_templates (&PathBuf::new ()).unwrap ()),
|
||||
request_rendezvous: Default::default (),
|
||||
server_status: Default::default (),
|
||||
response_rendezvous: Default::default (),
|
||||
shutdown_watch_tx,
|
||||
shutdown_watch_rx,
|
||||
|
@ -205,6 +226,16 @@ async fn handle_http_listen (
|
|||
return trip_error;
|
||||
}
|
||||
|
||||
// End of early returns
|
||||
|
||||
{
|
||||
let mut server_status = state.server_status.lock ().await;
|
||||
|
||||
let mut status = server_status.entry (watcher_code.clone ()).or_insert_with (Default::default);
|
||||
|
||||
status.last_seen = Utc::now ();
|
||||
}
|
||||
|
||||
use RequestRendezvous::*;
|
||||
|
||||
let (tx, rx) = oneshot::channel ();
|
||||
|
@ -454,6 +485,99 @@ async fn handle_http_request (
|
|||
}
|
||||
}
|
||||
|
||||
#[derive (Debug, PartialEq)]
|
||||
enum LastSeen {
|
||||
Negative,
|
||||
Connected,
|
||||
Description (String),
|
||||
}
|
||||
|
||||
// Mnemonic is "now - last_seen"
|
||||
|
||||
fn pretty_print_last_seen (
|
||||
now: DateTime <Utc>,
|
||||
last_seen: DateTime <Utc>
|
||||
) -> LastSeen
|
||||
{
|
||||
use LastSeen::*;
|
||||
|
||||
let dur = now.signed_duration_since (last_seen);
|
||||
|
||||
if dur < chrono::Duration::zero () {
|
||||
return Negative;
|
||||
}
|
||||
|
||||
if dur.num_minutes () < 1 {
|
||||
return Connected;
|
||||
}
|
||||
|
||||
if dur.num_hours () < 1 {
|
||||
return Description (format! ("{} m ago", dur.num_minutes ()));
|
||||
}
|
||||
|
||||
if dur.num_days () < 1 {
|
||||
return Description (format! ("{} h ago", dur.num_hours ()));
|
||||
}
|
||||
|
||||
Description (last_seen.to_rfc3339_opts (SecondsFormat::Secs, true))
|
||||
}
|
||||
|
||||
|
||||
async fn handle_server_list (
|
||||
state: Arc <RelayState>
|
||||
) -> Response <Body>
|
||||
{
|
||||
use std::borrow::Cow;
|
||||
|
||||
#[derive (Serialize)]
|
||||
struct ServerEntry <'a> {
|
||||
path: String,
|
||||
name: String,
|
||||
last_seen: Cow <'a, str>,
|
||||
}
|
||||
|
||||
#[derive (Serialize)]
|
||||
struct ServerListPage <'a> {
|
||||
servers: Vec <ServerEntry <'a>>,
|
||||
}
|
||||
|
||||
let servers = {
|
||||
let guard = state.server_status.lock ().await;
|
||||
(*guard).clone ()
|
||||
};
|
||||
|
||||
let now = Utc::now ();
|
||||
|
||||
let mut servers: Vec <_> = servers.into_iter ()
|
||||
.map (|(name, server)| {
|
||||
let display_name = percent_encoding::percent_decode_str (&name).decode_utf8 ().unwrap_or_else (|_| "Server name isn't UTF-8".into ()).to_string ();
|
||||
|
||||
use LastSeen::*;
|
||||
|
||||
let last_seen = match pretty_print_last_seen (now, server.last_seen) {
|
||||
Negative => "Error (negative time)".into (),
|
||||
Connected => "Connected".into (),
|
||||
Description (s) => s.into (),
|
||||
};
|
||||
|
||||
ServerEntry {
|
||||
name: display_name,
|
||||
path: name,
|
||||
last_seen: last_seen,
|
||||
}
|
||||
})
|
||||
.collect ();
|
||||
|
||||
servers.sort_by (|a, b| a.name.cmp (&b.name));
|
||||
|
||||
let page = ServerListPage {
|
||||
servers,
|
||||
};
|
||||
|
||||
let s = state.handlebars.render ("relay_server_list", &page).unwrap ();
|
||||
ok_reply (s)
|
||||
}
|
||||
|
||||
#[instrument (level = "trace", skip (req, state))]
|
||||
async fn handle_all (req: Request <Body>, state: Arc <RelayState>)
|
||||
-> Result <Response <Body>, Infallible>
|
||||
|
@ -487,35 +611,7 @@ async fn handle_all (req: Request <Body>, state: Arc <RelayState>)
|
|||
}
|
||||
else if let Some (rest) = prefix_match ("/frontend/servers/", path) {
|
||||
if rest == "" {
|
||||
use std::borrow::Cow;
|
||||
|
||||
#[derive (Serialize)]
|
||||
struct ServerEntry <'a> {
|
||||
path: &'a str,
|
||||
name: Cow <'a, str>,
|
||||
}
|
||||
|
||||
#[derive (Serialize)]
|
||||
struct ServerListPage <'a> {
|
||||
servers: Vec <ServerEntry <'a>>,
|
||||
}
|
||||
|
||||
let mut names = state.list_servers ().await;
|
||||
names.sort ();
|
||||
|
||||
//println! ("Found {} servers", names.len ());
|
||||
|
||||
let page = ServerListPage {
|
||||
servers: names.iter ()
|
||||
.map (|name| ServerEntry {
|
||||
name: percent_encoding::percent_decode_str (name).decode_utf8 ().unwrap_or_else (|_| "Server name isn't UTF-8".into ()),
|
||||
path: &name,
|
||||
})
|
||||
.collect (),
|
||||
};
|
||||
|
||||
let s = state.handlebars.render ("relay_server_list", &page).unwrap ();
|
||||
ok_reply (s)
|
||||
handle_server_list (state).await
|
||||
}
|
||||
else if let Some (idx) = rest.find ('/') {
|
||||
let listen_code = String::from (&rest [0..idx]);
|
||||
|
@ -635,5 +731,29 @@ pub async fn run_relay (
|
|||
|
||||
#[cfg (test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_pretty_print_last_seen () {
|
||||
use LastSeen::*;
|
||||
|
||||
let last_seen = DateTime::parse_from_rfc3339 ("2019-05-29T00:00:00+00:00").unwrap ().with_timezone (&Utc);
|
||||
|
||||
for (input, expected) in vec! [
|
||||
("2019-05-28T23:59:59+00:00", Negative),
|
||||
("2019-05-29T00:00:00+00:00", Connected),
|
||||
("2019-05-29T00:00:59+00:00", Connected),
|
||||
("2019-05-29T00:01:30+00:00", Description ("1 m ago".into ())),
|
||||
("2019-05-29T00:59:30+00:00", Description ("59 m ago".into ())),
|
||||
("2019-05-29T01:00:30+00:00", Description ("1 h ago".into ())),
|
||||
("2019-05-29T10:00:00+00:00", Description ("10 h ago".into ())),
|
||||
("2019-05-30T00:00:00+00:00", Description ("2019-05-29T00:00:00Z".into ())),
|
||||
("2019-05-30T10:00:00+00:00", Description ("2019-05-29T00:00:00Z".into ())),
|
||||
("2019-05-31T00:00:00+00:00", Description ("2019-05-29T00:00:00Z".into ())),
|
||||
].into_iter () {
|
||||
let now = DateTime::parse_from_rfc3339 (input).unwrap ().with_timezone (&Utc);
|
||||
let actual = pretty_print_last_seen (now, last_seen);
|
||||
assert_eq! (actual, expected);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -686,19 +686,12 @@ mod tests {
|
|||
path::{
|
||||
Component,
|
||||
Path,
|
||||
PathBuf
|
||||
},
|
||||
};
|
||||
|
||||
use maplit::*;
|
||||
use tokio::runtime::Runtime;
|
||||
|
||||
use always_equal::test::AlwaysEqual;
|
||||
|
||||
use crate::http_serde::{
|
||||
StatusCode,
|
||||
};
|
||||
|
||||
#[test]
|
||||
fn icons () {
|
||||
let video = "🎞️";
|
||||
|
|
Loading…
Reference in New Issue