home: latest panel reads the last 3 commits from git

This commit is contained in:
Jean Chevronnet 2026-08-31 00:40:07 +00:00
parent b3bcf67ee0
commit 4b700e280c
No known key found for this signature in database
GPG key ID: 439666D63A9477E4
3 changed files with 123 additions and 11 deletions

99
src/commits.rs Normal file
View file

@ -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<RwLock<Vec<Commit>>>;
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<Vec<Commit>> {
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<Commit> = 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::<usize>(), d.parse::<u32>()) {
if (1..=12).contains(&m) {
return format!("{} {}, {}", M[m - 1], day, y);
}
}
}
iso.to_string()
}

View file

@ -2,6 +2,7 @@
//! templates, a Markdown documentation section, and a small live "network status" //! templates, a Markdown documentation section, and a small live "network status"
//! pulled from the running ircd. //! pulled from the running ircd.
mod commits;
mod docs; mod docs;
mod stats; 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)] #[derive(Template)]
#[template(path = "index.html")] #[template(path = "index.html")]
struct IndexTemplate { struct IndexTemplate {
active: &'static str, active: &'static str,
status: Status, status: Status,
commits: Vec<commits::Commit>,
} }
#[derive(Template)] #[derive(Template)]
@ -92,8 +102,9 @@ fn page<T: Template>(t: T) -> Response {
} }
} }
async fn index(State(shared): State<stats::Shared>) -> Response { async fn index(State(st): State<AppState>) -> Response {
page(IndexTemplate { active: "home", status: view(&shared) }) 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 { async fn features() -> Response {
page(FeaturesTemplate { active: "features" }) page(FeaturesTemplate { active: "features" })
@ -179,8 +190,11 @@ async fn main() {
) )
.init(); .init();
let shared: stats::Shared = Default::default(); let stats: stats::Shared = Default::default();
stats::spawn_updater(shared.clone()); 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() let app = Router::new()
.route("/", get(index)) .route("/", get(index))
@ -197,7 +211,7 @@ async fn main() {
.fallback(not_found) .fallback(not_found)
.layer(CompressionLayer::new()) .layer(CompressionLayer::new())
.layer(TraceLayer::new_for_http()) .layer(TraceLayer::new_for_http())
.with_state(shared); .with_state(state);
let addr: SocketAddr = std::env::var("ECHO_WEB_ADDR") let addr: SocketAddr = std::env::var("ECHO_WEB_ADDR")
.unwrap_or_else(|_| "127.0.0.1:8099".into()) .unwrap_or_else(|_| "127.0.0.1:8099".into())

View file

@ -50,15 +50,14 @@
<section class="wrap latest"> <section class="wrap latest">
<h2>Latest</h2> <h2>Latest</h2>
<ul class="news"> <ul class="news">
{% for c in commits %}
<li> <li>
<span class="news-date">Aug 2026</span> <span class="news-date">{{ c.date }}</span>
<div><a href="https://git.devtronic.pro/echo/echoIRCd"><b>echoIRCd 5.0</b></a> — SASL <b>ECDSA-NIST256P-CHALLENGE</b>, post-quantum TLS&nbsp;1.3, the operator privilege model, and a native anti-abuse engine.</div> <div><a href="{{ c.url }}"><code>{{ c.short }}</code></a> — {{ c.subject }}</div>
</li>
<li>
<span class="news-date">Aug 2026</span>
<div><a href="https://git.devtronic.pro/echo/echo"><b>echo services</b></a> — key-based login (<code>SET PUBKEY</code>), event-sourced accounts, and SASL relayed over the server link.</div>
</li> </li>
{% endfor %}
</ul> </ul>
<p class="h-center more"><a href="/docs/changelog">Full changelog &#8594;</a></p>
</section> </section>
<section class="ctaband"> <section class="ctaband">