mirror of
https://github.com/qingchencloud/clawpanel.git
synced 2026-06-09 09:39:53 +08:00
feat(dashboard): rich data display to overview system states
- add tunnel, mcp configurations insights - show more statistical cards - add create backup quick action
This commit is contained in:
@@ -18,10 +18,14 @@ export async function render() {
|
||||
<div class="stat-card loading-placeholder"></div>
|
||||
<div class="stat-card loading-placeholder"></div>
|
||||
<div class="stat-card loading-placeholder"></div>
|
||||
<div class="stat-card loading-placeholder"></div>
|
||||
<div class="stat-card loading-placeholder"></div>
|
||||
</div>
|
||||
<div id="dashboard-overview-container"></div>
|
||||
<div class="quick-actions">
|
||||
<button class="btn btn-secondary" id="btn-restart-gw">重启 Gateway</button>
|
||||
<button class="btn btn-secondary" id="btn-check-update">检查更新</button>
|
||||
<button class="btn btn-secondary" id="btn-create-backup">创建备份</button>
|
||||
</div>
|
||||
<div class="config-section">
|
||||
<div class="config-section-title">最近日志</div>
|
||||
@@ -35,30 +39,47 @@ export async function render() {
|
||||
}
|
||||
|
||||
async function loadDashboardData(page) {
|
||||
const [servicesRes, versionRes, logsRes] = await Promise.allSettled([
|
||||
const [servicesRes, versionRes, logsRes, agentsRes, configRes, tunnelRes, mcpRes, clawappRes, backupsRes] = await Promise.allSettled([
|
||||
api.getServicesStatus(),
|
||||
api.getVersionInfo(),
|
||||
api.readLogTail('gateway', 20),
|
||||
api.listAgents(),
|
||||
api.readOpenclawConfig(),
|
||||
api.getCftunnelStatus(),
|
||||
api.readMcpConfig(),
|
||||
api.getClawappStatus(),
|
||||
api.listBackups(),
|
||||
])
|
||||
|
||||
const services = servicesRes.status === 'fulfilled' ? servicesRes.value : []
|
||||
const version = versionRes.status === 'fulfilled' ? versionRes.value : {}
|
||||
const logs = logsRes.status === 'fulfilled' ? logsRes.value : ''
|
||||
const agents = agentsRes.status === 'fulfilled' ? agentsRes.value : []
|
||||
const config = configRes.status === 'fulfilled' ? configRes.value : null
|
||||
const tunnel = tunnelRes.status === 'fulfilled' ? tunnelRes.value : null
|
||||
const mcpConfig = mcpRes.status === 'fulfilled' ? mcpRes.value : null
|
||||
const clawapp = clawappRes.status === 'fulfilled' ? clawappRes.value : null
|
||||
const backups = backupsRes.status === 'fulfilled' ? backupsRes.value : []
|
||||
|
||||
if (servicesRes.status === 'rejected') toast('服务状态加载失败', 'error')
|
||||
if (versionRes.status === 'rejected') toast('版本信息加载失败', 'error')
|
||||
if (logsRes.status === 'rejected') toast('日志加载失败', 'error')
|
||||
|
||||
renderStatCards(page, services, version)
|
||||
renderStatCards(page, services, version, agents, config, tunnel)
|
||||
renderOverview(page, services, clawapp, tunnel, mcpConfig, backups, config, agents)
|
||||
renderLogs(page, logs)
|
||||
bindActions(page)
|
||||
}
|
||||
|
||||
function renderStatCards(page, services, version) {
|
||||
function renderStatCards(page, services, version, agents, config, tunnel) {
|
||||
const cardsEl = page.querySelector('#stat-cards')
|
||||
const gw = services.find(s => s.label === 'ai.openclaw.gateway')
|
||||
const runningCount = services.filter(s => s.running).length
|
||||
|
||||
const defaultAgent = agents.find(a => a.id === 'main')?.name || 'main'
|
||||
const modelCount = config?.models?.providers ? Object.values(config.models.providers).reduce((acc, p) => acc + (p.models?.length || 0), 0) : 0
|
||||
const providerCount = config?.models?.providers ? Object.keys(config.models.providers).length : 0
|
||||
|
||||
cardsEl.innerHTML = `
|
||||
<div class="stat-card">
|
||||
<div class="stat-card-header">
|
||||
@@ -77,10 +98,129 @@ function renderStatCards(page, services, version) {
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="stat-card-header">
|
||||
<span class="stat-card-label">服务</span>
|
||||
<span class="stat-card-label">Agent 舰队</span>
|
||||
</div>
|
||||
<div class="stat-card-value">${agents.length} 个</div>
|
||||
<div class="stat-card-meta">默认: ${defaultAgent}</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="stat-card-header">
|
||||
<span class="stat-card-label">模型池</span>
|
||||
</div>
|
||||
<div class="stat-card-value">${modelCount} 个</div>
|
||||
<div class="stat-card-meta">基于 ${providerCount} 个渠道商</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="stat-card-header">
|
||||
<span class="stat-card-label">内网穿透隧道</span>
|
||||
<span class="status-dot ${tunnel?.running ? 'running' : 'stopped'}"></span>
|
||||
</div>
|
||||
<div class="stat-card-value">${tunnel?.running ? '运行中' : (tunnel?.installed ? '已停止' : '未配置')}</div>
|
||||
<div class="stat-card-meta">${tunnel?.routes ? tunnel.routes.length + ' 个路由映射' : '——'}</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="stat-card-header">
|
||||
<span class="stat-card-label">基础服务</span>
|
||||
</div>
|
||||
<div class="stat-card-value">${runningCount}/${services.length}</div>
|
||||
<div class="stat-card-meta">运行中</div>
|
||||
<div class="stat-card-meta">存活率 ${services.length ? Math.round(runningCount / services.length * 100) : 0}%</div>
|
||||
</div>
|
||||
`
|
||||
}
|
||||
|
||||
function renderOverview(page, services, clawapp, tunnel, mcpConfig, backups, config, agents) {
|
||||
const containerEl = page.querySelector('#dashboard-overview-container')
|
||||
const gw = services.find(s => s.label === 'ai.openclaw.gateway')
|
||||
const mcpCount = mcpConfig?.mcpServers ? Object.keys(mcpConfig.mcpServers).length : 0
|
||||
|
||||
const formatDate = (timestamp) => {
|
||||
if (!timestamp) return '——'
|
||||
const d = new Date(timestamp * 1000)
|
||||
return \`\${d.getMonth()+1}-\${d.getDate()} \${d.getHours().toString().padStart(2, '0')}:\${d.getMinutes().toString().padStart(2, '0')}\`
|
||||
}
|
||||
|
||||
const latestBackup = backups.length > 0 ? backups.sort((a,b) => b.created_at - a.created_at)[0] : null
|
||||
const lastUpdate = config?.meta?.lastTouchedVersion || '未知'
|
||||
|
||||
containerEl.innerHTML = `
|
||||
<div class="dashboard-overview">
|
||||
<div class="overview-section">
|
||||
<div class="overview-item">
|
||||
<div class="overview-label">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="2" y="3" width="20" height="14" rx="2" ry="2"></rect><line x1="8" y1="21" x2="16" y2="21"></line><line x1="12" y1="17" x2="12" y2="21"></line></svg>
|
||||
Gateway 核心网关
|
||||
</div>
|
||||
<div class="overview-value" style="color: ${gw?.running ? 'var(--success)' : 'var(--error)'}">
|
||||
${gw?.running ? '运行中' : '已停止'}
|
||||
</div>
|
||||
</div>
|
||||
<div class="overview-item">
|
||||
<div class="overview-label">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"></circle><polyline points="12 6 12 12 16 14"></polyline></svg>
|
||||
ClawApp 守护进程
|
||||
</div>
|
||||
<div class="overview-value" style="color: ${clawapp?.running ? 'var(--success)' : 'var(--error)'}">
|
||||
${clawapp?.running ? '端口 ' + clawapp.port : '未启动'}
|
||||
</div>
|
||||
</div>
|
||||
<div class="overview-item">
|
||||
<div class="overview-label">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"></path><path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"></path></svg>
|
||||
Cloudflare 隧道
|
||||
</div>
|
||||
<div class="overview-value" style="color: ${tunnel?.running ? 'var(--success)' : (tunnel?.installed ? 'var(--warning)' : 'var(--text-tertiary)')}">
|
||||
${tunnel?.running ? tunnel.tunnel_name : (tunnel?.installed ? '已停止' : '未安装')}
|
||||
</div>
|
||||
</div>
|
||||
<div class="overview-item">
|
||||
<div class="overview-label">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polygon points="12 2 2 7 12 12 22 7 12 2"></polygon><polyline points="2 17 12 22 22 17"></polyline><polyline points="2 12 12 17 22 12"></polyline></svg>
|
||||
MCP 扩展工具
|
||||
</div>
|
||||
<div class="overview-value">
|
||||
${mcpCount} 个已挂载
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="overview-section">
|
||||
<div class="overview-item">
|
||||
<div class="overview-label">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"></path><polyline points="17 8 12 3 7 8"></polyline><line x1="12" y1="3" x2="12" y2="15"></line></svg>
|
||||
最近备份
|
||||
</div>
|
||||
<div class="overview-value">
|
||||
${latestBackup ? formatDate(latestBackup.created_at) : '从无备份'}
|
||||
</div>
|
||||
</div>
|
||||
<div class="overview-item">
|
||||
<div class="overview-label">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"></path><path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z"></path></svg>
|
||||
配置版本标识
|
||||
</div>
|
||||
<div class="overview-value">
|
||||
${lastUpdate}
|
||||
</div>
|
||||
</div>
|
||||
<div class="overview-item">
|
||||
<div class="overview-label">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"></path><circle cx="9" cy="7" r="4"></circle><path d="M23 21v-2a4 4 0 0 0-3-3.87"></path><path d="M16 3.13a4 4 0 0 1 0 7.75"></path></svg>
|
||||
并行推理队列最大值
|
||||
</div>
|
||||
<div class="overview-value">
|
||||
${config?.agents?.defaults?.maxConcurrent || 4}
|
||||
</div>
|
||||
</div>
|
||||
<div class="overview-item">
|
||||
<div class="overview-label">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="3" width="18" height="18" rx="2" ry="2"></rect><line x1="9" y1="3" x2="9" y2="21"></line></svg>
|
||||
工作区文件隔离
|
||||
</div>
|
||||
<div class="overview-value" style="color: ${agents.some(a => a.workspace) ? 'var(--success)' : 'var(--text-tertiary)'}">
|
||||
${agents.filter(a => a.workspace).length} 个 Agent 启用
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`
|
||||
}
|
||||
@@ -96,6 +236,7 @@ function renderLogs(page, logs) {
|
||||
function bindActions(page) {
|
||||
const btnRestart = page.querySelector('#btn-restart-gw')
|
||||
const btnUpdate = page.querySelector('#btn-check-update')
|
||||
const btnCreateBackup = page.querySelector('#btn-create-backup')
|
||||
|
||||
btnRestart?.addEventListener('click', async () => {
|
||||
btnRestart.disabled = true
|
||||
@@ -129,6 +270,21 @@ function bindActions(page) {
|
||||
btnUpdate.textContent = '检查更新'
|
||||
}
|
||||
})
|
||||
|
||||
btnCreateBackup?.addEventListener('click', async () => {
|
||||
btnCreateBackup.disabled = true
|
||||
btnCreateBackup.innerHTML = '备份中...'
|
||||
try {
|
||||
const res = await api.createBackup()
|
||||
toast(`已备份: ${res.name}`, 'success')
|
||||
setTimeout(() => loadDashboardData(page), 500)
|
||||
} catch (e) {
|
||||
toast('备份失败: ' + e, 'error')
|
||||
} finally {
|
||||
btnCreateBackup.disabled = false
|
||||
btnCreateBackup.textContent = '创建备份'
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function escapeHtml(str) {
|
||||
|
||||
Reference in New Issue
Block a user