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>
906 lines
34 KiB
HTML
906 lines
34 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>WOOO Monitoring Services</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.0/css/all.min.css" rel="stylesheet">
|
||
<style>
|
||
:root {
|
||
--primary-color: #1e3c72;
|
||
--secondary-color: #2a5298;
|
||
--accent-color: #00d4ff;
|
||
--card-bg: rgba(255, 255, 255, 0.95);
|
||
}
|
||
|
||
* {
|
||
margin: 0;
|
||
padding: 0;
|
||
box-sizing: border-box;
|
||
}
|
||
|
||
body {
|
||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
|
||
min-height: 100vh;
|
||
background: linear-gradient(135deg, var(--primary-color) 0%, var(--secondary-color) 50%, #1a1a2e 100%);
|
||
background-attachment: fixed;
|
||
position: relative;
|
||
overflow-x: hidden;
|
||
}
|
||
|
||
body::before {
|
||
content: "";
|
||
position: fixed;
|
||
top: 0;
|
||
left: 0;
|
||
right: 0;
|
||
bottom: 0;
|
||
background:
|
||
radial-gradient(circle at 20% 80%, rgba(0, 212, 255, 0.1) 0%, transparent 50%),
|
||
radial-gradient(circle at 80% 20%, rgba(255, 255, 255, 0.05) 0%, transparent 50%),
|
||
radial-gradient(circle at 40% 40%, rgba(0, 212, 255, 0.05) 0%, transparent 30%);
|
||
pointer-events: none;
|
||
z-index: 0;
|
||
}
|
||
|
||
.particles {
|
||
position: fixed;
|
||
top: 0;
|
||
left: 0;
|
||
width: 100%;
|
||
height: 100%;
|
||
overflow: hidden;
|
||
z-index: 0;
|
||
pointer-events: none;
|
||
}
|
||
|
||
.particle {
|
||
position: absolute;
|
||
width: 4px;
|
||
height: 4px;
|
||
background: rgba(0, 212, 255, 0.6);
|
||
border-radius: 50%;
|
||
animation: float 15s infinite;
|
||
}
|
||
|
||
@keyframes float {
|
||
0%, 100% { transform: translateY(100vh) rotate(0deg); opacity: 0; }
|
||
10% { opacity: 1; }
|
||
90% { opacity: 1; }
|
||
100% { transform: translateY(-100vh) rotate(720deg); opacity: 0; }
|
||
}
|
||
|
||
.main-container {
|
||
position: relative;
|
||
z-index: 1;
|
||
padding: 40px 20px;
|
||
min-height: 100vh;
|
||
}
|
||
|
||
.header {
|
||
text-align: center;
|
||
margin-bottom: 30px;
|
||
}
|
||
|
||
.logo-container {
|
||
margin-bottom: 15px;
|
||
}
|
||
|
||
.logo {
|
||
width: 160px;
|
||
height: auto;
|
||
border-radius: 8px;
|
||
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.2);
|
||
}
|
||
|
||
.header h1 {
|
||
color: white;
|
||
font-size: 2.2rem;
|
||
font-weight: 700;
|
||
margin-bottom: 8px;
|
||
text-shadow: 0 2px 20px rgba(0, 0, 0, 0.3);
|
||
}
|
||
|
||
.header .subtitle {
|
||
color: rgba(255, 255, 255, 0.8);
|
||
font-size: 1rem;
|
||
}
|
||
|
||
.last-update {
|
||
color: rgba(255, 255, 255, 0.6);
|
||
font-size: 0.85rem;
|
||
margin-top: 5px;
|
||
}
|
||
|
||
/* Status Overview */
|
||
.status-overview {
|
||
background: rgba(255, 255, 255, 0.1);
|
||
border-radius: 16px;
|
||
padding: 20px;
|
||
margin-bottom: 30px;
|
||
backdrop-filter: blur(10px);
|
||
}
|
||
|
||
.status-grid {
|
||
display: grid;
|
||
grid-template-columns: repeat(auto-fit, minmax(120px, 1fr));
|
||
gap: 15px;
|
||
}
|
||
|
||
.status-item {
|
||
text-align: center;
|
||
padding: 15px 10px;
|
||
background: rgba(255, 255, 255, 0.1);
|
||
border-radius: 12px;
|
||
transition: all 0.3s ease;
|
||
}
|
||
|
||
.status-item:hover {
|
||
background: rgba(255, 255, 255, 0.15);
|
||
transform: translateY(-2px);
|
||
}
|
||
|
||
.status-icon {
|
||
font-size: 1.8rem;
|
||
margin-bottom: 8px;
|
||
}
|
||
|
||
.status-icon.healthy { color: #51cf66; }
|
||
.status-icon.unhealthy { color: #ff6b6b; }
|
||
.status-icon.loading { color: #ffd43b; animation: pulse 1s infinite; }
|
||
|
||
@keyframes pulse {
|
||
0%, 100% { opacity: 1; }
|
||
50% { opacity: 0.5; }
|
||
}
|
||
|
||
.status-label {
|
||
color: white;
|
||
font-size: 0.85rem;
|
||
font-weight: 500;
|
||
}
|
||
|
||
.status-value {
|
||
color: rgba(255, 255, 255, 0.7);
|
||
font-size: 0.75rem;
|
||
margin-top: 3px;
|
||
}
|
||
|
||
/* Alerts Panel */
|
||
.alerts-panel {
|
||
background: rgba(255, 255, 255, 0.95);
|
||
border-radius: 16px;
|
||
padding: 20px;
|
||
margin-bottom: 30px;
|
||
}
|
||
|
||
.alerts-header {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
margin-bottom: 15px;
|
||
}
|
||
|
||
.alerts-title {
|
||
font-size: 1.1rem;
|
||
font-weight: 600;
|
||
color: #1a1a2e;
|
||
}
|
||
|
||
.alerts-count {
|
||
background: #ff6b6b;
|
||
color: white;
|
||
padding: 4px 12px;
|
||
border-radius: 20px;
|
||
font-size: 0.85rem;
|
||
font-weight: 600;
|
||
}
|
||
|
||
.alerts-count.zero {
|
||
background: #51cf66;
|
||
}
|
||
|
||
.alert-item {
|
||
display: flex;
|
||
align-items: flex-start;
|
||
padding: 12px;
|
||
margin-bottom: 10px;
|
||
border-radius: 8px;
|
||
background: #fff5f5;
|
||
border-left: 4px solid #ff6b6b;
|
||
}
|
||
|
||
.alert-item.warning {
|
||
background: #fffbe6;
|
||
border-left-color: #ffd43b;
|
||
}
|
||
|
||
.alert-item.critical {
|
||
background: #fff0f0;
|
||
border-left-color: #e03131;
|
||
}
|
||
|
||
.alert-icon {
|
||
margin-right: 12px;
|
||
font-size: 1.2rem;
|
||
}
|
||
|
||
.alert-content {
|
||
flex: 1;
|
||
}
|
||
|
||
.alert-name {
|
||
font-weight: 600;
|
||
color: #333;
|
||
font-size: 0.95rem;
|
||
}
|
||
|
||
.alert-message {
|
||
color: #666;
|
||
font-size: 0.85rem;
|
||
margin-top: 3px;
|
||
}
|
||
|
||
.alert-instance {
|
||
color: #999;
|
||
font-size: 0.75rem;
|
||
margin-top: 5px;
|
||
}
|
||
|
||
.no-alerts {
|
||
text-align: center;
|
||
padding: 30px;
|
||
color: #51cf66;
|
||
}
|
||
|
||
.no-alerts i {
|
||
font-size: 2.5rem;
|
||
margin-bottom: 10px;
|
||
}
|
||
|
||
/* n8n Workflows Panel */
|
||
.workflows-panel {
|
||
background: rgba(255, 255, 255, 0.95);
|
||
border-radius: 16px;
|
||
padding: 20px;
|
||
margin-bottom: 30px;
|
||
}
|
||
|
||
.workflow-item {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
padding: 10px 15px;
|
||
margin-bottom: 8px;
|
||
background: #f8f9fa;
|
||
border-radius: 8px;
|
||
}
|
||
|
||
.workflow-name {
|
||
font-size: 0.9rem;
|
||
color: #333;
|
||
}
|
||
|
||
.workflow-status {
|
||
padding: 3px 10px;
|
||
border-radius: 12px;
|
||
font-size: 0.75rem;
|
||
font-weight: 500;
|
||
}
|
||
|
||
.workflow-status.active {
|
||
background: #d3f9d8;
|
||
color: #2b8a3e;
|
||
}
|
||
|
||
.workflow-status.inactive {
|
||
background: #ffe3e3;
|
||
color: #c92a2a;
|
||
}
|
||
|
||
/* Section */
|
||
.section {
|
||
margin-bottom: 30px;
|
||
}
|
||
|
||
.section-title {
|
||
color: white;
|
||
font-size: 1.2rem;
|
||
font-weight: 600;
|
||
margin-bottom: 15px;
|
||
padding-bottom: 10px;
|
||
border-bottom: 2px solid rgba(0, 212, 255, 0.3);
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 10px;
|
||
}
|
||
|
||
.section-title i {
|
||
color: var(--accent-color);
|
||
}
|
||
|
||
/* Cards Grid */
|
||
.cards-grid {
|
||
display: grid;
|
||
grid-template-columns: repeat(auto-fill, minmax(260px, 1fr));
|
||
gap: 15px;
|
||
}
|
||
|
||
/* Service Card */
|
||
.service-card {
|
||
background: var(--card-bg);
|
||
border-radius: 12px;
|
||
padding: 18px;
|
||
text-decoration: none;
|
||
color: #333;
|
||
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||
display: flex;
|
||
align-items: flex-start;
|
||
gap: 14px;
|
||
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.1);
|
||
border: 1px solid rgba(255, 255, 255, 0.2);
|
||
position: relative;
|
||
overflow: hidden;
|
||
}
|
||
|
||
.service-card::before {
|
||
content: "";
|
||
position: absolute;
|
||
top: 0;
|
||
left: 0;
|
||
right: 0;
|
||
height: 3px;
|
||
background: linear-gradient(90deg, var(--accent-color), var(--secondary-color));
|
||
transform: scaleX(0);
|
||
transition: transform 0.3s ease;
|
||
}
|
||
|
||
.service-card:hover {
|
||
transform: translateY(-5px);
|
||
box-shadow: 0 8px 30px rgba(0, 0, 0, 0.15);
|
||
color: #333;
|
||
text-decoration: none;
|
||
}
|
||
|
||
.service-card:hover::before {
|
||
transform: scaleX(1);
|
||
}
|
||
|
||
.card-icon {
|
||
width: 44px;
|
||
height: 44px;
|
||
border-radius: 10px;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
font-size: 1.3rem;
|
||
flex-shrink: 0;
|
||
}
|
||
|
||
.card-icon.grafana { background: linear-gradient(135deg, #f46800, #f9b233); color: white; }
|
||
.card-icon.prometheus { background: linear-gradient(135deg, #e6522c, #f0a000); color: white; }
|
||
.card-icon.portainer { background: linear-gradient(135deg, #13bef9, #0db7ed); color: white; }
|
||
.card-icon.pgadmin { background: linear-gradient(135deg, #336791, #0078d7); color: white; }
|
||
.card-icon.cadvisor { background: linear-gradient(135deg, #4285f4, #34a853); color: white; }
|
||
.card-icon.node { background: linear-gradient(135deg, #e84d3d, #c0392b); color: white; }
|
||
.card-icon.blackbox { background: linear-gradient(135deg, #9b59b6, #8e44ad); color: white; }
|
||
.card-icon.postgres { background: linear-gradient(135deg, #336791, #2f5e8d); color: white; }
|
||
.card-icon.gitlab { background: linear-gradient(135deg, #fc6d26, #e24329); color: white; }
|
||
.card-icon.registry { background: linear-gradient(135deg, #4a90d9, #60b044); color: white; }
|
||
.card-icon.watchtower { background: linear-gradient(135deg, #00b4d8, #0077b6); color: white; }
|
||
.card-icon.n8n { background: linear-gradient(135deg, #ea4b71, #ff6d5a); color: white; }
|
||
.card-icon.loki { background: linear-gradient(135deg, #f2c94c, #f2994a); color: white; }
|
||
.card-icon.superset { background: linear-gradient(135deg, #1fa8c9, #00A699); color: white; }
|
||
.card-icon.metabase { background: linear-gradient(135deg, #509ee3, #2d86d4); color: white; }
|
||
.card-icon.nextcloud { background: linear-gradient(135deg, #0082c9, #00639a); color: white; }
|
||
.card-icon.grist { background: linear-gradient(135deg, #16b378, #0d8957); color: white; }
|
||
.card-icon.alertmanager { background: linear-gradient(135deg, #e6522c, #c0392b); color: white; }
|
||
.card-icon.k8s { background: linear-gradient(135deg, #326ce5, #1d4cc4); color: white; }
|
||
|
||
.card-content {
|
||
flex: 1;
|
||
}
|
||
|
||
.card-content h3 {
|
||
font-size: 1rem;
|
||
font-weight: 600;
|
||
margin-bottom: 4px;
|
||
color: #1a1a2e;
|
||
}
|
||
|
||
.card-content p {
|
||
font-size: 0.8rem;
|
||
color: #666;
|
||
margin: 0;
|
||
line-height: 1.3;
|
||
}
|
||
|
||
.card-badge {
|
||
position: absolute;
|
||
top: 10px;
|
||
right: 10px;
|
||
font-size: 0.65rem;
|
||
padding: 2px 6px;
|
||
border-radius: 12px;
|
||
background: rgba(0, 212, 255, 0.1);
|
||
color: var(--secondary-color);
|
||
font-weight: 500;
|
||
}
|
||
|
||
.card-status {
|
||
position: absolute;
|
||
bottom: 10px;
|
||
right: 10px;
|
||
width: 10px;
|
||
height: 10px;
|
||
border-radius: 50%;
|
||
background: #adb5bd;
|
||
}
|
||
|
||
.card-status.healthy { background: #51cf66; box-shadow: 0 0 8px rgba(81, 207, 102, 0.5); }
|
||
.card-status.unhealthy { background: #ff6b6b; box-shadow: 0 0 8px rgba(255, 107, 107, 0.5); }
|
||
|
||
/* Footer */
|
||
.footer {
|
||
text-align: center;
|
||
margin-top: 40px;
|
||
padding: 20px;
|
||
color: rgba(255, 255, 255, 0.6);
|
||
font-size: 0.85rem;
|
||
}
|
||
|
||
.footer a {
|
||
color: var(--accent-color);
|
||
text-decoration: none;
|
||
}
|
||
|
||
/* Responsive */
|
||
@media (max-width: 768px) {
|
||
.header h1 { font-size: 1.6rem; }
|
||
.cards-grid { grid-template-columns: 1fr; }
|
||
.main-container { padding: 20px 15px; }
|
||
.status-grid { grid-template-columns: repeat(3, 1fr); }
|
||
}
|
||
</style>
|
||
</head>
|
||
<body>
|
||
<div class="particles">
|
||
<div class="particle" style="left: 10%; animation-delay: 0s;"></div>
|
||
<div class="particle" style="left: 20%; animation-delay: 2s;"></div>
|
||
<div class="particle" style="left: 30%; animation-delay: 4s;"></div>
|
||
<div class="particle" style="left: 40%; animation-delay: 1s;"></div>
|
||
<div class="particle" style="left: 50%; animation-delay: 3s;"></div>
|
||
<div class="particle" style="left: 60%; animation-delay: 5s;"></div>
|
||
<div class="particle" style="left: 70%; animation-delay: 2.5s;"></div>
|
||
<div class="particle" style="left: 80%; animation-delay: 4.5s;"></div>
|
||
<div class="particle" style="left: 90%; animation-delay: 1.5s;"></div>
|
||
</div>
|
||
|
||
<div class="main-container">
|
||
<div class="container">
|
||
<!-- Header -->
|
||
<div class="header">
|
||
<div class="logo-container">
|
||
<img src="/monitor-static/images/WOOO_Logo_trimmed.jpg" alt="WOOO Logo" class="logo">
|
||
</div>
|
||
<h1>Monitoring Services</h1>
|
||
<p class="subtitle"><i class="fas fa-server me-2"></i>WOOO TECH 監控服務中心</p>
|
||
<p class="last-update">最後更新: <span id="lastUpdate">-</span></p>
|
||
</div>
|
||
|
||
<!-- Status Overview -->
|
||
<div class="status-overview">
|
||
<div class="status-grid" id="statusGrid">
|
||
<div class="status-item">
|
||
<div class="status-icon loading" id="status-registry"><i class="fas fa-spinner fa-spin"></i></div>
|
||
<div class="status-label">Registry</div>
|
||
<div class="status-value" id="status-registry-val">檢測中...</div>
|
||
</div>
|
||
<div class="status-item">
|
||
<div class="status-icon loading" id="status-grafana"><i class="fas fa-spinner fa-spin"></i></div>
|
||
<div class="status-label">Grafana</div>
|
||
<div class="status-value" id="status-grafana-val">檢測中...</div>
|
||
</div>
|
||
<div class="status-item">
|
||
<div class="status-icon loading" id="status-prometheus"><i class="fas fa-spinner fa-spin"></i></div>
|
||
<div class="status-label">Prometheus</div>
|
||
<div class="status-value" id="status-prometheus-val">檢測中...</div>
|
||
</div>
|
||
<div class="status-item">
|
||
<div class="status-icon loading" id="status-n8n"><i class="fas fa-spinner fa-spin"></i></div>
|
||
<div class="status-label">n8n</div>
|
||
<div class="status-value" id="status-n8n-val">檢測中...</div>
|
||
</div>
|
||
<div class="status-item">
|
||
<div class="status-icon loading" id="status-momo_app"><i class="fas fa-spinner fa-spin"></i></div>
|
||
<div class="status-label">MOMO App</div>
|
||
<div class="status-value" id="status-momo_app-val">檢測中...</div>
|
||
</div>
|
||
<div class="status-item">
|
||
<div class="status-icon loading" id="status-database"><i class="fas fa-spinner fa-spin"></i></div>
|
||
<div class="status-label">Database</div>
|
||
<div class="status-value" id="status-database-val">檢測中...</div>
|
||
</div>
|
||
<div class="status-item">
|
||
<div class="status-icon loading" id="status-superset"><i class="fas fa-spinner fa-spin"></i></div>
|
||
<div class="status-label">Superset</div>
|
||
<div class="status-value" id="status-superset-val">檢測中...</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Alerts Panel -->
|
||
<div class="alerts-panel">
|
||
<div class="alerts-header">
|
||
<span class="alerts-title"><i class="fas fa-bell me-2"></i>即時告警</span>
|
||
<span class="alerts-count zero" id="alertsCount">0</span>
|
||
</div>
|
||
<div id="alertsList">
|
||
<div class="no-alerts">
|
||
<i class="fas fa-check-circle"></i>
|
||
<p>載入中...</p>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- n8n Workflows Panel -->
|
||
<div class="workflows-panel">
|
||
<div class="alerts-header">
|
||
<span class="alerts-title"><i class="fas fa-project-diagram me-2"></i>n8n 監控工作流程</span>
|
||
</div>
|
||
<div id="workflowsList">
|
||
<p class="text-muted text-center py-3">載入中...</p>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- CI/CD -->
|
||
<div class="section">
|
||
<h2 class="section-title"><i class="fas fa-rocket"></i>CI/CD</h2>
|
||
<div class="cards-grid">
|
||
<a href="http://192.168.0.110:8929/" target="_blank" class="service-card">
|
||
<div class="card-icon gitlab"><i class="fab fa-gitlab"></i></div>
|
||
<div class="card-content">
|
||
<h3>GitLab</h3>
|
||
<p>自建 Git 伺服器,CI/CD Pipeline</p>
|
||
</div>
|
||
<span class="card-badge">Self-hosted</span>
|
||
</a>
|
||
<a href="https://registry.wooo.work/" target="_blank" class="service-card">
|
||
<div class="card-icon registry"><i class="fas fa-box"></i></div>
|
||
<div class="card-content">
|
||
<h3>Registry</h3>
|
||
<p>Docker Container Registry</p>
|
||
</div>
|
||
<span class="card-badge">Self-hosted</span>
|
||
<div class="card-status" id="card-status-registry"></div>
|
||
</a>
|
||
<a href="http://192.168.0.110:5678/" target="_blank" class="service-card">
|
||
<div class="card-icon n8n"><i class="fas fa-project-diagram"></i></div>
|
||
<div class="card-content">
|
||
<h3>n8n</h3>
|
||
<p>工作流自動化平台</p>
|
||
</div>
|
||
<span class="card-badge">UAT</span>
|
||
<div class="card-status" id="card-status-n8n"></div>
|
||
</a>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 視覺化與分析 -->
|
||
<div class="section">
|
||
<h2 class="section-title"><i class="fas fa-chart-line"></i>視覺化與分析</h2>
|
||
<div class="cards-grid">
|
||
<a href="/grafana/" class="service-card">
|
||
<div class="card-icon grafana"><i class="fas fa-chart-area"></i></div>
|
||
<div class="card-content">
|
||
<h3>Grafana (Docker)</h3>
|
||
<p>儀表板、Loki 日誌、Prometheus 指標</p>
|
||
</div>
|
||
<div class="card-status" id="card-status-grafana"></div>
|
||
</a>
|
||
<a href="/k8s-grafana/" class="service-card">
|
||
<div class="card-icon k8s"><i class="fas fa-chart-area"></i></div>
|
||
<div class="card-content">
|
||
<h3>Grafana (K8s)</h3>
|
||
<p>K8s 叢集監控儀表板</p>
|
||
</div>
|
||
<div class="card-status" id="card-status-k8s-grafana"></div>
|
||
</a>
|
||
<a href="/prometheus/" class="service-card">
|
||
<div class="card-icon prometheus"><i class="fas fa-fire"></i></div>
|
||
<div class="card-content">
|
||
<h3>Prometheus</h3>
|
||
<p>時序資料庫,系統指標收集</p>
|
||
</div>
|
||
<div class="card-status" id="card-status-prometheus"></div>
|
||
</a>
|
||
<a href="/alertmanager/" class="service-card">
|
||
<div class="card-icon alertmanager"><i class="fas fa-bell"></i></div>
|
||
<div class="card-content">
|
||
<h3>Alertmanager</h3>
|
||
<p>告警路由與通知管理</p>
|
||
</div>
|
||
<div class="card-status" id="card-status-alertmanager"></div>
|
||
</a>
|
||
<a href="/loki/" class="service-card">
|
||
<div class="card-icon loki"><i class="fas fa-scroll"></i></div>
|
||
<div class="card-content">
|
||
<h3>Loki</h3>
|
||
<p>日誌聚合系統</p>
|
||
</div>
|
||
</a>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- BI 分析平台 -->
|
||
<div class="section">
|
||
<h2 class="section-title"><i class="fas fa-chart-pie"></i>BI 分析平台</h2>
|
||
<div class="cards-grid">
|
||
<a href="/superset/" class="service-card">
|
||
<div class="card-icon superset"><i class="fas fa-chart-bar"></i></div>
|
||
<div class="card-content">
|
||
<h3>Apache Superset</h3>
|
||
<p>商業智慧儀表板,資料視覺化</p>
|
||
</div>
|
||
<span class="card-badge">BI</span>
|
||
<div class="card-status" id="card-status-superset"></div>
|
||
</a>
|
||
<a href="/metabase/" class="service-card">
|
||
<div class="card-icon metabase"><i class="fas fa-analytics"></i></div>
|
||
<div class="card-content">
|
||
<h3>Metabase</h3>
|
||
<p>資料分析與報表工具</p>
|
||
</div>
|
||
<span class="card-badge">BI</span>
|
||
<div class="card-status" id="card-status-metabase"></div>
|
||
</a>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 雲端服務 -->
|
||
<div class="section">
|
||
<h2 class="section-title"><i class="fas fa-cloud"></i>雲端服務</h2>
|
||
<div class="cards-grid">
|
||
<a href="http://cloud.wooo.work/" target="_blank" class="service-card">
|
||
<div class="card-icon nextcloud"><i class="fas fa-cloud-upload-alt"></i></div>
|
||
<div class="card-content">
|
||
<h3>Nextcloud</h3>
|
||
<p>私有雲端檔案儲存</p>
|
||
</div>
|
||
<span class="card-badge">內網</span>
|
||
</a>
|
||
<a href="http://grist.wooo.work/" target="_blank" class="service-card">
|
||
<div class="card-icon grist"><i class="fas fa-table"></i></div>
|
||
<div class="card-content">
|
||
<h3>Grist</h3>
|
||
<p>線上試算表與資料庫</p>
|
||
</div>
|
||
<span class="card-badge">內網</span>
|
||
</a>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 系統管理 -->
|
||
<div class="section">
|
||
<h2 class="section-title"><i class="fas fa-cogs"></i>系統管理</h2>
|
||
<div class="cards-grid">
|
||
<a href="/portainer/" class="service-card">
|
||
<div class="card-icon portainer"><i class="fab fa-docker"></i></div>
|
||
<div class="card-content">
|
||
<h3>Portainer</h3>
|
||
<p>Docker 容器管理介面</p>
|
||
</div>
|
||
</a>
|
||
<a href="/pgadmin/" class="service-card">
|
||
<div class="card-icon pgadmin"><i class="fas fa-database"></i></div>
|
||
<div class="card-content">
|
||
<h3>pgAdmin</h3>
|
||
<p>PostgreSQL 管理介面</p>
|
||
</div>
|
||
</a>
|
||
<div class="service-card" style="cursor: default;">
|
||
<div class="card-icon watchtower"><i class="fas fa-sync-alt"></i></div>
|
||
<div class="card-content">
|
||
<h3>Watchtower</h3>
|
||
<p>自動偵測映像更新並重啟容器</p>
|
||
</div>
|
||
<span class="card-badge">Auto</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Exporters -->
|
||
<div class="section">
|
||
<h2 class="section-title"><i class="fas fa-download"></i>Exporters</h2>
|
||
<div class="cards-grid">
|
||
<a href="/cadvisor/" class="service-card">
|
||
<div class="card-icon cadvisor"><i class="fas fa-cube"></i></div>
|
||
<div class="card-content">
|
||
<h3>cAdvisor</h3>
|
||
<p>容器資源監控</p>
|
||
</div>
|
||
</a>
|
||
<a href="/node-exporter/metrics" class="service-card">
|
||
<div class="card-icon node"><i class="fas fa-microchip"></i></div>
|
||
<div class="card-content">
|
||
<h3>Node Exporter</h3>
|
||
<p>主機系統指標</p>
|
||
</div>
|
||
</a>
|
||
<a href="/blackbox/" class="service-card">
|
||
<div class="card-icon blackbox"><i class="fas fa-satellite-dish"></i></div>
|
||
<div class="card-content">
|
||
<h3>Blackbox Exporter</h3>
|
||
<p>端點探測監控</p>
|
||
</div>
|
||
</a>
|
||
<a href="/postgres-exporter/metrics" class="service-card">
|
||
<div class="card-icon postgres"><i class="fas fa-elephant"></i></div>
|
||
<div class="card-content">
|
||
<h3>PostgreSQL Exporter</h3>
|
||
<p>資料庫效能指標</p>
|
||
</div>
|
||
</a>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Footer -->
|
||
<div class="footer">
|
||
<p>© 2026 WOOO TECH. All rights reserved.</p>
|
||
<p><a href="https://mo.wooo.work/">返回 Momo Pro System</a></p>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<script>
|
||
// 監控資料 API (使用相對路徑,由 Nginx 代理到 mo.wooo.work)
|
||
const MONITOR_API = '/api/system/monitor/overview';
|
||
|
||
// 更新狀態圖示
|
||
function updateStatusIcon(service, healthy) {
|
||
const iconEl = document.getElementById(`status-${service}`);
|
||
const valEl = document.getElementById(`status-${service}-val`);
|
||
const cardStatus = document.getElementById(`card-status-${service}`);
|
||
|
||
if (iconEl) {
|
||
iconEl.className = `status-icon ${healthy ? 'healthy' : 'unhealthy'}`;
|
||
iconEl.innerHTML = healthy
|
||
? '<i class="fas fa-check-circle"></i>'
|
||
: '<i class="fas fa-times-circle"></i>';
|
||
}
|
||
if (valEl) {
|
||
valEl.textContent = healthy ? '運行中' : '異常';
|
||
}
|
||
if (cardStatus) {
|
||
cardStatus.className = `card-status ${healthy ? 'healthy' : 'unhealthy'}`;
|
||
}
|
||
}
|
||
|
||
// 更新告警列表
|
||
function updateAlerts(alerts) {
|
||
const listEl = document.getElementById('alertsList');
|
||
const countEl = document.getElementById('alertsCount');
|
||
|
||
countEl.textContent = alerts.length;
|
||
countEl.className = alerts.length === 0 ? 'alerts-count zero' : 'alerts-count';
|
||
|
||
if (alerts.length === 0) {
|
||
listEl.innerHTML = `
|
||
<div class="no-alerts">
|
||
<i class="fas fa-check-circle"></i>
|
||
<p>所有服務正常運行</p>
|
||
</div>
|
||
`;
|
||
return;
|
||
}
|
||
|
||
listEl.innerHTML = alerts.map(alert => `
|
||
<div class="alert-item ${alert.severity}">
|
||
<i class="alert-icon fas fa-exclamation-triangle" style="color: ${alert.severity === 'critical' ? '#e03131' : '#ffd43b'}"></i>
|
||
<div class="alert-content">
|
||
<div class="alert-name">${escapeHtml(alert.name)}</div>
|
||
<div class="alert-message">${escapeHtml(alert.message || '無詳細資訊')}</div>
|
||
${alert.instance ? `<div class="alert-instance">${escapeHtml(alert.instance)}</div>` : ''}
|
||
</div>
|
||
</div>
|
||
`).join('');
|
||
}
|
||
|
||
// 更新 n8n 工作流程列表
|
||
function updateWorkflows(workflows) {
|
||
const listEl = document.getElementById('workflowsList');
|
||
|
||
if (!workflows || workflows.length === 0) {
|
||
listEl.innerHTML = '<p class="text-muted text-center py-3">無監控工作流程</p>';
|
||
return;
|
||
}
|
||
|
||
// 只顯示監控相關的工作流程
|
||
const monitorWorkflows = workflows.filter(wf =>
|
||
wf.name && (
|
||
wf.name.includes('監控') ||
|
||
wf.name.includes('Monitor') ||
|
||
wf.name.includes('告警') ||
|
||
wf.name.includes('Alert') ||
|
||
wf.name.includes('Health') ||
|
||
wf.name.includes('健康')
|
||
)
|
||
);
|
||
|
||
if (monitorWorkflows.length === 0) {
|
||
listEl.innerHTML = '<p class="text-muted text-center py-3">無監控工作流程</p>';
|
||
return;
|
||
}
|
||
|
||
listEl.innerHTML = monitorWorkflows.map(wf => `
|
||
<div class="workflow-item">
|
||
<span class="workflow-name">${escapeHtml(wf.name)}</span>
|
||
<span class="workflow-status ${wf.active ? 'active' : 'inactive'}">
|
||
${wf.active ? '運行中' : '已停用'}
|
||
</span>
|
||
</div>
|
||
`).join('');
|
||
}
|
||
|
||
// HTML 轉義
|
||
function escapeHtml(text) {
|
||
if (!text) return '';
|
||
const div = document.createElement('div');
|
||
div.textContent = text;
|
||
return div.innerHTML;
|
||
}
|
||
|
||
// 載入監控資料
|
||
async function loadMonitorData() {
|
||
try {
|
||
const response = await fetch(MONITOR_API, {
|
||
method: 'GET',
|
||
headers: { 'Accept': 'application/json' }
|
||
});
|
||
|
||
if (!response.ok) throw new Error(`HTTP ${response.status}`);
|
||
|
||
const data = await response.json();
|
||
|
||
// 更新服務狀態
|
||
if (data.services) {
|
||
Object.entries(data.services).forEach(([service, info]) => {
|
||
updateStatusIcon(service, info.healthy);
|
||
});
|
||
}
|
||
|
||
// 更新告警
|
||
if (data.alerts) {
|
||
updateAlerts(data.alerts);
|
||
}
|
||
|
||
// 更新工作流程
|
||
if (data.n8n_workflows) {
|
||
updateWorkflows(data.n8n_workflows);
|
||
}
|
||
|
||
// 更新最後更新時間
|
||
document.getElementById('lastUpdate').textContent =
|
||
new Date().toLocaleString('zh-TW', { timeZone: 'Asia/Taipei' });
|
||
|
||
} catch (error) {
|
||
console.error('載入監控資料失敗:', error);
|
||
// 顯示錯誤狀態
|
||
['registry', 'grafana', 'prometheus', 'n8n', 'momo_app', 'database', 'superset'].forEach(service => {
|
||
const valEl = document.getElementById(`status-${service}-val`);
|
||
if (valEl) valEl.textContent = '連線失敗';
|
||
});
|
||
}
|
||
}
|
||
|
||
// 初始化
|
||
document.addEventListener('DOMContentLoaded', () => {
|
||
loadMonitorData();
|
||
// 每 30 秒更新一次
|
||
setInterval(loadMonitorData, 30000);
|
||
});
|
||
</script>
|
||
</body>
|
||
</html>
|