diff --git a/src/commits.rs b/src/commits.rs new file mode 100644 index 0000000..90a0771 --- /dev/null +++ b/src/commits.rs @@ -0,0 +1,99 @@ +//! The three most-recent daemon commits, shown in the home page "Latest" panel. +//! A background task reads them straight from the local echoIRCd git repo every +//! couple of minutes (mirrors `stats`), so the panel tracks `main` on its own — +//! no network, no JSON, no hardcoding. If git or the repo is unavailable the +//! list is simply empty and the template falls back to a changelog link. + +use std::sync::{Arc, RwLock}; +use std::time::Duration; +use tokio::time::sleep; + +#[derive(Clone)] +pub struct Commit { + pub short: String, + pub subject: String, + pub url: String, + pub date: String, +} + +pub type Shared = Arc>>; + +const COUNT: usize = 3; + +/// Spawn the background reader. First read is immediate, then every 2 minutes. +pub fn spawn_updater(shared: Shared) { + tokio::spawn(async move { + loop { + if let Some(list) = read_log().await { + if let Ok(mut g) = shared.write() { + *g = list; + } + } + sleep(Duration::from_secs(120)).await; + } + }); +} + +async fn read_log() -> Option> { + let repo = + std::env::var("ECHOIRCD_REPO").unwrap_or_else(|_| "/home/debian/irc/ircd/echoIRCd".into()); + let base = std::env::var("ECHOIRCD_REPO_URL") + .unwrap_or_else(|_| "https://git.devtronic.pro/echo/echoIRCd".into()); + let n = COUNT.to_string(); + + // full-sha \x1f short-sha \x1f subject \x1f committer-date, one commit per line. + let out = tokio::task::spawn_blocking(move || { + std::process::Command::new("git") + .args([ + "-C", + &repo, + "log", + "-n", + &n, + "--no-merges", + "--pretty=format:%H\x1f%h\x1f%s\x1f%cs", + ]) + .output() + }) + .await + .ok()? + .ok()?; + + if !out.status.success() { + return None; + } + let text = String::from_utf8_lossy(&out.stdout); + let list: Vec = text + .lines() + .filter_map(|line| { + let mut f = line.split('\x1f'); + let full = f.next()?; + let short = f.next()?; + let subject = f.next()?; + let date = f.next()?; + Some(Commit { + short: short.to_string(), + subject: subject.to_string(), + url: format!("{base}/commit/{full}"), + date: pretty_date(date), + }) + }) + .collect(); + (!list.is_empty()).then_some(list) +} + +/// "2026-08-30" -> "Aug 30, 2026"; anything unexpected passes through unchanged. +fn pretty_date(iso: &str) -> String { + const M: [&str; 12] = [ + "Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec", + ]; + let mut p = iso.split('-'); + if let (Some(y), Some(mo), Some(d)) = (p.next(), p.next(), p.next()) { + if let (Ok(m), Ok(day)) = (mo.parse::(), d.parse::()) { + if (1..=12).contains(&m) { + return format!("{} {}, {}", M[m - 1], day, y); + } + } + } + iso.to_string() +} diff --git a/src/main.rs b/src/main.rs index bf7fd0d..15a68d8 100644 --- a/src/main.rs +++ b/src/main.rs @@ -2,6 +2,7 @@ //! templates, a Markdown documentation section, and a small live "network status" //! pulled from the running ircd. +mod commits; mod docs; mod stats; @@ -35,11 +36,20 @@ fn view(shared: &stats::Shared) -> Status { } } +/// Shared application state: the live network stats and the latest commits, +/// each refreshed by its own background task. +#[derive(Clone)] +struct AppState { + stats: stats::Shared, + commits: commits::Shared, +} + #[derive(Template)] #[template(path = "index.html")] struct IndexTemplate { active: &'static str, status: Status, + commits: Vec, } #[derive(Template)] @@ -92,8 +102,9 @@ fn page(t: T) -> Response { } } -async fn index(State(shared): State) -> Response { - page(IndexTemplate { active: "home", status: view(&shared) }) +async fn index(State(st): State) -> Response { + let commits = st.commits.read().ok().map(|g| g.clone()).unwrap_or_default(); + page(IndexTemplate { active: "home", status: view(&st.stats), commits }) } async fn features() -> Response { page(FeaturesTemplate { active: "features" }) @@ -179,8 +190,11 @@ async fn main() { ) .init(); - let shared: stats::Shared = Default::default(); - stats::spawn_updater(shared.clone()); + let stats: stats::Shared = Default::default(); + stats::spawn_updater(stats.clone()); + let commits: commits::Shared = Default::default(); + commits::spawn_updater(commits.clone()); + let state = AppState { stats, commits }; let app = Router::new() .route("/", get(index)) @@ -197,7 +211,7 @@ async fn main() { .fallback(not_found) .layer(CompressionLayer::new()) .layer(TraceLayer::new_for_http()) - .with_state(shared); + .with_state(state); let addr: SocketAddr = std::env::var("ECHO_WEB_ADDR") .unwrap_or_else(|_| "127.0.0.1:8099".into()) diff --git a/templates/index.html b/templates/index.html index a08a769..9603b9a 100644 --- a/templates/index.html +++ b/templates/index.html @@ -50,15 +50,14 @@

Latest

    + {% for c in commits %}
  • - Aug 2026 -
    echoIRCd 5.0 — SASL ECDSA-NIST256P-CHALLENGE, post-quantum TLS 1.3, the operator privilege model, and a native anti-abuse engine.
    -
  • -
  • - Aug 2026 -
    echo services — key-based login (SET PUBKEY), event-sourced accounts, and SASL relayed over the server link.
    + {{ c.date }} +
    {{ c.short }} — {{ c.subject }}
  • + {% endfor %}
+

Full changelog →