Files
HarborForge.Frontend/src/pages/MonitorPage.tsx

286 lines
10 KiB
TypeScript

import { useEffect, useMemo, useState } from 'react'
import api from '@/services/api'
interface ProviderRow {
account_id: number
provider: string
label: string
usage_pct: number | null
status: string
error?: string | null
fetched_at?: string | null
reset_at?: string | null
window?: string | null
used?: number | null
limit?: number | null
}
interface ServerRow {
server_id: number
identifier: string
display_name: string
online: boolean
openclaw_version?: string | null
cpu_pct?: number | null
mem_pct?: number | null
disk_pct?: number | null
swap_pct?: number | null
agents: Array<{ id?: string; name?: string; status?: string }>
last_seen_at?: string | null
}
interface OverviewData {
issues: {
total_issues: number
new_issues_24h: number
processed_issues_24h: number
computed_at: string
}
providers: ProviderRow[]
servers: ServerRow[]
generated_at: string
}
interface AdminUser {
id: number
is_admin: boolean
}
interface ProviderAccountItem {
id: number
provider: string
label: string
is_enabled: boolean
credential_masked: string
}
interface ServerItem {
server_id: number
identifier: string
display_name: string
online: boolean
}
export default function MonitorPage() {
const [data, setData] = useState<OverviewData | null>(null)
const [loading, setLoading] = useState(true)
const [isAdmin, setIsAdmin] = useState(false)
const [providerAccounts, setProviderAccounts] = useState<ProviderAccountItem[]>([])
const [servers, setServers] = useState<ServerItem[]>([])
const [providerForm, setProviderForm] = useState({ provider: 'openai', label: '', credential: '' })
const [providerTestMsg, setProviderTestMsg] = useState('')
const [serverForm, setServerForm] = useState({ identifier: '', display_name: '' })
const canAdmin = useMemo(() => !!localStorage.getItem('token') && isAdmin, [isAdmin])
const load = async () => {
try {
const res = await api.get<OverviewData>('/monitor/public/overview')
setData(res.data)
const token = localStorage.getItem('token')
if (token) {
try {
const me = await api.get<AdminUser>('/auth/me')
setIsAdmin(!!me.data.is_admin)
} catch {
setIsAdmin(false)
}
} else {
setIsAdmin(false)
}
} finally {
setLoading(false)
}
}
const loadAdminData = async () => {
if (!canAdmin) return
const [p, s] = await Promise.all([
api.get<ProviderAccountItem[]>('/monitor/admin/providers/accounts'),
api.get<ServerItem[]>('/monitor/admin/servers'),
])
setProviderAccounts(p.data)
setServers(s.data)
}
useEffect(() => {
load()
const t = setInterval(load, 30000)
return () => clearInterval(t)
}, [])
useEffect(() => {
loadAdminData()
}, [canAdmin])
const testProvider = async () => {
const r = await api.post<{ ok: boolean; message: string }>('/monitor/admin/providers/test', {
provider: providerForm.provider,
credential: providerForm.credential,
})
setProviderTestMsg((r.data.ok ? '✅ ' : '❌ ') + r.data.message)
}
const addProvider = async () => {
await api.post('/monitor/admin/providers/accounts', providerForm)
setProviderForm({ ...providerForm, label: '', credential: '' })
await loadAdminData()
}
const deleteProvider = async (id: number) => {
await api.delete('/monitor/admin/providers/accounts/' + id)
await loadAdminData()
}
const addServer = async () => {
await api.post('/monitor/admin/servers', serverForm)
setServerForm({ identifier: '', display_name: '' })
await loadAdminData()
}
const deleteServer = async (id: number) => {
await api.delete('/monitor/admin/servers/' + id)
await loadAdminData()
}
const createChallenge = async (id: number) => {
const r = await api.post<{ identifier: string; challenge_uuid: string; expires_at: string }>('/monitor/admin/servers/' + id + '/challenge')
alert('identifier=' + r.data.identifier + ' | challenge_uuid=' + r.data.challenge_uuid + ' | expires_at=' + r.data.expires_at)
}
if (loading) return <div className="loading">Monitor loading...</div>
if (!data) return <div className="loading">Monitor load failed</div>
return (
<div className="dashboard monitor-page">
<h2>📡 Monitor</h2>
<div className="stats-grid">
<div className="stat-card total">
<span className="stat-number">{data.issues.total_issues}</span>
<span className="stat-label"> Issues</span>
</div>
<div className="stat-card" style={{ borderLeftColor: 'var(--accent)' }}>
<span className="stat-number">{data.issues.new_issues_24h}</span>
<span className="stat-label">24</span>
</div>
<div className="stat-card" style={{ borderLeftColor: 'var(--success)' }}>
<span className="stat-number">{data.issues.processed_issues_24h}</span>
<span className="stat-label">24</span>
</div>
</div>
<div className="section">
<div className="page-header">
<h3>Provider Usage</h3>
<span className="text-dim"> {data.generated_at}</span>
</div>
{data.providers.length === 0 ? <p className="empty"> provider </p> : (
<table>
<thead>
<tr>
<th>Provider</th>
<th>Label</th>
<th>Usage</th>
<th>Window</th>
<th>Reset</th>
<th>Status</th>
<th>Updated</th>
</tr>
</thead>
<tbody>
{data.providers.map((p) => (
<tr key={p.account_id}>
<td>{p.provider}</td>
<td>{p.label}</td>
<td>{p.usage_pct !== null ? p.usage_pct + '%' : '-'}</td>
<td>{p.window || '-'}</td>
<td>{p.reset_at || '-'}</td>
<td><span className={
'badge ' + (p.status === 'ok' ? 'status-ok' : p.status === 'error' ? 'status-error' : 'status-pending')
}>{p.status}</span></td>
<td>{p.fetched_at || '-'}</td>
</tr>
))}
</tbody>
</table>
)}
</div>
<div className="section">
<h3></h3>
{data.servers.length === 0 ? <p className="empty"></p> : (
<div className="monitor-grid">
{data.servers.map((s) => (
<div key={s.server_id} className="monitor-card">
<div className="monitor-card-header">
<div>
<strong>{s.display_name}</strong>
<div className="text-dim">{s.identifier}</div>
</div>
<span className={'badge ' + (s.online ? 'status-online' : 'status-offline')}>{s.online ? 'online' : 'offline'}</span>
</div>
<div className="monitor-metrics">
CPU {s.cpu_pct ?? '-'}% · MEM {s.mem_pct ?? '-'}% · DISK {s.disk_pct ?? '-'}% · SWAP {s.swap_pct ?? '-'}%
</div>
<div className="text-dim">OpenClaw: {s.openclaw_version || '-'}</div>
<div className="text-dim">Agents: {s.agents?.length || 0}</div>
</div>
))}
</div>
)}
</div>
{canAdmin && (
<div className="section">
<h3>Admin </h3>
<div className="monitor-admin">
<div className="monitor-card">
<h4>Provider </h4>
<div className="inline-form">
<select value={providerForm.provider} onChange={(e) => setProviderForm({ ...providerForm, provider: e.target.value })}>
<option value='openai'>openai</option>
<option value='anthropic'>anthropic</option>
<option value='minimax'>minimax</option>
<option value='kimi'>kimi</option>
<option value='qwen'>qwen</option>
</select>
<input placeholder='label' value={providerForm.label} onChange={(e) => setProviderForm({ ...providerForm, label: e.target.value })} />
<input placeholder='credential' value={providerForm.credential} onChange={(e) => setProviderForm({ ...providerForm, credential: e.target.value })} />
<button className="btn-primary" onClick={testProvider}></button>
<button className="btn-primary" onClick={addProvider}></button>
</div>
{providerTestMsg && <p className="text-dim">{providerTestMsg}</p>}
<ul>
{providerAccounts.map((p) => (
<li key={p.id}>{p.provider} / {p.label} / {p.credential_masked} <button className="btn-danger" onClick={() => deleteProvider(p.id)}></button></li>
))}
</ul>
</div>
<div className="monitor-card">
<h4></h4>
<div className="inline-form">
<input placeholder='identifier' value={serverForm.identifier} onChange={(e) => setServerForm({ ...serverForm, identifier: e.target.value })} />
<input placeholder='display_name' value={serverForm.display_name} onChange={(e) => setServerForm({ ...serverForm, display_name: e.target.value })} />
<button className="btn-primary" onClick={addServer}></button>
</div>
<ul>
{servers.map((s) => (
<li key={s.server_id}>
{s.display_name} ({s.identifier})
<button className="btn-secondary" onClick={() => createChallenge(s.server_id)} style={{ marginLeft: 8 }}></button>
<button className="btn-danger" onClick={() => deleteServer(s.server_id)} style={{ marginLeft: 8 }}></button>
</li>
))}
</ul>
</div>
</div>
</div>
)}
</div>
)
}