Replace Kuma iframe with live service health status tab
Uptime tab now polls /deploy/health-status (parallel HTTP checks) and shows a status grid with response times. Removes broken Kuma sub-path nginx locations. Auto-refreshes every 30s. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
05fa11b29a
commit
64efa435c8
2 changed files with 65 additions and 32 deletions
27
nginx.conf
27
nginx.conf
|
|
@ -18,33 +18,6 @@ server {
|
||||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
}
|
}
|
||||||
|
|
||||||
# Kuma assets and websocket — must come before /monitor/ to match first
|
|
||||||
location /monitor/assets/ {
|
|
||||||
proxy_pass http://10.10.10.105:3002/assets/;
|
|
||||||
proxy_set_header Host $host;
|
|
||||||
}
|
|
||||||
location /monitor/socket.io/ {
|
|
||||||
proxy_pass http://10.10.10.105:3002/socket.io/;
|
|
||||||
proxy_set_header Host $host;
|
|
||||||
proxy_set_header Upgrade $http_upgrade;
|
|
||||||
proxy_set_header Connection "upgrade";
|
|
||||||
proxy_http_version 1.1;
|
|
||||||
}
|
|
||||||
location /monitor/ {
|
|
||||||
proxy_pass http://10.10.10.105:3002/;
|
|
||||||
proxy_set_header Host $host;
|
|
||||||
proxy_set_header X-Real-IP $remote_addr;
|
|
||||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
|
||||||
proxy_set_header Upgrade $http_upgrade;
|
|
||||||
proxy_set_header Connection "upgrade";
|
|
||||||
proxy_http_version 1.1;
|
|
||||||
# Rewrite Kuma's absolute /assets/ and /socket.io/ refs to /monitor/*
|
|
||||||
proxy_set_header Accept-Encoding "";
|
|
||||||
sub_filter '"/assets/' '"/monitor/assets/';
|
|
||||||
sub_filter '"/socket.io/' '"/monitor/socket.io/';
|
|
||||||
sub_filter_once off;
|
|
||||||
}
|
|
||||||
|
|
||||||
location /deploy/ {
|
location /deploy/ {
|
||||||
proxy_pass http://10.10.10.105:9000/;
|
proxy_pass http://10.10.10.105:9000/;
|
||||||
proxy_set_header Host $host;
|
proxy_set_header Host $host;
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,12 @@ interface AppStatus {
|
||||||
checkedAt: string
|
checkedAt: string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface HealthStatus {
|
||||||
|
name: string
|
||||||
|
up: boolean
|
||||||
|
ms: number | null
|
||||||
|
}
|
||||||
|
|
||||||
interface DeployEntry {
|
interface DeployEntry {
|
||||||
repo: string
|
repo: string
|
||||||
host: string
|
host: string
|
||||||
|
|
@ -23,9 +29,10 @@ interface DeployEntry {
|
||||||
}
|
}
|
||||||
|
|
||||||
export function AdminMonitor({ user }: { user: User }) {
|
export function AdminMonitor({ user }: { user: User }) {
|
||||||
const [tab, setTab] = useState<'updates' | 'kuma' | 'deploys'>('updates')
|
const [tab, setTab] = useState<'updates' | 'uptime' | 'deploys'>('updates')
|
||||||
const [statuses, setStatuses] = useState<AppStatus[]>([])
|
const [statuses, setStatuses] = useState<AppStatus[]>([])
|
||||||
const [deploys, setDeploys] = useState<DeployEntry[]>([])
|
const [deploys, setDeploys] = useState<DeployEntry[]>([])
|
||||||
|
const [health, setHealth] = useState<HealthStatus[]>([])
|
||||||
const [loading, setLoading] = useState(false)
|
const [loading, setLoading] = useState(false)
|
||||||
const [deploying, setDeploying] = useState<Set<string>>(new Set())
|
const [deploying, setDeploying] = useState<Set<string>>(new Set())
|
||||||
|
|
||||||
|
|
@ -39,6 +46,16 @@ export function AdminMonitor({ user }: { user: User }) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function fetchHealth() {
|
||||||
|
setLoading(true)
|
||||||
|
try {
|
||||||
|
const res = await fetch('/deploy/health-status', { credentials: 'include' })
|
||||||
|
if (res.ok) setHealth(await res.json())
|
||||||
|
} finally {
|
||||||
|
setLoading(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async function fetchDeploys() {
|
async function fetchDeploys() {
|
||||||
setLoading(true)
|
setLoading(true)
|
||||||
try {
|
try {
|
||||||
|
|
@ -65,6 +82,11 @@ export function AdminMonitor({ user }: { user: User }) {
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (tab === 'updates') fetchStatus()
|
if (tab === 'updates') fetchStatus()
|
||||||
if (tab === 'deploys') fetchDeploys()
|
if (tab === 'deploys') fetchDeploys()
|
||||||
|
if (tab === 'uptime') {
|
||||||
|
fetchHealth()
|
||||||
|
const id = setInterval(fetchHealth, 30_000)
|
||||||
|
return () => clearInterval(id)
|
||||||
|
}
|
||||||
}, [tab])
|
}, [tab])
|
||||||
|
|
||||||
const updatesAvailable = statuses.filter(s => s.updateAvailable).length
|
const updatesAvailable = statuses.filter(s => s.updateAvailable).length
|
||||||
|
|
@ -77,7 +99,7 @@ export function AdminMonitor({ user }: { user: User }) {
|
||||||
display: 'flex', borderBottom: '1px solid var(--surface-2)',
|
display: 'flex', borderBottom: '1px solid var(--surface-2)',
|
||||||
background: 'var(--navy)', flexShrink: 0,
|
background: 'var(--navy)', flexShrink: 0,
|
||||||
}}>
|
}}>
|
||||||
{(['updates', 'kuma', 'deploys'] as const).map(t => (
|
{(['updates', 'uptime', 'deploys'] as const).map(t => (
|
||||||
<button key={t} onClick={() => setTab(t)} style={{
|
<button key={t} onClick={() => setTab(t)} style={{
|
||||||
background: tab === t ? 'var(--surface)' : 'transparent',
|
background: tab === t ? 'var(--surface)' : 'transparent',
|
||||||
border: 'none', borderBottom: tab === t ? '2px solid var(--gold)' : '2px solid transparent',
|
border: 'none', borderBottom: tab === t ? '2px solid var(--gold)' : '2px solid transparent',
|
||||||
|
|
@ -95,7 +117,7 @@ export function AdminMonitor({ user }: { user: User }) {
|
||||||
display: 'flex', alignItems: 'center', justifyContent: 'center',
|
display: 'flex', alignItems: 'center', justifyContent: 'center',
|
||||||
}}>{updatesAvailable}</span>
|
}}>{updatesAvailable}</span>
|
||||||
)}
|
)}
|
||||||
{t === 'kuma' && '📡 Uptime'}
|
{t === 'uptime' && '📡 Uptime'}
|
||||||
{t === 'deploys' && '📋 Deploy Log'}
|
{t === 'deploys' && '📋 Deploy Log'}
|
||||||
</button>
|
</button>
|
||||||
))}
|
))}
|
||||||
|
|
@ -168,8 +190,46 @@ export function AdminMonitor({ user }: { user: User }) {
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{tab === 'kuma' && (
|
{tab === 'uptime' && (
|
||||||
<iframe src="/monitor/" title="Uptime Kuma" style={{ flex: 1, border: 'none', width: '100%' }} />
|
<div style={{ flex: 1, overflowY: 'auto', padding: '1.5rem 2rem' }}>
|
||||||
|
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '1rem' }}>
|
||||||
|
<h2 style={{ fontSize: '1rem', fontWeight: 600 }}>Service Health</h2>
|
||||||
|
<button onClick={fetchHealth} disabled={loading} style={{
|
||||||
|
background: 'var(--surface-2)', border: 'none', borderRadius: '6px',
|
||||||
|
color: 'var(--text-muted)', padding: '0.4rem 1rem', fontSize: '0.8rem', cursor: 'pointer',
|
||||||
|
}}>
|
||||||
|
{loading ? 'Checking…' : 'Refresh'}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
{health.length === 0 && !loading && (
|
||||||
|
<p style={{ color: 'var(--text-muted)' }}>Checking services…</p>
|
||||||
|
)}
|
||||||
|
<div style={{ display: 'flex', flexDirection: 'column', gap: '0.5rem' }}>
|
||||||
|
{health.map(h => (
|
||||||
|
<div key={h.name} style={{
|
||||||
|
background: 'var(--surface)', border: '1px solid var(--surface-2)',
|
||||||
|
borderLeft: `3px solid ${h.up ? '#2d6a4f' : 'var(--danger, #ff4d4d)'}`,
|
||||||
|
borderRadius: '8px', padding: '0.75rem 1rem',
|
||||||
|
display: 'flex', alignItems: 'center', justifyContent: 'space-between',
|
||||||
|
}}>
|
||||||
|
<div style={{ display: 'flex', alignItems: 'center', gap: '0.6rem' }}>
|
||||||
|
<span style={{
|
||||||
|
width: '8px', height: '8px', borderRadius: '50%', flexShrink: 0,
|
||||||
|
background: h.up ? '#52b788' : 'var(--danger, #ff4d4d)',
|
||||||
|
boxShadow: h.up ? '0 0 6px #52b78866' : undefined,
|
||||||
|
}} />
|
||||||
|
<span style={{ fontWeight: 600, fontSize: '0.9rem' }}>{h.name}</span>
|
||||||
|
</div>
|
||||||
|
<span style={{
|
||||||
|
fontSize: '0.75rem', fontFamily: 'monospace',
|
||||||
|
color: h.up ? '#52b788' : 'var(--text-muted)',
|
||||||
|
}}>
|
||||||
|
{h.up ? `${h.ms}ms` : 'unreachable'}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{tab === 'deploys' && (
|
{tab === 'deploys' && (
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue