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

1186 lines
58 KiB
Plaintext
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!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')">&times;</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')">&times;</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')">&times;</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')">&times;</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>