webpty / views /terminal.ejs
Ditzzy AF
fix: Improve Windows network stats accuracy and terminal height
f937534
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Cv3inx - Web Terminal</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;600&display=swap" rel="stylesheet">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/xterm@5.3.0/css/xterm.css">
<style>
:root {
--primary: #10b981;
--primary-dark: #059669;
--bg: #0c0c0c;
--bg-card: #18181b;
--bg-header: #23232a;
--border: #27272a;
--border-light: #3f3f46;
--text: #e4e4e7;
--text-muted: #a1a1aa;
--danger: #ef4444;
--shadow: 0 4px 24px rgba(0,0,0,0.25);
}
* { box-sizing: border-box; margin: 0; padding: 0; }
html, body { height: 100%; }
body {
font-family: 'JetBrains Mono', monospace;
background: var(--bg);
color: var(--text);
min-height: 100vh;
overflow-x: hidden;
}
.header {
background: var(--bg-header);
border-bottom: 1px solid var(--border-light);
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 20px;
height: 56px;
position: sticky;
top: 0;
z-index: 10;
box-shadow: 0 2px 8px rgba(0,0,0,0.08);
}
.logo {
display: flex;
align-items: center;
gap: 10px;
font-weight: 600;
font-size: 18px;
letter-spacing: 1px;
}
.logo-dot {
width: 10px; height: 10px;
background: var(--primary);
border-radius: 50%;
animation: pulse 2s infinite;
}
@keyframes pulse {
0%, 100% { opacity: 1; }
50% { opacity: 0.3; }
}
.header-actions {
display: flex;
gap: 10px;
}
.btn {
background: rgba(255,255,255,0.05);
border: 1px solid var(--border-light);
color: var(--text);
padding: 6px 14px;
border-radius: 6px;
font-size: 13px;
cursor: pointer;
font-family: inherit;
transition: all 0.2s;
}
.btn:hover { background: rgba(16,185,129,0.08); border-color: var(--primary); }
.btn.danger:hover { background: rgba(239,68,68,0.15); border-color: var(--danger); }
.main {
display: flex;
flex-direction: row;
height: calc(100vh - 56px);
background: var(--border);
}
.terminal-panel {
flex: 2;
display: flex;
flex-direction: column;
justify-content: stretch;
background: var(--bg-card);
border-right: 1px solid var(--border-light);
min-width: 0;
}
.terminal-card {
background: var(--bg-card);
border-radius: 18px;
box-shadow: var(--shadow);
border: 1px solid var(--border);
margin: 24px;
margin-bottom: 0;
display: flex;
flex-direction: column;
flex: 1;
min-width: 0;
}
.terminal-header {
display: flex;
align-items: center;
gap: 10px;
padding: 14px 20px;
border-bottom: 1px solid var(--border-light);
font-size: 15px;
font-weight: 600;
background: rgba(39,39,42,0.92);
border-radius: 18px 18px 0 0;
}
.terminal-icon {
color: var(--primary);
font-size: 18px;
}
#terminal {
flex: 1;
padding: 16px;
background: var(--bg);
border-radius: 0 0 18px 18px;
min-width: 0;
min-height: 180px;
max-height: 60vh;
height: 40vh;
transition: height 0.2s;
box-sizing: border-box;
overflow: auto;
}
.connection-status {
position: absolute;
top: 18px;
right: 36px;
font-size: 12px;
color: var(--text-muted);
display: flex;
align-items: center;
gap: 6px;
}
.connection-dot {
width: 8px; height: 8px;
border-radius: 50%;
background: var(--danger);
transition: background-color 0.3s;
}
.connection-dot.connected { background: var(--primary); }
/* Sidebar */
.sidebar {
flex: 1;
background: var(--bg-card);
display: flex;
flex-direction: column;
border-left: 1px solid var(--border-light);
min-width: 0;
max-width: 370px;
transition: transform 0.3s;
}
.sidebar-inner {
padding: 24px 18px 18px 18px;
overflow-y: auto;
height: 100%;
}
.sidebar-section {
background: rgba(24,24,27,0.7);
border-radius: 14px;
border: 1px solid var(--border-light);
margin-bottom: 18px;
padding: 18px 16px;
box-shadow: 0 2px 8px rgba(0,0,0,0.08);
}
.section-title {
font-size: 13px;
font-weight: 700;
color: var(--primary);
margin-bottom: 10px;
letter-spacing: 0.5px;
display: flex;
align-items: center;
gap: 7px;
}
.info-item {
display: flex;
justify-content: space-between;
align-items: center;
font-size: 12px;
margin-bottom: 7px;
}
.info-label { color: var(--text-muted); }
.info-value { color: var(--text); font-weight: 500; }
.metric-bar {
width: 100%;
height: 5px;
background: rgba(255,255,255,0.07);
border-radius: 2px;
margin-bottom: 10px;
overflow: hidden;
}
.metric-fill {
height: 100%;
border-radius: 2px;
transition: width 0.3s;
}
.cpu-fill { background: linear-gradient(90deg, #10b981, #059669); }
.memory-fill { background: linear-gradient(90deg, #3b82f6, #2563eb); }
.disk-fill { background: linear-gradient(90deg, #f59e0b, #d97706); }
.mini-chart {
height: 48px;
background: rgba(255,255,255,0.03);
border-radius: 8px;
margin-top: 10px;
display: flex;
align-items: end;
gap: 1px;
padding: 3px;
}
.chart-bar {
flex: 1;
background: rgba(16,185,129,0.3);
border-radius: 1px 1px 0 0;
min-height: 2px;
transition: height 0.3s;
}
.processes {
max-height: 120px;
overflow-y: auto;
}
.process-item {
display: flex;
justify-content: space-between;
align-items: center;
font-size: 11px;
padding: 4px 0;
border-bottom: 1px solid rgba(255,255,255,0.05);
}
.process-item:last-child { border-bottom: none; }
.process-name { color: var(--text); flex: 1; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.process-cpu { color: var(--primary); font-weight: 500; margin-left: 8px; }
/* Mobile Styles */
@media (max-width: 900px) {
.main { flex-direction: column; }
.sidebar {
position: fixed;
left: 0; right: 0; bottom: 0;
top: unset;
max-width: 100vw;
height: 70vh;
z-index: 1001;
transform: translateY(100%);
box-shadow: 0 -10px 40px rgba(0,0,0,0.6);
border-radius: 18px 18px 0 0;
border-left: none;
border-top: 2px solid var(--border-light);
background: var(--bg-card);
transition: transform 0.3s;
display: flex;
}
.sidebar.show { transform: translateY(0); }
.sidebar-inner { padding: 18px 8px 8px 8px; }
.terminal-panel { border-right: none; }
.terminal-card { margin: 12px 6px 0 6px; border-radius: 14px; }
.terminal-header { border-radius: 14px 14px 0 0; padding: 10px 14px; }
#terminal { border-radius: 0 0 14px 14px; padding: 10px; }
.connection-status { right: 16px; top: 10px; font-size: 11px; }
}
@media (max-width: 600px) {
.header { height: 48px; padding: 0 10px; font-size: 14px; }
.logo { font-size: 14px; }
.terminal-card { margin: 8px 2px 0 2px; border-radius: 10px; }
.terminal-header { border-radius: 10px 10px 0 0; padding: 8px 8px; }
#terminal { border-radius: 0 0 10px 10px; padding: 6px; min-height: 90px; height: 28vh; max-height: 35vh; }
.sidebar { border-radius: 10px 10px 0 0; }
.sidebar-inner { padding: 8px 2px 2px 2px; }
}
/* Mobile toggle button */
.mobile-toggle {
display: none;
}
@media (max-width: 900px) {
.mobile-toggle {
display: flex;
position: fixed;
bottom: 18px;
right: 18px;
width: 54px;
height: 54px;
background: linear-gradient(135deg, var(--primary), var(--primary-dark));
border: none;
border-radius: 50%;
color: white;
font-size: 26px;
cursor: pointer;
z-index: 1100;
box-shadow: 0 8px 25px rgba(16,185,129,0.4);
align-items: center;
justify-content: center;
transition: all 0.3s;
}
.mobile-toggle.active {
background: linear-gradient(135deg, var(--danger), #dc2626);
transform: rotate(45deg);
}
}
.mobile-overlay {
display: none;
}
@media (max-width: 900px) {
.mobile-overlay {
display: block;
position: fixed;
top: 0; left: 0; right: 0; bottom: 0;
background: rgba(0,0,0,0.5);
z-index: 1000;
opacity: 0;
pointer-events: none;
transition: opacity 0.3s;
}
.mobile-overlay.show {
opacity: 1;
pointer-events: all;
}
}
</style>
</head>
<body>
<div class="header">
<div class="logo">
<div class="logo-dot"></div>
Cv3inx - Terminal
</div>
<div class="header-actions">
<button class="btn" id="clear-btn">Clear</button>
<a href="/logout" class="btn danger">Logout</a>
</div>
</div>
<div class="main">
<div class="terminal-panel">
<div class="terminal-card" style="position:relative;">
<div class="terminal-header">
<span class="terminal-icon">🖥️</span>
Shell Session
</div>
<div id="terminal"></div>
<div class="connection-status">
<div class="connection-dot" id="connection-dot"></div>
<span id="connection-text">Connecting...</span>
</div>
</div>
</div>
<div class="sidebar" id="sidebar">
<div class="sidebar-inner">
<div class="sidebar-section">
<div class="section-title">System Overview</div>
<div class="info-item"><span class="info-label">🖥️ Host</span><span class="info-value" id="hostname">-</span></div>
<div class="info-item"><span class="info-label">⏱️ Uptime</span><span class="info-value" id="uptime">-</span></div>
<div class="info-item"><span class="info-label">👥 Users</span><span class="info-value" id="users">-</span></div>
</div>
<div class="sidebar-section">
<div class="section-title">Performance</div>
<div class="info-item"><span class="info-label">🧠 CPU</span><span class="info-value" id="cpu-usage">0%</span></div>
<div class="metric-bar"><div class="metric-fill cpu-fill" id="cpu-bar" style="width:0%"></div></div>
<div class="info-item"><span class="info-label">💾 Memory</span><span class="info-value" id="memory-usage">0%</span></div>
<div class="metric-bar"><div class="metric-fill memory-fill" id="memory-bar" style="width:0%"></div></div>
<div class="info-item"><span class="info-label">💿 Disk</span><span class="info-value" id="disk-usage">0%</span></div>
<div class="metric-bar"><div class="metric-fill disk-fill" id="disk-bar" style="width:0%"></div></div>
<div class="mini-chart" id="cpu-chart"></div>
</div>
<div class="sidebar-section">
<div class="section-title">Network & Storage</div>
<div class="info-item"><span class="info-label">🌐 Network RX</span><span class="info-value" id="network-rx">0 KB/s</span></div>
<div class="info-item"><span class="info-label">📡 Network TX</span><span class="info-value" id="network-tx">0 KB/s</span></div>
<div class="info-item"><span class="info-label">💾 Memory Used</span><span class="info-value" id="memory-details">0 MB / 0 MB</span></div>
<div class="info-item"><span class="info-label">💿 Disk Space</span><span class="info-value" id="disk-details">0 GB / 0 GB</span></div>
</div>
<div class="sidebar-section">
<div class="section-title">System Details</div>
<div class="info-item"><span class="info-label">🏗️ Platform</span><span class="info-value" id="platform">-</span></div>
<div class="info-item"><span class="info-label">🔧 Architecture</span><span class="info-value" id="arch">-</span></div>
<div class="info-item"><span class="info-label">🧠 CPU Model</span><span class="info-value" id="cpu-model">-</span></div>
<div class="info-item"><span class="info-label">⚡ CPU Cores</span><span class="info-value" id="cpu-cores">-</span></div>
<div class="info-item"><span class="info-label">📊 Load Avg</span><span class="info-value" id="load-avg">-</span></div>
</div>
<div class="sidebar-section">
<div class="section-title">Top Processes</div>
<div class="processes" id="processes">
<div class="process-item">
<div class="process-name">Loading...</div>
<div class="process-cpu">-</div>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Mobile elements -->
<div class="mobile-overlay" id="mobile-overlay"></div>
<button class="mobile-toggle" id="mobile-toggle">📊</button>
<script src="/socket.io/socket.io.js"></script>
<script src="https://cdn.jsdelivr.net/npm/xterm@5.3.0/lib/xterm.js"></script>
<script>
// Terminal & socket logic (sama seperti sebelumnya)
let terminal, socket, isConnected = false, cpuHistory = [];
function initializeTerminal() {
terminal = new Terminal({
cursorBlink: true,
fontSize: 14,
fontFamily: 'JetBrains Mono, monospace',
theme: {
background: '#0c0c0c',
foreground: '#e4e4e7',
cursor: '#10b981',
selection: 'rgba(16,185,129,0.3)',
black: '#27272a',
red: '#ef4444',
green: '#10b981',
yellow: '#f59e0b',
blue: '#3b82f6',
magenta: '#a855f7',
cyan: '#06b6d4',
white: '#f4f4f5'
}
});
terminal.open(document.getElementById('terminal'));
terminal.focus();
connectToServer();
document.getElementById('clear-btn').addEventListener('click', clearTerminal);
initializeMobileToggle();
}
function initializeMobileToggle() {
const mobileToggle = document.getElementById('mobile-toggle');
const sidebar = document.getElementById('sidebar');
const overlay = document.getElementById('mobile-overlay');
if (mobileToggle && sidebar && overlay) {
mobileToggle.addEventListener('click', () => {
const isVisible = sidebar.classList.contains('show');
if (isVisible) hideMobileSidebar();
else showMobileSidebar();
});
overlay.addEventListener('click', hideMobileSidebar);
document.addEventListener('keydown', (e) => {
if (e.key === 'Escape' && sidebar.classList.contains('show')) hideMobileSidebar();
});
}
}
function showMobileSidebar() {
const sidebar = document.getElementById('sidebar');
const overlay = document.getElementById('mobile-overlay');
const toggle = document.getElementById('mobile-toggle');
sidebar.classList.add('show');
overlay.classList.add('show');
toggle.classList.add('active');
toggle.innerHTML = '✕';
document.body.style.overflow = 'hidden';
}
function hideMobileSidebar() {
const sidebar = document.getElementById('sidebar');
const overlay = document.getElementById('mobile-overlay');
const toggle = document.getElementById('mobile-toggle');
sidebar.classList.remove('show');
overlay.classList.remove('show');
toggle.classList.remove('active');
toggle.innerHTML = '📊';
document.body.style.overflow = '';
}
function connectToServer() {
socket = io();
socket.on('connect', () => {
isConnected = true;
updateConnectionStatus(true);
});
socket.on('disconnect', () => {
isConnected = false;
updateConnectionStatus(false);
terminal.writeln('\r\n\x1b[31m✗ Connection lost. Reconnecting...\x1b[0m');
});
socket.on('connect_error', (error) => {
updateConnectionStatus(false);
terminal.writeln('\r\n\x1b[31m✗ Connection failed\x1b[0m');
});
socket.on('terminal_output', (data) => { terminal.write(data); });
socket.on('system_info', (data) => { updateSystemInfo(data); });
terminal.onData(data => { if (isConnected && socket) socket.emit('terminal_input', data); });
terminal.onResize(({ cols, rows }) => { if (isConnected && socket) socket.emit('terminal_resize', { cols, rows }); });
}
function updateConnectionStatus(connected) {
const dot = document.getElementById('connection-dot');
const text = document.getElementById('connection-text');
if (connected) {
dot.classList.add('connected');
text.textContent = 'Connected';
} else {
dot.classList.remove('connected');
text.textContent = 'Disconnected';
}
}
function updateSystemInfo(data) {
if (!data) return;
document.getElementById('hostname').textContent = data.hostname || '-';
document.getElementById('uptime').textContent = data.uptime || '-';
document.getElementById('users').textContent = data.user_info ? '1' : '-';
document.getElementById('cpu-usage').textContent = `${Math.round(data.cpu_usage || 0)}%`;
document.getElementById('cpu-bar').style.width = `${data.cpu_usage || 0}%`;
document.getElementById('memory-usage').textContent = `${Math.round(data.mem_usage || 0)}%`;
document.getElementById('memory-bar').style.width = `${data.mem_usage || 0}%`;
document.getElementById('disk-usage').textContent = `${Math.round(data.disk_usage || 0)}%`;
document.getElementById('disk-bar').style.width = `${data.disk_usage || 0}%`;
document.getElementById('memory-details').textContent = `${data.used_mem || 0} MB / ${data.total_mem || 0} MB`;
const diskUsed = typeof data.disk_used === 'number' ? `${data.disk_used} GB` : data.disk_used || '0';
const diskTotal = typeof data.disk_total === 'number' ? `${data.disk_total} GB` : data.disk_total || '0';
document.getElementById('disk-details').textContent = `${diskUsed} / ${diskTotal}`;
document.getElementById('network-rx').textContent = formatBytes(data.network_rx || 0) + '/s';
document.getElementById('network-tx').textContent = formatBytes(data.network_tx || 0) + '/s';
document.getElementById('platform').textContent = data.platform || '-';
document.getElementById('arch').textContent = data.arch || '-';
document.getElementById('cpu-model').textContent = truncateText(data.cpu_model || '-', 20);
document.getElementById('cpu-cores').textContent = data.cpu_cores || '-';
if (data.loadavg && Array.isArray(data.loadavg)) {
document.getElementById('load-avg').textContent = data.loadavg.map(l => l.toFixed(2)).join(', ');
} else {
document.getElementById('load-avg').textContent = '-';
}
if (data.cpu_usage !== undefined) {
cpuHistory.push(data.cpu_usage);
if (cpuHistory.length > 20) cpuHistory.shift();
updateCpuChart();
}
if (data.processes && Array.isArray(data.processes)) updateProcessList(data.processes);
}
function formatBytes(bytes) {
if (bytes === 0) return '0 B';
const k = 1024, sizes = ['B', 'KB', 'MB', 'GB'];
const i = Math.floor(Math.log(Math.abs(bytes)) / Math.log(k));
return parseFloat((bytes / Math.pow(k, i)).toFixed(1)) + ' ' + sizes[i];
}
function truncateText(text, maxLength) {
if (text.length <= maxLength) return text;
return text.substring(0, maxLength - 3) + '...';
}
function updateCpuChart() {
const chartContainer = document.getElementById('cpu-chart');
chartContainer.innerHTML = '';
cpuHistory.forEach(value => {
const bar = document.createElement('div');
bar.className = 'chart-bar';
bar.style.height = `${(value / 100) * 100}%`;
chartContainer.appendChild(bar);
});
}
function updateProcessList(processes) {
const container = document.getElementById('processes');
if (!processes || !Array.isArray(processes) || processes.length === 0) {
container.innerHTML = `
<div class="process-item">
<div class="process-name">No data</div>
<div class="process-cpu">0%</div>
</div>
`;
return;
}
container.innerHTML = processes.map(proc => `
<div class="process-item">
<div class="process-name">${proc.name || 'unknown'}</div>
<div class="process-cpu">${Math.round(proc.cpu || 0)}%</div>
</div>
`).join('');
}
function clearTerminal() { if (terminal) terminal.clear(); }
document.addEventListener('DOMContentLoaded', initializeTerminal);
// Responsive terminal resize
let resizeTimeout;
window.addEventListener('resize', () => {
clearTimeout(resizeTimeout);
resizeTimeout = setTimeout(() => {
if (terminal && terminal.element && terminal.element.offsetParent !== null) {
try {
const container = document.getElementById('terminal');
if (container) {
const containerRect = container.getBoundingClientRect();
if (containerRect.width > 0 && containerRect.height > 0) {
const cols = Math.floor(containerRect.width / 9);
const rows = Math.floor(containerRect.height / 17);
if (cols > 10 && rows > 5) {
terminal.resize(cols, rows);
if (isConnected && socket) socket.emit('terminal_resize', { cols, rows });
}
}
}
} catch (error) {
try { terminal.resize(80, 24); } catch {}
}
}
}, 150);
});
window.addEventListener('orientationchange', () => {
setTimeout(() => { window.dispatchEvent(new Event('resize')); }, 500);
});
</script>
</body>
</html>