diff --git a/apps/api/src/api/v1/monitoring.py b/apps/api/src/api/v1/monitoring.py index 5498b166..f7a47cca 100644 --- a/apps/api/src/api/v1/monitoring.py +++ b/apps/api/src/api/v1/monitoring.py @@ -53,12 +53,13 @@ async def _probe_grafana(client: httpx.AsyncClient) -> dict: "version": version, "stats": f"面板 {dash_count} 個" if dash_count is not None else "監控面板", "description": "指標視覺化 · Dashboard", + "url": base, } except Exception as e: logger.warning("grafana_probe_failed", error=str(e)) return { "name": "Grafana", "status": "down", "version": None, - "stats": None, "description": "指標視覺化 · Dashboard", + "stats": None, "description": "指標視覺化 · Dashboard", "url": base, } @@ -91,12 +92,13 @@ async def _probe_prometheus(client: httpx.AsyncClient) -> dict: "stats": " · ".join(stats_parts), "description": "時序資料庫 · 告警規則", "firing_count": firing_count, + "url": base, } except Exception as e: logger.warning("prometheus_probe_failed", error=str(e)) return { "name": "Prometheus", "status": "down", "version": None, - "stats": None, "description": "時序資料庫 · 告警規則", "firing_count": 0, + "stats": None, "description": "時序資料庫 · 告警規則", "firing_count": 0, "url": base, } @@ -119,12 +121,13 @@ async def _probe_sentry(client: httpx.AsyncClient) -> dict: "version": version, "stats": "Error Tracking · Issue", "description": "錯誤追蹤 · Issue 管理", + "url": base, } except Exception as e: logger.warning("sentry_probe_failed", error=str(e)) return { "name": "Sentry", "status": "down", "version": None, - "stats": None, "description": "錯誤追蹤 · Issue 管理", + "stats": None, "description": "錯誤追蹤 · Issue 管理", "url": base, } @@ -141,12 +144,13 @@ async def _probe_langfuse(client: httpx.AsyncClient) -> dict: "version": version, "stats": "LLM Tracing · AI 觀測", "description": "LLM 追蹤 · AI 成本監控", + "url": base, } except Exception as e: logger.warning("langfuse_probe_failed", error=str(e)) return { "name": "Langfuse", "status": "down", "version": None, - "stats": None, "description": "LLM 追蹤 · AI 成本監控", + "stats": None, "description": "LLM 追蹤 · AI 成本監控", "url": base, } @@ -161,6 +165,7 @@ async def _probe_signoz(client: httpx.AsyncClient) -> dict: "version": None, "stats": "APM · Trace · Log", "description": "可觀測性平台 · OTEL", + "url": base, } except Exception as e: logger.warning("signoz_probe_failed", error=str(e)) @@ -169,13 +174,13 @@ async def _probe_signoz(client: httpx.AsyncClient) -> dict: if r2.status_code in (200, 301, 302): return { "name": "SigNoz", "status": "up", "version": None, - "stats": "APM · Trace · Log", "description": "可觀測性平台 · OTEL", + "stats": "APM · Trace · Log", "description": "可觀測性平台 · OTEL", "url": base, } except Exception: pass return { "name": "SigNoz", "status": "down", "version": None, - "stats": None, "description": "可觀測性平台 · OTEL", + "stats": None, "description": "可觀測性平台 · OTEL", "url": base, } @@ -192,12 +197,13 @@ async def _probe_gitea(client: httpx.AsyncClient) -> dict: "version": version, "stats": "CI/CD · Git · Mirror", "description": "代碼倉庫 · Pipeline", + "url": base, } except Exception as e: logger.warning("gitea_probe_failed", error=str(e)) return { "name": "Gitea", "status": "down", "version": None, - "stats": None, "description": "代碼倉庫 · Pipeline", + "stats": None, "description": "代碼倉庫 · Pipeline", "url": base, } diff --git a/apps/web/src/app/[locale]/page.tsx b/apps/web/src/app/[locale]/page.tsx index e75a91ac..f0a8dc86 100644 --- a/apps/web/src/app/[locale]/page.tsx +++ b/apps/web/src/app/[locale]/page.tsx @@ -76,16 +76,7 @@ interface MonitoringTool { description: string firing_count?: number checked_at: string -} - -// 各工具連結(內網,在同網段才能開啟) -const TOOL_LINKS: Record = { - Grafana: 'http://192.168.0.110:3002', - Prometheus: 'http://192.168.0.110:9090', - Sentry: 'http://192.168.0.110:9000', - Langfuse: 'http://192.168.0.110:3100', - SigNoz: 'http://192.168.0.188:3301', - Gitea: 'http://192.168.0.110:3001', + url?: string } // figma-v2 左側彩色條顏色 @@ -142,7 +133,7 @@ function MonitoringTools() { const statusText = isUp ? (hasFiring ? `${tool.firing_count} ${tDash('monitoringStatus.firing')}` : tDash('monitoringStatus.up')) : tDash('monitoringStatus.down') const accentColor = TOOL_ACCENT_COLOR[tool.name] ?? '#b0ad9f' const emoji = TOOL_EMOJI[tool.name] ?? '🔧' - const link = TOOL_LINKS[tool.name] ?? '#' + const link = tool.url ?? '#' const timeStr = (() => { try { return new Date(tool.checked_at).toLocaleTimeString('zh-TW', { timeZone: 'Asia/Taipei', hour: '2-digit', minute: '2-digit' }) } catch { return '--' } @@ -413,7 +404,30 @@ export default function Home({ params }: { params: { locale: string } }) { return { mttrAvg: avgStr, mttrTrend: trend } })() - // (pulseMetrics reserved for future sparklines) + // 今日事件 sparkline: 過去 6 小時每小時事件數 (真實數據) + const todaySparkValues = (() => { + if (!incidents?.length) return null + const now = Date.now() + const buckets = Array.from({ length: 6 }, (_, i) => { + const start = now - (6 - i) * 3600000 + const end = start + 3600000 + return incidents.filter(inc => { + const t = new Date(inc.created_at).getTime() + return t >= start && t < end + }).length + }) + return buckets.some(v => v > 0) ? buckets : null + })() + + // MTTR sparkline: 每筆已解決 incident 的修復時間 (分鐘) 序列 (真實數據) + const mttrSparkValues = (() => { + if (!incidents?.length) return null + const resolved = incidents + .filter(i => i.updated_at && (i.status === 'resolved' || i.status === 'closed')) + .map(i => Math.round((new Date(i.updated_at).getTime() - new Date(i.created_at).getTime()) / 60000)) + .filter(m => m > 0) + return resolved.length >= 2 ? resolved.slice(-6) : null + })() // POD health: healthy services / total const podHealthStr = totalServices > 0 ? `${healthyServices}/${totalServices}` : '--' @@ -475,12 +489,9 @@ export default function Home({ params }: { params: { locale: string } }) { label: tDashboard('todayIncidents'), value: todayIncidentCount, trend: todayIncidentCount > 0 ? { text: `↑${todayIncidentCount > 0 ? Math.max(1, Math.round(todayIncidentCount * 0.2)) : 0}`, color: '#d97757' } : undefined, - extra: ( - - - - - ), + extra: todaySparkValues ? ( + + ) : undefined, }, { label: tDashboard('autoRemediationRate'), @@ -499,12 +510,9 @@ export default function Home({ params }: { params: { locale: string } }) { label: tDashboard('mttrAvg'), value: mttrAvg, trend: mttrTrend, - extra: ( - - - - - ), + extra: mttrSparkValues ? ( + + ) : undefined, }, { label: tDashboard('podHealth'),