From fb5ea6bc8422ed50f3973f8e0f6f1b340495f1e1 Mon Sep 17 00:00:00 2001 From: Tao Date: Sat, 28 Feb 2026 20:53:03 +1000 Subject: [PATCH] feat: improve dashboard and routing UX --- DEVLOG.md | 38 ++++++++++- openclaw-manager.js | 152 +++++++++++++++++++++++++++++++------------- 2 files changed, 145 insertions(+), 45 deletions(-) diff --git a/DEVLOG.md b/DEVLOG.md index a972f53..13461a3 100644 --- a/DEVLOG.md +++ b/DEVLOG.md @@ -1,10 +1,46 @@ # OpenClaw Manager — 开发日志 -> 最后更新:2026-02-27 +> 最后更新:2026-02-28 > 当前版本:v0.7.1 --- +## Unreleased(2026-02-28) + +### Dashboard / Layout + +- Removed the dashboard `HTTP Ping` field (often misleading in Telegram gateway setups) +- Expanded dashboard visibility with richer gateway routing metadata: + - bind host + - telegram account count + - group count + - binding count + - allowFrom count +- Reordered dashboard cards and made `Agents` / `Gateway` cards wider for denser information display + +### Bug Fixes + +- Fixed `main` agent workspace file browsing/editing fallback: + - when agent-level workspace is empty, it now resolves from defaults or `workspace/` + - writing files now ensures workspace directory exists before save + +### UX Clarification + +- Channels page hint text updated to position it as **advanced routing management** +- Clarifies scope difference from Agents page (daily add/remove should stay in Agents) +- Navigation label updated from `Channels` to `Routing` to reduce conceptual overlap with Agent lifecycle management +- Added `Routing` page agent filter (`All Agents` + per-agent quick filter) for easier large-tree inspection + +### Settings UX + +- Merged `Models` and `Auth` pages into a single `Models & Auth` tab + +### Stats + +- Added daily (`1 day`) range option and set it as default + +--- + ## v0.7.1 更新日志(2026-02-27) ### New Features diff --git a/openclaw-manager.js b/openclaw-manager.js index 9ee0e92..a83df0c 100644 --- a/openclaw-manager.js +++ b/openclaw-manager.js @@ -137,6 +137,16 @@ function resolvePath(p) { return p.replace(/^~/, os.homedir()); } +function resolveAgentWorkspacePath(cfg, agentId, agent) { + const defaultsWs = cfg?.agents?.defaults?.workspace || ''; + const raw = (agent && agent.workspace) || (agentId === 'main' ? defaultsWs : ''); + const expanded = resolvePath(raw || ''); + if (expanded) { + return path.isAbsolute(expanded) ? expanded : path.resolve(OPENCLAW_DIR, expanded); + } + return path.join(OPENCLAW_DIR, 'workspace', agentId); +} + async function dirExists(p) { try { const s = await fsp.stat(p); return s.isDirectory(); } catch { return false; } } @@ -613,7 +623,7 @@ async function handleApi(req, res, urlObj, body) { const cfg = await readConfig(); const agent = cfg.agents?.list?.find(a => a.id === agentId); if (!agent) { res.writeHead(404); res.end(JSON.stringify({ error: 'Agent 不存在' })); return; } - const wsPath = resolvePath(agent.workspace); + const wsPath = resolveAgentWorkspacePath(cfg, agentId, agent); const files = {}; const fileStats = {}; try { @@ -649,7 +659,8 @@ async function handleApi(req, res, urlObj, body) { const cfg = await readConfig(); const agent = cfg.agents?.list?.find(a => a.id === agentId); if (!agent) { res.writeHead(404); res.end(JSON.stringify({ error: 'Agent 不存在' })); return; } - const wsPath = resolvePath(agent.workspace); + const wsPath = resolveAgentWorkspacePath(cfg, agentId, agent); + await fsp.mkdir(wsPath, { recursive: true }); await fsp.writeFile(path.join(wsPath, fname), body.content || '', 'utf8'); res.writeHead(200); res.end(JSON.stringify({ ok: true })); @@ -1355,17 +1366,25 @@ async function handleApi(req, res, urlObj, body) { } } catch (_) { gatewayRunning = 'stopped'; } - // HTTP ping gateway (port from config or default 3000) + // 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 (_) {} + + // Telegram routing/bot counts + let bindingCount = 0, accountCount = 0, groupCount = 0, allowFromCount = 0, configAgentCount = 0; + try { + bindingCount = (cfg?.bindings || []).length; + const accounts = cfg?.channels?.telegram?.accounts || {}; + accountCount = Object.keys(accounts).length; + groupCount = Object.keys(cfg?.channels?.telegram?.groups || {}).length; + if (!accountCount && cfg?.channels?.telegram?.botToken) accountCount = 1; // legacy single-token config + allowFromCount = (cfg?.channels?.telegram?.allowFrom || []).length; + configAgentCount = (cfg?.agents?.list || []).length; } catch (_) {} // Agent count (main vs sub) & last activity @@ -1408,8 +1427,8 @@ async function handleApi(req, res, urlObj, body) { res.end(JSON.stringify({ ok: true, system: { hostname, platform, nodeVer, cpuModel, cpuCores, uptime: sysUptime, totalMem, freeMem, diskTotal, diskUsed, diskFree, dirSize, cpuPercent, loadAvg }, - gateway: { status: gatewayRunning, pid: gatewayPid, port: gatewayPort, ping: gatewayPing }, - agents: { count: agentCount, mainCount: mainAgentCount, subCount: subAgentCount, lastActivity: lastActivity ? lastActivity.toISOString() : null }, + gateway: { status: gatewayRunning, pid: gatewayPid, port: gatewayPort, host: HOST, accountCount, groupCount, bindingCount, allowFromCount }, + agents: { count: agentCount, configCount: configAgentCount, mainCount: mainAgentCount, subCount: subAgentCount, lastActivity: lastActivity ? lastActivity.toISOString() : null }, ocmVersion: APP_VERSION, serverTime: now, })); @@ -1659,6 +1678,7 @@ select option { background:var(--surface); } .dash-gauge-label { font-size:12px; color:var(--muted); margin-top:8px; font-weight:500; } .dash-sections { display:grid; grid-template-columns:repeat(auto-fit,minmax(300px,1fr)); gap:16px; } .dash-card { padding:20px; } +.dash-card-wide { grid-column:span 2; } .dash-card h3 { font-size:14px; font-weight:600; margin-bottom:14px; color:var(--text); } .dash-notice { margin-top:18px; padding:16px 18px; } .dash-notice h3 { font-size:14px; margin-bottom:10px; } @@ -1672,6 +1692,9 @@ select option { background:var(--surface); } .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; } +@media (max-width: 980px) { + .dash-card-wide { grid-column:span 1; } +} /* ── Modal ── */ .backdrop { position:fixed; inset:0; background:rgba(0,0,0,.72); z-index:100; display:none; align-items:center; justify-content:center; } @@ -1735,6 +1758,9 @@ code { font-size:12px; background:rgba(0,0,0,.3); padding:2px 6px; border-radius .ch-badge { font-size:11px; padding:2px 7px; border-radius:12px; } .ch-tg { background:rgba(51,144,236,.15); color:#33a0ec; } .ch-any { background:var(--border); color:var(--muted); } +.channels-toolbar { display:flex; gap:8px; align-items:center; flex-wrap:wrap; } +.channels-filter-label { font-size:12px; color:var(--muted); } +.channels-filter { min-width:180px; max-width:260px; font-size:12px; padding:6px 10px; } /* NAS backup */ .nas-step { background:var(--bg); border:1px solid var(--border); border-radius:8px; padding:14px; margin-bottom:12px; } @@ -1816,9 +1842,8 @@ const MAIN_HTML_BODY = String.raw`