@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.
- package/.next/BUILD_COMMIT +1 -1
- package/.next/BUILD_ID +1 -1
- package/.next/build-manifest.json +3 -3
- package/.next/diagnostics/route-bundle-stats.json +12 -11
- package/.next/fallback-build-manifest.json +3 -3
- package/.next/server/app/_global-error.html +1 -1
- package/.next/server/app/_global-error.rsc +1 -1
- package/.next/server/app/_global-error.segments/__PAGE__.segment.rsc +1 -1
- package/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
- package/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
- package/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
- package/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
- package/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
- package/.next/server/app/_not-found.html +2 -2
- package/.next/server/app/_not-found.rsc +2 -2
- package/.next/server/app/_not-found.segments/_full.segment.rsc +2 -2
- package/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
- package/.next/server/app/_not-found.segments/_index.segment.rsc +2 -2
- package/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
- package/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
- package/.next/server/app/_not-found.segments/_tree.segment.rsc +2 -2
- package/.next/server/app/admin/page.js.nft.json +1 -1
- package/.next/server/app/admin/page_client-reference-manifest.js +1 -1
- package/.next/server/app/admin.html +2 -2
- package/.next/server/app/admin.rsc +3 -3
- package/.next/server/app/admin.segments/_full.segment.rsc +3 -3
- package/.next/server/app/admin.segments/_head.segment.rsc +1 -1
- package/.next/server/app/admin.segments/_index.segment.rsc +2 -2
- package/.next/server/app/admin.segments/_tree.segment.rsc +2 -2
- package/.next/server/app/admin.segments/admin/__PAGE__.segment.rsc +2 -2
- package/.next/server/app/admin.segments/admin.segment.rsc +1 -1
- package/.next/server/app/index.html +2 -2
- package/.next/server/app/index.rsc +3 -3
- package/.next/server/app/index.segments/__PAGE__.segment.rsc +2 -2
- package/.next/server/app/index.segments/_full.segment.rsc +3 -3
- package/.next/server/app/index.segments/_head.segment.rsc +1 -1
- package/.next/server/app/index.segments/_index.segment.rsc +2 -2
- package/.next/server/app/index.segments/_tree.segment.rsc +2 -2
- package/.next/server/app/login/page_client-reference-manifest.js +1 -1
- package/.next/server/app/login.html +2 -2
- package/.next/server/app/login.rsc +3 -3
- package/.next/server/app/login.segments/_full.segment.rsc +3 -3
- package/.next/server/app/login.segments/_head.segment.rsc +1 -1
- package/.next/server/app/login.segments/_index.segment.rsc +2 -2
- package/.next/server/app/login.segments/_tree.segment.rsc +2 -2
- package/.next/server/app/login.segments/login/__PAGE__.segment.rsc +2 -2
- package/.next/server/app/login.segments/login.segment.rsc +1 -1
- package/.next/server/app/logs/page_client-reference-manifest.js +1 -1
- package/.next/server/app/logs.html +2 -2
- package/.next/server/app/logs.rsc +2 -2
- package/.next/server/app/logs.segments/_full.segment.rsc +2 -2
- package/.next/server/app/logs.segments/_head.segment.rsc +1 -1
- package/.next/server/app/logs.segments/_index.segment.rsc +2 -2
- package/.next/server/app/logs.segments/_tree.segment.rsc +2 -2
- package/.next/server/app/logs.segments/logs/__PAGE__.segment.rsc +1 -1
- package/.next/server/app/logs.segments/logs.segment.rsc +1 -1
- package/.next/server/app/messages/page_client-reference-manifest.js +1 -1
- package/.next/server/app/messages.html +2 -2
- package/.next/server/app/messages.rsc +3 -3
- package/.next/server/app/messages.segments/_full.segment.rsc +3 -3
- package/.next/server/app/messages.segments/_head.segment.rsc +1 -1
- package/.next/server/app/messages.segments/_index.segment.rsc +2 -2
- package/.next/server/app/messages.segments/_tree.segment.rsc +2 -2
- package/.next/server/app/messages.segments/messages/__PAGE__.segment.rsc +2 -2
- package/.next/server/app/messages.segments/messages.segment.rsc +1 -1
- package/.next/server/app/node/page_client-reference-manifest.js +1 -1
- package/.next/server/app/node.html +2 -2
- package/.next/server/app/node.rsc +2 -2
- package/.next/server/app/node.segments/_full.segment.rsc +2 -2
- package/.next/server/app/node.segments/_head.segment.rsc +1 -1
- package/.next/server/app/node.segments/_index.segment.rsc +2 -2
- package/.next/server/app/node.segments/_tree.segment.rsc +2 -2
- package/.next/server/app/node.segments/node/__PAGE__.segment.rsc +1 -1
- package/.next/server/app/node.segments/node.segment.rsc +1 -1
- package/.next/server/app/nodes/page_client-reference-manifest.js +1 -1
- package/.next/server/app/nodes.html +2 -2
- package/.next/server/app/nodes.rsc +3 -3
- package/.next/server/app/nodes.segments/_full.segment.rsc +3 -3
- package/.next/server/app/nodes.segments/_head.segment.rsc +1 -1
- package/.next/server/app/nodes.segments/_index.segment.rsc +2 -2
- package/.next/server/app/nodes.segments/_tree.segment.rsc +2 -2
- package/.next/server/app/nodes.segments/nodes/__PAGE__.segment.rsc +2 -2
- package/.next/server/app/nodes.segments/nodes.segment.rsc +1 -1
- package/.next/server/app/page_client-reference-manifest.js +1 -1
- package/.next/server/app/server-logs/page_client-reference-manifest.js +1 -1
- package/.next/server/app/server-logs.html +2 -2
- package/.next/server/app/server-logs.rsc +2 -2
- package/.next/server/app/server-logs.segments/_full.segment.rsc +2 -2
- package/.next/server/app/server-logs.segments/_head.segment.rsc +1 -1
- package/.next/server/app/server-logs.segments/_index.segment.rsc +2 -2
- package/.next/server/app/server-logs.segments/_tree.segment.rsc +2 -2
- package/.next/server/app/server-logs.segments/server-logs/__PAGE__.segment.rsc +1 -1
- package/.next/server/app/server-logs.segments/server-logs.segment.rsc +1 -1
- package/.next/server/app/servers/page_client-reference-manifest.js +1 -1
- package/.next/server/app/servers.html +2 -2
- package/.next/server/app/servers.rsc +2 -2
- package/.next/server/app/servers.segments/_full.segment.rsc +2 -2
- package/.next/server/app/servers.segments/_head.segment.rsc +1 -1
- package/.next/server/app/servers.segments/_index.segment.rsc +2 -2
- package/.next/server/app/servers.segments/_tree.segment.rsc +2 -2
- package/.next/server/app/servers.segments/servers/__PAGE__.segment.rsc +1 -1
- package/.next/server/app/servers.segments/servers.segment.rsc +1 -1
- package/.next/server/app/settings/networks/page_client-reference-manifest.js +1 -1
- package/.next/server/app/settings/networks.html +2 -2
- package/.next/server/app/settings/networks.rsc +2 -2
- package/.next/server/app/settings/networks.segments/_full.segment.rsc +2 -2
- package/.next/server/app/settings/networks.segments/_head.segment.rsc +1 -1
- package/.next/server/app/settings/networks.segments/_index.segment.rsc +2 -2
- package/.next/server/app/settings/networks.segments/_tree.segment.rsc +2 -2
- package/.next/server/app/settings/networks.segments/settings/networks/__PAGE__.segment.rsc +1 -1
- package/.next/server/app/settings/networks.segments/settings/networks.segment.rsc +1 -1
- package/.next/server/app/settings/networks.segments/settings.segment.rsc +1 -1
- package/.next/server/app/settings/page_client-reference-manifest.js +1 -1
- package/.next/server/app/settings/tokens/page_client-reference-manifest.js +1 -1
- package/.next/server/app/settings/tokens.html +2 -2
- package/.next/server/app/settings/tokens.rsc +2 -2
- package/.next/server/app/settings/tokens.segments/_full.segment.rsc +2 -2
- package/.next/server/app/settings/tokens.segments/_head.segment.rsc +1 -1
- package/.next/server/app/settings/tokens.segments/_index.segment.rsc +2 -2
- package/.next/server/app/settings/tokens.segments/_tree.segment.rsc +2 -2
- package/.next/server/app/settings/tokens.segments/settings/tokens/__PAGE__.segment.rsc +1 -1
- package/.next/server/app/settings/tokens.segments/settings/tokens.segment.rsc +1 -1
- package/.next/server/app/settings/tokens.segments/settings.segment.rsc +1 -1
- package/.next/server/app/settings.html +2 -2
- package/.next/server/app/settings.rsc +3 -3
- package/.next/server/app/settings.segments/_full.segment.rsc +3 -3
- package/.next/server/app/settings.segments/_head.segment.rsc +1 -1
- package/.next/server/app/settings.segments/_index.segment.rsc +2 -2
- package/.next/server/app/settings.segments/_tree.segment.rsc +2 -2
- package/.next/server/app/settings.segments/settings/__PAGE__.segment.rsc +2 -2
- package/.next/server/app/settings.segments/settings.segment.rsc +1 -1
- package/.next/server/app/tasks/[id]/page_client-reference-manifest.js +1 -1
- package/.next/server/app/tasks/page_client-reference-manifest.js +1 -1
- package/.next/server/app/tasks.html +2 -2
- package/.next/server/app/tasks.rsc +2 -2
- package/.next/server/app/tasks.segments/_full.segment.rsc +2 -2
- package/.next/server/app/tasks.segments/_head.segment.rsc +1 -1
- package/.next/server/app/tasks.segments/_index.segment.rsc +2 -2
- package/.next/server/app/tasks.segments/_tree.segment.rsc +2 -2
- package/.next/server/app/tasks.segments/tasks/__PAGE__.segment.rsc +1 -1
- package/.next/server/app/tasks.segments/tasks.segment.rsc +1 -1
- package/.next/server/chunks/[root-of-the-server]__0jg663w._.js +1 -1
- package/.next/server/chunks/[root-of-the-server]__0jg663w._.js.map +1 -1
- package/.next/server/chunks/ssr/[root-of-the-server]__0-n_oet._.js +1 -1
- package/.next/server/chunks/ssr/[root-of-the-server]__0-n_oet._.js.map +1 -1
- package/.next/server/chunks/ssr/[root-of-the-server]__0jgx8j4._.js +1 -1
- package/.next/server/chunks/ssr/[root-of-the-server]__0jgx8j4._.js.map +1 -1
- package/.next/server/chunks/ssr/dashboard-pr4-338_app_0_vh2hn._.js +1 -1
- package/.next/server/chunks/ssr/dashboard-pr4-338_app_0_vh2hn._.js.map +1 -1
- package/.next/server/chunks/ssr/dashboard-pr4-338_app_0a352ge._.js +1 -1
- package/.next/server/chunks/ssr/dashboard-pr4-338_app_0a352ge._.js.map +1 -1
- package/.next/server/chunks/ssr/dashboard-pr4-338_app_0k0.-6z._.js +1 -1
- package/.next/server/chunks/ssr/dashboard-pr4-338_app_0k0.-6z._.js.map +1 -1
- package/.next/server/chunks/ssr/dashboard-pr4-338_app_12jui6l._.js +3 -0
- package/.next/server/chunks/ssr/dashboard-pr4-338_app_12jui6l._.js.map +1 -0
- package/.next/server/chunks/ssr/dashboard-pr4-338_app_nodes_page_tsx_0.1qp86._.js +2 -2
- package/.next/server/chunks/ssr/dashboard-pr4-338_app_nodes_page_tsx_0.1qp86._.js.map +1 -1
- package/.next/server/middleware-build-manifest.js +3 -3
- package/.next/server/pages/404.html +2 -2
- package/.next/server/pages/500.html +1 -1
- package/.next/static/chunks/01.7wksc03ge7.js +1 -0
- package/.next/static/chunks/{1238uy.r8ebu0.js → 05ludq~vmjrpi.js} +1 -1
- package/.next/static/chunks/0nt58h_jnn298.js +2 -0
- package/.next/static/chunks/0~qpo2.zfv914.css +1 -0
- package/.next/static/chunks/10-herg8p2_u0.js +1 -0
- package/.next/static/chunks/{0r4f9a-1h0l8n.js → 1162nvfdeg.tg.js} +2 -2
- package/.next/static/chunks/11as~l5x7cdjy.js +1 -0
- package/.next/static/chunks/{0dzyz0tl5mmyh.js → 14eb0vbqm-0fq.js} +1 -1
- package/.next/trace +2 -2
- package/.next/trace-build +1 -1
- package/app/api/anet/node-lifecycle/route.ts +54 -5
- package/app/components/CreateNodeWizard.tsx +70 -24
- package/app/components/HostSupervisorPicker.tsx +6 -3
- package/app/components/NodeLifecycleMenu.tsx +436 -0
- package/app/components/types.ts +4 -0
- package/app/lib/hooks.ts +22 -0
- package/app/nodes/page.tsx +42 -2
- package/package.json +1 -1
- package/.next/server/chunks/ssr/dashboard-pr4-338_app_0l07md.._.js +0 -3
- package/.next/server/chunks/ssr/dashboard-pr4-338_app_0l07md.._.js.map +0 -1
- package/.next/static/chunks/0okyajjiuijbh.css +0 -1
- package/.next/static/chunks/0zezb.fc41oyb.js +0 -1
- package/.next/static/chunks/0zta~z4xpak8b.js +0 -2
- package/.next/static/chunks/10rif2p3a6hdl.js +0 -1
- /package/.next/static/{S9Q_U9UD2ZJQPBZjZgrZy → 3WnbjoWrJbRnQkx4ilgVm}/_buildManifest.js +0 -0
- /package/.next/static/{S9Q_U9UD2ZJQPBZjZgrZy → 3WnbjoWrJbRnQkx4ilgVm}/_clientMiddlewareManifest.js +0 -0
- /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/<ts>-{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
|
+
}
|
package/app/components/types.ts
CHANGED
|
@@ -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 };
|
package/app/nodes/page.tsx
CHANGED
|
@@ -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">
|
|
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,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
|