// 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');
} else if (hash === '#tab-license') {
switchTab('tab-license');
}
initLicenseManager();
});
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';
const owner = (cfg.updater.owner && cfg.updater.owner !== 'hdhiswork') ? cfg.updater.owner : 'ongtrieuhau';
const repo = (cfg.updater.repo && cfg.updater.repo !== 'DUAN') ? cfg.updater.repo : 'web-giao-ban';
document.getElementById('cfgGiteaOwner').value = owner;
document.getElementById('cfgGiteaRepo').value = repo;
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, ''');
}
// -------------------------------------------------------------------
// LICENSE MANAGEMENT
// -------------------------------------------------------------------
const SAMPLE_LICENSE_KEY = "U2FsdGVkX18Zsq52FPbB7FS1QM7CwWh+t1Esi96gUMFILJD0vZXM69uQBC4sP7TjLVOiJPZt0KIq17iql+hXehLCVSbCEhwKKm+zNab+u2Swah63A52hz0KMJXddLRZ6vanmP2goLTzY7749HsFVRDq+3bdbi8fdUZCpAsSTEiqC2cRTpF5WVh0SHt1wX/i42YSQgOYPeXQtg+lRx/URk4p7lYSVQOYWXglp99kO+Va1ADoNcTgrfz3aaAxAkj0S";
function initLicenseManager() {
loadLicenseStatus();
const btnPaste = document.getElementById('btnPasteSampleKey');
if (btnPaste) {
btnPaste.addEventListener('click', () => {
const txt = document.getElementById('txtLicenseKey');
if (txt) {
txt.value = SAMPLE_LICENSE_KEY;
showToast('Đã dán mã bản quyền mẫu');
}
});
}
const btnActivate = document.getElementById('btnActivateLicense');
if (btnActivate) {
btnActivate.addEventListener('click', activateLicense);
}
}
async function loadLicenseStatus() {
try {
const res = await fetch(`${API_BASE}/api/license/status`);
const json = await res.json();
if (json.success && json.data) {
renderLicenseInfo(json.data);
}
} catch (err) {
console.error('Lỗi lấy thông tin bản quyền:', err);
}
}
function renderLicenseInfo(lic) {
const badge = document.getElementById('licenseStatusBadge');
const alertBox = document.getElementById('licenseAlertBox');
const alertMsg = document.getElementById('licenseAlertMsg');
document.getElementById('licTenBV').innerText = lic.TenBV || 'Chưa xác định';
document.getElementById('licMabvbh').innerText = lic.mabvbh || '--';
document.getElementById('licDateExpires').innerText = lic.DateExpires || '--';
document.getElementById('licTitle').innerText = lic.title || 'Sử dụng Web giao ban';
document.getElementById('licKey').innerText = lic.key || 'web_giao_ban';
document.getElementById('licDoiTac').innerText = lic['Đối tác'] || '--';
document.getElementById('licGhiChu').innerText = lic['Ghi chú'] || 'Không có';
if (lic.is_valid) {
badge.className = 'text-xs px-2.5 py-1 rounded-full font-bold bg-emerald-100 text-emerald-800 border border-emerald-300 flex items-center gap-1.5';
badge.innerHTML = ' Hợp Lệ';
document.getElementById('licRemainingDays').innerHTML = ` Còn ${lic.remaining_days} ngày`;
alertBox.className = 'mt-4 p-3.5 rounded-xl border text-xs flex items-center gap-2.5 bg-emerald-50 border-emerald-200 text-emerald-800';
alertBox.innerHTML = `${escapeHtml(lic.status_message || 'Bản quyền đang hoạt động bình thường.')}`;
} else {
badge.className = 'text-xs px-2.5 py-1 rounded-full font-bold bg-rose-100 text-rose-800 border border-rose-300 flex items-center gap-1.5';
badge.innerHTML = ' Chưa Hợp Lệ';
document.getElementById('licRemainingDays').innerHTML = ` Không khả dụng`;
alertBox.className = 'mt-4 p-3.5 rounded-xl border text-xs flex items-center gap-2.5 bg-rose-50 border-rose-200 text-rose-800';
alertBox.innerHTML = `${escapeHtml(lic.status_message || 'Chưa cập nhật key bản quyền hợp lệ.')}`;
}
// Pre-fill textarea if key exists and textarea is empty
const txt = document.getElementById('txtLicenseKey');
if (txt && !txt.value && lic.raw_encrypted) {
txt.value = lic.raw_encrypted;
}
}
async function activateLicense() {
const txt = document.getElementById('txtLicenseKey');
if (!txt) return;
const keyVal = txt.value.trim();
if (!keyVal) {
alert('Vui lòng nhập chuỗi key bản quyền (bắt đầu bằng U2FsdGVkX1...)');
txt.focus();
return;
}
const btn = document.getElementById('btnActivateLicense');
btn.disabled = true;
btn.innerHTML = ' Đang xác thực...';
try {
const res = await fetch(`${API_BASE}/api/license/activate`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ key: keyVal })
});
const json = await res.json();
if (json.success && json.data) {
showToast(json.message);
renderLicenseInfo(json.data);
alert('Kích hoạt thành công!\n\n' + json.message + '\nĐơn vị: ' + json.data.TenBV + '\nHạn sử dụng: ' + json.data.DateExpires);
} else {
alert('Kích hoạt không thành công: ' + (json.message || 'Lỗi không xác định'));
loadLicenseStatus();
}
} catch (err) {
alert('Lỗi kết nối máy chủ: ' + err.message);
} finally {
btn.disabled = false;
btn.innerHTML = ' Kích Hoạt Key Bản Quyền';
}
}