/* ===================================================================== MISSION CONTROL — frontend mantığı Saf HTML5 canvas (harici kütüphane yok): · 3 dolan (gauge) göstergesi — CPU, MEMORY, NET · disk yüzük göstergesi · 30 saniyelik akan neon CPU çizgi grafiği (izgara + glow) · canlı proses tablosu + KİLL (DELETE) · akan olay konsolu (ok/warn/crit) ===================================================================== */ 'use strict'; /* ---------- kısa yardımcılar ---------- */ const $ = (id) => document.getElementById(id); const clampV = (v, lo, hi) => v < lo ? lo : (v > hi ? hi : v); const mapRange = (v, inMin, inMax, outMin, outMax) => outMin + ((v - inMin) / (inMax - inMin)) * (outMax - outMin); const sys = { cpu: 0, cpuHistory: [], // son 30 sn cpu % (frontend ring) memPct: 0, memUsedMB: 0, diskPct: 0, diskUsedMB: 0, netIn: 0, netOut: 0, rowMap: new Map(), // pid -> lastLogTs: 0, // olay konsolu için karşılaştırma lastCpuTick: Date.now(), }; const HISTORY_LEN = 30; // saniye const LOG_MAX = 40; // konsol satır limiti /* ===================================================================== GAUGE ÇİZİMİ (dairesel/dolan) value 0..1, renk scale'le birlikte değişir ===================================================================== */ const GAUGE_KIND_TO_COLORS = { cpu: ['#00e5ff', '#ffb020', '#ff3550'], mem: ['#39ff88', '#ffb020', '#ff3550'], net: ['#00e5ff', '#39ff88', '#ff3550'], }; const GAUGE_CENTER = { cpu: $('gaugeCpu'), mem: $('gaugeMem'), net: $('gaugeNet') }; function colorForWarning(c, i) { // alt sınır yeşil/camgöbeği değil, % value'ya göre seçim yapalım if (c.warn >= 0.9) return '#ff3550'; if (c.warn >= 0.65) return '#ffb020'; return c.base; } function drawArcBar(canvas, frac, baseColor, tick) { const ctx = canvas.getContext('2d'); ctx.clearRect(0, 0, canvas._logW || canvas.width, canvas._logH || canvas.height); const w = canvas._logW || canvas.width, h = canvas._logH || canvas.height; const cx = w / 2, cy = h / 2; const R = Math.min(w, h) / 2 - 20; ctx.clearRect(0, 0, w, h); const start = Math.PI * 0.75; // başlangıç (220°) const end = Math.PI * 0.25; // bitiş const sweep = Math.PI * 1.5; // 270° dairesel yay const fracC = clampV(frac, 0, 1); const angleFrom = start; const angleTo = start + sweep * fracC; // taban kemer ctx.lineWidth = 13; ctx.lineCap = 'round'; ctx.beginPath(); ctx.arc(cx, cy, R, start, end, false); ctx.strokeStyle = 'rgba(80, 120, 160, 0.18)'; ctx.stroke(); // dolan kemer degrade const safeColor = colorForWarning({ base: baseColor, warn: fracC }, 0); const grad = ctx.createLinearGradient(cx - R, cy - R, cx + R, cy); grad.addColorStop(0, safeColor); grad.addColorStop(1, lighten(safeColor, 0.35)); ctx.shadowColor = safeColor; ctx.shadowBlur = 12; ctx.beginPath(); ctx.arc(cx, cy, R, angleFrom, angleTo, false); ctx.strokeStyle = grad; ctx.lineWidth = 13; ctx.stroke(); ctx.shadowBlur = 0; // uçta parlak "damla" nokta const tipX = cx + Math.cos(angleTo) * R; const tipY = cy + Math.sin(angleTo) * R; ctx.beginPath(); ctx.arc(tipX, tipY, 7, 0, Math.PI * 2); ctx.fillStyle = '#ffffff'; ctx.fill(); ctx.shadowColor = safeColor; ctx.shadowBlur = 14; ctx.fillStyle = safeColor; ctx.beginPath(); ctx.arc(tipX, tipY, 5, 0, Math.PI * 2); ctx.fill(); ctx.shadowBlur = 0; // merkezde yüzde ctx.textAlign = 'center'; ctx.textBaseline = 'middle'; ctx.font = 'bold 34px Consolas, monospace'; ctx.fillStyle = safeColor; ctx.fillText(String(Math.round(fracC * 100)) + '%', cx, cy - 10); ctx.font = '10px Consolas, monospace'; ctx.fillStyle = 'rgba(120,190,255,0.5)'; ctx.fillText(tick, cx, cy + 18); } function lighten(hex, amt) { const c = hex.replace('#', ''); const r = parseInt(c.slice(0, 2), 16), g = parseInt(c.slice(2, 4), 16), b = parseInt(c.slice(4, 6), 16); const L = (v) => Math.round(clampV(v + (255 - v) * amt, 0, 255)); return '#' + [L(r), L(g), L(b)].map(x => x.toString(16).padStart(2, '0')).join(''); } /* ===================================================================== DISK YÜZÜK GÖSTERGESİ (tam çember yüzdelik kullanım) ===================================================================== */ function drawDiskGauge(canvas, frac) { const ctx = canvas.getContext('2d'); const w = canvas._logW || canvas.width, h = canvas._logH || canvas.height; const cx = w / 2, cy = h / 2; const outer = Math.min(w, h) / 2 - 10; ctx.clearRect(0, 0, canvas.width, canvas.height); const f = clampV(frac, 0, 1); // renk: %70 altı yeşil, %70-90 amber, üstü kırmızı const color = f > 0.9 ? '#ff3550' : (f > 0.7 ? '#ffb020' : '#39ff88'); // taban ctx.lineWidth = 12; ctx.lineCap = 'round'; ctx.beginPath(); ctx.arc(cx, cy, outer, 0, Math.PI * 2); ctx.strokeStyle = 'rgba(80,120,160,0.16)'; ctx.stroke(); // dolan çember (yukarıdan başla) ctx.shadowColor = color; ctx.shadowBlur = 10; ctx.beginPath(); ctx.arc(cx, cy, outer, -Math.PI / 2, -Math.PI / 2 + Math.PI * 2 * f); ctx.strokeStyle = color; ctx.lineWidth = 12; ctx.stroke(); ctx.shadowBlur = 0; // uç nokta const a = -Math.PI / 2 + Math.PI * 2 * f; ctx.beginPath(); ctx.arc(cx + Math.cos(a) * outer, cy + Math.sin(a) * outer, 6, 0, Math.PI * 2); ctx.fillStyle = '#fff'; ctx.fill(); } /* ===================================================================== CPU GRAFİK — akan neon çizgi, izgara, degradeli dolgu ===================================================================== */ const chartCanvas = $('cpuChart'); let gridReady = false; function drawChartBG(ctx, W, H) { if (gridReady) return; // izgarayı sadece 1 kez çiz (arka katman) // dikey çizgiler const cols = 6, rows = 5; ctx.strokeStyle = 'rgba(0,180,255,0.08)'; ctx.lineWidth = 1; for (let i = 0; i <= cols; i++) { const x = Math.round(mapRange(i, 0, cols, 14, W - 10)); ctx.beginPath(); ctx.moveTo(x, 8); ctx.lineTo(x, H - 20); ctx.stroke(); } for (let j = 0; j <= rows; j++) { const y = Math.round(mapRange(j, 0, rows, 8, H - 20)); ctx.beginPath(); ctx.moveTo(14, y); ctx.lineTo(W - 10, y); ctx.stroke(); } // y eksen etiketleri ctx.fillStyle = 'rgba(120,190,255,0.4)'; ctx.font = '10px Consolas, monospace'; ctx.textAlign = 'left'; ctx.textBaseline = 'bottom'; ctx.fillText('100%', 14, 8); ctx.fillStyle = 'rgba(120,190,255,0.25)'; ctx.fillText('cpu load', W - 70, 14); gridReady = true; } function pushHistoryAndDraw() { const c = chartCanvas.getContext('2d'); const W = chartCanvas._logW || chartCanvas.width; const H = chartCanvas._logH || chartCanvas.height; c.clearRect(0, 0, chartCanvas.width, chartCanvas.height); drawChartBG(c, W, H); if (sys.cpuHistory.length < 2) return; const plotW = W - 24, plotH = H - 28; const n = HISTORY_LEN, maxVal = 100; // verinin son n noktası const arr = sys.cpuHistory.slice(-n); const px = (i) => 14 + (i / Math.max(1, n - 1)) * plotW; const py = (v) => 8 + (1 - clampV(v, 0, maxVal) / maxVal) * plotH; // degradeli dolgu const col = arr[arr.length - 1] > 85 ? '#ff3550' : '#00e5ff'; c.beginPath(); c.moveTo(px(0), py(arr[0])); for (let i = 1; i < arr.length; i++) c.lineTo(px(i), py(arr[i])); c.lineTo(px(arr.length - 1), H - 12); c.lineTo(px(0), H - 12); c.closePath(); const g = c.createLinearGradient(0, 8, 0, H - 12); g.addColorStop(0, 'rgba(0,229,255,0.28)'); g.addColorStop(1, 'rgba(0,229,255,0.0)'); c.fillStyle = col[0] === '#ff' ? 'rgba(255,53,80,0.22)' : g; c.fill(); // neon çizgi c.beginPath(); c.moveTo(px(0), py(arr[0])); for (let i = 1; i < arr.length; i++) c.lineTo(px(i), py(arr[i])); c.strokeStyle = col; c.lineWidth = 2.5; c.shadowColor = col; c.shadowBlur = 12; c.stroke(); c.shadowBlur = 0; // son nokta glow damlası const lx = px(arr.length - 1), ly = py(arr[arr.length - 1]); c.beginPath(); c.arc(lx, ly, 5, 0, Math.PI * 2); c.fillStyle = '#fff'; c.fill(); c.shadowColor = col; c.shadowBlur = 10; c.fillStyle = col; c.beginPath(); c.arc(lx, ly, 3.5, 0, Math.PI * 2); c.fill(); c.shadowBlur = 0; // %80 eşik çizgisi (kehribar kesikli) const tY = py(80); c.save(); c.strokeStyle = 'rgba(255,176,32,0.4)'; c.setLineDash([5, 4]); c.lineWidth = 1; c.shadowBlur = 0; c.beginPath(); c.moveTo(14, tY); c.lineTo(W - 10, tY); c.stroke(); c.restore(); // /dross: değer etiketi üst solda c.fillStyle = col; c.font = 'bold 13px Consolas, monospace'; c.textAlign = 'left'; c.textBaseline = 'top'; c.fillText('CPU ' + Math.max(0, arr[arr.length - 1]).toFixed(1) + '%', 18, 42); } /* ===================================================================== METRİKLERİ ÇEK & RENDER ===================================================================== */ async function refreshMetrics() { let data; try { const r = await fetch('/metrics'); data = await r.json(); } catch (e) { setServerDown(); return; } sys.cpu = data.cpu; sys.memPct = data.memory.percent; sys.memUsedMB = data.memory.usedMB; sys.diskPct = data.disk.percent; sys.diskUsedMB = data.disk.usedMB; sys.netIn = data.network.inMbps; sys.netOut = data.network.outMbps; sys.processesRunning = data.processesRunning; // gauge güncelle drawArcBar(GAUGE_CENTER.cpu, sys.cpu / 100, '#00e5ff', 'consolidated'); drawArcBar(GAUGE_CENTER.mem, sys.memPct / 100, '#39ff88', 'of 8192 MB'); drawArcBar(GAUGE_CENTER.net, (clampV(sys.netIn, 0, 80) / 80), '#00e5ff', 'ingress Mbps'); drawDiskGauge($('diskGauge'), sys.diskPct / 100); // etiketler $('cpuPct').textContent = Math.round(sys.cpu) + '%'; $('memPct').textContent = sys.memPct + '%'; $('memText').textContent = fmtMB(sys.memUsedMB); $('netVal').textContent = sys.netIn.toFixed(1); $('netIn').textContent = sys.netIn.toFixed(1); $('netOut').textContent = sys.netOut.toFixed(1); $('diskScore').textContent = sys.diskPct; $('diskText').textContent = fmtMB(sys.diskUsedMB); // istatistik üst $('serverStatus').textContent = data.uptimeSec ? 'ONLINE' : 'ONLINE'; $('runCount').textContent = data.processesRunning; $('uptime').textContent = fmtUp(data.uptimeSec); $('procBadge').textContent = data.processesRunning + ' online'; // canvas cpu geçmişini besle if (data.cpuHistory && Array.isArray(data.cpuHistory)) { sys.cpuHistory = data.cpuHistory.slice(); } else { sys.cpuHistory.push(sys.cpu); if (sys.cpuHistory.length > HISTORY_LEN) sys.cpuHistory.shift(); } pushHistoryAndDraw(); // olayları işle if (data.events) consumeEvents(data.events); } function setServerDown() { $('serverStatus').textContent = 'OFFLINE'; $('serverStatus').style.color = 'var(--red)'; } function fmtMB(mb) { if (mb >= 1024) return (mb / 1024).toFixed(2) + ' GB'; return mb.toFixed(0) + ' MB'; } function fmtUp(sec) { if (!sec) return '—'; const m = Math.floor(sec / 60), s = sec % 60; return m + 'm ' + String(s).padStart(2, '0') + 's'; } /* ===================================================================== OLAY KONSOLU ===================================================================== */ const consoleEl = $('logConsole'); const seenLog = new Set(); // aynı olayı tekrar basmasın (sürece) function consumeEvents(events) { for (const e of events) { const key = e.time + '|' + e.tag + '|' + e.message; if (seenLog.has(key)) continue; seenLog.add(key); if (seenLog.size > 120) seenLog.clear(); appendLog(e); } } function appendLog(e) { const line = document.createElement('div'); line.className = 'line type-' + (e.type || 'info'); const t = document.createElement('span'); t.className = 't'; t.textContent = e.time + ' ▸'; const tag = document.createElement('span'); tag.className = 'tag'; tag.textContent = e.tag.toUpperCase(); const msg = document.createElement('span'); msg.className = 'msg'; msg.textContent = e.message; line.appendChild(t); line.appendChild(tag); line.appendChild(msg); consoleEl.prepend(line); // max liste buda while (consoleEl.children.length > LOG_MAX) consoleEl.removeChild(consoleEl.lastChild); } /* ===================================================================== PROSES TABLOSU ===================================================================== */ const procBody = $('procBody'); async function loadProcesses() { let data; try { const r = await fetch('/processes'); data = await r.json(); } catch (err) { return; } const list = data.processes || []; // mevcut gösterilen pids const shownPids = new Set(); for (const p of list) { shownPids.add(p.pid); if (sys.rowMap.has(p.pid)) { updateRow(p, sys.rowMap.get(p.pid)); } else { renderRow(p); } } // listede artık yoksa (kill/durable) kaldır for (const pid of [...sys.rowMap.keys()]) { if (!shownPids.has(pid)) { const row = sys.rowMap.get(pid); row.remove(); sys.rowMap.delete(pid); } } $('procEmpty').classList.toggle('hidden', list.length > 0); $('procBadge').textContent = list.length + ' online'; } function renderRow(p) { const row = document.createElement('tr'); row.dataset.pid = p.pid; let html = ''; html += '' + p.pid + ''; html += '' + escapeHtml(p.service) + ''; html += '' + p.cpu.toFixed(1) + '%'; html += '' + fmtMB(p.memMB) + ''; html += 'RUNNING'; html += ''; row.innerHTML = html; procBody.appendChild(row); sys.rowMap.set(p.pid, row); } function updateRow(p, row) { const cells = row.querySelectorAll('td'); if (cells.length >= 4) { cells[2].textContent = p.cpu.toFixed(1) + '%'; cells[2].classList.toggle('hot', p.cpu > 85 || p.cpu > (p.maxCpu || 90)); cells[3].textContent = fmtMB(p.memMB); } } function escapeHtml(x) { return String(x).replace(/[&<>"]/g, (c) => ({ '&': '&', '<': '<', '>': '>', '"': '"' }[c])); } /* KİLL — DELETE isteği → satırı sil */ async function killProcess(pid, btn) { if (btn) { btn.disabled = true; btn.textContent = '…'; } try { const res = await fetch('/processes/' + encodeURIComponent(pid), { method: 'DELETE' }); const data = await res.json(); if (data.ok) { const row = sys.rowMap.get(data.pid); if (row) { row.remove(); sys.rowMap.delete(data.pid); } appendLog({ time: tsNow(), type: 'crit', tag: 'SYS', message: 'process ' + data.service + ' terminated by operator (pid ' + data.pid + ')' }); $('procEmpty').classList.toggle('hidden', sys.rowMap.size > 0); } else { if (btn) { btn.disabled = false; btn.textContent = '✕ KILL'; } appendLog({ time: tsNow(), type: 'warn', tag: 'SYS', message: 'kill failed — ' + (data.error || 'unknown') }); } } catch (e) { if (btn) { btn.disabled = false; btn.textContent = '✕ KILL'; } appendLog({ time: tsNow(), type: 'warn', tag: 'ERR', message: 'network error on terminate' }); } } function tsNow() { const d = new Date(), p = (n) => String(n).padStart(2, '0'); return p(d.getHours()) + ':' + p(d.getMinutes()) + ':' + p(d.getSeconds()); } /* event delegation — dinamik butonlar için */ function bindKillButtons() { procBody.addEventListener('click', (e) => { const btn = e.target.closest('.kill'); if (!btn) return; e.stopPropagation(); killProcess(btn.dataset.pid, btn); }); } /* ===================================================================== GÖREV DÖNGÜSÜ (1 sn'de 1) ===================================================================== */ function tick() { refreshMetrics(); // /metrics → gauge + grafik + olaylar loadProcesses(); // /processes → tablo yansıt } function init() { // kartuş değerleri sıfır göster → hızlı ilk boya drawArcBar(GAUGE_CENTER.cpu, 0, '#00e5ff', 'consolidated'); drawArcBar(GAUGE_CENTER.mem, 0, '#39ff88', 'of 8192 MB'); drawArcBar(GAUGE_CENTER.net, 0, '#00e5ff', 'ingress Mbps'); drawDiskGauge($('diskGauge'), 0); pushHistoryAndDraw(); bindKillButtons(); tick(); setInterval(tick, 1000); } // Canvas'ları yüksek dpi çözünürlüğe uyarla (keskin çizgi, boyutlar tutarlı kalır) function setupHiDPI() { const dpr = window.devicePixelRatio || 1; const setCanvas = (c, logW, logH) => { c.width = Math.round(logW * dpr); c.height = Math.round(logH * dpr); c.style.width = (c.id === 'cpuChart' ? '100%' : logW + 'px'); c.style.height = logH + 'px'; // DRAW fonksiyonları çizim alanını CSS/cihaz px cinsinden bilsin: // CSS koordinatlarına geri dönmek için scale store c._logW = logW; c._logH = logH; c._dpr = dpr; const ctx = c.getContext('2d'); ctx.setTransform(dpr, 0, 0, dpr, 0, 0); }; setCanvas(chartCanvas, 980, 240); setCanvas(GAUGE_CENTER.cpu, 220, 220); setCanvas(GAUGE_CENTER.mem, 220, 220); setCanvas(GAUGE_CENTER.net, 220, 220); setCanvas($('diskGauge'), 200, 200); gridReady = false; } window.addEventListener('load', () => { setupHiDPI(); init(); });