use std::io::{self, BufRead, Write};
use std::time::Instant;
use anyhow::{Context, Result, bail};
use stackpatrol_core::event::Event;
use stackpatrol_core::token;
use sysinfo::{Disks, System};
use crate::buffer::{self, Buffer};
use crate::client::{build_client, events_url, send_event};
use crate::config::Config;
pub fn init() -> Result<()> {
println!("StackPatrol setup");
println!();
println!("1. Open @StackPatrolBot on Telegram and run /start");
println!("2. Tap \"Add server\" and give it a name");
println!("3. Paste the token it gives you below");
println!();
let server_name = prompt("Server name (e.g. web-01)")?;
if server_name.is_empty() {
bail!("server name cannot be empty");
}
let raw_token = prompt("Server token (sp_live_…)")?;
token::validate(&raw_token).context("invalid token format")?;
let api_endpoint = prompt_default(
"API endpoint",
"https://api.stackpatrol.dev",
)?;
let cfg = Config {
server_name,
token: raw_token,
api_endpoint,
daemon: Default::default(),
docker: Default::default(),
systemd: Default::default(),
resources: Default::default(),
ports: Default::default(),
};
let path = cfg.save()?;
println!();
println!("✔ wrote {}", path.display());
println!(" next: stackpatrol alert-test");
Ok(())
}
pub async fn status() -> Result<()> {
let cfg = Config::load().context("could not load config — run `stackpatrol init` first")?;
println!(
"StackPatrol — server: {}, endpoint: {}",
cfg.server_name, cfg.api_endpoint
);
println!();
let mut sys = System::new();
sys.refresh_memory();
let disks = Disks::new_with_refreshed_list();
let load = System::load_average();
println!("System");
println!(" uptime {}", format_duration(System::uptime()));
println!(
" load (1m/5m/15m) {:.2} / {:.2} / {:.2}",
load.one, load.five, load.fifteen
);
let mem_total = sys.total_memory();
let mem_used = sys.used_memory();
let mem_pct = if mem_total > 0 {
(mem_used as f64 / mem_total as f64 * 100.0).round() as u32
} else {
0
};
println!(
" memory {} / {} ({}%)",
format_bytes(mem_used),
format_bytes(mem_total),
mem_pct
);
println!(" disks");
for disk in &disks {
let total = disk.total_space();
if total == 0 {
continue;
}
let used = total.saturating_sub(disk.available_space());
let pct = (used as f64 / total as f64 * 100.0).round() as u32;
println!(
" {:<30} {:>3}% ({} / {})",
disk.mount_point().display(),
pct,
format_bytes(used),
format_bytes(total)
);
}
println!();
println!("Probes");
println!(
" docker {}",
if cfg.docker.projects.is_empty() {
"off".to_string()
} else {
format!("{} project(s)", cfg.docker.projects.len())
}
);
println!(
" systemd {}",
if cfg.systemd.units.is_empty() {
"off".to_string()
} else {
format!("{} unit(s)", cfg.systemd.units.len())
}
);
println!(
" ports {}",
if cfg.ports.tcp.is_empty() {
"off".to_string()
} else {
format!("{} TCP target(s)", cfg.ports.tcp.len())
}
);
let load_threshold = if cfg.resources.load_1m_high > 0.0 {
format!("{:.2}", cfg.resources.load_1m_high)
} else {
"auto".to_string()
};
println!(
" resources disk≥{}% mem≥{}% load1≥{}",
cfg.resources.disk_high_percent, cfg.resources.memory_high_percent, load_threshold
);
println!();
println!("Buffer");
let buffer = Buffer::new(buffer::default_path()?);
let queued = buffer.read_all().await.unwrap_or_default();
println!(
" queue {} event(s) at {}",
queued.len(),
buffer.path().display()
);
println!();
println!("API");
let url = events_url(&cfg);
let client = build_client()?;
let started = Instant::now();
match client.get(&cfg.api_endpoint).send().await {
Ok(resp) => println!(
" reachable ✓ HTTP {} ({}ms)",
resp.status(),
started.elapsed().as_millis()
),
Err(e) => println!(" reachable ✗ {e}"),
}
println!(" endpoint {url}");
Ok(())
}
pub async fn alert_test() -> Result<()> {
let cfg = Config::load().context("could not load config — run `stackpatrol init` first")?;
let url = events_url(&cfg);
let client = build_client()?;
println!("→ sending service_down(\"alert-test\") to {url}");
send_event(&client, &url, &cfg, Event::ServiceDown { name: "alert-test".into() }).await?;
println!(" ✔ delivered. Check Telegram — you should see a 🔴 alert in a few seconds.");
tokio::time::sleep(std::time::Duration::from_secs(3)).await;
println!("→ sending service_up(\"alert-test\") to clear the demo alert");
send_event(&client, &url, &cfg, Event::ServiceUp { name: "alert-test".into() }).await?;
println!(" ✔ delivered.");
Ok(())
}
fn prompt(label: &str) -> Result<String> {
print!("{label}: ");
io::stdout().flush().ok();
let mut line = String::new();
io::stdin().lock().read_line(&mut line)?;
Ok(line.trim().to_string())
}
fn prompt_default(label: &str, default: &str) -> Result<String> {
print!("{label} [{default}]: ");
io::stdout().flush().ok();
let mut line = String::new();
io::stdin().lock().read_line(&mut line)?;
let trimmed = line.trim();
if trimmed.is_empty() {
Ok(default.to_string())
} else {
Ok(trimmed.to_string())
}
}
fn format_bytes(n: u64) -> String {
const KIB: u64 = 1024;
const MIB: u64 = KIB * 1024;
const GIB: u64 = MIB * 1024;
const TIB: u64 = GIB * 1024;
if n >= TIB {
format!("{:.1} TiB", n as f64 / TIB as f64)
} else if n >= GIB {
format!("{:.1} GiB", n as f64 / GIB as f64)
} else if n >= MIB {
format!("{:.1} MiB", n as f64 / MIB as f64)
} else if n >= KIB {
format!("{:.1} KiB", n as f64 / KIB as f64)
} else {
format!("{n} B")
}
}
fn format_duration(secs: u64) -> String {
let days = secs / 86_400;
let hours = (secs % 86_400) / 3_600;
let minutes = (secs % 3_600) / 60;
let seconds = secs % 60;
if days > 0 {
format!("{days}d {hours}h {minutes}m")
} else if hours > 0 {
format!("{hours}h {minutes}m")
} else if minutes > 0 {
format!("{minutes}m {seconds}s")
} else {
format!("{seconds}s")
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn format_bytes_picks_unit() {
assert_eq!(format_bytes(0), "0 B");
assert_eq!(format_bytes(512), "512 B");
assert_eq!(format_bytes(2048), "2.0 KiB");
assert_eq!(format_bytes(2 * 1024 * 1024), "2.0 MiB");
assert_eq!(format_bytes(3 * 1024 * 1024 * 1024), "3.0 GiB");
}
#[test]
fn format_duration_compacts() {
assert_eq!(format_duration(0), "0s");
assert_eq!(format_duration(45), "45s");
assert_eq!(format_duration(125), "2m 5s");
assert_eq!(format_duration(3661), "1h 1m");
assert_eq!(format_duration(90061), "1d 1h 1m");
}
}