Files
open-isp/views/admin/routers.ejs
T

993 lines
45 KiB
Plaintext

<!DOCTYPE html>
<html lang="<%= lang || 'id' %>">
<head>
<meta charset="UTF-8"><meta name="viewport" content="width=device-width,initial-scale=1">
<title><%= t('admin.nav.router_management', 'Manajemen Router') %> | <%= company %></title>
<link rel="stylesheet" href="/vendor/bootstrap-icons/bootstrap-icons.min.css">
<link rel="stylesheet" href="/css/admin.css">
<style>
.mtabs { display: flex; gap: 10px; margin-bottom: 18px; border-bottom: 1px solid var(--border); padding-bottom: 12px; }
.mtab { padding: 9px 16px; border-radius: 10px; cursor: pointer; color: var(--muted); transition: all 0.2s; font-weight: 700; text-decoration: none; display: flex; align-items: center; gap: 8px; }
.mtab:hover { background: rgba(255,255,255,0.06); color: var(--text); }
.mtab.active { background: linear-gradient(135deg, #6366f1 0%, #3b82f6 100%); color: white; box-shadow: 0 4px 14px rgba(99, 102, 241, 0.4); }
/* Router Grid Cards */
.router-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(360px, 1fr));
gap: 20px;
margin-bottom: 24px;
}
.rc-card {
background: linear-gradient(135deg, rgba(30, 41, 59, 0.85) 0%, rgba(15, 23, 42, 0.95) 100%);
border: 1px solid rgba(99, 102, 241, 0.22);
border-radius: 18px;
padding: 22px;
position: relative;
overflow: hidden;
box-shadow: 0 12px 32px rgba(0, 0, 0, 0.35);
transition: all 0.3s ease;
display: flex;
flex-direction: column;
justify-content: space-between;
}
.rc-card:hover {
transform: translateY(-4px);
border-color: rgba(99, 102, 241, 0.55);
box-shadow: 0 18px 45px rgba(99, 102, 241, 0.25);
}
.rc-header {
display: flex;
justify-content: space-between;
align-items: flex-start;
margin-bottom: 16px;
}
.rc-title-box {
display: flex;
align-items: center;
gap: 12px;
}
.rc-icon {
width: 48px;
height: 48px;
border-radius: 14px;
background: linear-gradient(135deg, rgba(99, 102, 241, 0.25) 0%, rgba(59, 130, 246, 0.15) 100%);
color: #818cf8;
display: flex;
align-items: center;
justify-content: center;
font-size: 24px;
border: 1px solid rgba(99, 102, 241, 0.35);
box-shadow: 0 4px 12px rgba(0,0,0,0.2);
}
.rc-name {
font-size: 18px;
font-weight: 800;
color: #f8fafc;
margin: 0;
line-height: 1.2;
}
.rc-host {
font-size: 12px;
color: #94a3b8;
font-family: ui-monospace, monospace;
margin-top: 3px;
}
.rc-status-pill {
padding: 5px 12px;
border-radius: 20px;
font-size: 11px;
font-weight: 800;
display: inline-flex;
align-items: center;
gap: 6px;
letter-spacing: 0.3px;
}
.rc-status-pill.online {
background: rgba(34, 197, 94, 0.18);
color: #4ade80;
border: 1px solid rgba(34, 197, 94, 0.4);
}
.rc-status-pill.offline {
background: rgba(239, 68, 68, 0.18);
color: #f87171;
border: 1px solid rgba(239, 68, 68, 0.4);
}
.rc-status-dot {
width: 8px;
height: 8px;
border-radius: 50%;
display: inline-block;
}
.rc-status-pill.online .rc-status-dot {
background: #4ade80;
box-shadow: 0 0 10px #4ade80;
}
.rc-status-pill.offline .rc-status-dot {
background: #f87171;
}
/* Meter Bars */
.rc-metrics {
display: flex;
flex-direction: column;
gap: 12px;
background: rgba(15, 23, 42, 0.6);
border-radius: 14px;
padding: 14px;
border: 1px solid rgba(255, 255, 255, 0.08);
margin-bottom: 16px;
}
.rc-metric-item {
display: flex;
flex-direction: column;
gap: 5px;
}
.rc-metric-head {
display: flex;
justify-content: space-between;
font-size: 11px;
color: #cbd5e1;
font-weight: 700;
}
.rc-progress {
height: 7px;
background: rgba(255, 255, 255, 0.1);
border-radius: 10px;
overflow: hidden;
}
.rc-progress-bar {
height: 100%;
border-radius: 10px;
transition: width 0.6s ease;
}
.bg-green { background: linear-gradient(90deg, #10b981, #34d399); }
.bg-yellow { background: linear-gradient(90deg, #f59e0b, #fbbf24); }
.bg-red { background: linear-gradient(90deg, #ef4444, #f87171); }
/* Specs Badges */
.rc-specs {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 10px;
margin-bottom: 18px;
text-align: center;
}
.rc-spec-box {
background: rgba(30, 41, 59, 0.6);
border: 1px solid rgba(255, 255, 255, 0.08);
border-radius: 12px;
padding: 10px 4px;
}
.rc-spec-val {
font-size: 16px;
font-weight: 800;
color: #f1f5f9;
}
.rc-spec-lbl {
font-size: 10px;
color: #94a3b8;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.5px;
margin-top: 2px;
}
.rc-actions {
display: flex;
gap: 8px;
flex-wrap: wrap;
margin-top: auto;
padding-top: 14px;
border-top: 1px solid rgba(255, 255, 255, 0.08);
}
.btn-detail-live {
background: linear-gradient(135deg, #6366f1 0%, #3b82f6 100%);
color: white !important;
border: none;
border-radius: 10px;
padding: 8px 14px;
font-weight: 700;
font-size: 13px;
box-shadow: 0 4px 14px rgba(99, 102, 241, 0.35);
transition: all 0.25s ease;
display: inline-flex;
align-items: center;
justify-content: center;
}
.btn-detail-live:hover {
transform: translateY(-2px);
box-shadow: 0 6px 20px rgba(99, 102, 241, 0.5);
filter: brightness(1.1);
}
.view-toggle-btn {
padding: 6px 14px;
border-radius: 8px;
background: rgba(255, 255, 255, 0.08);
color: var(--text);
border: 1px solid var(--border);
cursor: pointer;
font-size: 12px;
font-weight: 700;
}
.view-toggle-btn.active {
background: var(--primary);
color: white;
border-color: var(--primary);
}
/* Modal Styling Improvements */
.modal-tab-btn {
padding: 8px 14px;
border-radius: 8px;
border: 1px solid rgba(255, 255, 255, 0.1);
background: rgba(15, 23, 42, 0.6);
color: #94a3b8;
font-size: 12px;
font-weight: 700;
cursor: pointer;
transition: all 0.2s;
}
.modal-tab-btn:hover {
background: rgba(30, 41, 59, 0.8);
color: #f1f5f9;
}
.modal-tab-btn.active {
background: linear-gradient(135deg, #6366f1 0%, #3b82f6 100%);
color: white;
border-color: #6366f1;
box-shadow: 0 4px 12px rgba(99, 102, 241, 0.3);
}
/* ── Detail Router modal (gaya MikroDash, tema admin) ── */
.mono { font-family: 'JetBrains Mono', ui-monospace, monospace; }
.mt-head { display: flex; justify-content: space-between; align-items: center; gap: 12px; flex-wrap: wrap; padding: 14px 16px; border: 1px solid var(--border); border-radius: 14px; background: var(--bg3); margin-bottom: 14px; }
.mt-head-name { font-size: 16px; font-weight: 800; display: flex; align-items: center; gap: 8px; color: var(--text); }
.mt-head-name i { color: var(--primary); }
.mt-head-name span { overflow-wrap: anywhere; }
.mt-head-sub { font-size: 11.5px; color: var(--muted); margin-top: 3px; }
.mt-tabs { display: flex; gap: 8px; flex-wrap: wrap; margin-bottom: 14px; }
.mt-tab { padding: 8px 14px; border-radius: 10px; border: 1px solid var(--border); background: transparent; color: var(--muted); font-size: 12.5px; font-weight: 700; cursor: pointer; display: inline-flex; align-items: center; gap: 7px; transition: .18s; font-family: inherit; }
.mt-tab:hover { color: var(--text); background: rgba(255,255,255,.05); }
.mt-tab.active { color: #0A0D14; background: var(--primary); border-color: var(--primary); }
.mt-tab-badge { background: rgba(0,0,0,.22); padding: 0 7px; border-radius: 999px; font-size: 11px; font-weight: 800; }
.mt-tab.active .mt-tab-badge { background: rgba(0,0,0,.18); }
.mt-metrics { display: grid; grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); gap: 10px; margin-bottom: 14px; }
.mt-metric { background: var(--bg3); border: 1px solid var(--border); border-radius: 14px; padding: 14px; text-align: center; }
.mt-metric .lbl { font-size: 10px; font-weight: 800; letter-spacing: .7px; text-transform: uppercase; color: var(--muted); }
.mt-metric .val { font-size: 28px; font-weight: 800; line-height: 1.15; font-family: 'JetBrains Mono', ui-monospace, monospace; margin-top: 4px; color: var(--text); }
.mt-metric .val .u { font-size: 14px; opacity: .65; margin-left: 1px; }
.mt-metric .sub { font-size: 10.5px; color: var(--muted); margin-top: 2px; font-family: 'JetBrains Mono', monospace; }
.mt-metric .val.ok { color: var(--success); } .mt-metric .val.warn { color: var(--warning); } .mt-metric .val.crit { color: var(--danger); }
.mt-bar { height: 6px; border-radius: 999px; background: rgba(148,163,184,.2); overflow: hidden; margin-top: 9px; }
.mt-bar > span { display: block; height: 100%; border-radius: 999px; background: var(--primary); transition: width .4s ease; }
.mt-bar > span.ok { background: var(--success); } .mt-bar > span.warn { background: var(--warning); } .mt-bar > span.crit { background: var(--danger); }
.mt-kv { border: 1px solid var(--border); border-radius: 14px; overflow: hidden; margin-bottom: 14px; }
.mt-kv .row { display: flex; justify-content: space-between; gap: 14px; padding: 10px 14px; border-bottom: 1px solid var(--border); font-size: 13px; }
.mt-kv .row:last-child { border-bottom: none; }
.mt-kv .row:nth-child(odd) { background: rgba(255,255,255,.015); }
.mt-kv .row > span { color: var(--muted); white-space: nowrap; }
.mt-kv .row > b { font-weight: 700; color: var(--text); text-align: right; overflow-wrap: anywhere; }
.mt-kv .row > b.mt-ok { color: var(--success); }
.mt-stats { display: grid; grid-template-columns: repeat(3, 1fr); gap: 10px; }
.mt-stat { background: var(--bg3); border: 1px solid var(--border); border-radius: 12px; padding: 12px; text-align: center; min-width: 0; }
.mt-stat > span { display: block; font-size: 10.5px; color: var(--muted); text-transform: uppercase; letter-spacing: .5px; }
.mt-stat > b { font-size: 20px; font-weight: 800; font-family: 'JetBrains Mono', monospace; }
.mt-stat > b.mt-ok { color: var(--success); } .mt-stat > b.mt-warn { color: var(--warning); }
.mt-tablewrap { border: 1px solid var(--border); border-radius: 14px; overflow: auto; max-height: 300px; }
.mt-tbl { width: 100%; border-collapse: collapse; font-size: 12.5px; }
.mt-tbl thead th { position: sticky; top: 0; z-index: 1; background: var(--bg3); text-align: left; padding: 9px 12px; font-size: 10px; text-transform: uppercase; letter-spacing: .5px; color: var(--muted); white-space: nowrap; }
.mt-tbl td { padding: 9px 12px; border-bottom: 1px solid var(--border); white-space: nowrap; }
.mt-tbl tbody tr:last-child td { border-bottom: none; }
.mt-tbl tbody tr:hover td { background: rgba(255,255,255,.02); }
.mt-tbl .mono { font-family: 'JetBrains Mono', monospace; color: var(--muted); }
.mt-tbl .mt-idx { color: var(--muted); }
.mt-tbl .mt-strong { font-weight: 700; color: var(--text); }
.mt-tbl .mt-warn { color: var(--warning); }
.mt-empty { text-align: center; color: var(--muted); padding: 22px; white-space: normal; }
</style>
</head>
<body>
<%- include('partials/sidebar', { activePage: 'mikrotik', company }) %>
<div class="mw">
<div class="topbar">
<div class="tb-title">
<button class="hb-menu" onclick="toggleSidebar()"><i class="bi bi-list"></i></button>
<i class="bi bi-hdd-network"></i> <%= t('admin.nav.router_management', 'Manajemen Router') %>
</div>
<div class="tb-right">
<span class="adm-badge"><i class="bi bi-person-fill"></i> <%= (typeof session !== 'undefined' && session && session.isCashier) ? t('admin.role.cashier', 'Kasir') : t('admin.role.admin', 'Admin') %></span>
<a href="/admin/logout" class="btn btn-g btn-sm"><i class="bi bi-box-arrow-left"></i></a>
</div>
</div>
<div class="page">
<% const mTab = 'routers'; %>
<% if (msg) { %>
<div class="alert alert-<%= msg.type === 'success' ? 's' : 'd' %>" data-flash="1">
<i class="bi bi-<%= msg.type === 'success' ? 'check-circle' : 'exclamation-circle' %>"></i> <%= msg.text %>
</div>
<% } %>
<div class="mtabs">
<a href="/admin/mikrotik" class="mtab <%= mTab==='monitoring' ? 'active' : '' %>"><i class="bi bi-router"></i> Monitoring</a>
<a href="/admin/vouchers" class="mtab <%= mTab==='vouchers' ? 'active' : '' %>"><i class="bi bi-ticket-perforated"></i> Voucher Hotspot</a>
<a href="/admin/routers" class="mtab <%= mTab==='routers' ? 'active' : '' %>"><i class="bi bi-hdd-network"></i> Router</a>
</div>
<!-- Header Actions & View Mode -->
<div class="card mb-3" style="border:none; background:transparent;">
<div style="display:flex; justify-content:space-between; align-items:center; flex-wrap:wrap; gap:12px;">
<div style="display:flex; align-items:center; gap:8px;">
<h5 class="fw-bold text-white mb-0"><i class="bi bi-cpu text-primary me-2"></i><%= t('admin.rt.list','Daftar Router MikroTik') %> (<%= routers.length %>)</h5>
</div>
<div style="display:flex; gap:8px; align-items:center; flex-wrap:wrap;">
<div class="d-flex gap-1 bg-dark p-1 rounded-3 border border-secondary border-opacity-25">
<button type="button" class="view-toggle-btn active" id="btn-view-grid" onclick="switchView('grid')"><i class="bi bi-grid-fill me-1"></i> <%= t('admin.rt.view_grid','Grid Kartu') %></button>
<button type="button" class="view-toggle-btn" id="btn-view-table" onclick="switchView('table')"><i class="bi bi-table me-1"></i> Tabel</button>
</div>
<button type="button" class="btn btn-outline-light btn-sm" onclick="openIsolirScriptModal()"><i class="bi bi-filetype-txt me-1"></i> <%= t('admin.rt.script_isolir','Script Isolir') %></button>
<button class="btn btn-p btn-sm" onclick="openModal('addModal')"><i class="bi bi-plus-lg me-1"></i> <%= t('admin.rt.add','Tambah Router') %></button>
</div>
</div>
</div>
<!-- View 1: Modern Router Grid Cards -->
<div id="view-grid-container" class="router-grid">
<% if (routers.length === 0) { %>
<div class="card p-4 text-center text-muted" style="grid-column: 1 / -1;">
<i class="bi bi-hdd-network fs-1 mb-2"></i>
<h6><%= t('admin.routers.empty', 'Belum ada router yang ditambahkan') %></h6>
<button class="btn btn-p btn-sm mt-2" onclick="openModal('addModal')"><i class="bi bi-plus-lg me-1"></i> <%= t('admin.rt.add_now','Tambah Router Sekarang') %></button>
</div>
<% } %>
<% routers.forEach((r, i) => { %>
<div class="rc-card" id="router-card-<%= r.id %>">
<div>
<div class="rc-header">
<div class="rc-title-box">
<div class="rc-icon"><i class="bi bi-router-fill"></i></div>
<div>
<h6 class="rc-name"><%= r.name %></h6>
<div class="rc-host"><%= r.host %>:<%= r.port %></div>
</div>
</div>
<div>
<% if (r.is_active) { %>
<span class="rc-status-pill online" id="status-pill-<%= r.id %>"><span class="rc-status-dot"></span> <%= t('common.active','Aktif') %></span>
<% } else { %>
<span class="rc-status-pill offline" id="status-pill-<%= r.id %>"><span class="rc-status-dot"></span> <%= t('common.inactive','Nonaktif') %></span>
<% } %>
</div>
</div>
<!-- Live Metrics (Loaded via JS) -->
<div class="rc-metrics" id="metrics-box-<%= r.id %>">
<div class="rc-metric-item">
<div class="rc-metric-head">
<span><i class="bi bi-speedometer2 me-1"></i> CPU Load</span>
<span id="cpu-load-lbl-<%= r.id %>">--%</span>
</div>
<div class="rc-progress">
<div class="rc-progress-bar bg-green" id="cpu-load-bar-<%= r.id %>" style="width: 0%;"></div>
</div>
</div>
<div class="rc-metric-item">
<div class="rc-metric-head">
<span><i class="bi bi-memory me-1"></i> RAM Usage</span>
<span id="ram-usage-lbl-<%= r.id %>">-- MB</span>
</div>
<div class="rc-progress">
<div class="rc-progress-bar bg-yellow" id="ram-usage-bar-<%= r.id %>" style="width: 0%;"></div>
</div>
</div>
</div>
<!-- System Specs & Active Stats -->
<div class="rc-specs">
<div class="rc-spec-box">
<div class="rc-spec-val text-info" id="spec-cust-<%= r.id %>">-</div>
<div class="rc-spec-lbl"><%= t('common.customer','Pelanggan') %></div>
</div>
<div class="rc-spec-box">
<div class="rc-spec-val text-success" id="spec-pppoe-<%= r.id %>">-</div>
<div class="rc-spec-lbl"><%= t('admin.rt.pppoe_active','PPPoE Aktif') %></div>
</div>
<div class="rc-spec-box">
<div class="rc-spec-val text-warning" id="spec-hs-<%= r.id %>">-</div>
<div class="rc-spec-lbl">Hotspot</div>
</div>
</div>
</div>
<div class="rc-actions">
<button type="button" class="btn-detail-live flex-fill" title="<%= t('admin.rt.live_detail','Detail Live Status') %>" onclick="openLiveDetailModal(<%= r.id %>)">
<i class="bi bi-cpu-fill me-1"></i> Detail Live
</button>
<button type="button" class="btn btn-g btn-sm" title="<%= t('admin.routers.actions.test', 'Test Koneksi') %>" onclick="testConn(<%= r.id %>)">
<i class="bi bi-plug"></i>
</button>
<button type="button" class="btn btn-g btn-sm" title="<%= t('admin.rt.setup_fw','Setup Firewall Isolir') %>" onclick="setupFirewall(<%= r.id %>)">
<i class="bi bi-shield-lock"></i>
</button>
<button type="button" class="btn btn-g btn-sm" title="<%= t('common.edit', 'Edit') %>" onclick="editRouter(<%= i %>)">
<i class="bi bi-pencil"></i>
</button>
<form method="POST" action="/admin/routers/<%= r.id %>/delete" style="display:inline" onsubmit="return confirm('<%= t('admin.routers.confirm.delete', 'Hapus router ini?') %>')">
<button class="btn btn-d btn-sm" title="<%= t('common.delete', 'Hapus') %>"><i class="bi bi-trash"></i></button>
</form>
</div>
</div>
<% }) %>
</div>
<!-- View 2: Detailed Table View (Hidden by Default) -->
<div id="view-table-container" class="card" style="display: none;">
<div class="tbl-wrap">
<table class="dtbl">
<thead>
<tr>
<th>#</th>
<th><%= t('admin.routers.table.name', 'Nama Router') %></th>
<th><%= t('admin.routers.table.host', 'Host / IP') %></th>
<th><%= t('admin.routers.table.port', 'Port') %></th>
<th><%= t('admin.routers.table.username', 'Username') %></th>
<th><%= t('admin.routers.table.status', 'Status') %></th>
<th><%= t('common.actions', 'Aksi') %></th>
</tr>
</thead>
<tbody>
<% if (routers.length === 0) { %>
<tr><td colspan="7" class="text-center text-muted" style="padding:32px"><%= t('admin.routers.empty', 'Belum ada router yang ditambahkan') %></td></tr>
<% } %>
<% routers.forEach((r, i) => { %>
<tr>
<td class="text-muted"><%= i+1 %></td>
<td class="fw6"><%= r.name %></td>
<td><%= r.host %></td>
<td><%= r.port %></td>
<td><%= r.user %></td>
<td>
<% if (r.is_active) { %><span class="badge bs"><%= t('common.active', 'Aktif') %></span><% } else { %><span class="badge bm"><%= t('common.inactive', 'Nonaktif') %></span><% } %>
</td>
<td style="white-space:nowrap">
<form id="rdel-<%= r.id %>" method="POST" action="/admin/routers/<%= r.id %>/delete" style="display:none" onsubmit="return confirm('<%= t('admin.routers.confirm.delete', 'Hapus router ini?') %>')"></form>
<div class="act">
<button type="button" class="act-btn" title="Aksi" onclick="actToggle(event, this)"><i class="bi bi-three-dots-vertical"></i></button>
<div class="act-menu">
<button type="button" class="act-item primary" onclick="openLiveDetailModal(<%= r.id %>)"><i class="bi bi-cpu"></i> <%= t('admin.rt.live_detail','Detail Live Status') %></button>
<button type="button" class="act-item" onclick="testConn(<%= r.id %>)"><i class="bi bi-plug"></i> <%= t('admin.dash.test_conn','Test Koneksi') %></button>
<button type="button" class="act-item" onclick="setupFirewall(<%= r.id %>)"><i class="bi bi-shield-lock"></i> <%= t('admin.rt.setup_fw','Setup Firewall Isolir') %></button>
<div class="act-sep"></div>
<button type="button" class="act-item" onclick="editRouter(<%= i %>)"><i class="bi bi-pencil"></i> <%= t('admin.rt.edit','Edit Router') %></button>
<button type="submit" form="rdel-<%= r.id %>" class="act-item danger"><i class="bi bi-trash"></i> <%= t('admin.rt.del','Hapus Router') %></button>
</div>
</div>
</td>
</tr>
<% }) %>
</tbody>
</table>
</div>
</div>
</div>
</div>
<!-- Modal Live Detail Router -->
<div class="mo" id="routerLiveModal">
<div class="mb" style="max-width: 740px;">
<div class="mh">
<span class="mt"><i class="bi bi-cpu-fill text-primary me-2"></i> <span id="modal-router-name"><%= t('admin.rt.live_title','Detail Live Router') %></span></span>
<button class="mc" onclick="closeModal('routerLiveModal')">&times;</button>
</div>
<div class="mbody" id="modal-router-body" style="min-height:360px;">
<div class="text-center py-5 text-muted">
<i class="bi bi-arrow-repeat spin fs-2 mb-2 d-block text-primary"></i>
<span>Memuat metrik sistem & data live router...</span>
</div>
</div>
<div class="mf">
<button type="button" class="btn btn-g" onclick="closeModal('routerLiveModal')"><%= t('common.close','Tutup') %></button>
<button type="button" id="modal-btn-refresh" class="btn btn-p" onclick="refreshCurrentModal()"><i class="bi bi-arrow-clockwise me-1"></i> Refresh Data</button>
</div>
</div>
</div>
<!-- Modal Script & <%= t('admin.rt.setup_fw','Setup Firewall Isolir') %> Portal -->
<div class="mo" id="isolirScriptModal">
<div class="mb" style="max-width:740px">
<div class="mh">
<span class="mt"><i class="bi bi-shield-lock text-warning me-2"></i> Setup Firewall & Halaman Isolir Pelanggan</span>
<button class="mc" onclick="closeModal('isolirScriptModal')">&times;</button>
</div>
<div class="mbody">
<!-- Tabs Header -->
<div class="d-flex gap-2 mb-3 border-bottom pb-2">
<button type="button" class="btn btn-sm btn-p" id="tab-btn-auto" onclick="switchIsolirTab('auto')">
<i class="bi bi-lightning-charge-fill me-1"></i> 1-Click Auto Setup (API)
</button>
<button type="button" class="btn btn-sm btn-g" id="tab-btn-script" onclick="switchIsolirTab('script')">
<i class="bi bi-filetype-txt me-1"></i> Copy Script RouterOS (.rsc)
</button>
</div>
<!-- TAB 1: Auto Setup via API -->
<div id="isolir-tab-auto">
<div class="alert alert-info p-3 small mb-3" style="background:rgba(99,102,241,0.12); border:1px solid rgba(99,102,241,0.3); color:#e0e7ff;">
<h6 class="fw-bold mb-1"><i class="bi bi-info-circle me-1 text-info"></i> Otomatisasi Redirect Halaman Isolir</h6>
<p class="mb-0 small" style="opacity:0.9;">
Sistem akan mengirimkan perintah API ke MikroTik untuk membuat <strong>Rule NAT Redirect (Port 80/443)</strong>, <strong>Firewall Rule (LIST_ISOLIR)</strong>, serta memasang <strong>Hook Script on-up di Profil PPPoE Isolir</strong> secara otomatis.
</p>
</div>
<div class="form-group mb-3">
<label class="fl fw-bold text-white mb-2">Pilih Router MikroTik Target *</label>
<select class="fs w-100" id="isolir-modal-router-select">
<% routers.forEach(r => { %>
<option value="<%= r.id %>"><%= r.name %> (<%= r.host %>:<%= r.port %>)</option>
<% }) %>
</select>
</div>
<button type="button" class="btn btn-primary fw-bold w-100 py-2 shadow-sm" onclick="runAutoSetupFirewallFromModal()">
<i class="bi bi-play-circle-fill me-1"></i> Jalankan Setup Firewall & Hook Isolir Sekarang
</button>
<div id="isolir-auto-setup-result" style="display:none;"></div>
</div>
<!-- TAB 2: RouterOS Manual Script (.rsc) -->
<div id="isolir-tab-script" style="display:none;">
<p class="small text-muted mb-2">Script ini di-generate otomatis berdasarkan IP server & URL billing (app_url). Anda dapat menyalin dan menempelkannya di Winbox Terminal MikroTik.</p>
<div id="isolir-script-meta" class="small text-muted mb-2 fw-bold" style="color:#a5b4fc !important;"></div>
<textarea id="isolir-script-body" class="fc" readonly rows="14" style="width:100%;font-family:ui-monospace,monospace;font-size:12px;background:rgba(15,23,42,0.9);color:#818cf8;border-radius:8px;"></textarea>
</div>
</div>
<div class="mf">
<button type="button" class="btn btn-g" onclick="closeModal('isolirScriptModal')"><%= t('common.close', 'Tutup') %></button>
<button type="button" class="btn btn-p" id="btn-copy-script-modal" style="display:none;" onclick="copyIsolirScript()"><i class="bi bi-clipboard me-1"></i> <%= t('admin.routers.isolir_script_copy', 'Salin script') %></button>
</div>
</div>
</div>
<!-- Modal Tambah -->
<div class="mo" id="addModal">
<div class="mb">
<div class="mh"><span class="mt"><i class="bi bi-hdd-network"></i> <%= t('admin.routers.add', 'Tambah Router') %></span><button class="mc" onclick="closeModal('addModal')">&times;</button></div>
<form method="POST" action="/admin/routers">
<div class="mbody">
<div class="form-row">
<div class="fg"><label class="fl"><%= t('admin.routers.form.name', 'Nama Router') %> *</label><input class="fc" name="name" required placeholder="<%= t('admin.routers.form.name_placeholder', 'Contoh: Router Pusat') %>"></div>
<div class="fg"><label class="fl"><%= t('admin.routers.form.host', 'Host / IP') %> *</label><input class="fc" name="host" required placeholder="192.168.1.1"></div>
</div>
<div class="form-row">
<div class="fg"><label class="fl"><%= t('admin.routers.form.api_port', 'Port API') %> *</label><input class="fc" name="port" type="number" value="8728" required></div>
<div class="fg"><label class="fl"><%= t('admin.routers.form.username', 'Username MikroTik') %> *</label><input class="fc" name="user" required></div>
</div>
<div class="form-row">
<div class="fg"><label class="fl"><%= t('admin.routers.form.password', 'Password') %> *</label><input class="fc" name="password" type="password" required></div>
<div class="fg">
<label class="fl"><%= t('admin.routers.form.status', 'Status') %></label>
<select class="fs" name="is_active">
<option value="1"><%= t('common.active', 'Aktif') %></option>
<option value="0"><%= t('common.inactive', 'Nonaktif') %></option>
</select>
</div>
</div>
<div class="fg"><label class="fl"><%= t('admin.routers.form.description', 'Deskripsi / Lokasi') %></label><textarea class="fc" name="description" placeholder="<%= t('admin.routers.form.optional', 'Opsional...') %>"></textarea></div>
</div>
<div class="mf">
<button type="button" class="btn btn-g" onclick="closeModal('addModal')"><%= t('common.cancel', 'Batal') %></button>
<button type="submit" class="btn btn-p"><i class="bi bi-check-lg"></i> <%= t('admin.routers.actions.save', 'Simpan Router') %></button>
</div>
</form>
</div>
</div>
<!-- Modal Edit -->
<div class="mo" id="editModal">
<div class="mb">
<div class="mh"><span class="mt"><i class="bi bi-pencil"></i> <%= t('admin.routers.actions.edit_title', 'Edit Router') %></span><button class="mc" onclick="closeModal('editModal')">&times;</button></div>
<form method="POST" id="editForm">
<div class="mbody">
<div class="form-row">
<div class="fg"><label class="fl"><%= t('admin.routers.form.name', 'Nama Router') %> *</label><input class="fc" name="name" id="e_name" required></div>
<div class="fg"><label class="fl"><%= t('admin.routers.form.host', 'Host / IP') %> *</label><input class="fc" name="host" id="e_host" required></div>
</div>
<div class="form-row">
<div class="fg"><label class="fl"><%= t('admin.routers.form.api_port', 'Port API') %> *</label><input class="fc" name="port" id="e_port" type="number" required></div>
<div class="fg"><label class="fl"><%= t('admin.routers.form.username', 'Username MikroTik') %> *</label><input class="fc" name="user" id="e_user" required></div>
</div>
<div class="form-row">
<div class="fg"><label class="fl"><%= t('admin.routers.form.password_keep', 'Password (Kosongkan jika tetap)') %></label><input class="fc" name="password" type="password"></div>
<div class="fg">
<label class="fl"><%= t('admin.routers.form.status', 'Status') %></label>
<select class="fs" name="is_active" id="e_active">
<option value="1"><%= t('common.active', 'Aktif') %></option>
<option value="0"><%= t('common.inactive', 'Nonaktif') %></option>
</select>
</div>
</div>
<div class="fg"><label class="fl"><%= t('admin.routers.form.description', 'Deskripsi / Lokasi') %></label><textarea class="fc" name="description" id="e_desc"></textarea></div>
</div>
<div class="mf">
<button type="button" class="btn btn-g" onclick="closeModal('editModal')"><%= t('common.cancel', 'Batal') %></button>
<button type="submit" class="btn btn-p"><i class="bi bi-check-lg"></i> <%= t('admin.routers.actions.update', 'Update Router') %></button>
</div>
</form>
</div>
</div>
<script id="routers-data" type="application/json"><%- JSON.stringify(routers).split('<').join('\\u003c') %></script>
<script>
const routersData = JSON.parse(document.getElementById('routers-data').textContent);
let currentModalRouterId = null;
let currentModalData = null;
const I18N = {
testConfirm: "<%= t('admin.routers.confirm.test', 'Test koneksi ke router ini?') %>",
systemError: "<%= t('admin.routers.js.system_error', 'Terjadi kesalahan sistem.') %>"
};
function openModal(id){document.getElementById(id).classList.add('show')}
function closeModal(id){document.getElementById(id).classList.remove('show')}
function switchView(mode) {
const gridBtn = document.getElementById('btn-view-grid');
const tableBtn = document.getElementById('btn-view-table');
const gridContainer = document.getElementById('view-grid-container');
const tableContainer = document.getElementById('view-table-container');
if (mode === 'grid') {
gridBtn.classList.add('active');
tableBtn.classList.remove('active');
gridContainer.style.display = 'grid';
tableContainer.style.display = 'none';
} else {
tableBtn.classList.add('active');
gridBtn.classList.remove('active');
gridContainer.style.display = 'none';
tableContainer.style.display = 'block';
}
}
function formatBytes(bytes) {
if (!bytes || bytes === 0) return '0 MB';
const mb = bytes / (1024 * 1024);
return mb >= 1024 ? (mb / 1024).toFixed(1) + ' GB' : mb.toFixed(0) + ' MB';
}
async function loadRouterLiveSummary(r) {
try {
const res = await fetch('/admin/api/routers/' + r.id + '/details');
const data = await res.json();
if (!data.success || !data.live) return;
const live = data.live;
const pill = document.getElementById('status-pill-' + r.id);
const cpuLbl = document.getElementById('cpu-load-lbl-' + r.id);
const cpuBar = document.getElementById('cpu-load-bar-' + r.id);
const ramLbl = document.getElementById('ram-usage-lbl-' + r.id);
const ramBar = document.getElementById('ram-usage-bar-' + r.id);
const specCust = document.getElementById('spec-cust-' + r.id);
const specPppoe = document.getElementById('spec-pppoe-' + r.id);
const specHs = document.getElementById('spec-hs-' + r.id);
if (pill) {
if (live.connected) {
pill.className = 'rc-status-pill online';
pill.innerHTML = '<span class="rc-status-dot"></span> Online';
} else {
pill.className = 'rc-status-pill offline';
pill.innerHTML = '<span class="rc-status-dot"></span> Offline';
}
}
if (specCust) specCust.textContent = live.customersCount || 0;
if (specPppoe) specPppoe.textContent = live.activePppoe || 0;
if (specHs) specHs.textContent = live.activeHotspot || 0;
if (live.connected) {
const cpu = live.cpuLoad || 0;
if (cpuLbl) cpuLbl.textContent = cpu + '%';
if (cpuBar) {
cpuBar.style.width = cpu + '%';
cpuBar.className = 'rc-progress-bar ' + (cpu > 80 ? 'bg-red' : (cpu > 50 ? 'bg-yellow' : 'bg-green'));
}
if (live.totalMemory > 0) {
const usedMem = live.totalMemory - live.freeMemory;
const memPercent = Math.round((usedMem / live.totalMemory) * 100);
if (ramLbl) ramLbl.textContent = formatBytes(usedMem) + ' / ' + formatBytes(live.totalMemory);
if (ramBar) {
ramBar.style.width = memPercent + '%';
ramBar.className = 'rc-progress-bar ' + (memPercent > 85 ? 'bg-red' : (memPercent > 65 ? 'bg-yellow' : 'bg-green'));
}
}
}
} catch (e) {}
}
function switchModalTab(tabName) {
const tabs = ['sys', 'pppoe', 'hs'];
tabs.forEach(t => {
const btn = document.getElementById('mtab-btn-' + t);
const content = document.getElementById('mtab-content-' + t);
if (btn) btn.classList.toggle('active', t === tabName);
if (content) content.style.display = (t === tabName) ? 'block' : 'none';
});
}
async function openLiveDetailModal(id) {
currentModalRouterId = id;
const router = routersData.find(r => r.id === id);
const nameEl = document.getElementById('modal-router-name');
const bodyEl = document.getElementById('modal-router-body');
if (nameEl) nameEl.textContent = 'Detail Router: ' + (router ? router.name : '#' + id);
bodyEl.innerHTML = `
<div class="text-center py-5 text-muted">
<i class="bi bi-arrow-repeat spin fs-2 mb-2 d-block text-primary"></i>
<span>Mengambil metrik sistem & sesi live dari MikroTik...</span>
</div>
`;
openModal('routerLiveModal');
try {
const res = await fetch('/admin/api/routers/' + id + '/details');
const data = await res.json();
if (!data.success) throw new Error(data.error || 'Gagal mengambil data router');
currentModalData = data;
const l = data.live;
const r = data.router;
const usedMem = l.totalMemory - l.freeMemory;
const memPercent = l.totalMemory > 0 ? Math.round((usedMem / l.totalMemory) * 100) : 0;
const usedHdd = l.totalHdd - l.freeHdd;
const hddPercent = l.totalHdd > 0 ? Math.round((usedHdd / l.totalHdd) * 100) : 0;
const pppoeRows = (l.activePppoeList || []).map((p, idx) => `
<tr>
<td class="mt-idx">${idx + 1}</td>
<td class="mt-strong">${p.name}</td>
<td class="mono">${p.address}</td>
<td class="mt-warn">${p.uptime}</td>
<td class="mono">${p.callerId}</td>
</tr>
`).join('');
const hsRows = (l.activeHotspotList || []).map((h, idx) => `
<tr>
<td class="mt-idx">${idx + 1}</td>
<td class="mt-strong">${h.user}</td>
<td class="mono">${h.address}</td>
<td class="mono">${h.mac}</td>
<td class="mt-warn">${h.uptime}</td>
</tr>
`).join('');
const _cl = (p) => p > 80 ? 'crit' : (p > 50 ? 'warn' : 'ok');
const cpuCls = _cl(l.cpuLoad), memCls = _cl(memPercent), hddCls = _cl(hddPercent);
bodyEl.innerHTML = `
<div class="mt-head">
<div style="min-width:0">
<div class="mt-head-name"><i class="bi bi-hdd-network"></i> <span>${r.name}</span></div>
<div class="mt-head-sub mono">${r.host}:${r.port} &middot; ${r.user || '-'}</div>
</div>
<span class="rc-status-pill ${l.connected ? 'online' : 'offline'}"><span class="rc-status-dot"></span> ${l.connected ? 'Online' : 'Offline'}</span>
</div>
${l.error ? `<div class="alert alert-d"><i class="bi bi-exclamation-triangle"></i> ${l.error}</div>` : ''}
<div class="mt-tabs">
<button type="button" class="mt-tab active" id="mtab-btn-sys" onclick="switchModalTab('sys')"><i class="bi bi-cpu"></i> Sistem &amp; Metrik</button>
<button type="button" class="mt-tab" id="mtab-btn-pppoe" onclick="switchModalTab('pppoe')"><i class="bi bi-plug"></i> PPPoE <span class="mt-tab-badge">${l.activePppoe}</span></button>
<button type="button" class="mt-tab" id="mtab-btn-hs" onclick="switchModalTab('hs')"><i class="bi bi-wifi"></i> Hotspot <span class="mt-tab-badge">${l.activeHotspot}</span></button>
</div>
<div id="mtab-content-sys">
<div class="mt-metrics">
<div class="mt-metric">
<div class="lbl">CPU Load</div>
<div class="val ${cpuCls}">${l.cpuLoad}<span class="u">%</span></div>
<div class="mt-bar"><span class="${cpuCls}" style="width:${Math.min(100, l.cpuLoad)}%"></span></div>
</div>
<div class="mt-metric">
<div class="lbl">RAM Terpakai</div>
<div class="val ${memCls}">${memPercent}<span class="u">%</span></div>
<div class="sub">${formatBytes(usedMem)} / ${formatBytes(l.totalMemory)}</div>
<div class="mt-bar"><span class="${memCls}" style="width:${memPercent}%"></span></div>
</div>
<div class="mt-metric">
<div class="lbl">Disk</div>
<div class="val ${hddCls}">${hddPercent}<span class="u">%</span></div>
<div class="sub">${formatBytes(usedHdd)} / ${formatBytes(l.totalHdd)}</div>
<div class="mt-bar"><span class="${hddCls}" style="width:${hddPercent}%"></span></div>
</div>
</div>
<div class="mt-kv">
<div class="row"><span>Identity RouterOS</span><b class="mono">${l.identity}</b></div>
<div class="row"><span>Board / Tipe</span><b>${l.boardName}</b></div>
<div class="row"><span>RouterOS</span><b class="mt-ok">${l.version}</b></div>
<div class="row"><span>CPU</span><b>${l.architecture} &middot; ${l.cpuCount} core @ ${l.cpuFrequency} MHz</b></div>
<div class="row"><span><%= t('admin.common.uptime','Uptime') %></span><b class="mono">${l.uptime}</b></div>
</div>
<div class="mt-stats">
<div class="mt-stat"><span>Pelanggan DB</span><b>${l.customersCount}</b></div>
<div class="mt-stat"><span><%= t('admin.rt.pppoe_active','PPPoE Aktif') %></span><b class="mt-ok">${l.activePppoe}</b></div>
<div class="mt-stat"><span>Hotspot Aktif</span><b class="mt-warn">${l.activeHotspot}</b></div>
</div>
</div>
<div id="mtab-content-pppoe" style="display:none;">
<div class="mt-tablewrap">
<table class="mt-tbl">
<thead><tr><th>#</th><th>Username</th><th>IP Address</th><th><%= t('admin.common.uptime','Uptime') %></th><th>Caller ID / MAC</th></tr></thead>
<tbody>${pppoeRows || '<tr><td colspan="5" class="mt-empty">Tidak ada sesi PPPoE aktif di router ini</td></tr>'}</tbody>
</table>
</div>
</div>
<div id="mtab-content-hs" style="display:none;">
<div class="mt-tablewrap">
<table class="mt-tbl">
<thead><tr><th>#</th><th>User / Voucher</th><th>IP Address</th><th>MAC Address</th><th><%= t('admin.common.uptime','Uptime') %></th></tr></thead>
<tbody>${hsRows || '<tr><td colspan="5" class="mt-empty">Tidak ada pengguna Hotspot aktif di router ini</td></tr>'}</tbody>
</table>
</div>
</div>
`;
} catch (e) {
bodyEl.innerHTML = `<div class="alert alert-d"><i class="bi bi-exclamation-triangle"></i> ${e.message}</div>`;
}
}
function refreshCurrentModal() {
if (currentModalRouterId) openLiveDetailModal(currentModalRouterId);
}
function editRouter(idx) {
const r = routersData[idx];
document.getElementById('editForm').action = '/admin/routers/' + r.id + '/update';
document.getElementById('e_name').value = r.name;
document.getElementById('e_host').value = r.host;
document.getElementById('e_port').value = r.port;
document.getElementById('e_user').value = r.user;
document.getElementById('e_active').value = r.is_active;
document.getElementById('e_desc').value = r.description || '';
openModal('editModal');
}
async function testConn(id) {
if(!confirm(I18N.testConfirm)) return;
try {
const res = await fetch('/admin/api/routers/' + id + '/test');
const data = await res.json();
alert(data.success ? ' ' + data.message : ' ' + data.error);
loadRouterLiveSummary({ id });
} catch (e) { alert(I18N.systemError); }
}
function switchIsolirTab(tab) {
const autoTab = document.getElementById('isolir-tab-auto');
const scriptTab = document.getElementById('isolir-tab-script');
const autoBtn = document.getElementById('tab-btn-auto');
const scriptBtn = document.getElementById('tab-btn-script');
const copyBtn = document.getElementById('btn-copy-script-modal');
if (tab === 'auto') {
autoTab.style.display = 'block';
scriptTab.style.display = 'none';
autoBtn.className = 'btn btn-sm btn-p';
scriptBtn.className = 'btn btn-sm btn-g';
if (copyBtn) copyBtn.style.display = 'none';
} else {
autoTab.style.display = 'none';
scriptTab.style.display = 'block';
autoBtn.className = 'btn btn-sm btn-g';
scriptBtn.className = 'btn btn-sm btn-p';
if (copyBtn) copyBtn.style.display = 'inline-block';
}
}
async function setupFirewall(routerId) {
openIsolirScriptModal(routerId);
switchIsolirTab('auto');
}
async function openIsolirScriptModal(routerId = null) {
const ta = document.getElementById('isolir-script-body');
const meta = document.getElementById('isolir-script-meta');
const routerSel = document.getElementById('isolir-modal-router-select');
const statusBox = document.getElementById('isolir-auto-setup-result');
if (statusBox) statusBox.style.display = 'none';
if (routerSel && routerId) {
routerSel.value = String(routerId);
}
ta.value = '<%= t('common.loading', 'Memuat...') %>';
meta.textContent = '';
openModal('isolirScriptModal');
try {
const res = await fetch('/admin/api/isolir-portal-script');
const data = await res.json();
if (!data.success) throw new Error(data.error || 'Gagal');
ta.value = data.script || '';
meta.textContent = (data.appUrl ? ('app_url: ' + data.appUrl + ' | ') : '') + 'IP Server: ' + (data.billingIp || '-') + ' | NAT → HTTP:' + (data.httpNatPort ?? '-') + ' HTTPS:' + (data.httpsNatPort ?? '-');
} catch (e) {
ta.value = '';
meta.textContent = e.message || 'Error';
}
}
async function runAutoSetupFirewallFromModal() {
const routerSel = document.getElementById('isolir-modal-router-select');
const routerId = routerSel ? routerSel.value : null;
const statusBox = document.getElementById('isolir-auto-setup-result');
if (!routerId) {
alert('Pilih router terlebih dahulu!');
return;
}
statusBox.style.display = 'block';
statusBox.className = 'alert alert-info p-3 small mt-3';
statusBox.innerHTML = '<i class="bi bi-arrow-repeat spin me-1"></i> Sedang menghubungkan dan memasang rule Firewall/NAT isolir di MikroTik...';
try {
const res = await fetch('/admin/api/routers/' + routerId + '/setup-firewall', { method: 'POST' });
const data = await res.json();
if (data.success) {
statusBox.className = 'alert alert-success p-3 small mt-3';
statusBox.innerHTML = '<i class="bi bi-check-circle-fill me-1"></i> <strong>BERHASIL:</strong> ' + data.message;
} else {
statusBox.className = 'alert alert-danger p-3 small mt-3';
statusBox.innerHTML = '<i class="bi bi-exclamation-triangle-fill me-1"></i> <strong>GAGAL:</strong> ' + (data.error || data.message || 'Terjadi kesalahan');
}
} catch (e) {
statusBox.className = 'alert alert-danger p-3 small mt-3';
statusBox.innerHTML = '<i class="bi bi-exclamation-triangle-fill me-1"></i> <strong>ERROR:</strong> ' + e.message;
}
}
function copyIsolirScript() {
const ta = document.getElementById('isolir-script-body');
if (!ta || !ta.value) return;
ta.select();
document.execCommand('copy');
alert('<%= t('admin.routers.isolir_script_copied', 'Script disalin ke clipboard.') %>');
}
// Auto load metrics for all routers on page load
window.addEventListener('DOMContentLoaded', () => {
routersData.forEach(r => {
loadRouterLiveSummary(r);
});
});
</script>
</body>
</html>