redesign: light project site + live network status from a lusers probe

This commit is contained in:
Jean Chevronnet 2026-08-30 18:54:22 +00:00
parent 182f14a7f2
commit d1ec6962dd
No known key found for this signature in database
GPG key ID: 439666D63A9477E4
10 changed files with 473 additions and 282 deletions

101
src/stats.rs Normal file
View file

@ -0,0 +1,101 @@
//! Live network status, polled from the running ircd. A background task connects
//! to the local daemon as an ordinary client every minute, runs LUSERS/VERSION,
//! parses the reply, and stores it. If the daemon is unreachable the state simply
//! reads "offline" — it never blocks or breaks page rendering.
use std::sync::{Arc, RwLock};
use std::time::Duration;
use tokio::io::{AsyncBufReadExt, AsyncWriteExt, BufReader};
use tokio::net::TcpStream;
use tokio::time::{sleep, timeout};
#[derive(Clone, Default)]
pub struct NetStats {
pub online: bool,
pub users: Option<u64>,
pub version: Option<String>,
}
pub type Shared = Arc<RwLock<NetStats>>;
/// Spawn the background poller. Refreshes every 60s.
pub fn spawn_updater(shared: Shared) {
tokio::spawn(async move {
loop {
let next = match timeout(Duration::from_secs(7), probe()).await {
Ok(Some(s)) => s,
_ => NetStats::default(),
};
if let Ok(mut g) = shared.write() {
*g = next;
}
sleep(Duration::from_secs(60)).await;
}
});
}
async fn probe() -> Option<NetStats> {
let addr = std::env::var("ECHO_IRCD_ADDR").unwrap_or_else(|_| "127.0.0.1:6667".into());
let stream = TcpStream::connect(&addr).await.ok()?;
let (rd, mut wr) = stream.into_split();
wr.write_all(b"NICK echostat\r\nUSER echostat 0 * :echo website status\r\n")
.await
.ok()?;
let mut lines = BufReader::new(rd).lines();
let (mut users, mut version, mut asked) = (None, None, false);
for _ in 0..100 {
let line = match lines.next_line().await {
Ok(Some(l)) => l,
_ => break,
};
let line = line.trim_end_matches('\r');
if let Some(rest) = line.strip_prefix("PING ") {
let _ = wr.write_all(format!("PONG {rest}\r\n").as_bytes()).await;
continue;
}
match numeric(line) {
Some("001") if !asked => {
let _ = wr.write_all(b"LUSERS\r\nVERSION\r\n").await;
asked = true;
}
Some("251") => users = users.or_else(|| parse_users(line)),
Some("351") => version = version.or_else(|| parse_version(line)),
_ => {}
}
if users.is_some() && version.is_some() {
break;
}
}
let _ = wr.write_all(b"QUIT :bye\r\n").await;
Some(NetStats { online: true, users, version })
}
/// The three-digit numeric of an IRC line (`:src 251 nick …`), if any.
fn numeric(line: &str) -> Option<&str> {
let mut it = line.split(' ');
let first = it.next()?;
let code = if first.starts_with(':') { it.next()? } else { first };
(code.len() == 3 && code.bytes().all(|b| b.is_ascii_digit())).then_some(code)
}
/// RPL_LUSERCLIENT trailing: "There are 5 users on 1 server" → first number.
fn parse_users(line: &str) -> Option<u64> {
line.splitn(2, " :")
.nth(1)?
.split_whitespace()
.find_map(|w| w.parse::<u64>().ok())
}
/// RPL_VERSION trailing: "echoircd 5.0.0 · …" → "5.0.0".
fn parse_version(line: &str) -> Option<String> {
let tail = line.splitn(2, " :").nth(1)?;
let mut it = tail.split_whitespace();
let a = it.next()?;
if a.eq_ignore_ascii_case("echoircd") {
it.next().map(|v| v.to_string())
} else {
Some(a.to_string())
}
}