diff --git a/CHANGELOG.md b/CHANGELOG.md index c307e3b..7a3b9b8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,11 @@ Format mengikuti [Keep a Changelog](https://keepachangelog.com/) dan [Semantic Versioning](https://semver.org/). +## [0.5.14] — 2026-11-05 — "ai-test" + +### Ditambahkan +- Tombol **Test API** di Pengaturan → AI: memvalidasi provider, API key, base URL, dan model yang sedang diisi **tanpa harus menyimpan**. Menampilkan status valid/tidak, HTTP status, dan latensi. + ## [0.5.13] — 2026-11-05 — "lunas-notify" ### Diperbaiki diff --git a/README.md b/README.md index 7c82c86..dc71844 100644 --- a/README.md +++ b/README.md @@ -11,7 +11,7 @@
-
+
diff --git a/package.json b/package.json
index aed4612..9017fb5 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "open-isp",
- "version": "0.5.13",
+ "version": "0.5.14",
"description": "OPEN-ISP \u2014 sistem billing & manajemen ISP open-source (PWA pelanggan, multi-OLT, RADIUS, WhatsApp, QRIS)",
"main": "app-customer.js",
"scripts": {
diff --git a/routes/adminPortal.js b/routes/adminPortal.js
index 6750f19..a44a9d5 100644
--- a/routes/adminPortal.js
+++ b/routes/adminPortal.js
@@ -4788,6 +4788,76 @@ router.post('/api/telegram/sync', requireAdminSession, async (req, res) => {
}
});
+router.post('/settings/test-ai', requireAdminSession, express.json(), async (req, res) => {
+ const presets = {
+ deepseek: { baseUrl: 'https://api.deepseek.com', model: 'deepseek-chat' },
+ openai: { baseUrl: 'https://api.openai.com/v1', model: 'gpt-4o-mini' },
+ groq: { baseUrl: 'https://api.groq.com/openai/v1', model: 'llama-3.3-70b-versatile' },
+ openrouter: { baseUrl: 'https://openrouter.ai/api/v1', model: 'meta-llama/llama-3.3-70b-instruct' },
+ custom: { baseUrl: '', model: '' }
+ };
+ try {
+ const b = req.body || {};
+ const provider = String(b.provider || 'deepseek').trim().toLowerCase();
+ const p = presets[provider] || presets.deepseek;
+ const apiKey = String(b.apiKey || '').trim();
+ const baseUrl = String(b.baseUrl || p.baseUrl || '').trim().replace(/\/+$/, '');
+ const model = String(b.model || p.model || '').trim();
+
+ if (!baseUrl) return res.status(400).json({ ok: false, message: 'Base URL AI kosong.' });
+ if (!model) return res.status(400).json({ ok: false, message: 'Model AI kosong.' });
+ if (!apiKey && provider !== 'custom') return res.status(400).json({ ok: false, message: 'API key kosong.' });
+
+ const started = Date.now();
+ const controller = new AbortController();
+ const timer = setTimeout(function () { controller.abort(); }, 15000);
+ let r;
+ let text = '';
+ try {
+ r = await fetch(baseUrl + '/chat/completions', {
+ method: 'POST',
+ headers: Object.assign(
+ { 'Content-Type': 'application/json' },
+ apiKey ? { Authorization: 'Bearer ' + apiKey } : {}
+ ),
+ body: JSON.stringify({
+ model: model,
+ messages: [{ role: 'user', content: 'ping' }],
+ max_tokens: 5,
+ stream: false
+ }),
+ signal: controller.signal
+ });
+ text = await r.text();
+ } finally {
+ clearTimeout(timer);
+ }
+ const latencyMs = Date.now() - started;
+ let json = null;
+ try { json = JSON.parse(text); } catch (e) {}
+
+ if (!r.ok) {
+ const detail = (json && json.error && (json.error.message || json.error.code)) || text || ('HTTP ' + r.status);
+ const message = (r.status === 401 || r.status === 403)
+ ? 'API key tidak valid / tidak punya akses (HTTP ' + r.status + ').'
+ : 'Gagal (HTTP ' + r.status + '): ' + String(detail).replace(/\s+/g, ' ').slice(0, 200);
+ return res.json({ ok: false, provider, model, baseUrl, status: r.status, latencyMs, message });
+ }
+
+ const reply = (json && json.choices && json.choices[0] &&
+ ((json.choices[0].message && json.choices[0].message.content) || json.choices[0].text)) || '';
+ return res.json({
+ ok: true, provider, model, baseUrl, status: r.status, latencyMs,
+ message: 'API valid — ' + (reply ? ('balasan: ' + String(reply).trim().slice(0, 80)) : 'respon OK') + ' (' + latencyMs + ' ms)'
+ });
+ } catch (e) {
+ const message = e && e.name === 'AbortError'
+ ? 'Timeout 15 detik — server AI tidak merespons.'
+ : ('Tidak bisa menghubungi API: ' + (e && e.message ? e.message : String(e)));
+ return res.status(500).json({ ok: false, message });
+ }
+});
+
router.post('/settings', requireAdminSession, express.urlencoded({ extended: true }), (req, res) => {
try {
const newSettings = { ...req.body };
diff --git a/version.txt b/version.txt
index 7988f13..63df075 100644
--- a/version.txt
+++ b/version.txt
@@ -1 +1 @@
-0.5.13
\ No newline at end of file
+0.5.14
\ No newline at end of file
diff --git a/views/admin/settings.ejs b/views/admin/settings.ejs
index b8eecf8..ab563e5 100644
--- a/views/admin/settings.ejs
+++ b/views/admin/settings.ejs
@@ -238,6 +238,10 @@