Files
ewoooc/docker/nginx/html/monitor-realtime.html
ogt 1b4f3a7bbe
Some checks failed
CD Pipeline / deploy (push) Failing after 59s
feat: EwoooC 初始化 — 完整專案推版至 Gitea
- 建立 Gitea Actions CD pipeline (.gitea/workflows/cd.yaml)
- 部署模式: rsync Python 檔案至 188 → docker restart (volume mount)
- Dockerfile/requirements 變動時自動重建 Docker image
- 部署通知: Telegram (開始/成功/失敗)
- 健康檢查: https://mo.wooo.work/health (最多 5 次重試)
- 同步最新 CLAUDE.md / ADR-008 / memory (2026-04-19)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-19 01:21:13 +08:00

383 lines
16 KiB
HTML

<!DOCTYPE html>
<html lang="zh-TW">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>MOMO Pro - 監控中心 (即時狀態)</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css" rel="stylesheet">
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.2/css/all.min.css" rel="stylesheet">
<style>
:root {
--primary-gradient: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
--success-color: #28a745;
--warning-color: #ffc107;
--danger-color: #dc3545;
--info-color: #17a2b8;
}
body {
background: linear-gradient(135deg, #1a1a2e 0%, #16213e 50%, #0f3460 100%);
min-height: 100vh;
color: #fff;
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
}
.header {
background: var(--primary-gradient);
padding: 1.5rem 0;
margin-bottom: 1.5rem;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.3);
}
.header h1 { font-weight: 700; margin-bottom: 0.3rem; }
.header .subtitle { opacity: 0.9; font-size: 1rem; }
.refresh-bar {
background: rgba(0,0,0,0.3);
padding: 0.5rem 1rem;
border-radius: 8px;
display: flex;
align-items: center;
gap: 1rem;
font-size: 0.85rem;
}
.section-title {
color: #fff;
font-weight: 600;
margin-bottom: 1rem;
padding-bottom: 0.5rem;
border-bottom: 2px solid rgba(255, 255, 255, 0.2);
display: flex;
align-items: center;
gap: 0.75rem;
}
.service-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
gap: 1rem;
}
.service-card {
background: rgba(255, 255, 255, 0.1);
border-radius: 12px;
padding: 1.25rem;
backdrop-filter: blur(10px);
border: 1px solid rgba(255, 255, 255, 0.1);
transition: all 0.3s ease;
position: relative;
}
.service-card:hover {
transform: translateY(-2px);
box-shadow: 0 8px 25px rgba(0, 0, 0, 0.3);
}
.service-card.status-online { border-left: 4px solid var(--success-color); }
.service-card.status-offline { border-left: 4px solid var(--danger-color); }
.service-card.status-checking { border-left: 4px solid var(--warning-color); }
.service-header {
display: flex;
justify-content: space-between;
align-items: flex-start;
margin-bottom: 0.75rem;
}
.service-name {
font-weight: 600;
font-size: 1rem;
display: flex;
align-items: center;
gap: 0.5rem;
}
.status-indicator {
display: inline-flex;
align-items: center;
gap: 0.3rem;
padding: 0.2rem 0.6rem;
border-radius: 12px;
font-size: 0.7rem;
font-weight: 600;
}
.status-online-badge {
background: rgba(40, 167, 69, 0.2);
color: #28a745;
border: 1px solid rgba(40, 167, 69, 0.4);
}
.status-offline-badge {
background: rgba(220, 53, 69, 0.2);
color: #dc3545;
border: 1px solid rgba(220, 53, 69, 0.4);
}
.status-checking-badge {
background: rgba(255, 193, 7, 0.2);
color: #ffc107;
border: 1px solid rgba(255, 193, 7, 0.4);
}
.service-info {
font-size: 0.8rem;
color: rgba(255,255,255,0.6);
margin-bottom: 0.5rem;
}
.service-meta {
display: flex;
justify-content: space-between;
align-items: center;
font-size: 0.75rem;
color: rgba(255,255,255,0.5);
}
.response-time { font-family: monospace; }
.response-time.fast { color: #28a745; }
.response-time.medium { color: #ffc107; }
.response-time.slow { color: #dc3545; }
.env-badge {
font-size: 0.65rem;
padding: 0.15rem 0.4rem;
border-radius: 4px;
font-weight: 600;
}
.env-uat { background: #007bff; color: #fff; }
.env-gcp { background: #dc3545; color: #fff; }
.env-local { background: #6c757d; color: #fff; }
.btn-open {
background: rgba(255, 255, 255, 0.15);
border: 1px solid rgba(255, 255, 255, 0.2);
color: #fff;
font-size: 0.75rem;
padding: 0.3rem 0.6rem;
border-radius: 6px;
text-decoration: none;
}
.btn-open:hover { background: rgba(255, 255, 255, 0.25); color: #fff; }
.summary-cards {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 1rem;
margin-bottom: 1.5rem;
}
.summary-card {
background: rgba(255,255,255,0.1);
border-radius: 12px;
padding: 1rem;
text-align: center;
}
.summary-card .number { font-size: 2rem; font-weight: 700; }
.summary-card .label { font-size: 0.85rem; color: rgba(255,255,255,0.7); }
.summary-card.online .number { color: #28a745; }
.summary-card.offline .number { color: #dc3545; }
.summary-card.warning .number { color: #ffc107; }
.summary-card.total .number { color: #17a2b8; }
.footer {
text-align: center;
padding: 1.5rem 0;
color: rgba(255, 255, 255, 0.5);
font-size: 0.85rem;
}
@keyframes pulse {
0%, 100% { opacity: 1; }
50% { opacity: 0.5; }
}
.checking-animation { animation: pulse 1s infinite; }
@media (max-width: 768px) {
.summary-cards { grid-template-columns: repeat(2, 1fr); }
.service-grid { grid-template-columns: 1fr; }
}
</style>
</head>
<body>
<div class="header">
<div class="container">
<div class="d-flex justify-content-between align-items-center flex-wrap gap-2">
<div>
<h1><i class="fas fa-heartbeat me-2"></i>MOMO Pro 監控中心</h1>
<p class="subtitle mb-0">即時服務狀態監控 - UAT + GCP 雙環境</p>
</div>
<div class="refresh-bar">
<span id="lastUpdate">載入中...</span>
<button class="btn btn-sm btn-light" onclick="checkAllServices()">
<i class="fas fa-sync-alt"></i> 立即刷新
</button>
</div>
</div>
</div>
</div>
<div class="container">
<div class="summary-cards">
<div class="summary-card online">
<div class="number" id="onlineCount">-</div>
<div class="label"><i class="fas fa-check-circle me-1"></i>運行中</div>
</div>
<div class="summary-card offline">
<div class="number" id="offlineCount">-</div>
<div class="label"><i class="fas fa-times-circle me-1"></i>離線</div>
</div>
<div class="summary-card warning">
<div class="number" id="warningCount">-</div>
<div class="label"><i class="fas fa-exclamation-triangle me-1"></i>檢查中</div>
</div>
<div class="summary-card total">
<div class="number" id="totalCount">-</div>
<div class="label"><i class="fas fa-server me-1"></i>總服務數</div>
</div>
</div>
<h3 class="section-title"><i class="fas fa-rocket text-primary"></i>核心應用服務</h3>
<div class="service-grid" id="coreServices"></div>
<h3 class="section-title mt-4"><i class="fas fa-tools text-warning"></i>開發工具</h3>
<div class="service-grid" id="devTools"></div>
<h3 class="section-title mt-4"><i class="fas fa-chart-line text-success"></i>監控服務</h3>
<div class="service-grid" id="monitoringServices"></div>
<h3 class="section-title mt-4"><i class="fas fa-chart-pie text-info"></i>BI 分析平台</h3>
<div class="service-grid" id="biServices"></div>
</div>
<div class="footer">
<div class="container">
<p class="mb-1">MOMO Pro System &copy; 2026 WOOO TECH</p>
<p class="mb-0">自動刷新間隔: 30 秒 | UAT: mo.wooo.work | GCP: momo.wooo.work</p>
</div>
</div>
<script>
const services = {
core: [
{ id: 'momo-uat', name: 'MOMO App', env: 'uat', link: 'https://mo.wooo.work', icon: 'fas fa-store', description: 'UAT 測試環境主應用' },
{ id: 'momo-gcp', name: 'MOMO App', env: 'gcp', link: 'https://momo.wooo.work', icon: 'fas fa-store', description: 'GCP 正式環境主應用' }
],
devTools: [
{ id: 'gitlab', name: 'GitLab', env: 'local', link: 'http://192.168.0.110:8929', icon: 'fab fa-gitlab', description: 'Git 版本控制與 CI/CD' },
{ id: 'registry', name: 'Docker Registry', env: 'local', link: 'https://registry.wooo.work', icon: 'fab fa-docker', description: '容器映像倉庫' },
{ id: 'n8n', name: 'n8n', env: 'local', link: 'http://192.168.0.110:5678', icon: 'fas fa-project-diagram', description: '自動化工作流程引擎' }
],
monitoring: [
{ id: 'grafana', name: 'Grafana (K8s)', env: 'local', link: 'http://192.168.0.110:30030', icon: 'fas fa-chart-area', description: 'K8s 監控儀表板' },
{ id: 'prometheus', name: 'Prometheus', env: 'local', link: 'https://monitor.wooo.work/prometheus/', icon: 'fas fa-fire', description: '指標收集與告警' },
{ id: 'alertmanager', name: 'Alertmanager', env: 'local', link: 'https://monitor.wooo.work/alertmanager/', icon: 'fas fa-bell', description: '告警路由管理' }
],
bi: [
{ id: 'superset', name: 'Apache Superset', env: 'local', link: 'https://monitor.wooo.work/superset/', icon: 'fas fa-tachometer-alt', description: 'BI 分析儀表板' },
{ id: 'metabase', name: 'Metabase', env: 'local', link: 'http://192.168.0.110:3030', icon: 'fas fa-table', description: '資料分析平台' }
]
};
let serviceStatus = {};
function renderServiceCard(service, status) {
const statusClass = status ? (status.online ? 'status-online' : 'status-offline') : 'status-checking';
const statusBadgeClass = status ? (status.online ? 'status-online-badge' : 'status-offline-badge') : 'status-checking-badge';
const statusText = status ? (status.online ? '運行中' : '離線') : '檢查中...';
const statusIcon = status ? (status.online ? 'fa-check-circle' : 'fa-times-circle') : 'fa-spinner fa-spin';
const envBadgeClass = service.env === 'uat' ? 'env-uat' : service.env === 'gcp' ? 'env-gcp' : 'env-local';
let responseTimeHtml = '';
if (status && status.responseTime) {
const rtClass = status.responseTime < 500 ? 'fast' : status.responseTime < 2000 ? 'medium' : 'slow';
responseTimeHtml = `<span class="response-time ${rtClass}">${status.responseTime}ms</span>`;
}
return `
<div class="service-card ${statusClass}" id="card-${service.id}">
<div class="service-header">
<div class="service-name">
<i class="${service.icon}"></i>
<span class="env-badge ${envBadgeClass}">${service.env.toUpperCase()}</span>
${service.name}
</div>
<span class="status-indicator ${statusBadgeClass} ${status ? '' : 'checking-animation'}">
<i class="fas ${statusIcon}"></i>
${statusText}
</span>
</div>
<div class="service-info">${service.description}</div>
<div class="service-meta">
${responseTimeHtml}
<a href="${service.link}" target="_blank" class="btn-open">
<i class="fas fa-external-link-alt me-1"></i>開啟
</a>
</div>
</div>
`;
}
function renderAllServices() {
document.getElementById('coreServices').innerHTML = services.core.map(s => renderServiceCard(s, serviceStatus[s.id])).join('');
document.getElementById('devTools').innerHTML = services.devTools.map(s => renderServiceCard(s, serviceStatus[s.id])).join('');
document.getElementById('monitoringServices').innerHTML = services.monitoring.map(s => renderServiceCard(s, serviceStatus[s.id])).join('');
document.getElementById('biServices').innerHTML = services.bi.map(s => renderServiceCard(s, serviceStatus[s.id])).join('');
updateSummary();
}
function updateSummary() {
const allServices = [...services.core, ...services.devTools, ...services.monitoring, ...services.bi];
let online = 0, offline = 0, checking = 0;
allServices.forEach(s => {
const status = serviceStatus[s.id];
if (status) { status.online ? online++ : offline++; } else { checking++; }
});
document.getElementById('onlineCount').textContent = online;
document.getElementById('offlineCount').textContent = offline;
document.getElementById('warningCount').textContent = checking;
document.getElementById('totalCount').textContent = allServices.length;
}
async function checkAllServices() {
document.getElementById('lastUpdate').innerHTML = '<i class="fas fa-sync-alt fa-spin me-1"></i>正在檢查...';
try {
const response = await fetch('/api/health/all');
if (response.ok) {
const data = await response.json();
if (data.services) {
Object.keys(data.services).forEach(svcId => {
const svcData = data.services[svcId];
serviceStatus[svcId] = {
online: svcData.status === 'online',
responseTime: svcData.responseTime,
statusCode: svcData.code,
lastCheck: new Date()
};
});
}
}
} catch (error) {
console.error('Health check failed:', error);
}
renderAllServices();
const now = new Date().toLocaleTimeString('zh-TW');
document.getElementById('lastUpdate').innerHTML = `<i class="fas fa-clock me-1"></i>最後更新: ${now}`;
}
document.addEventListener('DOMContentLoaded', () => {
renderAllServices();
checkAllServices();
setInterval(checkAllServices, 30000);
});
</script>
</body>
</html>