feat(gowa): tombol Test Koneksi (health, auth, jumlah device)
All checks were successful
CI / test (push) Successful in 2m44s

- service testReachability: /health + /devices
- route POST /admin/whatsapp/test-gowa-connection
- UI status warna hijau/kuning/merah
- versi 0.5.11
This commit is contained in:
oxpoe
2026-11-05 17:01:13 +07:00
parent b49822b553
commit f8e6e86619
7 changed files with 75 additions and 4 deletions

View File

@@ -2,6 +2,11 @@
Format mengikuti [Keep a Changelog](https://keepachangelog.com/) dan [Semantic Versioning](https://semver.org/). Format mengikuti [Keep a Changelog](https://keepachangelog.com/) dan [Semantic Versioning](https://semver.org/).
## [0.5.11] — 2026-11-05 — "gowa-test"
### Ditambahkan
- Tombol **Test Koneksi** di Admin → WhatsApp → GOWA: menampilkan apakah GOWA terjangkau, status `/health`, kecocokan username/password Basic Auth, dan jumlah device terdaftar.
## [0.5.10] — 2026-11-05 — "gowa-mode" ## [0.5.10] — 2026-11-05 — "gowa-mode"
### Ditambahkan ### Ditambahkan

View File

@@ -11,7 +11,7 @@
<p align="center"> <p align="center">
<img src="https://img.shields.io/badge/lisensi-ISC-ADFF2F" alt="Lisensi: ISC"> <img src="https://img.shields.io/badge/lisensi-ISC-ADFF2F" alt="Lisensi: ISC">
<img src="https://img.shields.io/badge/versi-0.5.10_gowa--mode-ADFF2F" alt="Versi 0.5.10 (gowa-mode)"> <img src="https://img.shields.io/badge/versi-0.5.11_gowa--test-ADFF2F" alt="Versi 0.5.11 (gowa-test)">
<img src="https://img.shields.io/badge/Node.js-%E2%89%A520-339933" alt="Node.js >= 20"> <img src="https://img.shields.io/badge/Node.js-%E2%89%A520-339933" alt="Node.js >= 20">
<img src="https://img.shields.io/badge/docker-ready-2496ED?logo=docker&logoColor=white" alt="Docker"> <img src="https://img.shields.io/badge/docker-ready-2496ED?logo=docker&logoColor=white" alt="Docker">
<a href="https://demo-openisp.oxpoe.xyz"><img src="https://img.shields.io/badge/demo-live-ADFF2F?logo=googlechrome&logoColor=black" alt="Demo"></a> <a href="https://demo-openisp.oxpoe.xyz"><img src="https://img.shields.io/badge/demo-live-ADFF2F?logo=googlechrome&logoColor=black" alt="Demo"></a>

View File

@@ -1,6 +1,6 @@
{ {
"name": "open-isp", "name": "open-isp",
"version": "0.5.10", "version": "0.5.11",
"description": "OPEN-ISP \u2014 sistem billing & manajemen ISP open-source (PWA pelanggan, multi-OLT, RADIUS, WhatsApp, QRIS)", "description": "OPEN-ISP \u2014 sistem billing & manajemen ISP open-source (PWA pelanggan, multi-OLT, RADIUS, WhatsApp, QRIS)",
"main": "app-customer.js", "main": "app-customer.js",
"scripts": { "scripts": {

View File

@@ -6770,6 +6770,16 @@ router.post('/whatsapp/gateway-settings', requireAdminSession, express.urlencode
res.redirect('/admin/whatsapp'); res.redirect('/admin/whatsapp');
}); });
router.post('/whatsapp/test-gowa-connection', requireAdminSession, async (req, res) => {
try {
const gowaSvc = require('../services/gowaWhatsappService');
const r = await gowaSvc.testReachability();
return res.json(Object.assign({ ok: r.reachable, success: r.reachable }, r));
} catch (e) {
return res.status(500).json({ ok: false, success: false, message: e.message });
}
});
router.post('/whatsapp/test-gowa', requireAdminSession, express.json(), async (req, res) => { router.post('/whatsapp/test-gowa', requireAdminSession, express.json(), async (req, res) => {
try { try {
const { phone, message } = req.body; const { phone, message } = req.body;

View File

@@ -131,6 +131,41 @@ async function sendGowaDocument(toPhone, documentBuffer, filename = 'file.pdf',
}); });
return { success: true, gateway: 'gowa', raw: json }; return { success: true, gateway: 'gowa', raw: json };
} }
async function testReachability() {
const base = baseUrl();
const out = { baseUrl: base, reachable: false, healthStatus: 0, authOk: false, devices: 0, message: '' };
try {
const res = await fetch(base + '/health');
out.healthStatus = res.status;
out.reachable = res.ok;
} catch (e) {
out.message = 'Tidak terjangkau: ' + e.message;
return out;
}
if (!out.reachable) {
out.message = 'GOWA menjawab, tetapi /health mengembalikan HTTP ' + out.healthStatus;
return out;
}
try {
const res = await fetch(base + '/devices', { headers: commonHeaders() });
out.authStatus = res.status;
const json = await res.json().catch(function () { return null; });
const list = (json && Array.isArray(json.results)) ? json.results : [];
if (res.ok) {
out.authOk = true;
out.devices = list.length;
out.message = 'GOWA terjangkau' + (list.length ? ' — ' + list.length + ' device terdaftar' : ' — belum ada device, scan QR untuk menautkan');
} else if (res.status === 401) {
out.message = 'GOWA terjangkau, tetapi username/password Basic Auth salah atau kosong';
} else {
out.message = 'GOWA terjangkau, tetapi /devices membalas HTTP ' + res.status;
}
} catch (e) {
out.message = 'GOWA terjangkau, tetapi gagal cek kredensial: ' + e.message;
}
return out;
}
async function testGowaConnection(phone, message) { async function testGowaConnection(phone, message) {
return await sendGowaMessage(phone, message || 'Tes koneksi GOWA berhasil ✅'); return await sendGowaMessage(phone, message || 'Tes koneksi GOWA berhasil ✅');
} }
@@ -225,4 +260,4 @@ async function getLastQrImage() {
} }
} }
module.exports = { sendGowaMessage, sendGowaImage, sendGowaDocument, testGowaConnection, gowaStatus, listDevices, ensureDevice, getLoginQr, getLastQrImage, normalizePhoneJid, baseUrl, resolveDeviceId, invalidateDeviceCache }; module.exports = { sendGowaMessage, sendGowaImage, sendGowaDocument, testGowaConnection, testReachability, gowaStatus, listDevices, ensureDevice, getLoginQr, getLastQrImage, normalizePhoneJid, baseUrl, resolveDeviceId, invalidateDeviceCache };

View File

@@ -1 +1 @@
0.5.10 0.5.11

View File

@@ -199,6 +199,8 @@
<div style="font-size:11.5px;color:var(--muted);margin-bottom:8px"><%- t('admin.wa.qr_hint','Klik tombol, lalu buka WhatsApp &rarr; Perangkat Tertaut &rarr; Tautkan perangkat, scan QR. QR berlaku ~30 detik &amp; diperbarui otomatis.') %></div> <div style="font-size:11.5px;color:var(--muted);margin-bottom:8px"><%- t('admin.wa.qr_hint','Klik tombol, lalu buka WhatsApp &rarr; Perangkat Tertaut &rarr; Tautkan perangkat, scan QR. QR berlaku ~30 detik &amp; diperbarui otomatis.') %></div>
<button type="button" class="btn btn-p btn-sm" onclick="showGowaQr()"><i class="bi bi-qr-code-scan"></i> Tampilkan QR</button> <button type="button" class="btn btn-p btn-sm" onclick="showGowaQr()"><i class="bi bi-qr-code-scan"></i> Tampilkan QR</button>
<a href="<%= gowaUiUrl %>" target="_blank" rel="noopener" class="btn btn-g btn-sm" style="margin-left:6px"><i class="bi bi-box-arrow-up-right"></i> Buka UI GOWA</a> <a href="<%= gowaUiUrl %>" target="_blank" rel="noopener" class="btn btn-g btn-sm" style="margin-left:6px"><i class="bi bi-box-arrow-up-right"></i> Buka UI GOWA</a>
<button type="button" class="btn btn-g btn-sm" style="margin-left:6px" onclick="testGowaConn(this)"><i class="bi bi-plug"></i> Test Koneksi</button>
<div id="gowa_conn_status" style="display:none;margin-top:8px;font-size:12px;line-height:1.5"></div>
<div id="gowa_qr_wrap" style="display:none;margin-top:10px;text-align:center"> <div id="gowa_qr_wrap" style="display:none;margin-top:10px;text-align:center">
<img id="gowa_qr_img" alt="QR GOWA" style="max-width:240px;background:#fff;padding:10px;border-radius:12px" onerror="var s=document.getElementById('gowa_qr_status');if(s)s.textContent='Gambar QR gagal dimuat — cek log server (base URL/kredensial GOWA).'"> <img id="gowa_qr_img" alt="QR GOWA" style="max-width:240px;background:#fff;padding:10px;border-radius:12px" onerror="var s=document.getElementById('gowa_qr_status');if(s)s.textContent='Gambar QR gagal dimuat — cek log server (base URL/kredensial GOWA).'">
<div id="gowa_qr_status" style="font-size:12px;color:var(--muted);margin-top:6px"></div> <div id="gowa_qr_status" style="font-size:12px;color:var(--muted);margin-top:6px"></div>
@@ -237,6 +239,25 @@
</div> </div>
<script> <script>
async function testGowaConn(btn) {
var el = document.getElementById('gowa_conn_status');
var old = btn ? btn.innerHTML : '';
if (btn) { btn.disabled = true; btn.innerHTML = '<i class="bi bi-hourglass-split"></i> Menguji...'; }
if (el) { el.style.display = 'block'; el.style.color = 'var(--muted)'; el.textContent = 'Menghubungi ' + (document.querySelector('input[name="gowa_base_url"]') || {}).value + ' ...'; }
try {
var r = await fetch('/admin/whatsapp/test-gowa-connection', { method: 'POST' });
var d = await r.json();
if (el) {
el.textContent = d.message || (d.ok ? 'GOWA terjangkau.' : 'GOWA tidak terjangkau.');
el.style.color = (d.ok && d.authOk) ? 'var(--success)' : (d.ok ? '#f59e0b' : 'var(--danger)');
}
} catch (e) {
if (el) { el.style.color = 'var(--danger)'; el.textContent = 'Gagal menguji koneksi: ' + e.message; }
} finally {
if (btn) { btn.disabled = false; btn.innerHTML = old; }
}
}
function toggleGowaMode() { function toggleGowaMode() {
var mode = (document.getElementById('gowa_mode') || {}).value || 'cloud'; var mode = (document.getElementById('gowa_mode') || {}).value || 'cloud';
var hint = document.getElementById('gowa_mode_hint'); var hint = document.getElementById('gowa_mode_hint');