fix(monitoring+layout): 修復基礎架構消失 + 監控工具全線上
All checks were successful
CD Pipeline / build-and-deploy (push) Successful in 6m47s
All checks were successful
CD Pipeline / build-and-deploy (push) Successful in 6m47s
- page.tsx: 右側 panel overflow:hidden → overflowY:auto,基礎架構重新顯示 - page.tsx: 監控工具卡片對齊 figma (icon box + 版本/統計行 + ›箭頭) - monitoring.py: Gitea probe 改用 /api/v1/version (/-/readiness 404) - monitoring.py: Grafana dashboard count 加 Basic auth - NetworkPolicy: 補開 3002/9090/3001 egress (Grafana/Prometheus/Gitea) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -38,9 +38,12 @@ async def _probe_grafana(client: httpx.AsyncClient) -> dict:
|
||||
if r.status_code == 200:
|
||||
data = r.json()
|
||||
version = data.get("version")
|
||||
# Dashboard count requires basic auth (internal probe only)
|
||||
import base64 as _b64
|
||||
_token = _b64.b64encode(b"admin:WoooTech2026").decode()
|
||||
dash_r = await client.get(
|
||||
f"{base}/api/search?type=dash-db",
|
||||
headers={"X-Grafana-Org-Id": "1"},
|
||||
headers={"Authorization": f"Basic {_token}"},
|
||||
timeout=TIMEOUT,
|
||||
)
|
||||
dash_count = len(dash_r.json()) if dash_r.status_code == 200 and isinstance(dash_r.json(), list) else None
|
||||
@@ -179,12 +182,10 @@ async def _probe_signoz(client: httpx.AsyncClient) -> dict:
|
||||
async def _probe_gitea(client: httpx.AsyncClient) -> dict:
|
||||
base = "http://192.168.0.110:3001"
|
||||
try:
|
||||
r = await client.get(f"{base}/-/readiness", timeout=TIMEOUT)
|
||||
if r.status_code == 200:
|
||||
ver_r = await client.get(f"{base}/api/v1/version", timeout=TIMEOUT)
|
||||
version = None
|
||||
if ver_r.status_code == 200:
|
||||
version = ver_r.json().get("version")
|
||||
# Use /api/v1/version — /-/readiness returns 404 on this Gitea version
|
||||
ver_r = await client.get(f"{base}/api/v1/version", timeout=TIMEOUT)
|
||||
if ver_r.status_code == 200:
|
||||
version = ver_r.json().get("version")
|
||||
return {
|
||||
"name": "Gitea",
|
||||
"status": "up",
|
||||
|
||||
@@ -73,6 +73,16 @@ interface MonitoringTool {
|
||||
checked_at: string
|
||||
}
|
||||
|
||||
// icon SVG paths (inline, no emoji — consistent cross-platform)
|
||||
const TOOL_ICON_COLOR: Record<string, { bg: string; color: string; label: string }> = {
|
||||
Grafana: { bg: '#fff4e6', color: '#f46800', label: 'G' },
|
||||
Prometheus: { bg: '#fff0eb', color: '#e6522c', label: 'P' },
|
||||
Sentry: { bg: '#f3eeff', color: '#7b52bf', label: 'S' },
|
||||
Langfuse: { bg: '#eaf5ff', color: '#0077cc', label: 'L' },
|
||||
SigNoz: { bg: '#eefaf2', color: '#199058', label: 'N' },
|
||||
Gitea: { bg: '#fff0f3', color: '#cc2d40', label: 'T' },
|
||||
}
|
||||
|
||||
function MonitoringTools() {
|
||||
const [tools, setTools] = useState<MonitoringTool[]>([])
|
||||
const [loading, setLoading] = useState(true)
|
||||
@@ -89,17 +99,11 @@ function MonitoringTools() {
|
||||
return () => clearInterval(t)
|
||||
}, [])
|
||||
|
||||
const TOOL_ICONS: Record<string, string> = {
|
||||
Grafana: '📊',
|
||||
Prometheus: '🔥',
|
||||
SigNoz: '🔭',
|
||||
Gitea: '🐙',
|
||||
}
|
||||
|
||||
if (loading) return (
|
||||
<div style={{ padding: '12px 14px', fontSize: 12, color: '#87867f', fontFamily: 'var(--font-body), monospace' }}>
|
||||
載入中...
|
||||
</div>
|
||||
<div style={{ padding: '12px 14px', fontSize: 12, color: '#87867f' }}>載入中...</div>
|
||||
)
|
||||
if (tools.length === 0) return (
|
||||
<div style={{ padding: '12px 14px', fontSize: 12, color: '#cc2200' }}>無法連線</div>
|
||||
)
|
||||
|
||||
return (
|
||||
@@ -107,40 +111,65 @@ function MonitoringTools() {
|
||||
{tools.map((tool, i) => {
|
||||
const isUp = tool.status === 'up'
|
||||
const hasFiring = (tool.firing_count ?? 0) > 0
|
||||
const statusColor = isUp ? (hasFiring ? '#F59E0B' : '#22C55E') : '#cc2200'
|
||||
const statusBg = isUp ? (hasFiring ? 'rgba(245,158,11,0.08)' : 'rgba(34,197,94,0.08)') : 'rgba(204,34,0,0.08)'
|
||||
const statusText = isUp ? (hasFiring ? `${tool.firing_count} 觸發` : '正常') : '離線'
|
||||
const ic = TOOL_ICON_COLOR[tool.name] ?? { bg: '#f5f4ed', color: '#87867f', label: '?' }
|
||||
const timeStr = (() => {
|
||||
try { return new Date(tool.checked_at).toLocaleTimeString('zh-TW', { timeZone: 'Asia/Taipei', hour: '2-digit', minute: '2-digit' }) }
|
||||
catch { return '--' }
|
||||
})()
|
||||
return (
|
||||
<div key={tool.name} style={{
|
||||
padding: '10px 14px',
|
||||
padding: '9px 14px',
|
||||
borderBottom: i < tools.length - 1 ? '0.5px solid #f0ede4' : 'none',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: 10,
|
||||
display: 'flex', alignItems: 'center', gap: 10,
|
||||
cursor: 'default',
|
||||
}}>
|
||||
<div style={{ fontSize: 18, flexShrink: 0, width: 24, textAlign: 'center' }}>{TOOL_ICONS[tool.name] ?? '⚙️'}</div>
|
||||
{/* Icon box */}
|
||||
<div style={{
|
||||
width: 30, height: 30, borderRadius: 7, flexShrink: 0,
|
||||
background: ic.bg, display: 'flex', alignItems: 'center', justifyContent: 'center',
|
||||
}}>
|
||||
<span style={{ fontSize: 13, fontWeight: 800, color: ic.color, fontFamily: 'monospace' }}>{ic.label}</span>
|
||||
</div>
|
||||
|
||||
{/* Content */}
|
||||
<div style={{ flex: 1, minWidth: 0 }}>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 6, marginBottom: 2 }}>
|
||||
<span style={{ fontSize: 13, fontWeight: 700, color: '#141413', fontFamily: 'var(--font-body), monospace' }}>{tool.name}</span>
|
||||
{/* Row 1: name + badge */}
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 6, marginBottom: 1 }}>
|
||||
<span style={{ fontSize: 13, fontWeight: 700, color: '#141413', fontFamily: 'var(--font-body), monospace' }}>
|
||||
{tool.name}
|
||||
</span>
|
||||
<span style={{
|
||||
display: 'inline-flex', alignItems: 'center', gap: 3,
|
||||
fontSize: 10, fontWeight: 600,
|
||||
color: isUp ? (hasFiring ? '#F59E0B' : '#22C55E') : '#cc2200',
|
||||
background: isUp ? (hasFiring ? 'rgba(245,158,11,0.08)' : 'rgba(34,197,94,0.08)') : 'rgba(204,34,0,0.08)',
|
||||
border: `0.5px solid ${isUp ? (hasFiring ? 'rgba(245,158,11,0.25)' : 'rgba(34,197,94,0.25)') : 'rgba(204,34,0,0.25)'}`,
|
||||
fontSize: 10, fontWeight: 600, color: statusColor,
|
||||
background: statusBg,
|
||||
border: `0.5px solid ${statusColor}40`,
|
||||
borderRadius: 4, padding: '1px 5px',
|
||||
}}>
|
||||
<span style={{ width: 4, height: 4, borderRadius: '50%', background: 'currentColor', display: 'inline-block' }} />
|
||||
{isUp ? (hasFiring ? `${tool.firing_count} 觸發` : '正常') : '離線'}
|
||||
<span style={{ width: 4, height: 4, borderRadius: '50%', background: statusColor, display: 'inline-block', flexShrink: 0 }} />
|
||||
{statusText}
|
||||
</span>
|
||||
</div>
|
||||
{/* Row 2: description */}
|
||||
<div style={{ fontSize: 11, color: '#87867f', fontFamily: 'var(--font-body), monospace' }}>
|
||||
{tool.description}
|
||||
{tool.version && <span style={{ color: '#c0bdb4' }}> · v{tool.version}</span>}
|
||||
</div>
|
||||
{tool.stats && (
|
||||
<div style={{ fontSize: 11, color: '#a0a09a', fontFamily: 'var(--font-body), monospace', marginTop: 1 }}>{tool.stats}</div>
|
||||
{/* Row 3: version + stats */}
|
||||
{(tool.version || tool.stats) && (
|
||||
<div style={{ fontSize: 10, color: '#b0ad9f', fontFamily: 'var(--font-body), monospace', marginTop: 1 }}>
|
||||
{tool.version && `版本 v${tool.version}`}
|
||||
{tool.version && tool.stats && ' · '}
|
||||
{tool.stats}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div style={{ fontSize: 10, color: '#c0bdb4', fontFamily: 'var(--font-body), monospace', flexShrink: 0, textAlign: 'right' }}>
|
||||
{new Date(tool.checked_at).toLocaleTimeString('zh-TW', { timeZone: 'Asia/Taipei', hour: '2-digit', minute: '2-digit' })}
|
||||
|
||||
{/* Right: time + arrow */}
|
||||
<div style={{ display: 'flex', flexDirection: 'column', alignItems: 'flex-end', gap: 2, flexShrink: 0 }}>
|
||||
<span style={{ fontSize: 10, color: '#c0bdb4' }}>{timeStr}</span>
|
||||
<span style={{ fontSize: 12, color: '#c0bdb4' }}>›</span>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
@@ -528,7 +557,7 @@ export default function Home({ params }: { params: { locale: string } }) {
|
||||
<div style={{
|
||||
width: 530,
|
||||
flexShrink: 0,
|
||||
overflow: 'hidden',
|
||||
overflowY: 'auto',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
gap: 14,
|
||||
@@ -566,8 +595,7 @@ export default function Home({ params }: { params: { locale: string } }) {
|
||||
borderRadius: 12,
|
||||
overflow: 'hidden',
|
||||
boxShadow: '0 1px 4px rgba(0,0,0,0.05)',
|
||||
flex: 1,
|
||||
overflowY: 'auto',
|
||||
flexShrink: 0,
|
||||
}}>
|
||||
<div style={{
|
||||
padding: '10px 14px',
|
||||
|
||||
Reference in New Issue
Block a user