ec-web 0.0.7 → 0.0.9

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/paths.js CHANGED
@@ -42,6 +42,7 @@ export function resolvePaths() {
42
42
  const root = resolveRoot();
43
43
  return {
44
44
  root,
45
+ daemonConfig: path.join(root, 'daemon.json'),
45
46
  sessionsDir: path.join(root, 'agents'),
46
47
  instanceDir: path.join(root, 'data', 'instance'),
47
48
  dataDir: path.join(root, 'data'),
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * System 数据源 — 纯进程级看板。
3
3
  *
4
- * snapshot: menu.list(探活)+ menu.query name=system(version/fastaunVersion/uptime/pid/node)。
4
+ * snapshot: menu.list(探活)+ menu.query name=system(version/fastaunVersion/uptime/pid/nodejs)。
5
5
  * 另注入 ecwebVersion(ECWeb 进程自身 package.json 版本,由 server.ts 合并,不走 daemon IPC)。
6
6
  * check / upgrade 结果不在快照里——由前端按钮触发 menu.action 后写入 state。
7
7
  *
@@ -24,6 +24,20 @@ function readTriggerControlToken(instanceDir) {
24
24
  return undefined;
25
25
  }
26
26
  }
27
+ /**
28
+ * The system upgrade trigger belongs to the daemon/control AID, which is not
29
+ * returned by the regular self-agent menu. Read the process-level config so
30
+ * the Triggers view can include that scheduler owner in its agent selector.
31
+ */
32
+ function readDaemonOwnerAid(p) {
33
+ try {
34
+ const raw = JSON.parse(fs.readFileSync(p.daemonConfig, 'utf8'));
35
+ return typeof raw?.aid === 'string' && raw.aid.trim() ? raw.aid.trim() : '__daemon__';
36
+ }
37
+ catch {
38
+ return null;
39
+ }
40
+ }
27
41
  function normalizeAgent(item) {
28
42
  const aid = String(item?.value || item?.aid || '').trim();
29
43
  if (!aid)
@@ -102,6 +116,19 @@ async function buildSnapshot(params) {
102
116
  const agentInfos = agents
103
117
  .map((item) => normalizeAgent({ ...runtimeByAid.get(String(item?.value || item?.aid || '')), ...item }))
104
118
  .filter((x) => !!x);
119
+ const controlAid = readDaemonOwnerAid(p);
120
+ if (controlAid) {
121
+ const daemonOwnerAid = agentInfos.some(agentInfo => agentInfo.aid === controlAid)
122
+ ? `${controlAid}#daemon`
123
+ : controlAid;
124
+ if (!agentInfos.some(agentInfo => agentInfo.aid === daemonOwnerAid)) {
125
+ agentInfos.push({
126
+ aid: daemonOwnerAid,
127
+ label: `${daemonOwnerAid} (daemon)`,
128
+ channels: ['daemon'],
129
+ });
130
+ }
131
+ }
105
132
  if (agent) {
106
133
  const found = agentInfos.find((x) => x.aid === agent);
107
134
  const triggers = await listTriggersForAgent(agent, found?.label || agent);
@@ -139,7 +139,8 @@ const translations = {
139
139
  'agents.daemonStopped': '⚠ Daemon 主进程未运行,仅显示最近活动记录',
140
140
  'agents.empty.disabled': '暂无禁用 Agent',
141
141
  'agents.empty.enabled': '暂无启用 Agent',
142
- 'agents.stats.gateway': 'Gateway',
142
+ 'agents.stats.gateway': 'Daemon Gateway',
143
+ 'agents.stats.openGatewayDrawer': '查看全部 AUN 网关',
143
144
  'agents.stats.aids': 'AIDs',
144
145
  'agents.stats.total': 'total',
145
146
  'agents.stats.online': '在线',
@@ -148,6 +149,13 @@ const translations = {
148
149
  'agents.stats.version': 'Version',
149
150
  'agents.stats.pid': 'PID',
150
151
  'agents.stats.uptime': 'Uptime',
152
+ 'agents.gatewayDrawer.title': 'AUN 网关',
153
+ 'agents.gatewayDrawer.control': 'Daemon 控制 AID',
154
+ 'agents.gatewayDrawer.agent': 'Agent',
155
+ 'agents.gatewayDrawer.unresolved': '等待发现网关',
156
+ 'agents.gatewayDrawer.empty': '暂无 AUN 连接状态',
157
+ 'agents.gatewayDrawer.reconnects': '重连 {count}',
158
+ 'agents.gatewayDrawer.connectedFor': '已连接 {duration}',
151
159
 
152
160
  // Agent table headers
153
161
  'agents.th.agent': 'Agent',
@@ -1019,7 +1027,8 @@ const translations = {
1019
1027
  'agents.daemonStopped': '⚠ Daemon process is not running, showing recent activity only',
1020
1028
  'agents.empty.disabled': 'No disabled agents',
1021
1029
  'agents.empty.enabled': 'No enabled agents',
1022
- 'agents.stats.gateway': 'Gateway',
1030
+ 'agents.stats.gateway': 'Daemon Gateway',
1031
+ 'agents.stats.openGatewayDrawer': 'View all AUN gateways',
1023
1032
  'agents.stats.aids': 'AIDs',
1024
1033
  'agents.stats.total': 'total',
1025
1034
  'agents.stats.online': 'online',
@@ -1028,6 +1037,13 @@ const translations = {
1028
1037
  'agents.stats.version': 'Version',
1029
1038
  'agents.stats.pid': 'PID',
1030
1039
  'agents.stats.uptime': 'Uptime',
1040
+ 'agents.gatewayDrawer.title': 'AUN Gateways',
1041
+ 'agents.gatewayDrawer.control': 'Daemon control AID',
1042
+ 'agents.gatewayDrawer.agent': 'Agent',
1043
+ 'agents.gatewayDrawer.unresolved': 'Gateway discovery pending',
1044
+ 'agents.gatewayDrawer.empty': 'No AUN connection status available',
1045
+ 'agents.gatewayDrawer.reconnects': '{count} reconnects',
1046
+ 'agents.gatewayDrawer.connectedFor': 'connected {duration}',
1031
1047
 
1032
1048
  // Agent table headers
1033
1049
  'agents.th.agent': 'Agent',
@@ -2469,6 +2485,7 @@ function compareVer(a, b) {
2469
2485
  // 逐 AID 异步操作状态:aid → 操作中的描述文字
2470
2486
  const _agentOps = new Map(); // Map<aid, string>
2471
2487
  let _agSubtab = 'enabled'; // 'enabled' | 'disabled'
2488
+ let _gatewayDrawer = null;
2472
2489
 
2473
2490
  // 工作状态徽标:一旦收到过消息就不再回 connected。
2474
2491
  // stopped → connected(仅首次连接无消息时) → idle(收到第一条后) → working → idle ...
@@ -2640,7 +2657,138 @@ function initMsgTipFloat() {
2640
2657
  window.addEventListener('scroll', hideNow, true);
2641
2658
  }
2642
2659
 
2643
- // 顶部统计条:Gateway / AIDs total·connected·offline / Messages / Version·PID·Uptime
2660
+ function gatewayConnectionStatus(entry) {
2661
+ const status = String(entry?.status || '').trim().toLowerCase();
2662
+ if (status) return status;
2663
+ return entry?.connected === true ? 'connected' : 'disconnected';
2664
+ }
2665
+
2666
+ function gatewayConnectionStatusClass(entry) {
2667
+ const status = gatewayConnectionStatus(entry);
2668
+ if (status === 'connected') return 'on';
2669
+ if (status === 'connecting' || status === 'reconnecting') return 'idle';
2670
+ return 'off';
2671
+ }
2672
+
2673
+ function gatewayConnectionStatusLabel(entry) {
2674
+ const status = gatewayConnectionStatus(entry);
2675
+ if (status === 'connected') return t('status.connected');
2676
+ if (status === 'connecting') return t('status.connecting');
2677
+ if (status === 'reconnecting') return t('status.reconnecting');
2678
+ if (status === 'disconnected') return t('status.disconnected');
2679
+ return status || t('common.unknown');
2680
+ }
2681
+
2682
+ function gatewayConnectionUrl(entry) {
2683
+ return typeof entry?.gatewayUrl === 'string' ? entry.gatewayUrl.trim() : '';
2684
+ }
2685
+
2686
+ function gatewayConnections(data, aids) {
2687
+ const connections = [];
2688
+ const control = data?.status?.controlAid;
2689
+ if (control?.aid) connections.push({ ...control, role: 'control' });
2690
+ for (const aid of Array.isArray(aids) ? aids : []) {
2691
+ if (aid?.aid) connections.push({ ...aid, role: 'agent' });
2692
+ }
2693
+ return connections;
2694
+ }
2695
+
2696
+ function groupGatewayConnections(data, aids) {
2697
+ const groups = [];
2698
+ const byGateway = new Map();
2699
+ for (const connection of gatewayConnections(data, aids)) {
2700
+ const url = gatewayConnectionUrl(connection);
2701
+ const key = url || '__unresolved__';
2702
+ let group = byGateway.get(key);
2703
+ if (!group) {
2704
+ group = { url, connections: [] };
2705
+ byGateway.set(key, group);
2706
+ groups.push(group);
2707
+ }
2708
+ group.connections.push(connection);
2709
+ }
2710
+ return groups;
2711
+ }
2712
+
2713
+ function gatewayConnectionHtml(connection) {
2714
+ const status = gatewayConnectionStatusLabel(connection);
2715
+ const meta = [status];
2716
+ if (gatewayConnectionStatus(connection) === 'connected' && connection.lastConnectedAt) {
2717
+ meta.push(tf('agents.gatewayDrawer.connectedFor', {
2718
+ duration: fmtDur((Date.now() - connection.lastConnectedAt) / 1000),
2719
+ }));
2720
+ }
2721
+ if (connection.reconnectCount) {
2722
+ meta.push(tf('agents.gatewayDrawer.reconnects', { count: connection.reconnectCount }));
2723
+ }
2724
+ const role = connection.role === 'control'
2725
+ ? t('agents.gatewayDrawer.control')
2726
+ : t('agents.gatewayDrawer.agent');
2727
+ const error = connection.lastError
2728
+ ? `<div class="gateway-connection-error">${esc(connection.lastError)}</div>`
2729
+ : '';
2730
+ return `<div class="gateway-connection">` +
2731
+ `<span class="dot ${gatewayConnectionStatusClass(connection)}" title="${esc(status)}"></span>` +
2732
+ '<div class="gateway-connection-main">' +
2733
+ `<div class="gateway-connection-id"><span class="gateway-connection-role">${esc(role)}</span><code>${esc(connection.aid)}</code></div>` +
2734
+ `<div class="gateway-connection-meta">${meta.map(esc).join(' · ')}</div>` +
2735
+ error +
2736
+ '</div></div>';
2737
+ }
2738
+
2739
+ function gatewayDrawerContentHtml(data, aids) {
2740
+ const groups = groupGatewayConnections(data, aids);
2741
+ if (!groups.length) return `<div class="empty">${t('agents.gatewayDrawer.empty')}</div>`;
2742
+ return groups.map(group => {
2743
+ const connected = group.connections.filter(connection => gatewayConnectionStatus(connection) === 'connected').length;
2744
+ const url = group.url || t('agents.gatewayDrawer.unresolved');
2745
+ return '<section class="gateway-group">' +
2746
+ '<header class="gateway-group-head">' +
2747
+ `<code class="gateway-group-url" title="${esc(url)}">${esc(url)}</code>` +
2748
+ `<span class="gateway-group-count">${connected}/${group.connections.length} ${t('agents.stats.online')}</span>` +
2749
+ '</header>' +
2750
+ `<div class="gateway-connection-list">${group.connections.map(gatewayConnectionHtml).join('')}</div>` +
2751
+ '</section>';
2752
+ }).join('');
2753
+ }
2754
+
2755
+ function closeGatewayDrawer() {
2756
+ const ctx = _gatewayDrawer;
2757
+ if (!ctx) return;
2758
+ document.removeEventListener('keydown', ctx.onKeydown);
2759
+ try { ctx.backdrop.remove(); } catch {}
2760
+ _gatewayDrawer = null;
2761
+ }
2762
+
2763
+ function openGatewayDrawer(data, aids) {
2764
+ closeGatewayDrawer();
2765
+ const wrap = document.createElement('div');
2766
+ wrap.innerHTML = `<div class="ag-edit-backdrop gateway-drawer-backdrop" id="gateway-drawer-backdrop">` +
2767
+ `<aside class="ag-edit-drawer gateway-drawer" role="dialog" aria-modal="true" aria-labelledby="gateway-drawer-title">` +
2768
+ '<header class="ag-edit-head">' +
2769
+ `<div id="gateway-drawer-title" class="ag-edit-title">${t('agents.gatewayDrawer.title')}</div>` +
2770
+ `<button class="ag-edit-close" id="gateway-drawer-close" type="button" aria-label="${t('common.close')}">×</button>` +
2771
+ '</header>' +
2772
+ `<div class="gateway-drawer-body">${gatewayDrawerContentHtml(data, aids)}</div>` +
2773
+ '</aside></div>';
2774
+ const backdrop = wrap.firstChild;
2775
+ document.body.appendChild(backdrop);
2776
+ const ctx = {
2777
+ backdrop,
2778
+ onKeydown: e => { if (e.key === 'Escape') closeGatewayDrawer(); },
2779
+ };
2780
+ _gatewayDrawer = ctx;
2781
+ backdrop.addEventListener('click', e => { if (e.target === backdrop) closeGatewayDrawer(); });
2782
+ backdrop.querySelector('#gateway-drawer-close').addEventListener('click', closeGatewayDrawer);
2783
+ document.addEventListener('keydown', ctx.onKeydown);
2784
+ }
2785
+
2786
+ function refreshGatewayDrawer(data, aids) {
2787
+ const body = _gatewayDrawer?.backdrop?.querySelector('.gateway-drawer-body');
2788
+ if (body) body.innerHTML = gatewayDrawerContentHtml(data, aids);
2789
+ }
2790
+
2791
+ // 顶部统计条:Daemon Gateway / AIDs total·connected·offline / Messages / Version·PID·Uptime
2644
2792
  function agentsStatsBar(data, aids, agentStats) {
2645
2793
  const connected = aids.filter(a => (a.status || 'connected') === 'connected').length;
2646
2794
  const offline = aids.length - connected;
@@ -2652,15 +2800,16 @@ function agentsStatsBar(data, aids, agentStats) {
2652
2800
  errors += s.errors || 0;
2653
2801
  interrupts += s.interrupts || 0;
2654
2802
  }
2655
- const gws = [...new Set(aids.filter(a => a.gatewayUrl).map(a => a.gatewayUrl))];
2656
- const gw = gws.length ? gws.map(esc).join(', ') : '—';
2803
+ const control = data?.status?.controlAid;
2804
+ const gw = gatewayConnectionUrl(control) || (control?.aid ? t('agents.gatewayDrawer.unresolved') : '—');
2657
2805
  const st = data.status || {};
2658
2806
  const pid = st.pid != null ? st.pid : '—';
2659
2807
  const uptime = st.uptime != null ? fmtDur(st.uptime / 1000) : '—';
2660
2808
  const ver = data.version || '—';
2661
2809
 
2662
2810
  let h = '<div class="agents-stats">';
2663
- h += `<span class="sg"><span class="sg-k">${t('agents.stats.gateway')}</span><span class="sg-gw">${gw}</span></span>`;
2811
+ h += `<span class="sg"><span class="sg-k">${t('agents.stats.gateway')}</span><span class="dot ${gatewayConnectionStatusClass(control)}"></span><span class="sg-gw">${esc(gw)}</span>` +
2812
+ `<button id="gateway-drawer-open" class="sg-gateway-open" type="button" title="${t('agents.stats.openGatewayDrawer')}" aria-label="${t('agents.stats.openGatewayDrawer')}">›</button></span>`;
2664
2813
  h += `<span class="sg"><span class="sg-k">${t('agents.stats.aids')}</span>${aids.length} ${t('agents.stats.total')} · <span class="num-on">${connected} ${t('agents.stats.online')}</span>` +
2665
2814
  `${offline ? ` · <span class="num-off">${offline} ${t('agents.stats.offline')}</span>` : ''}</span>`;
2666
2815
  h += `<span class="sg"><span class="sg-k">${t('agents.stats.messages')}</span>${t('monitor.th.received')} ${recv} · ${t('monitor.th.sent')} ${sent} · ${t('monitor.th.errors')} ${errors} · ${t('monitor.th.interrupts')} ${interrupts} · ${t('monitor.th.completed')} ${done}</span>`;
@@ -2724,6 +2873,7 @@ function renderAgents(data) {
2724
2873
 
2725
2874
  const allAgents = data.agents || [];
2726
2875
  const aids = data.aids || [];
2876
+ refreshGatewayDrawer(data, aids);
2727
2877
  const statsByAid = {};
2728
2878
  for (const s of (data.stats || [])) statsByAid[s.aid] = s;
2729
2879
  const agentStatsByAid = {};
@@ -3677,6 +3827,9 @@ function ensureOpsOutsideClose() {
3677
3827
 
3678
3828
  function bindAgentsEvents(el) {
3679
3829
  el.querySelector('#agent-new-btn')?.addEventListener('click', agentOpNew);
3830
+ el.querySelector('#gateway-drawer-open')?.addEventListener('click', () => {
3831
+ openGatewayDrawer(state.agents, state.agents?.aids || []);
3832
+ });
3680
3833
  ensureOpsOutsideClose();
3681
3834
  // 子标签切换:仅切视图变量并重渲,不重新订阅
3682
3835
  el.querySelectorAll('.ag-subtab').forEach(btn => {
@@ -4956,7 +5109,7 @@ function renderSystem(data) {
4956
5109
  remote: up.ecweb.remote,
4957
5110
  hasUpdate: !!(up.ecweb.remote && data.ecwebVersion && compareVer(data.ecwebVersion, up.ecweb.remote) < 0),
4958
5111
  } : null);
4959
- html += `<div class="cache-card"><div class="card-label">NodeJS</div><div class="card-val">${esc(sys.node || '—')}</div></div>`;
5112
+ html += `<div class="cache-card"><div class="card-label">NodeJS</div><div class="card-val">${esc(sys.nodejs || '—')}</div></div>`;
4960
5113
  html += `<div class="cache-card"><div class="card-label">${t('system.card.uptime')}</div><div class="card-val">${esc(fmtDur(sys.uptime))}</div></div>`;
4961
5114
  html += `<div class="cache-card"><div class="card-label">PID</div><div class="card-val">${sys.pid || '—'}</div></div>`;
4962
5115
  html += '</div>';
@@ -48,7 +48,7 @@
48
48
 
49
49
  </nav>
50
50
  <span style="flex:1"></span>
51
- 2026-08-03 19:27
51
+ 2026-08-06 06:08
52
52
  <span id="conn-status" class="conn-status" data-i18n="status.connecting">连接中…</span>
53
53
  <button id="lang-btn" class="theme-btn" data-i18n-title="language.buttonTitle" title="切换语言">🌐</button>
54
54
  <button id="theme-btn" class="theme-btn" data-i18n-title="theme.buttonTitle" title="主题">🌗</button>
@@ -1011,11 +1011,61 @@ tr.ag-disabled .ag-name { color: var(--dim); }
1011
1011
  .agents-stats .sg { display: inline-flex; align-items: center; gap: 5px; white-space: nowrap; }
1012
1012
  .agents-stats .sg-k { color: var(--green); font-weight: 600; }
1013
1013
  .agents-stats .sg-gw { font-family: ui-monospace, Consolas, monospace; color: var(--fg); }
1014
+ .agents-stats .dot { margin: 0; }
1015
+ .sg-gateway-open {
1016
+ width: 22px;
1017
+ height: 22px;
1018
+ padding: 0;
1019
+ border: 1px solid transparent;
1020
+ border-radius: 4px;
1021
+ background: transparent;
1022
+ color: var(--dim);
1023
+ cursor: pointer;
1024
+ font: 16px/1 ui-monospace, Consolas, monospace;
1025
+ }
1026
+ .sg-gateway-open:hover,
1027
+ .sg-gateway-open:focus-visible { color: var(--accent); border-color: var(--border); outline: none; }
1014
1028
  .agents-stats .num-on { color: var(--green); font-weight: 600; }
1015
1029
  .agents-stats .num-off { color: var(--red); font-weight: 600; }
1016
1030
  .agents-stats .in { color: var(--green); }
1017
1031
  .agents-stats .out { color: var(--blue); }
1018
1032
 
1033
+ /* AUN 网关状态抽屉 */
1034
+ .gateway-drawer { width: min(680px, 100vw); }
1035
+ .gateway-drawer-body { flex: 1; overflow-y: auto; padding: 0 18px 18px; }
1036
+ .gateway-group { padding: 18px 0; border-bottom: 1px solid var(--border); }
1037
+ .gateway-group:last-child { border-bottom: none; }
1038
+ .gateway-group-head {
1039
+ display: flex;
1040
+ align-items: flex-start;
1041
+ justify-content: space-between;
1042
+ gap: 12px;
1043
+ margin-bottom: 8px;
1044
+ }
1045
+ .gateway-group-url {
1046
+ min-width: 0;
1047
+ overflow-wrap: anywhere;
1048
+ color: var(--accent);
1049
+ font: 12px/1.45 ui-monospace, Consolas, monospace;
1050
+ }
1051
+ .gateway-group-count { flex-shrink: 0; color: var(--dim); font-size: 12px; font-variant-numeric: tabular-nums; }
1052
+ .gateway-connection-list { border-top: 1px solid var(--border); }
1053
+ .gateway-connection {
1054
+ display: flex;
1055
+ align-items: flex-start;
1056
+ gap: 9px;
1057
+ padding: 10px 2px;
1058
+ border-bottom: 1px solid var(--border);
1059
+ }
1060
+ .gateway-connection:last-child { border-bottom: none; }
1061
+ .gateway-connection .dot { margin-top: 4px; flex-shrink: 0; }
1062
+ .gateway-connection-main { min-width: 0; flex: 1; }
1063
+ .gateway-connection-id { display: flex; align-items: baseline; gap: 8px; min-width: 0; }
1064
+ .gateway-connection-role { flex-shrink: 0; color: var(--dim); font-size: 11px; font-weight: 600; }
1065
+ .gateway-connection-id code { min-width: 0; overflow-wrap: anywhere; color: var(--fg); font: 12px/1.4 ui-monospace, Consolas, monospace; }
1066
+ .gateway-connection-meta { margin-top: 3px; color: var(--dim); font-size: 11px; }
1067
+ .gateway-connection-error { margin-top: 4px; color: var(--red); font-size: 11px; overflow-wrap: anywhere; }
1068
+
1019
1069
  /* 两行一个 agent:主行 + 信息行 */
1020
1070
  #view-agents tr.ag-main > td { border-bottom: none; padding-bottom: 3px; }
1021
1071
  #view-agents tr.ag-sub > td { padding-top: 0; padding-bottom: 9px; color: var(--dim); }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ec-web",
3
- "version": "0.0.7",
3
+ "version": "0.0.9",
4
4
  "description": "Web console and monitoring dashboard for EvolCore",
5
5
  "type": "module",
6
6
  "bin": {