Commit 6c19868e by Dedy Kurniawan

update

1 parent e4019636
......@@ -4,9 +4,9 @@ Dashboard manajemen PXE Server untuk lab komputer, dibangun dengan Node.js + Exp
## Fitur
- ✅ Login dengan username & password
- 📝 Editor teks untuk file `boot.uefi` (dengan backup otomatis)
- 📝 Editor teks untuk file `boot.ipxe` (dengan backup otomatis)
- 📤 Upload/replace file: `vmlinuz`, `initrd.img`, `filesystem.squashfs`
- ⚙️ Monitor & kontrol service: tftpd-hpa, nginx, isc-dhcp-server, nfs-kernel-server
- ⚙️ Monitor & kontrol service: dnsmaq, nginx, ipxe
- 🔄 Auto-refresh status service tiap 10 detik
## Struktur Direktori
......@@ -51,18 +51,15 @@ npm install
### 3. Konfigurasi sudo (untuk kontrol service)
Tambahkan ke `/etc/sudoers` (gunakan `visudo`):
```
www-data ALL=(ALL) NOPASSWD: /bin/systemctl start tftpd-hpa
www-data ALL=(ALL) NOPASSWD: /bin/systemctl stop tftpd-hpa
www-data ALL=(ALL) NOPASSWD: /bin/systemctl restart tftpd-hpa
www-data ALL=(ALL) NOPASSWD: /bin/systemctl start dnsmaq
www-data ALL=(ALL) NOPASSWD: /bin/systemctl stop dnsmaq
www-data ALL=(ALL) NOPASSWD: /bin/systemctl restart dnsmaq
www-data ALL=(ALL) NOPASSWD: /bin/systemctl start nginx
www-data ALL=(ALL) NOPASSWD: /bin/systemctl stop nginx
www-data ALL=(ALL) NOPASSWD: /bin/systemctl restart nginx
www-data ALL=(ALL) NOPASSWD: /bin/systemctl start isc-dhcp-server
www-data ALL=(ALL) NOPASSWD: /bin/systemctl stop isc-dhcp-server
www-data ALL=(ALL) NOPASSWD: /bin/systemctl restart isc-dhcp-server
www-data ALL=(ALL) NOPASSWD: /bin/systemctl start nfs-kernel-server
www-data ALL=(ALL) NOPASSWD: /bin/systemctl stop nfs-kernel-server
www-data ALL=(ALL) NOPASSWD: /bin/systemctl restart nfs-kernel-server
www-data ALL=(ALL) NOPASSWD: /bin/systemctl start ipxe
www-data ALL=(ALL) NOPASSWD: /bin/systemctl stop ipxe
www-data ALL=(ALL) NOPASSWD: /bin/systemctl restart ipxe
```
### 4. Pastikan direktori PXE ada
......@@ -96,7 +93,7 @@ Buka browser: **http://localhost:3000**
## Environment Variables
```bash
PORT=3000 # Port server (default: 3000)
PXE_ROOT=/var/www/html # Root direktori PXE (default: /var/www/html)
PXE_ROOT=/var/www/pxe-dashboard # Root direktori PXE (default: /var/www/pxe-dashboard)
```
## Menjalankan sebagai Service (systemd)
......@@ -113,7 +110,7 @@ WorkingDirectory=/path/to/pxe-dashboard
ExecStart=/usr/bin/node app.js
Restart=on-failure
Environment=PORT=3000
Environment=PXE_ROOT=/var/www/html
Environment=PXE_ROOT=/var/www/pxe-dashboard
[Install]
WantedBy=multi-user.target
......@@ -128,7 +125,7 @@ sudo systemctl start pxe-dashboard
## Jalur File yang Dikelola
| File | Path |
|------|------|
| boot.uefi | `/var/www/html/boot.uefi` |
| boot.uefi | `/var/www/html/boot.ipxe` |
| vmlinuz | `/var/www/html/debian12/live/vmlinuz` |
| initrd.img | `/var/www/html/debian12/live/initrd.img` |
| filesystem.squashfs | `/var/www/html/debian12/live/filesystem.squashfs` |
......@@ -39,12 +39,14 @@ const authRoutes = require('./routes/auth');
const dashboardRoutes = require('./routes/dashboard');
const fileRoutes = require('./routes/files');
const serviceRoutes = require('./routes/service');
const monitorRoutes = require('./routes/monitor');
app.use('/', authRoutes);
app.use('/dashboard', dashboardRoutes);
app.use('/files', fileRoutes);
app.use('/service', serviceRoutes);
app.use('/monitor', monitorRoutes);
app.listen(PORT, () => {
console.log(`PXE Dashboard berjalan di http://localhost:${PORT}`);
});
});
\ No newline at end of file
......@@ -1026,3 +1026,225 @@ body {
opacity: 0.3;
font-size: 10px;
}
/* ===== MONITOR PAGE ===== */
.mon-stat-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(180px, 1fr));
gap: 12px;
margin-bottom: 4px;
}
.mon-stat-card {
background: var(--card-bg);
border: 1px solid var(--border);
border-radius: 6px;
padding: 18px 20px;
}
.msc-label {
font-size: 9px;
letter-spacing: 2px;
color: var(--text3);
font-family: var(--font-mono);
margin-bottom: 8px;
}
.msc-value {
font-size: 26px;
font-weight: 700;
font-family: var(--font-mono);
color: #fff;
}
.msc-sep { color: var(--text3); margin: 0 4px; font-size: 18px; }
.accent-green { color: var(--accent3); }
.accent-blue { color: var(--accent); }
.accent-red { color: var(--danger); }
/* Bandwidth */
.bw-container {
background: var(--card-bg);
border: 1px solid var(--border);
border-radius: 6px;
padding: 20px;
}
.bw-iface-selector {
display: flex;
gap: 8px;
margin-bottom: 16px;
flex-wrap: wrap;
}
.bw-iface-btn {
padding: 5px 14px;
border: 1px solid var(--border2);
border-radius: 4px;
background: var(--bg3);
color: var(--text2);
font-family: var(--font-mono);
font-size: 12px;
cursor: pointer;
transition: all 0.2s;
}
.bw-iface-btn.active,
.bw-iface-btn:hover {
border-color: var(--accent);
color: var(--accent);
background: rgba(0,212,255,0.08);
}
.bw-stats-row {
display: flex;
gap: 32px;
margin-bottom: 16px;
flex-wrap: wrap;
}
.bw-stat { display: flex; flex-direction: column; gap: 4px; }
.bw-stat-label {
font-size: 9px;
letter-spacing: 2px;
color: var(--text3);
font-family: var(--font-mono);
}
.bw-stat-val {
font-size: 20px;
font-weight: 700;
font-family: var(--font-mono);
}
.chart-wrap {
position: relative;
height: 160px;
}
/* Two col layout */
.two-col {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 24px;
}
@media (max-width: 768px) { .two-col { grid-template-columns: 1fr; } }
/* Top lists */
.top-list {
background: var(--card-bg);
border: 1px solid var(--border);
border-radius: 6px;
overflow: hidden;
}
.top-item {
display: flex;
align-items: center;
gap: 12px;
padding: 10px 16px;
border-bottom: 1px solid var(--border);
transition: background 0.15s;
}
.top-item:last-child { border-bottom: none; }
.top-item:hover { background: rgba(255,255,255,0.02); }
.top-rank {
font-size: 10px;
color: var(--text3);
font-family: var(--font-mono);
width: 24px;
flex-shrink: 0;
}
.top-name {
flex: 1;
font-size: 12px;
color: var(--text);
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.top-count {
font-size: 11px;
font-family: var(--font-mono);
color: var(--accent);
flex-shrink: 0;
}
/* Log Table */
.log-filter-wrap { margin-left: auto; }
.log-filter-input {
background: var(--bg3);
border: 1px solid var(--border2);
border-radius: 4px;
padding: 4px 12px;
color: var(--text);
font-family: var(--font-mono);
font-size: 11px;
outline: none;
width: 200px;
transition: border-color 0.2s;
}
.log-filter-input:focus { border-color: var(--accent); }
.log-filter-input::placeholder { color: var(--text3); }
.section-title {
display: flex;
align-items: center;
gap: 10px;
}
.log-table-wrap {
background: var(--card-bg);
border: 1px solid var(--border);
border-radius: 6px;
overflow: auto;
max-height: 400px;
}
.log-table {
width: 100%;
border-collapse: collapse;
font-size: 12px;
}
.log-table thead {
position: sticky;
top: 0;
background: var(--bg2);
z-index: 1;
}
.log-table th {
padding: 10px 14px;
text-align: left;
font-size: 9px;
letter-spacing: 2px;
color: var(--text3);
font-family: var(--font-mono);
border-bottom: 1px solid var(--border);
font-weight: 400;
}
.log-table td {
padding: 8px 14px;
border-bottom: 1px solid rgba(30,45,61,0.5);
font-size: 11px;
color: var(--text2);
}
.log-row:hover td { background: rgba(255,255,255,0.02); }
.log-row.status-err td { color: rgba(255,61,90,0.7); }
.log-time { color: var(--text3) !important; font-size: 10px !important; }
.log-ip { color: var(--accent) !important; }
.log-file { color: var(--text) !important; max-width: 200px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.status-badge {
display: inline-block;
padding: 2px 8px;
border-radius: 3px;
font-size: 10px;
font-family: var(--font-mono);
font-weight: 700;
}
.status-badge.status-ok {
background: rgba(0,255,157,0.1);
color: var(--accent3);
border: 1px solid rgba(0,255,157,0.2);
}
.status-badge.status-err {
background: rgba(255,61,90,0.1);
color: var(--danger);
border: 1px solid rgba(255,61,90,0.2);
}
.empty-state {
text-align: center;
padding: 32px;
color: var(--text3);
font-family: var(--font-mono);
font-size: 12px;
}
const express = require('express');
const fs = require('fs');
const readline = require('readline');
const { exec } = require('child_process');
const { isAuthenticated } = require('../middleware/auth');
const router = express.Router();
const NGINX_LOG = process.env.NGINX_LOG || '/var/log/nginx/access.log';
// ─── Bandwidth dari /proc/net/dev ───────────────────────────────────────────
function getNetStats() {
try {
const raw = fs.readFileSync('/proc/net/dev', 'utf8');
const lines = raw.trim().split('\n').slice(2);
const result = {};
lines.forEach(line => {
const parts = line.trim().split(/\s+/);
const iface = parts[0].replace(':', '');
if (iface === 'lo') return;
result[iface] = {
rxBytes: parseInt(parts[1]),
txBytes: parseInt(parts[9]),
};
});
return result;
} catch(e) { return {}; }
}
// Cache untuk hitung delta bps
let prevStats = {};
let prevTime = Date.now();
function getBandwidth() {
const now = Date.now();
const current = getNetStats();
const elapsed = (now - prevTime) / 1000; // detik
const bandwidth = {};
Object.keys(current).forEach(iface => {
const prev = prevStats[iface];
if (prev && elapsed > 0) {
bandwidth[iface] = {
rxBps: Math.max(0, (current[iface].rxBytes - prev.rxBytes) / elapsed),
txBps: Math.max(0, (current[iface].txBytes - prev.txBytes) / elapsed),
rxBytes: current[iface].rxBytes,
txBytes: current[iface].txBytes,
};
} else {
bandwidth[iface] = { rxBps: 0, txBps: 0, ...current[iface] };
}
});
prevStats = current;
prevTime = now;
return bandwidth;
}
// ─── Parse nginx access log ──────────────────────────────────────────────────
// Format nginx default: IP - - [date] "METHOD /path HTTP/x.x" status bytes "ref" "ua"
function parseNginxLog(limit = 100) {
return new Promise((resolve) => {
const entries = [];
try {
if (!fs.existsSync(NGINX_LOG)) return resolve([]);
const content = fs.readFileSync(NGINX_LOG, 'utf8');
const lines = content.trim().split('\n').filter(Boolean);
// Ambil dari bawah (terbaru)
const recent = lines.slice(-limit).reverse();
recent.forEach(line => {
// Hanya tampilkan request ke /debian12/live/
if (!line.includes('/debian12/live/')) return;
// Parse format nginx combined log
const m = line.match(/^(\S+)\s+-\s+-\s+\[([^\]]+)\]\s+"(\S+)\s+(\S+)\s+\S+"\s+(\d+)\s+(\d+)/);
if (!m) return;
const [, ip, time, method, path, status, bytes] = m;
const filename = path.split('/').pop().split('?')[0] || path;
entries.push({
ip,
time,
method,
path,
filename,
status: parseInt(status),
bytes: parseInt(bytes),
});
});
} catch(e) { console.error('Gagal baca nginx log:', e.message); }
resolve(entries);
});
}
// ─── Routes ──────────────────────────────────────────────────────────────────
// Halaman monitoring
router.get('/', isAuthenticated, async (req, res) => {
const logs = await parseNginxLog(100);
const bandwidth = getBandwidth();
// Statistik ringkasan dari log
const stats = {
totalRequests: logs.length,
uniqueClients: new Set(logs.map(l => l.ip)).size,
totalBytes: logs.reduce((s, l) => s + (l.bytes || 0), 0),
success: logs.filter(l => l.status >= 200 && l.status < 300).length,
errors: logs.filter(l => l.status >= 400).length,
};
// Top file yang paling sering diakses
const fileCounts = {};
logs.forEach(l => {
fileCounts[l.filename] = (fileCounts[l.filename] || 0) + 1;
});
const topFiles = Object.entries(fileCounts)
.sort((a, b) => b[1] - a[1])
.slice(0, 5)
.map(([name, count]) => ({ name, count }));
// Top client IP
const ipCounts = {};
logs.forEach(l => { ipCounts[l.ip] = (ipCounts[l.ip] || 0) + 1; });
const topClients = Object.entries(ipCounts)
.sort((a, b) => b[1] - a[1])
.slice(0, 10)
.map(([ip, count]) => ({ ip, count }));
res.render('monitor', {
title: 'Traffic Monitor',
logs: logs.slice(0, 50),
bandwidth,
stats,
topFiles,
topClients,
nginxLog: NGINX_LOG,
});
});
// ─── API endpoint untuk polling realtime ─────────────────────────────────────
router.get('/api/bandwidth', isAuthenticated, (req, res) => {
res.json({ bandwidth: getBandwidth(), timestamp: Date.now() });
});
router.get('/api/logs', isAuthenticated, async (req, res) => {
const logs = await parseNginxLog(50);
const stats = {
totalRequests: logs.length,
uniqueClients: new Set(logs.map(l => l.ip)).size,
totalBytes: logs.reduce((s, l) => s + (l.bytes || 0), 0),
success: logs.filter(l => l.status >= 200 && l.status < 300).length,
errors: logs.filter(l => l.status >= 400).length,
};
res.json({ logs: logs.slice(0, 50), stats, timestamp: Date.now() });
});
module.exports = router;
\ No newline at end of file
......@@ -19,6 +19,9 @@
<li><a href="/service/status" class="nav-link <%= title === 'Status Service' ? 'active' : '' %>">
<span class="nav-icon">◈</span> Services
</a></li>
<li><a href="/monitor" class="nav-link <%= title === 'Traffic Monitor' ? 'active' : '' %>">
<span class="nav-icon">◈</span> Monitor
</a></li>
</ul>
<div class="nav-user">
<span class="user-badge"><%= user.username %></span>
......
Markdown is supported
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!