From 1f5f92ba3699da945441635112add3257374ea91 Mon Sep 17 00:00:00 2001 From: Tao Date: Thu, 26 Feb 2026 11:04:56 +0000 Subject: [PATCH] feat: add Dashboard tab + cache-control + version cache busting (v0.6.5) - New Dashboard landing tab with system info, gateway health, agent stats, storage - Cache-Control: no-store on all HTTP responses - Version-based cache busting with X-OCM-Version header + client-side detection - Confirmed old /api/agents/main endpoint fully removed Co-Authored-By: Claude Opus 4.6 --- DEVLOG.md | 49 ++++++++-- openclaw-manager.js | 218 ++++++++++++++++++++++++++++++++++++++++++-- 2 files changed, 251 insertions(+), 16 deletions(-) diff --git a/DEVLOG.md b/DEVLOG.md index 4a32a9e..ff17165 100644 --- a/DEVLOG.md +++ b/DEVLOG.md @@ -1,7 +1,40 @@ # OpenClaw Manager — 开发日志 > 最后更新:2026-02-26 -> 当前版本:v0.6.0 +> 当前版本:v0.6.5 + +--- + +## v0.6.5 更新日志(2026-02-26) + +### New Features + +**Dashboard Tab (🏠 Dashboard)** +- New default landing tab showing system overview at a glance +- **System card**: hostname, OS, Node.js version, CPU model/cores, uptime, memory usage with progress bar +- **Gateway card**: process status indicator (running/stopped/unknown) with coloured dot, port, PID, HTTP ping reachability +- **Agents card**: total agent count, last session activity timestamp (Brisbane time), OCM version, server time +- **Storage card**: OpenClaw dir size, disk usage with progress bar, free space +- New `GET /api/dashboard` endpoint aggregates all info (ps grep + curl ping + fs stat) +- Lazy-loaded on tab switch; auto-loaded on first page load + +**Cache-Control + Version-Based Cache Busting** +- All HTTP responses now include `Cache-Control: no-store, no-cache, must-revalidate` +- HTML responses include `ETag: "ocm-"` for version-based cache validation +- All responses include `X-OCM-Version` header +- Browser `api()` function checks `X-OCM-Version` against client version; shows toast notification when server has been updated, prompting user to refresh +- Prevents stale frontend from calling deleted/changed API endpoints after update + +### Cleanup + +**Old `/api/agents/main` endpoint fully removed** (confirmed via code search — no residual references) + +### Technical Notes + +- Dashboard gateway detection: `ps aux | grep openclaw.*gateway` for process status, `curl --max-time 2 http://127.0.0.1:` for HTTP ping +- Disk usage: `df -k` for filesystem stats, `du -sk` for OpenClaw dir size +- Agent last activity: scans `~/.openclaw/agents/*/sessions/*.jsonl` mtime +- Version cache busting: `OCM_CLIENT_VERSION` is injected into browser script via template literal `${APP_VERSION}`, compared against `X-OCM-Version` response header on every API call --- @@ -100,15 +133,13 @@ - **`assertBrowserScriptSyntax()`** 在启动时检查 MAIN_HTML_SCRIPT 的 evaluated 值 - **OpenClaw config 多 bot 格式**:`channels.telegram.accounts..botToken`,binding 用 `accountId` 路由 -### 下一步(v0.6.1+) +### 下一步(v0.6.2+) -- [ ] Dashboard 首页 tab(系统信息 + OpenClaw health 状态) - - 系统:OS、Node 版本、uptime、内存、磁盘 - - 健康:进程检测(ps grep)+ HTTP ping OpenClaw 端口 - - Gateway 状态指示灯(running/stopped/unknown) - - Agent 数量、最近活动时间 -- [ ] HTTP 响应加 `Cache-Control: no-store` + version-based cache busting(防止浏览器缓存旧前端触发已删除的 API) -- [ ] 彻底删除旧 `/api/agents/main` 端点残留(确认已清除) +- [x] Dashboard 首页 tab(系统信息 + OpenClaw health 状态)— done in v0.6.5 +- [x] HTTP 响应加 `Cache-Control: no-store` + version-based cache busting — done in v0.6.5 +- [x] 彻底删除旧 `/api/agents/main` 端点残留(确认已清除)— confirmed in v0.6.5 +- [ ] Dashboard: auto-refresh every 30s when tab is active +- [ ] Dashboard: OpenClaw version display (from `openclaw --version`) - [ ] DEVLOG.md 中文 → 逐步迁移为英文 --- diff --git a/openclaw-manager.js b/openclaw-manager.js index eca437b..817801c 100644 --- a/openclaw-manager.js +++ b/openclaw-manager.js @@ -1,6 +1,6 @@ #!/usr/bin/env node // ================================================================ -// OpenClaw Manager v0.6.0 +// OpenClaw Manager v0.6.5 // 跨平台本地管理工具 (Windows / macOS / Linux) // // 用法: @@ -24,7 +24,7 @@ const SCRIPT_DIR = __dirname; const MANAGER_CONFIG = path.join(SCRIPT_DIR, 'manager-config.json'); let PORT = 3333; let HOST = '0.0.0.0'; -const APP_VERSION = '0.6.0'; +const APP_VERSION = '0.6.5'; // --port 参数 const portIdx = process.argv.indexOf('--port'); if (portIdx !== -1 && process.argv[portIdx + 1]) PORT = parseInt(process.argv[portIdx + 1]) || 3333; @@ -1208,6 +1208,108 @@ async function handleApi(req, res, urlObj, body) { return; } + // GET /api/dashboard — system info + gateway health for Dashboard tab + if (method === 'GET' && pathname === '/api/dashboard') { + try { + const cfg = await configExists() ? await readConfig() : null; + // System info + const sysUptime = os.uptime(); + const totalMem = os.totalmem(); + const freeMem = os.freemem(); + const nodeVer = process.version; + const platform = `${os.type()} ${os.release()} (${os.arch()})`; + const hostname = os.hostname(); + const cpus = os.cpus(); + const cpuModel = cpus.length ? cpus[0].model.trim() : 'Unknown'; + const cpuCores = cpus.length; + + // Disk usage (best-effort, works on macOS/Linux) + let diskTotal = 0, diskUsed = 0, diskFree = 0; + try { + const dfOut = execSync('df -k ' + JSON.stringify(OPENCLAW_DIR), { encoding: 'utf8', timeout: 3000 }); + const dfLines = dfOut.trim().split('\\n'); + if (dfLines.length >= 2) { + const parts = dfLines[1].split(/\\s+/); + diskTotal = parseInt(parts[1] || 0) * 1024; + diskUsed = parseInt(parts[2] || 0) * 1024; + diskFree = parseInt(parts[3] || 0) * 1024; + } + } catch (_) {} + + // OpenClaw dir size (best-effort) + let dirSize = 0; + try { + const duOut = execSync('du -sk ' + JSON.stringify(OPENCLAW_DIR), { encoding: 'utf8', timeout: 5000 }); + dirSize = parseInt(duOut.split(/\\s/)[0] || 0) * 1024; + } catch (_) {} + + // Gateway process detection + let gatewayRunning = 'unknown'; + let gatewayPid = null; + try { + const psOut = execSync("ps aux 2>/dev/null | grep -i 'openclaw.*gateway' | grep -v grep", { encoding: 'utf8', timeout: 3000 }).trim(); + if (psOut) { + gatewayRunning = 'running'; + const psParts = psOut.split(/\\s+/); + gatewayPid = psParts[1] || null; + } else { + gatewayRunning = 'stopped'; + } + } catch (_) { gatewayRunning = 'stopped'; } + + // HTTP ping gateway (port from config or default 3000) + let gatewayPort = null; + let gatewayPing = false; + try { + if (cfg && cfg.channels && cfg.channels.telegram) { + gatewayPort = cfg.channels.telegram.port || null; + } + if (!gatewayPort) gatewayPort = 3000; + const pingResult = spawnSync('curl', ['-s', '-o', '/dev/null', '-w', '%{http_code}', '--max-time', '2', 'http://127.0.0.1:' + gatewayPort], { encoding: 'utf8', timeout: 4000 }); + const code = parseInt((pingResult.stdout || '').trim()); + gatewayPing = code > 0 && code < 500; + } catch (_) {} + + // Agent count & last activity + let agentCount = 0; + let lastActivity = null; + try { + const sessionsBase = path.join(OPENCLAW_DIR, 'agents'); + const agentDirs = await fsp.readdir(sessionsBase); + agentCount = agentDirs.length; + for (const ad of agentDirs) { + const sessDir = path.join(sessionsBase, ad, 'sessions'); + try { + const sessFiles = await fsp.readdir(sessDir); + for (const sf of sessFiles) { + if (sf.endsWith('.jsonl')) { + const st = await fsp.stat(path.join(sessDir, sf)); + if (!lastActivity || st.mtime > lastActivity) lastActivity = st.mtime; + } + } + } catch (_) {} + } + } catch (_) {} + + // Brisbane time for display + const now = new Date().toLocaleString('en-AU', { timeZone: 'Australia/Brisbane', hour12: false }); + + res.writeHead(200); + res.end(JSON.stringify({ + ok: true, + system: { hostname, platform, nodeVer, cpuModel, cpuCores, uptime: sysUptime, totalMem, freeMem, diskTotal, diskUsed, diskFree, dirSize }, + gateway: { status: gatewayRunning, pid: gatewayPid, port: gatewayPort, ping: gatewayPing }, + agents: { count: agentCount, lastActivity: lastActivity ? lastActivity.toISOString() : null }, + ocmVersion: APP_VERSION, + serverTime: now, + })); + } catch (e) { + res.writeHead(500); + res.end(JSON.stringify({ ok: false, error: e.message })); + } + return; + } + // POST /api/gateway/doctor if (method === 'POST' && pathname === '/api/gateway/doctor') { try { @@ -1426,6 +1528,23 @@ select option { background:var(--surface); } .input-pw-wrap input { padding-right:36px; } .pw-toggle { position:absolute; right:10px; top:50%; transform:translateY(-50%); background:none; border:none; color:var(--muted); cursor:pointer; font-size:14px; padding:0; } +/* ── Dashboard ── */ +.dash-grid { display:grid; grid-template-columns:repeat(auto-fit,minmax(300px,1fr)); gap:16px; } +.dash-card { padding:20px; } +.dash-card h3 { font-size:14px; font-weight:600; margin-bottom:14px; color:var(--text); } +.dash-row { display:flex; justify-content:space-between; align-items:center; padding:6px 0; border-bottom:1px solid var(--border); font-size:12px; } +.dash-row:last-child { border-bottom:none; } +.dash-label { color:var(--muted); } +.dash-val { color:var(--text); font-weight:500; font-family:monospace; font-size:11px; } +.dash-indicator { display:inline-block; width:8px; height:8px; border-radius:50%; margin-right:6px; } +.dash-indicator.running { background:#22c55e; box-shadow:0 0 6px rgba(34,197,94,.5); } +.dash-indicator.stopped { background:#ef4444; box-shadow:0 0 6px rgba(239,68,68,.5); } +.dash-indicator.unknown { background:#f59e0b; } +.dash-bar-wrap { width:100%; height:6px; background:var(--border); border-radius:3px; margin-top:4px; } +.dash-bar { height:100%; border-radius:3px; background:var(--accent); transition:width .3s; } +.dash-bar.warn { background:#f59e0b; } +.dash-bar.danger { background:#ef4444; } + /* ── Modal ── */ .backdrop { position:fixed; inset:0; background:rgba(0,0,0,.72); z-index:100; display:none; align-items:center; justify-content:center; } .backdrop.open { display:flex; } @@ -1567,7 +1686,8 @@ const MAIN_HTML_BODY = String.raw`