1186 lines
58 KiB
Plaintext
1186 lines
58 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.olt_management', 'Manajemen OLT') %> | <%= company %></title>
|
||
<link rel="stylesheet" href="/vendor/bootstrap-icons/bootstrap-icons.min.css">
|
||
<link rel="stylesheet" href="/css/admin.css">
|
||
<style>
|
||
/* New Dashboard Layout Styles */
|
||
.olt-dashboard { display: flex; flex-direction: column; gap: 20px; }
|
||
|
||
.stats-row {
|
||
display: grid;
|
||
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
||
gap: 15px;
|
||
}
|
||
|
||
.stat-card-v2 {
|
||
background: white;
|
||
border-radius: 12px;
|
||
padding: 20px;
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 15px;
|
||
box-shadow: 0 4px 6px -1px rgba(0,0,0,0.1);
|
||
border: 1px solid #e2e8f0;
|
||
}
|
||
|
||
.stat-card-v2 .icon-box {
|
||
width: 48px;
|
||
height: 48px;
|
||
border-radius: 10px;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
font-size: 24px;
|
||
color: white;
|
||
}
|
||
|
||
.stat-card-v2 .details h3 { margin: 0; font-size: 24px; font-weight: 700; color: #1e293b; line-height: 1; }
|
||
.stat-card-v2 .details p { margin: 0; font-size: 12px; color: #64748b; font-weight: 600; text-transform: uppercase; margin-top: 4px; }
|
||
|
||
.bg-blue { background: linear-gradient(135deg, #3b82f6, #2563eb); }
|
||
.bg-green { background: linear-gradient(135deg, #10b981, #059669); }
|
||
.bg-red { background: linear-gradient(135deg, #ef4444, #dc2626); }
|
||
.bg-orange { background: linear-gradient(135deg, #f59e0b, #d97706); }
|
||
|
||
.system-info-bar {
|
||
background: #f8fafc;
|
||
border: 1px solid #e2e8f0;
|
||
border-radius: 10px;
|
||
padding: 12px 20px;
|
||
display: flex;
|
||
flex-wrap: wrap;
|
||
gap: 20px;
|
||
align-items: center;
|
||
font-size: 13px;
|
||
}
|
||
|
||
.info-item { display: flex; align-items: center; gap: 8px; color: #475569; }
|
||
.info-item i { color: #3b82f6; }
|
||
|
||
.olt-selector-container {
|
||
background: white;
|
||
padding: 15px 20px;
|
||
border-radius: 12px;
|
||
border: 1px solid #e2e8f0;
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
gap: 15px;
|
||
flex-wrap: wrap;
|
||
}
|
||
|
||
.custom-select-v2 {
|
||
padding: 8px 12px;
|
||
border-radius: 8px;
|
||
border: 1px solid #cbd5e1;
|
||
font-weight: 600;
|
||
color: #1e293b;
|
||
min-width: 250px;
|
||
outline: none;
|
||
}
|
||
|
||
.custom-select-v2:focus { border-color: #3b82f6; box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1); }
|
||
|
||
.onu-main-card {
|
||
background: white;
|
||
border-radius: 12px;
|
||
border: 1px solid #e2e8f0;
|
||
overflow: hidden;
|
||
box-shadow: 0 4px 6px -1px rgba(0,0,0,0.1);
|
||
}
|
||
|
||
.onu-main-card .card-header {
|
||
padding: 15px 20px;
|
||
background: #f8fafc;
|
||
border-bottom: 1px solid #e2e8f0;
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
}
|
||
|
||
.onu-table-v2 { width: 100%; border-collapse: collapse; }
|
||
.onu-table-v2 th {
|
||
background: #f1f5f9;
|
||
padding: 12px 15px;
|
||
text-align: left;
|
||
font-size: 12px;
|
||
font-weight: 700;
|
||
color: #475569;
|
||
text-transform: uppercase;
|
||
border-bottom: 1px solid #e2e8f0;
|
||
}
|
||
.onu-table-v2 td { padding: 12px 15px; border-bottom: 1px solid #f1f5f9; font-size: 13px; color: #1e293b; }
|
||
.onu-table-v2 tr:hover { background: #f8fafc; }
|
||
|
||
.status-pill-v2 {
|
||
padding: 4px 10px;
|
||
border-radius: 20px;
|
||
font-size: 11px;
|
||
font-weight: 700;
|
||
display: inline-flex;
|
||
align-items: center;
|
||
gap: 5px;
|
||
}
|
||
.pill-online { background: #dcfce7; color: #15803d; }
|
||
.pill-offline { background: #fee2e2; color: #b91c1c; }
|
||
|
||
.power-badge {
|
||
font-family: 'JetBrains Mono', monospace;
|
||
font-weight: 600;
|
||
padding: 2px 6px;
|
||
border-radius: 4px;
|
||
}
|
||
.power-good { color: #059669; background: #ecfdf5; }
|
||
.power-warning { color: #d97706; background: #fffbeb; }
|
||
.power-danger { color: #dc2626; background: #fef2f2; font-weight: 800; }
|
||
|
||
.brand-badge { font-weight: 700; text-transform: uppercase; font-size: 10px; padding: 2px 5px; border-radius: 4px; }
|
||
.brand-zte { background: #eff6ff; color: #2563eb; }
|
||
.brand-huawei { background: #fef2f2; color: #dc2626; }
|
||
.brand-vsol { background: #f0fdf4; color: #16a34a; }
|
||
.brand-cdata { background: #fff7ed; color: #ea580c; }
|
||
.brand-hioso { background: #fffbeb; color: #d97706; }
|
||
.brand-hsgq { background: #f5f3ff; color: #7c3aed; }
|
||
.brand-bdcom { background: #fdf2f8; color: #db2777; }
|
||
.brand-fiberhome { background: #f0fdfa; color: #0d9488; }
|
||
.brand-generic { background: #f1f5f9; color: #475569; }
|
||
|
||
.spin { animation: rotate 2s linear infinite; display: inline-block; }
|
||
@keyframes rotate { from { transform: rotate(0deg); } to { transform: rotate(360deg); } }
|
||
|
||
.btn-action-v2 {
|
||
width: 32px;
|
||
height: 32px;
|
||
border-radius: 8px;
|
||
display: inline-flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
border: 1px solid #e2e8f0;
|
||
background: white;
|
||
color: #64748b;
|
||
transition: all 0.2s;
|
||
cursor: pointer;
|
||
}
|
||
.btn-action-v2:hover { background: #f1f5f9; color: #1e293b; border-color: #cbd5e1; }
|
||
.btn-reboot-v2:hover { color: #ef4444; background: #fef2f2; border-color: #fee2e2; }
|
||
.btn-rename-v2:hover { color: #3b82f6; background: #eff6ff; border-color: #dbeafe; }
|
||
.btn-wan-v2:hover { color: #059669; background: #ecfdf5; border-color: #d1fae5; }
|
||
|
||
.traffic-v2 { display: flex; gap: 12px; font-family: monospace; font-size: 12px; font-weight: 600; }
|
||
.traffic-in-v2 { color: #10b981; }
|
||
.traffic-out-v2 { color: #3b82f6; }
|
||
|
||
.empty-state { padding: 60px 20px; text-align: center; color: #94a3b8; }
|
||
.empty-state i { font-size: 48px; display: block; margin-bottom: 15px; }
|
||
|
||
.loading-overlay {
|
||
position: absolute;
|
||
top: 0; left: 0; right: 0; bottom: 0;
|
||
background: rgba(255,255,255,0.7);
|
||
backdrop-filter: blur(2px);
|
||
display: none;
|
||
align-items: center;
|
||
justify-content: center;
|
||
z-index: 10;
|
||
}
|
||
</style>
|
||
</head>
|
||
<body>
|
||
<%- include('partials/sidebar', { activePage: 'olts', 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-rack"></i>
|
||
<span><%= t('admin.nav.olt_management', 'Manajemen OLT') %></span>
|
||
</div>
|
||
<div class="tb-right">
|
||
<div class="d-flex gap-2">
|
||
<button class="btn btn-g btn-sm" onclick="openModal('manageOltsModal')"><i class="bi bi-gear"></i> <%= t('admin.olts.manage', 'Kelola OLT') %></button>
|
||
<button class="btn btn-p btn-sm" onclick="openModal('addOltModal')"><i class="bi bi-plus-lg"></i> <%= t('admin.olts.add', 'Tambah OLT') %></button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="page">
|
||
<% if (msg) { %>
|
||
<div class="alert alert-<%= msg.type==='error'?'d':'s' %>" data-flash="1"><%= msg.text %></div>
|
||
<% } %>
|
||
|
||
<div class="olt-dashboard">
|
||
<!-- OLT Selector & Global Actions -->
|
||
<div class="olt-selector-container">
|
||
<div class="d-flex align-items-center gap-3">
|
||
<label class="fw-bold text-muted small text-uppercase"><%= t('admin.olts.select_label', 'Pilih Perangkat OLT:') %></label>
|
||
<select id="olt-selector" class="custom-select-v2" onchange="handleOltChange()">
|
||
<% if (olts.length === 0) { %>
|
||
<option value=""><%= t('admin.olts.no_data', 'Tidak ada data OLT') %></option>
|
||
<% } else if (olts.length === 1) { %>
|
||
<option value="<%= olts[0].id %>" <%= olts[0].is_active ? '' : 'disabled' %> selected><%= olts[0].name %> (<%= olts[0].host %>)</option>
|
||
<option value="all"> <%= t('admin.olt.all_summary','Semua OLT (Ringkasan Total)') %></option>
|
||
<% } else { %>
|
||
<option value="all"> <%= t('admin.olt.all_summary','Semua OLT (Ringkasan Total)') %></option>
|
||
<% olts.forEach(olt => { %>
|
||
<option value="<%= olt.id %>" <%= olt.is_active ? '' : 'disabled' %>><%= olt.name %> (<%= olt.host %>)</option>
|
||
<% }) %>
|
||
<% } %>
|
||
</select>
|
||
</div>
|
||
<div class="d-flex gap-2">
|
||
<button class="btn btn-sm btn-p" onclick="refreshCurrentOlt()">
|
||
<i class="bi bi-arrow-clockwise"></i> <%= t('admin.olts.refresh_data', 'Refresh Data') %>
|
||
</button>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Summary Cards -->
|
||
<div class="stats-row">
|
||
<div class="stat-card-v2">
|
||
<div class="icon-box bg-blue"><i class="bi bi-device-hdd"></i></div>
|
||
<div class="details">
|
||
<h3 id="stat-total-onu">-</h3>
|
||
<p><%= t('admin.olts.stats.total_onu', 'Total ONU') %></p>
|
||
</div>
|
||
</div>
|
||
<div class="stat-card-v2">
|
||
<div class="icon-box bg-green"><i class="bi bi-wifi"></i></div>
|
||
<div class="details">
|
||
<h3 id="stat-online-onu">-</h3>
|
||
<p><%= t('admin.olts.stats.online', 'Online') %></p>
|
||
</div>
|
||
</div>
|
||
<div class="stat-card-v2">
|
||
<div class="icon-box bg-red"><i class="bi bi-wifi-off"></i></div>
|
||
<div class="details">
|
||
<h3 id="stat-offline-onu">-</h3>
|
||
<p><%= t('admin.olts.stats.offline', 'Offline') %></p>
|
||
</div>
|
||
</div>
|
||
<div class="stat-card-v2">
|
||
<div class="icon-box bg-orange"><i class="bi bi-exclamation-triangle"></i></div>
|
||
<div class="details">
|
||
<h3 id="stat-weak-onu">-</h3>
|
||
<p><%= t('admin.olts.stats.weak_signal', 'Sinyal Lemah') %></p>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- System Info Bar -->
|
||
<div class="system-info-bar">
|
||
<div class="info-item"><i class="bi bi-thermometer-half"></i> <%= t('admin.olts.system.temp', 'Suhu') %>: <strong id="stat-temp">-</strong></div>
|
||
<div class="info-item"><i class="bi bi-cpu"></i> <%= t('admin.olts.system.cpu', 'CPU') %>: <strong id="stat-cpu">-</strong></div>
|
||
<div class="info-item"><i class="bi bi-memory"></i> <%= t('admin.olts.system.ram', 'RAM') %>: <strong id="stat-ram">-</strong></div>
|
||
<div class="info-item"><i class="bi bi-clock-history"></i> <%= t('admin.olts.system.uptime', 'Uptime') %>: <strong id="stat-uptime">-</strong></div>
|
||
<div class="info-item ms-auto">
|
||
<div class="traffic-v2">
|
||
<span class="traffic-in-v2" title="<%= t('admin.olts.system.traffic_in', 'Traffic In') %>"><i class="bi bi-arrow-down"></i> <span id="stat-traffic-in">0 B</span></span>
|
||
<span class="traffic-out-v2" title="<%= t('admin.olts.system.traffic_out', 'Traffic Out') %>"><i class="bi bi-arrow-up"></i> <span id="stat-traffic-out">0 B</span></span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- OID & SNMP Info Section -->
|
||
<div id="oid-info-container" class="onu-main-card" style="margin-top: 15px; display: none;">
|
||
<div class="card-header d-flex justify-content-between align-items-center" style="background:#0f172a; color:white;">
|
||
<h4 class="fw-bold m-0 small text-uppercase" style="color:#60a5fa;"><i class="bi bi-diagram-3"></i> Informasi OID & Profil SNMP Perangkat</h4>
|
||
<span id="oid-brand-badge" class="brand-badge brand-hioso">-</span>
|
||
</div>
|
||
<div class="p-3" style="background:#1e293b; color:#f8fafc; font-size:12px;">
|
||
<div class="row g-3">
|
||
<div class="col-md-4">
|
||
<div style="background:#0f172a; padding:12px; border-radius:8px; border:1px solid #334155;">
|
||
<div class="text-muted fw-bold mb-2" style="font-size:11px; text-transform:uppercase; color:#94a3b8;"><i class="bi bi-hdd-rack"></i> Profil & Parameter Connection</div>
|
||
<div class="mb-1">Profil SNMP: <strong id="oid-profile-name" style="color:#38bdf8;">-</strong></div>
|
||
<div class="mb-1"><%= t('admin.olt.host','Host OLT:') %> <strong id="oid-host">-</strong></div>
|
||
<div class="mb-1"><%= t('admin.olt.snmp_port','Port SNMP:') %> <strong id="oid-port">161</strong></div>
|
||
<div>Community: <strong id="oid-community" style="color:#4ade80;">public</strong></div>
|
||
</div>
|
||
</div>
|
||
<div class="col-md-8">
|
||
<div style="background:#0f172a; padding:12px; border-radius:8px; border:1px solid #334155;">
|
||
<div class="text-muted fw-bold mb-2" style="font-size:11px; text-transform:uppercase; color:#94a3b8;"><i class="bi bi-card-checklist"></i> Daftar OID Utama (SNMP Walk & Get)</div>
|
||
<div class="table-responsive">
|
||
<table class="table table-dark table-sm mb-0" style="font-family:monospace; font-size:11px; border-color:#334155;">
|
||
<thead>
|
||
<tr>
|
||
<th style="color:#94a3b8">Deskripsi Metrik</th>
|
||
<th style="color:#94a3b8">OID Object Identifier</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody id="oid-table-body">
|
||
<!-- OID rows rendered via JS -->
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Card List Section -->
|
||
<div id="card-list-container" style="display: none;">
|
||
<div class="onu-main-card" style="margin-bottom: 20px;">
|
||
<div class="card-header">
|
||
<h4 class="fw-bold m-0 text-primary small text-uppercase"><i class="bi bi-grid-3x3-gap"></i> <%= t('admin.olts.card_list', 'Daftar Board / Card') %></h4>
|
||
</div>
|
||
<div class="table-responsive">
|
||
<table class="onu-table-v2">
|
||
<thead>
|
||
<tr>
|
||
<th>SLOT</th>
|
||
<th>TYPE</th>
|
||
<th>STATUS</th>
|
||
<th>PORTS</th>
|
||
<th>SERIAL</th>
|
||
<th>CPU</th>
|
||
<th>RAM</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody id="card-table-body">
|
||
<!-- Cards rendered here -->
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Unauth ONU Section -->
|
||
<div id="unauth-list-container" style="display: none;">
|
||
<div class="onu-main-card" style="margin-bottom: 20px; border-left: 4px solid #ffc107;">
|
||
<div class="card-header d-flex justify-content-between align-items-center">
|
||
<h4 class="fw-bold m-0 text-warning small text-uppercase"><i class="bi bi-exclamation-triangle"></i> <%= t('admin.olts.unauth_onu', 'ONU Belum Terdaftar (Unauth)') %></h4>
|
||
</div>
|
||
<div class="table-responsive">
|
||
<table class="onu-table-v2">
|
||
<thead>
|
||
<tr>
|
||
<th>PORT/INDEX</th>
|
||
<th>SN / SERIAL NUMBER</th>
|
||
<th>TYPE</th>
|
||
<th><%= t('admin.olt.th_action','AKSI') %></th>
|
||
</tr>
|
||
</thead>
|
||
<tbody id="unauth-table-body">
|
||
<!-- Unauth ONUs rendered here -->
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Main ONU Table Card -->
|
||
<div class="onu-main-card position-relative">
|
||
<div id="loading-overlay" class="loading-overlay">
|
||
<div class="text-center">
|
||
<div class="spinner-border text-primary" role="status"></div>
|
||
<div class="mt-2 fw-bold text-primary small"><%= t('admin.olts.loading', 'Mengambil Data OLT...') %></div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="card-header">
|
||
<div class="d-flex align-items-center gap-3">
|
||
<h6 class="mb-0 fw-bold"><i class="bi bi-list-check me-2"></i> <%= t('admin.olts.onu_list_title', 'Daftar Perangkat ONU Terdaftar') %></h6>
|
||
<div id="search-container" style="display:none;">
|
||
<input type="text" id="onu-search" class="fc fc-sm" placeholder="<%= t('admin.olts.search_placeholder', 'Cari ID / SN...') %>" onkeyup="filterOnuTable()" style="width: 250px; padding: 4px 12px; font-size: 12px;">
|
||
</div>
|
||
</div>
|
||
<div class="text-muted small"><%= t('admin.olts.last_update', 'Update Terakhir') %>: <span id="last-update">-</span></div>
|
||
</div>
|
||
|
||
<div class="tbl-wrap" style="max-height: 600px; overflow: auto;">
|
||
<table class="onu-table-v2" id="onu-table">
|
||
<thead>
|
||
<tr>
|
||
<th><%= t('admin.olts.table.no', 'No') %></th>
|
||
<th><%= t('admin.olts.table.id', 'Id') %></th>
|
||
<th><%= t('admin.olt.th_onu_name','Nama ONU / Pelanggan') %></th>
|
||
<th><%= t('admin.olts.table.serial', 'Serial Number') %></th>
|
||
<th><%= t('admin.olts.table.status', 'Status') %></th>
|
||
<th><%= t('admin.olts.table.tx', 'TxPower') %></th>
|
||
<th><%= t('admin.olts.table.rx', 'RxPower') %></th>
|
||
<th><%= t('admin.olts.table.distance', 'Jarak') %></th>
|
||
<th style="text-align:right"><%= t('common.actions', 'Aksi') %></th>
|
||
</tr>
|
||
</thead>
|
||
<tbody id="onu-table-body">
|
||
<tr>
|
||
<td colspan="9">
|
||
<div class="empty-state">
|
||
<i class="bi bi-hdd-network"></i>
|
||
<p><%= t('admin.olts.empty_select', 'Pilih perangkat OLT di atas untuk memantau data ONU.') %></p>
|
||
</div>
|
||
</td>
|
||
</tr>
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Modal Manage OLTs (Table List) -->
|
||
<div class="mo" id="manageOltsModal">
|
||
<div class="mb" style="max-width: 900px;">
|
||
<div class="mh">
|
||
<span class="mt"><%= t('admin.olts.manage_modal.title', 'Manajemen Perangkat OLT') %></span>
|
||
<button type="button" class="mc" onclick="closeModal('manageOltsModal')">×</button>
|
||
</div>
|
||
<div class="mbody">
|
||
<table class="dtbl">
|
||
<thead>
|
||
<tr>
|
||
<th><%= t('admin.olts.manage_modal.table.name', 'Nama OLT') %></th>
|
||
<th><%= t('admin.olts.manage_modal.table.host', 'Host / IP') %></th>
|
||
<th><%= t('admin.olts.manage_modal.table.brand', 'Brand') %></th>
|
||
<th><%= t('admin.olts.manage_modal.table.status', 'Status') %></th>
|
||
<th><%= t('admin.olts.manage_modal.table.actions', 'Aksi') %></th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
<% olts.forEach(olt => { %>
|
||
<tr>
|
||
<td><strong><%= olt.name %></strong></td>
|
||
<td><code><%= olt.host %></code></td>
|
||
<td><span class="brand-badge brand-<%= olt.brand %>"><%= olt.brand %></span></td>
|
||
<td><span class="badge <%= olt.is_active ? 'bs' : 'bd' %>"><%= olt.is_active ? t('common.active', 'Aktif') : t('common.inactive', 'Nonaktif') %></span></td>
|
||
<td style="white-space:nowrap">
|
||
<form id="odl-<%= olt.id %>" action="/admin/olts/<%= olt.id %>/delete" method="POST" style="display:none" onsubmit="return confirm(I18N.confirmDeleteOlt)"></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="testExistingOlt('<%= olt.id %>')"><i class="bi bi-broadcast"></i> <%= t('admin.dash.test_conn','Test Koneksi') %></button>
|
||
<button type="button" class="act-item" onclick="editOlt('<%= olt.id %>')"><i class="bi bi-pencil"></i> Edit OLT</button>
|
||
<button type="submit" form="odl-<%= olt.id %>" class="act-item danger"><i class="bi bi-trash"></i> <%= t('admin.olt.del_olt','Hapus OLT') %></button>
|
||
</div>
|
||
</div>
|
||
</td>
|
||
</tr>
|
||
<% }) %>
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
<div class="mf">
|
||
<button type="button" class="btn btn-g" onclick="closeModal('manageOltsModal')"><%= t('common.close', 'Tutup') %></button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Modal Tambah & Edit (Same as before but keeping them) -->
|
||
<!-- Modal Tambah -->
|
||
<div class="mo" id="addOltModal">
|
||
<div class="mb">
|
||
<form action="/admin/olts" method="POST">
|
||
<div class="mh"><span class="mt"><%= t('admin.olts.add_modal.title', 'Tambah OLT Baru') %></span><button type="button" class="mc" onclick="closeModal('addOltModal')">×</button></div>
|
||
<div class="mbody">
|
||
<div class="fg"><label class="fl"><%= t('admin.olts.form.name', 'Nama OLT') %></label><input type="text" name="name" class="fc" placeholder="<%= t('admin.olts.form.name_placeholder', 'Contoh: OLT Pusat') %>" required></div>
|
||
<div class="fg"><label class="fl"><%= t('admin.olts.form.host', 'IP Address / Host') %></label><input type="text" name="host" class="fc" placeholder="192.168.1.10" required></div>
|
||
<div class="form-row">
|
||
<div class="fg"><label class="fl"><%= t('admin.olts.form.snmp_port', 'SNMP Port') %></label><input type="number" name="snmp_port" class="fc" value="161"></div>
|
||
<div class="fg"><label class="fl"><%= t('admin.olts.form.brand', 'Brand / Type') %></label>
|
||
<select name="brand" class="fs">
|
||
<option value="zte">ZTE (C300 / C320 / C600)</option>
|
||
<option value="huawei">Huawei (MA5600 / MA5800)</option>
|
||
<option value="vsol">V-SOL (V1600 / V2400 EPON/GPON)</option>
|
||
<option value="fiberhome">FiberHome (AN5516)</option>
|
||
<option value="cdata">C-DATA (FD1600 / FD1800)</option>
|
||
<option value="hioso">HIOSO (EPON / GPON)</option>
|
||
<option value="hsgq">HSGQ (EPON / GPON)</option>
|
||
<option value="bdcom">BDCOM (EPON / GPON)</option>
|
||
<option value="generic"><%= t('admin.olt.opt_generic','Generic / Lainnya (Standard SNMP EPON/GPON)') %></option>
|
||
</select>
|
||
</div>
|
||
</div>
|
||
<div class="fg"><label class="fl"><%= t('admin.olts.form.snmp_community', 'SNMP Community') %></label><input type="text" name="snmp_community" class="fc" value="public"></div>
|
||
<div class="form-row">
|
||
<div class="fg"><label class="fl"><%= t('admin.olts.form.web_user_optional', 'Login User (Optional)') %></label><input type="text" name="web_user" class="fc" placeholder="admin"></div>
|
||
<div class="fg"><label class="fl"><%= t('admin.olts.form.web_password_optional', 'Login Pass (Optional)') %></label><input type="password" name="web_password" class="fc" placeholder="admin"></div>
|
||
</div>
|
||
<div class="form-row">
|
||
<div class="fg"><label class="fl">Telnet port</label><input type="number" name="telnet_port" class="fc" value="23" min="1" max="65535"></div>
|
||
<div class="fg"><label class="fl">Enable password (ZTE)</label><input type="password" name="enable_password" class="fc" placeholder="<%= t('admin.olt.ph_enable_pw','zxr10 (opsional)') %>"></div>
|
||
</div>
|
||
<div class="fg"><label class="fl">API Base URL (go-api-c320 ZTE)</label><input type="url" name="api_base_url" class="fc" placeholder="http://127.0.0.1:8081 — kosong jika tidak pakai"></div>
|
||
<div class="fg"><label class="fl"><%= t('admin.olts.form.description', 'Deskripsi / Lokasi') %></label><textarea name="description" class="fc" rows="2"></textarea></div>
|
||
<div id="add-test-result" class="alert alert-i small mt-2" style="display:none; font-size:12px;"></div>
|
||
</div>
|
||
<div class="mf d-flex justify-content-between">
|
||
<button type="button" class="btn btn-outline-primary btn-sm" onclick="testFormOlt('add')"><i class="bi bi-broadcast"></i> <%= t('admin.dash.test_conn','Test Koneksi') %></button>
|
||
<div class="d-flex gap-2">
|
||
<button type="button" class="btn btn-g" onclick="closeModal('addOltModal')"><%= t('common.cancel', 'Batal') %></button>
|
||
<button type="submit" class="btn btn-p"><%= t('admin.olts.add_modal.save', 'Simpan Perangkat') %></button>
|
||
</div>
|
||
</div>
|
||
</form>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Modal Edit -->
|
||
<div class="mo" id="editOltModal">
|
||
<div class="mb">
|
||
<form id="editOltForm" method="POST">
|
||
<div class="mh"><span class="mt"><%= t('admin.olts.edit_modal.title', 'Edit Data OLT') %></span><button type="button" class="mc" onclick="closeModal('editOltModal')">×</button></div>
|
||
<div class="mbody">
|
||
<div class="fg"><label class="fl"><%= t('admin.olts.form.name', 'Nama OLT') %></label><input type="text" name="name" id="e_name" class="fc" required></div>
|
||
<div class="fg"><label class="fl"><%= t('admin.olts.form.host', 'IP Address / Host') %></label><input type="text" name="host" id="e_host" class="fc" required></div>
|
||
<div class="form-row">
|
||
<div class="fg"><label class="fl"><%= t('admin.olts.form.snmp_port', 'SNMP Port') %></label><input type="number" name="snmp_port" id="e_snmp_port" class="fc"></div>
|
||
<div class="fg"><label class="fl"><%= t('admin.olts.form.brand', 'Brand / Type') %></label>
|
||
<select name="brand" id="e_brand" class="fs">
|
||
<option value="zte">ZTE (C300 / C320 / C600)</option>
|
||
<option value="huawei">Huawei (MA5600 / MA5800)</option>
|
||
<option value="vsol">V-SOL (V1600 / V2400 EPON/GPON)</option>
|
||
<option value="fiberhome">FiberHome (AN5516)</option>
|
||
<option value="cdata">C-DATA (FD1600 / FD1800)</option>
|
||
<option value="hioso">HIOSO (EPON / GPON)</option>
|
||
<option value="hsgq">HSGQ (EPON / GPON)</option>
|
||
<option value="bdcom">BDCOM (EPON / GPON)</option>
|
||
<option value="generic"><%= t('admin.olt.opt_generic','Generic / Lainnya (Standard SNMP EPON/GPON)') %></option>
|
||
</select>
|
||
</div>
|
||
</div>
|
||
<div class="fg"><label class="fl"><%= t('admin.olts.form.snmp_community', 'SNMP Community') %></label><input type="text" name="snmp_community" id="e_snmp_community" class="fc"></div>
|
||
<div class="form-row">
|
||
<div class="fg"><label class="fl"><%= t('admin.olts.form.web_user', 'Login User') %></label><input type="text" name="web_user" id="e_web_user" class="fc"></div>
|
||
<div class="fg"><label class="fl"><%= t('admin.olts.form.web_password', 'Login Pass') %></label><input type="password" name="web_password" id="e_web_password" class="fc"></div>
|
||
</div>
|
||
<div class="form-row">
|
||
<div class="fg"><label class="fl">Telnet port</label><input type="number" name="telnet_port" id="e_telnet_port" class="fc" min="1" max="65535"></div>
|
||
<div class="fg"><label class="fl">Enable password (ZTE)</label><input type="password" name="enable_password" id="e_enable_password" class="fc" placeholder="<%= t('admin.olt.ph_enable_pw2','Kosongkan jika tidak diubah') %>"></div>
|
||
</div>
|
||
<div class="fg"><label class="fl">API Base URL (go-api-c320)</label><input type="url" name="api_base_url" id="e_api_base_url" class="fc" placeholder="http://IP:8081"></div>
|
||
<div class="fg"><label class="fl"><%= t('admin.olts.form.description', 'Deskripsi / Lokasi') %></label><textarea name="description" id="e_description" class="fc" rows="2"></textarea></div>
|
||
<div class="fg">
|
||
<label class="fl"><%= t('admin.olts.form.monitoring_status', 'Status Monitoring') %></label>
|
||
<select name="is_active" id="e_is_active" class="fs">
|
||
<option value="1"><%= t('common.active', 'Aktif') %></option>
|
||
<option value="0"><%= t('common.inactive', 'Nonaktif') %></option>
|
||
</select>
|
||
</div>
|
||
<div id="edit-test-result" class="alert alert-i small mt-2" style="display:none; font-size:12px;"></div>
|
||
</div>
|
||
<div class="mf d-flex justify-content-between">
|
||
<button type="button" class="btn btn-outline-primary btn-sm" onclick="testFormOlt('edit')"><i class="bi bi-broadcast"></i> <%= t('admin.dash.test_conn','Test Koneksi') %></button>
|
||
<div class="d-flex gap-2">
|
||
<button type="button" class="btn btn-g" onclick="closeModal('editOltModal')"><%= t('common.cancel', 'Batal') %></button>
|
||
<button type="submit" class="btn btn-p"><%= t('admin.olts.edit_modal.update', 'Update Perangkat') %></button>
|
||
</div>
|
||
</div>
|
||
</form>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- WAN Configuration Modal -->
|
||
<div class="mo" id="wanConfigModal">
|
||
<div class="mb" style="max-width: 500px;">
|
||
<div class="mh">
|
||
<span class="mt"><i class="bi bi-globe"></i> <%= t('admin.olt.wan_title','Konfigurasi WAN ONU') %></span>
|
||
<button type="button" class="mc" onclick="closeModal('wanConfigModal')">×</button>
|
||
</div>
|
||
<form id="wanConfigForm" onsubmit="submitWanConfig(event)">
|
||
<div class="mbody">
|
||
<input type="hidden" id="wan_olt_id">
|
||
<input type="hidden" id="wan_onu_index">
|
||
<input type="hidden" id="wan_onu_sn">
|
||
|
||
<div class="fg">
|
||
<label class="fl"><%= t('admin.olt.wan_name','NAMA ONU') %></label>
|
||
<input type="text" id="wan_onu_name" class="fc" readonly>
|
||
</div>
|
||
|
||
<div class="fg">
|
||
<label class="fl"><%= t('admin.olt.wan_method','METODE KONFIGURASI') %></label>
|
||
<select id="wan_method" class="fs" onchange="onWanMethodChange()">
|
||
<option value="telnet">Telnet CLI (OLT OMCI)</option>
|
||
<option value="go-api">REST go-api-c320 (ZTE VLAN / service-port)</option>
|
||
<option value="tr069">TR069 (GenieACS)</option>
|
||
</select>
|
||
</div>
|
||
<div id="wan_goapi_hint" class="alert alert-i" style="display:none;font-size:12px;padding:10px 12px">
|
||
<i class="bi bi-info-circle"></i> Isi <strong>API Base URL</strong> di edit OLT (mis. <code>http://10.0.0.5:8081</code>). Layanan <a href="https://github.com/s4lfanet/go-api-c320" target="_blank" rel="noopener">go-api-c320</a> memetakan ke <code>POST /api/v1/vlan/onu</code> (pon_port format <code>1/slot/pon</code>). Hanya mode <strong>Bridge + VLAN</strong>; PPPoE tetap Telnet/TR069.
|
||
</div>
|
||
|
||
<div class="fg">
|
||
<label class="fl"><%= t('admin.olt.wan_mode','MODE WAN') %></label>
|
||
<select id="wan_mode" class="fs" onchange="toggleWanFields()">
|
||
<option value="Bridge">Bridge</option>
|
||
<option value="PPPoE">PPPoE</option>
|
||
</select>
|
||
</div>
|
||
|
||
<div class="fg">
|
||
<label class="fl">VLAN ID</label>
|
||
<input type="number" id="wan_vlan" class="fc" value="100" required>
|
||
</div>
|
||
<div id="wan_goapi_extra" style="display:none">
|
||
<div class="form-row">
|
||
<div class="fg"><label class="fl"><%= t('admin.olt.wan_cvlan','CVLAN (opsional)') %></label><input type="number" id="wan_cvlan" class="fc" min="1" max="4094" placeholder="<%= t('admin.olt.ph_cvlan','Default = VLAN') %>"></div>
|
||
<div class="fg"><label class="fl">Priority (0–7)</label><input type="number" id="wan_priority" class="fc" min="0" max="7" value="0"></div>
|
||
</div>
|
||
<div class="fg">
|
||
<label class="fl"><%= t('admin.olt.wan_vlan_mode','VLAN mode') %></label>
|
||
<select id="wan_vlan_mode" class="fs">
|
||
<option value="tag">tag</option>
|
||
<option value="translation">translation</option>
|
||
<option value="transparent">transparent</option>
|
||
</select>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="fg">
|
||
<label class="fl"><%= t('admin.olt.wan_bind_lan','BINDING LAN') %></label>
|
||
<div class="d-flex flex-wrap gap-3 mt-1">
|
||
<label class="d-flex align-items-center gap-1 small cursor-pointer"><input type="checkbox" name="wan_lan" value="1" checked> LAN1</label>
|
||
<label class="d-flex align-items-center gap-1 small cursor-pointer"><input type="checkbox" name="wan_lan" value="2"> LAN2</label>
|
||
<label class="d-flex align-items-center gap-1 small cursor-pointer"><input type="checkbox" name="wan_lan" value="3"> LAN3</label>
|
||
<label class="d-flex align-items-center gap-1 small cursor-pointer"><input type="checkbox" name="wan_lan" value="4"> LAN4</label>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="fg">
|
||
<label class="fl"><%= t('admin.olt.wan_bind_ssid','BINDING SSID (WIFI)') %></label>
|
||
<div class="d-flex flex-wrap gap-3 mt-1">
|
||
<label class="d-flex align-items-center gap-1 small cursor-pointer"><input type="checkbox" name="wan_ssid" value="1" checked> SSID1</label>
|
||
<label class="d-flex align-items-center gap-1 small cursor-pointer"><input type="checkbox" name="wan_ssid" value="2"> SSID2</label>
|
||
<label class="d-flex align-items-center gap-1 small cursor-pointer"><input type="checkbox" name="wan_ssid" value="3"> SSID3</label>
|
||
<label class="d-flex align-items-center gap-1 small cursor-pointer"><input type="checkbox" name="wan_ssid" value="4"> SSID4</label>
|
||
</div>
|
||
</div>
|
||
|
||
<div id="pppoe_fields" style="display: none;">
|
||
<div class="fg">
|
||
<label class="fl">PPPoE USERNAME</label>
|
||
<input type="text" id="wan_username" class="fc">
|
||
</div>
|
||
<div class="fg">
|
||
<label class="fl">PPPoE PASSWORD</label>
|
||
<input type="password" id="wan_password" class="fc">
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div class="mf">
|
||
<button type="button" class="btn btn-g" onclick="closeModal('wanConfigModal')"><%= t('common.cancel', 'Batal') %></button>
|
||
<button type="submit" class="btn btn-p"><%= t('admin.olts.edit_modal.update', 'Update Konfigurasi') %></button>
|
||
</div>
|
||
</form>
|
||
</div>
|
||
</div>
|
||
|
||
<div id="olt-data-store" data-olts='<%- JSON.stringify(olts || []).split('<').join('\\u003c') %>' style="display:none;"></div>
|
||
|
||
<script>
|
||
const I18N = {
|
||
lang: "<%= lang || 'id' %>",
|
||
confirmDeleteOlt: "<%= t('admin.olts.confirm_delete', 'Hapus OLT ini?') %>",
|
||
serverConnectFailed: "<%= t('admin.olts.errors.server_connect_failed', 'Gagal menghubungi server') %>",
|
||
noOnuData: "<%= t('admin.olts.empty_no_onu', 'Tidak ada data ONU ditemukan.') %>",
|
||
tryAgain: "<%= t('admin.olts.actions.try_again', 'Coba Lagi') %>",
|
||
renamePrompt: "<%= t('admin.olts.actions.rename_prompt', 'Masukkan nama baru untuk ONU:') %>",
|
||
renameTitle: "<%= t('admin.olts.actions.rename', 'Ubah Nama') %>",
|
||
rebootTitle: "<%= t('admin.olts.actions.reboot', 'Reboot ONU') %>",
|
||
confirmReboot: "<%= t('admin.olts.confirm_reboot', 'Yakin ingin reboot ONU ini?') %>",
|
||
rebootSent: "<%= t('admin.olts.reboot_sent', 'Perintah reboot berhasil dikirim.') %>",
|
||
failedPrefix: "<%= t('admin.olts.failed_prefix', 'Gagal') %>: ",
|
||
errorPrefix: "<%= t('admin.olts.error_prefix', 'Error') %>: ",
|
||
statusOnline: "<%= t('admin.olts.status.online', 'Online') %>",
|
||
statusOffline: "<%= t('admin.olts.status.offline', 'Offline') %>"
|
||
};
|
||
|
||
function openModal(id) { document.getElementById(id).classList.add('show'); }
|
||
function closeModal(id) { document.getElementById(id).classList.remove('show'); }
|
||
|
||
const oltsData = JSON.parse(document.getElementById('olt-data-store').dataset.olts);
|
||
|
||
function editOlt(id) {
|
||
const olt = oltsData.find(o => o.id == id);
|
||
if (!olt) return;
|
||
document.getElementById('editOltForm').action = '/admin/olts/' + olt.id + '/update';
|
||
document.getElementById('e_name').value = olt.name;
|
||
document.getElementById('e_host').value = olt.host;
|
||
document.getElementById('e_snmp_port').value = olt.snmp_port;
|
||
document.getElementById('e_brand').value = olt.brand;
|
||
document.getElementById('e_snmp_community').value = olt.snmp_community;
|
||
document.getElementById('e_web_user').value = olt.web_user || 'admin';
|
||
document.getElementById('e_web_password').value = olt.web_password || 'admin';
|
||
document.getElementById('e_telnet_port').value = olt.telnet_port != null ? olt.telnet_port : 23;
|
||
document.getElementById('e_enable_password').value = '';
|
||
document.getElementById('e_api_base_url').value = olt.api_base_url || '';
|
||
document.getElementById('e_description').value = olt.description || '';
|
||
document.getElementById('e_is_active').value = olt.is_active ? '1' : '0';
|
||
closeModal('manageOltsModal');
|
||
openModal('editOltModal');
|
||
}
|
||
|
||
function formatBytes(bytes) {
|
||
if (!bytes || bytes === 0) return '0 B';
|
||
const k = 1024;
|
||
const sizes = ['B', 'KB', 'MB', 'GB', 'TB'];
|
||
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
||
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
|
||
}
|
||
|
||
async function loadOltDashboard(id) {
|
||
if (!id) return;
|
||
|
||
const overlay = document.getElementById('loading-overlay');
|
||
overlay.style.display = 'flex';
|
||
|
||
try {
|
||
const url = (id === 'all') ? `/admin/olts/all/stats?full=true` : `/admin/olts/${id}/stats?full=true`;
|
||
const res = await fetch(url);
|
||
if (!res.ok) throw new Error(I18N.serverConnectFailed);
|
||
const stats = await res.json();
|
||
|
||
// Update Stats Cards
|
||
document.getElementById('stat-total-onu').innerText = stats.onus_total || 0;
|
||
document.getElementById('stat-online-onu').innerText = stats.onus_online || 0;
|
||
document.getElementById('stat-offline-onu').innerText = stats.onus_offline || 0;
|
||
document.getElementById('stat-weak-onu').innerText = stats.onus_weak || 0;
|
||
|
||
// Update System Info
|
||
if (stats.is_all) {
|
||
if (stats.olts_summary && stats.olts_summary.length === 1) {
|
||
const s0 = stats.olts_summary[0];
|
||
document.getElementById('stat-temp').innerText = s0.temp || stats.temp || 'N/A';
|
||
document.getElementById('stat-cpu').innerText = s0.cpu || stats.cpu || 'N/A';
|
||
document.getElementById('stat-ram').innerText = s0.ram || stats.ram || 'N/A';
|
||
document.getElementById('stat-uptime').innerText = s0.uptime || stats.uptime || 'N/A';
|
||
document.getElementById('stat-traffic-in').innerText = formatBytes(s0.uplink_rx || stats.uplink_rx || 0) + '/s';
|
||
document.getElementById('stat-traffic-out').innerText = formatBytes(s0.uplink_tx || stats.uplink_tx || 0) + '/s';
|
||
} else {
|
||
document.getElementById('stat-temp').innerText = stats.temp || ('Agregasi ' + (stats.olts_summary?.length || 0) + ' OLT');
|
||
document.getElementById('stat-cpu').innerText = stats.cpu || '-';
|
||
document.getElementById('stat-ram').innerText = stats.ram || '-';
|
||
document.getElementById('stat-uptime').innerText = stats.uptime || 'Multi-OLT Active';
|
||
document.getElementById('stat-traffic-in').innerText = formatBytes(stats.uplink_rx || 0) + '/s';
|
||
document.getElementById('stat-traffic-out').innerText = formatBytes(stats.uplink_tx || 0) + '/s';
|
||
}
|
||
} else {
|
||
document.getElementById('stat-temp').innerText = stats.temp || 'N/A';
|
||
document.getElementById('stat-cpu').innerText = stats.cpu || 'N/A';
|
||
document.getElementById('stat-ram').innerText = stats.ram || 'N/A';
|
||
document.getElementById('stat-uptime').innerText = stats.uptime || 'N/A';
|
||
document.getElementById('stat-traffic-in').innerText = formatBytes(stats.uplink_rx) + '/s';
|
||
document.getElementById('stat-traffic-out').innerText = formatBytes(stats.uplink_tx) + '/s';
|
||
}
|
||
|
||
// Render OID Info Box
|
||
const oidInfoBox = document.getElementById('oid-info-container');
|
||
const oidTableBody = document.getElementById('oid-table-body');
|
||
const oidBrandBadge = document.getElementById('oid-brand-badge');
|
||
const oidProfileName = document.getElementById('oid-profile-name');
|
||
const oidHost = document.getElementById('oid-host');
|
||
const oidPort = document.getElementById('oid-port');
|
||
const oidCommunity = document.getElementById('oid-community');
|
||
|
||
if (stats.is_all && stats.olts_summary && stats.olts_summary.length > 0) {
|
||
oidInfoBox.style.display = 'block';
|
||
oidBrandBadge.innerText = 'MULTI OLT';
|
||
oidProfileName.innerText = 'Semua OLT (' + stats.olts_summary.length + ' Perangkat)';
|
||
oidHost.innerText = 'Multi-Host';
|
||
oidPort.innerText = '161';
|
||
oidCommunity.innerText = 'Various';
|
||
|
||
let rowsHtml = '';
|
||
stats.olts_summary.forEach((oSummary, idx) => {
|
||
const prof = oSummary.activeProfile;
|
||
const brand = (oSummary.brand || 'N/A').toUpperCase();
|
||
rowsHtml += `
|
||
<tr style="border-top:2px solid #334155; background:rgba(255,255,255,0.03);">
|
||
<td colspan="2" style="color:#38bdf8; font-weight:bold; padding-top:8px;">OLT #${idx+1}: ${oSummary.name} (${oSummary.host}) - Brand: ${brand} [${oSummary.status}]</td>
|
||
</tr>
|
||
<tr><td><%= t('admin.olt.oid_active','Profil SNMP Aktif') %></td><td><code>${prof ? prof.name : (oSummary.error || 'Pending/No Match')}</code></td></tr>
|
||
<tr><td>OID Status Table</td><td><code>${prof ? prof.status_table : '-'}</code></td></tr>
|
||
<tr><td>OID Name Table</td><td><code>${prof ? prof.name_table : '-'}</code></td></tr>
|
||
<tr><td>OID Serial Table</td><td><code>${prof ? prof.sn_table : '-'}</code></td></tr>
|
||
<tr><td>OID RX Power Table</td><td><code>${prof ? (prof.rx_power_table || '-') : '-'}</code></td></tr>
|
||
`;
|
||
});
|
||
oidTableBody.innerHTML = rowsHtml;
|
||
|
||
} else if (stats.activeProfile) {
|
||
oidInfoBox.style.display = 'block';
|
||
const prof = stats.activeProfile;
|
||
const sysOids = stats.systemOids || {};
|
||
const cfg = stats.snmpConfig || {};
|
||
|
||
oidBrandBadge.innerText = (cfg.brand || stats.brand || 'HIOSO').toUpperCase();
|
||
oidProfileName.innerText = prof.name || 'Custom SNMP';
|
||
oidHost.innerText = cfg.host || stats.host || '-';
|
||
oidPort.innerText = cfg.port || '161';
|
||
oidCommunity.innerText = cfg.community || 'public';
|
||
|
||
oidTableBody.innerHTML = `
|
||
<tr><td style="color:#94a3b8">OID Probe Autodeteksi</td><td><code style="color:#4ade80">${prof.probe_oid || '-'}</code></td></tr>
|
||
<tr><td style="color:#94a3b8">OID Tabel Status ONU</td><td><code>${prof.status_table || '-'}</code></td></tr>
|
||
<tr><td style="color:#94a3b8">OID Tabel Nama ONU</td><td><code>${prof.name_table || '-'}</code></td></tr>
|
||
<tr><td style="color:#94a3b8">OID Tabel Serial Number (SN)</td><td><code>${prof.sn_table || '-'}</code></td></tr>
|
||
<tr><td style="color:#94a3b8">OID Tabel Redaman RX Power</td><td><code>${prof.rx_power_table || '-'}</code></td></tr>
|
||
<tr><td style="color:#94a3b8">OID Tabel Redaman TX Power</td><td><code>${prof.tx_power_table || '-'}</code></td></tr>
|
||
<tr><td style="color:#94a3b8">OID Tabel Jarak ONU (Distance)</td><td><code>${prof.distance_table || '-'}</code></td></tr>
|
||
<tr><td style="color:#94a3b8">OID Tabel Alasan Offline</td><td><code>${prof.offline_reason_table || '-'}</code></td></tr>
|
||
<tr><td style="color:#94a3b8">OID Suhu / CPU / RAM System</td><td><code>${sysOids.temp || '-' } / ${sysOids.cpu || '-'} / ${sysOids.ram || '-'}</code></td></tr>
|
||
<tr><td style="color:#94a3b8">OID Traffic Uplink In / Out</td><td><code>${sysOids.uplink_rx || '-'} / ${sysOids.uplink_tx || '-'}</code></td></tr>
|
||
`;
|
||
} else {
|
||
oidInfoBox.style.display = 'none';
|
||
}
|
||
|
||
// Render Card List
|
||
const cardContainer = document.getElementById('card-list-container');
|
||
const cardBody = document.getElementById('card-table-body');
|
||
if (stats.cards && stats.cards.length > 0) {
|
||
cardContainer.style.display = 'block';
|
||
cardBody.innerHTML = stats.cards.map(card => {
|
||
const statusClass = card.status === 'INSERVICE' || card.status === 'ONLINE' ? 'pill-online' : 'pill-offline';
|
||
return `<tr>
|
||
<td><strong>${card.index}</strong></td>
|
||
<td><span class="badge-v2">${card.type}</span></td>
|
||
<td><span class="status-pill-v2 ${statusClass}">${card.status}</span></td>
|
||
<td>${card.ports}</td>
|
||
<td><code class="sn-mac">${formatSn(card.serial)}</code></td>
|
||
<td>${card.cpu}</td>
|
||
<td>${card.ram}</td>
|
||
</tr>`;
|
||
}).join('');
|
||
} else {
|
||
cardContainer.style.display = 'none';
|
||
}
|
||
|
||
// Render Unauth List
|
||
const unauthContainer = document.getElementById('unauth-list-container');
|
||
const unauthBody = document.getElementById('unauth-table-body');
|
||
if (stats.unauth_onus && stats.unauth_onus.length > 0) {
|
||
unauthContainer.style.display = 'block';
|
||
unauthBody.innerHTML = stats.unauth_onus.map(onu => {
|
||
const targetOltId = onu.olt_id || id;
|
||
return `<tr>
|
||
<td><code class="sn-mac">${onu.id || onu.index}</code></td>
|
||
<td><code class="sn-mac">${formatSn(onu.sn)}</code> ${onu.olt_name ? `<span class="badge bg-secondary ms-1">${onu.olt_name}</span>` : ''}</td>
|
||
<td><span class="badge-v2">${onu.type}</span></td>
|
||
<td>
|
||
<button class="btn btn-sm btn-p" onclick="authorizeOnuPrompt('${targetOltId}', '${onu.index}', '${onu.sn}')">
|
||
<i class="bi bi-check-circle"></i> Otorisasi
|
||
</button>
|
||
</td>
|
||
</tr>`;
|
||
}).join('');
|
||
} else {
|
||
unauthContainer.style.display = 'none';
|
||
}
|
||
|
||
// Render Table
|
||
const tbody = document.getElementById('onu-table-body');
|
||
if (stats.onus && stats.onus.length > 0) {
|
||
document.getElementById('search-container').style.display = 'block';
|
||
tbody.innerHTML = stats.onus.map((onu, index) => {
|
||
const statusVal = String(onu.status || '');
|
||
const isOnline = statusVal.toLowerCase() === 'online';
|
||
const statusClass = isOnline ? 'pill-online' : 'pill-offline';
|
||
const rxVal = parseFloat(onu.rx);
|
||
let rxClass = 'power-good';
|
||
if (!Number.isFinite(rxVal)) rxClass = '';
|
||
else if (rxVal < -27) rxClass = 'power-danger';
|
||
else if (rxVal < -24) rxClass = 'power-warning';
|
||
const targetOltId = onu.olt_id || id;
|
||
const oltBadge = onu.olt_name ? `<span class="badge bg-dark border border-secondary text-info me-1" style="font-size:10px">${onu.olt_name}</span>` : '';
|
||
|
||
const custBadge = onu.customer_name ? `<div class="mt-1" style="font-size:11px;color:#2563eb;font-weight:600;"><i class="bi bi-person-fill"></i> ${onu.customer_name}</div>` : '';
|
||
|
||
return `<tr>
|
||
<td>${index + 1}</td>
|
||
<td>${oltBadge}<code class="sn-mac">${onu.id || '-'}</code></td>
|
||
<td>
|
||
<strong>${onu.name || '-'}</strong>
|
||
${custBadge}
|
||
</td>
|
||
<td><code class="sn-mac">${formatSn(onu.sn) || '-'}</code></td>
|
||
<td>
|
||
<span class="status-pill-v2 ${statusClass}"><i class="bi bi-circle-fill" style="font-size:6px;"></i> ${formatStatus(statusVal) || '-'}</span>
|
||
${!isOnline && onu.offline_reason ? `<div class="text-danger small mt-1" style="font-size: 11px; font-weight: 500;"><i class="bi bi-exclamation-octagon"></i> ${onu.offline_reason}</div>` : ''}
|
||
</td>
|
||
<td><span class="power-badge">${onu.tx || 'N/A'}</span></td>
|
||
<td><span class="power-badge ${rxClass}">${onu.rx || 'N/A'}</span></td>
|
||
<td><span class="small text-muted">${onu.distance != null ? onu.distance : '-'}</span></td>
|
||
<td style="text-align:right">
|
||
<div class="d-flex justify-content-end gap-1">
|
||
<button class="btn btn-sm btn-outline-primary py-0 px-2" onclick="configureWanPrompt('${targetOltId}', '${onu.index}', '${onu.name}', '${onu.sn}')" style="font-size: 11px;">
|
||
<i class="bi bi-gear-fill"></i> WAN
|
||
</button>
|
||
<button class="btn-action-v2 btn-rename-v2" onclick="renameOnuPrompt('${targetOltId}', '${onu.index}', '${onu.name}')" title="${I18N.renameTitle}"><i class="bi bi-pencil-square"></i></button>
|
||
<button class="btn-action-v2 btn-reboot-v2" onclick="rebootOnu('${targetOltId}', '${onu.index}')" title="${I18N.rebootTitle}"><i class="bi bi-arrow-clockwise"></i></button>
|
||
</div>
|
||
</td>
|
||
</tr>`;
|
||
}).join('');
|
||
} else {
|
||
document.getElementById('search-container').style.display = 'none';
|
||
tbody.innerHTML = `<tr><td colspan="9">
|
||
<div class="empty-state">
|
||
<i class="bi bi-exclamation-triangle"></i>
|
||
<p>${stats.error || I18N.noOnuData}</p>
|
||
${stats.error ? '<button class="btn btn-sm btn-p mt-2" onclick="refreshCurrentOlt()">' + I18N.tryAgain + '</button>' : ''}
|
||
</div>
|
||
</td></tr>`;
|
||
}
|
||
} catch (e) {
|
||
document.getElementById('onu-table-body').innerHTML = `<tr><td colspan="9" class="text-center p-5 text-danger">${I18N.errorPrefix}${e.message}</td></tr>`;
|
||
} finally {
|
||
overlay.style.display = 'none';
|
||
}
|
||
}
|
||
|
||
function formatSn(sn) {
|
||
if (!sn || typeof sn !== 'string') return sn || '-';
|
||
const raw = sn.trim();
|
||
if (!raw || raw === '-' || raw === 'N/A') return raw;
|
||
const clean = raw.replace(/[^a-zA-Z0-9]/g, '').toUpperCase();
|
||
// Jika 12 karakter hex (MAC address format MikroTik, misal 2C334187A820 -> 2C:33:41:87:A8:20)
|
||
if (/^[0-9A-F]{12}$/.test(clean)) {
|
||
return clean.match(/.{1,2}/g).join(':');
|
||
}
|
||
// Jika 16 karakter hex (8 bytes GPON hex)
|
||
if (/^[0-9A-F]{16}$/.test(clean)) {
|
||
return clean.match(/.{1,2}/g).join(':');
|
||
}
|
||
return raw;
|
||
}
|
||
|
||
function formatStatus(statusVal) {
|
||
const v = String(statusVal || '').trim();
|
||
if (!v) return '';
|
||
if (v.toLowerCase() === 'online') return I18N.statusOnline;
|
||
if (v.toLowerCase() === 'offline') return I18N.statusOffline;
|
||
return v;
|
||
}
|
||
|
||
function handleOltChange() {
|
||
const id = document.getElementById('olt-selector').value;
|
||
if (id) loadOltDashboard(id);
|
||
}
|
||
|
||
function refreshCurrentOlt() {
|
||
handleOltChange();
|
||
}
|
||
|
||
function filterOnuTable() {
|
||
const input = document.getElementById('onu-search');
|
||
const filter = input.value.toLowerCase().trim();
|
||
const filterClean = filter.replace(/[:\-\.]/g, '');
|
||
const rows = document.querySelectorAll('#onu-table-body tr');
|
||
|
||
rows.forEach(row => {
|
||
const text = row.innerText.toLowerCase();
|
||
const textClean = text.replace(/[:\-\.]/g, '');
|
||
row.style.display = (text.includes(filter) || (filterClean && textClean.includes(filterClean))) ? '' : 'none';
|
||
});
|
||
}
|
||
|
||
async function renameOnuPrompt(oltId, index, oldName) {
|
||
const newName = prompt(I18N.renamePrompt, oldName);
|
||
if (!newName || newName === oldName) return;
|
||
try {
|
||
const res = await fetch(`/admin/olts/${oltId}/onu/${index}/rename`, {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
||
body: new URLSearchParams({ name: newName })
|
||
});
|
||
const data = await res.json();
|
||
if (data.success) { refreshCurrentOlt(); } else { alert(I18N.failedPrefix + data.error); }
|
||
} catch (e) { alert(I18N.errorPrefix + e.message); }
|
||
}
|
||
|
||
async function rebootOnu(oltId, index) {
|
||
if (!confirm(I18N.confirmReboot)) return;
|
||
try {
|
||
const res = await fetch(`/admin/olts/${oltId}/onu/${index}/reboot`, { method: 'POST' });
|
||
const data = await res.json();
|
||
if (data.success) { alert(I18N.rebootSent); } else { alert(I18N.failedPrefix + data.error); }
|
||
} catch (e) { alert(I18N.errorPrefix + e.message); }
|
||
}
|
||
|
||
async function authorizeOnuPrompt(oltId, index, sn) {
|
||
const name = prompt('Masukkan Nama Pelanggan:', '');
|
||
if (!name) return;
|
||
const vlan = prompt('Masukkan VLAN ID:', '100');
|
||
if (!vlan) return;
|
||
|
||
const overlay = document.getElementById('loading-overlay');
|
||
overlay.style.display = 'flex';
|
||
|
||
try {
|
||
const res = await fetch(`/admin/olts/${oltId}/onu/authorize`, {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
||
body: new URLSearchParams({ index, sn, name, vlan })
|
||
});
|
||
const data = await res.json();
|
||
if (data.success) {
|
||
alert('ONU berhasil diotorisasi!');
|
||
refreshCurrentOlt();
|
||
} else {
|
||
alert(I18N.failedPrefix + data.error);
|
||
}
|
||
} catch (e) {
|
||
alert(I18N.errorPrefix + e.message);
|
||
} finally {
|
||
overlay.style.display = 'none';
|
||
}
|
||
}
|
||
|
||
function configureWanPrompt(oltId, index, name, sn) {
|
||
document.getElementById('wan_olt_id').value = oltId;
|
||
document.getElementById('wan_onu_index').value = index;
|
||
document.getElementById('wan_onu_name').value = name;
|
||
document.getElementById('wan_onu_sn').value = sn;
|
||
document.getElementById('wan_method').value = 'telnet';
|
||
onWanMethodChange();
|
||
openModal('wanConfigModal');
|
||
}
|
||
|
||
function onWanMethodChange() {
|
||
const m = document.getElementById('wan_method').value;
|
||
const hint = document.getElementById('wan_goapi_hint');
|
||
const extra = document.getElementById('wan_goapi_extra');
|
||
const modeSel = document.getElementById('wan_mode');
|
||
if (hint) hint.style.display = m === 'go-api' ? 'block' : 'none';
|
||
if (extra) extra.style.display = m === 'go-api' ? 'block' : 'none';
|
||
if (m === 'go-api' && modeSel) {
|
||
modeSel.value = 'Bridge';
|
||
toggleWanFields();
|
||
}
|
||
}
|
||
|
||
function toggleWanFields() {
|
||
const mode = document.getElementById('wan_mode').value;
|
||
document.getElementById('pppoe_fields').style.display = mode === 'PPPoE' ? 'block' : 'none';
|
||
}
|
||
|
||
async function submitWanConfig(e) {
|
||
e.preventDefault();
|
||
const oltId = document.getElementById('wan_olt_id').value;
|
||
const method = document.getElementById('wan_method').value;
|
||
const mode = document.getElementById('wan_mode').value;
|
||
if (method === 'go-api' && mode !== 'Bridge') {
|
||
alert('Metode go-api-c320 pada integrasi ini hanya untuk mode Bridge + VLAN. Pilih Bridge atau ganti metode.');
|
||
return;
|
||
}
|
||
const lans = Array.from(document.querySelectorAll('input[name="wan_lan"]:checked')).map(cb => cb.value);
|
||
const ssids = Array.from(document.querySelectorAll('input[name="wan_ssid"]:checked')).map(cb => cb.value);
|
||
|
||
const payload = {
|
||
index: document.getElementById('wan_onu_index').value,
|
||
sn: document.getElementById('wan_onu_sn').value,
|
||
method,
|
||
mode,
|
||
vlan: document.getElementById('wan_vlan').value,
|
||
username: document.getElementById('wan_username').value,
|
||
password: document.getElementById('wan_password').value,
|
||
lans: lans.join(','),
|
||
ssids: ssids.join(',')
|
||
};
|
||
if (method === 'go-api') {
|
||
payload.cvlan = document.getElementById('wan_cvlan').value;
|
||
payload.vlan_mode = document.getElementById('wan_vlan_mode').value;
|
||
payload.priority = document.getElementById('wan_priority').value;
|
||
}
|
||
|
||
const overlay = document.getElementById('loading-overlay');
|
||
overlay.style.display = 'flex';
|
||
|
||
try {
|
||
const res = await fetch(`/admin/olts/${oltId}/onu/configure-wan`, {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
||
body: new URLSearchParams(payload)
|
||
});
|
||
const data = await res.json();
|
||
if (data.success) {
|
||
alert('Konfigurasi WAN berhasil diterapkan!');
|
||
closeModal('wanConfigModal');
|
||
refreshCurrentOlt();
|
||
} else {
|
||
alert(I18N.failedPrefix + data.error);
|
||
}
|
||
} catch (e) {
|
||
alert(I18N.errorPrefix + e.message);
|
||
} finally {
|
||
overlay.style.display = 'none';
|
||
}
|
||
}
|
||
|
||
async function testExistingOlt(id) {
|
||
if (!confirm('Uji koneksi SNMP & Telnet ke OLT ini?')) return;
|
||
const overlay = document.getElementById('loading-overlay');
|
||
if (overlay) overlay.style.display = 'flex';
|
||
try {
|
||
const res = await fetch(`/admin/olts/${id}/test-connection`, { method: 'POST' });
|
||
const data = await res.json();
|
||
alert((data.success ? ' ' : ' ') + (data.message || 'Hasil tes koneksi selesai.'));
|
||
} catch (e) {
|
||
alert(' Error: ' + e.message);
|
||
} finally {
|
||
if (overlay) overlay.style.display = 'none';
|
||
}
|
||
}
|
||
|
||
async function testFormOlt(type) {
|
||
const isEdit = type === 'edit';
|
||
const resultDiv = document.getElementById(isEdit ? 'edit-test-result' : 'add-test-result');
|
||
|
||
const form = isEdit ? document.getElementById('editOltForm') : document.querySelector('#addOltModal form');
|
||
const host = isEdit ? document.getElementById('e_host').value : form.querySelector('input[name="host"]').value;
|
||
const snmp_port = isEdit ? document.getElementById('e_snmp_port').value : form.querySelector('input[name="snmp_port"]').value;
|
||
const snmp_community = isEdit ? document.getElementById('e_snmp_community').value : form.querySelector('input[name="snmp_community"]').value;
|
||
const brand = isEdit ? document.getElementById('e_brand').value : form.querySelector('select[name="brand"]').value;
|
||
const web_user = isEdit ? document.getElementById('e_web_user').value : form.querySelector('input[name="web_user"]').value;
|
||
const web_password = isEdit ? document.getElementById('e_web_password').value : form.querySelector('input[name="web_password"]').value;
|
||
const telnet_port = isEdit ? document.getElementById('e_telnet_port').value : form.querySelector('input[name="telnet_port"]').value;
|
||
const enable_password = isEdit ? document.getElementById('e_enable_password').value : form.querySelector('input[name="enable_password"]').value;
|
||
|
||
if (!host) {
|
||
alert('Harap isi IP Address / Host OLT terlebih dahulu.');
|
||
return;
|
||
}
|
||
|
||
resultDiv.style.display = 'block';
|
||
resultDiv.className = 'alert alert-i small mt-2';
|
||
resultDiv.innerHTML = '<i class="bi bi-hourglass-split spin"></i> Menguji koneksi SNMP & Telnet ke ' + host + '...';
|
||
|
||
try {
|
||
const res = await fetch('/admin/olts/test-connection', {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({
|
||
host, snmp_port, snmp_community, brand, web_user, web_password, telnet_port, enable_password
|
||
})
|
||
});
|
||
const data = await res.json();
|
||
if (data.success) {
|
||
resultDiv.className = 'alert alert-s small mt-2';
|
||
resultDiv.innerHTML = '<i class="bi bi-check-circle-fill"></i> <strong><%= t('admin.olt.conn_ok','Koneksi Sukses!') %></strong><br>' + (data.message || 'OLT merespons.');
|
||
} else {
|
||
resultDiv.className = 'alert alert-d small mt-2';
|
||
resultDiv.innerHTML = '<i class="bi bi-exclamation-triangle-fill"></i> <strong><%= t('admin.olt.conn_fail','Koneksi Gagal:') %></strong><br>' + (data.message || data.error);
|
||
}
|
||
} catch (e) {
|
||
resultDiv.className = 'alert alert-d small mt-2';
|
||
resultDiv.innerHTML = '<i class="bi bi-x-circle-fill"></i> <strong><%= t('common.error_colon','Error:') %></strong> ' + e.message;
|
||
}
|
||
}
|
||
|
||
document.addEventListener('DOMContentLoaded', () => {
|
||
const selector = document.getElementById('olt-selector');
|
||
if (selector && selector.value) {
|
||
handleOltChange();
|
||
}
|
||
});
|
||
</script>
|
||
</body>
|
||
</html>
|