hierottu GPU-tukea
This commit is contained in:
272
network-poc/hub/src/db.rs
Normal file
272
network-poc/hub/src/db.rs
Normal file
@@ -0,0 +1,272 @@
|
||||
use rusqlite::{Connection, params};
|
||||
use std::sync::Mutex;
|
||||
|
||||
pub struct NodeDb {
|
||||
conn: Mutex<Connection>,
|
||||
}
|
||||
|
||||
impl NodeDb {
|
||||
pub fn new(path: &str) -> Self {
|
||||
let conn = Connection::open(path).expect("SQLite-tietokantaa ei voitu avata");
|
||||
|
||||
conn.execute_batch("
|
||||
CREATE TABLE IF NOT EXISTS node_sessions (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
node_id INTEGER NOT NULL,
|
||||
ip TEXT NOT NULL,
|
||||
node_type TEXT NOT NULL DEFAULT 'browser',
|
||||
connected_at TEXT NOT NULL,
|
||||
disconnected_at TEXT,
|
||||
|
||||
-- Järjestelmätiedot
|
||||
platform TEXT,
|
||||
hostname TEXT,
|
||||
os TEXT,
|
||||
cpu_cores INTEGER,
|
||||
cpu_model TEXT,
|
||||
ram_mb INTEGER,
|
||||
|
||||
-- GPU
|
||||
gpu_name TEXT,
|
||||
gpu_vendor TEXT,
|
||||
gpu_backend TEXT,
|
||||
vram_total_mb INTEGER,
|
||||
vram_used_mb INTEGER,
|
||||
gpu_temp_c INTEGER,
|
||||
gpu_util_pct INTEGER,
|
||||
|
||||
-- Varaus
|
||||
allocated_gb INTEGER,
|
||||
|
||||
-- WebGPU-tuki
|
||||
has_webgpu BOOLEAN,
|
||||
|
||||
-- Tehtävätilastot
|
||||
tasks_completed INTEGER DEFAULT 0
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS pair_results (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
node_id INTEGER NOT NULL,
|
||||
created_at TEXT NOT NULL,
|
||||
en_text TEXT,
|
||||
fi_text TEXT,
|
||||
en_tokens INTEGER,
|
||||
fi_tokens INTEGER,
|
||||
en_chars_per_token REAL,
|
||||
fi_chars_per_token REAL,
|
||||
overhead_pct REAL,
|
||||
duration_ms INTEGER
|
||||
);
|
||||
").expect("Tietokantataulujen luonti epäonnistui");
|
||||
|
||||
NodeDb { conn: Mutex::new(conn) }
|
||||
}
|
||||
|
||||
pub fn insert_session(
|
||||
&self,
|
||||
node_id: u64,
|
||||
ip: &str,
|
||||
node_type: &str,
|
||||
auth_data: &serde_json::Value,
|
||||
) -> i64 {
|
||||
let conn = self.conn.lock().unwrap();
|
||||
let now = chrono::Utc::now().to_rfc3339();
|
||||
|
||||
// Selainsolmun tiedot
|
||||
let platform = auth_data.get("platform").and_then(|v| v.as_str());
|
||||
let cpu_cores = auth_data.get("cpu_cores").and_then(|v| v.as_u64());
|
||||
let ram = auth_data.get("device_memory_gb").and_then(|v| v.as_f64()).map(|v| (v * 1024.0) as i64);
|
||||
let allocated = auth_data.get("allocated_gb").and_then(|v| v.as_u64());
|
||||
|
||||
// GPU (selain)
|
||||
let gpu_vendor = auth_data.get("gpu").and_then(|g| g.get("vendor")).and_then(|v| v.as_str());
|
||||
let gpu_desc = auth_data.get("gpu").and_then(|g| g.get("description")).and_then(|v| v.as_str());
|
||||
let gpu_backend = if gpu_vendor.is_some() { Some("WebGPU") } else { None };
|
||||
let has_webgpu = gpu_vendor.is_some();
|
||||
|
||||
// Natiivi-solmun tiedot
|
||||
let sys = auth_data.get("system");
|
||||
let hostname = sys.and_then(|s| s.get("hostname")).and_then(|v| v.as_str());
|
||||
let os = sys.and_then(|s| s.get("os")).and_then(|v| v.as_str());
|
||||
let native_cores = sys.and_then(|s| s.get("cpu_cores")).and_then(|v| v.as_u64());
|
||||
let cpu_model = sys.and_then(|s| s.get("cpu_model")).and_then(|v| v.as_str());
|
||||
let native_ram = sys.and_then(|s| s.get("ram_total_mb")).and_then(|v| v.as_u64());
|
||||
|
||||
// GPU (natiivi — ensimmäinen GPU)
|
||||
let gpu = auth_data.get("gpus").and_then(|v| v.as_array()).and_then(|a| a.first());
|
||||
let native_gpu_name = gpu.and_then(|g| g.get("name")).and_then(|v| v.as_str());
|
||||
let native_gpu_vendor = gpu.and_then(|g| g.get("vendor")).and_then(|v| v.as_str());
|
||||
let native_gpu_backend = gpu.and_then(|g| g.get("backend")).and_then(|v| v.as_str());
|
||||
let vram_total = gpu.and_then(|g| g.get("vram_total_mb")).and_then(|v| v.as_u64());
|
||||
let vram_used = gpu.and_then(|g| g.get("vram_used_mb")).and_then(|v| v.as_u64());
|
||||
let gpu_temp = gpu.and_then(|g| g.get("temperature_c")).and_then(|v| v.as_u64());
|
||||
let gpu_util = gpu.and_then(|g| g.get("gpu_util_pct")).and_then(|v| v.as_u64());
|
||||
|
||||
conn.execute(
|
||||
"INSERT INTO node_sessions (
|
||||
node_id, ip, node_type, connected_at,
|
||||
platform, hostname, os, cpu_cores, cpu_model, ram_mb,
|
||||
gpu_name, gpu_vendor, gpu_backend, vram_total_mb, vram_used_mb, gpu_temp_c, gpu_util_pct,
|
||||
allocated_gb, has_webgpu
|
||||
) VALUES (?1,?2,?3,?4,?5,?6,?7,?8,?9,?10,?11,?12,?13,?14,?15,?16,?17,?18,?19)",
|
||||
params![
|
||||
node_id as i64, ip, node_type, now,
|
||||
platform, hostname, os,
|
||||
native_cores.or(cpu_cores).map(|v| v as i64),
|
||||
cpu_model,
|
||||
native_ram.map(|v| v as i64).or(ram),
|
||||
native_gpu_name.or(gpu_desc),
|
||||
native_gpu_vendor.or(gpu_vendor),
|
||||
native_gpu_backend.or(gpu_backend),
|
||||
vram_total.map(|v| v as i64),
|
||||
vram_used.map(|v| v as i64),
|
||||
gpu_temp.map(|v| v as i64),
|
||||
gpu_util.map(|v| v as i64),
|
||||
allocated.map(|v| v as i64),
|
||||
has_webgpu,
|
||||
],
|
||||
).expect("Session insert epäonnistui");
|
||||
|
||||
conn.last_insert_rowid()
|
||||
}
|
||||
|
||||
pub fn close_session(&self, node_id: u64) {
|
||||
let conn = self.conn.lock().unwrap();
|
||||
let now = chrono::Utc::now().to_rfc3339();
|
||||
let _ = conn.execute(
|
||||
"UPDATE node_sessions SET disconnected_at = ?1 WHERE node_id = ?2 AND disconnected_at IS NULL",
|
||||
params![now, node_id as i64],
|
||||
);
|
||||
}
|
||||
|
||||
pub fn increment_tasks(&self, node_id: u64) {
|
||||
let conn = self.conn.lock().unwrap();
|
||||
let _ = conn.execute(
|
||||
"UPDATE node_sessions SET tasks_completed = tasks_completed + 1 WHERE node_id = ?1 AND disconnected_at IS NULL",
|
||||
params![node_id as i64],
|
||||
);
|
||||
}
|
||||
|
||||
pub fn get_sessions(&self, limit: u32) -> Vec<serde_json::Value> {
|
||||
let conn = self.conn.lock().unwrap();
|
||||
let mut stmt = conn.prepare(
|
||||
"SELECT id, node_id, ip, node_type, connected_at, disconnected_at,
|
||||
platform, hostname, os, cpu_cores, cpu_model, ram_mb,
|
||||
gpu_name, gpu_vendor, gpu_backend, vram_total_mb, gpu_temp_c, gpu_util_pct,
|
||||
allocated_gb, has_webgpu, tasks_completed
|
||||
FROM node_sessions ORDER BY id DESC LIMIT ?1"
|
||||
).unwrap();
|
||||
|
||||
stmt.query_map(params![limit], |row| {
|
||||
Ok(serde_json::json!({
|
||||
"id": row.get::<_, i64>(0)?,
|
||||
"node_id": row.get::<_, i64>(1)?,
|
||||
"ip": row.get::<_, String>(2)?,
|
||||
"node_type": row.get::<_, String>(3)?,
|
||||
"connected_at": row.get::<_, String>(4)?,
|
||||
"disconnected_at": row.get::<_, Option<String>>(5)?,
|
||||
"platform": row.get::<_, Option<String>>(6)?,
|
||||
"hostname": row.get::<_, Option<String>>(7)?,
|
||||
"os": row.get::<_, Option<String>>(8)?,
|
||||
"cpu_cores": row.get::<_, Option<i64>>(9)?,
|
||||
"cpu_model": row.get::<_, Option<String>>(10)?,
|
||||
"ram_mb": row.get::<_, Option<i64>>(11)?,
|
||||
"gpu_name": row.get::<_, Option<String>>(12)?,
|
||||
"gpu_vendor": row.get::<_, Option<String>>(13)?,
|
||||
"gpu_backend": row.get::<_, Option<String>>(14)?,
|
||||
"vram_total_mb": row.get::<_, Option<i64>>(15)?,
|
||||
"gpu_temp_c": row.get::<_, Option<i64>>(16)?,
|
||||
"gpu_util_pct": row.get::<_, Option<i64>>(17)?,
|
||||
"allocated_gb": row.get::<_, Option<i64>>(18)?,
|
||||
"has_webgpu": row.get::<_, Option<bool>>(19)?,
|
||||
"tasks_completed": row.get::<_, i64>(20)?,
|
||||
}))
|
||||
}).unwrap().filter_map(|r| r.ok()).collect()
|
||||
}
|
||||
|
||||
pub fn get_pair_results(&self, limit: u32) -> Vec<serde_json::Value> {
|
||||
let conn = self.conn.lock().unwrap();
|
||||
let mut stmt = conn.prepare(
|
||||
"SELECT id, node_id, created_at, en_text, fi_text,
|
||||
en_tokens, fi_tokens, en_chars_per_token, fi_chars_per_token,
|
||||
overhead_pct, duration_ms
|
||||
FROM pair_results ORDER BY id DESC LIMIT ?1"
|
||||
).unwrap();
|
||||
|
||||
stmt.query_map(params![limit], |row| {
|
||||
Ok(serde_json::json!({
|
||||
"id": row.get::<_, i64>(0)?,
|
||||
"node_id": row.get::<_, i64>(1)?,
|
||||
"created_at": row.get::<_, String>(2)?,
|
||||
"en_text": row.get::<_, Option<String>>(3)?,
|
||||
"fi_text": row.get::<_, Option<String>>(4)?,
|
||||
"en_tokens": row.get::<_, Option<i64>>(5)?,
|
||||
"fi_tokens": row.get::<_, Option<i64>>(6)?,
|
||||
"en_chars_per_token": row.get::<_, Option<f64>>(7)?,
|
||||
"fi_chars_per_token": row.get::<_, Option<f64>>(8)?,
|
||||
"overhead_pct": row.get::<_, Option<f64>>(9)?,
|
||||
"duration_ms": row.get::<_, Option<i64>>(10)?,
|
||||
}))
|
||||
}).unwrap().filter_map(|r| r.ok()).collect()
|
||||
}
|
||||
|
||||
pub fn get_stats(&self) -> serde_json::Value {
|
||||
let conn = self.conn.lock().unwrap();
|
||||
|
||||
let total_sessions: i64 = conn.query_row("SELECT COUNT(*) FROM node_sessions", [], |r| r.get(0)).unwrap_or(0);
|
||||
let active_sessions: i64 = conn.query_row("SELECT COUNT(*) FROM node_sessions WHERE disconnected_at IS NULL", [], |r| r.get(0)).unwrap_or(0);
|
||||
let total_pairs: i64 = conn.query_row("SELECT COUNT(*) FROM pair_results", [], |r| r.get(0)).unwrap_or(0);
|
||||
let avg_overhead: f64 = conn.query_row("SELECT COALESCE(AVG(overhead_pct), 0) FROM pair_results", [], |r| r.get(0)).unwrap_or(0.0);
|
||||
let avg_en_cpt: f64 = conn.query_row("SELECT COALESCE(AVG(en_chars_per_token), 0) FROM pair_results", [], |r| r.get(0)).unwrap_or(0.0);
|
||||
let avg_fi_cpt: f64 = conn.query_row("SELECT COALESCE(AVG(fi_chars_per_token), 0) FROM pair_results", [], |r| r.get(0)).unwrap_or(0.0);
|
||||
|
||||
let webgpu_count: i64 = conn.query_row("SELECT COUNT(*) FROM node_sessions WHERE has_webgpu = 1", [], |r| r.get(0)).unwrap_or(0);
|
||||
let cpu_fallback_count: i64 = conn.query_row("SELECT COUNT(*) FROM node_sessions WHERE has_webgpu = 0 OR has_webgpu IS NULL", [], |r| r.get(0)).unwrap_or(0);
|
||||
|
||||
let unique_ips: i64 = conn.query_row("SELECT COUNT(DISTINCT ip) FROM node_sessions", [], |r| r.get(0)).unwrap_or(0);
|
||||
|
||||
serde_json::json!({
|
||||
"total_sessions": total_sessions,
|
||||
"active_sessions": active_sessions,
|
||||
"unique_ips": unique_ips,
|
||||
"total_pairs": total_pairs,
|
||||
"avg_overhead_pct": (avg_overhead * 10.0).round() / 10.0,
|
||||
"avg_en_chars_per_token": (avg_en_cpt * 100.0).round() / 100.0,
|
||||
"avg_fi_chars_per_token": (avg_fi_cpt * 100.0).round() / 100.0,
|
||||
"webgpu_sessions": webgpu_count,
|
||||
"cpu_fallback_sessions": cpu_fallback_count,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn insert_pair_result(
|
||||
&self,
|
||||
node_id: u64,
|
||||
en: &serde_json::Value,
|
||||
fi: &serde_json::Value,
|
||||
overhead: f64,
|
||||
duration_ms: u64,
|
||||
) {
|
||||
let conn = self.conn.lock().unwrap();
|
||||
let now = chrono::Utc::now().to_rfc3339();
|
||||
let _ = conn.execute(
|
||||
"INSERT INTO pair_results (
|
||||
node_id, created_at, en_text, fi_text,
|
||||
en_tokens, fi_tokens, en_chars_per_token, fi_chars_per_token,
|
||||
overhead_pct, duration_ms
|
||||
) VALUES (?1,?2,?3,?4,?5,?6,?7,?8,?9,?10)",
|
||||
params![
|
||||
node_id as i64,
|
||||
now,
|
||||
en.get("text").and_then(|v| v.as_str()),
|
||||
fi.get("text").and_then(|v| v.as_str()),
|
||||
en.get("token_count").and_then(|v| v.as_u64()).map(|v| v as i64),
|
||||
fi.get("token_count").and_then(|v| v.as_u64()).map(|v| v as i64),
|
||||
en.get("chars_per_token").and_then(|v| v.as_f64()),
|
||||
fi.get("chars_per_token").and_then(|v| v.as_f64()),
|
||||
overhead,
|
||||
duration_ms as i64,
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -13,6 +13,8 @@ use tokio::sync::broadcast;
|
||||
use tower_http::services::ServeDir;
|
||||
use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt};
|
||||
|
||||
mod db;
|
||||
|
||||
const MAX_MESSAGE_SIZE: usize = 16 * 1024;
|
||||
|
||||
// Sallitut originit — estää cross-site WebSocket hijackingin
|
||||
@@ -30,12 +32,173 @@ struct AppState {
|
||||
nodes_vram: Mutex<HashMap<u64, u32>>,
|
||||
total_tasks: Mutex<u64>,
|
||||
stats_tx: broadcast::Sender<String>,
|
||||
// IP-rajoitus: max 2 yhteyttä per IP (dashboard-UI + selainsolmu)
|
||||
ip_connections: Mutex<HashMap<IpAddr, u32>>,
|
||||
// Node ID → IP -mappaus (siivousta varten)
|
||||
node_ips: Mutex<HashMap<u64, IpAddr>>,
|
||||
db: db::NodeDb,
|
||||
}
|
||||
|
||||
const ADMIN_HTML: &str = r##"<!DOCTYPE html>
|
||||
<html lang="fi">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Kipina Admin</title>
|
||||
<style>
|
||||
:root { --bg:#0d1117; --panel:#161b22; --text:#c9d1d9; --accent:#58a6ff; --green:#3fb950; --yellow:#d29922; --red:#f85149; --border:#30363d; }
|
||||
* { box-sizing:border-box; margin:0; padding:0; }
|
||||
body { font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,sans-serif; background:var(--bg); color:var(--text); padding:20px; }
|
||||
h1 { color:var(--accent); margin-bottom:5px; }
|
||||
.sub { color:#8b949e; margin-bottom:20px; }
|
||||
.stats-grid { display:grid; grid-template-columns:repeat(auto-fit,minmax(150px,1fr)); gap:12px; margin-bottom:24px; }
|
||||
.stat-card { background:var(--panel); border:1px solid var(--border); border-radius:8px; padding:16px; text-align:center; }
|
||||
.stat-card .val { font-size:28px; font-weight:700; color:var(--accent); }
|
||||
.stat-card .label { font-size:12px; color:#8b949e; margin-top:4px; }
|
||||
table { width:100%; border-collapse:collapse; margin-bottom:24px; font-size:13px; }
|
||||
th { background:var(--panel); color:var(--accent); text-align:left; padding:10px 8px; border-bottom:2px solid var(--border); position:sticky; top:0; }
|
||||
td { padding:8px; border-bottom:1px solid var(--border); }
|
||||
tr:hover td { background:#1c2333; }
|
||||
.badge { display:inline-block; padding:2px 8px; border-radius:10px; font-size:11px; font-weight:600; }
|
||||
.badge-green { background:#23392050; color:var(--green); border:1px solid #23392080; }
|
||||
.badge-yellow { background:#d2992220; color:var(--yellow); border:1px solid #d2992240; }
|
||||
.badge-red { background:#f8514920; color:var(--red); border:1px solid #f8514940; }
|
||||
.badge-blue { background:#58a6ff20; color:var(--accent); border:1px solid #58a6ff40; }
|
||||
.tabs { display:flex; gap:8px; margin-bottom:16px; }
|
||||
.tab { padding:8px 16px; border-radius:6px; border:1px solid var(--border); background:var(--panel); color:var(--text); cursor:pointer; font-size:14px; }
|
||||
.tab.active { background:var(--accent); color:#0d1117; border-color:var(--accent); }
|
||||
.panel { display:none; }
|
||||
.panel.active { display:block; }
|
||||
.table-wrap { overflow-x:auto; max-height:70vh; overflow-y:auto; }
|
||||
.online { color:var(--green); }
|
||||
.offline { color:#8b949e; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1>Kipina Admin</h1>
|
||||
<p class="sub">Node-sessiot ja tokenisointivertailut</p>
|
||||
|
||||
<div id="stats" class="stats-grid"></div>
|
||||
|
||||
<div class="tabs">
|
||||
<div class="tab active" onclick="showTab('sessions')">Sessiot</div>
|
||||
<div class="tab" onclick="showTab('pairs')">Tokenisointiparit</div>
|
||||
</div>
|
||||
|
||||
<div id="sessions" class="panel active">
|
||||
<div class="table-wrap">
|
||||
<table><thead><tr>
|
||||
<th>ID</th><th>Tila</th><th>Tyyppi</th><th>IP</th><th>Alusta</th>
|
||||
<th>OS</th><th>CPU</th><th>RAM</th><th>GPU</th><th>VRAM</th>
|
||||
<th>WebGPU</th><th>Teht.</th><th>Yhdistetty</th><th>Kesto</th>
|
||||
</tr></thead><tbody id="sessions-body"></tbody></table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="pairs" class="panel">
|
||||
<div class="table-wrap">
|
||||
<table><thead><tr>
|
||||
<th>Solmu</th><th>EN</th><th>EN tok</th><th>EN m/t</th>
|
||||
<th>FI</th><th>FI tok</th><th>FI m/t</th><th>Ylikust.</th><th>Aika</th>
|
||||
</tr></thead><tbody id="pairs-body"></tbody></table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
function showTab(name) {
|
||||
document.querySelectorAll('.panel').forEach(p => p.classList.remove('active'));
|
||||
document.querySelectorAll('.tab').forEach(t => t.classList.remove('active'));
|
||||
document.getElementById(name).classList.add('active');
|
||||
event.target.classList.add('active');
|
||||
}
|
||||
|
||||
function badge(text, cls) { return `<span class="badge badge-${cls}">${text}</span>`; }
|
||||
|
||||
function timeSince(iso) {
|
||||
if (!iso) return '';
|
||||
const d = new Date(iso);
|
||||
const now = new Date();
|
||||
const s = Math.floor((now - d) / 1000);
|
||||
if (s < 60) return s + 's';
|
||||
if (s < 3600) return Math.floor(s/60) + 'min';
|
||||
if (s < 86400) return Math.floor(s/3600) + 'h';
|
||||
return Math.floor(s/86400) + 'pv';
|
||||
}
|
||||
|
||||
function duration(start, end) {
|
||||
if (!start) return '';
|
||||
const s = end ? new Date(end) : new Date();
|
||||
const d = Math.floor((s - new Date(start)) / 1000);
|
||||
if (d < 60) return d + 's';
|
||||
if (d < 3600) return Math.floor(d/60) + 'min';
|
||||
return Math.floor(d/3600) + 'h ' + (Math.floor(d/60)%60) + 'min';
|
||||
}
|
||||
|
||||
async function load() {
|
||||
const [statsRes, sessionsRes, pairsRes] = await Promise.all([
|
||||
fetch('/api/stats'), fetch('/api/sessions'), fetch('/api/pairs')
|
||||
]);
|
||||
const stats = await statsRes.json();
|
||||
const sessions = await sessionsRes.json();
|
||||
const pairs = await pairsRes.json();
|
||||
|
||||
// Stats
|
||||
document.getElementById('stats').innerHTML = [
|
||||
{v: stats.total_sessions, l: 'Sessioita'},
|
||||
{v: stats.active_sessions, l: 'Aktiivisia'},
|
||||
{v: stats.unique_ips, l: 'Uniikkeja IP'},
|
||||
{v: stats.webgpu_sessions, l: 'WebGPU'},
|
||||
{v: stats.cpu_fallback_sessions, l: 'CPU fallback'},
|
||||
{v: stats.total_pairs, l: 'Pareja'},
|
||||
{v: stats.avg_en_chars_per_token, l: 'EN m/t (ka.)'},
|
||||
{v: stats.avg_fi_chars_per_token, l: 'FI m/t (ka.)'},
|
||||
{v: stats.avg_overhead_pct + '%', l: 'FI ylikust. (ka.)'},
|
||||
].map(s => `<div class="stat-card"><div class="val">${s.v}</div><div class="label">${s.l}</div></div>`).join('');
|
||||
|
||||
// Sessions
|
||||
document.getElementById('sessions-body').innerHTML = sessions.map(s => {
|
||||
const online = !s.disconnected_at;
|
||||
const status = online ? '<span class="online">ONLINE</span>' : '<span class="offline">offline</span>';
|
||||
const typeBadge = s.node_type === 'native' ? badge('native','blue') : badge('browser','yellow');
|
||||
const gpuBadge = s.has_webgpu ? badge('WebGPU','green') : badge('CPU','red');
|
||||
const gpu = s.gpu_name ? `${s.gpu_name}` : '-';
|
||||
const vram = s.vram_total_mb ? `${s.vram_total_mb} MB` : '-';
|
||||
const ram = s.ram_mb ? `${s.ram_mb} MB` : '-';
|
||||
const cores = s.cpu_cores || '-';
|
||||
const plat = s.platform || s.hostname || '-';
|
||||
const os = s.os || '-';
|
||||
const time = s.connected_at ? new Date(s.connected_at).toLocaleString('fi-FI') : '';
|
||||
const dur = duration(s.connected_at, s.disconnected_at);
|
||||
return `<tr>
|
||||
<td>${s.node_id}</td><td>${status}</td><td>${typeBadge}</td><td>${s.ip}</td>
|
||||
<td>${plat}</td><td>${os}</td><td>${cores}</td><td>${ram}</td>
|
||||
<td>${gpu}</td><td>${vram}</td><td>${gpuBadge}</td>
|
||||
<td>${s.tasks_completed}</td><td>${time}</td><td>${dur}</td>
|
||||
</tr>`;
|
||||
}).join('');
|
||||
|
||||
// Pairs
|
||||
document.getElementById('pairs-body').innerHTML = pairs.map(p => {
|
||||
const ovColor = p.overhead_pct > 50 ? 'red' : p.overhead_pct > 20 ? 'yellow' : 'green';
|
||||
const enCpt = p.en_chars_per_token?.toFixed(2) || '-';
|
||||
const fiCpt = p.fi_chars_per_token?.toFixed(2) || '-';
|
||||
const ov = p.overhead_pct?.toFixed(1) || '-';
|
||||
return `<tr>
|
||||
<td>#${p.node_id}</td>
|
||||
<td style="max-width:250px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap">${p.en_text||''}</td>
|
||||
<td>${p.en_tokens||'-'}</td><td>${enCpt}</td>
|
||||
<td style="max-width:250px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap">${p.fi_text||''}</td>
|
||||
<td>${p.fi_tokens||'-'}</td><td>${fiCpt}</td>
|
||||
<td>${badge(ov+'%', ovColor)}</td>
|
||||
<td>${p.duration_ms||0}ms</td>
|
||||
</tr>`;
|
||||
}).join('');
|
||||
}
|
||||
|
||||
load();
|
||||
setInterval(load, 10000);
|
||||
</script>
|
||||
</body>
|
||||
</html>"##;
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() {
|
||||
tracing_subscriber::registry()
|
||||
@@ -55,8 +218,11 @@ async fn main() {
|
||||
stats_tx: stats_tx.clone(),
|
||||
ip_connections: Mutex::new(HashMap::new()),
|
||||
node_ips: Mutex::new(HashMap::new()),
|
||||
db: db::NodeDb::new(&std::env::var("DATABASE_PATH").unwrap_or_else(|_| "nodes.db".to_string())),
|
||||
});
|
||||
|
||||
tracing::info!("Tietokanta alustettu");
|
||||
|
||||
let state_for_task = state.clone();
|
||||
|
||||
// Ajastin, joka jakaa satunnaisia tekoälytehtäviä eri pituuksilla
|
||||
@@ -100,18 +266,41 @@ async fn main() {
|
||||
});
|
||||
|
||||
let app = Router::new()
|
||||
.nest_service("/", ServeDir::new(std::env::var("STATIC_DIR").unwrap_or_else(|_| "../static".to_string())))
|
||||
.route("/ws", get(ws_handler))
|
||||
.route("/api/sessions", get(api_sessions))
|
||||
.route("/api/pairs", get(api_pairs))
|
||||
.route("/api/stats", get(api_stats))
|
||||
.route("/admin", get(admin_page))
|
||||
.nest_service("/", ServeDir::new(std::env::var("STATIC_DIR").unwrap_or_else(|_| "../static".to_string())))
|
||||
.with_state(state);
|
||||
|
||||
let addr = SocketAddr::from(([0, 0, 0, 0], 3000));
|
||||
tracing::debug!("Kipinä Agent Hub käynnistyy osoitteessa http://localhost:3000");
|
||||
|
||||
let listener = tokio::net::TcpListener::bind(&addr).await.unwrap();
|
||||
axum::serve(
|
||||
listener,
|
||||
app.into_make_service_with_connect_info::<SocketAddr>(),
|
||||
).await.unwrap();
|
||||
axum::serve(listener, app.into_make_service_with_connect_info::<SocketAddr>()).await.unwrap();
|
||||
}
|
||||
|
||||
async fn api_sessions(
|
||||
axum::extract::State(state): axum::extract::State<Arc<AppState>>,
|
||||
) -> impl IntoResponse {
|
||||
axum::Json(state.db.get_sessions(200))
|
||||
}
|
||||
|
||||
async fn api_pairs(
|
||||
axum::extract::State(state): axum::extract::State<Arc<AppState>>,
|
||||
) -> impl IntoResponse {
|
||||
axum::Json(state.db.get_pair_results(500))
|
||||
}
|
||||
|
||||
async fn api_stats(
|
||||
axum::extract::State(state): axum::extract::State<Arc<AppState>>,
|
||||
) -> impl IntoResponse {
|
||||
axum::Json(state.db.get_stats())
|
||||
}
|
||||
|
||||
async fn admin_page() -> impl IntoResponse {
|
||||
axum::response::Html(ADMIN_HTML)
|
||||
}
|
||||
|
||||
async fn ws_handler(
|
||||
@@ -298,6 +487,9 @@ async fn handle_socket(socket: WebSocket, state: Arc<AppState>, ip: IpAddr) {
|
||||
map.insert(node_id, allocated);
|
||||
}
|
||||
|
||||
// Tallennetaan sessiotieto tietokantaan
|
||||
state.db.insert_session(node_id, &ip.to_string(), node_type, &json);
|
||||
|
||||
if node_type == "native" {
|
||||
let sys = json.get("system");
|
||||
let hostname = sys.and_then(|s| s.get("hostname")).and_then(|v| v.as_str()).unwrap_or("?");
|
||||
@@ -383,6 +575,12 @@ async fn handle_socket(socket: WebSocket, state: Arc<AppState>, ip: IpAddr) {
|
||||
println!(" {} merkkiä → \x1b[35m{} tokenia\x1b[0m | \x1b[32m{:.2} merkkiä/token\x1b[0m", fi_chars, fi_tokens, fi_cpt);
|
||||
println!(" {}Suomen ylikustannus: {:+.1}%\x1b[0m", overhead_color, overhead);
|
||||
|
||||
// Tallennetaan parin tulos tietokantaan
|
||||
let en_ref = obj.get("en").cloned().unwrap_or_default();
|
||||
let fi_ref = obj.get("fi").cloned().unwrap_or_default();
|
||||
state.db.insert_pair_result(node_id, &en_ref, &fi_ref, overhead, duration);
|
||||
state.db.increment_tasks(node_id);
|
||||
|
||||
obj.insert("node_id".to_string(), serde_json::json!(node_id));
|
||||
}
|
||||
let _ = state.stats_tx.send(json.to_string());
|
||||
@@ -404,7 +602,8 @@ async fn handle_socket(socket: WebSocket, state: Arc<AppState>, ip: IpAddr) {
|
||||
}
|
||||
}
|
||||
|
||||
// Yhteys katkesi — siivotaan IP-laskuri ja node-tiedot
|
||||
// Yhteys katkesi — merkitään session päättyneeksi ja siivotaan
|
||||
state.db.close_session(node_id);
|
||||
{
|
||||
let mut conns = state.ip_connections.lock().unwrap();
|
||||
if let Some(count) = conns.get_mut(&ip) {
|
||||
|
||||
Reference in New Issue
Block a user