// Admin & Configuration JavaScript const API_BASE = window.location.pathname.startsWith('/web-giao-ban-go') ? '/web-giao-ban-go' : (window.location.pathname.startsWith('/web-giao-ban') ? '/web-giao-ban' : ''); let autoRefreshTimer = null; let currentUpdateAsset = null; document.addEventListener('DOMContentLoaded', () => { initTabs(); loadConfig(); initLogViewer(); initUpdateChecker(); // Handle URL hash tab const hash = window.location.hash; if (hash === '#tab-update') { switchTab('tab-update'); checkUpdate(); } else if (hash === '#tab-logs') { switchTab('tab-logs'); } }); function showToast(msg) { const toast = document.getElementById('toast'); const toastMsg = document.getElementById('toastMsg'); if (!toast || !toastMsg) return; toastMsg.innerText = msg; toast.classList.remove('translate-y-20', 'opacity-0'); toast.classList.add('translate-y-0', 'opacity-100'); setTimeout(() => { toast.classList.remove('translate-y-0', 'opacity-100'); toast.classList.add('translate-y-20', 'opacity-0'); }, 2500); } function initTabs() { document.querySelectorAll('.admin-tab').forEach(btn => { btn.addEventListener('click', () => { const target = btn.getAttribute('data-tab'); switchTab(target); }); }); } function switchTab(targetId) { document.querySelectorAll('.admin-tab').forEach(b => { const isTarget = b.getAttribute('data-tab') === targetId; b.classList.toggle('active', isTarget); b.classList.toggle('border-indigo-600', isTarget); b.classList.toggle('text-indigo-600', isTarget); b.classList.toggle('border-transparent', !isTarget); b.classList.toggle('text-slate-500', !isTarget); }); document.querySelectorAll('.admin-content').forEach(c => { c.classList.toggle('hidden', c.id !== targetId); }); if (targetId === 'tab-logs') { fetchLogs(); } } // ------------------------------------------------------------------- // CONFIGURATION // ------------------------------------------------------------------- async function loadConfig() { try { const res = await fetch(`${API_BASE}/api/admin/config`); const json = await res.json(); if (json.success && json.data) { const cfg = json.data; // Server document.getElementById('cfgPort').value = cfg.server.port || 3000; document.getElementById('cfgAdminPath').value = cfg.server.admin_path || '/admin'; // Database document.getElementById('cfgDbHost').value = cfg.database.host || '192.168.50.79'; document.getElementById('cfgDbPort').value = cfg.database.port || 5437; document.getElementById('cfgDbUser').value = cfg.database.user || 'postgres'; document.getElementById('cfgDbPassword').value = ''; document.getElementById('cfgDbDefault').value = cfg.database.default_db || ''; // Databases Table renderDbList(cfg.database.databases || []); // Gitea Updater document.getElementById('cfgGiteaUrl').value = cfg.updater.gitea_url || 'https://i.dh-his.com'; document.getElementById('cfgGiteaOwner').value = cfg.updater.owner || 'hdhiswork'; document.getElementById('cfgGiteaRepo').value = cfg.updater.repo || 'DUAN'; document.getElementById('cfgGiteaToken').value = ''; const ver = cfg.updater.current_version || 'v1.0.0'; document.getElementById('appVersionBadge').innerText = ver; document.getElementById('currentVerDisplay').innerText = ver; } } catch (err) { console.error('Lỗi nạp cấu hình:', err); } } function renderDbList(list) { const tbody = document.getElementById('bodyDbList'); tbody.innerHTML = ''; list.forEach(db => { addDbRow(db.id, db.name); }); } function addDbRow(id = '', name = '') { const tbody = document.getElementById('bodyDbList'); const tr = document.createElement('tr'); tr.className = 'hover:bg-slate-50 transition-colors'; tr.innerHTML = ` `; tr.querySelector('.btn-del-row').addEventListener('click', () => tr.remove()); tbody.appendChild(tr); } document.getElementById('btnAddDbRow')?.addEventListener('click', () => addDbRow()); // Password Toggle document.getElementById('btnToggleDbPass')?.addEventListener('click', () => { const inp = document.getElementById('cfgDbPassword'); const btn = document.getElementById('btnToggleDbPass'); if (inp.type === 'password') { inp.type = 'text'; btn.innerText = 'Ẩn'; } else { inp.type = 'password'; btn.innerText = 'Hiện'; } }); document.getElementById('btnToggleToken')?.addEventListener('click', () => { const inp = document.getElementById('cfgGiteaToken'); const btn = document.getElementById('btnToggleToken'); if (inp.type === 'password') { inp.type = 'text'; btn.innerText = 'Ẩn'; } else { inp.type = 'password'; btn.innerText = 'Hiện'; } }); // Test Connection document.getElementById('btnTestConnection')?.addEventListener('click', async () => { const resEl = document.getElementById('testConnResult'); resEl.innerHTML = ' Đang thử kết nối...'; const body = { host: document.getElementById('cfgDbHost').value, port: parseInt(document.getElementById('cfgDbPort').value, 10), user: document.getElementById('cfgDbUser').value, password: document.getElementById('cfgDbPassword').value, database: document.getElementById('cfgDbDefault').value, }; try { const res = await fetch(`${API_BASE}/api/admin/test-db`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body), }); const json = await res.json(); if (json.success) { resEl.innerHTML = ' Kết nối thành công!'; } else { resEl.innerHTML = ` ${json.message}`; } } catch (err) { resEl.innerHTML = ` ${err.message}`; } }); // Save Config Form document.getElementById('formConfig')?.addEventListener('submit', async (e) => { e.preventDefault(); const btn = document.getElementById('btnSaveConfig'); btn.disabled = true; btn.innerHTML = ' Đang lưu...'; const dbs = []; document.querySelectorAll('#bodyDbList tr').forEach(tr => { const id = tr.querySelector('.db-row-id').value.trim(); const name = tr.querySelector('.db-row-name').value.trim(); if (id) { dbs.push({ id, name: name || id }); } }); const body = { server: { port: parseInt(document.getElementById('cfgPort').value, 10), admin_path: document.getElementById('cfgAdminPath').value.trim(), }, database: { host: document.getElementById('cfgDbHost').value.trim(), port: parseInt(document.getElementById('cfgDbPort').value, 10), user: document.getElementById('cfgDbUser').value.trim(), password: document.getElementById('cfgDbPassword').value, default_db: document.getElementById('cfgDbDefault').value.trim(), databases: dbs, }, updater: { enabled: true, gitea_url: document.getElementById('cfgGiteaUrl').value.trim(), owner: document.getElementById('cfgGiteaOwner').value.trim(), repo: document.getElementById('cfgGiteaRepo').value.trim(), token: document.getElementById('cfgGiteaToken').value, current_version: document.getElementById('currentVerDisplay').innerText, } }; try { const res = await fetch(`${API_BASE}/api/admin/config`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body), }); const json = await res.json(); if (json.success) { showToast('Đã lưu cấu hình & mã hóa thành công!'); loadConfig(); } else { alert('Lỗi lưu cấu hình: ' + json.message); } } catch (err) { alert('Lỗi kết nối server: ' + err.message); } finally { btn.disabled = false; btn.innerHTML = ' Lưu Cấu Hình & Mã Hóa'; } }); // Save Gitea Config Form document.getElementById('formGiteaConfig')?.addEventListener('submit', (e) => { e.preventDefault(); document.getElementById('formConfig').dispatchEvent(new Event('submit')); }); // ------------------------------------------------------------------- // LOG VIEWER // ------------------------------------------------------------------- function initLogViewer() { document.getElementById('btnRefreshLogs')?.addEventListener('click', fetchLogs); document.getElementById('selectLogLevel')?.addEventListener('change', fetchLogs); document.getElementById('btnClearLogs')?.addEventListener('click', clearLogs); const chk = document.getElementById('chkAutoRefresh'); chk?.addEventListener('change', () => { if (chk.checked) { startAutoRefresh(); } else { clearInterval(autoRefreshTimer); } }); startAutoRefresh(); } function startAutoRefresh() { clearInterval(autoRefreshTimer); autoRefreshTimer = setInterval(() => { const logsTab = document.getElementById('tab-logs'); if (logsTab && !logsTab.classList.contains('hidden')) { fetchLogs(); } }, 3000); } async function fetchLogs() { const level = document.getElementById('selectLogLevel')?.value || 'ALL'; const box = document.getElementById('logTerminal'); const countBadge = document.getElementById('logCountBadge'); try { const res = await fetch(`${API_BASE}/api/admin/logs?limit=300&level=${level}`); const json = await res.json(); if (json.success && Array.isArray(json.data)) { countBadge.innerText = `${json.data.length} dòng`; if (json.data.length === 0) { box.innerHTML = '
Không có dòng log nào.
'; return; } box.innerHTML = ''; json.data.forEach(entry => { let colorClass = 'text-slate-300'; let badgeColor = 'bg-slate-800 text-slate-300'; switch (entry.level) { case 'INFO': colorClass = 'text-emerald-400'; badgeColor = 'bg-emerald-950 text-emerald-300 border border-emerald-800'; break; case 'WARN': colorClass = 'text-amber-400'; badgeColor = 'bg-amber-950 text-amber-300 border border-amber-800'; break; case 'ERROR': colorClass = 'text-rose-400 font-bold'; badgeColor = 'bg-rose-950 text-rose-300 border border-rose-800'; break; case 'SQL': colorClass = 'text-cyan-400'; badgeColor = 'bg-cyan-950 text-cyan-300 border border-cyan-800'; break; } const div = document.createElement('div'); div.className = 'py-0.5 border-b border-slate-900/60 flex items-start gap-2'; div.innerHTML = ` ${entry.timestamp} ${entry.level} ${escapeHtml(entry.message)} `; box.appendChild(div); }); } } catch (err) { console.error('Lỗi tải log:', err); } } async function clearLogs() { if (!confirm('Bạn có chắc muốn làm sạch toàn bộ bộ nhớ log không?')) return; try { await fetch(`${API_BASE}/api/admin/logs/clear`, { method: 'POST' }); fetchLogs(); showToast('Đã xóa bộ nhớ log!'); } catch (err) { alert('Lỗi xóa log: ' + err.message); } } // ------------------------------------------------------------------- // UPDATE CHECKER (GITEA) // ------------------------------------------------------------------- function initUpdateChecker() { document.getElementById('btnCheckUpdate')?.addEventListener('click', checkUpdate); document.getElementById('btnApplyUpdate')?.addEventListener('click', applyUpdate); } async function checkUpdate() { const btn = document.getElementById('btnCheckUpdate'); const latestDisplay = document.getElementById('latestVerDisplay'); const notesEl = document.getElementById('updateReleaseNotes'); const statusTitle = document.getElementById('updateStatusTitle'); const applyArea = document.getElementById('applyUpdateArea'); btn.disabled = true; btn.innerHTML = ' Đang kiểm tra...'; try { const res = await fetch(`${API_BASE}/api/update/check`); const json = await res.json(); if (json.success && json.data) { const u = json.data; latestDisplay.innerText = u.latest_version || u.current_version; if (u.has_update) { statusTitle.innerHTML = ` Có bản cập nhật mới: ${u.release_title || u.latest_version}`; notesEl.innerText = u.release_notes || 'Không có mô tả chi tiết.'; if (u.asset_url) { currentUpdateAsset = u; applyArea.classList.remove('hidden'); document.getElementById('updateAssetInfo').innerText = `File: ${u.asset_name} (${(u.asset_size / (1024 * 1024)).toFixed(2)} MB)`; } else { applyArea.classList.add('hidden'); notesEl.innerText += '\n\n(Lưu ý: Chưa tìm thấy file nhị phân tương thích trong bản release).'; } } else { statusTitle.innerHTML = ` Bạn đang sử dụng phiên bản mới nhất (${u.current_version})`; notesEl.innerText = u.release_notes || 'Hệ thống đã cập nhật đầy đủ các tính năng mới nhất.'; applyArea.classList.add('hidden'); } } else { latestDisplay.innerText = 'Lỗi kết nối'; notesEl.innerText = json.message || 'Không thể kết nối đến máy chủ Gitea.'; } } catch (err) { latestDisplay.innerText = 'Lỗi'; notesEl.innerText = err.message; } finally { btn.disabled = false; btn.innerHTML = ' Kiểm Tra Cập Nhật'; } } async function applyUpdate() { if (!currentUpdateAsset || !currentUpdateAsset.asset_url) return; if (!confirm(`Bạn có chắc muốn tự động cập nhật lên phiên bản ${currentUpdateAsset.latest_version}?`)) return; const btn = document.getElementById('btnApplyUpdate'); btn.disabled = true; btn.innerHTML = ' Đang tải và cài đặt...'; try { const res = await fetch(`${API_BASE}/api/update/apply`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ asset_url: currentUpdateAsset.asset_url, version: currentUpdateAsset.latest_version, }), }); const json = await res.json(); if (json.success) { alert(json.message); location.reload(); } else { alert('Lỗi cập nhật: ' + json.message); } } catch (err) { alert('Lỗi kết nối cập nhật: ' + err.message); } finally { btn.disabled = false; btn.innerHTML = ' Tải & Cập Nhật Tự Động'; } } function escapeHtml(str) { if (!str) return ''; return String(str) .replace(/&/g, '&') .replace(//g, '>') .replace(/"/g, '"') .replace(/'/g, '''); }