feat: DH-HIS Web Giao Ban Go v1.0.1 (Favicon, Exe icon, System Tray, Gitea auto-update)
This commit is contained in:
+446
@@ -0,0 +1,446 @@
|
||||
// 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 & 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 & Cập Nhật Tự Động</span>';
|
||||
}
|
||||
}
|
||||
|
||||
function escapeHtml(str) {
|
||||
if (!str) return '';
|
||||
return String(str)
|
||||
.replace(/&/g, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/"/g, '"')
|
||||
.replace(/'/g, ''');
|
||||
}
|
||||
Reference in New Issue
Block a user