Files

1093 lines
61 KiB
Plaintext

<!DOCTYPE html>
<html lang="<%= (typeof lang !== 'undefined' ? lang : 'id') %>">
<head>
<meta charset="UTF-8"><meta name="viewport" content="width=device-width,initial-scale=1">
<title>GenieACS Pro | <%= company %></title>
<link rel="stylesheet" href="/vendor/bootstrap-icons/bootstrap-icons.min.css">
<link rel="stylesheet" href="/css/admin.css">
<style>
.stats-grid { display:grid; grid-template-columns:repeat(auto-fit,minmax(200px,1fr)); gap:20px; margin-bottom:30px; }
.stat-card { background:var(--card-bg); border:1px solid var(--border); border-radius:18px; padding:24px; display:flex; align-items:center; gap:20px; cursor:pointer; transition:all 0.3s; position:relative; overflow:hidden; }
.stat-card:hover { transform:translateY(-5px); border-color:var(--primary); background:var(--sidebar-bg); }
.stat-card::after { content:''; position:absolute; left:0; top:0; height:100%; width:4px; background:var(--card-color); }
.stat-icon { width:52px; height:52px; border-radius:12px; display:flex; align-items:center; justify-content:center; font-size:24px; background:var(--sub-bg); color:var(--card-color); }
.stat-info h3 { font-size:28px; font-weight:800; margin:0; line-height:1; }
.stat-info p { font-size:13px; color:var(--muted); margin:8px 0 0; }
.acs-box { background:var(--card-bg); border:1px solid var(--border); border-radius:22px; overflow:hidden; }
.tab-bar { display:flex; background:var(--sub-bg); border-bottom:1px solid var(--border); }
.tab-bar button { padding:18px 25px; background:none; border:none; color:var(--muted); font-weight:600; cursor:pointer; transition:all 0.3s; border-bottom:3px solid transparent; display:flex; align-items:center; gap:8px; font-size:14px; }
.tab-bar button.active { color:var(--primary); border-bottom-color:var(--primary); background:rgba(59,130,246,0.05); }
.tab-pane { padding:30px; display:none; animation:fadeIn 0.4s ease; }
.tab-pane.active { display:block; }
@keyframes fadeIn { from { opacity:0; transform:translateY(10px); } to { opacity:1; transform:translateY(0); } }
.table-container { width:100%; overflow-x:auto; }
table { width:100%; border-collapse:collapse; }
th { background:var(--sub-bg); color:var(--muted); font-weight:600; font-size:11px; text-transform:uppercase; letter-spacing:1px; padding:15px 20px; text-align:left; }
td { padding:15px 20px; border-bottom:1px solid var(--border); font-size:13px; color:var(--text); vertical-align:middle; }
tr:hover td { background:var(--hover-bg); }
.status-badge { padding:6px 12px; border-radius:20px; font-size:11px; font-weight:700; display:inline-flex; align-items:center; gap:6px; }
.status-up { background:rgba(16,185,129,0.1); color:#10B981; border:1px solid rgba(16,185,129,0.2); }
.status-down { background:rgba(239,68,68,0.1); color:#EF4444; border:1px solid rgba(239,68,68,0.2); }
.btn-action { width:32px; height:32px; border-radius:8px; display:flex; align-items:center; justify-content:center; border:1px solid var(--border); background:var(--sub-bg); color:var(--muted); cursor:pointer; transition:all 0.2s; }
.btn-action:hover { background:var(--primary); color:white; border-color:var(--primary); transform:scale(1.1); }
.btn-action.danger:hover { background:var(--danger); border-color:var(--danger); }
.btn-action:disabled { opacity:0.5; cursor:not-allowed; }
.btn-action:disabled:hover { background:var(--sub-bg); color:var(--muted); border-color:var(--border); transform:none; }
.acs-server-tag { display:inline-flex; align-items:center; gap:6px; padding:4px 10px; border-radius:6px; font-size:11px; font-weight:600; background:rgba(59,130,246,0.1); color:var(--primary); border:1px solid rgba(59,130,246,0.2); }
.acs-server-tag.offline { background:rgba(239,68,68,0.1); color:#EF4444; border-color:rgba(239,68,68,0.2); }
.modal-overlay { position:fixed; inset:0; background:var(--overlay-bg); backdrop-filter:blur(6px); display:none; align-items:center; justify-content:center; z-index:2000; padding:20px; }
.modal-card { background:var(--sidebar-bg); border:1px solid var(--border); border-radius:24px; width:100%; max-width:500px; padding:30px; }
.device-link { text-decoration: none; display: block; transition: all 0.2s; }
.device-link:hover { opacity: 0.8; }
.device-link:hover div:first-child { text-decoration: underline; }
.bind-tag { display: flex; align-items: center; justify-content: center; gap: 6px; padding: 10px; border: 1px solid var(--border); border-radius: 10px; background: var(--card-bg); font-size: 12px; font-weight: 700; cursor: pointer; text-align: center; transition: all 0.2s; color: var(--text); }
.bind-tag:hover { border-color: var(--primary); background: var(--hover-bg); }
.bind-tag.active { background: var(--primary) !important; color: white !important; border-color: var(--primary) !important; box-shadow: 0 4px 12px rgba(59,130,246,0.2); }
</style>
</head>
<body>
<%- include('partials/sidebar', { activePage: 'acs_pro', company }) %>
<div class="mw">
<div class="topbar">
<div class="tb-title"><button class="hb-menu" onclick="toggleSidebar()"><i class="bi bi-list"></i></button><i class="bi bi-hdd-network"></i> <%= t('admin.acs.title','GenieACS Monitoring Pro') %></div>
<div class="tb-right">
<span class="adm-badge" style="font-size:12px;opacity:0.7;"><%= company %></span>
<span class="adm-badge"><i class="bi bi-person-fill"></i> <%= (typeof session !== 'undefined' && session && session.isCashier) ? 'Kasir' : 'Admin' %></span>
<a href="/admin/logout" class="btn btn-g btn-sm"><i class="bi bi-box-arrow-left"></i></a>
</div>
</div>
<div class="page">
<div class="acs-head" style="display:flex; justify-content:space-between; align-items:center; margin-bottom:22px;">
<h1 class="page-title" style="margin:0;"><%= t('admin.acs.monitoring','GenieACS Monitoring') %></h1>
<div style="display:flex; gap:8px;">
<button class="btn btn-g" onclick="syncOnuTags()" title="<%= t('admin.acs.sync_title','Sinkron Tag ONU pelanggan dari device.id ACS (berdasarkan PPPoE user)') %>">
<i class="bi bi-tags"></i> Sync Tag ONU
</button>
<button class="btn btn-p" onclick="syncAllACS()">
<i class="bi bi-arrow-repeat"></i> Sync All ACS
</button>
</div>
</div>
<div class="stats-grid">
<div class="stat-card" style="--card-color: var(--primary)">
<div class="stat-icon"><i class="bi bi-cpu"></i></div>
<div class="stat-info">
<h3><%= devices.length %></h3>
<p><%= t('admin.acs.total_devices','Total Devices') %></p>
</div>
</div>
<div class="stat-card" style="--card-color: #10B981">
<div class="stat-icon"><i class="bi bi-wifi"></i></div>
<div class="stat-info">
<h3><%= devices.filter(d => d.isOnline).length %></h3>
<p>Online</p>
</div>
</div>
</div>
<div class="acs-box">
<div class="tab-bar">
<button class="active" onclick="switchTab('devices', this)"><i class="bi bi-display"></i> <%= t('admin.acs.tab_devices','Perangkat') %></button>
<button onclick="switchTab('servers', this)"><i class="bi bi-hdd-rack"></i> ACS Servers</button>
<button onclick="switchTab('logs', this)"><i class="bi bi-clipboard-data"></i> Log</button>
</div>
<div id="tab-devices" class="tab-pane active">
<form action="/admin/acs" method="GET" class="srow">
<div class="sinput" style="max-width:340px">
<i class="bi bi-search"></i>
<input class="fc" name="q" placeholder='<%= t("admin.acs.search_ph","Cari SN, Pelanggan, PPPoE...") %>' value="<%= (typeof searchQuery !== 'undefined' && searchQuery) ? searchQuery : '' %>">
</div>
<select class="fs" id="acsFilter" name="acs" style="width:190px" onchange="this.form.submit()">
<option value="all"><%= t('admin.acs.all_servers','All ACS Servers') %></option>
<% acsServers.forEach(s => { %>
<option value="<%= s.id %>" <%= String(selectedAcsId) === String(s.id) ? 'selected' : '' %>><%= s.name %></option>
<% }); %>
</select>
<select class="fs" name="dev_status" style="width:150px" onchange="this.form.submit()">
<option value=""><%= t('admin.acs.all_status','Semua Status') %></option>
<option value="online" <%= (typeof filterDevStatus!=='undefined' && filterDevStatus==='online')?'selected':'' %>>Online</option>
<option value="offline" <%= (typeof filterDevStatus!=='undefined' && filterDevStatus==='offline')?'selected':'' %>>Offline</option>
</select>
<select class="fs" name="vendor" style="width:150px" onchange="this.form.submit()">
<option value=""><%= t('admin.acs.all_vendor','Semua Vendor') %></option>
<% (typeof vendors!=='undefined'?vendors:[]).forEach(v => { %><option value="<%= v %>" <%= (typeof filterVendor!=='undefined' && filterVendor===v)?'selected':'' %>><%= v %></option><% }) %>
</select>
<button class="btn btn-g btn-sm" type="submit"><i class="bi bi-search"></i> Search</button>
<% if ((typeof searchQuery!=='undefined' && searchQuery) || (typeof filterDevStatus!=='undefined' && filterDevStatus) || (typeof filterVendor!=='undefined' && filterVendor)) { %>
<a href="/admin/acs" class="btn btn-g btn-sm"><i class="bi bi-x"></i> Reset</a>
<% } %>
</form>
<!-- Bulk Actions Panel -->
<div id="bulkActionsPanel" style="display: none; background: rgba(59,130,246,0.05); border: 1px solid rgba(59,130,246,0.15); padding: 12px 20px; border-radius: 12px; margin-bottom: 20px; align-items: center; justify-content: space-between;">
<div style="font-size: 13px; font-weight: 600; color: var(--text);">
<span id="selectedCount">0</span> perangkat terpilih
</div>
<div style="display: flex; gap: 10px;">
<button class="btn btn-p btn-sm" onclick="bulkSummon()" style="background: var(--primary);"><i class="bi bi-arrow-repeat"></i> Summon Terpilih</button>
<button class="btn btn-d btn-sm" onclick="bulkDelete()" style="background: var(--danger);"><i class="bi bi-trash"></i> Hapus Terpilih</button>
</div>
</div>
<div class="table-container">
<table>
<thead>
<tr>
<th style="width: 40px;"><input type="checkbox" id="selectAll<%= t('admin.acs.tab_devices','Perangkat') %>" onchange="toggleSelectAll(this)"></th>
<th><%= t('admin.acs.device_id','Device ID / SN') %></th>
<th><%= t('admin.acs.customer_name','Customer Name') %></th>
<th>IP Address</th>
<th>RX Power</th>
<th>PPPoE User</th>
<th>ACS Server</th>
<th><%= t('common.status','Status') %></th>
<th><%= t('admin.acs.action','Aksi') %></th>
</tr>
</thead>
<tbody>
<% devices.forEach(d => { %>
<tr>
<td>
<input type="checkbox" class="device-checkbox" data-id="<%= d.id %>" data-acs="<%= d.acs_server_id || selectedAcsId %>" onchange="updateBulkPanel()">
</td>
<td>
<a href="/admin/acs/device/<%= encodeURIComponent(d.id) %>?acsId=<%= d.acs_server_id || selectedAcsId %>" class="device-link">
<div style="font-weight:700; color:var(--primary);"><%= d.sn %></div>
<div style="font-size:10px; color:var(--muted);"><%= d.id %></div>
</a>
</td>
<td>
<% if (d.customer_name && d.customer_name !== '-') { %>
<div style="font-weight:700; color:var(--text);"><i class="bi bi-person-fill text-primary me-1"></i><%= d.customer_name %></div>
<% } else { %>
<span class="text-muted" style="font-size:12px;">-</span>
<% } %>
</td>
<td><code><%= d.ip || d.pppoe_ip || '-' %></code></td>
<td><%= d.rx_power %> dBm</td>
<td><%= d.pppoe_user %></td>
<td><span class="acs-server-tag"><%= d.acs_server_name %></span></td>
<td>
<span class="status-badge <%= d.isOnline ? 'status-up' : 'status-down' %>">
<%= d.isOnline ? 'Online' : 'Offline' %>
</span>
</td>
<td style="white-space:nowrap">
<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="refreshDevice('<%= d.id %>')"><i class="bi bi-arrow-repeat"></i> Refresh Device</button>
<button type="button" class="act-item" onclick="openAddWanModal('<%= d.id %>', '<%= d.acs_server_id || selectedAcsId %>', '<%= d.sn %>', '<%= d.manufacturer || "" %>', '<%= d.model || "" %>')"><i class="bi bi-diagram-3"></i> Add WAN (All-in-One)</button>
<div class="act-sep"></div>
<button type="button" class="act-item danger" onclick="deleteDevice('<%= d.id %>', '<%= d.acs_server_id || selectedAcsId %>')"><i class="bi bi-trash"></i> <%= t('admin.acs.del_device','Hapus Perangkat') %></button>
</div>
</div>
</td>
</tr>
<% }); %>
</tbody>
</table>
</div>
</div>
<div id="tab-servers" class="tab-pane">
<div style="display:flex; justify-content:space-between; margin-bottom:20px;">
<h3>ACS Servers</h3>
<button class="btn btn-p btn-sm" onclick="openAddServerModal()"><i class="bi bi-plus-lg"></i> <%= t('admin.acs.add_server','Tambah Server ACS') %></button>
</div>
<div class="table-container">
<table>
<thead>
<tr>
<th><%= t('admin.acs.name','Name') %></th>
<th>URL</th>
<th><%= t('admin.acs.location','Location') %></th>
<th><%= t('admin.acs.devices','Devices') %></th>
<th><%= t('admin.acs.action','Aksi') %></th>
</tr>
</thead>
<tbody>
<% acsServers.forEach(s => { %>
<tr>
<td><%= s.name %></td>
<td><code><%= s.url %></code></td>
<td><%= s.location %></td>
<td><%= s.device_count %></td>
<td>
<% if (String(s.id) !== 'legacy') { %>
<div class="act" style="display:inline-block">
<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="openEditServerModal(this)" data-id="<%= s.id %>" data-name="<%= s.name %>" data-url="<%= s.url %>" data-username="<%= s.username || '' %>" data-location="<%= s.location || '' %>"><i class="bi bi-pencil-square"></i> Edit Server</button>
<div class="act-sep"></div>
<button type="button" class="act-item danger" onclick="deleteServer('<%= s.id %>')"><i class="bi bi-trash"></i> <%= t('admin.acs.del_server','Hapus Server') %></button>
</div>
</div>
<% } else { %>
<div class="act" style="display:inline-block">
<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="openEditLegacyAcsModal()"><i class="bi bi-pencil-square"></i> <%= t('admin.acs.edit_default','Edit Default ACS') %></button>
<button type="button" class="act-item danger" disabled style="opacity:.5;cursor:not-allowed"><i class="bi bi-trash"></i> Hapus (tidak bisa)</button>
</div>
</div>
<% } %>
</td>
</tr>
<% }); %>
</tbody>
</table>
</div>
</div>
<div id="tab-logs" class="tab-pane">
<div style="display:flex; gap:10px; align-items:center; flex-wrap:wrap; margin-bottom:16px;">
<h3 style="margin:0;">Log Aktivitas ACS</h3>
<select class="fs" id="logServerFilter" style="width:220px;" onchange="loadAcsLogs()">
<option value="all"><%= t('admin.acs.all_server','Semua Server') %></option>
<% acsServers.forEach(s => { %>
<option value="<%= s.id %>"><%= s.name %></option>
<% }); %>
</select>
<label style="display:flex; gap:6px; align-items:center; font-size:13px; color:var(--muted);">
<input type="checkbox" id="logOnlyError" onchange="loadAcsLogs()"> Hanya error / fault
</label>
<button class="btn btn-p btn-sm" onclick="loadAcsLogs()"><i class="bi bi-arrow-clockwise"></i> <%= t('admin.acs.reload','Muat Ulang') %></button>
<span style="font-size:12px; color:var(--muted);">Log diambil dari tiap ACS (Built-in: riwayat task; GenieACS: <code>/faults</code>).</span>
</div>
<div id="acsLogContainer"><div class="text-muted" style="padding:12px;">Pilih filter lalu klik "<%= t('admin.acs.reload','Muat Ulang') %>"<%= t('admin.acs.load_log',' untuk memuat log.') %></div></div>
</div>
</div>
<footer style="margin-top: 30px; padding: 20px; text-align: center; color: var(--muted); font-size: 12px; border-top: 1px solid var(--border);">
&copy; <%= new Date().getFullYear() %> <%= (typeof settings !== 'undefined' ? settings.company_header : 'OPEN-ISP') %> - GenieACS Pro
</footer>
</div>
</div>
<!-- Unified Add WAN All-in-One Modal -->
<div class="mo" id="addWanModal">
<div class="mb" style="max-width:650px">
<div class="mh">
<span class="mt"><i class="bi bi-diagram-3"></i> <%= t('admin.acs.prov_title','Provisioning All-in-One WAN') %></span>
<button class="mc" onclick="closeModal('addWanModal')">&times;</button>
</div>
<div class="mbody" style="max-height: 70vh; overflow-y: auto; padding-right: 10px;">
<form id="addWanForm" onsubmit="submitAddWanForm(event)">
<input type="hidden" id="wanDeviceId" value="">
<input type="hidden" id="wanAcsId" value="">
<div style="background: var(--sub-bg); padding: 15px; border-radius: 12px; margin-bottom: 20px; border: 1px dashed var(--border);">
<div style="display: flex; gap: 15px; font-size: 13px;">
<div><strong>SN Perangkat:</strong> <span id="lblWanSn" class="text-primary" style="font-weight: 700;">-</span></div>
<div><strong>Vendor:</strong> <span id="lblWanVendor" style="text-transform: capitalize; font-weight: 700;">-</span></div>
<div><strong>Tipe:</strong> <span id="lblWanModel" style="font-weight: 700;">-</span></div>
</div>
</div>
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 15px; margin-bottom: 15px;">
<div class="fg" style="margin: 0;">
<label class="fl">WAN Connection Mode</label>
<select id="wanMode" class="fc" onchange="toggleWanModeFields()" style="height: 42px;">
<option value="pppoe">PPPoE (Router Mode)</option>
<option value="bridge">Bridge Mode</option>
</select>
</div>
<div class="fg" style="margin: 0;">
<label class="fl">VLAN ID</label>
<input type="number" id="wanVlanId" class="fc" required min="1" max="4094" placeholder="e.g. 100" style="height: 42px;">
</div>
</div>
<div style="margin-bottom: 20px;">
<label style="display:flex; align-items:center; gap:10px; font-size:13px; color:var(--text); font-weight:700; cursor:pointer;">
<input type="checkbox" id="wanDhcp" checked style="width:18px; height:18px;">
<i class="bi bi-hdd-rack text-primary"></i> Aktifkan DHCP Server LAN (Rekomendasi: Nonaktifkan jika Bridge)
</label>
</div>
<!-- PPPoE Credentials Block -->
<div id="pppoeFields" style="background: rgba(59,130,246,0.03); border: 1px solid rgba(59,130,246,0.1); padding: 18px; border-radius: 16px; margin-bottom: 15px;">
<h4 style="margin: 0 0 12px 0; font-size: 14px; color: var(--primary);"><i class="bi bi-key"></i> Kredensial PPPoE</h4>
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 15px; margin-bottom: 12px;">
<div class="fg" style="margin: 0;">
<label class="fl">Username PPPoE</label>
<input type="text" id="wanPppoeUser" class="fc" placeholder="e.g. customer123">
</div>
<div class="fg" style="margin: 0;">
<label class="fl">Password PPPoE</label>
<input type="text" id="wanPppoePass" class="fc" placeholder="e.g. secretpass">
</div>
</div>
<div style="display: grid; grid-template-columns: 1.2fr 0.8fr; gap: 15px; align-items: center; margin-top: 15px; padding-top: 12px; border-top: 1px dashed var(--border);">
<div>
<label style="display:flex; align-items:center; gap:8px; font-size:12px; color:var(--text); font-weight:600; cursor:pointer;">
<input type="checkbox" id="wanAutoMikrotik" checked style="width:18px; height:18px;" onchange="toggleMikrotikProfileField()">
Auto-create PPPoE Secret di MikroTik
</label>
</div>
<div class="fg" style="margin: 0;" id="pppoeProfileContainer">
<label class="fl"><%= t('admin.acs.profile_mikrotik','MikroTik Profile') %></label>
<select id="wanPppoeProfile" class="fc" style="height: 38px;">
<% pppoeProfiles.forEach(p => { %>
<option value="<%= p.name %>" <%= p.name === 'default' ? 'selected' : '' %>><%= p.name %></option>
<% }); %>
</select>
</div>
</div>
</div>
<!-- Unified Wi-Fi Block -->
<div style="border: 1px solid var(--border); border-radius: 16px; margin-bottom: 20px; overflow: hidden;">
<div style="background: var(--sub-bg); padding: 12px 18px; display: flex; justify-content: space-between; align-items: center; border-bottom: 1px solid var(--border);">
<label style="display: flex; align-items: center; gap: 10px; font-weight: 700; font-size: 13px; cursor: pointer; margin: 0; width: 100%;">
<input type="checkbox" id="wanConfigureWifi" onchange="toggleWifiFields()" style="width: 18px; height: 18px;">
<i class="bi bi-wifi text-primary"></i> Konfigurasikan Wi-Fi Sekaligus
</label>
</div>
<div id="wifiFields" style="display: none; padding: 20px; background: rgba(16,185,129,0.01);">
<div id="wifiLoading" style="text-align: center; padding: 15px; color: var(--muted);">
<i class="bi bi-arrow-clockwise spin" style="font-size: 20px;"></i> Mengunduh SSID lama...
</div>
<div id="wifiBandsContainer" style="display: grid; gap: 15px;">
<!-- 2.4GHz Band -->
<div id="band24Fields" style="display: block;">
<h5 style="margin: 0 0 10px 0; font-size: 13px; color: var(--text); font-weight: 700;"><i class="bi bi-broadcast"></i> Wi-Fi 2.4GHz</h5>
<div class="form-row2">
<div class="fg" style="margin: 0;">
<label class="fl"><%= t('admin.acs.ssid','SSID (Nama Wi-Fi)') %></label>
<input type="text" id="wifiSsid24" class="fc" placeholder="<%= t('admin.acs.loading','Mengunduh...') %>">
</div>
<div class="fg" style="margin: 0;">
<label class="fl"><%= t('admin.acs.wifi_pass','Sandi Wi-Fi (WPA2)') %></label>
<input type="password" id="wifiPass24" class="fc" placeholder="<%= t('admin.common.keep_empty','Biarkan kosong jika tidak diganti') %>">
</div>
</div>
</div>
<!-- 5GHz Band -->
<div id="band5Fields" style="display: none; border-top: 1px dashed var(--border); padding-top: 15px; margin-top: 10px;">
<h5 style="margin: 0 0 10px 0; font-size: 13px; color: var(--text); font-weight: 700;"><i class="bi bi-lightning-charge"></i> Wi-Fi 5GHz</h5>
<div class="form-row2">
<div class="fg" style="margin: 0;">
<label class="fl"><%= t('admin.acs.ssid','SSID (Nama Wi-Fi)') %></label>
<input type="text" id="wifiSsid5" class="fc" placeholder="<%= t('admin.acs.loading','Mengunduh...') %>">
</div>
<div class="fg" style="margin: 0;">
<label class="fl"><%= t('admin.acs.wifi_pass','Sandi Wi-Fi (WPA2)') %></label>
<input type="password" id="wifiPass5" class="fc" placeholder="<%= t('admin.common.keep_empty','Biarkan kosong jika tidak diganti') %>">
</div>
</div>
</div>
</div>
</div>
</div>
<!-- LAN / WLAN Port Binding -->
<div style="background: var(--sub-bg); border: 1px solid var(--border); padding: 18px; border-radius: 16px; margin-bottom: 5px;">
<h4 style="margin: 0 0 12px 0; font-size: 13px; color: var(--text); font-weight: 700;"><i class="bi bi-bezier2 text-primary"></i> LAN / WLAN Port Binding</h4>
<label class="fl" style="margin-bottom: 8px;"><%= t('admin.acs.bind_lan','Pilih LAN Port yang akan di-bind:') %></label>
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); gap: 10px; margin-bottom: 15px;">
<label class="bind-tag active" id="lblLan1">
<input type="checkbox" name="lanPorts" value="LAN1" checked style="display: none;" onchange="syncTagClass(this, 'lblLan1')"> LAN 1
</label>
<label class="bind-tag active" id="lblLan2">
<input type="checkbox" name="lanPorts" value="LAN2" checked style="display: none;" onchange="syncTagClass(this, 'lblLan2')"> LAN 2
</label>
<label class="bind-tag active" id="lblLan3">
<input type="checkbox" name="lanPorts" value="LAN3" checked style="display: none;" onchange="syncTagClass(this, 'lblLan3')"> LAN 3
</label>
<label class="bind-tag active" id="lblLan4">
<input type="checkbox" name="lanPorts" value="LAN4" checked style="display: none;" onchange="syncTagClass(this, 'lblLan4')"> LAN 4
</label>
</div>
<label class="fl" style="margin-bottom: 8px;"><%= t('admin.acs.bind_ssid','Pilih WLAN (SSID) yang akan di-bind:') %></label>
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); gap: 10px;">
<label class="bind-tag active" id="lblWlan1">
<input type="checkbox" name="wlanSsids" value="SSID1" checked style="display: none;" onchange="syncTagClass(this, 'lblWlan1')"> SSID 1
</label>
<label class="bind-tag" id="lblWlan2">
<input type="checkbox" name="wlanSsids" value="SSID2" style="display: none;" onchange="syncTagClass(this, 'lblWlan2')"> SSID 2
</label>
<label class="bind-tag" id="lblWlan3">
<input type="checkbox" name="wlanSsids" value="SSID3" style="display: none;" onchange="syncTagClass(this, 'lblWlan3')"> SSID 3
</label>
<label class="bind-tag" id="lblWlan4">
<input type="checkbox" name="wlanSsids" value="SSID4" style="display: none;" onchange="syncTagClass(this, 'lblWlan4')"> SSID 4
</label>
</div>
</div>
<div id="addWanProgressBox" style="display:none; margin-top:16px; border:1px solid var(--border); border-radius:16px; padding:16px; background:rgba(59,130,246,0.04);">
<div style="display:flex; justify-content:space-between; gap:12px; align-items:center; margin-bottom:8px;">
<strong style="font-size:13px; color:var(--text);"><i class="bi bi-diagram-3 me-1"></i><%= t('admin.acs.progress_add_wan','Progress Add WAN') %></strong>
<span id="addWanProgressBadge" class="text-primary" style="font-size:12px; font-weight:700;"><%= t('admin.acs.waiting','Menunggu') %></span>
</div>
<div id="addWanProgressSummary" style="font-size:12px; color:var(--muted); margin-bottom:10px;"><%= t('admin.acs.workflow_not_started','Workflow not started.') %></div>
<div id="addWanProgressList" style="display:grid; gap:8px;"></div>
</div>
</form>
</div>
<div class="mf">
<button type="button" class="btn btn-g" onclick="closeModal('addWanModal')"><%= t('admin.acs.cancel','Batal') %></button>
<button type="submit" form="addWanForm" class="btn btn-p" id="btn-save-add-wan">
<span class="btn-text"><i class="bi bi-save"></i> <%= t('admin.acs.run_prov','Jalankan Provisioning') %></span>
<span class="btn-loader" style="display:none"><i class="bi bi-arrow-clockwise spin"></i> Memproses...</span>
</button>
</div>
</div>
</div>
<div class="mo" id="serverModal">
<div class="mb" style="max-width:560px">
<div class="mh">
<span class="mt" id="acsServerModalTitle"><i class="bi bi-server"></i> <%= t('admin.acs.add_server','Tambah Server ACS') %></span>
<button class="mc" onclick="closeModal('serverModal')">&times;</button>
</div>
<div class="mbody">
<form id="acsServerForm" onsubmit="saveServer(event)">
<input type="hidden" id="sId" value="">
<div class="fg">
<label class="fl"><%= t('admin.acs.server_name','Server Name') %></label>
<input type="text" id="sName" class="fc" required placeholder="e.g. Server Pusat">
</div>
<div class="fg">
<label class="fl">API URL</label>
<input type="text" id="sUrl" class="fc" required placeholder="http://192.168.1.10:7557">
</div>
<div class="form-row2">
<div class="fg" style="margin:0">
<label class="fl">Username</label>
<input type="text" id="sUser" class="fc" placeholder='<%= t("admin.acs.optional","Opsional") %>'>
<label style="display:flex;align-items:center;gap:8px;margin-top:8px;font-size:12px;color:var(--muted);font-weight:600">
<input type="checkbox" id="sClearUser" style="width:16px;height:16px">
Kosongkan username
</label>
</div>
<div class="fg" style="margin:0">
<label class="fl">Password</label>
<input type="password" id="sPass" class="fc" placeholder='<%= t("admin.acs.optional","Opsional") %>'>
<label style="display:flex;align-items:center;gap:8px;margin-top:8px;font-size:12px;color:var(--muted);font-weight:600">
<input type="checkbox" id="sClearPass" style="width:16px;height:16px">
Kosongkan password
</label>
</div>
</div>
<div class="fg">
<label class="fl"><%= t('admin.acs.location_note','Location / Note') %></label>
<input type="text" id="sLoc" class="fc" placeholder='<%= t("admin.acs.location_note_ph","cth. Data Center") %>'>
</div>
</form>
</div>
<div class="mf">
<button type="button" class="btn btn-g" onclick="closeModal('serverModal')"><%= t('admin.acs.cancel','Batal') %></button>
<button type="submit" form="acsServerForm" class="btn btn-p" id="btn-save-acs-server">
<span class="btn-text"><i class="bi bi-save"></i> <%= t('common.save','Simpan') %></span>
<span class="btn-loader" style="display:none"><i class="bi bi-arrow-clockwise spin"></i> Menyimpan...</span>
</button>
</div>
</div>
</div>
<script>
function switchTab(id, btn) {
document.querySelectorAll('.tab-pane').forEach(c => c.classList.remove('active'));
document.querySelectorAll('.tab-bar button').forEach(b => b.classList.remove('active'));
var pane = document.getElementById('tab-' + id);
if (pane) pane.classList.add('active');
if (btn) btn.classList.add('active');
if (id === 'logs' && typeof loadAcsLogs === 'function' && !window._acsLogsLoaded) { window._acsLogsLoaded = true; loadAcsLogs(); }
}
function escAcs(v){ return String(v == null ? '' : v).replace(/[&<>"]/g, function(c){ return ({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;'}[c]); }); }
function loadAcsLogs() {
var el = document.getElementById('acsLogContainer');
if (!el) return;
var server = (document.getElementById('logServerFilter') || {}).value || 'all';
var onlyErr = (document.getElementById('logOnlyError') || {}).checked ? '1' : '0';
el.innerHTML = '<div class="text-muted" style="padding:12px;">Memuat log…</div>';
fetch('/admin/acs/logs?server=' + encodeURIComponent(server) + '&onlyError=' + onlyErr + '&limit=200')
.then(function(r){ return r.json(); })
.then(function(d){
if (!d || !d.ok) { el.innerHTML = '<div style="color:var(--danger);padding:12px;">Gagal: ' + escAcs((d && d.error) || '') + '</div>'; return; }
if (!d.servers.length) { el.innerHTML = '<div class="text-muted" style="padding:12px;">Tidak ada server ACS.</div>'; return; }
el.innerHTML = d.servers.map(function(s){
var rows = (s.entries && s.entries.length) ? s.entries.map(function(e){
var color = e.status === 'completed' ? '#10b981' : (e.status === 'skipped' ? '#f59e0b' : '#ef4444');
return '<tr>' +
'<td style="white-space:nowrap;font-size:12px;color:var(--muted);">' + escAcs(e.time || '-') + '</td>' +
'<td><code style="font-size:12px;">' + escAcs(e.device || '-') + '</code></td>' +
'<td style="font-size:12px;">' + escAcs(e.action || '-') + '</td>' +
'<td style="font-size:12px;font-weight:700;color:' + color + ';">' + escAcs(e.status || '-') + '</td>' +
'<td style="font-size:12px;color:var(--muted);max-width:520px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;" title="' + escAcs(e.message || '') + '">' + escAcs(e.message || '') + '</td>' +
'</tr>';
}).join('') : '<tr><td colspan="5" class="text-muted" style="text-align:center;padding:16px;">Tidak ada log.</td></tr>';
return '<div style="margin-bottom:22px;">' +
'<div style="font-weight:700;margin-bottom:8px;"><i class="bi bi-hdd-rack"></i> ' + escAcs(s.name) + ' <code style="font-size:11px;color:var(--muted);">' + escAcs(s.url) + '</code> <span class="badge" style="font-size:10px;">' + (s.entries ? s.entries.length : 0) + '</span></div>' +
'<div class="table-container"><table><thead><tr><th><%= t('collector.col_time','Waktu') %></th><th>Perangkat</th><th><%= t('common.actions','Aksi') %></th><th><%= t('common.status','Status') %></th><th>Pesan</th></tr></thead><tbody>' + rows + '</tbody></table></div>' +
'</div>';
}).join('');
})
.catch(function(e){ el.innerHTML = '<div style="color:var(--danger);padding:12px;">Error: ' + escAcs(e.message) + '</div>'; });
}
function openModal(id) { document.getElementById(id).classList.add('show'); }
function closeModal(id) {
document.getElementById(id).classList.remove('show');
if (id === 'addWanModal') stopAddWanProgressPolling();
}
document.querySelectorAll('.mo').forEach(m => m.addEventListener('click', e => {
if (e.target === m) closeModal(m.id);
}));
function openAddServerModal() {
document.getElementById('acsServerModalTitle').innerHTML = '<i class="bi bi-server"></i> <%= t('admin.acs.add_server','Tambah Server ACS') %>';
document.getElementById('sId').value = '';
document.getElementById('sName').value = '';
document.getElementById('sUrl').value = '';
document.getElementById('sUser').value = '';
document.getElementById('sPass').value = '';
document.getElementById('sLoc').value = '';
document.getElementById('sClearUser').checked = false;
document.getElementById('sClearPass').checked = false;
document.getElementById('sName').disabled = false;
document.getElementById('sLoc').disabled = false;
openModal('serverModal');
}
function openEditServerModal(btn) {
document.getElementById('acsServerModalTitle').innerHTML = '<i class="bi bi-server"></i> Edit ACS Server';
document.getElementById('sId').value = btn.dataset.id || '';
document.getElementById('sName').value = btn.dataset.name || '';
document.getElementById('sUrl').value = btn.dataset.url || '';
document.getElementById('sUser').value = btn.dataset.username || '';
document.getElementById('sPass').value = '';
document.getElementById('sLoc').value = btn.dataset.location || '';
document.getElementById('sClearUser').checked = false;
document.getElementById('sClearPass').checked = false;
document.getElementById('sName').disabled = false;
document.getElementById('sLoc').disabled = false;
openModal('serverModal');
}
function openEditLegacyAcsModal() {
document.getElementById('acsServerModalTitle').innerHTML = '<i class="bi bi-server"></i> <%= t('admin.acs.edit_default','Edit Default ACS') %>';
document.getElementById('sId').value = 'legacy';
document.getElementById('sName').value = 'Default ACS';
document.getElementById('sUrl').value = '<%= (typeof settings !== "undefined" && settings.genieacs_url) ? String(settings.genieacs_url) : "" %>';
document.getElementById('sUser').value = '<%= (typeof settings !== "undefined" && settings.genieacs_username) ? String(settings.genieacs_username) : "" %>';
document.getElementById('sPass').value = '';
document.getElementById('sLoc').value = '';
document.getElementById('sClearUser').checked = false;
document.getElementById('sClearPass').checked = false;
document.getElementById('sName').disabled = true;
document.getElementById('sLoc').disabled = true;
openModal('serverModal');
}
async function saveServer(e) {
e.preventDefault();
const btn = document.getElementById('btn-save-acs-server');
const txt = btn.querySelector('.btn-text');
const ldr = btn.querySelector('.btn-loader');
btn.disabled = true;
txt.style.display = 'none';
ldr.style.display = 'inline-block';
const id = document.getElementById('sId').value.trim();
const body = {
name: document.getElementById('sName').value,
url: document.getElementById('sUrl').value,
username: document.getElementById('sUser').value,
password: document.getElementById('sPass').value,
location: document.getElementById('sLoc').value,
clear_username: document.getElementById('sClearUser').checked,
clear_password: document.getElementById('sClearPass').checked
};
try {
const isLegacy = id === 'legacy';
const endpoint = isLegacy ? '/admin/acs/api/servers/legacy' : (id ? ('/admin/acs/api/servers/' + encodeURIComponent(id)) : '/admin/acs/api/servers');
const method = isLegacy ? 'PUT' : (id ? 'PUT' : 'POST');
const res = await fetch(endpoint, {
method,
headers: {'Content-Type':'application/json'},
body: JSON.stringify(body)
});
const data = await res.json();
if (data && data.success) return location.reload();
alert((data && data.message) ? data.message : 'Gagal menyimpan server');
} finally {
btn.disabled = false;
txt.style.display = 'inline-block';
ldr.style.display = 'none';
}
}
async function deleteServer(id) {
if(!confirm('Hapus server ini?')) return;
const res = await fetch('/admin/acs/api/servers/' + id, { method: 'DELETE' });
if((await res.json()).success) location.reload();
}
async function refreshDevice(deviceId) {
if(!confirm('Refresh device ' + deviceId + '?')) return;
const res = await fetch('/admin/acs/api/refresh/' + encodeURIComponent(deviceId), {
method: 'POST',
headers: {'Content-Type':'application/json'},
body: JSON.stringify({ acsId: '<%= selectedAcsId %>' })
});
const d = await res.json();
alert(d.message);
}
// Sync class active on binding tag label based on checkbox state
function syncTagClass(checkbox, labelId) {
const lbl = document.getElementById(labelId);
if (checkbox.checked) {
lbl.classList.add('active');
} else {
lbl.classList.remove('active');
}
}
// Toggle visibility of PPPoE-specific fields
function toggleWanModeFields() {
const mode = document.getElementById('wanMode').value;
const pppoeFields = document.getElementById('pppoeFields');
const wanDhcp = document.getElementById('wanDhcp');
if (mode === 'pppoe') {
pppoeFields.style.display = 'block';
if (wanDhcp) wanDhcp.checked = true;
} else {
pppoeFields.style.display = 'none';
if (wanDhcp) wanDhcp.checked = false;
}
}
// Toggle visibility of Profile MikroTik field based on auto-create check
function toggleMikrotikProfileField() {
const autoChk = document.getElementById('wanAutoMikrotik');
const container = document.getElementById('pppoeProfileContainer');
if (autoChk.checked) {
container.style.visibility = 'visible';
} else {
container.style.visibility = 'hidden';
}
}
// Toggle Wi-Fi configuration slide-down and AJAX fetch
async function toggleWifiFields() {
const configureWifi = document.getElementById('wanConfigureWifi').checked;
const wifiFields = document.getElementById('wifiFields');
if (configureWifi) {
wifiFields.style.display = 'block';
const deviceId = document.getElementById('wanDeviceId').value;
const acsId = document.getElementById('wanAcsId').value;
const wifiLoading = document.getElementById('wifiLoading');
const band24Fields = document.getElementById('band24Fields');
const band5Fields = document.getElementById('band5Fields');
wifiLoading.style.display = 'block';
band24Fields.style.opacity = '0.5';
band5Fields.style.opacity = '0.5';
try {
const res = await fetch(`/admin/acs/api/wifi-settings/${encodeURIComponent(deviceId)}?acsId=${encodeURIComponent(acsId)}`);
const d = await res.json();
if (d && d.success && Array.isArray(d.bands)) {
// Reset fields
document.getElementById('wifiSsid24').value = '';
document.getElementById('wifiSsid5').value = '';
band5Fields.style.display = 'none';
d.bands.forEach(b => {
if (b.index === '1') {
document.getElementById('wifiSsid24').value = b.ssid;
document.getElementById('wifiSsid24').placeholder = 'Nama Wi-Fi 2.4G';
} else if (b.index === '5' || b.index === '2') {
band5Fields.style.display = 'block';
document.getElementById('wifiSsid5').value = b.ssid;
document.getElementById('wifiSsid5').placeholder = 'Nama Wi-Fi 5G';
}
});
}
} catch (e) {
console.error('Gagal mengambil SSID perangkat:', e);
} finally {
wifiLoading.style.display = 'none';
band24Fields.style.opacity = '1';
band5Fields.style.opacity = '1';
}
} else {
wifiFields.style.display = 'none';
}
}
let addWanProgressTimer = null;
let addWanTrackingState = null;
function stopAddWanProgressPolling() {
if (addWanProgressTimer) {
clearTimeout(addWanProgressTimer);
addWanProgressTimer = null;
}
}
function resetAddWanProgress() {
stopAddWanProgressPolling();
addWanTrackingState = null;
const box = document.getElementById('addWanProgressBox');
const badge = document.getElementById('addWanProgressBadge');
const summary = document.getElementById('addWanProgressSummary');
const list = document.getElementById('addWanProgressList');
if (box) box.style.display = 'none';
if (badge) {
badge.className = 'text-primary';
badge.textContent = '<%= t("admin.acs.waiting","Menunggu") %>';
}
if (summary) {
summary.textContent = '<%= t("admin.acs.workflow_not_started","Workflow not started.") %>';
}
if (list) {
list.innerHTML = '';
}
}
function renderAddWanProgress(data) {
const box = document.getElementById('addWanProgressBox');
const badge = document.getElementById('addWanProgressBadge');
const summary = document.getElementById('addWanProgressSummary');
const list = document.getElementById('addWanProgressList');
if (!box || !badge || !summary || !list) return;
box.style.display = 'block';
summary.textContent = data.summary || 'Workflow sedang diproses.';
if (data.hasFailure) {
badge.className = 'text-danger';
badge.textContent = 'Ada Gagal';
} else if (data.done) {
badge.className = 'text-success';
badge.textContent = 'Selesai';
} else {
badge.className = 'text-warning';
badge.textContent = 'Berjalan';
}
const tasks = Array.isArray(data.tasks) ? data.tasks : [];
list.innerHTML = tasks.map(task => {
const status = String(task.status || 'pending');
let statusClass = 'text-muted';
let statusLabel = 'Pending';
if (status === 'completed') {
statusClass = 'text-success';
statusLabel = 'Selesai';
} else if (status === 'failed') {
statusClass = 'text-danger';
statusLabel = 'Gagal';
} else if (status === 'in_progress') {
statusClass = 'text-warning';
statusLabel = 'Diproses';
}
return `
<div style="display:flex; justify-content:space-between; gap:12px; border:1px dashed var(--border); border-radius:12px; padding:10px 12px; background:rgba(15,23,42,0.08);">
<span style="font-size:12px; color:var(--text);">${task.label || task.name || 'Task ACS'}</span>
<span class="${statusClass}" style="font-size:12px; font-weight:700;">${statusLabel}</span>
</div>
`;
}).join('');
}
async function pollAddWanProgress() {
if (!addWanTrackingState || !addWanTrackingState.deviceId) return;
const params = new URLSearchParams();
if (addWanTrackingState.workflowId) params.set('workflowId', addWanTrackingState.workflowId);
if (addWanTrackingState.rootTaskId) params.set('rootTaskId', String(addWanTrackingState.rootTaskId));
try {
const res = await fetch(`/admin/acs/api/add-wan-status/${encodeURIComponent(addWanTrackingState.deviceId)}?${params.toString()}`, {
cache: 'no-store'
});
const data = await res.json();
if (!data || !data.success) throw new Error(data && data.message ? data.message : 'Gagal membaca progress Add WAN');
renderAddWanProgress(data);
if (data.done) {
stopAddWanProgressPolling();
const btn = document.getElementById('btn-save-add-wan');
const txt = btn ? btn.querySelector('.btn-text') : null;
const ldr = btn ? btn.querySelector('.btn-loader') : null;
if (btn) btn.disabled = false;
if (txt) txt.style.display = 'inline-block';
if (ldr) ldr.style.display = 'none';
setTimeout(() => {
closeModal('addWanModal');
location.reload();
}, data.hasFailure ? 2500 : 1800);
return;
}
addWanProgressTimer = setTimeout(pollAddWanProgress, 4000);
} catch (err) {
stopAddWanProgressPolling();
const summary = document.getElementById('addWanProgressSummary');
if (summary) summary.textContent = 'Gagal membaca progress workflow Add WAN.';
const btn = document.getElementById('btn-save-add-wan');
const txt = btn ? btn.querySelector('.btn-text') : null;
const ldr = btn ? btn.querySelector('.btn-loader') : null;
if (btn) btn.disabled = false;
if (txt) txt.style.display = 'inline-block';
if (ldr) ldr.style.display = 'none';
}
}
// Open modal Add WAN All-in-One and populate basic details
async function openAddWanModal(deviceId, acsId, sn, manufacturer, model) {
resetAddWanProgress();
document.getElementById('wanDeviceId').value = deviceId;
document.getElementById('wanAcsId').value = acsId;
document.getElementById('lblWanSn').innerText = sn || deviceId;
document.getElementById('lblWanVendor').innerText = manufacturer || '-';
document.getElementById('lblWanModel').innerText = model || '-';
// Reset form fields
document.getElementById('wanVlanId').value = '';
document.getElementById('wanPppoeUser').value = '';
document.getElementById('wanPppoePass').value = '';
document.getElementById('wanConfigureWifi').checked = false;
document.getElementById('wifiFields').style.display = 'none';
// Default active bindings
document.querySelectorAll('input[name="lanPorts"]').forEach(c => {
c.checked = true;
syncTagClass(c, 'lbl' + c.value.charAt(0) + c.value.slice(1).toLowerCase());
});
// Reset SSID labels to original text first
for (let i = 1; i <= 4; i++) {
const lbl = document.getElementById(`lblWlan${i}`);
if (lbl) {
lbl.innerHTML = `<input type="checkbox" name="wlanSsids" value="SSID${i}" ${i === 1 ? 'checked' : ''} style="display: none;" onchange="syncTagClass(this, 'lblWlan${i}')"> SSID ${i}`;
if (i === 1) lbl.classList.add('active');
else lbl.classList.remove('active');
}
}
toggleWanModeFields();
toggleMikrotikProfileField();
openModal('addWanModal');
// Async fetch WLAN actual SSID names
try {
const res = await fetch(`/admin/acs/api/wifi-settings/${encodeURIComponent(deviceId)}?acsId=${encodeURIComponent(acsId)}`);
const d = await res.json();
if (d && d.success && Array.isArray(d.bands)) {
d.bands.forEach(b => {
const lbl = document.getElementById(`lblWlan${b.index}`);
if (lbl) {
const isChecked = b.index === '1'; // Default SSID 1 is checked
lbl.innerHTML = `<input type="checkbox" name="wlanSsids" value="SSID${b.index}" ${isChecked ? 'checked' : ''} style="display: none;" onchange="syncTagClass(this, 'lblWlan${b.index}')"> SSID ${b.index}: ${b.ssid}`;
}
});
}
} catch (e) {
console.error('Gagal mengambil nama SSID:', e);
}
}
// AJAX submission of the Provisioning form
async function submitAddWanForm(e) {
e.preventDefault();
const btn = document.getElementById('btn-save-add-wan');
const txt = btn.querySelector('.btn-text');
const ldr = btn.querySelector('.btn-loader');
btn.disabled = true;
txt.style.display = 'none';
ldr.style.display = 'inline-block';
const deviceId = document.getElementById('wanDeviceId').value;
const acsId = document.getElementById('wanAcsId').value;
const lanPorts = [];
document.querySelectorAll('input[name="lanPorts"]:checked').forEach(c => lanPorts.push(c.value));
const wlanSsids = [];
document.querySelectorAll('input[name="wlanSsids"]:checked').forEach(c => wlanSsids.push(c.value));
const body = {
acsId,
mode: document.getElementById('wanMode').value,
vlanId: document.getElementById('wanVlanId').value,
pppoeUser: document.getElementById('wanPppoeUser').value,
pppoePass: document.getElementById('wanPppoePass').value,
pppoeProfile: document.getElementById('wanPppoeProfile').value,
autoCreateMikrotik: document.getElementById('wanAutoMikrotik').checked,
lanPorts,
wlanSsids,
configureWifi: document.getElementById('wanConfigureWifi').checked,
wifiSsid24: document.getElementById('wifiSsid24').value,
wifiPass24: document.getElementById('wifiPass24').value,
wifiSsid5: document.getElementById('wifiSsid5').value,
wifiPass5: document.getElementById('wifiPass5').value,
dhcp: document.getElementById('wanDhcp').checked
};
try {
const res = await fetch(`/admin/acs/api/add-wan/${encodeURIComponent(deviceId)}`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(body)
});
const data = await res.json();
if (data && data.success) {
if (data.trackingSupported) {
addWanTrackingState = {
deviceId,
workflowId: data.workflowId || '',
rootTaskId: data.rootTaskId || ''
};
renderAddWanProgress({
summary: data.message || 'Workflow Add WAN dimulai.',
done: false,
hasFailure: false,
tasks: []
});
addWanProgressTimer = setTimeout(pollAddWanProgress, 1200);
} else {
alert(data.message);
closeModal('addWanModal');
location.reload();
}
} else {
alert(data.message || 'Gagal menjalankan provisioning');
}
} catch (err) {
alert('Terjadi kesalahan: ' + err.message);
} finally {
btn.disabled = false;
txt.style.display = 'inline-block';
ldr.style.display = 'none';
}
}
async function syncAllACS() {
const res = await fetch('/admin/acs/api/sync/all', { method: 'POST' });
const d = await res.json();
alert(d.message);
if(d.success) location.reload();
}
async function syncOnuTags() {
if (!confirm('Sinkron Tag ONU pelanggan dari device.id ACS sekarang? (Tag diisi otomatis dari PPPoE user ONU)')) return;
try {
const res = await fetch('/admin/acs/api/sync/tags', { method: 'POST' });
const d = await res.json();
alert(d.message);
if (d.success) location.reload();
} catch (e) { alert('Gagal sync tag: ' + e.message); }
}
function toggleSelectAll(master) {
const checkboxes = document.querySelectorAll('.device-checkbox');
checkboxes.forEach(cb => cb.checked = master.checked);
updateBulkPanel();
}
function updateBulkPanel() {
const checkboxes = document.querySelectorAll('.device-checkbox:checked');
const panel = document.getElementById('bulkActionsPanel');
const countSpan = document.getElementById('selectedCount');
if (checkboxes.length > 0) {
panel.style.display = 'flex';
countSpan.textContent = checkboxes.length;
} else {
panel.style.display = 'none';
document.getElementById('selectAll<%= t('admin.acs.tab_devices','Perangkat') %>').checked = false;
}
}
function getSelected<%= t('admin.acs.tab_devices','Perangkat') %>() {
const checkboxes = document.querySelectorAll('.device-checkbox:checked');
const devices = [];
checkboxes.forEach(cb => {
devices.push({
id: cb.dataset.id,
acsId: cb.dataset.acs
});
});
return devices;
}
async function bulkSummon() {
const devices = getSelected<%= t('admin.acs.tab_devices','Perangkat') %>();
if (devices.length === 0) return;
if (!confirm(`Summon / Sinkronisasi ulang ${devices.length} perangkat?`)) return;
try {
const res = await fetch('/admin/acs/api/bulk/refresh', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ devices })
});
const d = await res.json();
alert(d.message);
if (d.success) location.reload();
} catch (err) {
alert('Gagal memproses bulk summon: ' + err.message);
}
}
async function bulkDelete() {
const devices = getSelected<%= t('admin.acs.tab_devices','Perangkat') %>();
if (devices.length === 0) return;
if (!confirm(`HAPUS ${devices.length} perangkat dari database ACS?\n(Tindakan ini tidak dapat dibatalkan)`)) return;
try {
const res = await fetch('/admin/acs/api/bulk/delete', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ devices })
});
const d = await res.json();
alert(d.message);
if (d.success) location.reload();
} catch (err) {
alert('Gagal memproses bulk delete: ' + err.message);
}
}
async function deleteDevice(deviceId, acsId) {
if (!confirm('Hapus perangkat ' + deviceId + ' dari database ACS?')) return;
try {
const res = await fetch('/admin/acs/api/device/' + encodeURIComponent(deviceId), {
method: 'DELETE',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ acsId })
});
const d = await res.json();
alert(d.message);
if (d.success) location.reload();
} catch (err) {
alert('Gagal menghapus perangkat: ' + err.message);
}
}
</script>
</body>
</html>