Files
DashBoard/frontend/src/portal/main.js
2026-02-08 08:30:48 +08:00

194 lines
7.2 KiB
JavaScript

import './portal.css';
(function initPortal() {
const tabs = document.querySelectorAll('.tab');
const frames = document.querySelectorAll('iframe');
const toolFrame = document.getElementById('toolFrame');
const healthDot = document.getElementById('healthDot');
const healthLabel = document.getElementById('healthLabel');
const healthPopup = document.getElementById('healthPopup');
const healthStatus = document.getElementById('healthStatus');
const dbStatus = document.getElementById('dbStatus');
const redisStatus = document.getElementById('redisStatus');
const cacheEnabled = document.getElementById('cacheEnabled');
const cacheSysDate = document.getElementById('cacheSysDate');
const cacheUpdatedAt = document.getElementById('cacheUpdatedAt');
const resourceCacheEnabled = document.getElementById('resourceCacheEnabled');
const resourceCacheCount = document.getElementById('resourceCacheCount');
const resourceCacheUpdatedAt = document.getElementById('resourceCacheUpdatedAt');
const routeCacheMode = document.getElementById('routeCacheMode');
const routeCacheHitRate = document.getElementById('routeCacheHitRate');
const routeCacheDegraded = document.getElementById('routeCacheDegraded');
function setFrameHeight() {
const header = document.querySelector('.header');
const tabArea = document.querySelector('.tabs');
if (!header || !tabArea) return;
const height = Math.max(600, window.innerHeight - header.offsetHeight - tabArea.offsetHeight - 60);
frames.forEach((frame) => {
frame.style.height = `${height}px`;
});
}
function activateTab(targetId) {
tabs.forEach((tab) => tab.classList.remove('active'));
frames.forEach((frame) => frame.classList.remove('active'));
const tabBtn = document.querySelector(`[data-target="${targetId}"]`);
const targetFrame = document.getElementById(targetId);
if (tabBtn) tabBtn.classList.add('active');
if (targetFrame) {
targetFrame.classList.add('active');
if (targetFrame.dataset.src && !targetFrame.src) {
targetFrame.src = targetFrame.dataset.src;
}
}
}
function openTool(path) {
if (!toolFrame) return false;
tabs.forEach((tab) => tab.classList.remove('active'));
frames.forEach((frame) => frame.classList.remove('active'));
toolFrame.classList.add('active');
if (toolFrame.src !== path) {
toolFrame.src = path;
}
return false;
}
function toggleHealthPopup() {
if (!healthPopup) return;
healthPopup.classList.toggle('show');
}
function formatStatus(status) {
const icons = { ok: '✓', error: '✗', disabled: '○' };
return icons[status] || status;
}
function setStatusClass(element, status) {
if (!element) return;
element.classList.remove('ok', 'error', 'disabled');
element.classList.add(status === 'ok' ? 'ok' : status === 'error' ? 'error' : 'disabled');
}
function formatDateTime(dateStr) {
if (!dateStr) return '--';
try {
const date = new Date(dateStr);
if (Number.isNaN(date.getTime())) return dateStr;
return date.toLocaleString('zh-TW', {
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit'
});
} catch {
return dateStr;
}
}
async function checkHealth() {
if (!healthDot || !healthLabel) return;
try {
const response = await fetch('/health', { cache: 'no-store' });
const data = await response.json();
healthDot.classList.remove('loading', 'healthy', 'degraded', 'unhealthy');
if (data.status === 'healthy') {
healthDot.classList.add('healthy');
healthLabel.textContent = '連線正常';
} else if (data.status === 'degraded') {
healthDot.classList.add('degraded');
healthLabel.textContent = '部分降級';
} else {
healthDot.classList.add('unhealthy');
healthLabel.textContent = '連線異常';
}
const dbState = data.services?.database || 'error';
if (dbStatus) dbStatus.innerHTML = `${formatStatus(dbState)} ${dbState === 'ok' ? '正常' : '異常'}`;
setStatusClass(dbStatus, dbState);
const redisState = data.services?.redis || 'disabled';
const redisText = redisState === 'ok' ? '正常' : redisState === 'disabled' ? '未啟用' : '異常';
if (redisStatus) redisStatus.innerHTML = `${formatStatus(redisState)} ${redisText}`;
setStatusClass(redisStatus, redisState);
const cache = data.cache || {};
if (cacheEnabled) cacheEnabled.textContent = cache.enabled ? '已啟用' : '未啟用';
if (cacheSysDate) cacheSysDate.textContent = cache.sys_date || '--';
if (cacheUpdatedAt) cacheUpdatedAt.textContent = formatDateTime(cache.updated_at);
const resCache = data.resource_cache || {};
if (resCache.enabled) {
if (resourceCacheEnabled) {
resourceCacheEnabled.textContent = resCache.loaded ? '已載入' : '未載入';
resourceCacheEnabled.style.color = resCache.loaded ? '#22c55e' : '#f59e0b';
}
if (resourceCacheCount) resourceCacheCount.textContent = resCache.count ? `${resCache.count}` : '--';
if (resourceCacheUpdatedAt) resourceCacheUpdatedAt.textContent = formatDateTime(resCache.updated_at);
} else {
if (resourceCacheEnabled) {
resourceCacheEnabled.textContent = '未啟用';
resourceCacheEnabled.style.color = '#9ca3af';
}
if (resourceCacheCount) resourceCacheCount.textContent = '--';
if (resourceCacheUpdatedAt) resourceCacheUpdatedAt.textContent = '--';
}
const routeCache = data.route_cache || {};
if (routeCacheMode) {
routeCacheMode.textContent = routeCache.mode || '--';
}
if (routeCacheHitRate) {
const l1 = routeCache.l1_hit_rate ?? '--';
const l2 = routeCache.l2_hit_rate ?? '--';
routeCacheHitRate.textContent = `${l1} / ${l2}`;
}
if (routeCacheDegraded) {
routeCacheDegraded.textContent = routeCache.degraded ? '是' : '否';
routeCacheDegraded.style.color = routeCache.degraded ? '#f59e0b' : '#22c55e';
}
} catch (error) {
console.error('Health check failed:', error);
healthDot.classList.remove('loading', 'healthy', 'degraded');
healthDot.classList.add('unhealthy');
healthLabel.textContent = '無法連線';
if (dbStatus) {
dbStatus.innerHTML = '✗ 無法確認';
setStatusClass(dbStatus, 'error');
}
if (redisStatus) {
redisStatus.innerHTML = '✗ 無法確認';
setStatusClass(redisStatus, 'error');
}
}
}
tabs.forEach((tab) => {
tab.addEventListener('click', () => activateTab(tab.dataset.target));
});
if (tabs.length > 0) {
activateTab(tabs[0].dataset.target);
}
window.openTool = openTool;
window.toggleHealthPopup = toggleHealthPopup;
if (healthStatus) {
healthStatus.addEventListener('click', toggleHealthPopup);
}
document.addEventListener('click', (e) => {
if (!e.target.closest('#healthStatus') && !e.target.closest('#healthPopup') && healthPopup) {
healthPopup.classList.remove('show');
}
});
checkHealth();
setInterval(checkHealth, 30000);
window.addEventListener('resize', setFrameHeight);
setFrameHeight();
})();