eka toimiva

This commit is contained in:
2026-04-01 22:14:48 +03:00
parent d70fd81f05
commit 02f6684378
8 changed files with 235 additions and 24 deletions

26
network-poc/deploy.sh Executable file
View File

@@ -0,0 +1,26 @@
#!/bin/bash
set -e
SERVER="ubuntu@86.50.252.98"
REMOTE_DIR="~/code/agentic-studio/network-poc"
SSH_OPTS="-o StrictHostKeyChecking=no"
echo "=== Kipinä Studio Deploy ==="
# 1. Rakennetaan Docker-image lokaalisti
echo "[1/4] Rakennetaan image lokaalisti..."
docker build -f Dockerfile.prod -t kipina-agentic:latest .
# 2. Tallennetaan ja siirretään
echo "[2/4] Siirretään image palvelimelle..."
docker save kipina-agentic:latest | gzip | ssh $SSH_OPTS $SERVER "gunzip | docker load"
# 3. Päivitetään konfiguraatiot
echo "[3/4] Päivitetään konfiguraatiot..."
scp $SSH_OPTS docker-compose.prod.yml Caddyfile.prod $SERVER:$REMOTE_DIR/
# 4. Käynnistetään uudelleen
echo "[4/4] Käynnistetään palvelut..."
ssh $SSH_OPTS $SERVER "cd $REMOTE_DIR && docker compose -f docker-compose.prod.yml up -d"
echo "=== Valmis! https://kipina.studio ==="

View File

@@ -14,9 +14,7 @@ services:
- hub
hub:
build:
context: .
dockerfile: Dockerfile.prod
image: kipina-agentic:latest
container_name: kipina-agentic-hub
restart: unless-stopped
environment:

View File

@@ -1,6 +1,6 @@
[package]
name = "hub"
version = "0.1.0"
version = "0.2.0"
edition = "2021"
[dependencies]

View File

@@ -245,7 +245,7 @@ impl NodeDb {
en: &serde_json::Value,
fi: &serde_json::Value,
overhead: f64,
duration_ms: u64,
duration_ms: f64,
) {
let conn = self.conn.lock().unwrap();
let now = chrono::Utc::now().to_rfc3339();
@@ -265,7 +265,7 @@ impl NodeDb {
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,
duration_ms,
],
);
}

View File

@@ -74,7 +74,7 @@ tr:hover td { background:#1c2333; }
</head>
<body>
<h1>Kipina Admin</h1>
<p class="sub">Node-sessiot ja tokenisointivertailut</p>
<p class="sub">Node-sessiot ja tokenisointivertailut · <span id="admin-version" style="color:var(--accent)">-</span></p>
<div id="stats" class="stats-grid"></div>
@@ -140,6 +140,9 @@ async function load() {
const sessions = await sessionsRes.json();
const pairs = await pairsRes.json();
// Versio
if (stats.version) document.getElementById('admin-version').textContent = 'v' + stats.version;
// Stats
document.getElementById('stats').innerHTML = [
{v: stats.total_sessions, l: 'Sessioita'},
@@ -194,7 +197,7 @@ async function load() {
}
load();
setInterval(load, 10000);
setInterval(load, 1000);
</script>
</body>
</html>"##;
@@ -275,7 +278,7 @@ async fn main() {
.with_state(state);
let addr = SocketAddr::from(([0, 0, 0, 0], 3000));
tracing::debug!("Kipinä Agent Hub käynnistyy osoitteessa http://localhost:3000");
tracing::info!("Kipinä Agent Hub v{} käynnistyy osoitteessa http://localhost:3000", env!("CARGO_PKG_VERSION"));
let listener = tokio::net::TcpListener::bind(&addr).await.unwrap();
axum::serve(listener, app.into_make_service_with_connect_info::<SocketAddr>()).await.unwrap();
@@ -296,7 +299,9 @@ async fn api_pairs(
async fn api_stats(
axum::extract::State(state): axum::extract::State<Arc<AppState>>,
) -> impl IntoResponse {
axum::Json(state.db.get_stats())
let mut stats = state.db.get_stats();
stats.as_object_mut().unwrap().insert("version".to_string(), serde_json::json!(env!("CARGO_PKG_VERSION")));
axum::Json(stats)
}
async fn admin_page() -> impl IntoResponse {
@@ -358,6 +363,7 @@ async fn broadcast_stats(state: &Arc<AppState>) {
let completed = *state.total_tasks.lock().unwrap();
let stats_msg = serde_json::json!({
"type": "stats",
"version": env!("CARGO_PKG_VERSION"),
"nodes": total_nodes,
"vram_gb": total_vram,
"tasks": completed
@@ -553,7 +559,7 @@ async fn handle_socket(socket: WebSocket, state: Arc<AppState>, ip: IpAddr) {
let en = obj.get("en").unwrap_or(&empty);
let fi = obj.get("fi").unwrap_or(&empty);
let overhead = obj.get("overhead_pct").and_then(|v| v.as_f64()).unwrap_or(0.0);
let duration = obj.get("duration_ms").and_then(|v| v.as_u64()).unwrap_or(0);
let duration = obj.get("duration_ms").and_then(|v| v.as_f64()).unwrap_or(0.0);
let en_text = en.get("text").and_then(|v| v.as_str()).unwrap_or("");
let en_tokens = en.get("token_count").and_then(|v| v.as_u64()).unwrap_or(0);
@@ -568,7 +574,7 @@ async fn handle_socket(socket: WebSocket, state: Arc<AppState>, ip: IpAddr) {
let overhead_color = if overhead > 10.0 { "\x1b[31m" } else if overhead < -10.0 { "\x1b[32m" } else { "\x1b[33m" };
println!();
println!("\x1b[36m━━━ Solmu {} ━━━ {}ms ━━━\x1b[0m", node_id, duration);
println!("\x1b[36m━━━ Solmu {} ━━━ {:.2}ms ━━━\x1b[0m", node_id, duration);
println!(" \x1b[34mEN\x1b[0m \"{}\"", en_text);
println!(" {} merkkiä → \x1b[35m{} tokenia\x1b[0m | \x1b[32m{:.2} merkkiä/token\x1b[0m", en_chars, en_tokens, en_cpt);
println!(" \x1b[33mFI\x1b[0m \"{}\"", fi_text);

View File

@@ -15,6 +15,7 @@ web-sys = { version = "0.3.68", features = [
"HtmlElement",
"WebSocket",
"MessageEvent",
"Performance",
"console",
] }
serde = { version = "1.0", features = ["derive"] }

View File

@@ -117,10 +117,11 @@ async fn run_pair_comparison(en_text: String, fi_text: String, ws: Rc<RefCell<We
return;
};
let start_time = js_sys::Date::now();
let perf = web_sys::window().unwrap().performance().unwrap();
let start_time = perf.now();
let en_result = tokenize_text(&tokenizer, &en_text);
let fi_result = tokenize_text(&tokenizer, &fi_text);
let duration = (js_sys::Date::now() - start_time) as u64;
let duration_ms = perf.now() - start_time; // millisekunteja desimaalitarkkuudella
let en_cpt = en_result["chars_per_token"].as_f64().unwrap_or(0.0);
let fi_cpt = fi_result["chars_per_token"].as_f64().unwrap_or(0.0);
@@ -132,15 +133,15 @@ async fn run_pair_comparison(en_text: String, fi_text: String, ws: Rc<RefCell<We
((fi_tokens as f64 / en_tokens as f64) - 1.0) * 100.0
} else { 0.0 };
console_log!("EN: {} tokenia ({:.2} m/t) vs FI: {} tokenia ({:.2} m/t) | ylikustannus: {:.0}%",
en_tokens, en_cpt, fi_tokens, fi_cpt, overhead_pct);
console_log!("EN: {} tokenia ({:.2} m/t) vs FI: {} tokenia ({:.2} m/t) | ylikustannus: {:.0}% | {:.2}ms",
en_tokens, en_cpt, fi_tokens, fi_cpt, overhead_pct, duration_ms);
let pair_done = serde_json::json!({
"type": "pair_done",
"en": en_result,
"fi": fi_result,
"overhead_pct": (overhead_pct * 10.0).round() / 10.0,
"duration_ms": duration,
"duration_ms": (duration_ms * 100.0).round() / 100.0,
"tokenizer": "Qwen2.5-Coder-0.5B",
});
let _ = ws.borrow().send_with_str(&pair_done.to_string());

View File

@@ -129,6 +129,40 @@
.btn:hover { background-color: #2ea043; }
.hidden { display: none; }
.compat-banner {
border-radius: 6px;
padding: 14px 18px;
margin-bottom: 20px;
font-size: 14px;
line-height: 1.6;
display: none;
}
.compat-banner.gpu {
background: #23392020;
border: 1px solid #3fb95040;
color: var(--success-color);
}
.compat-banner.cpu {
background: #d2992215;
border: 1px solid #d2992240;
color: #d29922;
}
.compat-banner code {
background: #0d1117;
padding: 2px 6px;
border-radius: 3px;
font-size: 12px;
color: var(--text-color);
}
.compat-banner summary {
cursor: pointer;
font-weight: 600;
margin-bottom: 6px;
}
.compat-banner details[open] summary {
margin-bottom: 10px;
}
.chat-box {
background-color: var(--panel-bg);
border: 1px solid var(--border-color);
@@ -191,12 +225,30 @@
cursor: pointer;
}
.toggle-tokens:hover { color: var(--text-color); border-color: #8b949e; }
.metric-card {
background: var(--panel-bg);
border: 1px solid var(--border-color);
border-radius: 6px;
padding: 10px;
text-align: center;
}
.metric-val {
font-size: 20px;
font-weight: 700;
color: var(--accent-color);
}
.metric-label {
font-size: 11px;
color: #8b949e;
margin-top: 2px;
}
</style>
</head>
<body>
<div class="container">
<h1>Kipinä <span>Agent Dashboard</span></h1>
<p class="sub">Hajautettu WebGPU Laskentaverkko</p>
<p class="sub">Hajautettu WebGPU Laskentaverkko · <span id="hub-version" style="color:#58a6ff">-</span></p>
<!-- Global Cluster Statistics (UI) -->
<div class="dashboard-panel">
@@ -215,16 +267,51 @@
</div>
<div id="device-info" class="device-info"></div>
<div id="compat-banner" class="compat-banner"></div>
<div id="initial-state">
<button id="start-btn" class="btn">Liity laskentaverkkoon</button>
</div>
<div id="active-state" class="hidden">
<div class="slider-container">
<label for="gpu-load">Oman Laitteen Kuormitusrajoitin: <strong id="load-display" style="color:var(--accent-color);">50%</strong></label>
<input type="range" id="gpu-load" min="0" max="75" value="50">
<p style="font-size: 11px; color:#8b949e;">Hallitsee "Duty Cyclea" kuinka pitkään Wasm-ydin pakotetaan nukkumaan Tensorimatriisien laskennan välissä.</p>
<!-- Resurssipaneeli -->
<div style="background:#0d1117;border:1px solid var(--border-color);border-radius:6px;padding:16px;margin-bottom:16px">
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:12px">
<span style="font-weight:600;font-size:15px">Resurssien hallinta</span>
<span id="node-status" style="font-size:12px;color:var(--success-color)">Aktiivinen</span>
</div>
<!-- Kuormitussäädin -->
<div style="margin-bottom:14px">
<div style="display:flex;justify-content:space-between;font-size:13px;margin-bottom:4px">
<span>Laskentatehon rajoitin</span>
<strong id="load-display" style="color:var(--accent-color)">50%</strong>
</div>
<input type="range" id="gpu-load" min="0" max="100" value="50" style="width:100%;accent-color:var(--accent-color)">
<div style="display:flex;justify-content:space-between;font-size:11px;color:#8b949e;margin-top:2px">
<span>Pysäytetty</span><span>Säästö</span><span>Tasapaino</span><span>Suorituskyky</span><span>Maksimi</span>
</div>
</div>
<!-- Reaaliaikaiset metriikat -->
<div id="metrics-grid" style="display:grid;grid-template-columns:repeat(4,1fr);gap:8px;margin-top:12px">
<div class="metric-card">
<div class="metric-val" id="m-tasks">0</div>
<div class="metric-label">Tehtäviä</div>
</div>
<div class="metric-card">
<div class="metric-val" id="m-avg-time">-</div>
<div class="metric-label">Ka. aika</div>
</div>
<div class="metric-card">
<div class="metric-val" id="m-tokens">0</div>
<div class="metric-label">Tokeneita</div>
</div>
<div class="metric-card">
<div class="metric-val" id="m-uptime">0s</div>
<div class="metric-label">Käynnissä</div>
</div>
</div>
</div>
<div id="chat-box" class="chat-box hidden">
@@ -251,6 +338,29 @@
let currentChatMsg = null;
// Reaaliaikaiset metriikat
const metrics = {
tasks: 0,
totalTokens: 0,
totalTimeMs: 0,
startTime: null,
};
function updateMetrics() {
document.getElementById('m-tasks').textContent = metrics.tasks;
document.getElementById('m-tokens').textContent = metrics.totalTokens.toLocaleString('fi-FI');
document.getElementById('m-avg-time').textContent = metrics.tasks > 0
? (metrics.totalTimeMs / metrics.tasks).toFixed(1) + 'ms'
: '-';
if (metrics.startTime) {
const sec = Math.floor((Date.now() - metrics.startTime) / 1000);
if (sec < 60) document.getElementById('m-uptime').textContent = sec + 's';
else if (sec < 3600) document.getElementById('m-uptime').textContent = Math.floor(sec/60) + 'min';
else document.getElementById('m-uptime').textContent = Math.floor(sec/3600) + 'h ' + (Math.floor(sec/60)%60) + 'min';
}
}
setInterval(updateMetrics, 1000);
// Ylikirjoitetaan console.log uppoamaan lokilaatikkoon
const originalLog = console.log;
console.log = function(...args) {
@@ -272,9 +382,22 @@
// UI Slider Listener -> Lähettää arvon suoraan WebAssemblyn ytimeen!
loadSlider.addEventListener('input', (e) => {
loadDisplay.textContent = e.target.value + '%';
const val = parseInt(e.target.value);
loadDisplay.textContent = val + '%';
if (window.wasm_active) {
set_gpu_load(parseInt(e.target.value));
set_gpu_load(val);
}
// Tilapäivitys
const statusEl = document.getElementById('node-status');
if (val === 0) {
statusEl.textContent = 'Pysäytetty';
statusEl.style.color = '#f85149';
} else if (val <= 25) {
statusEl.textContent = 'Säästötila';
statusEl.style.color = '#d29922';
} else {
statusEl.textContent = 'Aktiivinen';
statusEl.style.color = 'var(--success-color)';
}
});
@@ -289,6 +412,9 @@
if (data.tasks !== undefined) {
statTasks.textContent = data.tasks;
}
if (data.version) {
document.getElementById('hub-version').textContent = 'v' + data.version;
}
} else if (data.type === "node_joined") {
chatBox.classList.remove('hidden');
const msgDiv = document.createElement('div');
@@ -321,6 +447,12 @@
const nodeId = data.node_id || "?";
const ms = data.duration_ms || 0;
// Päivitetään metriikat
metrics.tasks++;
metrics.totalTokens += (en.token_count || 0) + (fi.token_count || 0);
metrics.totalTimeMs += ms;
updateMetrics();
const enCpt = parseFloat((en.chars_per_token || 0).toFixed(2));
const fiCpt = parseFloat((fi.chars_per_token || 0).toFixed(2));
@@ -432,6 +564,52 @@
`Varaus: <span>${deviceInfo.allocated_gb} GB</span>`
].filter(Boolean).join(' &middot; ');
// Yhteensopivuusbanneri
const banner = document.getElementById('compat-banner');
banner.style.display = 'block';
if (hasWebGPU) {
banner.className = 'compat-banner gpu';
banner.innerHTML = `GPU-kiihdytys aktiivinen — ${gpuStr}`;
} else {
// Tunnistetaan selain ohjeen personointia varten
const ua = navigator.userAgent;
const isFirefox = ua.includes('Firefox');
const isChrome = ua.includes('Chrome') && !ua.includes('Edg');
const isBrave = ua.includes('Brave') || (navigator.brave && navigator.brave.isBrave);
const isSafari = ua.includes('Safari') && !ua.includes('Chrome');
const isLinux = ua.includes('Linux');
let browserTip = '';
if (isFirefox) {
browserTip = `
<p><strong>Firefox</strong> ei tue WebGPU:ta oletuksena.</p>
<p>Ota käyttöön: <code>about:config</code> → <code>dom.webgpu.enabled</code> = <code>true</code> → käynnistä uudelleen.</p>
<p>Tai vaihda Chromeen/Braveen — niissä WebGPU toimii oletuksena.</p>`;
} else if ((isChrome || isBrave) && isLinux) {
const browser = isBrave ? 'brave-browser' : 'google-chrome';
browserTip = `
<p><strong>${isBrave ? 'Brave' : 'Chrome'} + Linux</strong>: GPU-ajuri ei ehkä tarjoa WebGPU:ta Wayland-ympäristössä.</p>
<p>Kokeile käynnistää selain komentoriviltä:</p>
<code>${browser} --enable-unsafe-webgpu --enable-features=Vulkan --ignore-gpu-blocklist --use-angle=vulkan --ozone-platform=x11</code>`;
} else if (isSafari) {
browserTip = `
<p><strong>Safari</strong>: WebGPU on tuettu versiosta 26 alkaen (macOS Tahoe).</p>
<p>Vanhemmissa versioissa: Develop → Feature Flags → WebGPU.</p>`;
} else {
browserTip = `
<p>Selaimesi ei tue WebGPU:ta. Kokeile <strong>Chrome 113+</strong> tai <strong>Brave</strong>.</p>`;
}
banner.className = 'compat-banner cpu';
banner.innerHTML = `
<details>
<summary>CPU-laskenta (WebGPU ei käytettävissä) — klikkaa ohjeita</summary>
${browserTip}
<p style="margin-top:8px;color:#8b949e;font-size:12px">Laskenta toimii silti CPU:lla, mutta GPU-kiihdytys olisi nopeampi.</p>
</details>`;
}
document.getElementById('initial-state').classList.add('hidden');
document.getElementById('active-state').classList.remove('hidden');
btn.style.display = 'none';
@@ -440,6 +618,7 @@
console.log("Ladataan Burn Wasm -binääriä...");
await init();
window.wasm_active = true;
metrics.startTime = Date.now();
// Varmistetaan, että Wasm saa nykyisen sliderin arvon heti kärkeen
set_gpu_load(parseInt(loadSlider.value));