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

1
Cargo.lock generated
View file

@ -717,6 +717,7 @@ version = "1.53.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "202caea871b69668250d242070849eb495be178ed697a3e98aebce5bc81a0bed"
dependencies = [
"bytes",
"libc",
"mio",
"pin-project-lite",

View file

@ -7,7 +7,7 @@ license = "MIT"
[dependencies]
axum = "0.7"
tokio = { version = "1", features = ["macros", "rt-multi-thread", "net", "signal"] }
tokio = { version = "1", features = ["macros", "rt-multi-thread", "net", "signal", "io-util", "time"] }
askama = "0.12"
tower-http = { version = "0.6", features = ["trace", "compression-gzip"] }
tracing = "0.1"

View file

@ -1,9 +1,11 @@
//! The echoIRCd project website: a small Axum server that renders a handful of
//! compile-time Askama templates. Templates and static assets are baked into the
//! binary, so the deployed artifact is fully self-contained.
//! The echoIRCd project website: an Axum server rendering compile-time Askama
//! templates, with a small live "network status" pulled from the running ircd.
mod stats;
use askama::Template;
use axum::{
extract::State,
http::{header, StatusCode},
response::{Html, IntoResponse, Response},
routing::get,
@ -12,10 +14,27 @@ use axum::{
use std::net::SocketAddr;
use tower_http::{compression::CompressionLayer, trace::TraceLayer};
/// Display-ready snapshot of the live network state for templates.
struct Status {
online: bool,
users: String,
version: String,
}
fn view(shared: &stats::Shared) -> Status {
let s = shared.read().ok().map(|g| g.clone()).unwrap_or_default();
Status {
online: s.online,
users: s.users.map(|n| n.to_string()).unwrap_or_else(|| "".into()),
version: s.version.unwrap_or_else(|| "5.0.0".into()),
}
}
#[derive(Template)]
#[template(path = "index.html")]
struct IndexTemplate {
active: &'static str,
status: Status,
}
#[derive(Template)]
@ -30,7 +49,6 @@ struct ConnectTemplate {
active: &'static str,
}
/// Render a template to an HTML response, or a 500 if rendering fails.
fn page<T: Template>(t: T) -> Response {
match t.render() {
Ok(body) => Html(body).into_response(),
@ -41,8 +59,8 @@ fn page<T: Template>(t: T) -> Response {
}
}
async fn index() -> Response {
page(IndexTemplate { active: "home" })
async fn index(State(shared): State<stats::Shared>) -> Response {
page(IndexTemplate { active: "home", status: view(&shared) })
}
async fn features() -> Response {
page(FeaturesTemplate { active: "features" })
@ -66,7 +84,6 @@ async fn favicon() -> impl IntoResponse {
async fn health() -> &'static str {
"ok"
}
async fn not_found() -> Response {
(
StatusCode::NOT_FOUND,
@ -84,6 +101,9 @@ async fn main() {
)
.init();
let shared: stats::Shared = Default::default();
stats::spawn_updater(shared.clone());
let app = Router::new()
.route("/", get(index))
.route("/features", get(features))
@ -93,7 +113,8 @@ async fn main() {
.route("/health", get(health))
.fallback(not_found)
.layer(CompressionLayer::new())
.layer(TraceLayer::new_for_http());
.layer(TraceLayer::new_for_http())
.with_state(shared);
let addr: SocketAddr = std::env::var("ECHO_WEB_ADDR")
.unwrap_or_else(|_| "127.0.0.1:8099".into())
@ -111,7 +132,6 @@ async fn main() {
.expect("server error");
}
/// Resolve on SIGINT (Ctrl-C) or SIGTERM (systemd stop) for a clean shutdown.
async fn shutdown() {
use tokio::signal::unix::{signal, SignalKind};
let mut term = signal(SignalKind::terminate()).expect("install SIGTERM handler");

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())
}
}

View file

@ -1,5 +1,6 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64">
<rect width="64" height="64" rx="13" fill="#0d0e12"/>
<text x="13" y="45" font-family="ui-monospace,monospace" font-size="33" font-weight="700" fill="#e2a25a">&#8250;</text>
<rect x="37" y="27" width="13" height="20" rx="1" fill="#c8ccd4"/>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32">
<rect width="32" height="32" rx="7" fill="#12a594"/>
<circle cx="10" cy="16" r="2.8" fill="#fff"/>
<path d="M15.5 10a8 8 0 0 1 0 12" fill="none" stroke="#fff" stroke-width="2.2" stroke-linecap="round"/>
<path d="M19.5 6.5a13 13 0 0 1 0 19" fill="none" stroke="#eafffb" stroke-width="2.2" stroke-linecap="round"/>
</svg>

Before

Width:  |  Height:  |  Size: 315 B

After

Width:  |  Height:  |  Size: 389 B

Before After
Before After

View file

@ -1,126 +1,141 @@
/* echoIRCd — the site as an IRC client. Monospace, warm phosphor, real IRC grammar. */
/* echoIRCd — a friendly, light project site. Teal is echo's own colour. */
:root{
--bg:#08090c; --win:#0d0e12; --panel:#14161c; --panel2:#0f1116;
--edge:#22252e; --edge2:#2d313c;
--fg:#c8ccd4; --dim:#767b85; --dimmer:#4a4e58;
--amber:#e2a25a; --green:#5fbf6b; --blue:#5f9bef; --magenta:#c58af0; --cyan:#4fcccf; --red:#ec6d6d;
--link:#6aa6f2; --ok:#5fbf6b;
--mono:ui-monospace,"SF Mono","JetBrains Mono","Cascadia Code",Menlo,Consolas,"DejaVu Sans Mono",monospace;
--teal:#12a594; --teal-d:#0e8577; --teal-dd:#0a6a5f; --teal-t:#e7f6f3;
--ink:#1b2733; --muted:#5c6773; --faint:#8b95a0;
--line:#e4e8eb; --line2:#d8dee2; --bg:#f5f6f7; --card:#ffffff;
--code-bg:#132430; --code-fg:#e7edf0; --cmd:#6fe0cf;
--amber:#e8a13c; --green:#22b573; --red:#e0604f;
--sans:"Rubik",system-ui,-apple-system,"Segoe UI",Roboto,Helvetica,Arial,sans-serif;
--mono:ui-monospace,"SF Mono",Menlo,Consolas,"DejaVu Sans Mono",monospace;
--wrap:1080px;
}
*{box-sizing:border-box}
html{-webkit-text-size-adjust:100%}
body{
margin:0;background:var(--bg);color:var(--fg);
font:14.5px/1.65 var(--mono);
background-image:radial-gradient(1200px 600px at 50% -10%,rgba(226,162,90,.05),transparent 70%);
margin:0;color:var(--ink);font-family:var(--sans);font-size:16px;line-height:1.65;
background-color:var(--bg);
background-image:radial-gradient(rgba(18,165,148,.06) 1px,transparent 1px);
background-size:24px 24px;background-position:-12px -12px;
-webkit-font-smoothing:antialiased;
}
a{color:var(--link);text-decoration:none}
h1,h2,h3{line-height:1.15;margin:0 0 .5rem;color:var(--ink);font-weight:700;letter-spacing:-.01em}
p{margin:0 0 1rem}
a{color:var(--teal-d);text-decoration:none}
a:hover{text-decoration:underline}
b{color:var(--fg);font-weight:600}
.dim{color:var(--dim)}
.kw{color:var(--amber)}
.ok{color:var(--ok)}
.i{font-style:italic}
.n-amber{color:var(--amber)} .n-green{color:var(--green)} .n-blue{color:var(--blue)}
.n-magenta{color:var(--magenta)} .n-cyan{color:var(--cyan)} .n-red{color:var(--red)}
code{font-family:var(--mono);font-size:.9em;background:rgba(18,165,148,.09);color:var(--teal-dd);padding:.08em .38em;border-radius:5px}
.wrap{max-width:var(--wrap);margin:0 auto;padding:0 1.25rem}
.mono-line{font-family:var(--mono);color:var(--muted)}
/* the client window */
.client{
max-width:1060px;margin:1.6rem auto;background:var(--win);
border:1px solid var(--edge);border-radius:9px;overflow:hidden;
box-shadow:0 24px 60px -30px rgba(0,0,0,.8),0 0 0 1px rgba(255,255,255,.01);
/* buttons */
.btn{display:inline-block;font-weight:500;font-size:.98rem;padding:.62rem 1.15rem;border-radius:9px;border:1.5px solid transparent;transition:transform .05s ease}
.btn:hover{text-decoration:none;transform:translateY(-1px)}
.btn-primary{background:var(--teal);color:#fff;box-shadow:0 8px 18px -8px rgba(18,165,148,.7)}
.btn-primary:hover{background:var(--teal-d)}
.btn-ghost{border-color:var(--line2);color:var(--ink);background:#fff}
.btn-ghost:hover{border-color:var(--teal);color:var(--teal-d)}
/* top bar */
.topbar{background:var(--teal);color:#fff;position:sticky;top:0;z-index:20;box-shadow:0 2px 12px -6px rgba(0,0,0,.35)}
.bar-in{max-width:var(--wrap);margin:0 auto;padding:.7rem 1.25rem;display:flex;align-items:center;gap:1.5rem;flex-wrap:wrap}
.brand{display:flex;align-items:center;gap:.55rem;color:#fff;font-weight:700;font-size:1.25rem}
.brand:hover{text-decoration:none;opacity:.95}
.mark{width:26px;height:26px;color:#fff;flex:none}
.mainnav{margin-left:auto;display:flex;gap:1.4rem;align-items:center;flex-wrap:wrap}
.mainnav a{color:#eafffb;font-weight:500;font-size:.96rem;opacity:.9;padding:.15rem 0;border-bottom:2px solid transparent}
.mainnav a:hover{opacity:1;text-decoration:none}
.mainnav a.on{opacity:1;border-bottom-color:#fff}
.mainnav a.ext{opacity:.8}
/* hero */
.hero{padding:3.4rem 0 2.6rem}
.hero-in{max-width:var(--wrap);margin:0 auto;padding:0 1.25rem;display:grid;grid-template-columns:1.4fr 1fr;gap:2.5rem;align-items:center}
.eyebrow{color:var(--teal-d);font-weight:600;font-size:.82rem;text-transform:uppercase;letter-spacing:.14em;margin:0 0 .8rem}
.hero h1{font-size:clamp(2.4rem,5.4vw,3.7rem);margin-bottom:1rem}
.hero-sub{color:var(--muted);font-size:1.12rem;max-width:34rem;margin-bottom:1.6rem}
.cta{display:flex;gap:.7rem;flex-wrap:wrap}
/* status card */
.statuscard{background:var(--card);border:1px solid var(--line);border-radius:14px;padding:1.2rem 1.3rem;box-shadow:0 20px 40px -28px rgba(20,40,60,.4)}
.sc-head{display:flex;align-items:center;justify-content:space-between;margin-bottom:.7rem}
.sc-title{font-size:.72rem;text-transform:uppercase;letter-spacing:.15em;color:var(--faint);font-weight:600}
.dot{width:9px;height:9px;border-radius:50%}
.dot.on{background:var(--green);box-shadow:0 0 0 0 rgba(34,181,115,.6);animation:pulse 2s infinite}
.dot.off{background:var(--faint)}
@keyframes pulse{0%{box-shadow:0 0 0 0 rgba(34,181,115,.5)}70%{box-shadow:0 0 0 7px rgba(34,181,115,0)}100%{box-shadow:0 0 0 0 rgba(34,181,115,0)}}
.sc-rows{margin:0}
.sc-rows>div{display:flex;justify-content:space-between;align-items:baseline;padding:.42rem 0;border-bottom:1px dashed var(--line)}
.sc-rows>div:last-child{border-bottom:0}
.sc-rows dt{color:var(--muted);font-size:.9rem}
.sc-rows dd{margin:0;font-weight:500;font-family:var(--mono);font-size:.9rem;color:var(--ink)}
.sc-rows .live{color:var(--green)} .sc-rows .down{color:var(--red)}
.sc-foot{margin-top:.7rem;font-size:.78rem;color:var(--faint)}
/* feature band */
.band{background:linear-gradient(180deg,#fff,#fbfcfc);border-top:1px solid var(--line);border-bottom:1px solid var(--line);padding:3.2rem 0}
.h-center{text-align:center}
.band h2{font-size:1.9rem;margin-bottom:2rem}
.cards{display:grid;grid-template-columns:repeat(3,1fr);gap:1.1rem}
.fcard{background:var(--card);border:1px solid var(--line);border-radius:12px;padding:1.3rem 1.4rem;transition:border-color .15s,box-shadow .15s}
.fcard:hover{border-color:var(--teal);box-shadow:0 16px 30px -24px rgba(18,165,148,.6)}
.fcard h3{font-size:1.12rem;color:var(--teal-dd)}
.fcard p{margin:0;color:var(--muted);font-size:.95rem}
.more{margin-top:1.8rem}.more a{font-weight:600}
/* latest / news */
.latest{padding:3rem 1.25rem}
.latest h2{font-size:1.6rem;margin-bottom:1.2rem}
.news{list-style:none;margin:0;padding:0}
.news li{display:flex;gap:1.2rem;padding:1rem 0;border-top:1px solid var(--line)}
.news li:last-child{border-bottom:1px solid var(--line)}
.news-date{flex:none;width:6.5rem;color:var(--faint);font-size:.85rem;font-family:var(--mono);padding-top:.15rem}
.news div{color:var(--muted)}
/* cta band */
.ctaband{background:var(--teal-t);border-top:1px solid #cdeae4}
.ctaband-in{display:flex;align-items:center;justify-content:space-between;gap:1.5rem;padding:2.3rem 1.25rem;flex-wrap:wrap}
.ctaband h2{color:var(--teal-dd);margin-bottom:.2rem;font-size:1.5rem}
.ctaband .mono-line{color:var(--teal-d)}
/* page head + doc pages */
.pagehead{background:linear-gradient(180deg,#fff,#fbfcfc);border-bottom:1px solid var(--line);padding:2.6rem 0 2rem}
.pagehead h1{font-size:2.3rem}
.pagehead p{color:var(--muted);margin:0;max-width:44rem}
.doc{padding:2.4rem 1.25rem}
.doc-block{margin:0 0 2.2rem}
.doc-block h2{font-size:1.3rem;color:var(--ink);padding-bottom:.5rem;border-bottom:2px solid var(--teal-t);margin-bottom:.9rem}
.doc-block ul{margin:.3rem 0;padding-left:1.2rem}
.doc-block li{margin:.4rem 0;color:var(--muted)}
.doc-block li b{color:var(--ink)}
.chips{display:flex;flex-wrap:wrap;gap:.4rem;margin-top:.6rem}
.chips span{font-family:var(--mono);font-size:.8rem;background:#fff;border:1px solid var(--line2);color:var(--muted);border-radius:6px;padding:.22rem .5rem}
.detail{border-collapse:collapse;width:100%;max-width:34rem;margin:.4rem 0 1rem}
.detail th{text-align:left;font-weight:500;color:var(--muted);padding:.5rem .9rem .5rem 0;border-bottom:1px solid var(--line);white-space:nowrap;width:1%;vertical-align:top}
.detail td{padding:.5rem 0;border-bottom:1px solid var(--line)}
.note{color:var(--faint);font-size:.9rem}
.webchat{margin:.6rem 0 0}
.code{background:var(--code-bg);color:var(--code-fg);border-radius:10px;padding:.9rem 1.1rem;overflow-x:auto;
font-family:var(--mono);font-size:.88rem;line-height:1.7;white-space:pre;margin:.5rem 0}
/* footer */
.site-foot{background:#0f1a22;color:#aeb8c0;margin-top:1rem}
.foot-in{max-width:var(--wrap);margin:0 auto;padding:2rem 1.25rem;display:flex;align-items:center;gap:1rem 1.5rem;flex-wrap:wrap}
.foot-brand{display:flex;align-items:center;gap:.5rem;color:#fff;font-weight:700}
.foot-brand .mark{width:22px;height:22px;color:var(--teal)}
.foot-tag{margin:0;color:#8994a0;font-size:.9rem}
.foot-tag code{background:rgba(255,255,255,.08);color:#cfe3dd}
.foot-links{margin-left:auto;display:flex;gap:1.2rem;flex-wrap:wrap}
.foot-links a{color:#aeb8c0;font-size:.9rem}
.foot-links a:hover{color:#fff}
@media(max-width:820px){
.hero-in{grid-template-columns:1fr;gap:1.6rem}
.statuscard{max-width:26rem}
.cards{grid-template-columns:1fr 1fr}
}
/* title bar */
.titlebar{display:flex;align-items:center;gap:.6rem;padding:.55rem .9rem;background:var(--panel);border-bottom:1px solid var(--edge)}
.conn{width:9px;height:9px;border-radius:50%;background:var(--green);box-shadow:0 0 8px var(--green);flex:none}
.tb-name{color:var(--fg);font-weight:600}
.tb-server{margin-left:auto;color:var(--dim);font-size:.82rem}
/* tabs */
.tabs{display:flex;gap:.15rem;padding:0 .5rem;background:var(--panel);border-bottom:1px solid var(--edge);overflow-x:auto}
.tab{padding:.5rem .8rem;color:var(--dim);border-bottom:2px solid transparent;white-space:nowrap}
.tab:hover{color:var(--fg);text-decoration:none}
.tab.on{color:var(--amber);border-bottom-color:var(--amber)}
.tab.ext{margin-left:auto;color:var(--dimmer)}
/* topic */
.topic{padding:.5rem .95rem;background:var(--panel2);border-bottom:1px solid var(--edge);color:var(--dim);font-size:.85rem}
.topic .tp{color:var(--amber);margin-right:.35rem}
/* workspace = buffer + nicklist */
.workspace{display:flex;align-items:stretch}
.buffer{flex:1;min-width:0;padding:1.1rem 1.1rem 1.4rem}
.nicklist{width:13.5rem;flex:none;background:var(--panel2);border-left:1px solid var(--edge);padding:1rem .85rem;font-size:.86rem}
.nl-head{color:var(--dim);text-transform:uppercase;letter-spacing:.12em;font-size:.72rem;margin-bottom:.6rem}
.nicklist ul{list-style:none;margin:0;padding:0}
.nicklist li{display:flex;align-items:center;gap:.15rem;padding:.14rem 0;white-space:nowrap}
.nicklist .s{display:inline-block;width:1.1em;text-align:center;color:var(--dim)}
.bf{margin-left:.35rem;font-size:.62rem;color:var(--dimmer);border:1px solid var(--edge2);border-radius:3px;padding:0 .25em}
/* buffer lines — [time] [gutter] [message] */
.ln{display:grid;grid-template-columns:3.1em 7em 1fr;column-gap:.75em;align-items:baseline;padding:.09rem 0}
.ln .t{color:var(--dimmer);font-size:.82em}
.ln .g{text-align:right;white-space:nowrap;color:var(--dim)}
.ln .m{min-width:0;word-wrap:break-word;overflow-wrap:anywhere}
.ln .m a{color:var(--link)}
/* banner / wordmark */
.banner{padding:.6rem 0 1.4rem;border-bottom:1px dashed var(--edge);margin-bottom:1rem}
.wordmark{font-size:clamp(2.3rem,7vw,3.7rem);font-weight:700;letter-spacing:-2px;color:var(--fg);line-height:1}
.wordmark .w-hi{color:var(--amber)}
.w-sub{color:var(--dim);margin-top:.5rem;font-size:.95rem}
.cur{display:inline-block;width:.55em;color:var(--amber);animation:blink 1.1s steps(1) infinite}
.cur-lg{width:.5em;margin-left:.06em}
@keyframes blink{50%{opacity:0}}
@media(prefers-reduced-motion:reduce){.cur{animation:none}}
/* quickfacts */
.quickfacts{display:flex;flex-wrap:wrap;gap:.5rem;margin-top:1.3rem}
.qf{border:1px solid var(--edge);border-radius:6px;padding:.4rem .7rem;color:var(--dim);background:var(--panel2);font-size:.85rem}
.qf:hover{border-color:var(--edge2);color:var(--fg);text-decoration:none}
.qf .qf-k{color:var(--amber)}
/* feature groups */
.grp{margin:1.5rem 0}
.grp-h{color:var(--amber);font-size:.82rem;text-transform:uppercase;letter-spacing:.14em;padding-bottom:.4rem;margin-bottom:.7rem;border-bottom:1px solid var(--edge);position:relative}
.grp-h::before{content:"── ";color:var(--dimmer)}
.bul{list-style:none;margin:0;padding:0}
.bul li{padding:.22rem 0 .22rem 1.3em;position:relative;color:var(--fg)}
.bul li::before{content:"·";position:absolute;left:.3em;color:var(--amber)}
.srvline{display:grid;grid-template-columns:7em 1fr;column-gap:.75em;align-items:baseline;margin:.2rem 0 .8rem}
.srvline .g{text-align:right}
.capline{color:var(--dim);word-break:break-word;line-height:1.7}
/* connect page: tables + code */
.detail{width:100%;border-collapse:collapse;margin:.7rem 0 1rem;font-size:.9rem}
.detail th{text-align:left;color:var(--dim);font-weight:400;padding:.4rem .8rem .4rem 0;border-bottom:1px solid var(--edge);white-space:nowrap;vertical-align:top;width:1%}
.detail td{padding:.4rem 0;border-bottom:1px solid var(--edge);color:var(--fg)}
.note{color:var(--dim);margin:.2rem 0 .6rem;font-size:.9rem}
.code{background:var(--panel2);border:1px solid var(--edge);border-left:2px solid var(--amber);border-radius:6px;
padding:.8rem 1rem;overflow-x:auto;color:var(--green);font-size:.88rem;line-height:1.75;white-space:pre;margin:.4rem 0}
.webchat{display:flex;align-items:center;gap:.8rem;flex-wrap:wrap;margin:1.1rem 0}
.btn{display:inline-block;border:1px solid var(--edge2);background:var(--panel);color:var(--fg);padding:.4rem .85rem;border-radius:6px;font-size:.88rem}
.btn:hover{border-color:var(--amber);color:var(--amber);text-decoration:none}
/* input + status bars */
.inputbar{display:flex;align-items:baseline;gap:.5rem;padding:.55rem .95rem;background:var(--panel);border-top:1px solid var(--edge);font-size:.9rem;overflow:hidden}
.ib-ch{color:var(--dim)}
.ib-prompt{color:var(--amber)}
.ib-text{color:var(--fg);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.statusbar{display:flex;align-items:center;gap:.8rem;padding:.4rem .95rem;background:var(--panel2);border-top:1px solid var(--edge);font-size:.74rem;color:var(--dim)}
.statusbar .sb-m{margin:0 auto;color:var(--amber);opacity:.75}
.statusbar a{color:var(--dim)}
.statusbar a:hover{color:var(--fg)}
@media(max-width:720px){
.client{margin:0;border:0;border-radius:0;min-height:100vh}
.workspace{flex-direction:column}
.nicklist{width:auto;border-left:0;border-top:1px solid var(--edge);order:2}
.nicklist ul{display:flex;flex-wrap:wrap;gap:.1rem .9rem}
.ln{grid-template-columns:5.5em 1fr}
.ln .t{display:none}
.srvline{grid-template-columns:5.5em 1fr}
.tb-server{display:none}
@media(max-width:560px){
.cards{grid-template-columns:1fr}
.mainnav{gap:1rem}
.news li{flex-direction:column;gap:.2rem}
.news-date{width:auto}
.ctaband-in{flex-direction:column;align-items:flex-start}
}

View file

@ -3,67 +3,60 @@
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="color-scheme" content="dark">
<title>{% block title %}echoIRCd{% endblock %}</title>
<meta name="description" content="echoIRCd — an IRC network written from scratch in Rust. Full IRCv3, modern TLS, native services.">
<title>{% block title %}echoIRCd — a from-scratch IRC server in Rust{% endblock %}</title>
<meta name="description" content="echoIRCd is a modern IRC daemon and services suite, written from scratch in Rust — full IRCv3, modern TLS, SASL.">
<meta property="og:title" content="echoIRCd">
<meta property="og:description" content="An IRC network written from scratch in Rust.">
<meta property="og:description" content="A from-scratch IRC server, written in Rust.">
<meta property="og:type" content="website">
<link rel="icon" href="/favicon.svg" type="image/svg+xml">
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Rubik:wght@400;500;600;700&display=swap" rel="stylesheet">
<link rel="stylesheet" href="/static/style.css">
</head>
<body>
<div class="client">
<div class="titlebar">
<span class="conn" title="connected"></span>
<span class="tb-name">echoircd.org</span>
<span class="tb-server">irc.echoircd.org · +6697 · TLS1.3</span>
</div>
<nav class="tabs">
<a href="/" class="tab{% if active == "home" %} on{% endif %}">#home</a>
<a href="/features" class="tab{% if active == "features" %} on{% endif %}">#features</a>
<a href="/connect" class="tab{% if active == "connect" %} on{% endif %}">#connect</a>
<a href="https://git.devtronic.pro/echo/echoIRCd" class="tab ext">git&#8599;</a>
<header class="topbar">
<div class="bar-in">
<a class="brand" href="/">
<svg class="mark" viewBox="0 0 32 32" aria-hidden="true">
<circle cx="8" cy="16" r="3.1" fill="currentColor"/>
<path d="M14 9.5a9 9 0 0 1 0 13" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round"/>
<path d="M18.5 5.5a15 15 0 0 1 0 21" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round"/>
</svg>
<span class="brand-name">echoIRCd</span>
</a>
<nav class="mainnav">
<a href="/"{% if active == "home" %} class="on"{% endif %}>Home</a>
<a href="/features"{% if active == "features" %} class="on"{% endif %}>Features</a>
<a href="/connect"{% if active == "connect" %} class="on"{% endif %}>Connect</a>
<a href="https://orbit.devtronic.pro">Web&nbsp;client</a>
<a href="https://git.devtronic.pro/echo/echoIRCd" class="ext">Source&#8599;</a>
</nav>
<div class="topic"><span class="tp">»</span> {% block topic %}echoIRCd — a native-Rust IRC daemon &amp; services{% endblock %}</div>
<div class="workspace">
<div class="buffer">
{% block buffer %}{% endblock %}
</div>
<aside class="nicklist">
{% block nicklist %}
<div class="nl-head">names · 9</div>
<ul class="nl">
<li><span class="s n-amber">~</span><span class="n-amber">echoircd</span></li>
<li><span class="s n-green">@</span><span class="n-green">NickServ</span><span class="bf">B</span></li>
<li><span class="s n-blue">@</span><span class="n-blue">ChanServ</span><span class="bf">B</span></li>
<li><span class="s n-magenta">@</span><span class="n-magenta">OperServ</span><span class="bf">B</span></li>
<li><span class="s n-cyan">@</span><span class="n-cyan">MemoServ</span><span class="bf">B</span></li>
<li><span class="s n-blue">+</span><span>TLS1.3</span></li>
<li><span class="s n-blue">+</span><span>IRCv3</span></li>
<li><span class="s n-blue">+</span><span>SASL</span></li>
<li><span class="s dim">·</span><span class="dim">you</span></li>
</ul>
{% endblock %}
</aside>
</div>
</header>
<div class="inputbar">
<span class="ib-ch">[{% block chan %}#home{% endblock %}]</span>
<span class="ib-prompt"></span>
<span class="ib-text">{% block cmd %}/connect{% endblock %}</span><span class="cur"></span>
</div>
<main>
{% block content %}{% endblock %}
</main>
<div class="statusbar">
<span class="sb-l">echoIRCd 5.0.0</span>
<span class="sb-m">#![forbid(unsafe_code)]</span>
<span class="sb-r"><a href="https://git.devtronic.pro/echo/echo">services</a> · <a href="https://git.devtronic.pro/echo/website">website</a></span>
<footer class="site-foot">
<div class="foot-in">
<div class="foot-brand">
<svg class="mark" viewBox="0 0 32 32" aria-hidden="true">
<circle cx="8" cy="16" r="3.1" fill="currentColor"/>
<path d="M14 9.5a9 9 0 0 1 0 13" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round"/>
<path d="M18.5 5.5a15 15 0 0 1 0 21" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round"/>
</svg>
<span>echoIRCd</span>
</div>
</div>
<p class="foot-tag">A native-Rust IRC daemon &amp; services. No forks, no C, no <code>unsafe</code>.</p>
<nav class="foot-links">
<a href="https://git.devtronic.pro/echo/echoIRCd">echoIRCd</a>
<a href="https://git.devtronic.pro/echo/echo">services</a>
<a href="https://git.devtronic.pro/echo/website">website</a>
<a href="https://orbit.devtronic.pro">web client</a>
</nav>
</div>
</footer>
</body>
</html>

View file

@ -1,54 +1,55 @@
{% extends "base.html" %}
{% block title %}#connect — echoIRCd{% endblock %}
{% block topic %}#connect — point any client at irc.echoircd.org, or use the browser{% endblock %}
{% block chan %}#connect{% endblock %}
{% block cmd %}/server irc.echoircd.org +6697{% endblock %}
{% block buffer %}
<div class="ln"><span class="t">19:33</span><span class="g n-cyan">&lt;you&gt;</span><span class="m">/server irc.echoircd.org +6697</span></div>
<div class="ln"><span class="t">19:33</span><span class="g dim">-!-</span><span class="m dim">connecting to irc.echoircd.org:6697 … TLS1.3 up (X25519MLKEM768)</span></div>
<div class="ln"><span class="t">19:33</span><span class="g dim">-!-</span><span class="m dim">Welcome to the echoiRCd IRC Network</span></div>
{% block title %}Connect — echoIRCd{% endblock %}
{% block content %}
<section class="pagehead">
<div class="wrap">
<h1>Connect</h1>
<p>Point any IRC client at the network — or open it right in your browser.</p>
</div>
</section>
<section class="wrap doc">
<div class="doc-block">
<table class="detail">
<tr><th>server</th><td>irc.echoircd.org</td></tr>
<tr><th>tls (recommended)</th><td>6697</td></tr>
<tr><th>plaintext</th><td>6667</td></tr>
<tr><th>network</th><td>echoiRCd</td></tr>
<tr><th>Server</th><td><code>irc.echoircd.org</code></td></tr>
<tr><th>TLS (recommended)</th><td><code>6697</code></td></tr>
<tr><th>Plaintext</th><td><code>6667</code></td></tr>
<tr><th>Network</th><td>echoiRCd</td></tr>
</table>
<div class="webchat">
<span class="dim">prefer the browser?</span>
<a class="btn" href="https://orbit.devtronic.pro">open the web client &#8599;</a>
<p class="webchat"><a class="btn btn-ghost" href="https://orbit.devtronic.pro">Open the web client &#8599;</a></p>
</div>
<div class="grp">
<div class="grp-h">client quick-start</div>
<p class="note">most clients take a one-line server string; the leading <span class="kw">+</span> means TLS.</p>
<div class="doc-block">
<h2>Client quick-start</h2>
<p>Most clients take a one-line server string; the leading <code>+</code> on the port means TLS.</p>
<pre class="code">/server irc.echoircd.org +6697
/join #echoircd</pre>
<table class="detail">
<tr><th>HexChat</th><td>add irc.echoircd.org/+6697 · tick “Use SSL”</td></tr>
<tr><th>WeeChat</th><td>/server add echo irc.echoircd.org/6697 -tls</td></tr>
<tr><th>irssi</th><td>/connect -tls irc.echoircd.org 6697</td></tr>
<tr><th>mIRC</th><td>/server irc.echoircd.org +6697</td></tr>
<tr><th>HexChat</th><td>add <code>irc.echoircd.org/+6697</code> · tick “Use SSL”</td></tr>
<tr><th>WeeChat</th><td><code>/server add echo irc.echoircd.org/6697 -tls</code></td></tr>
<tr><th>irssi</th><td><code>/connect -tls irc.echoircd.org 6697</code></td></tr>
<tr><th>mIRC</th><td><code>/server irc.echoircd.org +6697</code></td></tr>
</table>
</div>
<div class="grp">
<div class="grp-h">register your nick</div>
<p class="note">keep your nick, found channels, and log in with SASL.</p>
<div class="doc-block">
<h2>Register your nick</h2>
<p>Registering lets you keep your nickname, found channels, and log in with SASL.</p>
<pre class="code">/msg NickServ REGISTER &lt;password&gt; &lt;email&gt;
/msg NickServ IDENTIFY &lt;password&gt;</pre>
</div>
<div class="grp">
<div class="grp-h">log in with a key (ecdsa)</div>
<p class="note">sign a server challenge with a NIST&nbsp;P-256 key instead of sending a password — nothing secret crosses the wire.</p>
<div class="doc-block">
<h2>Log in with a key (ECDSA)</h2>
<p>Sign a server challenge with a NIST&nbsp;P-256 key instead of sending a password — nothing
secret crosses the wire.</p>
<pre class="code"># generate a key, read its public half
ecdsatool keygen ~/.ecdsa.pem
ecdsatool pubkey ~/.ecdsa.pem
# store it, then pick ECDSA-NIST256P-CHALLENGE for SASL
/msg NickServ SET PUBKEY &lt;printed-public-key&gt;</pre>
<p class="note">point your client's SASL settings at the same key file.</p>
<p class="note">Point your client's SASL settings at the same key file.</p>
</div>
</section>
{% endblock %}

View file

@ -1,66 +1,85 @@
{% extends "base.html" %}
{% block title %}#features — echoIRCd{% endblock %}
{% block topic %}#features — what's inside the daemon &amp; services{% endblock %}
{% block chan %}#features{% endblock %}
{% block cmd %}/msg echoircd help{% endblock %}
{% block buffer %}
<div class="ln"><span class="t">19:31</span><span class="g n-cyan">&lt;you&gt;</span><span class="m">/msg echoircd what can you do</span></div>
<div class="ln"><span class="t">19:31</span><span class="g n-amber">&lt;echoircd&gt;</span><span class="m">plenty — here's the short tour. all of it is original, native Rust.</span></div>
{% block title %}Features — echoIRCd{% endblock %}
{% block content %}
<section class="pagehead">
<div class="wrap">
<h1>Features</h1>
<p>Everything below is original, native Rust. The InspIRCd protocol is a reference for wire
compatibility — never a source of code.</p>
</div>
</section>
<div class="grp">
<div class="grp-h">the daemon</div>
<ul class="bul">
<li><b>safe by construction</b><span class="kw">#![forbid(unsafe_code)]</span> across the tree, enforced in CI</li>
<li><b>reactor core</b> — single core thread + mio/epoll pool, zero-copy broadcast, parallel channel fan-out</li>
<li><b>full mode set</b> — every standard channel/user mode, host cloaking, custom prefixes</li>
<li><b>operators</b> — a privilege model: command grants, named privs, per-type mode allow-list</li>
<li><b>the edge</b> — connection classes, per-class flood/fakelag, RFC1413 ident, PROXY v1/v2</li>
<li><b>observability</b> — a metrics endpoint, JSON logging, native syslog</li>
<section class="wrap doc">
<div class="doc-block">
<h2>The daemon</h2>
<ul>
<li><b>Safe by construction</b><code>#![forbid(unsafe_code)]</code> across the tree, enforced in CI.</li>
<li><b>Reactor core</b> — a single core thread with a mio/epoll pool; zero-copy broadcast and parallel channel fan-out.</li>
<li><b>Full mode set</b> — every standard channel and user mode, host cloaking, custom prefixes.</li>
<li><b>Operators</b> — a privilege model: command grants, named privileges, and a per-type mode allow-list.</li>
<li><b>The edge</b> — connection classes, per-class flood/fakelag, RFC&nbsp;1413 ident, PROXY protocol v1/v2.</li>
<li><b>Observability</b> — a metrics endpoint, structured JSON logging, and native syslog.</li>
</ul>
</div>
<div class="grp">
<div class="grp-h">transport &amp; tls</div>
<ul class="bul">
<li><b>6697</b> direct TLS · <b>7799</b> WebSocket (wss) · <b>6667</b> plaintext</li>
<li>two backends — <b>OpenSSL</b> and <b>rustls</b> — both serving TLS 1.3</li>
<li>with OpenSSL 3.5 the handshake negotiates post-quantum <span class="kw">X25519MLKEM768</span></li>
<li>per-host <b>SNI</b> certificates, reloaded live on rehash</li>
<div class="doc-block">
<h2>Transport &amp; TLS</h2>
<ul>
<li><b>6697</b> direct TLS · <b>7799</b> WebSocket (wss) · <b>6667</b> plaintext.</li>
<li>Two interchangeable backends — <b>OpenSSL</b> and <b>rustls</b> — both serving TLS&nbsp;1.3.</li>
<li>With OpenSSL&nbsp;3.5 the handshake negotiates post-quantum <code>X25519MLKEM768</code>.</li>
<li>Per-host <b>SNI</b> certificates, reloaded live on rehash.</li>
</ul>
</div>
<div class="grp">
<div class="grp-h">ircv3</div>
<div class="srvline"><span class="g n-amber">&lt;echoircd&gt;</span><span class="capline">CAP * LS :server-time message-tags account-tag account-notify extended-join chghost multi-prefix away-notify invite-notify setname echo-message userhost-in-names batch labeled-response standard-replies extended-monitor draft/chathistory draft/event-playback draft/message-redaction draft/multiline draft/metadata-2 draft/read-marker draft/webpush draft/account-registration sts</span></div>
<div class="doc-block">
<h2>IRCv3</h2>
<p>Capabilities advertised to clients:</p>
<div class="chips">
<span>server-time</span><span>message-tags</span><span>account-tag</span><span>account-notify</span>
<span>extended-join</span><span>chghost</span><span>multi-prefix</span><span>away-notify</span>
<span>invite-notify</span><span>setname</span><span>echo-message</span><span>userhost-in-names</span>
<span>batch</span><span>labeled-response</span><span>standard-replies</span><span>extended-monitor</span>
<span>draft/chathistory</span><span>draft/event-playback</span><span>draft/message-redaction</span>
<span>draft/multiline</span><span>draft/metadata-2</span><span>draft/read-marker</span>
<span>draft/webpush</span><span>draft/account-registration</span><span>sts</span>
</div>
</div>
<div class="grp">
<div class="grp-h">sasl</div>
<div class="srvline"><span class="g n-amber">&lt;echoircd&gt;</span><span class="capline">sasl=PLAIN,EXTERNAL,SCRAM-SHA-256,ECDSA-NIST256P-CHALLENGE</span></div>
<ul class="bul">
<li><b>EXTERNAL</b> — authenticate by your TLS client-certificate fingerprint</li>
<li><b>SCRAM-SHA-256</b> &amp; <b>ECDSA-NIST256P-CHALLENGE</b> — challenge/response, no secret on the wire</li>
<div class="doc-block">
<h2>SASL</h2>
<ul>
<li><b>PLAIN</b> — classic username / password.</li>
<li><b>EXTERNAL</b> — authenticate by your TLS client-certificate fingerprint.</li>
<li><b>SCRAM-SHA-256</b> — challenge / response, no password on the wire.</li>
<li><b>ECDSA-NIST256P-CHALLENGE</b> — sign a challenge with a NIST&nbsp;P-256 key; the private key never leaves your client.</li>
</ul>
</div>
<div class="grp">
<div class="grp-h">services</div>
<ul class="bul">
<li><b>NickServ</b> — registration, grouped nicks, certs, public keys, vhosts, profiles</li>
<li><b>ChanServ</b> — founder/access, auto-op, akick, topic &amp; mode locks</li>
<li><b>OperServ · MemoServ · more</b> — network admin, offline messages, games</li>
<li><b>event-sourced</b> store; SASL relayed to services mechanism-agnostically</li>
<div class="doc-block">
<h2>Services</h2>
<ul>
<li><b>NickServ</b> — registration, grouped nicks, certificates, public keys, vhosts, profiles.</li>
<li><b>ChanServ</b> — founder/access, auto-op, akick, topic and mode locks.</li>
<li><b>OperServ · MemoServ · more</b> — network administration, offline messaging, and games.</li>
<li><b>Event-sourced</b> store; SASL relayed to services mechanism-agnostically.</li>
</ul>
</div>
<div class="grp">
<div class="grp-h">security</div>
<ul class="bul">
<li>a native anti-abuse engine in the core — connection/nick-flood and mass-join screening</li>
<li>behavioral and content heuristics with computed-pattern mining</li>
<li>a DEFCON state machine plus DNSBL / MX screening via a native async resolver</li>
<div class="doc-block">
<h2>Security</h2>
<ul>
<li>A native anti-abuse engine in the core — connection/nick-flood and mass-join screening.</li>
<li>Behavioral and content heuristics with computed-pattern mining.</li>
<li>A DEFCON state machine plus DNSBL / MX screening via a native async resolver.</li>
</ul>
</div>
</section>
<div class="ln"><span class="t">19:32</span><span class="g">*</span><span class="m i">echoircd nods toward <a href="/connect">#connect</a></span></div>
<section class="ctaband">
<div class="wrap ctaband-in">
<div><h2>Try it</h2><p class="mono-line">irc.echoircd.org · +6697 · TLS 1.3</p></div>
<a class="btn btn-primary" href="/connect">Connect &amp; register</a>
</div>
</section>
{% endblock %}

View file

@ -1,28 +1,68 @@
{% extends "base.html" %}
{% block title %}echoIRCd — a native-Rust IRC network{% endblock %}
{% block topic %}#home — echoIRCd, a native-Rust IRC daemon &amp; services · no fork · no C · no unsafe{% endblock %}
{% block chan %}#home{% endblock %}
{% block cmd %}/connect{% endblock %}
{% block buffer %}
<div class="banner">
<div class="wordmark">echo<span class="w-hi">IRCd</span><span class="cur cur-lg"></span></div>
<div class="w-sub">an IRC network written from scratch in Rust — built, not forked</div>
{% block title %}echoIRCd — a from-scratch IRC server in Rust{% endblock %}
{% block content %}
<section class="hero">
<div class="hero-in">
<div class="hero-main">
<p class="eyebrow">IRC daemon &amp; services · written in Rust</p>
<h1>The from-scratch<br>IRC server.</h1>
<p class="hero-sub">echoIRCd is a modern IRC daemon and a full services suite, built from the
ground up in Rust — full IRCv3, modern TLS, and SASL. No forks. No C. No <code>unsafe</code>.</p>
<div class="cta">
<a class="btn btn-primary" href="/connect">Connect to the network</a>
<a class="btn btn-ghost" href="/features">See the features</a>
</div>
</div>
<div class="ln"><span class="t">19:28</span><span class="g dim">-!-</span><span class="m dim">Now talking on <b>#home</b></span></div>
<div class="ln"><span class="t">19:28</span><span class="g dim">-!-</span><span class="m dim">echoIRCd/5.0.0 · irc.echoircd.org · network echoiRCd · <span class="ok">+6697 TLS1.3</span></span></div>
<div class="ln"><span class="t">19:28</span><span class="g n-green">--&gt;</span><span class="m dim">you (~guest@echoircd.org) has joined <b>#home</b></span></div>
<div class="ln"><span class="t">19:29</span><span class="g n-amber">&lt;echoircd&gt;</span><span class="m">hey — you're looking at an IRC network written from the ground up in Rust.</span></div>
<div class="ln"><span class="t">19:29</span><span class="g n-amber">&lt;echoircd&gt;</span><span class="m">original code: no InspIRCd fork, no C, and <span class="kw">#![forbid(unsafe_code)]</span> across the whole tree.</span></div>
<div class="ln"><span class="t">19:29</span><span class="g n-amber">&lt;echoircd&gt;</span><span class="m">the daemon speaks full IRCv3 and modern TLS; services handle accounts, channels and SASL.</span></div>
<div class="ln"><span class="t">19:30</span><span class="g">*</span><span class="m i">echoircd slides the tabs your way — <b>#features</b> for the tech, <b>#connect</b> to join</span></div>
<div class="ln"><span class="t">19:30</span><span class="g n-cyan">&lt;you&gt;</span><span class="m">nice. how do I get on?</span></div>
<div class="ln"><span class="t">19:30</span><span class="g n-amber">&lt;echoircd&gt;</span><span class="m"><a href="/connect">/connect</a> — or open <a href="https://orbit.devtronic.pro">the web client</a> right in your browser.</span></div>
<div class="quickfacts">
<a class="qf" href="/features"><span class="qf-k">tls</span> 1.3 · post-quantum</a>
<a class="qf" href="/features"><span class="qf-k">ircv3</span> full cap set</a>
<a class="qf" href="/features"><span class="qf-k">sasl</span> incl. ecdsa</a>
<a class="qf" href="/features"><span class="qf-k">unsafe</span> 0 lines</a>
<aside class="statuscard" aria-label="live network status">
<div class="sc-head"><span class="sc-title">network status</span>
{% if status.online %}<span class="dot on" title="online"></span>{% else %}<span class="dot off" title="offline"></span>{% endif %}
</div>
<dl class="sc-rows">
<div><dt>state</dt><dd>{% if status.online %}<b class="live">online</b>{% else %}<b class="down">offline</b>{% endif %}</dd></div>
<div><dt>users</dt><dd>{{ status.users }}</dd></div>
<div><dt>network</dt><dd>echoiRCd</dd></div>
<div><dt>version</dt><dd>echoIRCd {{ status.version }}</dd></div>
<div><dt>tls</dt><dd>1.3 · X25519MLKEM768</dd></div>
</dl>
<div class="sc-foot">live from <code>irc.echoircd.org</code></div>
</aside>
</div>
</section>
<section class="band">
<div class="wrap">
<h2 class="h-center">Why echoIRCd</h2>
<div class="cards">
<div class="fcard"><h3>Safe by construction</h3><p><code>#![forbid(unsafe_code)]</code> across the whole tree, enforced in CI. Original code — not a fork of anything.</p></div>
<div class="fcard"><h3>Full IRCv3</h3><p>server-time, message-tags, account-tag, batch, labeled-response, chathistory, multiline, standard-replies and more.</p></div>
<div class="fcard"><h3>Modern TLS</h3><p>Direct TLS and WebSocket, OpenSSL &amp; rustls backends, TLS&nbsp;1.3 with post-quantum X25519MLKEM768, per-host SNI.</p></div>
<div class="fcard"><h3>SASL, incl. ECDSA</h3><p>PLAIN, EXTERNAL, SCRAM-SHA-256, and ECDSA-NIST256P-CHALLENGE — sign a challenge, no password on the wire.</p></div>
<div class="fcard"><h3>Native services</h3><p>NickServ, ChanServ, OperServ, MemoServ and more over S2S — accounts, channels, vhosts, an event-sourced store.</p></div>
<div class="fcard"><h3>Anti-abuse built in</h3><p>A native security engine: connection &amp; nick-flood detection, behavioral/content heuristics, DEFCON, DNSBL/MX screening.</p></div>
</div>
<p class="h-center more"><a href="/features">Everything that's inside &#8594;</a></p>
</div>
</section>
<section class="wrap latest">
<h2>Latest</h2>
<ul class="news">
<li>
<span class="news-date">Aug 2026</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>
</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>
</ul>
</section>
<section class="ctaband">
<div class="wrap ctaband-in">
<div><h2>Ready to connect?</h2><p class="mono-line">irc.echoircd.org · +6697 · TLS 1.3</p></div>
<a class="btn btn-primary" href="/connect">Get connected</a>
</div>
</section>
{% endblock %}