feat: ops & UX batch — jobs survive restarts, logs/system in the console, media queue, gallery, chat, exact stream tokens
Fifteen changes aimed at the first day of real use: - Import and media jobs persist to <data>/jobs-*.json and reload on start (running ones settle as errors); the Register button no longer vanishes because the gateway restarted. - Docker ships the cortiq binary (built in the builder stage) — local .cmf chat models and image/video generation now work in containers, where auto-install never could (no cargo in the runtime image). libvulkan1 added for optional GPU offload. - Open admin console on 0.0.0.0 without a token now warns in the log and shows a dismissible banner — the VPS footgun is at least labeled. - "Restart gateway" button in Settings: stops managed model servers, spawns a fresh copy (bind retried while the port frees), Docker relies on the container restart policy instead. - Dashboard "System" panel: CPU sparkline + load, RAM (with the gateway's own RSS), free disk at the models dir — sampled by sysinfo every 2 s. - Featured model cards warn "not enough disk" / "may not fit in RAM" before the download, from the same live snapshot. - Log viewer in the sidebar: a 500-line in-memory ring fed by a second ANSI-free tracing layer; filter, level, pause, copy. - Update check: crates.io polled twice a day, the console shows an "update available" notice. - Media generations queue up (with position shown) instead of being rejected; progress gained an honest ETA from measured throughput. - Generation gallery under the image/video tabs: lazy PPM thumbnails, click to reopen with seed/steps, delete. - Playground chat is a real dialogue now: history is resent, bubbles, Clear, Cmd/Ctrl+Enter. - "Connect your app" snippets (curl / Python / JS) on the keys page, with the real key inlined right after creation. - One-click benchmark on the Models page via `cortiq bench --json`. - Config export now downloads the actual gateway.toml (comments intact); import accepts TOML (validated, hot-reloaded) and the legacy JSON. - Streaming token counts are exact when the model server provides usage (cortiq-server patch, shipped separately); the gateway fallback now counts content chunks (1/token on cortiq) instead of chars/4. Wizard's Finish lands on the Playground. 37 new strings in all 7 languages.
This commit is contained in:
@@ -0,0 +1,86 @@
|
||||
//! Host metrics sampler for the admin dashboard: CPU load, RAM, free disk at
|
||||
//! the models dir, and the gateway's own RSS. One background task refreshes a
|
||||
//! snapshot every 2 s; the API endpoint only reads it.
|
||||
|
||||
use serde::Serialize;
|
||||
use std::sync::{Arc, Mutex};
|
||||
|
||||
#[derive(Clone, Default, Serialize)]
|
||||
pub struct Snapshot {
|
||||
pub ts: u64,
|
||||
/// Whole-machine CPU usage 0..100 (average over all cores).
|
||||
pub cpu_percent: f32,
|
||||
pub cores: usize,
|
||||
pub load1: f64,
|
||||
pub ram_total: u64,
|
||||
pub ram_used: u64,
|
||||
/// Disk that holds models_dir.
|
||||
pub disk_total: u64,
|
||||
pub disk_free: u64,
|
||||
/// Resident memory of the gateway process itself.
|
||||
pub process_rss: u64,
|
||||
}
|
||||
|
||||
pub struct SysMon {
|
||||
snap: Mutex<Snapshot>,
|
||||
}
|
||||
|
||||
impl SysMon {
|
||||
pub fn snapshot(&self) -> Snapshot {
|
||||
self.snap.lock().unwrap().clone()
|
||||
}
|
||||
}
|
||||
|
||||
fn now() -> u64 {
|
||||
std::time::SystemTime::now()
|
||||
.duration_since(std::time::UNIX_EPOCH)
|
||||
.unwrap_or_default()
|
||||
.as_secs()
|
||||
}
|
||||
|
||||
/// Start the sampler. `models_dir` picks which disk's free space is reported.
|
||||
pub fn spawn(models_dir: String) -> Arc<SysMon> {
|
||||
let mon = Arc::new(SysMon {
|
||||
snap: Mutex::new(Snapshot::default()),
|
||||
});
|
||||
let ret = mon.clone();
|
||||
tokio::task::spawn_blocking(move || {
|
||||
use sysinfo::{Disks, ProcessesToUpdate, System};
|
||||
let mut sys = System::new();
|
||||
let pid = sysinfo::Pid::from_u32(std::process::id());
|
||||
let models_abs = std::fs::canonicalize(&models_dir)
|
||||
.unwrap_or_else(|_| std::path::PathBuf::from(&models_dir));
|
||||
loop {
|
||||
sys.refresh_cpu_usage();
|
||||
sys.refresh_memory();
|
||||
let _ = sys.refresh_processes(ProcessesToUpdate::Some(&[pid]), true);
|
||||
let disks = Disks::new_with_refreshed_list();
|
||||
// the disk whose mount point is the longest prefix of models_dir
|
||||
let mut best: Option<(usize, u64, u64)> = None;
|
||||
for d in disks.list() {
|
||||
let mp = d.mount_point();
|
||||
if models_abs.starts_with(mp) {
|
||||
let len = mp.as_os_str().len();
|
||||
if best.map(|(l, _, _)| len > l).unwrap_or(true) {
|
||||
best = Some((len, d.total_space(), d.available_space()));
|
||||
}
|
||||
}
|
||||
}
|
||||
let (disk_total, disk_free) = best.map(|(_, t, f)| (t, f)).unwrap_or((0, 0));
|
||||
let snap = Snapshot {
|
||||
ts: now(),
|
||||
cpu_percent: sys.global_cpu_usage(),
|
||||
cores: sys.cpus().len(),
|
||||
load1: System::load_average().one,
|
||||
ram_total: sys.total_memory(),
|
||||
ram_used: sys.used_memory(),
|
||||
disk_total,
|
||||
disk_free,
|
||||
process_rss: sys.process(pid).map(|p| p.memory()).unwrap_or(0),
|
||||
};
|
||||
*mon.snap.lock().unwrap() = snap;
|
||||
std::thread::sleep(std::time::Duration::from_secs(2));
|
||||
}
|
||||
});
|
||||
ret
|
||||
}
|
||||
Reference in New Issue
Block a user