style: align monitor page with dashboard look and add monitor styles
This commit is contained in:
@@ -183,3 +183,21 @@ dd { font-size: .9rem; }
|
||||
.setup-done { text-align: center; }
|
||||
.setup-done h2 { color: var(--success); margin-bottom: 12px; }
|
||||
@keyframes fadeIn { from { opacity: 0; transform: translateY(8px); } to { opacity: 1; transform: none; } }
|
||||
|
||||
|
||||
/* Monitor */
|
||||
.monitor-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(260px, 1fr)); gap: 16px; margin-top: 12px; }
|
||||
.monitor-card { background: var(--bg-card); border: 1px solid var(--border); border-radius: 8px; padding: 16px; }
|
||||
.monitor-card-header { display: flex; justify-content: space-between; align-items: flex-start; margin-bottom: 8px; }
|
||||
.monitor-metrics { margin: 8px 0; font-size: .9rem; }
|
||||
.monitor-admin { display: grid; grid-template-columns: repeat(auto-fit, minmax(320px, 1fr)); gap: 16px; }
|
||||
|
||||
.status-ok { background: var(--success); }
|
||||
.status-error { background: var(--danger); }
|
||||
.status-pending { background: var(--warning); }
|
||||
.status-online { background: var(--success); }
|
||||
.status-offline { background: var(--danger); }
|
||||
|
||||
.btn-secondary { background: none; border: 1px solid var(--border); color: var(--text); padding: 6px 12px; border-radius: 6px; cursor: pointer; }
|
||||
.btn-danger { background: var(--danger); color: #fff; border: none; padding: 6px 12px; border-radius: 6px; cursor: pointer; }
|
||||
.btn-danger:hover { opacity: .9; }
|
||||
|
||||
@@ -11,6 +11,8 @@ interface ProviderRow {
|
||||
fetched_at?: string | null
|
||||
reset_at?: string | null
|
||||
window?: string | null
|
||||
used?: number | null
|
||||
limit?: number | null
|
||||
}
|
||||
|
||||
interface ServerRow {
|
||||
@@ -144,102 +146,139 @@ export default function MonitorPage() {
|
||||
|
||||
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 + '\nchallenge_uuid=' + r.data.challenge_uuid + '\nexpires_at=' + r.data.expires_at)
|
||||
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>
|
||||
if (loading) return <div className="loading">Monitor loading...</div>
|
||||
if (!data) return <div className="loading">Monitor load failed</div>
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="dashboard monitor-page">
|
||||
<h2>📡 Monitor</h2>
|
||||
|
||||
<section>
|
||||
<h3>Issue 概览(24小时窗口)</h3>
|
||||
<ul>
|
||||
<li>所有项目总 Issue:{data.issues.total_issues}</li>
|
||||
<li>24小时新增:{data.issues.new_issues_24h}</li>
|
||||
<li>24小时已处理(resolved/closed):{data.issues.processed_issues_24h}</li>
|
||||
</ul>
|
||||
</section>
|
||||
<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>
|
||||
|
||||
<section>
|
||||
<h3>Provider Usage</h3>
|
||||
{data.providers.length === 0 ? <p>暂无 provider 账号</p> : (
|
||||
<ul>
|
||||
{data.providers.map((p) => (
|
||||
<li key={p.account_id}>
|
||||
<strong>{p.provider}</strong> / {p.label} · status: {p.status}
|
||||
{p.usage_pct !== null ? (' · usage: ' + p.usage_pct + '%') : ''}
|
||||
{p.reset_at ? (' · reset: ' + p.reset_at) : ''}
|
||||
{p.window ? (' · window: ' + p.window) : ''}
|
||||
{p.fetched_at ? (' · updated: ' + p.fetched_at) : ''}
|
||||
{p.error ? (' · error: ' + p.error) : ''}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
<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>
|
||||
)}
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<section>
|
||||
<div className="section">
|
||||
<h3>服务器监测</h3>
|
||||
{data.servers.length === 0 ? <p>暂无监测服务器</p> : (
|
||||
<div>
|
||||
{data.servers.length === 0 ? <p className="empty">暂无监测服务器</p> : (
|
||||
<div className="monitor-grid">
|
||||
{data.servers.map((s) => (
|
||||
<div key={s.server_id} style={{ marginBottom: 12, padding: 12, border: '1px solid #ddd', borderRadius: 8 }}>
|
||||
<div>
|
||||
<strong>{s.display_name}</strong> ({s.identifier}) · {s.online ? '🟢 在线' : '🔴 离线'}
|
||||
<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>
|
||||
CPU: {s.cpu_pct ?? '-'}% · MEM: {s.mem_pct ?? '-'}% · DISK: {s.disk_pct ?? '-'}% · SWAP: {s.swap_pct ?? '-'}%
|
||||
<div className="monitor-metrics">
|
||||
CPU {s.cpu_pct ?? '-'}% · MEM {s.mem_pct ?? '-'}% · DISK {s.disk_pct ?? '-'}% · SWAP {s.swap_pct ?? '-'}%
|
||||
</div>
|
||||
<div>OpenClaw: {s.openclaw_version || '-'}</div>
|
||||
<div>Agents: {s.agents?.length || 0}</div>
|
||||
<div className="text-dim">OpenClaw: {s.openclaw_version || '-'}</div>
|
||||
<div className="text-dim">Agents: {s.agents?.length || 0}</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</section>
|
||||
</div>
|
||||
|
||||
{canAdmin && (
|
||||
<section style={{ marginTop: 24 }}>
|
||||
<h3>Admin: Provider 管理</h3>
|
||||
<div style={{ display: 'flex', gap: 8, flexWrap: 'wrap' }}>
|
||||
<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 onClick={testProvider}>测试连接</button>
|
||||
<button onClick={addProvider}>添加账号</button>
|
||||
</div>
|
||||
{providerTestMsg && <p>{providerTestMsg}</p>}
|
||||
<ul>
|
||||
{providerAccounts.map((p) => (
|
||||
<li key={p.id}>{p.provider} / {p.label} / {p.credential_masked} <button onClick={() => deleteProvider(p.id)}>删除</button></li>
|
||||
))}
|
||||
</ul>
|
||||
<div className="section">
|
||||
<h3>Admin 管理</h3>
|
||||
|
||||
<h3>Admin: 服务器管理</h3>
|
||||
<div style={{ display: 'flex', gap: 8, flexWrap: 'wrap' }}>
|
||||
<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 onClick={addServer}>添加服务器</button>
|
||||
<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>
|
||||
<ul>
|
||||
{servers.map((s) => (
|
||||
<li key={s.server_id}>
|
||||
{s.display_name} ({s.identifier})
|
||||
<button onClick={() => createChallenge(s.server_id)} style={{ marginLeft: 8 }}>生成挑战</button>
|
||||
<button onClick={() => deleteServer(s.server_id)} style={{ marginLeft: 8 }}>删除</button>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</section>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user