OPEN-ISP v0.5 (pickle-lime) — rilis open-source perdana

Aplikasi billing & manajemen ISP (fiber optic) — PWA pelanggan, portal teknisi & kolektor,
admin panel lengkap, multi-vendor OLT, RADIUS, WhatsApp/Telegram, pembayaran QRIS.

Kredit & lisensi (ISC):
- Base kode & repo: alijayanet/billing-rtrw oleh Alijaya Net — terima kasih banyak
  atas base kode dan repo yang bermanfaat.
- Add-on: GoWA, LibreSpeed/OpenSpeedTest, Uptime Kuma, notification-forwarder.
- Dikembangkan menjadi OPEN-ISP oleh oxpoe — https://github.com/oxpoe/open-isp
This commit is contained in:
oxpoe committed 2026-10-09 05:51:21 +07:00
commit eaf05f2811
280 files changed
+123975

No files matched your search

+5
View File
@@ -0,0 +1,5 @@
-- Add PPPoE manual input fields
ALTER TABLE customers ADD COLUMN pppoe_password TEXT DEFAULT '';
ALTER TABLE customers ADD COLUMN pppoe_remote_address TEXT DEFAULT '';
-- Made with Bob
+65
View File
@@ -0,0 +1,65 @@
#!/usr/bin/env node
/**
* Script untuk menambahkan tabel promo_slides ke database existing
*/
const Database = require('better-sqlite3');
const path = require('path');
const dbPath = path.join(__dirname, '../database/billing.db');
try {
const db = new Database(dbPath);
console.log('[Script] Opening database:', dbPath);
// Cek apakah table sudah ada
const tableExists = db.prepare(`
SELECT name FROM sqlite_master
WHERE type='table' AND name='promo_slides'
`).get();
if (tableExists) {
console.log('[Script] ✓ Table promo_slides sudah ada');
db.close();
process.exit(0);
}
// Buat table jika belum ada
console.log('[Script] Creating table promo_slides...');
db.exec(`
CREATE TABLE IF NOT EXISTS promo_slides (
id INTEGER PRIMARY KEY AUTOINCREMENT,
title TEXT DEFAULT '',
description TEXT DEFAULT '',
image_path TEXT NOT NULL,
url TEXT DEFAULT '',
open_in_new_tab INTEGER DEFAULT 0,
sort_order INTEGER DEFAULT 0,
is_active INTEGER DEFAULT 1,
start_date DATE,
end_date DATE,
created_at DATETIME DEFAULT (CURRENT_TIMESTAMP),
updated_at DATETIME DEFAULT (CURRENT_TIMESTAMP)
);
`);
console.log('[Script] ✓ Table promo_slides berhasil dibuat');
// Verifikasi table
const result = db.prepare(`
SELECT name FROM sqlite_master
WHERE type='table' AND name='promo_slides'
`).get();
if (result) {
console.log('[Script] ✓ Verifikasi berhasil: table promo_slides ada di database');
}
db.close();
console.log('[Script] ✓ Selesai! Database updated successfully.');
process.exit(0);
} catch (e) {
console.error('[Script] ✗ Error:', e.message);
process.exit(1);
}
+36
View File
@@ -0,0 +1,36 @@
/**
* Script untuk memeriksa data Mikhmon di database
*/
const path = require('path');
const Database = require('better-sqlite3');
const dbPath = path.join(__dirname, '../database/billing.db');
const db = new Database(dbPath);
console.log('=== MEMERIKSA DATA MIKHMON DI DATABASE ===\n');
// Cari profiles dengan Mikhmon
const profiles = db.prepare(`
SELECT name, onLogin
FROM hotspot_profiles
WHERE onLogin LIKE '%rem%'
LIMIT 10
`).all();
console.log(`Ditemukan ${profiles.length} profile dengan Mikhmon:\n`);
if (profiles.length === 0) {
console.log('Tidak ada profile dengan Mikhmon di database.');
} else {
profiles.forEach((p, i) => {
console.log(`--- Profile ${i + 1}: ${p.name} ---`);
console.log('onLogin:', p.onLogin ? p.onLogin.substring(0, 150) + (p.onLogin.length > 150 ? '...' : '') : 'null');
console.log('');
});
}
// Cek semua profiles
const allProfiles = db.prepare('SELECT name, onLogin FROM hotspot_profiles LIMIT 20').all();
console.log(`\nTotal profiles di database: ${allProfiles.length}`);
db.close();
+33
View File
@@ -0,0 +1,33 @@
-- Script SQL untuk menambahkan kolom yang hilang di tabel customers
-- Aman dijalankan berkali-kali (akan skip jika kolom sudah ada)
-- Kolom Hotspot
ALTER TABLE customers ADD COLUMN hotspot_username TEXT DEFAULT '';
ALTER TABLE customers ADD COLUMN hotspot_password TEXT DEFAULT '';
ALTER TABLE customers ADD COLUMN hotspot_profile TEXT DEFAULT '';
-- Kolom Connection Type
ALTER TABLE customers ADD COLUMN mac_address TEXT;
ALTER TABLE customers ADD COLUMN connection_type TEXT DEFAULT 'pppoe';
ALTER TABLE customers ADD COLUMN static_ip TEXT;
-- Kolom Auto Isolate
ALTER TABLE customers ADD COLUMN auto_isolate INTEGER DEFAULT 1;
ALTER TABLE customers ADD COLUMN isolate_day INTEGER DEFAULT 10;
-- Kolom Email
ALTER TABLE customers ADD COLUMN email TEXT DEFAULT '';
-- Kolom Router & OLT
ALTER TABLE customers ADD COLUMN router_id INTEGER REFERENCES routers(id) ON DELETE SET NULL;
ALTER TABLE customers ADD COLUMN olt_id INTEGER REFERENCES olts(id) ON DELETE SET NULL;
ALTER TABLE customers ADD COLUMN pon_port TEXT DEFAULT '';
ALTER TABLE customers ADD COLUMN odp_id INTEGER REFERENCES odps(id) ON DELETE SET NULL;
-- Kolom Lokasi
ALTER TABLE customers ADD COLUMN lat TEXT;
ALTER TABLE customers ADD COLUMN lng TEXT;
ALTER TABLE customers ADD COLUMN cable_path TEXT;
-- Kolom Promo
ALTER TABLE customers ADD COLUMN promo_cycles_used INTEGER DEFAULT 0;
+59
View File
@@ -0,0 +1,59 @@
#!/bin/bash
# Script untuk memperbaiki struktur database di server produksi
# Menambahkan kolom yang hilang di tabel customers
echo "=========================================="
echo "Fix Database Columns - Billing RTRW"
echo "=========================================="
echo ""
# Cek apakah database ada
DB_PATH="database/billing.db"
if [ ! -f "$DB_PATH" ]; then
echo "❌ Error: Database tidak ditemukan di $DB_PATH"
echo " Pastikan Anda menjalankan script ini dari root folder aplikasi"
exit 1
fi
echo "✓ Database ditemukan: $DB_PATH"
echo ""
# Backup database
BACKUP_PATH="database/billing.db.backup-$(date +%Y%m%d-%H%M%S)"
echo "📦 Membuat backup database..."
cp "$DB_PATH" "$BACKUP_PATH"
if [ $? -eq 0 ]; then
echo "✓ Backup berhasil: $BACKUP_PATH"
else
echo "❌ Gagal membuat backup!"
exit 1
fi
echo ""
# Jalankan script verifikasi
echo "🔍 Memeriksa struktur database..."
echo ""
node scripts/verify-database.js
if [ $? -eq 0 ]; then
echo ""
echo "=========================================="
echo "✓ Selesai!"
echo "=========================================="
echo ""
echo "Langkah selanjutnya:"
echo "1. Restart aplikasi: pm2 restart all"
echo "2. Test login pelanggan"
echo "3. Cek log untuk memastikan tidak ada error"
echo ""
echo "Jika ada masalah, restore backup:"
echo "cp $BACKUP_PATH $DB_PATH"
echo ""
else
echo ""
echo "❌ Terjadi error saat memperbaiki database"
echo " Database tidak diubah, backup tersimpan di:"
echo " $BACKUP_PATH"
exit 1
fi
+49
View File
@@ -0,0 +1,49 @@
/**
* Migration Script: Add Photo Fields to Attendance Table
* Run this once to update existing database
*/
const Database = require('better-sqlite3');
const path = require('path');
const dbPath = path.join(__dirname, '../database/billing.db');
const db = new Database(dbPath);
console.log('🔄 Starting migration: Add photo fields to attendance table...');
try {
// Check if columns already exist
const tableInfo = db.prepare("PRAGMA table_info(attendance)").all();
const columnNames = tableInfo.map(col => col.name);
const hasCheckInPhoto = columnNames.includes('check_in_photo');
const hasCheckOutPhoto = columnNames.includes('check_out_photo');
if (hasCheckInPhoto && hasCheckOutPhoto) {
console.log('✅ Migration already applied. All columns exist.');
process.exit(0);
}
// Add columns if they don't exist
if (!hasCheckInPhoto) {
console.log('➕ Adding column: check_in_photo');
db.exec("ALTER TABLE attendance ADD COLUMN check_in_photo TEXT DEFAULT ''");
}
if (!hasCheckOutPhoto) {
console.log('➕ Adding column: check_out_photo');
db.exec("ALTER TABLE attendance ADD COLUMN check_out_photo TEXT DEFAULT ''");
}
console.log('✅ Migration completed successfully!');
console.log('');
console.log('New columns added:');
console.log(' - check_in_photo: Path foto saat check-in');
console.log(' - check_out_photo: Path foto saat check-out');
} catch (error) {
console.error('❌ Migration failed:', error.message);
process.exit(1);
} finally {
db.close();
}
+70
View File
@@ -0,0 +1,70 @@
/**
* Migration Script: Add Photo Fields to Tickets Table
* Run this once to update existing database
*/
const Database = require('better-sqlite3');
const path = require('path');
const dbPath = path.join(__dirname, '../database/billing.db');
const db = new Database(dbPath);
console.log('🔄 Starting migration: Add photo fields to tickets table...');
try {
// Check if columns already exist
const tableInfo = db.prepare("PRAGMA table_info(tickets)").all();
const columnNames = tableInfo.map(col => col.name);
const hasNotes = columnNames.includes('technician_notes');
const hasPhotos = columnNames.includes('photos');
const hasMetadata = columnNames.includes('photo_metadata');
const hasCustomerPhotos = columnNames.includes('customer_photos');
const hasCustomerMetadata = columnNames.includes('customer_photo_metadata');
if (hasNotes && hasPhotos && hasMetadata && hasCustomerPhotos && hasCustomerMetadata) {
console.log('✅ Migration already applied. All columns exist.');
process.exit(0);
}
// Add columns if they don't exist
if (!hasNotes) {
console.log('➕ Adding column: technician_notes');
db.exec("ALTER TABLE tickets ADD COLUMN technician_notes TEXT DEFAULT ''");
}
if (!hasPhotos) {
console.log('➕ Adding column: photos');
db.exec("ALTER TABLE tickets ADD COLUMN photos TEXT DEFAULT ''");
}
if (!hasMetadata) {
console.log('➕ Adding column: photo_metadata');
db.exec("ALTER TABLE tickets ADD COLUMN photo_metadata TEXT DEFAULT ''");
}
if (!hasCustomerPhotos) {
console.log('➕ Adding column: customer_photos');
db.exec("ALTER TABLE tickets ADD COLUMN customer_photos TEXT DEFAULT ''");
}
if (!hasCustomerMetadata) {
console.log('➕ Adding column: customer_photo_metadata');
db.exec("ALTER TABLE tickets ADD COLUMN customer_photo_metadata TEXT DEFAULT ''");
}
console.log('✅ Migration completed successfully!');
console.log('');
console.log('New columns added:');
console.log(' - technician_notes: Catatan teknisi saat menyelesaikan tugas');
console.log(' - photos: JSON array path foto yang diupload teknisi');
console.log(' - photo_metadata: JSON array metadata foto teknisi (timestamp, GPS, dll)');
console.log(' - customer_photos: JSON array path foto yang diupload pelanggan');
console.log(' - customer_photo_metadata: JSON array metadata foto pelanggan');
} catch (error) {
console.error('❌ Migration failed:', error.message);
process.exit(1);
} finally {
db.close();
}
+345
View File
@@ -0,0 +1,345 @@
#!/usr/bin/env node
/**
* OLT Probe — deteksi cepat kompatibilitas OLT baru (multi-vendor).
*
* Tujuan: sebelum/saat membeli OLT merk baru, jalankan probe ini untuk tahu:
* 1. SNMP hidup? vendor-nya siapa (dari sysObjectID / enterprise OID)?
* 2. Profil brand bawaan mana yang cocok (hioso/hsgq/cdata/vsol/generic/...)?
* 3. Telnet/SSH/Web UI terbuka? keluarga CLI-nya apa?
* 4. Rekomendasi: brand yang dipilih di aplikasi + apakah rename perlu adapter.
*
* HANYA perintah BACA (read-only). Tidak mengubah konfigurasi apa pun.
*
* Contoh:
* node scripts/olt-probe.js --from-db 1
* node scripts/olt-probe.js --host 192.168.16.2 --user admin --pass admin
* node scripts/olt-probe.js --host 10.0.0.9 --community public --cmds "show version,show onu"
* node scripts/olt-probe.js --from-db 1 --json /tmp/probe-olt1.json
*/
const net = require('net');
const http = require('http');
const https = require('https');
const snmp = require('net-snmp');
const oltSvc = require('../services/oltService');
/* ── Pemetaan enterprise OID → vendor (untuk tebakan cepat) ───────────── */
const ENTERPRISE_VENDOR = {
'25355': 'Hioso',
'3320': 'CTC-style EPON (umum di OLT China)',
'3902': 'ZTE',
'2011': 'Huawei',
'34592': 'C-Data',
'37950': 'V-SOL',
'27332': 'FiberHome',
'35048': 'BDCOM?',
'13158': 'Cisco (umum)',
'8072': 'Net-SNMP generik — vendor tidak menyetel sysObjectID (banyak OLT embedded Linux); penentuan pakai hasil "Profil brand" di bawah',
};
/* ── util ─────────────────────────────────────────────────────────────── */
function arg(name, def = null) {
const i = process.argv.indexOf('--' + name);
if (i === -1) return def;
const v = process.argv[i + 1];
return (v && !v.startsWith('--')) ? v : true;
}
function tcpCheck(host, port, timeout = 3000) {
return new Promise((resolve) => {
const s = new net.Socket();
const done = (ok) => { try { s.destroy(); } catch (e) {} resolve(ok); };
s.setTimeout(timeout);
s.once('connect', () => done(true));
s.once('timeout', () => done(false));
s.once('error', () => done(false));
s.connect(port, host);
});
}
function snmpValueToString(value) {
try {
let v = value;
if (Buffer.isBuffer(v)) v = v.toString('utf8');
v = String(v == null ? '' : v).replace(/\0/g, '').trim();
// beberapa perangkat mengembalikan string ber-quote
if ((v.startsWith('"') && v.endsWith('"')) || (v.startsWith("'") && v.endsWith("'"))) v = v.slice(1, -1).trim();
return v;
} catch (e) { return String(value); }
}
function looksLikeOidString(s) {
return /^\d+(\.\d+){4,}$/.test(String(s || '').trim());
}
function decodeOid(buf) {
try {
const b = Buffer.from(buf);
if (!b.length) return '';
const out = [Math.floor(b[0] / 40), b[0] % 40];
let val = 0;
for (let i = 1; i < b.length; i++) {
val = (val << 7) | (b[i] & 0x7f);
if (!(b[i] & 0x80)) { out.push(val); val = 0; }
}
return out.join('.');
} catch (e) { return String(buf); }
}
function snmpGet(session, oids, timeout = 4000) {
return new Promise((resolve) => {
let finished = false;
const t = setTimeout(() => { if (!finished) { finished = true; resolve(null); } }, timeout);
try {
session.get(oids, (err, vbs) => {
if (finished) return;
finished = true; clearTimeout(t);
resolve(err ? null : vbs);
});
} catch (e) { if (!finished) { finished = true; clearTimeout(t); resolve(null); } }
});
}
function snmpSubtreeCount(session, oid, limit = 8, timeout = 5000) {
return new Promise((resolve) => {
const rows = [];
let finished = false;
const done = () => { if (!finished) { finished = true; resolve(rows); } };
const t = setTimeout(done, timeout);
try {
session.subtree(oid, 20, (vbs) => {
for (const vb of vbs) {
if (rows.length >= limit) break;
let val = vb.value;
if (Buffer.isBuffer(val)) {
const s = val.toString('utf8').replace(/\0/g, '').trim();
val = /^[\x20-\x7E]+$/.test(s) && s.length >= 2 ? s : val.toString('hex').toUpperCase();
}
rows.push({ oid: vb.oid, value: String(val).slice(0, 60) });
}
if (rows.length >= limit) { try { session.close(); } catch (e) {} }
}, () => { clearTimeout(t); done(); });
} catch (e) { clearTimeout(t); done(); }
});
}
function httpProbe(url, timeout = 5000, allowSelfSigned = false) {
return new Promise((resolve) => {
try {
const u = new URL(url);
const lib = u.protocol === 'https:' ? https : http;
const req = lib.request({
host: u.hostname, port: u.port || (u.protocol === 'https:' ? 443 : 80), path: u.pathname,
method: 'GET', timeout,
rejectUnauthorized: false,
headers: { 'User-Agent': 'OPEN-ISP-OLT-Probe/1.0' },
}, (res) => {
let body = '';
res.on('data', (d) => { if (body.length < 4000) body += d.toString('utf8'); });
res.on('end', () => {
const title = (body.match(/<title[^>]*>([^<]{0,80})</i) || [])[1] || '';
resolve({ ok: true, status: res.statusCode, server: res.headers.server || '', title: title.trim() });
});
});
req.on('timeout', () => { req.destroy(); resolve({ ok: false, why: 'timeout' }); });
req.on('error', (e) => resolve({ ok: false, why: e.code || e.message }));
req.end();
} catch (e) { resolve({ ok: false, why: e.message }); }
});
}
/* Fallback telnet yang tahan terhadap prompt aneh (mis. '#' tanpa newline) */
async function rawTelnetRun(host, port, user, pass, cmds, waitMs = 2200) {
const sock = new net.Socket();
let buf = '';
sock.setTimeout(15000);
sock.on('data', (d) => { buf += d.toString('utf8'); });
await new Promise((resolve, reject) => {
sock.connect(port, host, resolve);
sock.once('error', reject);
});
const wait = (ms) => new Promise(r => setTimeout(r, ms));
const drain = () => { const x = buf; buf = ''; return x; };
const clean = (t) => String(t).replace(/\x1b\[[0-9;]*[A-Za-z]/g, '').replace(/\0/g, '').replace(/[^\x20-\x7E\r\n]/g, '');
await wait(2500);
let banner = clean(drain());
if (/(login|username)\s*[:>]/i.test(banner)) { sock.write(String(user || 'admin') + '\r\n'); await wait(1600); banner += clean(drain()); }
if (/(password|passwd)\s*[:>]/i.test(banner)) { sock.write(String(pass || 'admin') + '\r\n'); await wait(1800); banner += clean(drain()); }
sock.write('\r\n'); await wait(800); banner += clean(drain());
const outputs = [];
for (const cmd of cmds) {
sock.write(cmd + '\r\n');
await wait(waitMs);
outputs.push(clean(drain()));
}
try { sock.end(); sock.destroy(); } catch (e) {}
return { banner: banner.trim(), outputs };
}
function truncate(s, n) { s = String(s || ''); return s.length > n ? (s.slice(0, n) + '\n…[dipotong]') : s; }
/* ── main ─────────────────────────────────────────────────────────────── */
(async () => {
const fromDb = arg('from-db');
let host = arg('host');
let community = arg('community', 'public');
let snmpPort = Number(arg('snmp-port', 161));
let user = arg('user', 'admin');
let pass = arg('pass', 'admin');
let telnetPort = Number(arg('telnet-port', 23));
let cmds = String(arg('cmds', 'show version')).split(',').map(s => s.trim()).filter(Boolean);
const jsonOut = arg('json');
const enablePassword = arg('enable-password');
if (fromDb) {
const db = require('../config/database');
const olt = db.prepare('SELECT * FROM olts WHERE id = ?').get(Number(fromDb));
if (!olt) { console.error('OLT id=' + fromDb + ' tidak ditemukan di database'); process.exit(1); }
host = host || olt.host;
community = (community === 'public') ? (olt.snmp_community || community) : community;
snmpPort = snmpPort || Number(olt.snmp_port || 161);
user = (user === 'admin') ? (olt.web_user || user) : user;
pass = (pass === 'admin') ? (olt.web_password || pass) : pass;
telnetPort = telnetPort || Number(olt.telnet_port || 23);
console.log(`[i] Memakai data OLT dari DB: id=${olt.id} "${olt.name}" brand=${olt.brand} host=${host}`);
}
if (!host) {
console.log('Penggunaan: node scripts/olt-probe.js --host <ip> [--community public] [--user admin --pass admin]\n' +
' node scripts/olt-probe.js --from-db <id-olt> [--json hasil.json]');
process.exit(1);
}
const result = { host, at: new Date().toISOString(), snmp: null, tcp: {}, profiles: [], telnet: null, web: null, verdict: [] };
console.log(`\n=== OLT PROBE — ${host} ===\n`);
/* 1) cek port TCP */
console.log('-- Konektivitas --');
for (const p of [23, 22, 80, 443]) {
const ok = await tcpCheck(host, p);
result.tcp[p] = ok;
console.log(` port ${String(p).padEnd(3)} : ${ok ? 'TERBUKA' : 'tertutup/tidak merespons'}`);
}
/* 2) SNMP: identitas */
console.log('\n-- SNMP --');
const session = snmp.createSession(host, community, { port: snmpPort, version: snmp.Version2c, timeout: 4000, retries: 1 });
const idVbs = await snmpGet(session, ['1.3.6.1.2.1.1.1.0', '1.3.6.1.2.1.1.2.0', '1.3.6.1.2.1.1.5.0', '1.3.6.1.2.1.1.3.0'], 5000);
if (!idVbs) {
console.log(' SNMP: TIDAK MERESPONS (community salah / SNMP mati / port beda)');
result.snmp = { ok: false };
} else {
const sysDescr = idVbs[0] && idVbs[0].value != null ? snmpValueToString(idVbs[0].value) : '-';
const sysObjRaw = idVbs[1] && idVbs[1].value != null ? snmpValueToString(idVbs[1].value) : '';
const sysObjFromBuf = idVbs[1] && idVbs[1].value != null ? decodeOid(idVbs[1].value) : '-';
const sysObj = looksLikeOidString(sysObjRaw) ? sysObjRaw : (looksLikeOidString(sysObjFromBuf) ? sysObjFromBuf : sysObjFromBuf);
const sysName = idVbs[2] && idVbs[2].value != null ? snmpValueToString(idVbs[2].value) : '-';
const uptime = idVbs[3] && idVbs[3].value != null ? String(idVbs[3].value) : '-';
const ent = (sysObj.split('.')[6] || '');
let vendor = ENTERPRISE_VENDOR[ent] || '';
if (!vendor) {
// fallback: cari enterprise di daftar berdasarkan kemunculan substring OID
for (const [k, v] of Object.entries(ENTERPRISE_VENDOR)) {
if (sysObj.includes('4.1.' + k + '.')) { vendor = v + ' (terdeteksi dari OID)'; break; }
}
}
if (!vendor) vendor = ent ? `(enterprise ${ent} — belum terdaftar, laporkan ke developer)` : '-';
result.snmp = { ok: true, sysDescr, sysObjectID: sysObj, sysName, uptime, enterprise: ent, vendorGuess: vendor };
console.log(` sysDescr : ${truncate(sysDescr, 120)}`);
console.log(` sysObjectID : ${sysObj}`);
console.log(` enterprise : ${ent} → tebakan vendor: ${vendor}`);
console.log(` sysName : ${sysName}`);
}
/* 3) probe semua profil brand bawaan */
if (result.snmp && result.snmp.ok) {
console.log('\n-- Profil brand bawaan (uji OID probe) --');
for (const brandKey of Object.keys(oltSvc.BRAND_PROFILES || {})) {
for (const prof of (oltSvc.BRAND_PROFILES[brandKey] || [])) {
const probe = prof.probe_oid;
if (!probe) continue;
const vbs = await snmpGet(session, [probe], 3500);
if (vbs) {
const sample = await snmpSubtreeCount(session, prof.status_table || probe, 5, 5000);
result.profiles.push({ brandKey, profile: prof.name, ok: true, sample: sample.slice(0, 5) });
console.log(` ✅ ${brandKey.toUpperCase()} / ${prof.name} — HIDUP (contoh ${sample.length} entri: ${sample[0] ? sample[0].value : '-'} ...)`);
} else {
result.profiles.push({ brandKey, profile: prof.name, ok: false });
}
}
}
if (!result.profiles.some(p => p.ok)) console.log(' (tidak ada profil yang cocok — kemungkinan MIB privat; kirim hasil JSON ini ke developer)');
}
try { session.close(); } catch (e) {}
/* 4) Telnet (read-only) */
console.log('\n-- Telnet CLI --');
try {
const out = await oltSvc.telnetLoginAndRun(host, user, pass, cmds, {
port: telnetPort,
enablePassword: enablePassword !== null ? String(enablePassword) : null,
});
result.telnet = { ok: true, cmds, output: truncate(out, 2500) };
const head = String(out).split('\n').map(l => l.trim()).filter(Boolean).slice(0, 12).join('\n ');
console.log(` ✅ Login telnet BERHASIL (${cmds.join(' | ')}) — cuplikan:\n ${head}`);
const hay = String(out).toLowerCase();
const fam = hay.includes('hioso') ? 'Hioso' : hay.includes('hsgq') ? 'HSGQ' : hay.includes('c-data') || hay.includes('cdata') ? 'C-Data' : hay.includes('vsol') ? 'V-SOL' : null;
if (fam) console.log(` [i] Keluarga CLI terdeteksi: ${fam}`);
} catch (e) {
result.telnet = { ok: false, error: e.message || String(e) };
console.log(' ✖ Telnet gagal: ' + (e.message || String(e)));
// Diagnostik: tangkap banner mentah 4 detik agar terlihat prompt/flow-nya
try {
const raw = await new Promise((resolve) => {
const sock = new net.Socket();
let buf = '';
const t = setTimeout(() => { try { sock.destroy(); } catch (e2) {} resolve(buf); }, 4000);
sock.setTimeout(4000);
sock.on('data', (d) => { buf += d.toString('utf8').replace(/\0/g, ''); });
sock.once('error', () => { clearTimeout(t); resolve(buf); });
sock.once('timeout', () => { clearTimeout(t); try { sock.destroy(); } catch (e2) {} resolve(buf); });
sock.connect(telnetPort, host, () => {});
});
const clean = String(raw).replace(/\x1b\[[0-9;]*[A-Za-z]/g, '').replace(/[^\x20-\x7E\r\n]/g, '').trim();
result.telnet.rawBanner = truncate(clean, 600);
console.log(' [i] Banner telnet mentah:\n ' + truncate(clean, 400).split('\n').join('\n '));
} catch (e2) {}
// Fallback: mode raw (prompt longgar) — tetap READ-ONLY
try {
console.log(' [i] Mencoba mode telnet raw (prompt longgar)…');
const raw = await rawTelnetRun(host, telnetPort, user, pass, cmds);
result.telnetRaw = { ok: true, banner: truncate(raw.banner, 500), outputs: raw.outputs.map(o => truncate(o, 1200)) };
const joined = (raw.banner + '\n' + raw.outputs.join('\n'));
const head = String(joined).split('\n').map(l => l.trim()).filter(Boolean).slice(0, 10).join('\n ');
console.log(' ✅ Telnet RAW berhasil — cuplikan:\n ' + head);
const hay = joined.toLowerCase();
const fam = hay.includes('hioso') ? 'Hioso' : hay.includes('hsgq') ? 'HSGQ' : hay.includes('c-data') || hay.includes('cdata') ? 'C-Data' : hay.includes('vsol') ? 'V-SOL' : hay.includes('gpon') ? 'GPON-generik' : hay.includes('epon') ? 'EPON-generik' : null;
if (fam) console.log(' [i] Keluarga CLI terdeteksi (raw): ' + fam);
} catch (e3) {
console.log(' ✖ Telnet raw juga gagal: ' + (e3.message || String(e3)));
}
}
/* 5) Web UI */
console.log('\n-- Web UI --');
for (const url of [`http://${host}/`, `https://${host}/`]) {
const w = await httpProbe(url, 5000);
if (w.ok) {
result.web = { url, ...w };
console.log(` ${url} → HTTP ${w.status} | Server: ${w.server || '-'} | Title: ${w.title || '-'}`);
break;
} else {
console.log(` ${url} → ${w.why}`);
}
}
/* 6) kesimpulan */
console.log('\n=== KESIMPULAN ===');
const live = [...new Set(result.profiles.filter(p => p.ok).map(p => p.brandKey))];
if (result.snmp && result.snmp.ok) {
if (live.length) console.log(`✅ Pilih brand di aplikasi: ${live.map(s => s.toUpperCase()).join(' / ')} (monitoring SNMP siap)`);
else console.log('⚠️ SNMP hidup tapi tidak ada profil cocok → perlu profil baru (kirim JSON hasil ini).');
} else {
console.log('⚠️ SNMP mati/kosong. Jika telnet hidup, monitoring bisa dibuat via driver CLI (kirim cuplikan telnet ini).');
}
if (result.telnet && result.telnet.ok) console.log('ℹ️ Telnet hidup → jalur rename/authorize CLI memungkinkan (tergantung keluarga CLI).');
console.log('ℹ️ Rename nama pelanggan→OLT saat ini: hioso/hsgq (SNMP) & zte/huawei (CLI). Brand lain perlu adapter.');
if (jsonOut) {
require('fs').writeFileSync(String(jsonOut), JSON.stringify(result, null, 2));
console.log(`\n[i] Hasil lengkap disimpan: ${jsonOut} (kirim file ini untuk analisis lanjutan)`);
}
process.exit(0);
})().catch((e) => { console.error('Probe error:', e); process.exit(1); });
+92
View File
@@ -0,0 +1,92 @@
#!/usr/bin/env node
// Script untuk restart otomatis aplikasi jika terjadi error
const { spawn } = require('child_process');
const fs = require('fs');
const path = require('path');
const APP_SCRIPT = 'app-whatsapp-only.js';
const LOG_FILE = 'logs/restart.log';
const MAX_RESTARTS = 5;
const RESTART_DELAY = 10000; // 10 detik
let restartCount = 0;
let isRestarting = false;
// Fungsi untuk menulis log
function writeLog(message) {
const timestamp = new Date().toISOString();
const logEntry = `[${timestamp}] ${message}\n`;
// Pastikan direktori logs ada
const logDir = path.dirname(LOG_FILE);
if (!fs.existsSync(logDir)) {
fs.mkdirSync(logDir, { recursive: true });
}
fs.appendFileSync(LOG_FILE, logEntry);
console.log(message);
}
// Fungsi untuk start aplikasi
function startApp() {
if (isRestarting) {
writeLog('App is already restarting, skipping...');
return;
}
writeLog(`Starting application (attempt ${restartCount + 1}/${MAX_RESTARTS})`);
const app = spawn('node', [APP_SCRIPT], {
stdio: ['inherit', 'inherit', 'inherit'],
detached: false
});
app.on('error', (error) => {
writeLog(`Error starting app: ${error.message}`);
handleAppError();
});
app.on('exit', (code, signal) => {
writeLog(`App exited with code ${code} and signal ${signal}`);
if (code !== 0 && restartCount < MAX_RESTARTS) {
handleAppError();
} else if (restartCount >= MAX_RESTARTS) {
writeLog(`Maximum restart attempts (${MAX_RESTARTS}) reached. Stopping restart attempts.`);
process.exit(1);
}
});
// Handle process termination
process.on('SIGINT', () => {
writeLog('Received SIGINT, terminating app...');
app.kill('SIGINT');
process.exit(0);
});
process.on('SIGTERM', () => {
writeLog('Received SIGTERM, terminating app...');
app.kill('SIGTERM');
process.exit(0);
});
}
// Fungsi untuk handle error aplikasi
function handleAppError() {
if (isRestarting) return;
isRestarting = true;
restartCount++;
writeLog(`App crashed, restarting in ${RESTART_DELAY/1000} seconds...`);
setTimeout(() => {
isRestarting = false;
startApp();
}, RESTART_DELAY);
}
// Main execution
writeLog('Starting application with auto-restart enabled');
startApp();
+129
View File
@@ -0,0 +1,129 @@
#!/usr/bin/env node
/**
* OPEN-ISP — Seed Data Demo untuk Tester
* ======================================
* Mengisi database dengan data contoh agar dashboard bisa langsung dilihat:
* paket, area, pelanggan, tagihan (sebagian lunas), tiket, dan voucher.
*
* Jalankan (SEKALI saja, aman — tidak menimpa bila sudah ada data):
* node scripts/seed-demo.js
*
* Untuk Docker:
* docker exec open-isp node scripts/seed-demo.js
*
* Login default hasil seed:
* ADMIN : admin / demo-isp123
* PELANGGAN: lihat "Login Pelanggan" di output (ID Pelanggan / No. WhatsApp)
*/
const db = require('../config/database');
const customerSvc = require('../services/customerService');
const billingSvc = require('../services/billingService');
const log = (...a) => console.log('[seed-demo]', ...a);
function count(tbl) {
try { return db.prepare(`SELECT COUNT(*) c FROM ${tbl}`).get().c; } catch (e) { return 0; }
}
(async () => {
/* ── Guard: jangan timpa data yang sudah ada ─────────────────────────── */
const existing = count('customers') + count('packages');
if (existing > 0) {
log('Database sudah berisi data (customers/packages > 0) — seed DILEWATI.');
log('(Hapus isi database bila ingin seed ulang di instalasi bersih.)');
process.exit(0);
}
/* ── 1. Paket ─────────────────────────────────────────────────────────── */
const pkgs = [
{ name: 'Demo Lite 10 Mbps', speed_down: 10, speed_up: 10, price: 100000, description: 'Paket hemat untuk kebutuhan dasar', billing_type: 'postpaid' },
{ name: 'Demo Home 20 Mbps', speed_down: 20, speed_up: 20, price: 130000, description: 'Paket favorit keluarga', billing_type: 'postpaid' },
{ name: 'Demo Pro 30 Mbps', speed_down: 30, speed_up: 30, price: 175000, description: 'Bekerja & streaming lancar', billing_type: 'postpaid' },
{ name: 'Demo Bisnis 50 Mbps', speed_down: 50, speed_up: 50, price: 275000, description: 'Untuk kantor & usaha', billing_type: 'postpaid' },
];
const pkgIds = [];
for (const p of pkgs) {
try { const r = customerSvc.createPackage(p); pkgIds.push(r.lastInsertRowid); log('Paket:', p.name); }
catch (e) { log('Gagal paket', p.name, e.message); }
}
/* ── 2. Area ──────────────────────────────────────────────────────────── */
const areas = ['Demo Area Pusat', 'Demo Area Timur', 'Demo Area Barat'];
for (const a of areas) {
try { db.prepare('INSERT OR IGNORE INTO areas (name, description) VALUES (?, ?)').run(a, 'Area contoh (data demo)'); log('Area:', a); }
catch (e) { log('Gagal area', a, e.message); }
}
/* ── 3. Pelanggan ─────────────────────────────────────────────────────── */
const custs = [
['Budi Santoso', '081200000001', 'Jl. Merdeka No. 1'],
['Siti Aminah', '081200000002', 'Jl. Sudirman No. 12'],
['Agus Wijaya', '081200000003', 'Jl. Diponegoro No. 3'],
['Dewi Lestari', '081200000004', 'Jl. Gajah Mada No. 44'],
['Rudi Hartono', '081200000005', 'Jl. Hayam Wuruk No. 8'],
['Sri Wahyuni', '081200000006', 'Jl. Pahlawan No. 21'],
['Joko Susilo', '081200000007', 'Jl. Ahmad Yani No. 5'],
['Ratna Sari', '081200000008', 'Jl. Kartini No. 17'],
['Hendra Gunawan', '081200000009', 'Jl. Cendrawasih No. 9'],
['Maya Putri', '081200000010', 'Jl. Melati No. 2'],
['Anton Prasetyo', '081200000011', 'Jl. Kenanga No. 30'],
['Lina Marlina', '081200000012', 'Jl. Anggrek No. 7'],
];
const today = new Date();
const installDate = new Date(today.getTime() - 35 * 86400000).toISOString().slice(0, 10);
const custIds = [];
let i = 0;
for (const [name, phone, address] of custs) {
i++;
const pkgId = pkgIds[(i - 1) % (pkgIds.length || 1)];
try {
const r = customerSvc.createCustomer({
name, phone, address,
area: areas[(i - 1) % areas.length],
package_id: pkgId,
status: (i === 11 || i === 12) ? 'inactive' : 'active', // 2 pelanggan nonaktif utk variasi
install_date: installDate,
connection_type: 'pppoe',
pppoe_username: `demo${String(i).padStart(2, '0')}@demo.id`,
pppoe_password: 'demo-isp123',
auto_isolate: 1,
isolate_day: 10,
notes: 'Data demo — silakan hapus',
});
custIds.push({ id: r.lastInsertRowid, name, phone });
log('Pelanggan:', name);
} catch (e) { log('Gagal pelanggan', name, e.message); }
}
/* ── 4. Tagihan bulan ini + sebagian dibuat LUNAS ─────────────────────── */
const month = today.getMonth() + 1;
const year = today.getFullYear();
let invCount = 0;
try { invCount = billingSvc.generateMonthlyInvoices(month, year); log(`Generate tagihan ${month}/${year}:`, invCount); }
catch (e) { log('Gagal generate tagihan:', e.message); }
// 8 pelanggan pertama -> LUNAS, sisanya dibiarkan belum bayar
let paid = 0;
for (let k = 0; k < Math.min(8, custIds.length); k++) {
try { billingSvc.payInvoiceForCustomerPeriod(custIds[k].id, month, year, 'Admin Demo', 'Pembayaran demo'); paid++; }
catch (e) { log('Gagal pelunasan', custIds[k].name, e.message); }
}
log(`Pelunasan demo: ${paid} pelanggan`);
/* ── 5. Tiket contoh ──────────────────────────────────────────────────── */
try {
const ticketSvc = require('../services/ticketService');
if (custIds[0]) ticketSvc.createTicket(custIds[0].id, 'Koneksi lambat', 'Sejak tadi malam internet terasa lambat, mohon dicek. (data demo)');
if (custIds[1]) ticketSvc.createTicket(custIds[1].id, 'Modem mati', 'Lampu modem tidak menyala sejak pagi. (data demo)');
log('Tiket demo: 2');
} catch (e) { log('Tiket dilewati:', e.message); }
/* ── Ringkasan ────────────────────────────────────────────────────────── */
const demo = db.prepare("SELECT customer_code, name, phone FROM customers WHERE notes LIKE 'Data demo%' LIMIT 2").all();
log('==================== SELESAI ====================');
log('Login ADMIN : admin / demo-isp123');
log('Login PELANGGAN: gunakan ID Pelanggan atau No. WhatsApp di bawah ini');
demo.forEach(d => log(` • ${d.name} — ${d.customer_code} / ${d.phone}`));
log('=================================================');
process.exit(0);
})().catch(e => { console.error('[seed-demo] ERROR:', e.message); process.exit(1); });
+56
View File
@@ -0,0 +1,56 @@
#!/usr/bin/env node
/**
* Jalankan manual sinkronisasi ONU (auto-isi MAC pelanggan + rename nama ONU).
*
* Contoh:
* node scripts/sync-onu-names.js --dry-run
* node scripts/sync-onu-names.js --olt 1
* node scripts/sync-onu-names.js --skip-fill
* node scripts/sync-onu-names.js
*/
const onuSync = require('../services/onuSyncService');
function printOltSummary(s) {
console.log(`${s.oltName} (id=${s.oltId}): total=${s.total}, rename=${s.renamed}, sama=${s.same}, belum-match=${s.unmatch}, gagal=${s.failed}`);
for (const e of s.errors || []) {
console.log(` GAGAL ${e.onu || ''}${e.sn ? ' (' + e.sn + ')' : ''} => ${e.target || ''}: ${e.error || ''}`);
}
}
function printMacSummary(m) {
if (!m || m.skipped) {
console.log('Auto-isi mac_address: dilewati (--skip-fill)');
return;
}
console.log(
`Auto-isi mac_address: total=${m.total}, diisi=${m.filled}, diperbarui=${m.refreshed}, ` +
`bukan-MAC-ONU=${m.skippedNonOnu}, tanpa-pelanggan=${m.noCustomer}, konflik=${m.conflict}, gagal=${m.errors.length}`
);
for (const e of m.errors) console.log(` GAGAL ${e.customer}: ${e.error}`);
}
(async () => {
const args = process.argv.slice(2);
const dryRun = args.includes('--dry-run');
const skipFill = args.includes('--skip-fill');
const skipNames = args.includes('--skip-names');
const oltIdx = args.indexOf('--olt');
const oltId = oltIdx >= 0 && args[oltIdx + 1] ? args[oltIdx + 1] : null;
if (dryRun) console.log('MODE: DRY-RUN (tidak ada perubahan nyata)');
const { mac, olts } = await onuSync.runSyncOnu({ dryRun, oltId, skipFill, skipNames });
console.log('-- Auto-isi/perbarui mac_address pelanggan (dari PPPoE aktif) --');
printMacSummary(mac);
if (!skipNames) {
console.log('-- Sinkronisasi nama ONU --');
for (const s of olts || []) printOltSummary(s);
}
process.exit(0);
})().catch((e) => {
console.error('FATAL:', e.message);
process.exit(1);
});
+94
View File
@@ -0,0 +1,94 @@
#!/usr/bin/env node
// Script untuk test admin functionality
const fs = require('fs');
const path = require('path');
// Load settings
const settings = require('../settings.json');
console.log('=== Test Admin Functionality ===\n');
// Test admin numbers
console.log('📋 Admin Configuration:');
console.log(`Admin numbers: ${JSON.stringify(settings.admins)}`);
console.log(`Technician numbers: ${JSON.stringify(settings.technician_numbers)}`);
console.log('');
// Test isAdminNumber function
function testIsAdminNumber(number) {
try {
const cleanNumber = number.replace(/\D/g, '');
// Cek admin dari settings.json
const adminNumbers = settings.admins || [];
for (const adminNumber of adminNumbers) {
const cleanAdminNumber = adminNumber.replace(/\D/g, '');
if (cleanNumber === cleanAdminNumber) {
return true;
}
}
// Cek technician numbers dari settings.json
const technicianNumbers = settings.technician_numbers || [];
for (const techNumber of technicianNumbers) {
const cleanTechNumber = techNumber.replace(/\D/g, '');
if (cleanNumber === cleanTechNumber) {
return true;
}
}
return false;
} catch (error) {
console.error('Error in testIsAdminNumber:', error);
return false;
}
}
// Test beberapa nomor
const testNumbers = [
'6281947215703',
'081947215703',
'083807665697',
'082218094778',
'081234567890' // Nomor test yang bukan admin
];
console.log('🔍 Testing Admin Number Validation:');
for (const number of testNumbers) {
const isAdmin = testIsAdminNumber(number);
console.log(`${number}: ${isAdmin ? '✅ Admin' : '❌ Not Admin'}`);
}
console.log('');
// Test message
const testMessage = `🧪 *TEST ADMIN BOT*\n\n` +
`✅ Ini adalah pesan test untuk memverifikasi fungsi admin\n` +
`📅 Waktu: ${new Date().toLocaleString()}\n\n` +
`🔧 Jika Anda menerima pesan ini, berarti:\n` +
`• Fungsi isAdminNumber berfungsi dengan baik\n` +
`• Pengiriman pesan ke admin berhasil\n` +
`• Bot siap digunakan\n\n` +
`🏢 *ALIJAYA DIGITAL NETWORK*`;
console.log('📝 Test message yang akan dikirim:');
console.log(testMessage);
console.log('');
// Cek file superadmin.txt
try {
const superAdminPath = path.join(__dirname, '../config/superadmin.txt');
if (fs.existsSync(superAdminPath)) {
const superAdmin = fs.readFileSync(superAdminPath, 'utf8').trim();
console.log(`👑 Super admin: ${superAdmin}`);
} else {
console.log('❌ File superadmin.txt tidak ditemukan');
}
} catch (error) {
console.log('❌ Error reading superadmin.txt:', error.message);
}
console.log('');
console.log('✅ Script test admin selesai.');
console.log('💡 Tips: Jalankan aplikasi dengan "node scripts/restart-on-error.js"');
console.log('📱 Test dengan mengirim pesan "menu" atau "admin" ke bot WhatsApp');
+49
View File
@@ -0,0 +1,49 @@
#!/usr/bin/env node
// Script untuk test pengiriman pesan ke admin
const fs = require('fs');
const path = require('path');
// Load settings
const settings = require('../settings.json');
console.log('=== Test Pengiriman Pesan WhatsApp ===\n');
// Cek konfigurasi admin
console.log('📋 Konfigurasi Admin:');
console.log(`Admin numbers: ${JSON.stringify(settings.admins)}`);
console.log(`WhatsApp timeout: ${settings.whatsapp_timeout}ms`);
console.log(`Notification timeout: ${settings.notification_timeout}ms`);
console.log('');
// Cek file superadmin.txt
try {
const superAdminPath = path.join(__dirname, '../config/superadmin.txt');
if (fs.existsSync(superAdminPath)) {
const superAdmin = fs.readFileSync(superAdminPath, 'utf8').trim();
console.log(`Super admin: ${superAdmin}`);
} else {
console.log('❌ File superadmin.txt tidak ditemukan');
}
} catch (error) {
console.log('❌ Error reading superadmin.txt:', error.message);
}
console.log('');
// Test message
const testMessage = `🧪 *TEST PESAN BOT*\n\n` +
`✅ Ini adalah pesan test untuk memverifikasi koneksi WhatsApp\n` +
`📅 Waktu: ${new Date().toLocaleString()}\n\n` +
`🔧 Jika Anda menerima pesan ini, berarti:\n` +
`• Koneksi WhatsApp berfungsi dengan baik\n` +
`• Pengiriman pesan ke admin berhasil\n` +
`• Bot siap digunakan\n\n` +
`🏢 *ALIJAYA DIGITAL NETWORK*`;
console.log('📝 Pesan test yang akan dikirim:');
console.log(testMessage);
console.log('');
console.log('✅ Script test selesai. Jalankan aplikasi utama untuk test pengiriman pesan.');
console.log('💡 Tips: Gunakan "node scripts/restart-on-error.js" untuk menjalankan dengan auto-restart');
+141
View File
@@ -0,0 +1,141 @@
/**
* Test script untuk Mikhmon parser
* Format: :put (",rem,4000,2d,5000,,Disable,");
*/
// Copy parser dari customerPortal.js
function parseMikhmonOnLogin(script) {
if (!script) return null;
const s = String(script).trim();
// Cari pattern :put (",rem, ... , ... , ...
// Updated regex untuk support format: :put (",rem,4000,2d,5000,,Disable,");
const putMatch = s.match(/:\s*put\s*\(\s*[",]rem[",]?\s*,\s*([^,]+)\s*,\s*([^,]+)\s*,\s*([^,]+)/i);
if (putMatch) {
const cost = String(putMatch[1] || '').trim();
const validity = String(putMatch[2] || '').trim();
const priceStr = String(putMatch[3] || '').trim();
const price = Number(priceStr.replace(/[^\d]/g, '')) || 0;
if (validity && price > 0) {
return { validity, price, cost: Number(cost.replace(/[^\d]/g, '')) || 0 };
}
}
// Fallback: split by comma
const parts = s.split(',').map(p => String(p).trim());
let remIdx = -1;
for (let i = 0; i < parts.length; i++) {
if (parts[i].includes('rem')) {
remIdx = i;
break;
}
}
if (remIdx >= 0 && remIdx + 3 < parts.length) {
const cost = String(parts[remIdx + 1] || '').trim();
const validity = String(parts[remIdx + 2] || '').trim();
const priceStr = String(parts[remIdx + 3] || '').trim();
const price = Number(priceStr.replace(/[^\d]/g, '')) || 0;
if (validity && price > 0) {
return { validity, price, cost: Number(cost.replace(/[^\d]/g, '')) || 0 };
}
}
return null;
}
// Test cases
const testCases = [
{
name: 'Format Mikhmon Baru (ROS6/ROS7)',
script: ':put (",rem,4000,2d,5000,,Disable,");',
expected: { validity: '2d', price: 5000, cost: 4000 }
},
{
name: 'Format Mikhmon Baru (tanpa koma di akhir)',
script: ':put (",rem,4000,2d,5000,,Disable,")',
expected: { validity: '2d', price: 5000, cost: 4000 }
},
{
name: 'Format Mikhmon Baru (dengan spasi)',
script: ':put ( "rem" , 4000 , 2d , 5000 , , Disable , ) ;',
expected: { validity: '2d', price: 5000, cost: 4000 }
},
{
name: 'Format Mikhmon Lama (tanpa rem)',
script: ':put (",10000,1d,15000,");',
expected: null
},
{
name: 'Script lengkap dengan banyak baris',
script: `:put (",rem,4000,2d,5000,,Disable,");
:local comment [ /ip hotspot user get [/ip hotspot user find where name="$user"] comment];
:local ucode [:pic $comment 0 2];
:if ($ucode = "vc" or $ucode = "up" or $comment = "") do={
:local date [ /system clock get date ];
:local year [ :pick $date 7 11 ];
:local month [ :pick $date 0 3 ];
/sys sch add name="$user" disable=no start-date=$date interval="2d";
:delay 5s;
:local exp [ /sys sch get [ /sys sch find where name="$user" ] next-run];
:local getxp [len $exp];
:if ($getxp = 15) do={
:local d [:pic $exp 0 6];
:local t [:pic $exp 7 16];
:local s ("/");
:local exp ("$d$s$year $t");
/ip hotspot user set comment="$exp" [find where name="$user"];
};
:if ($getxp = 8) do={
/ip hotspot user set comment="$date $exp" [find where name="$user"];
};
:if ($getxp > 15) do={
/ip hotspot user set comment="$exp" [find where name="$user"];
};
:delay 5s;
/sys sch remove [find where name="$user"]
}`,
expected: { validity: '2d', price: 5000, cost: 4000 }
}
];
console.log('=== TEST MIKHMON PARSER ===\n');
let passed = 0;
let failed = 0;
for (const test of testCases) {
const result = parseMikhmonOnLogin(test.script);
const success = result &&
result.validity === test.expected.validity &&
result.price === test.expected.price &&
result.cost === test.expected.cost;
if (success) {
console.log(`✅ ${test.name}`);
console.log(` Result: validity=${result.validity}, price=${result.price}, cost=${result.cost}\n`);
passed++;
} else if (!result && !test.expected) {
console.log(`✅ ${test.name} (expected null)`);
console.log(` Result: null (correct)\n`);
passed++;
} else {
console.log(`❌ ${test.name}`);
console.log(` Expected: validity=${test.expected.validity}, price=${test.expected.price}, cost=${test.expected.cost}`);
console.log(` Got: ${JSON.stringify(result)}\n`);
failed++;
}
}
console.log('=== RINGKASAN ===');
console.log(`Total: ${testCases.length}`);
console.log(`✅ Passed: ${passed}`);
console.log(`❌ Failed: ${failed}`);
if (failed === 0) {
console.log('\n🎉 Semua test passed!');
} else {
console.log('\n⚠️ Ada test yang gagal!');
}
+63
View File
@@ -0,0 +1,63 @@
// Test regex dari referensi billing-rtrw-main
const tests = [
':put (",rem,4000,2d,5000,,Disable,");',
':put (",rem,4000,2d,5000,,Disable,"); {:local comment [ /ip hotspot user get [/ip hotspot user find where name="$user"] comment];}',
':put (",rem,10000,1d,15000,,Disable,");',
':put (",rem,0,30m,3000,,Disable,");',
// Format tanpa harga beli (COST=0)
':put (",rem,,1h,5000,,Disable,");',
];
// Regex dari referensi
function parseMikhmonRef(script) {
if (!script) return null;
const m = String(script).match(/",rem,.*?,(.*?),(.*?),.*?"/);
if (!m) return null;
const validity = String(m[1] || '').trim();
const price = Number(String(m[2] || '').replace(/[^\d]/g, '')) || 0;
return { validity, price };
}
// Regex yang sedang dipakai di project
function parseMikhmonCurrent(script) {
if (!script) return null;
const s = String(script).trim();
const putMatch = s.match(/:\s*put\s*\(\s*[",]rem[",]?\s*,\s*([^,]+)\s*,\s*([^,]+)\s*,\s*([^,]+)/i);
if (putMatch) {
const cost = String(putMatch[1] || '').trim();
const validity = String(putMatch[2] || '').trim();
const priceStr = String(putMatch[3] || '').trim();
const price = Number(priceStr.replace(/[^\d]/g, '')) || 0;
if (validity && price > 0) {
return { validity, price, cost: Number(cost.replace(/[^\d]/g, '')) || 0 };
}
}
// Fallback split
const parts = s.split(',').map(p => String(p).trim());
let remIdx = -1;
for (let i = 0; i < parts.length; i++) {
if (parts[i].includes('rem')) { remIdx = i; break; }
}
if (remIdx >= 0 && remIdx + 3 < parts.length) {
const cost = String(parts[remIdx + 1] || '').trim();
const validity = String(parts[remIdx + 2] || '').trim();
const priceStr = String(parts[remIdx + 3] || '').trim();
const price = Number(priceStr.replace(/[^\d]/g, '')) || 0;
if (validity && price > 0) {
return { validity, price, cost: Number(cost.replace(/[^\d]/g, '')) || 0 };
}
}
return null;
}
console.log('=== PERBANDINGAN REGEX ===\n');
for (const t of tests) {
const ref = parseMikhmonRef(t);
const cur = parseMikhmonCurrent(t);
const match = JSON.stringify(ref) === JSON.stringify({validity: cur?.validity, price: cur?.price});
console.log('Script:', t.substring(0, 60));
console.log(' Referensi :', ref);
console.log(' Sekarang :', cur);
console.log(' Sama? :', match ? '✅' : '❌');
console.log();
}
+82
View File
@@ -0,0 +1,82 @@
/**
* Script untuk memverifikasi dan memperbaiki struktur database
* Menambahkan kolom yang hilang jika diperlukan
*/
const Database = require('better-sqlite3');
const path = require('path');
const dbPath = path.join(__dirname, '../database/billing.db');
console.log('Memeriksa database:', dbPath);
const db = new Database(dbPath);
db.pragma('journal_mode = WAL');
// Fungsi untuk cek apakah kolom ada
function columnExists(tableName, columnName) {
const columns = db.prepare(`PRAGMA table_info(${tableName})`).all();
return columns.some(col => col.name === columnName);
}
// Daftar kolom yang harus ada di tabel customers
const requiredColumns = [
{ name: 'hotspot_username', type: 'TEXT DEFAULT \'\'', description: 'Username Hotspot' },
{ name: 'hotspot_password', type: 'TEXT DEFAULT \'\'', description: 'Password Hotspot' },
{ name: 'hotspot_profile', type: 'TEXT DEFAULT \'\'', description: 'Profile Hotspot' },
{ name: 'mac_address', type: 'TEXT', description: 'MAC Address' },
{ name: 'connection_type', type: 'TEXT DEFAULT \'pppoe\'', description: 'Tipe Koneksi' },
{ name: 'static_ip', type: 'TEXT', description: 'Static IP' },
{ name: 'auto_isolate', type: 'INTEGER DEFAULT 1', description: 'Auto Isolir' },
{ name: 'isolate_day', type: 'INTEGER DEFAULT 10', description: 'Hari Isolir' },
{ name: 'email', type: 'TEXT DEFAULT \'\'', description: 'Email' },
{ name: 'router_id', type: 'INTEGER REFERENCES routers(id) ON DELETE SET NULL', description: 'Router ID' },
{ name: 'olt_id', type: 'INTEGER REFERENCES olts(id) ON DELETE SET NULL', description: 'OLT ID' },
{ name: 'pon_port', type: 'TEXT DEFAULT \'\'', description: 'PON Port' },
{ name: 'odp_id', type: 'INTEGER REFERENCES odps(id) ON DELETE SET NULL', description: 'ODP ID' },
{ name: 'lat', type: 'TEXT', description: 'Latitude' },
{ name: 'lng', type: 'TEXT', description: 'Longitude' },
{ name: 'cable_path', type: 'TEXT', description: 'Cable Path' },
{ name: 'promo_cycles_used', type: 'INTEGER DEFAULT 0', description: 'Promo Cycles Used' }
];
console.log('\n=== VERIFIKASI TABEL CUSTOMERS ===\n');
let missingColumns = [];
let existingColumns = [];
for (const col of requiredColumns) {
const exists = columnExists('customers', col.name);
if (exists) {
existingColumns.push(col.name);
console.log(`✓ Kolom '${col.name}' sudah ada`);
} else {
missingColumns.push(col);
console.log(`✗ Kolom '${col.name}' TIDAK ADA`);
}
}
console.log(`\n=== RINGKASAN ===`);
console.log(`Total kolom yang diperiksa: ${requiredColumns.length}`);
console.log(`Kolom yang sudah ada: ${existingColumns.length}`);
console.log(`Kolom yang hilang: ${missingColumns.length}`);
if (missingColumns.length > 0) {
console.log(`\n=== MENAMBAHKAN KOLOM YANG HILANG ===\n`);
for (const col of missingColumns) {
try {
const sql = `ALTER TABLE customers ADD COLUMN ${col.name} ${col.type}`;
console.log(`Menambahkan: ${col.name} (${col.description})`);
db.exec(sql);
console.log(`✓ Berhasil menambahkan kolom '${col.name}'`);
} catch (e) {
console.error(`✗ Gagal menambahkan kolom '${col.name}':`, e.message);
}
}
console.log(`\n=== SELESAI ===`);
console.log(`Silakan restart aplikasi untuk menerapkan perubahan.`);
} else {
console.log(`\n✓ Semua kolom sudah lengkap!`);
}
db.close();