Files
web-giao-ban-go/public/admin.js
T

447 lines
16 KiB
JavaScript

// 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 = `
<td class="py-2 px-2.5">
<input type="text" class="w-full rounded border border-slate-300 px-2 py-1 font-mono text-xs db-row-id" value="${id}" placeholder="Tên CSDL trong Postgres">
</td>
<td class="py-2 px-2.5">
<input type="text" class="w-full rounded border border-slate-300 px-2 py-1 text-xs db-row-name" value="${name}" placeholder="Tên hiển thị dropdown">
</td>
<td class="py-2 px-2 text-center">
<button type="button" class="text-rose-500 hover:text-rose-700 p-1 text-xs btn-del-row" title="Xóa dòng">
<i class="fa-solid fa-trash-can"></i>
</button>
</td>
`;
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 = '<span class="text-blue-600"><i class="fa-solid fa-spinner fa-spin mr-1"></i> Đang thử kết nối...</span>';
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 = '<span class="text-emerald-600"><i class="fa-solid fa-circle-check mr-1"></i> Kết nối thành công!</span>';
} else {
resEl.innerHTML = `<span class="text-rose-600"><i class="fa-solid fa-circle-xmark mr-1"></i> ${json.message}</span>`;
}
} catch (err) {
resEl.innerHTML = `<span class="text-rose-600"><i class="fa-solid fa-circle-xmark mr-1"></i> ${err.message}</span>`;
}
});
// Save Config Form
document.getElementById('formConfig')?.addEventListener('submit', async (e) => {
e.preventDefault();
const btn = document.getElementById('btnSaveConfig');
btn.disabled = true;
btn.innerHTML = '<i class="fa-solid fa-spinner fa-spin"></i> <span>Đang lưu...</span>';
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 = '<i class="fa-solid fa-floppy-disk"></i> <span>Lưu Cấu Hình &amp; Mã Hóa</span>';
}
});
// 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 = '<div class="text-slate-500 italic">Không có dòng log nào.</div>';
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 = `
<span class="text-slate-500 text-[10px] whitespace-nowrap">${entry.timestamp}</span>
<span class="text-[10px] font-bold px-1.5 rounded ${badgeColor}">${entry.level}</span>
<span class="${colorClass} break-all flex-1">${escapeHtml(entry.message)}</span>
`;
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 = '<i class="fa-solid fa-spinner fa-spin"></i> <span>Đang kiểm tra...</span>';
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 = `<span class="text-emerald-600 font-bold"><i class="fa-solid fa-bell"></i> Có bản cập nhật mới: ${u.release_title || u.latest_version}</span>`;
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 = `<span class="text-slate-600 font-bold"><i class="fa-solid fa-circle-check text-emerald-500"></i> Bạn đang sử dụng phiên bản mới nhất (${u.current_version})</span>`;
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 = '<i class="fa-solid fa-arrows-rotate"></i> <span>Kiểm Tra Cập Nhật</span>';
}
}
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 = '<i class="fa-solid fa-spinner fa-spin mr-1"></i> Đ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 = '<i class="fa-solid fa-download"></i> <span>Tải &amp; Cập Nhật Tự Động</span>';
}
}
function escapeHtml(str) {
if (!str) return '';
return String(str)
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#039;');
}