monitor.ejs 12.8 KB
<!DOCTYPE html>
<html lang="id">
<head><%- include('partials/head') %></head>
<body class="app-page">
  <%- include('partials/nav') %>

  <main class="main-content">
    <div class="page-header">
      <div>
        <h1 class="page-title">TRAFFIC MONITOR</h1>
        <p class="page-sub">Bandwidth & HTTP akses client PXE — <code class="path-inline"><%= nginxLog %></code></p>
      </div>
      <div class="header-time">
        <span class="status-dot active pulse"></span>
        <span>Live — update tiap 3 detik</span>
      </div>
    </div>

    <%- include('partials/flash') %>

    <!-- Stat Cards -->
    <section class="section">
      <h2 class="section-title">RINGKASAN (100 REQUEST TERAKHIR)</h2>
      <div class="mon-stat-grid">
        <div class="mon-stat-card">
          <div class="msc-label">TOTAL REQUEST</div>
          <div class="msc-value" id="stat-total"><%= stats.totalRequests || 0 %></div>
        </div>
        <div class="mon-stat-card">
          <div class="msc-label">CLIENT UNIK</div>
          <div class="msc-value accent-green" id="stat-clients"><%= stats.uniqueClients || 0 %></div>
        </div>
        <div class="mon-stat-card">
          <div class="msc-label">DATA TERKIRIM</div>
          <div class="msc-value accent-blue" id="stat-bytes">0 B</div>
        </div>
        <div class="mon-stat-card">
          <div class="msc-label">SUKSES / ERROR</div>
          <div class="msc-value">
            <span class="accent-green" id="stat-ok"><%= stats.success || 0 %></span>
            <span class="msc-sep">/</span>
            <span class="accent-red" id="stat-err"><%= stats.errors || 0 %></span>
          </div>
        </div>
      </div>
    </section>

    <!-- Bandwidth Chart -->
    <section class="section">
      <h2 class="section-title">BANDWIDTH REALTIME</h2>
      <div class="bw-container">
        <div class="bw-iface-selector" id="iface-selector">
          <% if (bandwidth && Object.keys(bandwidth).length > 0) { %>
            <% Object.keys(bandwidth).forEach(function(iface, i) { %>
            <button class="bw-iface-btn <%= i === 0 ? 'active' : '' %>" data-iface="<%= iface %>">
              <%= iface %>
            </button>
            <% }); %>
          <% } else { %>
            <span class="bw-no-iface">Mendeteksi interface...</span>
          <% } %>
        </div>
        <div class="bw-stats-row">
          <div class="bw-stat">
            <span class="bw-stat-label">↓ RX</span>
            <span class="bw-stat-val accent-green" id="bw-rx">0 bps</span>
          </div>
          <div class="bw-stat">
            <span class="bw-stat-label">↑ TX</span>
            <span class="bw-stat-val accent-blue" id="bw-tx">0 bps</span>
          </div>
          <div class="bw-stat">
            <span class="bw-stat-label">TOTAL RX</span>
            <span class="bw-stat-val" id="bw-total-rx"></span>
          </div>
          <div class="bw-stat">
            <span class="bw-stat-label">TOTAL TX</span>
            <span class="bw-stat-val" id="bw-total-tx"></span>
          </div>
        </div>
        <div class="chart-wrap">
          <canvas id="bw-chart"></canvas>
        </div>
      </div>
    </section>

    <!-- Top Files & Top Clients -->
    <div class="two-col">
      <section class="section">
        <h2 class="section-title">TOP FILE DIAKSES</h2>
        <div class="top-list" id="top-files">
          <% if (topFiles && topFiles.length > 0) { %>
            <% topFiles.forEach(function(f, i) { %>
            <div class="top-item">
              <span class="top-rank">#<%= i+1 %></span>
              <span class="top-name"><%= f.name || '(root)' %></span>
              <span class="top-count"><%= f.count %>x</span>
            </div>
            <% }); %>
          <% } else { %>
            <div class="empty-state">Belum ada data</div>
          <% } %>
        </div>
      </section>

      <section class="section">
        <h2 class="section-title">TOP CLIENT IP</h2>
        <div class="top-list" id="top-clients">
          <% if (topClients && topClients.length > 0) { %>
            <% topClients.forEach(function(c, i) { %>
            <div class="top-item">
              <span class="top-rank">#<%= i+1 %></span>
              <span class="top-name mono"><%= c.ip %></span>
              <span class="top-count"><%= c.count %>x</span>
            </div>
            <% }); %>
          <% } else { %>
            <div class="empty-state">Belum ada data</div>
          <% } %>
        </div>
      </section>
    </div>

    <!-- Live Log Table -->
    <section class="section">
      <h2 class="section-title">LOG HTTP LIVE
        <span class="log-filter-wrap">
          <input type="text" id="log-filter" class="log-filter-input" placeholder="Filter IP / file...">
        </span>
      </h2>
      <div class="log-table-wrap">
        <table class="log-table">
          <thead>
            <tr>
              <th>WAKTU</th>
              <th>CLIENT IP</th>
              <th>FILE</th>
              <th>STATUS</th>
              <th>UKURAN</th>
            </tr>
          </thead>
          <tbody id="log-body">
            <% if (logs && logs.length > 0) { %>
              <% logs.forEach(function(log) { %>
              <tr class="log-row status-<%= log.status >= 400 ? 'err' : 'ok' %>"
                  data-ip="<%= log.ip %>" data-file="<%= log.filename %>">
                <td class="mono log-time"><%= log.time %></td>
                <td class="mono log-ip"><%= log.ip %></td>
                <td class="mono log-file" title="<%= log.path %>"><%= log.filename %></td>
                <td><span class="status-badge status-<%= log.status >= 400 ? 'err' : 'ok' %>"><%= log.status %></span></td>
                <td class="mono" data-bytes="<%= log.bytes %>"></td>
              </tr>
              <% }); %>
            <% } else { %>
              <tr><td colspan="5" class="empty-state">Tidak ada request ke /debian12/live/ ditemukan di log</td></tr>
            <% } %>
          </tbody>
        </table>
      </div>
    </section>

  </main>

  <%- include('partials/footer') %>

  <script src="https://cdnjs.cloudflare.com/ajax/libs/Chart.js/4.4.1/chart.umd.min.js"></script>
  <script>
    // Helpers
    function fmtBytes(b) {
      b = parseInt(b) || 0;
      if (b === 0) return '0 B';
      const k = 1024, s = ['B','KB','MB','GB','TB'];
      const i = Math.floor(Math.log(b) / Math.log(k));
      return (b / Math.pow(k, i)).toFixed(2) + ' ' + s[i];
    }
    function fmtBps(b) {
      b = parseFloat(b) || 0;
      if (b === 0) return '0 bps';
      const k = 1000, s = ['bps','Kbps','Mbps','Gbps'];
      const i = Math.floor(Math.log(Math.max(b, 1)) / Math.log(k));
      return (b / Math.pow(k, i)).toFixed(1) + ' ' + s[i];
    }

    // Format ukuran di tabel saat load
    document.querySelectorAll('td[data-bytes]').forEach(td => {
      td.textContent = fmtBytes(td.dataset.bytes);
    });

    // Update stat bytes saat load
    document.getElementById('stat-bytes').textContent = fmtBytes(<%= stats.totalBytes || 0 %>);

    // Chart
    const MAX_POINTS = 30;
    const rxData = Array(MAX_POINTS).fill(0);
    const txData = Array(MAX_POINTS).fill(0);
    let activeIface = '';

    const ctx = document.getElementById('bw-chart').getContext('2d');
    const bwChart = new Chart(ctx, {
      type: 'line',
      data: {
        labels: Array(MAX_POINTS).fill(''),
        datasets: [
          {
            label: '↓ RX',
            data: rxData,
            borderColor: '#00ff9d',
            backgroundColor: 'rgba(0,255,157,0.08)',
            borderWidth: 2,
            pointRadius: 0,
            fill: true,
            tension: 0.4,
          },
          {
            label: '↑ TX',
            data: txData,
            borderColor: '#00d4ff',
            backgroundColor: 'rgba(0,212,255,0.08)',
            borderWidth: 2,
            pointRadius: 0,
            fill: true,
            tension: 0.4,
          }
        ]
      },
      options: {
        animation: false,
        responsive: true,
        maintainAspectRatio: false,
        scales: {
          x: { display: false },
          y: {
            beginAtZero: true,
            grid: { color: 'rgba(255,255,255,0.05)' },
            ticks: {
              color: '#4a6478',
              font: { family: 'Share Tech Mono', size: 10 },
              callback: v => fmtBps(v)
            }
          }
        },
        plugins: {
          legend: {
            labels: { color: '#7a94aa', font: { family: 'Share Tech Mono', size: 11 } }
          },
          tooltip: {
            callbacks: { label: c => fmtBps(c.raw) }
          }
        }
      }
    });

    // Poll bandwidth
    async function pollBandwidth() {
      try {
        const r = await fetch('/monitor/api/bandwidth');
        if (!r.ok) return;
        const d = await r.json();
        const ifaces = Object.keys(d.bandwidth || {});
        if (ifaces.length === 0) return;

        // Auto-set activeIface pertama kali
        if (!activeIface || !d.bandwidth[activeIface]) {
          activeIface = ifaces[0];
          // Update selector buttons
          const sel = document.getElementById('iface-selector');
          sel.innerHTML = ifaces.map((iface, i) =>
            `<button class="bw-iface-btn ${i === 0 ? 'active' : ''}" data-iface="${iface}">${iface}</button>`
          ).join('');
          sel.querySelectorAll('.bw-iface-btn').forEach(btn => {
            btn.addEventListener('click', () => {
              sel.querySelectorAll('.bw-iface-btn').forEach(b => b.classList.remove('active'));
              btn.classList.add('active');
              activeIface = btn.dataset.iface;
              rxData.fill(0); txData.fill(0);
              bwChart.update();
            });
          });
        }

        const iface = d.bandwidth[activeIface];
        if (!iface) return;

        rxData.push(iface.rxBps || 0); rxData.shift();
        txData.push(iface.txBps || 0); txData.shift();
        bwChart.update();

        document.getElementById('bw-rx').textContent = fmtBps(iface.rxBps);
        document.getElementById('bw-tx').textContent = fmtBps(iface.txBps);
        document.getElementById('bw-total-rx').textContent = fmtBytes(iface.rxBytes);
        document.getElementById('bw-total-tx').textContent = fmtBytes(iface.txBytes);
      } catch(e) { console.error('Bandwidth poll error:', e); }
    }

    // Poll logs
    async function pollLogs() {
      try {
        const r = await fetch('/monitor/api/logs');
        if (!r.ok) return;
        const d = await r.json();

        document.getElementById('stat-total').textContent = d.stats.totalRequests || 0;
        document.getElementById('stat-clients').textContent = d.stats.uniqueClients || 0;
        document.getElementById('stat-bytes').textContent = fmtBytes(d.stats.totalBytes || 0);
        document.getElementById('stat-ok').textContent = d.stats.success || 0;
        document.getElementById('stat-err').textContent = d.stats.errors || 0;

        const filter = document.getElementById('log-filter').value.toLowerCase();
        const tbody = document.getElementById('log-body');
        tbody.innerHTML = '';

        const logs = d.logs || [];
        if (logs.length === 0) {
          tbody.innerHTML = '<tr><td colspan="5" class="empty-state">Tidak ada request ke /debian12/live/ di log</td></tr>';
          return;
        }

        logs.forEach(log => {
          if (filter && !log.ip.includes(filter) && !(log.filename || '').toLowerCase().includes(filter)) return;
          const isErr = log.status >= 400;
          const tr = document.createElement('tr');
          tr.className = `log-row status-${isErr ? 'err' : 'ok'}`;
          tr.innerHTML = `
            <td class="mono log-time">${log.time || '—'}</td>
            <td class="mono log-ip">${log.ip || '—'}</td>
            <td class="mono log-file" title="${log.path || ''}">${log.filename || '—'}</td>
            <td><span class="status-badge status-${isErr ? 'err' : 'ok'}">${log.status}</span></td>
            <td class="mono">${fmtBytes(log.bytes)}</td>
          `;
          tbody.appendChild(tr);
        });
      } catch(e) { console.error('Log poll error:', e); }
    }

    // Filter
    document.getElementById('log-filter').addEventListener('input', pollLogs);

    // Clock
    setInterval(() => {
      document.getElementById('footer-clock').textContent = new Date().toLocaleString('id-ID');
    }, 1000);
    document.getElementById('footer-clock').textContent = new Date().toLocaleString('id-ID');

    // Start
    pollBandwidth();
    pollLogs();
    setInterval(pollBandwidth, 3000);
    setInterval(pollLogs, 5000);
  </script>
</body>
</html>