Files
vmis/frontend/admin-portal/system-status.html
VMIS Developer 62baadb06f feat(vmis): 租戶自動開通完整流程 + Admin Portal SSO + NC 行事曆訂閱
Backend:
- schedule_tenant: NC 新容器自動 pgsql 安裝 (_nc_db_check 全新容器處理)
- schedule_tenant: NC 初始化加入 Redis + APCu memcache 設定 (修正 OIDC invalid_state)
- schedule_tenant: 新租戶 KC realm 自動設定 accessCodeLifespan=600s (修正 authentication_expired)
- schedule_account: NC Mail 帳號自動設定 (nc_mail_result/nc_mail_done_at)
- schedule_account: NC 台灣國定假日行事曆自動訂閱 (CalDAV MKCALENDAR)
- nextcloud_client: 新增 subscribe_calendar() CalDAV 訂閱方法
- settings: 新增系統設定 API (site_title/version/timezone/SSO/Keycloak)
- models/result: 新增 nc_mail_result, nc_mail_done_at 欄位
- alembic: 遷移 002(system_settings) 003(keycloak_admin) 004(nc_mail_result)

Frontend (Admin Portal):
- 新增完整管理後台 (index/tenants/accounts/servers/schedules/logs/settings/system-status)
- api.js: Keycloak JS Adapter SSO 整合 (PKCE/S256, fallback KC JS 來源, 自動 token 更新)
- index.html: Promise.allSettled 取代 Promise.all,防止單一 API 失敗影響整頁
- 所有頁面加入 try/catch + toast 錯誤處理
- 新增品牌 LOGO 與 favicon

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-15 15:31:37 +08:00

164 lines
5.9 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!DOCTYPE html>
<html lang="zh-TW">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>系統狀態 — VMIS Admin</title>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.min.css">
<link rel="stylesheet" href="css/style.css">
<style>
.matrix-table th, .matrix-table td { vertical-align: middle; text-align: center; }
.matrix-table th:first-child, .matrix-table td:first-child { text-align: left; }
.env-badge-test { background: #cfe2ff; color: #084298; }
.env-badge-prod { background: #d1e7dd; color: #0a3622; }
.service-icon { font-size: 1.1rem; margin-right: 0.35rem; }
.light-lg { width: 18px; height: 18px; }
.log-time { font-size: 0.78rem; color: #6c757d; }
.history-dot {
display: inline-block;
width: 10px; height: 10px;
border-radius: 50%;
margin: 1px;
}
</style>
<link rel="icon" href="img/logo.png">
</head>
<body>
<!-- Sidebar -->
<nav id="sidebar">
<a class="brand" href="index.html"><img src="img/logo.png" alt="logo" style="height:28px;object-fit:contain;vertical-align:middle;" class="me-2">VMIS Admin</a>
<div class="pt-2">
<div class="nav-section">租戶服務</div>
<a class="nav-link" href="tenants.html"><i class="bi bi-building"></i> 租戶管理</a>
<a class="nav-link" href="accounts.html"><i class="bi bi-people"></i> 帳號管理</a>
<div class="nav-section">基礎設施</div>
<a class="nav-link" href="servers.html"><i class="bi bi-hdd-network"></i> 伺服器狀態</a>
<a class="nav-link active" href="system-status.html"><i class="bi bi-activity"></i> 系統狀態</a>
<div class="nav-section">排程</div>
<a class="nav-link" href="schedules.html"><i class="bi bi-clock"></i> 排程管理</a>
<a class="nav-link" href="schedule-logs.html"><i class="bi bi-list-ul"></i> 執行紀錄</a>
<div class="nav-section">系統</div>
<a class="nav-link" href="settings.html"><i class="bi bi-gear"></i> 系統設定</a>
</div>
</nav>
<!-- Main -->
<div id="main">
<div id="topbar">
<span class="fw-semibold text-secondary"><i class="bi bi-activity me-1"></i>系統狀態</span>
<div class="ms-auto text-muted small" id="last-update">載入中...</div>
</div>
<div id="content">
<!-- Matrix -->
<div class="card shadow-sm mb-4">
<div class="card-header bg-white d-flex align-items-center">
<span class="fw-semibold">基礎設施狀態矩陣</span>
<span class="ms-2 text-muted small">(最新一次執行結果)</span>
</div>
<div class="card-body">
<div id="matrix-area">
<p class="text-muted text-center py-4">尚無資料,請等待系統狀態排程執行(每日 08:00</p>
</div>
</div>
</div>
<!-- Legend -->
<div class="d-flex gap-3 mb-4 text-sm">
<span><span class="light light-green me-1"></span>正常</span>
<span><span class="light light-red me-1"></span>異常</span>
<span><span class="light light-grey me-1"></span>無資料</span>
</div>
</div>
</div>
<div id="toast-container" class="position-fixed bottom-0 end-0 p-3" style="z-index:9999"></div>
<script src="https://cdn.jsdelivr.net/npm/jquery@3.7.1/dist/jquery.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script>
<script src="js/api.js"></script>
<script>
const SERVICE_ICON = {
traefik: 'bi-diagram-3',
keycloak: 'bi-shield-lock',
mail: 'bi-envelope',
db: 'bi-database',
};
const SERVICE_LABEL = {
traefik: 'Traefik',
keycloak: 'Keycloak',
mail: 'Mail Server',
db: 'PostgreSQL',
};
async function load() {
const rows = await apiFetch('/system-status');
if (!rows || rows.length === 0) {
document.getElementById('last-update').textContent = '尚無執行紀錄';
return;
}
// Group by service_name, environment
// rows: [{ environment, service_name, service_desc, result, fail_reason, recorded_at }, ...]
const latest = {};
rows.forEach(r => {
const key = `${r.environment}__${r.service_name}`;
if (!latest[key]) latest[key] = r;
});
// Update time from first row
const anyRow = rows[0];
document.getElementById('last-update').textContent = '最後更新:' + fmtDt(anyRow.recorded_at);
// Build matrix: services × environments
const services = ['traefik', 'keycloak', 'mail', 'db'];
const envs = ['test', 'prod'];
const envLabel = { test: '測試環境', prod: '正式環境' };
let html = `
<table class="table matrix-table table-bordered mb-0">
<thead>
<tr>
<th style="width:200px">服務</th>
${envs.map(e => `<th><span class="badge ${e === 'test' ? 'env-badge-test' : 'env-badge-prod'}">${envLabel[e]}</span></th>`).join('')}
</tr>
</thead>
<tbody>`;
services.forEach(svc => {
html += `<tr>
<td class="text-start">
<i class="bi ${SERVICE_ICON[svc] || 'bi-circle'} service-icon text-primary"></i>
${SERVICE_LABEL[svc] || svc}
</td>`;
envs.forEach(env => {
const r = latest[`${env}__${svc}`];
if (!r) {
html += `<td><span class="light light-grey light-lg"></span></td>`;
} else {
const cls = r.result ? 'light-green' : 'light-red';
const title = r.fail_reason ? ` title="${r.fail_reason}"` : '';
html += `<td>
<span class="light ${cls} light-lg"${title}></span>
${!r.result && r.fail_reason
? `<br><small class="text-danger">${r.fail_reason.substring(0, 60)}${r.fail_reason.length > 60 ? '...' : ''}</small>`
: ''}
<div class="log-time">${fmtDt(r.recorded_at)}</div>
</td>`;
}
});
html += '</tr>';
});
html += `</tbody></table>`;
document.getElementById('matrix-area').innerHTML = html;
}
load();
</script>
</body>
</html>