Files
ewoooc/docker/nginx/html/monitor-index.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

906 lines
34 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.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>&copy; 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>