Monitor page: Uptime Kuma iframe + deploy log; expose /deploy/ via nginx
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
1dd265b46d
commit
ea9dba0c58
3 changed files with 128 additions and 1 deletions
|
|
@ -4,6 +4,7 @@ import { Login } from './pages/Login'
|
|||
import { Dashboard } from './pages/Dashboard'
|
||||
import { AppFrame } from './pages/AppFrame'
|
||||
import { AdminUsers } from './pages/AdminUsers'
|
||||
import { AdminMonitor } from './pages/AdminMonitor'
|
||||
|
||||
// The portal is only ever the top-level frame. If it finds itself loaded inside
|
||||
// an iframe, it means an app's path fell back to the portal (that app isn't
|
||||
|
|
@ -42,7 +43,7 @@ export default function App() {
|
|||
<Route path="/" element={<Dashboard user={user} />} />
|
||||
<Route path="/app/:slug" element={<AppFrame user={user} />} />
|
||||
<Route path="/admin/users" element={user.is_admin ? <AdminUsers user={user} /> : <Navigate to="/" />} />
|
||||
<Route path="/admin/monitor" element={<Navigate to="/notices/" />} />
|
||||
<Route path="/admin/monitor" element={user.is_admin ? <AdminMonitor user={user} /> : <Navigate to="/" />} />
|
||||
<Route path="*" element={<Navigate to="/" />} />
|
||||
</Routes>
|
||||
)}
|
||||
|
|
|
|||
119
src/pages/AdminMonitor.tsx
Normal file
119
src/pages/AdminMonitor.tsx
Normal file
|
|
@ -0,0 +1,119 @@
|
|||
import { useEffect, useState } from 'react'
|
||||
import { Sidebar } from '../components/Sidebar'
|
||||
import type { User } from '../types'
|
||||
|
||||
interface DeployEntry {
|
||||
repo: string
|
||||
host: string
|
||||
started: string
|
||||
finished?: string
|
||||
status: 'running' | 'success' | 'failed'
|
||||
output?: string
|
||||
error?: string
|
||||
}
|
||||
|
||||
export function AdminMonitor({ user }: { user: User }) {
|
||||
const [tab, setTab] = useState<'kuma' | 'deploys'>('kuma')
|
||||
const [deploys, setDeploys] = useState<DeployEntry[]>([])
|
||||
const [loading, setLoading] = useState(false)
|
||||
|
||||
async function fetchDeploys() {
|
||||
setLoading(true)
|
||||
try {
|
||||
const res = await fetch('/deploy/deploys', { credentials: 'include' })
|
||||
if (res.ok) setDeploys(await res.json())
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (tab === 'deploys') fetchDeploys()
|
||||
}, [tab])
|
||||
|
||||
return (
|
||||
<div style={{ display: 'flex', height: '100dvh' }}>
|
||||
<Sidebar user={user} />
|
||||
<div style={{ flex: 1, display: 'flex', flexDirection: 'column', overflow: 'hidden' }}>
|
||||
<div style={{
|
||||
display: 'flex', gap: '0', borderBottom: '1px solid var(--surface-2)',
|
||||
background: 'var(--navy)', flexShrink: 0,
|
||||
}}>
|
||||
{(['kuma', 'deploys'] as const).map(t => (
|
||||
<button key={t} onClick={() => setTab(t)} style={{
|
||||
background: tab === t ? 'var(--surface)' : 'transparent',
|
||||
border: 'none', borderBottom: tab === t ? '2px solid var(--gold)' : '2px solid transparent',
|
||||
color: tab === t ? 'var(--gold)' : 'var(--text-muted)',
|
||||
padding: '0.75rem 1.5rem', fontSize: '0.85rem', fontWeight: 600, cursor: 'pointer',
|
||||
}}>
|
||||
{t === 'kuma' ? '📡 Uptime Monitor' : '🚀 Deploy Log'}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{tab === 'kuma' && (
|
||||
<iframe
|
||||
src="/monitor/"
|
||||
title="Uptime Kuma"
|
||||
style={{ flex: 1, border: 'none', width: '100%' }}
|
||||
/>
|
||||
)}
|
||||
|
||||
{tab === 'deploys' && (
|
||||
<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 }}>Deploy History</h2>
|
||||
<button onClick={fetchDeploys} 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 ? 'Loading…' : 'Refresh'}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{deploys.length === 0 && !loading && (
|
||||
<p style={{ color: 'var(--text-muted)', fontSize: '0.9rem' }}>
|
||||
No deploys recorded yet. Configure Forgejo webhooks to point to{' '}
|
||||
<code style={{ background: 'var(--surface-2)', padding: '0.1rem 0.3rem', borderRadius: '3px' }}>
|
||||
https://manage.hotelnumberfour.com/deploy/webhook
|
||||
</code>
|
||||
</p>
|
||||
)}
|
||||
|
||||
{deploys.map((d, i) => (
|
||||
<div key={i} style={{
|
||||
background: 'var(--surface)', border: '1px solid var(--surface-2)',
|
||||
borderLeft: `3px solid ${d.status === 'success' ? '#2d6a4f' : d.status === 'failed' ? 'var(--danger)' : 'var(--gold)'}`,
|
||||
borderRadius: '8px', padding: '1rem', marginBottom: '0.75rem',
|
||||
}}>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', marginBottom: '0.4rem' }}>
|
||||
<span style={{ fontWeight: 600, fontSize: '0.9rem' }}>{d.repo}</span>
|
||||
<span style={{
|
||||
fontSize: '0.75rem', fontWeight: 600, padding: '0.15rem 0.5rem', borderRadius: '4px',
|
||||
background: d.status === 'success' ? '#2d6a4f33' : d.status === 'failed' ? '#ff4d4d22' : '#f5a62322',
|
||||
color: d.status === 'success' ? '#52b788' : d.status === 'failed' ? 'var(--danger)' : 'var(--gold)',
|
||||
}}>
|
||||
{d.status}
|
||||
</span>
|
||||
</div>
|
||||
<div style={{ fontSize: '0.75rem', color: 'var(--text-muted)', marginBottom: '0.4rem' }}>
|
||||
{d.host} · started {new Date(d.started).toLocaleString()}
|
||||
{d.finished && ` · finished ${new Date(d.finished).toLocaleString()}`}
|
||||
</div>
|
||||
{(d.output || d.error) && (
|
||||
<pre style={{
|
||||
background: 'var(--navy-dark)', borderRadius: '4px', padding: '0.5rem',
|
||||
fontSize: '0.7rem', color: 'var(--text-muted)', overflowX: 'auto',
|
||||
margin: '0.5rem 0 0', maxHeight: '8rem', overflowY: 'auto',
|
||||
}}>
|
||||
{d.output || d.error}
|
||||
</pre>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue