diff --git a/public/app.js b/public/app.js new file mode 100644 index 0000000..3b8ec2d --- /dev/null +++ b/public/app.js @@ -0,0 +1,484 @@ +/* ===================================================================== + 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(); +});