ec-web 0.0.3 → 0.0.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/server.js CHANGED
@@ -446,6 +446,7 @@ function handleConnection(ws, req, log) {
446
446
  const ip = clientIp(req);
447
447
  let unsubscribe = null;
448
448
  let currentView = null;
449
+ let subscriptionGeneration = 0;
449
450
  log(`◆ WS 连接 from ${ip}`);
450
451
  const send = (obj) => {
451
452
  if (ws.readyState === ws.OPEN)
@@ -455,6 +456,7 @@ function handleConnection(ws, req, log) {
455
456
  catch { }
456
457
  };
457
458
  const switchSubscription = async (view, params) => {
459
+ const generation = ++subscriptionGeneration;
458
460
  if (unsubscribe) {
459
461
  unsubscribe();
460
462
  unsubscribe = null;
@@ -468,16 +470,28 @@ function handleConnection(ws, req, log) {
468
470
  const sourceParams = { ...params, __menuAuth: daemonOwnerMenuAuth() };
469
471
  // System 视图:把 ECWeb 自身版本合并进快照(前端版本卡用)
470
472
  const decorate = (data) => (view === 'system' && data ? { ...data, ecwebVersion: ECWEB_VERSION } : data);
473
+ let initialSnapshot;
471
474
  try {
472
- send({ type: 'snapshot', view, data: decorate(await source.snapshot(sourceParams)) });
475
+ initialSnapshot = await source.snapshot(sourceParams);
476
+ if (generation !== subscriptionGeneration)
477
+ return;
478
+ send({ type: 'snapshot', view, data: decorate(initialSnapshot) });
473
479
  }
474
480
  catch (e) {
481
+ if (generation !== subscriptionGeneration)
482
+ return;
475
483
  send({ type: 'error', message: `snapshot failed: ${e?.message || e}` });
476
484
  }
477
- unsubscribe = source.subscribe(sourceParams, (data) => {
478
- if (currentView === view)
485
+ const nextUnsubscribe = source.subscribe(sourceParams, (data) => {
486
+ if (generation === subscriptionGeneration && currentView === view) {
479
487
  send({ type: 'delta', view, data: decorate(data) });
488
+ }
480
489
  });
490
+ if (generation !== subscriptionGeneration) {
491
+ nextUnsubscribe();
492
+ return;
493
+ }
494
+ unsubscribe = nextUnsubscribe;
481
495
  };
482
496
  ws.on('message', async (raw) => {
483
497
  let msg;
@@ -563,10 +577,15 @@ function handleConnection(ws, req, log) {
563
577
  ws.ping();
564
578
  }, 25000);
565
579
  ws.on('pong', () => { alive = true; });
566
- ws.on('close', () => { clearInterval(heartbeat); if (unsubscribe) {
567
- unsubscribe();
568
- unsubscribe = null;
569
- } log(`◇ WS 断开 from ${ip}`); });
580
+ ws.on('close', () => {
581
+ subscriptionGeneration += 1;
582
+ clearInterval(heartbeat);
583
+ if (unsubscribe) {
584
+ unsubscribe();
585
+ unsubscribe = null;
586
+ }
587
+ log(`◇ WS 断开 from ${ip}`);
588
+ });
570
589
  ws.on('error', () => { });
571
590
  }
572
591
  // ── Port binding ──
@@ -8,11 +8,44 @@
8
8
  * - fine : 2s 桶 × 60 ≈ 近 2 分钟
9
9
  * - mid : 10s 桶 × 60 ≈ 近 10 分钟
10
10
  * - coarse : 60s 桶 × 60 ≈ 近 1 小时
11
- * 每个桶内对 CPU/内存做均值聚合,避免长周期下点数爆炸。
11
+ * 每个桶内对 CPU/内存做均值聚合,关联进程先按类型和名称合并,
12
+ * 避免长周期下点数爆炸,也不让 PID 变化拆散同一条曲线。
12
13
  */
13
14
  import fs from 'node:fs';
14
15
  import { resolvePaths } from '../paths.js';
15
16
  import { ipcQuery } from '../ipc-client.js';
17
+ function processGroupKey(process) {
18
+ return `${process.kind}\u0000${process.name}`;
19
+ }
20
+ /** 同一类、同一可执行名的多个实例在一个采样点中汇总。 */
21
+ export function mergeProcessesByIdentity(processes) {
22
+ const merged = new Map();
23
+ for (const process of processes) {
24
+ const name = String(process.name || 'process');
25
+ const kind = String(process.kind || 'child');
26
+ const key = processGroupKey({ name, kind });
27
+ const current = merged.get(key);
28
+ if (current) {
29
+ current.cpuPercent += Number(process.cpuPercent) || 0;
30
+ current.rss += Number(process.rss) || 0;
31
+ }
32
+ else {
33
+ merged.set(key, {
34
+ name,
35
+ kind,
36
+ cpuPercent: Number(process.cpuPercent) || 0,
37
+ rss: Number(process.rss) || 0,
38
+ });
39
+ }
40
+ }
41
+ return [...merged.values()].sort((left, right) => processGroupKey(left).localeCompare(processGroupKey(right)));
42
+ }
43
+ /** 将进程的单核 CPU 百分比换算为整机全部逻辑 CPU 的容量占比。 */
44
+ export function normalizeProcessCpuPercent(cpuPercent, cpuCount) {
45
+ const count = Number(cpuCount);
46
+ const divisor = Number.isFinite(count) && count > 0 ? count : 1;
47
+ return (Number(cpuPercent) || 0) / divisor;
48
+ }
16
49
  // 模块级缓冲区——所有订阅者共享同一份累积时序(单一事实源)。
17
50
  const resolutions = {
18
51
  fine: { bucketMs: 2_000, cap: 60, buf: [] },
@@ -29,10 +62,12 @@ function ingest(res, sample) {
29
62
  last.sysCpu = last.sysCpu + (sample.sysCpu - last.sysCpu) / n;
30
63
  last.procRss = last.procRss + (sample.procRss - last.procRss) / n;
31
64
  last.sysMemUsed = last.sysMemUsed + (sample.sysMemUsed - last.sysMemUsed) / n;
65
+ last.diskReadBps = last.diskReadBps + (sample.diskReadBps - last.diskReadBps) / n;
66
+ last.diskWriteBps = last.diskWriteBps + (sample.diskWriteBps - last.diskWriteBps) / n;
32
67
  last.netRxBps = last.netRxBps + (sample.netRxBps - last.netRxBps) / n;
33
68
  last.netTxBps = last.netTxBps + (sample.netTxBps - last.netTxBps) / n;
34
69
  for (const process of sample.processes) {
35
- const current = last.processes.find(item => item.pid === process.pid);
70
+ const current = last.processes.find(item => processGroupKey(item) === processGroupKey(process));
36
71
  if (current) {
37
72
  const processSamples = current._n + 1;
38
73
  current.cpuPercent += (process.cpuPercent - current.cpuPercent) / processSamples;
@@ -61,10 +96,11 @@ function exportBuf(res) {
61
96
  sysCpu: round1(b.sysCpu),
62
97
  procRss: Math.round(b.procRss),
63
98
  sysMemUsed: Math.round(b.sysMemUsed),
99
+ diskReadBps: Math.round(b.diskReadBps),
100
+ diskWriteBps: Math.round(b.diskWriteBps),
64
101
  netRxBps: Math.round(b.netRxBps),
65
102
  netTxBps: Math.round(b.netTxBps),
66
103
  processes: b.processes.map(process => ({
67
- pid: process.pid,
68
104
  name: process.name,
69
105
  kind: process.kind,
70
106
  cpuPercent: round1(process.cpuPercent),
@@ -73,6 +109,49 @@ function exportBuf(res) {
73
109
  }));
74
110
  }
75
111
  function round1(n) { return Math.round(n * 10) / 10; }
112
+ const DISK_SECTOR_BYTES = 512;
113
+ let previousDiskSample = null;
114
+ let latestDiskRate = { readBps: 0, writeBps: 0 };
115
+ /** 解析 Linux diskstats,只累计整盘,避免分区和虚拟映射重复计数。 */
116
+ export function parseLinuxDiskStats(input, isPartition = name => fs.existsSync(`/sys/class/block/${name}/partition`)) {
117
+ let readBytes = 0;
118
+ let writeBytes = 0;
119
+ for (const line of input.split('\n')) {
120
+ const fields = line.trim().split(/\s+/);
121
+ if (fields.length < 10)
122
+ continue;
123
+ const name = fields[2];
124
+ if (!name || /^(loop|ram|zram|fd|sr|dm-|md|zd)/.test(name) || isPartition(name))
125
+ continue;
126
+ readBytes += (Number(fields[5]) || 0) * DISK_SECTOR_BYTES;
127
+ writeBytes += (Number(fields[9]) || 0) * DISK_SECTOR_BYTES;
128
+ }
129
+ return { readBytes, writeBytes };
130
+ }
131
+ function readDiskTotals() {
132
+ try {
133
+ return parseLinuxDiskStats(fs.readFileSync('/proc/diskstats', 'utf8'));
134
+ }
135
+ catch {
136
+ return { readBytes: 0, writeBytes: 0 };
137
+ }
138
+ }
139
+ function sampleDiskRate(ts) {
140
+ const totals = readDiskTotals();
141
+ if (!previousDiskSample) {
142
+ previousDiskSample = { ...totals, ts };
143
+ return latestDiskRate;
144
+ }
145
+ const elapsedMs = ts - previousDiskSample.ts;
146
+ if (elapsedMs < 250)
147
+ return latestDiskRate;
148
+ latestDiskRate = {
149
+ readBps: Math.max(0, (totals.readBytes - previousDiskSample.readBytes) * 1000 / elapsedMs),
150
+ writeBps: Math.max(0, (totals.writeBytes - previousDiskSample.writeBytes) * 1000 / elapsedMs),
151
+ };
152
+ previousDiskSample = { ...totals, ts };
153
+ return latestDiskRate;
154
+ }
76
155
  let previousNetworkSample = null;
77
156
  let latestNetworkRate = { rxBps: 0, txBps: 0 };
78
157
  function readNetworkTotals() {
@@ -122,22 +201,26 @@ async function fetchSnapshot() {
122
201
  }
123
202
  const s = resp.snapshot;
124
203
  const ts = Number(s.ts) || Date.now();
204
+ const cpuCount = Number(s.cpuCount) || 1;
205
+ const disk = sampleDiskRate(ts);
125
206
  const network = sampleNetworkRate(ts);
126
207
  const sample = {
127
208
  ts,
128
- procCpu: s.cpuPercent ?? 0,
209
+ procCpu: normalizeProcessCpuPercent(s.cpuPercent, cpuCount),
129
210
  sysCpu: s.system?.cpuPercent ?? 0,
130
211
  procRss: s.memory?.rss ?? 0,
131
212
  sysMemUsed: s.system?.memUsed ?? 0,
213
+ diskReadBps: disk.readBps,
214
+ diskWriteBps: disk.writeBps,
132
215
  netRxBps: network.rxBps,
133
216
  netTxBps: network.txBps,
134
- processes: Array.isArray(s.processTree?.processes) ? s.processTree.processes.map((process) => ({
217
+ processes: mergeProcessesByIdentity(Array.isArray(s.processTree?.processes) ? s.processTree.processes.map((process) => ({
135
218
  pid: Number(process.pid),
136
219
  name: String(process.name || 'process'),
137
220
  kind: String(process.kind || 'child'),
138
- cpuPercent: Number(process.cpuPercent) || 0,
221
+ cpuPercent: normalizeProcessCpuPercent(process.cpuPercent, cpuCount),
139
222
  rss: Number(process.rss) || 0,
140
- })) : [],
223
+ })) : []),
141
224
  };
142
225
  ingest(resolutions.fine, sample);
143
226
  ingest(resolutions.mid, sample);
@@ -67,6 +67,9 @@ function readLastJsonlObject(file) {
67
67
  const rows = readAllJsonlLines(file);
68
68
  return rows.length ? rows[rows.length - 1] : null;
69
69
  }
70
+ export function latestMessageAt(messages) {
71
+ return messages.reduce((latest, message) => Math.max(latest, Number(message.ts) || 0), 0);
72
+ }
70
73
  function loadThreadSessions(dirPath) {
71
74
  const threadsDir = path.join(dirPath, '_threads');
72
75
  const index = readJsonFile(path.join(threadsDir, 'thread-index.json')) || {};
@@ -316,8 +319,9 @@ function loadChats() {
316
319
  continue;
317
320
  const thread = item.threadId ? threadById.get(item.threadId) : undefined;
318
321
  const partitionActive = item.partition === 'thread' ? thread?.active || {} : item.partition === 'main' ? active : {};
319
- const lastMsgAt = item.messages.reduce((max, message) => Math.max(max, message.ts || 0), 0);
320
- const updatedAt = Math.max(Number(partitionActive.updatedAt || 0), lastMsgAt);
322
+ // active.updatedAt also changes for session metadata maintenance (for example,
323
+ // a project-path migration), so it is not a conversation activity timestamp.
324
+ const updatedAt = latestMessageAt(item.messages);
321
325
  chats.push({
322
326
  id: safeId(`${selfAID}\n${channelType}\n${channelId}\n${chatType}\n${item.partition}\n${item.threadId || ''}`),
323
327
  dirPath,
@@ -661,30 +661,8 @@ export const relationRolesSource = {
661
661
  return { agents: [], conversations: [], error: String(err) };
662
662
  }
663
663
  },
664
- subscribe(_params, push) {
665
- let lastJson = '';
666
- let stopped = false;
667
- const tick = async () => {
668
- if (stopped)
669
- return;
670
- try {
671
- const snap = await buildSnapshot(_params.__menuAuth ?? {});
672
- const json = JSON.stringify(snap);
673
- if (json !== lastJson) {
674
- lastJson = json;
675
- push(snap);
676
- }
677
- }
678
- catch (err) {
679
- console.error('[relation-roles] polling error:', err);
680
- }
681
- };
682
- tick();
683
- const timer = setInterval(tick, 2000);
684
- return () => {
685
- stopped = true;
686
- clearInterval(timer);
687
- };
664
+ subscribe(_params, _push) {
665
+ return () => { };
688
666
  },
689
667
  };
690
668
  function requireScope(body) {
@@ -4,12 +4,12 @@
4
4
  * 功能:
5
5
  * 1. 读取 roles.json 或内置角色配置
6
6
  * 2. 提供角色定义的 CRUD API
7
- * 3. WebSocket 推送角色配置变化
7
+ * 3. WebSocket 提供按需快照
8
8
  *
9
9
  * 数据流:
10
10
  * - 读取:从 evolcore/dist/config/roles.ts 导入 readRolesConfig
11
11
  * - 写入:保存到 .evolcore/roles.json
12
- * - 推送:2秒轮询 + JSON diff
12
+ * - 刷新:进入页面、切换 Agent 或完成写操作时按需加载
13
13
  */
14
14
  import { getModelCatalogSnapshot } from './models.js';
15
15
  import { resolveParentDistModule, toFileUrl } from './parent-package.js';
@@ -116,6 +116,11 @@ async function rolePolicyRevision(aid, auth) {
116
116
  throw Object.assign(new Error(response.error.message), { menuResponse: response });
117
117
  return response.data.policyRevision;
118
118
  }
119
+ export function visibleRoleDefinitionOptions(roleOptions) {
120
+ return roleOptions
121
+ .filter(item => item?.kind === 'user' || item?.value === 'admin')
122
+ .sort((left, right) => Number(right.value === 'admin') - Number(left.value === 'admin'));
123
+ }
119
124
  async function buildMenuSnapshot(aid, agents, auth) {
120
125
  const [roleOptions, defaults] = await Promise.all([
121
126
  execRoleMenu(aid, { type: 'menu.options', id: `ecweb-role-options-${Date.now()}`, args: { option: 'roles' } }, auth),
@@ -125,8 +130,8 @@ async function buildMenuSnapshot(aid, agents, auth) {
125
130
  throw Object.assign(new Error(roleOptions.error.message), { menuResponse: roleOptions });
126
131
  if (defaults.error)
127
132
  throw Object.assign(new Error(defaults.error.message), { menuResponse: defaults });
128
- const userRoles = roleOptions.data.filter(item => item.kind === 'user');
129
- const details = await Promise.all(userRoles.map(item => execRoleMenu(aid, {
133
+ const visibleRoles = visibleRoleDefinitionOptions(roleOptions.data);
134
+ const details = await Promise.all(visibleRoles.map(item => execRoleMenu(aid, {
130
135
  type: 'menu.query', id: `ecweb-role-${item.value}-${Date.now()}`,
131
136
  args: { view: 'role', roleId: item.value },
132
137
  }, auth)));
@@ -634,31 +639,8 @@ export const roleDefinitionsSource = {
634
639
  async snapshot(params = {}) {
635
640
  return buildSnapshot(params, params.__menuAuth ?? {});
636
641
  },
637
- subscribe(_params, push) {
638
- let lastJson = '';
639
- let stopped = false;
640
- const tick = async () => {
641
- if (stopped)
642
- return;
643
- try {
644
- const snap = await buildSnapshot(_params, _params.__menuAuth ?? {});
645
- const json = JSON.stringify(snap);
646
- if (json !== lastJson) {
647
- lastJson = json;
648
- push(snap);
649
- }
650
- }
651
- catch (err) {
652
- console.error('[role-definitions] Polling error:', err);
653
- }
654
- };
655
- // 立即执行一次
656
- tick();
657
- const timer = setInterval(tick, 2000); // 2秒轮询
658
- return () => {
659
- stopped = true;
660
- clearInterval(timer);
661
- };
642
+ subscribe(_params, _push) {
643
+ return () => { };
662
644
  }
663
645
  };
664
646
  async function handleRoleMenuCompatibility(req, res, auth, aid, agents, parsedUrl) {
@@ -374,13 +374,11 @@ const translations = {
374
374
  'monitor.card.systemMemory': '系统内存',
375
375
  'monitor.chart.cpu': 'CPU 占用',
376
376
  'monitor.chart.memory': '内存占用',
377
- 'monitor.chart.activity1h': '近一小时活动',
377
+ 'monitor.chart.disk': '磁盘读写',
378
378
  'monitor.chart.network': '网络占用',
379
- 'monitor.activity.received': '接收',
380
- 'monitor.activity.completed': '完成',
381
- 'monitor.activity.errors': '错误',
382
- 'monitor.activity.interrupts': '中断',
383
- 'monitor.activity.toolErrors': '工具错误',
379
+ 'monitor.series.diskRead': '读取',
380
+ 'monitor.series.diskWrite': '写入',
381
+ 'monitor.series.processTotal': '关联进程',
384
382
  'monitor.series.processRss': 'Daemon 进程组 RSS',
385
383
  'monitor.processes.daemon': '主进程',
386
384
  'monitor.processes.ecweb': 'ECWeb',
@@ -394,7 +392,7 @@ const translations = {
394
392
  'monitor.processes.sandbox': '沙箱',
395
393
  'monitor.processes.runner-helper': 'Runner 辅助',
396
394
  'monitor.processes.child': '关联进程',
397
- 'monitor.series.systemUsed': '系统已用',
395
+ 'monitor.series.systemUsed': '整机系统(已用)',
398
396
  'monitor.series.networkDownload': '下载',
399
397
  'monitor.series.networkUpload': '上传',
400
398
  'monitor.th.received': '收',
@@ -705,7 +703,7 @@ const translations = {
705
703
  // Usage view
706
704
  'usage.subtab.overview': '总览',
707
705
  'usage.subtab.explorer': '详细统计',
708
- 'usage.overview.range.today': '今日',
706
+ 'usage.overview.range.last24': '最近24小时',
709
707
  'usage.overview.range.week': '本周',
710
708
  'usage.overview.range.lastWeek': '上周',
711
709
  'usage.overview.range.month': '本月',
@@ -1232,13 +1230,11 @@ const translations = {
1232
1230
  'monitor.card.systemMemory': 'System Memory',
1233
1231
  'monitor.chart.cpu': 'CPU Usage',
1234
1232
  'monitor.chart.memory': 'Memory Usage',
1235
- 'monitor.chart.activity1h': 'Activity in Last Hour',
1233
+ 'monitor.chart.disk': 'Disk I/O',
1236
1234
  'monitor.chart.network': 'Network Usage',
1237
- 'monitor.activity.received': 'Received',
1238
- 'monitor.activity.completed': 'Completed',
1239
- 'monitor.activity.errors': 'Errors',
1240
- 'monitor.activity.interrupts': 'Interrupts',
1241
- 'monitor.activity.toolErrors': 'ToolErr',
1235
+ 'monitor.series.diskRead': 'Read',
1236
+ 'monitor.series.diskWrite': 'Write',
1237
+ 'monitor.series.processTotal': 'Related Processes',
1242
1238
  'monitor.series.processRss': 'Daemon process group RSS',
1243
1239
  'monitor.processes.daemon': 'Daemon',
1244
1240
  'monitor.processes.ecweb': 'ECWeb',
@@ -1563,7 +1559,7 @@ const translations = {
1563
1559
  // Usage view
1564
1560
  'usage.subtab.overview': 'Overview',
1565
1561
  'usage.subtab.explorer': 'Detailed Statistics',
1566
- 'usage.overview.range.today': 'Today',
1562
+ 'usage.overview.range.last24': 'Last 24 Hours',
1567
1563
  'usage.overview.range.week': 'This Week',
1568
1564
  'usage.overview.range.lastWeek': 'Last Week',
1569
1565
  'usage.overview.range.month': 'This Month',
@@ -7002,7 +6998,7 @@ function disposeWindowChart(key) {
7002
6998
  }
7003
6999
 
7004
7000
  function refreshThemeSensitiveViews() {
7005
- ['_hourlyChart', '_modelChart', '_monCpu', '_monMem', '_monMsg', '_monNet'].forEach(disposeWindowChart);
7001
+ ['_hourlyChart', '_modelChart', '_monCpu', '_monMem', '_monDisk', '_monNet'].forEach(disposeWindowChart);
7006
7002
  if (typeof _explorerChart !== 'undefined' && _explorerChart) {
7007
7003
  _explorerChart.dispose();
7008
7004
  _explorerChart = null;
@@ -7060,7 +7056,12 @@ function fmtTokens(n) {
7060
7056
  }
7061
7057
 
7062
7058
  // ── Usage Overview(总览,支持日期范围筛选)──
7063
- let _ovCurrentRange = 'today'; // 当前选择的范围
7059
+ let _ovCurrentRange = 'last24'; // 当前选择的范围
7060
+
7061
+ function rollingLast24HourRange(now) {
7062
+ const toTs = (now || new Date()).getTime();
7063
+ return { fromTs: toTs - 24 * 60 * 60 * 1000, toTs };
7064
+ }
7064
7065
 
7065
7066
  async function loadUsageOverview(rangeType, customFrom, customTo) {
7066
7067
  rangeType = rangeType || _ovCurrentRange;
@@ -7073,9 +7074,8 @@ async function loadUsageOverview(rangeType, customFrom, customTo) {
7073
7074
  const todayEnd = new Date(now.getFullYear(), now.getMonth(), now.getDate(), 23, 59, 59, 999).getTime();
7074
7075
 
7075
7076
  switch (rangeType) {
7076
- case 'today':
7077
- fromTs = todayStart;
7078
- toTs = todayEnd;
7077
+ case 'last24':
7078
+ ({ fromTs, toTs } = rollingLast24HourRange(now));
7079
7079
  break;
7080
7080
  case 'week': // 本周(周一到今天)
7081
7081
  const dayOfWeek = now.getDay() || 7; // 周日=7
@@ -7104,8 +7104,7 @@ async function loadUsageOverview(rangeType, customFrom, customTo) {
7104
7104
  fromTs = new Date(customFrom).getTime();
7105
7105
  toTs = new Date(customTo).getTime();
7106
7106
  } else {
7107
- fromTs = todayStart;
7108
- toTs = todayEnd;
7107
+ ({ fromTs, toTs } = rollingLast24HourRange(now));
7109
7108
  }
7110
7109
  break;
7111
7110
  default:
@@ -7559,7 +7558,7 @@ var _usageSubtabsBound = false;
7559
7558
 
7560
7559
  function activeOverviewRange() {
7561
7560
  const active = document.querySelector('.ov-range-btn.active');
7562
- return active ? active.getAttribute('data-range') || 'today' : 'today';
7561
+ return active ? active.getAttribute('data-range') || 'last24' : 'last24';
7563
7562
  }
7564
7563
 
7565
7564
  function refreshUsageView() {
@@ -7609,16 +7608,16 @@ function initUsageSubtabs() {
7609
7608
  });
7610
7609
  });
7611
7610
 
7612
- // 初始化总览页面的过滤器并加载默认数据(今日)
7611
+ // 初始化总览页面的过滤器并加载默认数据(最近 24 小时)
7613
7612
  initOverviewFilters();
7614
- loadUsageOverview('today');
7613
+ loadUsageOverview('last24');
7615
7614
  }
7616
7615
 
7617
7616
  // ── Explorer ──
7618
7617
  var _explorerChart = null;
7619
7618
  var _explorerInited = false;
7620
7619
  var _expSelection = { type: null, key: null }; // { type: 'agent'|'peer', key: string } or null
7621
- var _expCurrentRange = 'today'; // Explorer 当前选择的时间范围
7620
+ var _expCurrentRange = 'last24'; // Explorer 当前选择的时间范围
7622
7621
  var _expTimeRange = { fromTs: null, toTs: null }; // Explorer 的时间范围
7623
7622
 
7624
7623
  function initExplorer() {
@@ -7653,6 +7652,7 @@ function initExplorerTimeFilters() {
7653
7652
  if (customDateEl) customDateEl.style.display = 'none';
7654
7653
  _expCurrentRange = range;
7655
7654
  calculateExplorerTimeRange(range);
7655
+ setExplorerGranularityForRange(range);
7656
7656
  loadExplorerModels(); // 加载可用模型
7657
7657
  runExplorerQuery();
7658
7658
  }
@@ -7675,15 +7675,22 @@ function initExplorerTimeFilters() {
7675
7675
  });
7676
7676
  }
7677
7677
 
7678
- // 设置默认时间范围(今日)并初始化日期选择器
7678
+ // 设置默认时间范围(最近 24 小时)并初始化日期选择器
7679
7679
  const now = new Date();
7680
+ const last24 = rollingLast24HourRange(now);
7680
7681
  const fromEl = $('#exp-from');
7681
7682
  const toEl = $('#exp-to');
7682
- if (fromEl) fromEl.value = formatDatetimeLocal(new Date(now.getFullYear(), now.getMonth(), now.getDate()));
7683
+ if (fromEl) fromEl.value = formatDatetimeLocal(new Date(last24.fromTs));
7683
7684
  if (toEl) toEl.value = formatDatetimeLocal(now);
7684
7685
 
7685
- // 计算默认时间范围(今日)
7686
- calculateExplorerTimeRange('today');
7686
+ // 计算默认时间范围(最近 24 小时)
7687
+ calculateExplorerTimeRange('last24');
7688
+ setExplorerGranularityForRange('last24');
7689
+ }
7690
+
7691
+ function setExplorerGranularityForRange(rangeType) {
7692
+ const granularityEl = $('#exp-granularity');
7693
+ if (rangeType === 'last24' && granularityEl) granularityEl.value = 'hour';
7687
7694
  }
7688
7695
 
7689
7696
  // 计算 Explorer 的时间范围
@@ -7693,9 +7700,8 @@ function calculateExplorerTimeRange(rangeType) {
7693
7700
  const todayEnd = new Date(now.getFullYear(), now.getMonth(), now.getDate(), 23, 59, 59, 999).getTime();
7694
7701
 
7695
7702
  switch (rangeType) {
7696
- case 'today':
7697
- _expTimeRange.fromTs = todayStart;
7698
- _expTimeRange.toTs = todayEnd;
7703
+ case 'last24':
7704
+ _expTimeRange = rollingLast24HourRange(now);
7699
7705
  break;
7700
7706
  case 'week':
7701
7707
  const dayOfWeek = now.getDay() || 7;
@@ -8108,12 +8114,12 @@ function monitorLayoutHtml() {
8108
8114
  '</div>' +
8109
8115
  `<div id="mon-cards" class="usage-cards"><div class="empty" style="grid-column:1/-1">${t('common.loading')}</div></div>` +
8110
8116
  '<div class="mon-row2"><div id="mon-cpu-chart" class="mon-chart"></div><div id="mon-mem-chart" class="mon-chart"></div></div>' +
8111
- '<div class="mon-row2"><div id="mon-msg-chart" class="mon-chart"></div><div id="mon-net-chart" class="mon-chart"></div></div>' +
8117
+ '<div class="mon-row2"><div id="mon-disk-chart" class="mon-chart"></div><div id="mon-net-chart" class="mon-chart"></div></div>' +
8112
8118
  '</div>';
8113
8119
  }
8114
8120
 
8115
8121
  function disposeMonitorCharts() {
8116
- ['_monCpu', '_monMem', '_monMsg', '_monNet'].forEach(disposeWindowChart);
8122
+ ['_monCpu', '_monMem', '_monDisk', '_monNet'].forEach(disposeWindowChart);
8117
8123
  }
8118
8124
 
8119
8125
  function bindMonRangeTabs(wrap) {
@@ -8136,36 +8142,65 @@ function formatMonitorResponse(ms) {
8136
8142
  return value >= 10000 ? (value / 1000).toFixed(1) + 's' : Math.round(value) + 'ms';
8137
8143
  }
8138
8144
 
8139
- function monitorProcessLabel(process) {
8145
+ function monitorProcessKind(process) {
8140
8146
  var knownKinds = ['daemon', 'ecweb', 'watch-web', 'restart-monitor', 'watch', 'watch-aid', 'codex-app-server', 'claude-runner', 'gemini-runner', 'sandbox', 'runner-helper', 'child'];
8141
- var kind = knownKinds.includes(process.kind) ? process.kind : 'child';
8142
- return `${t('monitor.processes.' + kind)} · ${process.name || 'process'} · #${process.pid}`;
8147
+ return knownKinds.includes(process.kind) ? process.kind : 'child';
8148
+ }
8149
+
8150
+ function monitorProcessKey(process) {
8151
+ return `${monitorProcessKind(process)}\u0000${process.name || 'process'}`;
8152
+ }
8153
+
8154
+ function monitorProcessLabel(process) {
8155
+ return `${t('monitor.processes.' + monitorProcessKind(process))} · ${process.name || 'process'}`;
8143
8156
  }
8144
8157
 
8145
- function monitorProcessColor(pid) {
8158
+ function monitorProcessColor(key) {
8146
8159
  var colors = ['#4f6ef7', '#38a169', '#dd6b20', '#805ad5', '#d53f8c', '#319795', '#e53e3e', '#3182ce', '#718096', '#b7791f'];
8147
- var value = Math.abs(Number(pid) || 0);
8160
+ var value = 0;
8161
+ for (var i = 0; i < key.length; i += 1) value = ((value * 31) + key.charCodeAt(i)) | 0;
8162
+ value = Math.abs(value);
8148
8163
  return colors[value % colors.length];
8149
8164
  }
8150
8165
 
8151
8166
  function monitorProcessSeries(history, currentProcesses, metric) {
8152
8167
  var catalog = new Map();
8153
- for (const process of (currentProcesses || [])) catalog.set(process.pid, process);
8168
+ function addProcess(process) {
8169
+ var key = monitorProcessKey(process);
8170
+ if (!catalog.has(key)) catalog.set(key, { key: key, kind: monitorProcessKind(process), name: process.name || 'process' });
8171
+ }
8172
+ for (const process of (currentProcesses || [])) addProcess(process);
8154
8173
  for (const point of history) {
8155
- for (const process of (point.processes || [])) catalog.set(process.pid, process);
8174
+ for (const process of (point.processes || [])) addProcess(process);
8156
8175
  }
8157
- return [...catalog.values()].sort(function (a, b) { return a.pid - b.pid; }).map(function (process) {
8176
+ return [...catalog.values()].sort(function (a, b) { return monitorProcessLabel(a).localeCompare(monitorProcessLabel(b)); }).map(function (process) {
8158
8177
  return {
8159
8178
  name: monitorProcessLabel(process),
8160
8179
  data: history.map(function (point) {
8161
- var sample = (point.processes || []).find(function (item) { return item.pid === process.pid; });
8162
- return sample ? sample[metric] : null;
8180
+ var value = 0;
8181
+ var hasSample = false;
8182
+ for (const sample of (point.processes || [])) {
8183
+ if (monitorProcessKey(sample) !== process.key) continue;
8184
+ value += Number(sample[metric]) || 0;
8185
+ hasSample = true;
8186
+ }
8187
+ return hasSample ? value : null;
8163
8188
  }),
8164
- color: monitorProcessColor(process.pid),
8189
+ color: monitorProcessColor(process.key),
8165
8190
  };
8166
8191
  });
8167
8192
  }
8168
8193
 
8194
+ function monitorAggregateSeries(name, history, metric, color) {
8195
+ return {
8196
+ name: name,
8197
+ data: history.map(function (point) { return point[metric] != null ? point[metric] : null; }),
8198
+ color: color,
8199
+ width: 3,
8200
+ areaOpacity: 0.12,
8201
+ };
8202
+ }
8203
+
8169
8204
  function renderMonitor(data) {
8170
8205
  var wrap = $('#sys-content .mon-layout');
8171
8206
  if (!wrap) return;
@@ -8187,6 +8222,8 @@ function renderMonitor(data) {
8187
8222
  var lh = (s.stats && s.stats.lastHour) || {};
8188
8223
  var errRate = (lh.received > 0) ? ((lh.errors / lh.received) * 100).toFixed(1) + '%' : '0%';
8189
8224
  var processTree = s.processTree || {};
8225
+ var cpuCount = Math.max(1, Number(s.cpuCount) || 1);
8226
+ var processCpuPercent = (Number(s.cpuPercent) || 0) / cpuCount;
8190
8227
 
8191
8228
  // ── Stat cards ──
8192
8229
  var sysMemPct = (sys.memTotal > 0) ? Math.round((sys.memUsed / sys.memTotal) * 100) : 0;
@@ -8195,7 +8232,7 @@ function renderMonitor(data) {
8195
8232
  [t('monitor.card.messages1h'), lh.received || 0],
8196
8233
  [t('monitor.card.avgResponse'), formatMonitorResponse(lh.avgResponseMs)],
8197
8234
  [t('monitor.card.errorRate'), errRate],
8198
- [t('monitor.card.processCpu'), (s.cpuPercent != null ? s.cpuPercent : 0) + '%'],
8235
+ [t('monitor.card.processCpu'), processCpuPercent.toFixed(1) + '%'],
8199
8236
  [t('monitor.card.systemCpu'), (sys.cpuPercent != null ? sys.cpuPercent : 0) + '%'],
8200
8237
  [t('monitor.card.processMemory'), fmtBytes(s.memory ? s.memory.rss : 0)],
8201
8238
  [t('monitor.card.processCount'), processTree.processCount || 1],
@@ -8211,52 +8248,32 @@ function renderMonitor(data) {
8211
8248
  var cProc = css('--accent'), cSys = css('--orange');
8212
8249
 
8213
8250
  var currentProcesses = Array.isArray(processTree.processes) ? processTree.processes : [];
8214
- var cpuSeries = monitorProcessSeries(h, currentProcesses, 'cpuPercent');
8215
- cpuSeries.push({ name: t('monitor.legend.system'), data: h.map(function (p) { return p.sysCpu != null ? p.sysCpu : null; }), color: cSys });
8251
+ var cpuSeries = [
8252
+ monitorAggregateSeries(t('monitor.legend.system'), h, 'sysCpu', cSys),
8253
+ monitorAggregateSeries(t('monitor.series.processTotal'), h, 'procCpu', cProc),
8254
+ ].concat(monitorProcessSeries(h, currentProcesses, 'cpuPercent'));
8216
8255
 
8217
- // ── CPU:每个关联 PID + 系统 ──
8256
+ // ── CPU:整机系统、关联进程、每类关联进程 ──
8218
8257
  monDualLine('mon-cpu-chart', '_monCpu', ts, isDark, t('monitor.chart.cpu'),
8219
8258
  cpuSeries,
8220
8259
  function (v) { return Number(v).toFixed(1) + '%'; }, null);
8221
8260
 
8222
- // ── Memory:每个关联 PID ──
8261
+ // ── Memory:整机系统、关联进程、每类关联进程 ──
8262
+ var memorySeries = [
8263
+ monitorAggregateSeries(t('monitor.series.systemUsed'), h, 'sysMemUsed', cSys),
8264
+ monitorAggregateSeries(t('monitor.series.processTotal'), h, 'procRss', cProc),
8265
+ ].concat(monitorProcessSeries(h, currentProcesses, 'rss'));
8223
8266
  monDualLine('mon-mem-chart', '_monMem', ts, isDark, t('monitor.chart.memory'),
8224
- monitorProcessSeries(h, currentProcesses, 'rss'),
8267
+ memorySeries,
8225
8268
  function (v) { return fmtBytes(v); }, null);
8226
8269
 
8227
- // ── Message activity bar chart ──
8228
- var msgEl = $('#mon-msg-chart');
8229
- if (msgEl) {
8230
- if (!window._monMsg) window._monMsg = echarts.init(msgEl, isDark ? 'dark' : null);
8231
- window._monMsg.setOption({
8232
- title: { text: t('monitor.chart.activity1h'), left: 'center', top: 4, textStyle: { fontSize: 12, color: isDark ? '#e6edf3' : '#1a202c' } },
8233
- tooltip: { trigger: 'axis' },
8234
- grid: { top: 36, bottom: 24, left: 44, right: 12 },
8235
- xAxis: {
8236
- type: 'category',
8237
- data: [
8238
- t('monitor.activity.received'),
8239
- t('monitor.activity.completed'),
8240
- t('monitor.activity.errors'),
8241
- t('monitor.activity.interrupts'),
8242
- t('monitor.activity.toolErrors'),
8243
- ],
8244
- axisLabel: { fontSize: 9 },
8245
- },
8246
- yAxis: { type: 'value', minInterval: 1 },
8247
- series: [{
8248
- type: 'bar', barWidth: '45%',
8249
- data: [
8250
- { value: lh.received || 0, itemStyle: { color: css('--accent') } },
8251
- { value: lh.completed || 0, itemStyle: { color: css('--green') } },
8252
- { value: lh.errors || 0, itemStyle: { color: css('--red') } },
8253
- { value: lh.interrupts || 0, itemStyle: { color: css('--orange') } },
8254
- { value: lh.toolErrors || 0, itemStyle: { color: css('--blue') } },
8255
- ],
8256
- }],
8257
- animation: false,
8258
- });
8259
- }
8270
+ // ── Disk throughput ──
8271
+ monDualLine('mon-disk-chart', '_monDisk', ts, isDark, t('monitor.chart.disk'),
8272
+ [
8273
+ { name: t('monitor.series.diskRead'), data: h.map(function (p) { return p.diskReadBps || 0; }), color: css('--blue') || cProc },
8274
+ { name: t('monitor.series.diskWrite'), data: h.map(function (p) { return p.diskWriteBps || 0; }), color: cSys },
8275
+ ],
8276
+ function (v) { return fmtBytes(v) + '/s'; }, null);
8260
8277
 
8261
8278
  // ── Network throughput ──
8262
8279
  monDualLine('mon-net-chart', '_monNet', ts, isDark, t('monitor.chart.network'),
@@ -8268,7 +8285,7 @@ function renderMonitor(data) {
8268
8285
 
8269
8286
  }
8270
8287
 
8271
- // 双线时序图(进程 + 系统)。series: [{name,data,color}]
8288
+ // 时序图。series: [{name,data,color,width?,areaOpacity?}]
8272
8289
  function monDualLine(elId, varKey, times, isDark, title, series, fmtY, yRange) {
8273
8290
  var el = $('#' + elId);
8274
8291
  if (!el) return;
@@ -8307,8 +8324,8 @@ function monDualLine(elId, varKey, times, isDark, title, series, fmtY, yRange) {
8307
8324
  return {
8308
8325
  name: sr.name, type: 'line', data: sr.data, smooth: true, symbol: 'none',
8309
8326
  connectNulls: true,
8310
- lineStyle: { width: 2, color: sr.color },
8311
- areaStyle: { color: sr.color, opacity: 0.08 },
8327
+ lineStyle: { width: sr.width || 2, color: sr.color },
8328
+ areaStyle: { color: sr.color, opacity: sr.areaOpacity != null ? sr.areaOpacity : 0.08 },
8312
8329
  itemStyle: { color: sr.color },
8313
8330
  };
8314
8331
  }),
@@ -8769,8 +8786,7 @@ function renderRoleDefinitions(data) {
8769
8786
  }
8770
8787
 
8771
8788
  if (!data) {
8772
- console.warn('[roleDefinitions] No data received');
8773
- grid.innerHTML = '<div class="role-error">❌ 未收到角色定义数据</div>';
8789
+ grid.innerHTML = `<div class="role-error">${esc(t('common.loading'))}</div>`;
8774
8790
  return;
8775
8791
  }
8776
8792
 
@@ -8877,6 +8893,8 @@ function renderDefaultRoleSelector(data) {
8877
8893
  alert('保存默认角色失败' + (detail ? ': ' + detail : ''));
8878
8894
  privateSelect.value = defaults.private;
8879
8895
  groupSelect.value = defaults.group;
8896
+ } else {
8897
+ subscribe('roleDefinitions', roleDefinitionsParams());
8880
8898
  }
8881
8899
  } catch (err) {
8882
8900
  console.error('[roleDefinitions] Failed to save defaultRoles:', err);
@@ -9154,6 +9172,7 @@ function renderCommandPermissionsList(roleDef, operations, readonly = false) {
9154
9172
 
9155
9173
  function renderCommandPermissionsEditor(roleDef, operations) {
9156
9174
  const stats = getCommandPermissionStats(roleDef?.commandPermissions);
9175
+ const rawPermissions = formatCommandPermissions(roleDef?.commandPermissions);
9157
9176
  return `
9158
9177
  <div class="form-section command-permissions-section">
9159
9178
  <h4>Command / CLI Permissions</h4>
@@ -9168,6 +9187,18 @@ function renderCommandPermissionsEditor(roleDef, operations) {
9168
9187
  ? '清单来自后端 operation registry。每行显示当前有效权限和命中的规则;修改后会写入精确 operation 规则。若要撤销内置继承规则,请对同名 operation 设置“拒绝”。'
9169
9188
  : 'The list comes from the backend operation registry. Each row shows the current effective permission and matched rule. Changes are saved as exact operation rules.'}
9170
9189
  </div>
9190
+ <details class="cli-raw-editor">
9191
+ <summary>${currentLang === 'zh-CN' ? '编辑原始规则 JSON' : 'Edit raw rules JSON'}</summary>
9192
+ <div class="cli-help">
9193
+ ${currentLang === 'zh-CN'
9194
+ ? '用于编辑通配符或未出现在 operation 清单中的规则。下方结构化修改会合并到这份 JSON。'
9195
+ : 'Use this for wildcard rules or operations missing from the catalog. Structured changes below are merged into this JSON.'}
9196
+ </div>
9197
+ <textarea id="edit-commandPermissions"
9198
+ class="form-textarea cli-raw-textarea"
9199
+ rows="12"
9200
+ spellcheck="false">${esc(rawPermissions)}</textarea>
9201
+ </details>
9171
9202
  ${renderCommandPermissionsList(roleDef, operations, false)}
9172
9203
  </div>
9173
9204
  `;
@@ -9209,21 +9240,24 @@ function validateCommandPermissionsClient(commandPermissions) {
9209
9240
 
9210
9241
  function collectCommandPermissions(container, fallback = {}) {
9211
9242
  const rows = Array.from(container.querySelectorAll('[data-command-operation]'));
9212
- if (rows.length === 0) {
9213
- const input = container.querySelector('#edit-commandPermissions');
9214
- if (!input) return { ok: true, value: getCommandPermissionsObject(fallback) };
9243
+ const input = container.querySelector('#edit-commandPermissions');
9244
+ let commandPermissions = getCommandPermissionsObject(fallback);
9245
+
9246
+ if (input) {
9215
9247
  try {
9216
9248
  const parsed = JSON.parse(input.value.trim() || '{}');
9217
9249
  const validationError = validateCommandPermissionsClient(parsed);
9218
9250
  if (validationError) return { ok: false, error: validationError };
9219
- return { ok: true, value: parsed };
9251
+ commandPermissions = parsed;
9220
9252
  } catch (err) {
9221
9253
  return { ok: false, error: `Command permissions JSON is invalid: ${err.message}` };
9222
9254
  }
9223
9255
  }
9224
9256
 
9257
+ if (rows.length === 0) return { ok: true, value: commandPermissions };
9258
+
9225
9259
  const operationsById = new Map((roleOperationsCache || []).map(operation => [operation.id, operation]));
9226
- const commandPermissions = { ...getCommandPermissionsObject(fallback) };
9260
+ commandPermissions = { ...commandPermissions };
9227
9261
 
9228
9262
  for (const row of rows) {
9229
9263
  const operationId = row.dataset.commandOperation;
@@ -9435,7 +9469,7 @@ function createRoleCard(roleName, roleDef) {
9435
9469
  const commandStats = getCommandPermissionStats(roleDef.commandPermissions);
9436
9470
 
9437
9471
  // 判断是否为内置角色
9438
- const builtinRoles = ['member', 'visitor'];
9472
+ const builtinRoles = ['admin', 'member', 'visitor'];
9439
9473
  const isBuiltin = builtinRoles.includes(roleName);
9440
9474
 
9441
9475
  card.innerHTML = `
@@ -9530,6 +9564,7 @@ function initRoleDefinitionsTab() {
9530
9564
  alert(t('roleDefs.saveFailed'));
9531
9565
  } else {
9532
9566
  alert(t('roleDefs.saveSuccess'));
9567
+ subscribe('roleDefinitions', roleDefinitionsParams());
9533
9568
  }
9534
9569
  } catch (err) {
9535
9570
  alert(t('pair.error.network'));
@@ -9575,6 +9610,7 @@ function initRoleDefinitionsTab() {
9575
9610
  alert(t('roleDefs.saveFailed') + ': ' + (err.error || 'unknown'));
9576
9611
  } else {
9577
9612
  alert('角色已删除');
9613
+ subscribe('roleDefinitions', roleDefinitionsParams());
9578
9614
  }
9579
9615
  } catch (err) {
9580
9616
  alert(t('pair.error.network'));
@@ -10244,14 +10280,8 @@ async function showRoleEditModal(roleName, roleDef) {
10244
10280
  formHtml += renderRoleUsageLimitsEditor(roleDef);
10245
10281
 
10246
10282
  formHtml += `
10247
- <div class="role-editor-tabs" role="tablist">
10248
- <button type="button" class="role-editor-tab active" data-role-tab="fields">Field Permissions</button>
10249
- <button type="button" class="role-editor-tab" data-role-tab="cli">CLI Permissions</button>
10250
- </div>
10251
-
10252
- <div class="role-editor-tab-panel active" data-role-tab-panel="fields">
10253
- <div class="form-section">
10254
- <h4>权限配置</h4>
10283
+ <div class="form-section field-permissions-section">
10284
+ <h4>Field Permissions</h4>
10255
10285
  `;
10256
10286
 
10257
10287
  const permissions = roleDef.permissions || {};
@@ -10421,16 +10451,13 @@ async function showRoleEditModal(roleName, roleDef) {
10421
10451
  formHtml += `</div>`;
10422
10452
  });
10423
10453
 
10424
- formHtml += `</div></div>
10425
- <div class="role-editor-tab-panel" data-role-tab-panel="cli">
10426
- ${renderCommandPermissionsEditor(roleDef, operations)}
10427
- </div>
10454
+ formHtml += `</div>
10455
+ ${renderCommandPermissionsEditor(roleDef, operations)}
10428
10456
  </div>`;
10429
10457
 
10430
10458
  body.innerHTML = formHtml;
10431
10459
 
10432
10460
  bindTagsInputEvents(body);
10433
- initRoleEditorTabs(body);
10434
10461
  initCommandPermissionEditor(body);
10435
10462
  initRoleModelPermissionEditor(body, roleName, roleDef);
10436
10463
 
@@ -47,7 +47,7 @@
47
47
 
48
48
  </nav>
49
49
  <span style="flex:1"></span>
50
- 2026-07-23 12:11
50
+ 2026-07-25 03:26
51
51
  <span id="conn-status" class="conn-status" data-i18n="status.connecting">连接中…</span>
52
52
  <button id="lang-btn" class="theme-btn" data-i18n-title="language.buttonTitle" title="切换语言">🌐</button>
53
53
  <button id="theme-btn" class="theme-btn" data-i18n-title="theme.buttonTitle" title="主题">🌗</button>
@@ -141,7 +141,7 @@
141
141
  <div id="usage-overview" class="usage-subpanel active">
142
142
  <div class="overview-filters">
143
143
  <div class="ov-range-tabs">
144
- <button class="ov-range-btn active" data-range="today" data-i18n="usage.overview.range.today">Today</button>
144
+ <button class="ov-range-btn active" data-range="last24" data-i18n="usage.overview.range.last24">Last 24 Hours</button>
145
145
  <button class="ov-range-btn" data-range="week" data-i18n="usage.overview.range.week">This Week</button>
146
146
  <button class="ov-range-btn" data-range="lastWeek" data-i18n="usage.overview.range.lastWeek">Last Week</button>
147
147
  <button class="ov-range-btn" data-range="month" data-i18n="usage.overview.range.month">This Month</button>
@@ -218,7 +218,7 @@
218
218
  <!-- 时间范围选择(与总览类似,但支持精确到时分秒) -->
219
219
  <div class="exp-time-filters">
220
220
  <div class="exp-range-tabs">
221
- <button class="exp-range-btn active" data-range="today" data-i18n="usage.overview.range.today">Today</button>
221
+ <button class="exp-range-btn active" data-range="last24" data-i18n="usage.overview.range.last24">Last 24 Hours</button>
222
222
  <button class="exp-range-btn" data-range="week" data-i18n="usage.overview.range.week">This Week</button>
223
223
  <button class="exp-range-btn" data-range="lastWeek" data-i18n="usage.overview.range.lastWeek">Last Week</button>
224
224
  <button class="exp-range-btn" data-range="month" data-i18n="usage.overview.range.month">This Month</button>
@@ -245,8 +245,8 @@
245
245
  <label>
246
246
  <span data-i18n="usage.explorer.filter.granularity">Granularity</span>
247
247
  <select id="exp-granularity">
248
- <option value="hour" data-i18n="usage.explorer.filter.granularity.hour">Hour</option>
249
- <option value="day" selected data-i18n="usage.explorer.filter.granularity.day">Day</option>
248
+ <option value="hour" selected data-i18n="usage.explorer.filter.granularity.hour">Hour</option>
249
+ <option value="day" data-i18n="usage.explorer.filter.granularity.day">Day</option>
250
250
  <option value="week" data-i18n="usage.explorer.filter.granularity.week">Week</option>
251
251
  <option value="month" data-i18n="usage.explorer.filter.granularity.month">Month</option>
252
252
  </select>
@@ -3643,6 +3643,32 @@ tr.ag-disabled .ag-name { color: var(--dim); }
3643
3643
  line-height: 1.5;
3644
3644
  }
3645
3645
 
3646
+ .cli-raw-editor {
3647
+ border: 1px solid var(--border);
3648
+ border-radius: 6px;
3649
+ background: var(--bg2);
3650
+ padding: 10px 12px;
3651
+ }
3652
+
3653
+ .cli-raw-editor summary {
3654
+ color: var(--fg);
3655
+ cursor: pointer;
3656
+ font-size: 13px;
3657
+ font-weight: 600;
3658
+ }
3659
+
3660
+ .cli-raw-editor[open] summary {
3661
+ margin-bottom: 8px;
3662
+ }
3663
+
3664
+ .cli-raw-textarea {
3665
+ margin-top: 10px;
3666
+ min-height: 180px;
3667
+ font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", monospace;
3668
+ font-size: 12px;
3669
+ line-height: 1.5;
3670
+ }
3671
+
3646
3672
  .cli-category-section {
3647
3673
  display: flex;
3648
3674
  flex-direction: column;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ec-web",
3
- "version": "0.0.3",
3
+ "version": "0.0.4",
4
4
  "description": "Web console and monitoring dashboard for EvolCore",
5
5
  "type": "module",
6
6
  "bin": {
@@ -22,7 +22,7 @@
22
22
  "author": "",
23
23
  "license": "MIT",
24
24
  "peerDependencies": {
25
- "evolcore": ">=0.0.2"
25
+ "evolcore": ">=0.0.3"
26
26
  },
27
27
  "dependencies": {
28
28
  "ws": "^8.18.0"