fix(monitoring+layout): 修復基礎架構消失 + 監控工具全線上
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:
OG T
2026-04-03 00:50:53 +08:00
parent b6105b8214
commit 702350925a
2 changed files with 67 additions and 38 deletions

View File

@@ -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",

View File

@@ -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',