Some checks failed
CD Pipeline / deploy (push) Failing after 59s
- 建立 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>
383 lines
16 KiB
HTML
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 © 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>
|