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();
+});