home: latest panel reads the last 3 commits from git
This commit is contained in:
parent
b3bcf67ee0
commit
4b700e280c
3 changed files with 123 additions and 11 deletions
99
src/commits.rs
Normal file
99
src/commits.rs
Normal 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()
|
||||
}
|
||||
24
src/main.rs
24
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<commits::Commit>,
|
||||
}
|
||||
|
||||
#[derive(Template)]
|
||||
|
|
@ -92,8 +102,9 @@ fn page<T: Template>(t: T) -> Response {
|
|||
}
|
||||
}
|
||||
|
||||
async fn index(State(shared): State<stats::Shared>) -> Response {
|
||||
page(IndexTemplate { active: "home", status: view(&shared) })
|
||||
async fn index(State(st): State<AppState>) -> 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())
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue