@sleep2agi/agent-network-dashboard 0.6.3-preview.3 → 0.6.3-preview.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.
Files changed (187) hide show
  1. package/.next/BUILD_COMMIT +1 -1
  2. package/.next/BUILD_ID +1 -1
  3. package/.next/build-manifest.json +3 -3
  4. package/.next/diagnostics/route-bundle-stats.json +12 -11
  5. package/.next/fallback-build-manifest.json +3 -3
  6. package/.next/server/app/_global-error.html +1 -1
  7. package/.next/server/app/_global-error.rsc +1 -1
  8. package/.next/server/app/_global-error.segments/__PAGE__.segment.rsc +1 -1
  9. package/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  10. package/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
  11. package/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
  12. package/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  13. package/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
  14. package/.next/server/app/_not-found.html +2 -2
  15. package/.next/server/app/_not-found.rsc +2 -2
  16. package/.next/server/app/_not-found.segments/_full.segment.rsc +2 -2
  17. package/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
  18. package/.next/server/app/_not-found.segments/_index.segment.rsc +2 -2
  19. package/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
  20. package/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
  21. package/.next/server/app/_not-found.segments/_tree.segment.rsc +2 -2
  22. package/.next/server/app/admin/page.js.nft.json +1 -1
  23. package/.next/server/app/admin/page_client-reference-manifest.js +1 -1
  24. package/.next/server/app/admin.html +2 -2
  25. package/.next/server/app/admin.rsc +3 -3
  26. package/.next/server/app/admin.segments/_full.segment.rsc +3 -3
  27. package/.next/server/app/admin.segments/_head.segment.rsc +1 -1
  28. package/.next/server/app/admin.segments/_index.segment.rsc +2 -2
  29. package/.next/server/app/admin.segments/_tree.segment.rsc +2 -2
  30. package/.next/server/app/admin.segments/admin/__PAGE__.segment.rsc +2 -2
  31. package/.next/server/app/admin.segments/admin.segment.rsc +1 -1
  32. package/.next/server/app/index.html +2 -2
  33. package/.next/server/app/index.rsc +3 -3
  34. package/.next/server/app/index.segments/__PAGE__.segment.rsc +2 -2
  35. package/.next/server/app/index.segments/_full.segment.rsc +3 -3
  36. package/.next/server/app/index.segments/_head.segment.rsc +1 -1
  37. package/.next/server/app/index.segments/_index.segment.rsc +2 -2
  38. package/.next/server/app/index.segments/_tree.segment.rsc +2 -2
  39. package/.next/server/app/login/page_client-reference-manifest.js +1 -1
  40. package/.next/server/app/login.html +2 -2
  41. package/.next/server/app/login.rsc +3 -3
  42. package/.next/server/app/login.segments/_full.segment.rsc +3 -3
  43. package/.next/server/app/login.segments/_head.segment.rsc +1 -1
  44. package/.next/server/app/login.segments/_index.segment.rsc +2 -2
  45. package/.next/server/app/login.segments/_tree.segment.rsc +2 -2
  46. package/.next/server/app/login.segments/login/__PAGE__.segment.rsc +2 -2
  47. package/.next/server/app/login.segments/login.segment.rsc +1 -1
  48. package/.next/server/app/logs/page_client-reference-manifest.js +1 -1
  49. package/.next/server/app/logs.html +2 -2
  50. package/.next/server/app/logs.rsc +2 -2
  51. package/.next/server/app/logs.segments/_full.segment.rsc +2 -2
  52. package/.next/server/app/logs.segments/_head.segment.rsc +1 -1
  53. package/.next/server/app/logs.segments/_index.segment.rsc +2 -2
  54. package/.next/server/app/logs.segments/_tree.segment.rsc +2 -2
  55. package/.next/server/app/logs.segments/logs/__PAGE__.segment.rsc +1 -1
  56. package/.next/server/app/logs.segments/logs.segment.rsc +1 -1
  57. package/.next/server/app/messages/page_client-reference-manifest.js +1 -1
  58. package/.next/server/app/messages.html +2 -2
  59. package/.next/server/app/messages.rsc +3 -3
  60. package/.next/server/app/messages.segments/_full.segment.rsc +3 -3
  61. package/.next/server/app/messages.segments/_head.segment.rsc +1 -1
  62. package/.next/server/app/messages.segments/_index.segment.rsc +2 -2
  63. package/.next/server/app/messages.segments/_tree.segment.rsc +2 -2
  64. package/.next/server/app/messages.segments/messages/__PAGE__.segment.rsc +2 -2
  65. package/.next/server/app/messages.segments/messages.segment.rsc +1 -1
  66. package/.next/server/app/node/page_client-reference-manifest.js +1 -1
  67. package/.next/server/app/node.html +2 -2
  68. package/.next/server/app/node.rsc +2 -2
  69. package/.next/server/app/node.segments/_full.segment.rsc +2 -2
  70. package/.next/server/app/node.segments/_head.segment.rsc +1 -1
  71. package/.next/server/app/node.segments/_index.segment.rsc +2 -2
  72. package/.next/server/app/node.segments/_tree.segment.rsc +2 -2
  73. package/.next/server/app/node.segments/node/__PAGE__.segment.rsc +1 -1
  74. package/.next/server/app/node.segments/node.segment.rsc +1 -1
  75. package/.next/server/app/nodes/page_client-reference-manifest.js +1 -1
  76. package/.next/server/app/nodes.html +2 -2
  77. package/.next/server/app/nodes.rsc +3 -3
  78. package/.next/server/app/nodes.segments/_full.segment.rsc +3 -3
  79. package/.next/server/app/nodes.segments/_head.segment.rsc +1 -1
  80. package/.next/server/app/nodes.segments/_index.segment.rsc +2 -2
  81. package/.next/server/app/nodes.segments/_tree.segment.rsc +2 -2
  82. package/.next/server/app/nodes.segments/nodes/__PAGE__.segment.rsc +2 -2
  83. package/.next/server/app/nodes.segments/nodes.segment.rsc +1 -1
  84. package/.next/server/app/page_client-reference-manifest.js +1 -1
  85. package/.next/server/app/server-logs/page_client-reference-manifest.js +1 -1
  86. package/.next/server/app/server-logs.html +2 -2
  87. package/.next/server/app/server-logs.rsc +2 -2
  88. package/.next/server/app/server-logs.segments/_full.segment.rsc +2 -2
  89. package/.next/server/app/server-logs.segments/_head.segment.rsc +1 -1
  90. package/.next/server/app/server-logs.segments/_index.segment.rsc +2 -2
  91. package/.next/server/app/server-logs.segments/_tree.segment.rsc +2 -2
  92. package/.next/server/app/server-logs.segments/server-logs/__PAGE__.segment.rsc +1 -1
  93. package/.next/server/app/server-logs.segments/server-logs.segment.rsc +1 -1
  94. package/.next/server/app/servers/page_client-reference-manifest.js +1 -1
  95. package/.next/server/app/servers.html +2 -2
  96. package/.next/server/app/servers.rsc +2 -2
  97. package/.next/server/app/servers.segments/_full.segment.rsc +2 -2
  98. package/.next/server/app/servers.segments/_head.segment.rsc +1 -1
  99. package/.next/server/app/servers.segments/_index.segment.rsc +2 -2
  100. package/.next/server/app/servers.segments/_tree.segment.rsc +2 -2
  101. package/.next/server/app/servers.segments/servers/__PAGE__.segment.rsc +1 -1
  102. package/.next/server/app/servers.segments/servers.segment.rsc +1 -1
  103. package/.next/server/app/settings/networks/page_client-reference-manifest.js +1 -1
  104. package/.next/server/app/settings/networks.html +2 -2
  105. package/.next/server/app/settings/networks.rsc +2 -2
  106. package/.next/server/app/settings/networks.segments/_full.segment.rsc +2 -2
  107. package/.next/server/app/settings/networks.segments/_head.segment.rsc +1 -1
  108. package/.next/server/app/settings/networks.segments/_index.segment.rsc +2 -2
  109. package/.next/server/app/settings/networks.segments/_tree.segment.rsc +2 -2
  110. package/.next/server/app/settings/networks.segments/settings/networks/__PAGE__.segment.rsc +1 -1
  111. package/.next/server/app/settings/networks.segments/settings/networks.segment.rsc +1 -1
  112. package/.next/server/app/settings/networks.segments/settings.segment.rsc +1 -1
  113. package/.next/server/app/settings/page_client-reference-manifest.js +1 -1
  114. package/.next/server/app/settings/tokens/page_client-reference-manifest.js +1 -1
  115. package/.next/server/app/settings/tokens.html +2 -2
  116. package/.next/server/app/settings/tokens.rsc +2 -2
  117. package/.next/server/app/settings/tokens.segments/_full.segment.rsc +2 -2
  118. package/.next/server/app/settings/tokens.segments/_head.segment.rsc +1 -1
  119. package/.next/server/app/settings/tokens.segments/_index.segment.rsc +2 -2
  120. package/.next/server/app/settings/tokens.segments/_tree.segment.rsc +2 -2
  121. package/.next/server/app/settings/tokens.segments/settings/tokens/__PAGE__.segment.rsc +1 -1
  122. package/.next/server/app/settings/tokens.segments/settings/tokens.segment.rsc +1 -1
  123. package/.next/server/app/settings/tokens.segments/settings.segment.rsc +1 -1
  124. package/.next/server/app/settings.html +2 -2
  125. package/.next/server/app/settings.rsc +3 -3
  126. package/.next/server/app/settings.segments/_full.segment.rsc +3 -3
  127. package/.next/server/app/settings.segments/_head.segment.rsc +1 -1
  128. package/.next/server/app/settings.segments/_index.segment.rsc +2 -2
  129. package/.next/server/app/settings.segments/_tree.segment.rsc +2 -2
  130. package/.next/server/app/settings.segments/settings/__PAGE__.segment.rsc +2 -2
  131. package/.next/server/app/settings.segments/settings.segment.rsc +1 -1
  132. package/.next/server/app/tasks/[id]/page_client-reference-manifest.js +1 -1
  133. package/.next/server/app/tasks/page_client-reference-manifest.js +1 -1
  134. package/.next/server/app/tasks.html +2 -2
  135. package/.next/server/app/tasks.rsc +2 -2
  136. package/.next/server/app/tasks.segments/_full.segment.rsc +2 -2
  137. package/.next/server/app/tasks.segments/_head.segment.rsc +1 -1
  138. package/.next/server/app/tasks.segments/_index.segment.rsc +2 -2
  139. package/.next/server/app/tasks.segments/_tree.segment.rsc +2 -2
  140. package/.next/server/app/tasks.segments/tasks/__PAGE__.segment.rsc +1 -1
  141. package/.next/server/app/tasks.segments/tasks.segment.rsc +1 -1
  142. package/.next/server/chunks/[root-of-the-server]__0jg663w._.js +1 -1
  143. package/.next/server/chunks/[root-of-the-server]__0jg663w._.js.map +1 -1
  144. package/.next/server/chunks/ssr/[root-of-the-server]__0-n_oet._.js +1 -1
  145. package/.next/server/chunks/ssr/[root-of-the-server]__0-n_oet._.js.map +1 -1
  146. package/.next/server/chunks/ssr/[root-of-the-server]__0jgx8j4._.js +1 -1
  147. package/.next/server/chunks/ssr/[root-of-the-server]__0jgx8j4._.js.map +1 -1
  148. package/.next/server/chunks/ssr/dashboard-pr4-338_app_0_vh2hn._.js +1 -1
  149. package/.next/server/chunks/ssr/dashboard-pr4-338_app_0_vh2hn._.js.map +1 -1
  150. package/.next/server/chunks/ssr/dashboard-pr4-338_app_0a352ge._.js +1 -1
  151. package/.next/server/chunks/ssr/dashboard-pr4-338_app_0a352ge._.js.map +1 -1
  152. package/.next/server/chunks/ssr/dashboard-pr4-338_app_0k0.-6z._.js +1 -1
  153. package/.next/server/chunks/ssr/dashboard-pr4-338_app_0k0.-6z._.js.map +1 -1
  154. package/.next/server/chunks/ssr/dashboard-pr4-338_app_12jui6l._.js +3 -0
  155. package/.next/server/chunks/ssr/dashboard-pr4-338_app_12jui6l._.js.map +1 -0
  156. package/.next/server/chunks/ssr/dashboard-pr4-338_app_nodes_page_tsx_0.1qp86._.js +2 -2
  157. package/.next/server/chunks/ssr/dashboard-pr4-338_app_nodes_page_tsx_0.1qp86._.js.map +1 -1
  158. package/.next/server/middleware-build-manifest.js +3 -3
  159. package/.next/server/pages/404.html +2 -2
  160. package/.next/server/pages/500.html +1 -1
  161. package/.next/static/chunks/01.7wksc03ge7.js +1 -0
  162. package/.next/static/chunks/{1238uy.r8ebu0.js → 05ludq~vmjrpi.js} +1 -1
  163. package/.next/static/chunks/0nt58h_jnn298.js +2 -0
  164. package/.next/static/chunks/0~qpo2.zfv914.css +1 -0
  165. package/.next/static/chunks/10-herg8p2_u0.js +1 -0
  166. package/.next/static/chunks/{0r4f9a-1h0l8n.js → 1162nvfdeg.tg.js} +2 -2
  167. package/.next/static/chunks/11as~l5x7cdjy.js +1 -0
  168. package/.next/static/chunks/{0dzyz0tl5mmyh.js → 14eb0vbqm-0fq.js} +1 -1
  169. package/.next/trace +2 -2
  170. package/.next/trace-build +1 -1
  171. package/app/api/anet/node-lifecycle/route.ts +54 -5
  172. package/app/components/CreateNodeWizard.tsx +70 -24
  173. package/app/components/HostSupervisorPicker.tsx +6 -3
  174. package/app/components/NodeLifecycleMenu.tsx +436 -0
  175. package/app/components/types.ts +4 -0
  176. package/app/lib/hooks.ts +22 -0
  177. package/app/nodes/page.tsx +42 -2
  178. package/package.json +1 -1
  179. package/.next/server/chunks/ssr/dashboard-pr4-338_app_0l07md.._.js +0 -3
  180. package/.next/server/chunks/ssr/dashboard-pr4-338_app_0l07md.._.js.map +0 -1
  181. package/.next/static/chunks/0okyajjiuijbh.css +0 -1
  182. package/.next/static/chunks/0zezb.fc41oyb.js +0 -1
  183. package/.next/static/chunks/0zta~z4xpak8b.js +0 -2
  184. package/.next/static/chunks/10rif2p3a6hdl.js +0 -1
  185. /package/.next/static/{S9Q_U9UD2ZJQPBZjZgrZy → 3WnbjoWrJbRnQkx4ilgVm}/_buildManifest.js +0 -0
  186. /package/.next/static/{S9Q_U9UD2ZJQPBZjZgrZy → 3WnbjoWrJbRnQkx4ilgVm}/_clientMiddlewareManifest.js +0 -0
  187. /package/.next/static/{S9Q_U9UD2ZJQPBZjZgrZy → 3WnbjoWrJbRnQkx4ilgVm}/_ssgManifest.js +0 -0
@@ -0,0 +1,436 @@
1
+ 'use client';
2
+
3
+ import { useCallback, useEffect, useRef, useState } from 'react';
4
+
5
+ // RFC-027 PR2 — per-node ⋮ lifecycle menu + stop/delete modals.
6
+ //
7
+ // State-aware menu items (RFC-027 §3.1 table):
8
+ // active → 重启 / 停止 / 删除
9
+ // stopping/deleting → loading (all disabled,菜单 surfaces "请稍候")
10
+ // stopped → 启动 / 删除 (停止 灰掉 — already stopped)
11
+ // stop_failed → 启动 / 删除 (warn — operator should investigate)
12
+ //
13
+ // Stop modal: low-friction confirm (RFC-027 §3.3)
14
+ // Delete modal: alias-input confirm + force-on-in-flight checkbox
15
+ // (RFC-027 §3.2; mirrors hub's confirm_alias gate + force semantics).
16
+ //
17
+ // Error propagation per 通信龙 PR2 requirement: structured payloads
18
+ // from hub (node_not_active / cannot_delete_daemon_via_delete_node /
19
+ // node_busy_in_flight / confirm_alias_mismatch / forbidden_cross_tenant)
20
+ // land in `lastError` with their real fields surfaced in the UI — no
21
+ // generic "操作失败".
22
+ //
23
+ // React rules-of-hooks: all hooks declared BEFORE early returns.
24
+
25
+ export type LifecycleState =
26
+ | 'active'
27
+ | 'stopping'
28
+ | 'stopped'
29
+ | 'stop_failed'
30
+ | 'deleting'
31
+ | string; // forward-compat for unknown server values
32
+
33
+ export interface NodeLifecycleMenuProps {
34
+ nodeId: string;
35
+ alias: string;
36
+ lifecycleState: LifecycleState | null | undefined;
37
+ networkId?: string | null;
38
+ /** Called when a lifecycle op completes successfully (caller refreshes nodes list). */
39
+ onCompleted?: () => void;
40
+ }
41
+
42
+ type LifecycleAction = 'restart' | 'stop' | 'delete';
43
+
44
+ type ApiReply = {
45
+ ok?: boolean;
46
+ error?: string;
47
+ message?: string;
48
+ in_flight_count?: number;
49
+ hint?: string;
50
+ lifecycle_state?: string;
51
+ unconfirmed?: boolean;
52
+ [k: string]: unknown;
53
+ };
54
+
55
+ const normalizeState = (s: LifecycleState | null | undefined): LifecycleState =>
56
+ (typeof s === 'string' && s.length > 0 ? s : 'active');
57
+
58
+ async function callLifecycle(
59
+ action: LifecycleAction,
60
+ body: Record<string, unknown>,
61
+ ): Promise<ApiReply & { httpStatus: number }> {
62
+ try {
63
+ const r = await fetch('/api/anet/node-lifecycle', {
64
+ method: 'POST',
65
+ headers: { 'Content-Type': 'application/json' },
66
+ body: JSON.stringify({ action, ...body }),
67
+ });
68
+ const data = (await r.json().catch(() => ({}))) as ApiReply;
69
+ return { ...data, httpStatus: r.status };
70
+ } catch (e: unknown) {
71
+ return {
72
+ ok: false,
73
+ error: 'network_error',
74
+ message: e instanceof Error ? e.message : String(e),
75
+ httpStatus: 0,
76
+ };
77
+ }
78
+ }
79
+
80
+ export function NodeLifecycleMenu({
81
+ nodeId,
82
+ alias,
83
+ lifecycleState,
84
+ networkId,
85
+ onCompleted,
86
+ }: NodeLifecycleMenuProps) {
87
+ // All hooks above any early return (avoid hooks-order regression that
88
+ // bit the mobile app at v0.1.29; 通信龙 explicit call-out for PR2).
89
+ const [menuOpen, setMenuOpen] = useState(false);
90
+ const [stopModal, setStopModal] = useState(false);
91
+ const [deleteModal, setDeleteModal] = useState(false);
92
+ const [restartBusy, setRestartBusy] = useState(false);
93
+ const [lastError, setLastError] = useState<ApiReply | null>(null);
94
+ const menuRef = useRef<HTMLDivElement | null>(null);
95
+
96
+ // Outside-click + Escape close the menu (focus management).
97
+ useEffect(() => {
98
+ if (!menuOpen) return;
99
+ const onDoc = (e: MouseEvent) => {
100
+ if (menuRef.current && !menuRef.current.contains(e.target as Node)) setMenuOpen(false);
101
+ };
102
+ const onKey = (e: KeyboardEvent) => { if (e.key === 'Escape') setMenuOpen(false); };
103
+ document.addEventListener('mousedown', onDoc);
104
+ document.addEventListener('keydown', onKey);
105
+ return () => {
106
+ document.removeEventListener('mousedown', onDoc);
107
+ document.removeEventListener('keydown', onKey);
108
+ };
109
+ }, [menuOpen]);
110
+
111
+ const onRestart = useCallback(async () => {
112
+ setMenuOpen(false);
113
+ setLastError(null);
114
+ setRestartBusy(true);
115
+ const r = await callLifecycle('restart', {
116
+ node_id: nodeId,
117
+ ...(networkId ? { network_id: networkId } : {}),
118
+ });
119
+ setRestartBusy(false);
120
+ if (r.ok) {
121
+ onCompleted?.();
122
+ return;
123
+ }
124
+ setLastError(r);
125
+ }, [nodeId, networkId, onCompleted]);
126
+
127
+ const state = normalizeState(lifecycleState);
128
+ const isLoading = state === 'stopping' || state === 'deleting';
129
+ const isActive = state === 'active';
130
+ const isStopped = state === 'stopped' || state === 'stop_failed';
131
+
132
+ // Menu items per RFC-027 §3.1 table.
133
+ const items: Array<{ label: string; key: string; danger?: boolean; onClick: () => void; disabled?: boolean }> = [];
134
+ if (isLoading) {
135
+ items.push({
136
+ label: state === 'stopping' ? '停止中…' : '删除中…',
137
+ key: 'loading', disabled: true, onClick: () => {},
138
+ });
139
+ } else if (isActive) {
140
+ items.push({ label: restartBusy ? '重启中…' : '重启节点', key: 'restart', disabled: restartBusy, onClick: onRestart });
141
+ items.push({ label: '停止节点', key: 'stop', onClick: () => { setMenuOpen(false); setStopModal(true); } });
142
+ items.push({ label: '删除节点', key: 'delete', danger: true, onClick: () => { setMenuOpen(false); setDeleteModal(true); } });
143
+ } else if (isStopped) {
144
+ // stopped + stop_failed: "启动" = restart_node (hub resets lifecycle_state→active on success per PR1.2a)
145
+ items.push({ label: restartBusy ? '启动中…' : '启动节点', key: 'restart', disabled: restartBusy, onClick: onRestart });
146
+ items.push({ label: '删除节点', key: 'delete', danger: true, onClick: () => { setMenuOpen(false); setDeleteModal(true); } });
147
+ } else {
148
+ // Unknown state — surface read-only badge, no destructive action.
149
+ items.push({
150
+ label: `状态 ${state} (不识别)`,
151
+ key: 'unknown', disabled: true, onClick: () => {},
152
+ });
153
+ }
154
+
155
+ return (
156
+ <div ref={menuRef} className="relative inline-block" onClick={e => e.stopPropagation()}>
157
+ <button
158
+ type="button"
159
+ aria-label={`节点 ${alias} 生命周期操作`}
160
+ aria-haspopup="menu"
161
+ aria-expanded={menuOpen}
162
+ onClick={() => setMenuOpen(v => !v)}
163
+ className="inline-flex h-8 w-8 items-center justify-center rounded-md text-gray-400 hover:bg-white/5 hover:text-white focus:outline-none focus:ring-1 focus:ring-cyan-500/50"
164
+ >
165
+ <svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor" aria-hidden>
166
+ <circle cx="12" cy="5" r="1.6" />
167
+ <circle cx="12" cy="12" r="1.6" />
168
+ <circle cx="12" cy="19" r="1.6" />
169
+ </svg>
170
+ </button>
171
+ {menuOpen && (
172
+ <div role="menu" className="absolute right-0 z-40 mt-1 w-44 overflow-hidden rounded-md border border-[#26262b] bg-[#161618] shadow-xl">
173
+ {items.map(item => (
174
+ <button
175
+ key={item.key}
176
+ type="button"
177
+ role="menuitem"
178
+ disabled={item.disabled}
179
+ onClick={item.onClick}
180
+ className={`block w-full px-3 py-2 text-left text-sm transition-colors ${
181
+ item.disabled
182
+ ? 'text-gray-600 cursor-not-allowed'
183
+ : item.danger
184
+ ? 'text-red-300 hover:bg-red-900/20'
185
+ : 'text-gray-200 hover:bg-white/5'
186
+ }`}
187
+ >
188
+ {item.label}
189
+ </button>
190
+ ))}
191
+ </div>
192
+ )}
193
+
194
+ {/* Last error inline (under the button) — pass-through hub's structured fields */}
195
+ {lastError && lastError.ok === false && !stopModal && !deleteModal && (
196
+ <div className="absolute right-0 z-40 mt-1 w-72 rounded-md border border-red-700/40 bg-red-900/20 px-3 py-2 text-xs text-red-200">
197
+ <div className="flex items-start justify-between gap-2">
198
+ <div className="min-w-0 flex-1">
199
+ <div className="font-semibold">{lastError.error || 'error'}</div>
200
+ {lastError.message ? <div className="mt-0.5 text-red-300/80">{lastError.message}</div> : null}
201
+ {typeof lastError.in_flight_count === 'number' && (
202
+ <div className="mt-0.5 text-red-300/80">in-flight: {lastError.in_flight_count}</div>
203
+ )}
204
+ {lastError.hint ? <div className="mt-1 text-red-300/60">{lastError.hint}</div> : null}
205
+ </div>
206
+ <button type="button" onClick={() => setLastError(null)} className="shrink-0 text-red-400 hover:text-red-200" aria-label="关闭">×</button>
207
+ </div>
208
+ </div>
209
+ )}
210
+
211
+ {stopModal && (
212
+ <StopNodeModal
213
+ alias={alias}
214
+ nodeId={nodeId}
215
+ networkId={networkId}
216
+ onClose={() => setStopModal(false)}
217
+ onCompleted={() => { setStopModal(false); onCompleted?.(); }}
218
+ />
219
+ )}
220
+ {deleteModal && (
221
+ <DeleteNodeModal
222
+ alias={alias}
223
+ nodeId={nodeId}
224
+ networkId={networkId}
225
+ onClose={() => setDeleteModal(false)}
226
+ onCompleted={() => { setDeleteModal(false); onCompleted?.(); }}
227
+ />
228
+ )}
229
+ </div>
230
+ );
231
+ }
232
+
233
+ // ──────────────────────────────────────────────────────────────────────
234
+
235
+ interface ModalProps {
236
+ alias: string;
237
+ nodeId: string;
238
+ networkId?: string | null;
239
+ onClose: () => void;
240
+ onCompleted: () => void;
241
+ }
242
+
243
+ function StopNodeModal({ alias, nodeId, networkId, onClose, onCompleted }: ModalProps) {
244
+ const [busy, setBusy] = useState(false);
245
+ const [force, setForce] = useState(false);
246
+ const [reply, setReply] = useState<ApiReply | null>(null);
247
+
248
+ useEffect(() => {
249
+ const onKey = (e: KeyboardEvent) => { if (e.key === 'Escape' && !busy) onClose(); };
250
+ window.addEventListener('keydown', onKey);
251
+ return () => window.removeEventListener('keydown', onKey);
252
+ }, [onClose, busy]);
253
+
254
+ const inFlight = reply && typeof reply.in_flight_count === 'number' ? reply.in_flight_count : 0;
255
+
256
+ const onConfirm = async () => {
257
+ setBusy(true);
258
+ setReply(null);
259
+ const r = await callLifecycle('stop', {
260
+ node_id: nodeId,
261
+ ...(networkId ? { network_id: networkId } : {}),
262
+ ...(force ? { force: true } : {}),
263
+ });
264
+ setBusy(false);
265
+ if (r.ok) { onCompleted(); return; }
266
+ setReply(r);
267
+ };
268
+
269
+ return (
270
+ <>
271
+ <div className="fixed inset-0 z-40 bg-black/50" onClick={busy ? undefined : onClose} aria-hidden />
272
+ <div role="dialog" aria-label={`停止节点 ${alias}`} className="fixed left-1/2 top-1/2 z-50 w-[min(92vw,420px)] -translate-x-1/2 -translate-y-1/2 rounded-xl border border-[#26262b] bg-[#161618] shadow-2xl">
273
+ <div className="flex items-center justify-between border-b border-[#26262b] px-4 py-3">
274
+ <div className="text-sm font-semibold text-white">停止节点</div>
275
+ <button onClick={busy ? undefined : onClose} aria-label="关闭" className="text-gray-500 hover:text-white">×</button>
276
+ </div>
277
+ <div className="space-y-3 px-4 py-4">
278
+ <div className="text-sm text-gray-300">
279
+ 将停止 <span className="font-mono text-cyan-300">{alias}</span> 子进程(SIGTERM,~10s 内)。配置目录保留,可通过「启动」恢复。
280
+ </div>
281
+ {/* Surface hub's structured payload field-by-field; never collapse to generic */}
282
+ {reply && reply.ok === false && (
283
+ <div className="rounded-md border border-red-700/40 bg-red-900/20 px-3 py-2 text-xs text-red-200">
284
+ <div className="font-semibold">{reply.error}</div>
285
+ {reply.message ? <div className="mt-0.5 text-red-300/80">{reply.message}</div> : null}
286
+ {typeof reply.in_flight_count === 'number' && (
287
+ <div className="mt-0.5 text-red-300/80">
288
+ 当前 in-flight 任务: {reply.in_flight_count}
289
+ </div>
290
+ )}
291
+ {reply.hint ? <div className="mt-1 text-red-300/60">{reply.hint}</div> : null}
292
+ </div>
293
+ )}
294
+ {(inFlight > 0 || force) && (
295
+ <label className="flex items-start gap-2 rounded-md border border-amber-700/40 bg-amber-900/10 px-3 py-2 text-xs text-amber-200">
296
+ <input
297
+ type="checkbox"
298
+ checked={force}
299
+ onChange={e => setForce(e.target.checked)}
300
+ className="mt-0.5"
301
+ />
302
+ <span>
303
+ <span className="font-semibold text-amber-300">强制停止</span>{inFlight > 0 ? `(会丢弃 ${inFlight} 条待处理任务)` : '(覆盖 in-flight 拒绝;hub 会写 audit 行)'}
304
+ </span>
305
+ </label>
306
+ )}
307
+ </div>
308
+ <div className="flex items-center justify-end gap-2 border-t border-[#26262b] px-4 py-3">
309
+ <button type="button" disabled={busy} onClick={onClose} className="rounded-md px-3 py-1.5 text-sm text-gray-400 hover:text-gray-200 disabled:opacity-50">取消</button>
310
+ <button
311
+ type="button"
312
+ disabled={busy || (inFlight > 0 && !force)}
313
+ onClick={onConfirm}
314
+ className="rounded-md bg-cyan-600 px-4 py-1.5 text-sm font-medium text-white hover:bg-cyan-500 disabled:cursor-not-allowed disabled:opacity-50"
315
+ >
316
+ {busy ? '停止中…' : '确认停止'}
317
+ </button>
318
+ </div>
319
+ </div>
320
+ </>
321
+ );
322
+ }
323
+
324
+ function DeleteNodeModal({ alias, nodeId, networkId, onClose, onCompleted }: ModalProps) {
325
+ const [busy, setBusy] = useState(false);
326
+ const [confirmInput, setConfirmInput] = useState('');
327
+ const [force, setForce] = useState(false);
328
+ const [keepConfig, setKeepConfig] = useState(false);
329
+ const [reply, setReply] = useState<ApiReply | null>(null);
330
+
331
+ useEffect(() => {
332
+ const onKey = (e: KeyboardEvent) => { if (e.key === 'Escape' && !busy) onClose(); };
333
+ window.addEventListener('keydown', onKey);
334
+ return () => window.removeEventListener('keydown', onKey);
335
+ }, [onClose, busy]);
336
+
337
+ const inFlight = reply && typeof reply.in_flight_count === 'number' ? reply.in_flight_count : 0;
338
+ const confirmValid = confirmInput === alias;
339
+
340
+ const onConfirm = async () => {
341
+ if (!confirmValid) return;
342
+ setBusy(true);
343
+ setReply(null);
344
+ const r = await callLifecycle('delete', {
345
+ node_id: nodeId,
346
+ confirm_alias: confirmInput,
347
+ ...(networkId ? { network_id: networkId } : {}),
348
+ ...(force ? { force: true } : {}),
349
+ ...(keepConfig ? { delete_config: false } : {}),
350
+ });
351
+ setBusy(false);
352
+ if (r.ok) { onCompleted(); return; }
353
+ setReply(r);
354
+ };
355
+
356
+ return (
357
+ <>
358
+ <div className="fixed inset-0 z-40 bg-black/50" onClick={busy ? undefined : onClose} aria-hidden />
359
+ <div role="dialog" aria-label={`删除节点 ${alias}`} className="fixed left-1/2 top-1/2 z-50 w-[min(94vw,480px)] -translate-x-1/2 -translate-y-1/2 rounded-xl border border-red-700/40 bg-[#161618] shadow-2xl">
360
+ <div className="flex items-center justify-between border-b border-[#26262b] px-4 py-3">
361
+ <div className="text-sm font-semibold text-red-300">删除节点</div>
362
+ <button onClick={busy ? undefined : onClose} aria-label="关闭" className="text-gray-500 hover:text-white">×</button>
363
+ </div>
364
+ <div className="space-y-3 px-4 py-4 text-sm text-gray-300">
365
+ <div>这将:</div>
366
+ <ul className="list-inside list-disc space-y-1 pl-2 text-xs text-gray-400">
367
+ <li>停止 <span className="font-mono text-cyan-300">{alias}</span> 子进程(SIGTERM ~10s)</li>
368
+ <li>从 hub 注销该节点</li>
369
+ <li>撤销该节点的 ntok</li>
370
+ {!keepConfig && <li>配置文件备份到 <code className="text-amber-300">~/.anet/deleted/&lt;ts&gt;-{alias}/</code>(30 天后自动清除)</li>}
371
+ {keepConfig && <li className="text-amber-300">配置文件保留原地(不备份不清理 — 你勾选了「保留配置」)</li>}
372
+ </ul>
373
+
374
+ {/* Surface hub's structured payload field-by-field */}
375
+ {reply && reply.ok === false && (
376
+ <div className="rounded-md border border-red-700/40 bg-red-900/20 px-3 py-2 text-xs text-red-200">
377
+ <div className="font-semibold">{reply.error}</div>
378
+ {reply.message ? <div className="mt-0.5 text-red-300/80">{reply.message}</div> : null}
379
+ {typeof reply.in_flight_count === 'number' && (
380
+ <div className="mt-0.5 text-red-300/80">当前 in-flight 任务: {reply.in_flight_count}</div>
381
+ )}
382
+ {reply.hint ? <div className="mt-1 text-red-300/60">{reply.hint}</div> : null}
383
+ </div>
384
+ )}
385
+
386
+ {(inFlight > 0 || force) && (
387
+ <label className="flex items-start gap-2 rounded-md border border-amber-700/40 bg-amber-900/10 px-3 py-2 text-xs text-amber-200">
388
+ <input
389
+ type="checkbox"
390
+ checked={force}
391
+ onChange={e => setForce(e.target.checked)}
392
+ className="mt-0.5"
393
+ />
394
+ <span>
395
+ <span className="font-semibold text-amber-300">强制删除</span>{inFlight > 0 ? `(会丢弃 ${inFlight} 条待处理任务)` : '(覆盖 in-flight 拒绝)'}
396
+ </span>
397
+ </label>
398
+ )}
399
+
400
+ <label className="flex items-center gap-2 text-xs text-gray-400">
401
+ <input type="checkbox" checked={keepConfig} onChange={e => setKeepConfig(e.target.checked)} />
402
+ 保留配置目录(不备份;高级用法)
403
+ </label>
404
+
405
+ <label className="block space-y-1">
406
+ <span className="text-xs text-gray-400">输入节点名字 <span className="font-mono text-cyan-300">{alias}</span> 确认</span>
407
+ <input
408
+ autoFocus
409
+ value={confirmInput}
410
+ onChange={e => setConfirmInput(e.target.value)}
411
+ placeholder={alias}
412
+ className="w-full rounded-md border border-[#26262b] bg-[#0e0e10] px-3 py-2 text-sm text-gray-200 focus:border-red-500 focus:outline-none"
413
+ autoCapitalize="none"
414
+ autoCorrect="off"
415
+ spellCheck={false}
416
+ />
417
+ {confirmInput && !confirmValid && (
418
+ <span className="text-[11px] text-red-400">名字不匹配(区分大小写)</span>
419
+ )}
420
+ </label>
421
+ </div>
422
+ <div className="flex items-center justify-end gap-2 border-t border-[#26262b] px-4 py-3">
423
+ <button type="button" disabled={busy} onClick={onClose} className="rounded-md px-3 py-1.5 text-sm text-gray-400 hover:text-gray-200 disabled:opacity-50">取消</button>
424
+ <button
425
+ type="button"
426
+ disabled={busy || !confirmValid || (inFlight > 0 && !force)}
427
+ onClick={onConfirm}
428
+ className="rounded-md bg-red-600 px-4 py-1.5 text-sm font-medium text-white hover:bg-red-500 disabled:cursor-not-allowed disabled:opacity-50"
429
+ >
430
+ {busy ? '删除中…' : `删除 ${alias}`}
431
+ </button>
432
+ </div>
433
+ </div>
434
+ </>
435
+ );
436
+ }
@@ -19,6 +19,10 @@ export interface Session {
19
19
  // commhub-server 0.8.1-preview.0; null on older nodes / older hubs.
20
20
  model?: string | null;
21
21
  runtime?: string | null;
22
+ // RFC-027 PR2 — lifecycle_state. Sourced from /api/nodes (preview.12+);
23
+ // merged into Session client-side via useNodeLifecycle hook. Absent →
24
+ // treated as 'active' so older sessions/hubs don't break the menu.
25
+ lifecycle_state?: string | null;
22
26
  }
23
27
 
24
28
  export interface Health {
package/app/lib/hooks.ts CHANGED
@@ -30,6 +30,28 @@ export function useSessions() {
30
30
  return { sessions: data?.sessions || [], hint: data?._hint, error, isLoading };
31
31
  }
32
32
 
33
+ // RFC-027 PR2 — alias → lifecycle_state map sourced from /api/nodes
34
+ // (commhub-server preview.12+). /api/status reads from `sessions` which
35
+ // doesn't carry lifecycle_state; /api/nodes does. Cheap secondary
36
+ // fetch, same SWR cadence as useSessions; merged client-side by alias.
37
+ // Absent alias → caller defaults to 'active'.
38
+ interface NodesResp { nodes?: Array<{ alias?: string; node_id?: string; lifecycle_state?: string | null }>; }
39
+ export function useNodeLifecycle() {
40
+ const { networkId } = useNetworkId();
41
+ const { data, error } = useSWR<NodesResp>(
42
+ withNetwork('/api/hub/nodes', networkId),
43
+ fetcher,
44
+ SWR_OPTIONS,
45
+ );
46
+ const lifecycleByAlias: Record<string, string> = {};
47
+ const nodeIdByAlias: Record<string, string> = {};
48
+ for (const n of data?.nodes ?? []) {
49
+ if (n.alias && typeof n.lifecycle_state === 'string') lifecycleByAlias[n.alias] = n.lifecycle_state;
50
+ if (n.alias && typeof n.node_id === 'string') nodeIdByAlias[n.alias] = n.node_id;
51
+ }
52
+ return { lifecycleByAlias, nodeIdByAlias, error };
53
+ }
54
+
33
55
  export function useHealth() {
34
56
  const { data, error } = useSWR<Health>('/api/hub/health', fetcher, SWR_OPTIONS);
35
57
  return { health: data || null, error };
@@ -2,9 +2,11 @@
2
2
 
3
3
  import { useState } from 'react';
4
4
  import { timeAgo } from '../components/utils';
5
- import { useSessions, useHealth } from '../lib/hooks';
5
+ import { useSessions, useHealth, useNodeLifecycle } from '../lib/hooks';
6
+ import { useSWRConfig } from 'swr';
6
7
  import { TaskChatPanel } from '../components/TaskChatPanel';
7
8
  import { CreateNodeWizard } from '../components/CreateNodeWizard';
9
+ import { NodeLifecycleMenu } from '../components/NodeLifecycleMenu';
8
10
  import { EmptyState, NodesEmptyState } from '../components/EmptyState';
9
11
  import { AliasAvatar } from '../components/AliasAvatar';
10
12
  import { useCollapsibleSearch } from '../components/CollapsibleSearch';
@@ -26,7 +28,16 @@ type SessionRow = Session & { online: boolean };
26
28
  export default function NodesPage() {
27
29
  const { sessions, isLoading: loading } = useSessions();
28
30
  const { health } = useHealth();
31
+ const { lifecycleByAlias, nodeIdByAlias } = useNodeLifecycle();
29
32
  const { hasUnread } = useChatUnread();
33
+ const { mutate } = useSWRConfig();
34
+ // RFC-027 PR2 — after a stop/delete/restart dispatch lands, refresh
35
+ // /api/hub/nodes (lifecycle_state) AND /api/hub/status (sessions).
36
+ // Both feed the menu's state-aware items; otherwise menu stays
37
+ // stale until next 5s SWR poll.
38
+ const refreshNodes = () => {
39
+ mutate((k: unknown) => typeof k === 'string' && (k.startsWith('/api/hub/nodes') || k.startsWith('/api/hub/status')));
40
+ };
30
41
  const sseMap = health?.sse_sessions || {};
31
42
  // SSE keys are `network_id:alias` since server v0.7+. Look up with the
32
43
  // composite key first, fall back to alias-only for legacy hubs.
@@ -215,6 +226,16 @@ export default function NodesPage() {
215
226
  </div>
216
227
  </div>
217
228
  <span className="hidden shrink-0 rounded-lg border border-cyan-500/15 bg-cyan-500/5 px-2 py-1 text-[10px] text-cyan-300/70 sm:inline">Tap to chat</span>
229
+ {/* RFC-027 PR2 — per-node ⋮ lifecycle menu. The button stopsPropagation
230
+ internally so it doesn't open chat. lifecycleByAlias absent → 'active'
231
+ default, menu renders normal stop/delete options. */}
232
+ <NodeLifecycleMenu
233
+ nodeId={nodeIdByAlias[s.alias] || s.node_id || s.alias}
234
+ alias={s.alias}
235
+ lifecycleState={lifecycleByAlias[s.alias]}
236
+ networkId={s.network_id || null}
237
+ onCompleted={refreshNodes}
238
+ />
218
239
  </div>
219
240
 
220
241
  <div className="mt-3 rounded-lg border border-[#1c1c1f] bg-[#0e0e10] px-3 py-2 text-xs">
@@ -282,7 +303,18 @@ export default function NodesPage() {
282
303
  <span className="truncate">{s.agent || '--'}<span className="text-gray-700 mx-1.5">·</span>{shortServer(s.server)}</span>
283
304
  </div>
284
305
  <div className="col-span-4 truncate text-xs text-gray-500" title={s.task || ''}>{s.task || '--'}</div>
285
- <div className="col-span-1 text-xs text-gray-500">{timeAgo(s.last_seen_at || s.updated_at)}</div>
306
+ <div className="col-span-1 flex items-center justify-between gap-2 text-xs text-gray-500">
307
+ <span className="truncate">{timeAgo(s.last_seen_at || s.updated_at)}</span>
308
+ {/* RFC-027 PR2 ⋮ lifecycle menu — desktop list. Defaults
309
+ to 'active' when lifecycleByAlias absent. */}
310
+ <NodeLifecycleMenu
311
+ nodeId={nodeIdByAlias[s.alias] || s.node_id || s.alias}
312
+ alias={s.alias}
313
+ lifecycleState={lifecycleByAlias[s.alias]}
314
+ networkId={s.network_id || null}
315
+ onCompleted={refreshNodes}
316
+ />
317
+ </div>
286
318
  </div>
287
319
  {/* R7 of #190: mobile node row was ~340px tall × ~150
288
320
  rows = the 51k page. Wins this round, in priority
@@ -307,6 +339,14 @@ export default function NodesPage() {
307
339
  <span className={`shrink-0 text-xs px-2 py-0.5 rounded-md border ${STATUS_COLORS[statusKey] || STATUS_COLORS.offline}`}>
308
340
  {statusKey}
309
341
  </span>
342
+ {/* RFC-027 PR2 ⋮ lifecycle menu — mobile list. */}
343
+ <NodeLifecycleMenu
344
+ nodeId={nodeIdByAlias[s.alias] || s.node_id || s.alias}
345
+ alias={s.alias}
346
+ lifecycleState={lifecycleByAlias[s.alias]}
347
+ networkId={s.network_id || null}
348
+ onCompleted={refreshNodes}
349
+ />
310
350
  </div>
311
351
  {s.task && <div className="truncate text-xs text-gray-500">{s.task}</div>}
312
352
  </div>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sleep2agi/agent-network-dashboard",
3
- "version": "0.6.3-preview.3",
3
+ "version": "0.6.3-preview.4",
4
4
  "description": "Agent Network Dashboard — Web UI for managing AI Agent networks",
5
5
  "main": "apps/desktop/electron/main.cjs",
6
6
  "scripts": {
@@ -1,3 +0,0 @@
1
- module.exports=[42221,a=>{"use strict";var b=a.i(12453);a.s(["previewContent",0,function(a){return a&&a.replace(/!\[([^\]]*)\]\([^)]*\)/g,"$1").replace(/\[([^\]]+)\]\([^)]*\)/g,"$1").replace(/`([^`]+)`/g,"$1").replace(/\s+/g," ").trim()||"--"},"timeAgo",0,function(a){return(0,b.relativeAgo)(a)??"--"}])},70049,a=>{"use strict";var b=a.i(51653);let c=[180,200,220,270,300,330,30,90];function d(a){let b=0;for(let c=0;c<a.length;c++)b=31*b+a.charCodeAt(c)>>>0;let d=c[b%c.length];return{bg:`hsl(${d} 55% 22%)`,ring:`hsl(${d} 60% 45%)`,text:`hsl(${d} 80% 78%)`}}function e(a){return a?(a.trim().match(/[\p{L}\p{N}]/u)?.[0]||a.trim()[0]||"·").toUpperCase():"·"}a.s(["AliasAvatar",0,function({alias:a,size:c=28,className:f=""}){if(/\bgrok\b|grok-build|grok测试员|grok-demo/i.test(a))return(0,b.jsx)("span",{className:`anet-alias-avatar inline-flex items-center justify-center rounded-full border border-emerald-500/45 bg-emerald-950/70 shrink-0 ${f}`,style:{width:c,height:c,backgroundImage:"url(/vendors/grok.svg)",backgroundPosition:"center",backgroundRepeat:"no-repeat",backgroundSize:"68% 68%"},title:a,"aria-hidden":!0});let g=d(a),h=Math.max(9,Math.round(.42*c));return(0,b.jsx)("span",{className:`anet-alias-avatar inline-flex items-center justify-center rounded-full border shrink-0 font-semibold ${f}`,style:{width:c,height:c,fontSize:h,backgroundColor:g.bg,borderColor:g.ring,color:g.text},title:a,"aria-hidden":!0,children:e(a)})},"aliasAvatarColors",0,d,"aliasInitial",0,e])},28644,a=>{"use strict";var b=a.i(67784),c=a.i(436);let d=async a=>{let b=await fetch(a);if(401===b.status)throw window.location.assign("/login"),Error("unauthorized");return b.json()},e={refreshInterval:5e3,dedupingInterval:3e3};function f(a,b){if(!b)return a;let c=a.includes("?")?"&":"?";return`${a}${c}network_id=${encodeURIComponent(b)}`}a.s(["useAnetConfig",0,function(){let{data:a}=(0,b.default)("/api/anet/config",d,{refreshInterval:3e4});return{config:a||null}},"useHealth",0,function(){let{data:a,error:c}=(0,b.default)("/api/hub/health",d,e);return{health:a||null,error:c}},"useMessages",0,function(a=100){let{networkId:g}=(0,c.useNetworkId)(),{data:h,error:i,isLoading:j}=(0,b.default)(f(`/api/hub/messages?limit=${a}`,g),d,{...e,keepPreviousData:!0});return{messages:h?.messages||[],error:i,isLoading:j}},"useSessions",0,function(){let{networkId:a}=(0,c.useNetworkId)(),{data:g,error:h,isLoading:i}=(0,b.default)(f("/api/hub/status",a),d,e);return{sessions:g?.sessions||[],hint:g?._hint,error:h,isLoading:i}},"useStats",0,function(){let{networkId:a}=(0,c.useNetworkId)(),{data:g,error:h}=(0,b.default)(f("/api/hub/stats",a),d,e);return{stats:g?.ok?g:null,error:h}},"useTasks",0,function(a){let{networkId:g}=(0,c.useNetworkId)(),h=new URLSearchParams({limit:"100",...a}).toString(),{data:i,error:j,isLoading:k}=(0,b.default)(f(`/api/hub/tasks?${h}`,g),d,e);return{tasks:i?.tasks||[],count:i?.count??0,source:i?.source,error:j,isLoading:k}}])},30396,a=>{"use strict";a.s(["SESSION_STATUS_CHIP_CLASS",0,{working:"bg-green-500/10 text-green-300 border-green-500/20",idle:"bg-gray-500/10 text-gray-300 border-gray-500/20",blocked:"bg-yellow-500/10 text-yellow-300 border-yellow-500/20",error:"bg-red-500/10 text-red-300 border-red-500/20",offline:"bg-gray-500/10 text-gray-500 border-gray-500/20"},"SESSION_STATUS_TEXT_CLASS",0,{working:"text-green-400",idle:"text-gray-400",blocked:"text-yellow-400",error:"text-red-400",offline:"text-gray-500"},"STATUS_BAR_CLASS",0,{created:"bg-gray-500",delivered:"bg-gray-400",acked:"bg-gray-300",running:"bg-green-500",replied:"bg-gray-400",closed:"bg-gray-600",failed:"bg-red-500",cancelled:"bg-gray-600",expired:"bg-gray-600"},"STATUS_CHIP_CLASS",0,{created:"bg-gray-500/10 text-gray-400 border-gray-500/20",delivered:"bg-gray-500/10 text-gray-300 border-gray-500/20",acked:"bg-gray-500/10 text-gray-300 border-gray-500/20",running:"bg-green-500/10 text-green-300 border-green-500/20",replied:"bg-gray-500/10 text-gray-300 border-gray-500/20",closed:"bg-gray-500/10 text-gray-500 border-gray-500/20",failed:"bg-red-500/10 text-red-300 border-red-500/20",cancelled:"bg-gray-500/10 text-gray-500 border-gray-500/20",expired:"bg-gray-500/10 text-gray-500 border-gray-500/20"},"STATUS_DOT_HEX",0,{created:"#9ca3af",delivered:"#9ca3af",acked:"#9ca3af",running:"#4ade80",replied:"#9ca3af",closed:"#6b7280",failed:"#f87171",cancelled:"#6b7280",expired:"#6b7280"},"TASK_STATUSES",0,["created","delivered","acked","running","replied","closed","failed","cancelled","expired"]])},16727,a=>{"use strict";var b=a.i(51653),c=a.i(85895),d=a.i(73391),e=a.i(28644),f=a.i(42221),g=a.i(70049),h=a.i(30396);a.s(["default",0,function(){let a,{sessions:i}=(0,e.useSessions)(),{health:j}=(0,e.useHealth)(),{stats:k}=(0,e.useStats)(),l=j?.sse_sessions||{},m=a=>(a.network_id?l[`${a.network_id}:${a.alias}`]:void 0)??l[a.alias],[n,o]=(0,c.useState)(""),[p,q]=(0,c.useState)(""),[r,s]=(0,c.useState)(""),[t,u]=(0,c.useState)(""),[v,w]=(0,c.useState)(""),[x,y]=(0,c.useState)(""),[z,A]=(0,c.useState)(""),[B,C]=(0,c.useState)(""),D=async()=>{if(n.trim()&&p.trim()){try{let a=await fetch("/api/hub/auth",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({action:"register",username:n,password:p})}),b=await a.json();s(b.ok?`Created: ${b.user.username} (${b.user.user_id})`:`Failed: ${b.error}`),b.ok&&(o(""),q(""))}catch{s("Failed")}setTimeout(()=>s(""),8e3)}},E=async()=>{if(t.trim()){try{let a=await fetch("/api/hub/broadcast",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({message:t})}),b=await a.json();w(b.ok?`Sent to ${b.recipients} nodes`:`Failed: ${b.error}`),b.ok&&u("")}catch{w("Failed")}setTimeout(()=>w(""),5e3)}},F=async()=>{if(x&&z.trim()){try{let a=await fetch("/api/hub/send",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({alias:x,task:z})}),b=await a.json();C(b.ok?"Task sent":`Failed: ${b.error}`),b.ok&&A("")}catch{C("Failed")}setTimeout(()=>C(""),5e3)}},G=i.filter(a=>m(a)),H=i.filter(a=>!m(a));return(0,b.jsxs)("div",{className:"min-h-screen bg-[#0b0b0d] text-gray-100 p-4 sm:p-6",children:[(0,b.jsx)("h1",{className:"text-2xl font-bold text-white mb-3 lg:ml-0 ml-10",children:"Admin"}),(0,b.jsx)("nav",{className:"mb-4 sm:mb-8 flex flex-wrap gap-2 text-xs",children:[{href:"#status",label:"Status"},{href:"#actions",label:"Actions"},{href:"#users",label:"Users"}].map(a=>(0,b.jsx)("a",{href:a.href,className:"inline-flex min-h-[44px] items-center rounded-md border border-[#26262b] bg-[#0e0e10]/60 px-3 py-2 text-gray-400 hover:border-cyan-500/50 hover:text-cyan-300 hover:bg-cyan-500/10 transition-colors",children:a.label},a.href))}),(0,b.jsxs)("div",{className:"max-w-6xl space-y-10",children:[(0,b.jsxs)("div",{id:"status",className:"space-y-4 scroll-mt-6",children:[(0,b.jsxs)("div",{className:"flex items-center gap-2 px-1",children:[(0,b.jsx)("div",{className:"text-[11px] uppercase tracking-[0.14em] text-gray-400 font-semibold",children:"Status"}),(0,b.jsx)("div",{className:"flex-1 h-px bg-[#26262b]"})]}),(0,b.jsxs)("div",{className:"grid grid-cols-1 lg:grid-cols-2 gap-4",children:[(0,b.jsxs)("section",{className:"bg-[#161618] border border-[#26262b] rounded-xl p-4 sm:p-5",children:[(0,b.jsx)("h2",{className:"text-sm font-semibold text-gray-300 mb-3 sm:mb-4",children:"Server Overview"}),(0,b.jsxs)("div",{className:"grid grid-cols-2 gap-4 text-sm",children:[(0,b.jsxs)("div",{children:[(0,b.jsx)("div",{className:"text-2xl sm:text-3xl font-bold text-green-400 tabular-nums",children:G.length}),(0,b.jsx)("div",{className:"text-xs text-gray-500",children:"Online"})]}),(0,b.jsxs)("div",{children:[(0,b.jsx)("div",{className:"text-2xl sm:text-3xl font-bold text-gray-500 tabular-nums",children:H.length}),(0,b.jsx)("div",{className:"text-xs text-gray-500",children:"Offline"})]}),(0,b.jsxs)("div",{children:[(0,b.jsx)("div",{className:"text-2xl sm:text-3xl font-bold text-cyan-400 tabular-nums",children:j?.sse_connections??"--"}),(0,b.jsxs)("div",{className:"text-xs text-gray-500",children:["SSE Streams ",(0,b.jsx)("span",{className:"text-gray-600",children:"· server"})]})]}),(0,b.jsxs)("div",{children:[(0,b.jsx)("div",{className:"text-2xl sm:text-3xl font-bold text-white tabular-nums",children:k?.tasks?.total??"--"}),(0,b.jsx)("div",{className:"text-xs text-gray-500",children:"Total Tasks"})]})]}),k?.tasks?.by_status&&k.tasks.by_status.length>0&&(a=Object.fromEntries(k.tasks.by_status.map(a=>[a.status,a.count])),(0,b.jsxs)("div",{className:"mt-4 flex items-center justify-between border-t border-[#26262b] pt-3 text-xs",children:[(0,b.jsxs)("div",{className:"text-gray-400 tabular-nums",children:[(0,b.jsxs)("span",{className:a.running?"text-green-400":"text-gray-500",children:[a.running||0," running"]}),(0,b.jsx)("span",{className:"text-gray-600",children:" · "}),(0,b.jsxs)("span",{className:a.failed?"text-red-400":"text-gray-500",children:[a.failed||0," failed"]})]}),(0,b.jsx)(d.default,{href:"/tasks",prefetch:!1,className:"text-cyan-400 hover:text-cyan-300",children:"View all →"})]}))]}),(0,b.jsxs)("section",{className:"bg-[#161618] border border-[#26262b] rounded-xl p-5",children:[(0,b.jsxs)("h2",{className:"text-sm font-semibold text-gray-300 mb-4",children:["Online Sessions ",(0,b.jsxs)("span",{className:"text-gray-600",children:["(",G.length,")"]})]}),(0,b.jsx)("div",{className:"space-y-2 max-h-80 overflow-y-auto",children:0===G.length?(0,b.jsx)("div",{className:"text-xs text-gray-600 text-center py-4",children:"No online sessions"}):G.map(a=>(0,b.jsxs)("div",{className:"flex items-center gap-3 bg-[#0e0e10] rounded-lg px-3 py-2 border border-[#1c1c1f]",children:[(0,b.jsx)(g.AliasAvatar,{alias:a.alias,size:20}),(0,b.jsxs)("div",{className:"min-w-0 flex-1",children:[(0,b.jsx)("div",{className:"text-sm text-white font-medium truncate",children:a.alias}),(0,b.jsxs)("div",{className:"text-xs text-gray-500 truncate",children:[a.agent||"--"," · ",a.task||"idle"]})]}),(0,b.jsxs)("div",{className:"flex items-center gap-2 shrink-0",children:[(0,b.jsx)("span",{className:`text-xs px-2 py-0.5 rounded border ${h.SESSION_STATUS_CHIP_CLASS[a.status]||h.SESSION_STATUS_CHIP_CLASS.idle}`,children:a.status}),(0,b.jsx)("span",{className:"text-[10px] text-gray-600",children:(0,f.timeAgo)(a.updated_at)})]})]},a.alias))})]})]})]}),(0,b.jsxs)("div",{id:"actions",className:"space-y-4 scroll-mt-6",children:[(0,b.jsxs)("div",{className:"flex items-center gap-2 px-1",children:[(0,b.jsx)("div",{className:"text-[11px] uppercase tracking-[0.14em] text-gray-400 font-semibold",children:"Actions"}),(0,b.jsx)("div",{className:"flex-1 h-px bg-[#26262b]"})]}),0===i.length?(0,b.jsx)("div",{className:"bg-[#161618] border border-[#26262b] rounded-xl px-5 py-4 text-sm text-gray-500",children:"Broadcast and Send Task become available after the first agent registers."}):(0,b.jsxs)("div",{className:"grid grid-cols-1 lg:grid-cols-2 gap-4",children:[(0,b.jsxs)("section",{className:"bg-[#161618] border border-[#26262b] rounded-xl p-5",children:[(0,b.jsx)("h2",{className:"text-sm font-semibold text-gray-300 mb-4",children:"Broadcast"}),(0,b.jsx)("textarea",{value:t,onChange:a=>u(a.target.value),placeholder:"Message to all online nodes...",rows:3,className:"w-full bg-[#0e0e10] border border-[#26262b] rounded-lg px-3 py-2 text-sm text-white placeholder-gray-600 focus:border-cyan-500/50 focus:outline-none resize-none"}),(0,b.jsxs)("div",{className:"flex justify-between items-center mt-3",children:[(0,b.jsxs)("span",{className:"text-xs text-gray-600",children:[t.length,"/500"]}),(0,b.jsx)("button",{onClick:E,disabled:!t.trim(),className:"px-4 py-2 bg-purple-600 hover:bg-purple-500 disabled:bg-gray-800 disabled:text-gray-600 text-white text-sm rounded-lg transition-colors",children:"Broadcast"})]}),v&&(0,b.jsx)("div",{className:`mt-2 text-xs ${v.startsWith("Failed")?"text-red-400":"text-green-400"}`,children:v})]}),(0,b.jsxs)("section",{className:"bg-[#161618] border border-[#26262b] rounded-xl p-5",children:[(0,b.jsx)("h2",{className:"text-sm font-semibold text-gray-300 mb-4",children:"Send Task"}),(0,b.jsxs)("select",{value:x,onChange:a=>y(a.target.value),className:"w-full bg-[#0e0e10] border border-[#26262b] rounded-lg px-3 py-2 text-base sm:text-sm text-white focus:border-cyan-500/50 focus:outline-none mb-3",children:[(0,b.jsx)("option",{value:"",children:"Select target node..."}),G.map(a=>(0,b.jsxs)("option",{value:a.alias,children:[a.alias," (",a.status,")"]},a.alias)),(0,b.jsx)("optgroup",{label:"Offline",children:H.map(a=>(0,b.jsxs)("option",{value:a.alias,children:[a.alias," (offline)"]},a.alias))})]}),(0,b.jsx)("textarea",{value:z,onChange:a=>A(a.target.value),placeholder:"Task content...",rows:3,className:"w-full bg-[#0e0e10] border border-[#26262b] rounded-lg px-3 py-2 text-sm text-white placeholder-gray-600 focus:border-cyan-500/50 focus:outline-none resize-none"}),(0,b.jsx)("div",{className:"flex justify-end mt-3",children:(0,b.jsx)("button",{onClick:F,disabled:!x||!z.trim(),className:"px-4 py-2 bg-cyan-600 hover:bg-cyan-500 disabled:bg-gray-800 disabled:text-gray-600 text-white text-sm rounded-lg transition-colors",children:"Send Task"})}),B&&(0,b.jsx)("div",{className:`mt-2 text-xs ${B.startsWith("Failed")?"text-red-400":"text-green-400"}`,children:B})]})]})]}),(0,b.jsxs)("div",{id:"users",className:"space-y-4 scroll-mt-6",children:[(0,b.jsxs)("div",{className:"flex items-center gap-2 px-1",children:[(0,b.jsx)("div",{className:"text-[11px] uppercase tracking-[0.14em] text-gray-400 font-semibold",children:"Users"}),(0,b.jsx)("div",{className:"flex-1 h-px bg-[#26262b]"})]}),(0,b.jsx)("div",{className:"grid grid-cols-1 lg:grid-cols-2 gap-4",children:(0,b.jsxs)("section",{className:"bg-[#161618] border border-[#26262b] rounded-xl p-5",children:[(0,b.jsx)("h2",{className:"text-sm font-semibold text-gray-300 mb-4",children:"Register User (V3)"}),(0,b.jsxs)("div",{className:"space-y-3",children:[(0,b.jsx)("input",{type:"text",value:n,onChange:a=>o(a.target.value),placeholder:"Username",className:"w-full bg-[#0e0e10] border border-[#26262b] rounded-lg px-3 py-2 text-sm text-white placeholder-gray-600 focus:border-cyan-500/50 focus:outline-none"}),(0,b.jsx)("input",{type:"password",value:p,onChange:a=>q(a.target.value),placeholder:"Password",className:"w-full bg-[#0e0e10] border border-[#26262b] rounded-lg px-3 py-2 text-sm text-white placeholder-gray-600 focus:border-cyan-500/50 focus:outline-none"}),(0,b.jsx)("button",{onClick:D,disabled:!n.trim()||!p.trim(),className:"w-full px-4 py-2 bg-green-600 hover:bg-green-500 disabled:bg-gray-800 disabled:text-gray-600 text-white text-sm rounded-lg transition-colors",children:"Register"})]}),r&&(0,b.jsx)("div",{className:`mt-2 text-xs ${r.startsWith("Failed")?"text-red-400":"text-green-400"}`,children:r})]})})]})]})]})}])}];
2
-
3
- //# sourceMappingURL=dashboard-pr4-338_app_0l07md.._.js.map