@burdenoff/microfe-vibecontrols 2026.610.1 → 2026.610.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/components/agents/AgentCard.js +80 -93
- package/dist/components/agents/AgentCard.js.map +1 -1
- package/dist/components/agents/AgentConnectionRecovery.js +1 -1
- package/dist/components/agents/AgentConnectionRecovery.js.map +1 -1
- package/dist/components/agents/AgentForm.js +3 -3
- package/dist/components/agents/AgentForm.js.map +1 -1
- package/dist/components/agents/AgentList.js +1 -1
- package/dist/components/agents/AgentList.js.map +1 -1
- package/dist/components/agents/AgentMaintenanceTab.js +1 -1
- package/dist/components/agents/AgentMaintenanceTab.js.map +1 -1
- package/dist/components/agents/AgentStatusBadge.js +8 -7
- package/dist/components/agents/AgentStatusBadge.js.map +1 -1
- package/dist/components/agents/AgentStorageConfig.js +1 -1
- package/dist/components/agents/AgentStorageConfig.js.map +1 -1
- package/dist/components/agents/ConnectionLogPanel.js +4 -4
- package/dist/components/agents/ConnectionLogPanel.js.map +1 -1
- package/dist/components/agents/GatewayAuthCard.js +222 -218
- package/dist/components/agents/GatewayAuthCard.js.map +1 -1
- package/dist/components/agents/OpenInBrowserEditorButton.js +1 -1
- package/dist/components/agents/OpenInBrowserEditorButton.js.map +1 -1
- package/dist/components/agents/OpenInEditorMenu.js +1 -1
- package/dist/components/agents/OpenInEditorMenu.js.map +1 -1
- package/dist/components/agents/PluginUIPanel.js +9 -9
- package/dist/components/agents/PluginUIPanel.js.map +1 -1
- package/dist/components/agents/RemoteTaskPermissionsCard.js +88 -83
- package/dist/components/agents/RemoteTaskPermissionsCard.js.map +1 -1
- package/dist/components/graph/ConnectionPermissionsDialog.js +3 -3
- package/dist/components/graph/ConnectionPermissionsDialog.js.map +1 -1
- package/dist/components/graph/MeshAgentNode.js +2 -2
- package/dist/components/graph/MeshAgentNode.js.map +1 -1
- package/dist/components/graph/MeshDeployWizard.js +4 -4
- package/dist/components/graph/MeshDeployWizard.js.map +1 -1
- package/dist/components/graph/MeshGraphView.js +1 -1
- package/dist/components/graph/MeshGraphView.js.map +1 -1
- package/dist/components/graph/nodeIcons.js +10 -11
- package/dist/components/graph/nodeIcons.js.map +1 -1
- package/dist/components/shared/Card.js +9 -20
- package/dist/components/shared/Card.js.map +1 -1
- package/dist/components/shared/ConfirmDialog.js +2 -2
- package/dist/components/shared/ConfirmDialog.js.map +1 -1
- package/dist/components/shared/ErrorState.js +7 -5
- package/dist/components/shared/ErrorState.js.map +1 -1
- package/dist/components/shared/LoadingState.js +1 -13
- package/dist/components/shared/LoadingState.js.map +1 -1
- package/dist/components/shared/StatCard.js +8 -8
- package/dist/components/shared/StatCard.js.map +1 -1
- package/dist/components/shared/StatusBadge.js +2 -2
- package/dist/components/shared/StatusBadge.js.map +1 -1
- package/dist/components/shared/cardMotion.js +3 -10
- package/dist/components/shared/cardMotion.js.map +1 -1
- package/dist/components/targets/TargetCard.js +78 -96
- package/dist/components/targets/TargetCard.js.map +1 -1
- package/dist/pages/AgentDetailsPage.js +6 -6
- package/dist/pages/AgentDetailsPage.js.map +1 -1
- package/dist/pages/AgentGraphPage.js +2 -2
- package/dist/pages/AgentGraphPage.js.map +1 -1
- package/dist/pages/AgentsPage.js +1 -1
- package/dist/pages/AgentsPage.js.map +1 -1
- package/dist/pages/TargetsPage.js +10 -10
- package/dist/pages/TargetsPage.js.map +1 -1
- package/package.json +1 -1
|
@@ -5,118 +5,105 @@ import { getAgentStatus as r } from "./agentStatus.js";
|
|
|
5
5
|
import { classifyAgentHealth as i } from "../../utils/agentHealth.js";
|
|
6
6
|
import { AgentStatusBadge as a } from "./AgentStatusBadge.js";
|
|
7
7
|
import { Clock as o, Cpu as s, Globe as c, Link as l, Play as u, RefreshCw as d, Server as f, Settings as p, Square as m } from "lucide-react";
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
10
|
-
import { Checkbox as y } from "@burdenoff/fe-libs/ui";
|
|
8
|
+
import { jsx as h, jsxs as g } from "react/jsx-runtime";
|
|
9
|
+
import { Checkbox as _ } from "@burdenoff/fe-libs/ui";
|
|
11
10
|
//#region src/components/agents/AgentCard.tsx
|
|
12
|
-
var
|
|
13
|
-
let
|
|
14
|
-
if (!e) return
|
|
11
|
+
var v = ({ agent: v, onSelect: y, onConfigure: b, onSettings: x, onStart: S, onStop: C, onDisconnect: w, onEditTunnelUrl: T, selected: E = !1, onToggleSelect: D, className: O = "", healthErrorMessage: k }) => {
|
|
12
|
+
let A = e(), j = b || x, M = r(v), N = (e) => {
|
|
13
|
+
if (!e) return A("agentsPage.never", "Never");
|
|
15
14
|
let t = Date.now() - new Date(e).getTime();
|
|
16
|
-
return t < 6e4 ?
|
|
17
|
-
},
|
|
18
|
-
return
|
|
15
|
+
return t < 6e4 ? A("agentsPage.justNow", "Just now") : t < 36e5 ? `${Math.floor(t / 6e4)}m ago` : t < 864e5 ? `${Math.floor(t / 36e5)}h ago` : `${Math.floor(t / 864e5)}d ago`;
|
|
16
|
+
}, P = [];
|
|
17
|
+
return M === "ACTIVE" && C && P.push({
|
|
19
18
|
key: "stop",
|
|
20
|
-
label:
|
|
19
|
+
label: A("agentsPage.stopAgent", "Stop Agent"),
|
|
21
20
|
icon: m,
|
|
22
|
-
onSelect: () =>
|
|
23
|
-
}), (
|
|
21
|
+
onSelect: () => C(v)
|
|
22
|
+
}), (M === "STOPPED" || M === "OFFLINE") && S && P.push({
|
|
24
23
|
key: "start",
|
|
25
|
-
label:
|
|
24
|
+
label: A("agentsPage.startAgent", "Start Agent"),
|
|
26
25
|
icon: u,
|
|
27
|
-
onSelect: () =>
|
|
28
|
-
}),
|
|
26
|
+
onSelect: () => S(v)
|
|
27
|
+
}), M === "DEGRADED" && S && P.push({
|
|
29
28
|
key: "reconnect",
|
|
30
|
-
label:
|
|
29
|
+
label: A("agentsPage.reconnectAgent", "Reconnect"),
|
|
31
30
|
icon: d,
|
|
32
|
-
onSelect: () =>
|
|
33
|
-
}), (
|
|
31
|
+
onSelect: () => S(v)
|
|
32
|
+
}), (M === "ACTIVE" || M === "STOPPED" || M === "OFFLINE" || M === "DEGRADED") && w && P.push({
|
|
34
33
|
key: "disconnect",
|
|
35
|
-
label:
|
|
34
|
+
label: M === "OFFLINE" ? A("agentsPage.clearConnection", "Clear Connection") : A("agentsPage.disconnect", "Disconnect"),
|
|
36
35
|
icon: m,
|
|
37
|
-
onSelect: () =>
|
|
36
|
+
onSelect: () => w(v),
|
|
38
37
|
variant: "danger"
|
|
39
|
-
}),
|
|
38
|
+
}), T && P.push({
|
|
40
39
|
key: "edit-tunnel",
|
|
41
|
-
label:
|
|
40
|
+
label: A("agentsPage.editTunnel", "Edit Tunnel"),
|
|
42
41
|
icon: l,
|
|
43
|
-
onSelect: () =>
|
|
44
|
-
}),
|
|
42
|
+
onSelect: () => T(v)
|
|
43
|
+
}), j && P.push({
|
|
45
44
|
key: "settings",
|
|
46
|
-
label:
|
|
45
|
+
label: A("agentsPage.settings", "Settings"),
|
|
47
46
|
icon: p,
|
|
48
|
-
onSelect: () =>
|
|
49
|
-
}), /* @__PURE__ */
|
|
47
|
+
onSelect: () => j(v)
|
|
48
|
+
}), /* @__PURE__ */ g("div", {
|
|
50
49
|
role: "button",
|
|
51
50
|
tabIndex: 0,
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
transition: {
|
|
55
|
-
duration: .15,
|
|
56
|
-
ease: [
|
|
57
|
-
.4,
|
|
58
|
-
0,
|
|
59
|
-
.2,
|
|
60
|
-
1
|
|
61
|
-
]
|
|
62
|
-
}
|
|
63
|
-
},
|
|
64
|
-
className: `bg-bg-surface border rounded-lg p-4 h-full hover:border-border-strong hover:shadow-[var(--shadow-elevation-2)] transition-all cursor-pointer focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-border-focus focus-visible:ring-offset-2 focus-visible:ring-offset-bg-canvas ${O ? "border-action-primary-bg ring-1 ring-action-primary-bg/30" : "border-border-default"} ${A}`,
|
|
65
|
-
onClick: () => x?.(b),
|
|
51
|
+
className: `bg-bg-surface border rounded-card p-4 h-full hover:border-border-strong hover:shadow-[var(--shadow-elevation-1)] transition-[colors,box-shadow] duration-150 cursor-pointer focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-border-focus focus-visible:ring-offset-2 focus-visible:ring-offset-bg-canvas ${E ? "border-action-primary-bg ring-1 ring-action-primary-bg/30" : "border-border-default"} ${O}`,
|
|
52
|
+
onClick: () => y?.(v),
|
|
66
53
|
onKeyDown: (e) => {
|
|
67
|
-
e.target === e.currentTarget && (e.key === "Enter" || e.key === " ") && (e.preventDefault(),
|
|
54
|
+
e.target === e.currentTarget && (e.key === "Enter" || e.key === " ") && (e.preventDefault(), y?.(v));
|
|
68
55
|
},
|
|
69
56
|
children: [
|
|
70
|
-
/* @__PURE__ */
|
|
57
|
+
/* @__PURE__ */ g("div", {
|
|
71
58
|
className: "flex items-start justify-between gap-2",
|
|
72
|
-
children: [/* @__PURE__ */
|
|
59
|
+
children: [/* @__PURE__ */ g("div", {
|
|
73
60
|
className: "flex items-start gap-2 sm:gap-3 min-w-0 flex-1",
|
|
74
61
|
children: [
|
|
75
|
-
|
|
62
|
+
D && /* @__PURE__ */ h("span", {
|
|
76
63
|
className: "flex flex-shrink-0 items-center justify-center max-md:min-h-11 max-md:min-w-11 max-md:-m-2 md:mt-0.5",
|
|
77
64
|
onClick: (e) => {
|
|
78
|
-
e.stopPropagation(), e.target === e.currentTarget &&
|
|
65
|
+
e.stopPropagation(), e.target === e.currentTarget && D();
|
|
79
66
|
},
|
|
80
|
-
children: /* @__PURE__ */ _
|
|
81
|
-
checked:
|
|
82
|
-
onCheckedChange: () =>
|
|
67
|
+
children: /* @__PURE__ */ h(_, {
|
|
68
|
+
checked: E,
|
|
69
|
+
onCheckedChange: () => D(),
|
|
83
70
|
onClick: (e) => e.stopPropagation(),
|
|
84
|
-
"aria-label":
|
|
71
|
+
"aria-label": A("agentsPage.selectAgent", "Select {{name}}", { name: v.name })
|
|
85
72
|
})
|
|
86
73
|
}),
|
|
87
|
-
/* @__PURE__ */
|
|
74
|
+
/* @__PURE__ */ h("div", {
|
|
88
75
|
className: "flex-shrink-0 size-8 sm:size-10 rounded-lg bg-bg-sunken flex items-center justify-center",
|
|
89
|
-
children: /* @__PURE__ */
|
|
76
|
+
children: /* @__PURE__ */ h(f, {
|
|
90
77
|
className: "size-4 sm:size-5 text-text-secondary",
|
|
91
78
|
"aria-hidden": "true"
|
|
92
79
|
})
|
|
93
80
|
}),
|
|
94
|
-
/* @__PURE__ */
|
|
81
|
+
/* @__PURE__ */ g("div", {
|
|
95
82
|
className: "min-w-0 flex-1",
|
|
96
83
|
children: [
|
|
97
|
-
/* @__PURE__ */
|
|
84
|
+
/* @__PURE__ */ g("div", {
|
|
98
85
|
className: "flex items-center gap-1.5 sm:gap-2 min-w-0",
|
|
99
|
-
children: [/* @__PURE__ */
|
|
86
|
+
children: [/* @__PURE__ */ h("h3", {
|
|
100
87
|
className: "font-semibold text-text-primary truncate min-w-0 text-sm sm:text-base",
|
|
101
|
-
children:
|
|
102
|
-
}), /* @__PURE__ */
|
|
103
|
-
platform:
|
|
88
|
+
children: v.name
|
|
89
|
+
}), /* @__PURE__ */ h(n, {
|
|
90
|
+
platform: v.platform,
|
|
104
91
|
className: "size-4 flex-shrink-0 text-text-secondary"
|
|
105
92
|
})]
|
|
106
93
|
}),
|
|
107
|
-
/* @__PURE__ */
|
|
94
|
+
/* @__PURE__ */ h("p", {
|
|
108
95
|
className: "text-xs sm:text-sm text-text-secondary truncate",
|
|
109
|
-
children:
|
|
96
|
+
children: v.hostname || `ID: ${v.id}`
|
|
110
97
|
}),
|
|
111
98
|
(() => {
|
|
112
|
-
let e = i(
|
|
113
|
-
return e.phase === "none" ? null : /* @__PURE__ */
|
|
99
|
+
let e = i(M, k ?? null);
|
|
100
|
+
return e.phase === "none" ? null : /* @__PURE__ */ g("p", {
|
|
114
101
|
className: `mt-1 flex items-start gap-1 text-xs ${e.phase === "attention" ? "text-status-warning-text" : "text-content-tertiary"}`,
|
|
115
|
-
children: [e.inProgress && /* @__PURE__ */
|
|
102
|
+
children: [e.inProgress && /* @__PURE__ */ h(d, {
|
|
116
103
|
className: "size-3 mt-0.5 flex-shrink-0 animate-spin",
|
|
117
104
|
"aria-hidden": "true"
|
|
118
|
-
}), /* @__PURE__ */
|
|
119
|
-
/* @__PURE__ */
|
|
105
|
+
}), /* @__PURE__ */ g("span", { children: [
|
|
106
|
+
/* @__PURE__ */ g("span", {
|
|
120
107
|
className: "font-medium",
|
|
121
108
|
children: [e.label, "."]
|
|
122
109
|
}),
|
|
@@ -128,75 +115,75 @@ var b = ({ agent: b, onSelect: x, onConfigure: S, onSettings: C, onStart: w, onS
|
|
|
128
115
|
]
|
|
129
116
|
})
|
|
130
117
|
]
|
|
131
|
-
}), /* @__PURE__ */
|
|
118
|
+
}), /* @__PURE__ */ g("div", {
|
|
132
119
|
className: "flex items-center gap-1 sm:gap-2 flex-shrink-0",
|
|
133
|
-
children: [/* @__PURE__ */
|
|
134
|
-
status:
|
|
135
|
-
reason:
|
|
136
|
-
linkTo: `/vibecontrols/agents/${
|
|
120
|
+
children: [/* @__PURE__ */ h(a, {
|
|
121
|
+
status: M,
|
|
122
|
+
reason: k ?? null,
|
|
123
|
+
linkTo: `/vibecontrols/agents/${v.id}?tab=connection`,
|
|
137
124
|
onClick: (e) => {
|
|
138
125
|
e.stopPropagation();
|
|
139
126
|
}
|
|
140
|
-
}), (
|
|
141
|
-
ariaLabel:
|
|
142
|
-
title:
|
|
127
|
+
}), (j || S || C) && P.length > 0 && /* @__PURE__ */ h(t, {
|
|
128
|
+
ariaLabel: A("agentsPage.agentActionsMenu", "Agent actions menu"),
|
|
129
|
+
title: A("agentsPage.agentActions", "Agent actions"),
|
|
143
130
|
align: "end",
|
|
144
|
-
items:
|
|
131
|
+
items: P
|
|
145
132
|
})]
|
|
146
133
|
})]
|
|
147
134
|
}),
|
|
148
|
-
/* @__PURE__ */
|
|
135
|
+
/* @__PURE__ */ g("div", {
|
|
149
136
|
className: "mt-4 grid grid-cols-2 gap-3 text-sm",
|
|
150
|
-
children: [/* @__PURE__ */
|
|
137
|
+
children: [/* @__PURE__ */ g("div", {
|
|
151
138
|
className: "flex items-center gap-2 text-text-secondary",
|
|
152
|
-
children: [/* @__PURE__ */
|
|
139
|
+
children: [/* @__PURE__ */ h(o, {
|
|
153
140
|
className: "size-4",
|
|
154
141
|
"aria-hidden": "true"
|
|
155
|
-
}), /* @__PURE__ */
|
|
156
|
-
}),
|
|
142
|
+
}), /* @__PURE__ */ h("span", { children: N(v.lastHeartbeat) })]
|
|
143
|
+
}), v.architecture && /* @__PURE__ */ g("div", {
|
|
157
144
|
className: "flex items-center gap-2 text-text-secondary",
|
|
158
|
-
children: [/* @__PURE__ */
|
|
145
|
+
children: [/* @__PURE__ */ h(s, {
|
|
159
146
|
className: "size-4",
|
|
160
147
|
"aria-hidden": "true"
|
|
161
|
-
}), /* @__PURE__ */
|
|
148
|
+
}), /* @__PURE__ */ h("span", { children: v.architecture })]
|
|
162
149
|
})]
|
|
163
150
|
}),
|
|
164
|
-
|
|
151
|
+
v.tunnelUrl && /* @__PURE__ */ g("div", {
|
|
165
152
|
className: "mt-3 flex items-center gap-2 text-sm",
|
|
166
153
|
children: [
|
|
167
|
-
/* @__PURE__ */
|
|
154
|
+
/* @__PURE__ */ h(c, {
|
|
168
155
|
className: "size-4 flex-shrink-0 text-text-secondary",
|
|
169
156
|
"aria-hidden": "true"
|
|
170
157
|
}),
|
|
171
|
-
/* @__PURE__ */
|
|
158
|
+
/* @__PURE__ */ h("span", {
|
|
172
159
|
className: "text-text-secondary flex-shrink-0",
|
|
173
160
|
children: "Tunnel:"
|
|
174
161
|
}),
|
|
175
|
-
/* @__PURE__ */
|
|
176
|
-
href:
|
|
162
|
+
/* @__PURE__ */ h("a", {
|
|
163
|
+
href: v.tunnelUrl,
|
|
177
164
|
target: "_blank",
|
|
178
165
|
rel: "noopener noreferrer",
|
|
179
166
|
onClick: (e) => e.stopPropagation(),
|
|
180
|
-
"aria-label":
|
|
167
|
+
"aria-label": A("agentsPage.tunnelOpensInNewTab", "{{url}} (opens in new tab)", { url: v.tunnelUrl }),
|
|
181
168
|
className: "min-w-0 truncate text-text-link hover:text-text-linkHover hover:underline",
|
|
182
|
-
children:
|
|
169
|
+
children: v.tunnelUrl
|
|
183
170
|
})
|
|
184
171
|
]
|
|
185
172
|
}),
|
|
186
|
-
|
|
173
|
+
v.capabilities && v.capabilities.length > 0 && /* @__PURE__ */ g("div", {
|
|
187
174
|
className: "mt-3 flex flex-wrap gap-1",
|
|
188
|
-
children: [
|
|
175
|
+
children: [v.capabilities.slice(0, 4).map((e) => /* @__PURE__ */ h("span", {
|
|
189
176
|
className: "px-2 py-0.5 text-xs bg-bg-sunken text-text-secondary rounded-pill",
|
|
190
177
|
children: e
|
|
191
|
-
}, e)),
|
|
178
|
+
}, e)), v.capabilities.length > 4 && /* @__PURE__ */ g("span", {
|
|
192
179
|
className: "px-2 py-0.5 text-xs bg-bg-sunken text-text-secondary rounded-pill",
|
|
193
|
-
children: ["+",
|
|
180
|
+
children: ["+", v.capabilities.length - 4]
|
|
194
181
|
})]
|
|
195
182
|
})
|
|
196
183
|
]
|
|
197
184
|
});
|
|
198
185
|
};
|
|
199
186
|
//#endregion
|
|
200
|
-
export {
|
|
187
|
+
export { v as AgentCard };
|
|
201
188
|
|
|
202
189
|
//# sourceMappingURL=AgentCard.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"AgentCard.js","names":[],"sources":["../../../src/components/agents/AgentCard.tsx"],"sourcesContent":["import type { FC } from 'react';\nimport { motion, useReducedMotion } from 'framer-motion';\nimport { Server, Clock, Cpu, Play, Square, Settings, Globe, Link, RefreshCw } from 'lucide-react';\nimport { Checkbox } from '@burdenoff/fe-libs/ui';\nimport { AgentStatusBadge } from './AgentStatusBadge';\nimport type { Agent } from './types';\nimport { getAgentStatus } from './agentStatus';\nimport { classifyAgentHealth } from '../../utils/agentHealth';\nimport { ActionMenu, type ActionMenuItem } from '../shared/ActionMenu';\nimport { PlatformIcon } from '../shared/PlatformIcon';\nimport { useTr } from '../../shared/hooks/useTr';\n\nexport interface AgentCardProps {\n agent: Agent;\n onSelect?: (agent: Agent) => void;\n onConfigure?: (agent: Agent) => void;\n onSettings?: (agent: Agent) => void;\n onStart?: (agent: Agent) => void;\n onStop?: (agent: Agent) => void;\n onDisconnect?: (agent: Agent) => void;\n onEditTunnelUrl?: (agent: Agent) => void;\n selected?: boolean;\n onToggleSelect?: () => void;\n className?: string;\n /**\n * Last error from /health/ready or the live probe — shown in the badge\n * tooltip when the agent is degraded so users know WHY without curling\n * the tunnel themselves.\n */\n healthErrorMessage?: string | null;\n}\n\n/**\n * AgentCard - Display agent information in a card format\n *\n * @example\n * ```tsx\n * <AgentCard\n * agent={myAgent}\n * onSelect={handleSelect}\n * onConfigure={handleConfigure}\n * />\n * ```\n */\nexport const AgentCard: FC<AgentCardProps> = ({\n agent,\n onSelect,\n onConfigure,\n onSettings,\n onStart,\n onStop,\n onDisconnect,\n onEditTunnelUrl,\n selected = false,\n onToggleSelect,\n className = '',\n healthErrorMessage,\n}) => {\n const tr = useTr();\n const reduceMotion = useReducedMotion();\n // Support both onConfigure and onSettings\n const handleConfigure = onConfigure || onSettings;\n\n const status = getAgentStatus(agent);\n\n const formatLastHeartbeat = (timestamp: string | null | undefined) => {\n if (!timestamp) return tr('agentsPage.never', 'Never');\n const diff = Date.now() - new Date(timestamp).getTime();\n if (diff < 60000) return tr('agentsPage.justNow', 'Just now');\n if (diff < 3600000) return `${Math.floor(diff / 60000)}m ago`;\n if (diff < 86400000) return `${Math.floor(diff / 3600000)}h ago`;\n return `${Math.floor(diff / 86400000)}d ago`;\n };\n\n const menuItems: ActionMenuItem[] = [];\n if (status === 'ACTIVE' && onStop) {\n menuItems.push({\n key: 'stop',\n label: tr('agentsPage.stopAgent', 'Stop Agent'),\n icon: Square,\n onSelect: () => onStop(agent),\n });\n }\n if ((status === 'STOPPED' || status === 'OFFLINE') && onStart) {\n menuItems.push({\n key: 'start',\n label: tr('agentsPage.startAgent', 'Start Agent'),\n icon: Play,\n onSelect: () => onStart(agent),\n });\n }\n if (status === 'DEGRADED' && onStart) {\n // Reconnect on a degraded agent fires the same startVibecontrolsAgent\n // mutation as a fresh start — svc-side that POSTs lifecycle/start to the\n // agent's tunnel, which usually clears transient failures (tunnel hiccup,\n // finalize retry). Labelled \"Reconnect\" not \"Start\" so the user knows\n // it's a recovery action, not creating a new instance.\n menuItems.push({\n key: 'reconnect',\n label: tr('agentsPage.reconnectAgent', 'Reconnect'),\n icon: RefreshCw,\n onSelect: () => onStart(agent),\n });\n }\n if (\n (status === 'ACTIVE' ||\n status === 'STOPPED' ||\n status === 'OFFLINE' ||\n status === 'DEGRADED') &&\n onDisconnect\n ) {\n menuItems.push({\n key: 'disconnect',\n label:\n status === 'OFFLINE'\n ? tr('agentsPage.clearConnection', 'Clear Connection')\n : tr('agentsPage.disconnect', 'Disconnect'),\n icon: Square,\n onSelect: () => onDisconnect(agent),\n variant: 'danger',\n });\n }\n if (onEditTunnelUrl) {\n menuItems.push({\n key: 'edit-tunnel',\n label: tr('agentsPage.editTunnel', 'Edit Tunnel'),\n icon: Link,\n onSelect: () => onEditTunnelUrl(agent),\n });\n }\n if (handleConfigure) {\n menuItems.push({\n key: 'settings',\n label: tr('agentsPage.settings', 'Settings'),\n icon: Settings,\n onSelect: () => handleConfigure(agent),\n });\n }\n\n return (\n <motion.div\n role=\"button\"\n tabIndex={0}\n whileHover={\n reduceMotion ? undefined : { y: -2, transition: { duration: 0.15, ease: [0.4, 0, 0.2, 1] } }\n }\n className={`bg-bg-surface border rounded-lg p-4 h-full hover:border-border-strong hover:shadow-[var(--shadow-elevation-2)] transition-all cursor-pointer focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-border-focus focus-visible:ring-offset-2 focus-visible:ring-offset-bg-canvas ${\n selected\n ? 'border-action-primary-bg ring-1 ring-action-primary-bg/30'\n : 'border-border-default'\n } ${className}`}\n onClick={() => onSelect?.(agent)}\n onKeyDown={(e) => {\n if (e.target !== e.currentTarget) return; // children (menu, checkbox, links) handle their own keys\n if (e.key === 'Enter' || e.key === ' ') {\n e.preventDefault();\n onSelect?.(agent);\n }\n }}\n >\n <div className=\"flex items-start justify-between gap-2\">\n <div className=\"flex items-start gap-2 sm:gap-3 min-w-0 flex-1\">\n {/* Selection Checkbox */}\n {onToggleSelect && (\n <span\n className=\"flex flex-shrink-0 items-center justify-center max-md:min-h-11 max-md:min-w-11 max-md:-m-2 md:mt-0.5\"\n onClick={(e) => {\n e.stopPropagation();\n if (e.target === e.currentTarget) onToggleSelect();\n }}\n >\n <Checkbox\n checked={selected}\n onCheckedChange={() => onToggleSelect()}\n onClick={(e) => e.stopPropagation()}\n aria-label={tr('agentsPage.selectAgent', 'Select {{name}}', { name: agent.name })}\n />\n </span>\n )}\n <div className=\"flex-shrink-0 size-8 sm:size-10 rounded-lg bg-bg-sunken flex items-center justify-center\">\n <Server className=\"size-4 sm:size-5 text-text-secondary\" aria-hidden=\"true\" />\n </div>\n <div className=\"min-w-0 flex-1\">\n <div className=\"flex items-center gap-1.5 sm:gap-2 min-w-0\">\n <h3 className=\"font-semibold text-text-primary truncate min-w-0 text-sm sm:text-base\">\n {agent.name}\n </h3>\n <PlatformIcon\n platform={agent.platform}\n className=\"size-4 flex-shrink-0 text-text-secondary\"\n />\n </div>\n <p className=\"text-xs sm:text-sm text-text-secondary truncate\">\n {agent.hostname || `ID: ${agent.id}`}\n </p>\n {(() => {\n // Always-visible (not just hover) guidance when degraded, so the\n // user knows whether it's self-healing or needs their action.\n const health = classifyAgentHealth(status, healthErrorMessage ?? null);\n if (health.phase === 'none') return null;\n return (\n <p\n className={`mt-1 flex items-start gap-1 text-xs ${\n health.phase === 'attention'\n ? 'text-status-warning-text'\n : 'text-content-tertiary'\n }`}\n >\n {health.inProgress && (\n <RefreshCw\n className=\"size-3 mt-0.5 flex-shrink-0 animate-spin\"\n aria-hidden=\"true\"\n />\n )}\n <span>\n <span className=\"font-medium\">{health.label}.</span> {health.actionHint}\n </span>\n </p>\n );\n })()}\n </div>\n </div>\n {/* Status and Actions - always visible */}\n <div className=\"flex items-center gap-1 sm:gap-2 flex-shrink-0\">\n <AgentStatusBadge\n status={status}\n reason={healthErrorMessage ?? null}\n linkTo={`/vibecontrols/agents/${agent.id}?tab=connection`}\n onClick={(event) => {\n // The card itself has a `onSelect` click target that\n // routes to the agent detail. Stop propagation so a\n // badge click doesn't *also* fire that handler — the\n // anchor's href already routes to the right tab and\n // React Router picks it up via the surrounding shell.\n event.stopPropagation();\n }}\n />\n {(handleConfigure || onStart || onStop) && menuItems.length > 0 && (\n <ActionMenu\n ariaLabel={tr('agentsPage.agentActionsMenu', 'Agent actions menu')}\n title={tr('agentsPage.agentActions', 'Agent actions')}\n align=\"end\"\n items={menuItems}\n />\n )}\n </div>\n </div>\n\n <div className=\"mt-4 grid grid-cols-2 gap-3 text-sm\">\n <div className=\"flex items-center gap-2 text-text-secondary\">\n <Clock className=\"size-4\" aria-hidden=\"true\" />\n <span>{formatLastHeartbeat(agent.lastHeartbeat)}</span>\n </div>\n {agent.architecture && (\n <div className=\"flex items-center gap-2 text-text-secondary\">\n <Cpu className=\"size-4\" aria-hidden=\"true\" />\n <span>{agent.architecture}</span>\n </div>\n )}\n </div>\n\n {agent.tunnelUrl && (\n // Plain \"Tunnel: <link>\" row. The colored status chip that used\n // to sit here was a redundant second status surface — the\n // canonical state lives in the <AgentStatusBadge> top-right.\n // The Globe glyph stays as a neutral icon (no status color).\n <div className=\"mt-3 flex items-center gap-2 text-sm\">\n <Globe className=\"size-4 flex-shrink-0 text-text-secondary\" aria-hidden=\"true\" />\n <span className=\"text-text-secondary flex-shrink-0\">Tunnel:</span>\n <a\n href={agent.tunnelUrl}\n target=\"_blank\"\n rel=\"noopener noreferrer\"\n onClick={(e) => e.stopPropagation()}\n aria-label={tr('agentsPage.tunnelOpensInNewTab', '{{url}} (opens in new tab)', {\n url: agent.tunnelUrl,\n })}\n className=\"min-w-0 truncate text-text-link hover:text-text-linkHover hover:underline\"\n >\n {agent.tunnelUrl}\n </a>\n </div>\n )}\n\n {agent.capabilities && agent.capabilities.length > 0 && (\n <div className=\"mt-3 flex flex-wrap gap-1\">\n {agent.capabilities.slice(0, 4).map((cap) => (\n <span\n key={cap}\n className=\"px-2 py-0.5 text-xs bg-bg-sunken text-text-secondary rounded-pill\"\n >\n {cap}\n </span>\n ))}\n {agent.capabilities.length > 4 && (\n <span className=\"px-2 py-0.5 text-xs bg-bg-sunken text-text-secondary rounded-pill\">\n +{agent.capabilities.length - 4}\n </span>\n )}\n </div>\n )}\n </motion.div>\n );\n};\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"AgentCard.js","names":[],"sources":["../../../src/components/agents/AgentCard.tsx"],"sourcesContent":["import type { FC } from 'react';\nimport { Server, Clock, Cpu, Play, Square, Settings, Globe, Link, RefreshCw } from 'lucide-react';\nimport { Checkbox } from '@burdenoff/fe-libs/ui';\nimport { AgentStatusBadge } from './AgentStatusBadge';\nimport type { Agent } from './types';\nimport { getAgentStatus } from './agentStatus';\nimport { classifyAgentHealth } from '../../utils/agentHealth';\nimport { ActionMenu, type ActionMenuItem } from '../shared/ActionMenu';\nimport { PlatformIcon } from '../shared/PlatformIcon';\nimport { useTr } from '../../shared/hooks/useTr';\n\nexport interface AgentCardProps {\n agent: Agent;\n onSelect?: (agent: Agent) => void;\n onConfigure?: (agent: Agent) => void;\n onSettings?: (agent: Agent) => void;\n onStart?: (agent: Agent) => void;\n onStop?: (agent: Agent) => void;\n onDisconnect?: (agent: Agent) => void;\n onEditTunnelUrl?: (agent: Agent) => void;\n selected?: boolean;\n onToggleSelect?: () => void;\n className?: string;\n /**\n * Last error from /health/ready or the live probe — shown in the badge\n * tooltip when the agent is degraded so users know WHY without curling\n * the tunnel themselves.\n */\n healthErrorMessage?: string | null;\n}\n\n/**\n * AgentCard - Display agent information in a card format\n *\n * @example\n * ```tsx\n * <AgentCard\n * agent={myAgent}\n * onSelect={handleSelect}\n * onConfigure={handleConfigure}\n * />\n * ```\n */\nexport const AgentCard: FC<AgentCardProps> = ({\n agent,\n onSelect,\n onConfigure,\n onSettings,\n onStart,\n onStop,\n onDisconnect,\n onEditTunnelUrl,\n selected = false,\n onToggleSelect,\n className = '',\n healthErrorMessage,\n}) => {\n const tr = useTr();\n // Support both onConfigure and onSettings\n const handleConfigure = onConfigure || onSettings;\n\n const status = getAgentStatus(agent);\n\n const formatLastHeartbeat = (timestamp: string | null | undefined) => {\n if (!timestamp) return tr('agentsPage.never', 'Never');\n const diff = Date.now() - new Date(timestamp).getTime();\n if (diff < 60000) return tr('agentsPage.justNow', 'Just now');\n if (diff < 3600000) return `${Math.floor(diff / 60000)}m ago`;\n if (diff < 86400000) return `${Math.floor(diff / 3600000)}h ago`;\n return `${Math.floor(diff / 86400000)}d ago`;\n };\n\n const menuItems: ActionMenuItem[] = [];\n if (status === 'ACTIVE' && onStop) {\n menuItems.push({\n key: 'stop',\n label: tr('agentsPage.stopAgent', 'Stop Agent'),\n icon: Square,\n onSelect: () => onStop(agent),\n });\n }\n if ((status === 'STOPPED' || status === 'OFFLINE') && onStart) {\n menuItems.push({\n key: 'start',\n label: tr('agentsPage.startAgent', 'Start Agent'),\n icon: Play,\n onSelect: () => onStart(agent),\n });\n }\n if (status === 'DEGRADED' && onStart) {\n // Reconnect on a degraded agent fires the same startVibecontrolsAgent\n // mutation as a fresh start — svc-side that POSTs lifecycle/start to the\n // agent's tunnel, which usually clears transient failures (tunnel hiccup,\n // finalize retry). Labelled \"Reconnect\" not \"Start\" so the user knows\n // it's a recovery action, not creating a new instance.\n menuItems.push({\n key: 'reconnect',\n label: tr('agentsPage.reconnectAgent', 'Reconnect'),\n icon: RefreshCw,\n onSelect: () => onStart(agent),\n });\n }\n if (\n (status === 'ACTIVE' ||\n status === 'STOPPED' ||\n status === 'OFFLINE' ||\n status === 'DEGRADED') &&\n onDisconnect\n ) {\n menuItems.push({\n key: 'disconnect',\n label:\n status === 'OFFLINE'\n ? tr('agentsPage.clearConnection', 'Clear Connection')\n : tr('agentsPage.disconnect', 'Disconnect'),\n icon: Square,\n onSelect: () => onDisconnect(agent),\n variant: 'danger',\n });\n }\n if (onEditTunnelUrl) {\n menuItems.push({\n key: 'edit-tunnel',\n label: tr('agentsPage.editTunnel', 'Edit Tunnel'),\n icon: Link,\n onSelect: () => onEditTunnelUrl(agent),\n });\n }\n if (handleConfigure) {\n menuItems.push({\n key: 'settings',\n label: tr('agentsPage.settings', 'Settings'),\n icon: Settings,\n onSelect: () => handleConfigure(agent),\n });\n }\n\n return (\n <div\n role=\"button\"\n tabIndex={0}\n className={`bg-bg-surface border rounded-card p-4 h-full hover:border-border-strong hover:shadow-[var(--shadow-elevation-1)] transition-[colors,box-shadow] duration-150 cursor-pointer focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-border-focus focus-visible:ring-offset-2 focus-visible:ring-offset-bg-canvas ${\n selected\n ? 'border-action-primary-bg ring-1 ring-action-primary-bg/30'\n : 'border-border-default'\n } ${className}`}\n onClick={() => onSelect?.(agent)}\n onKeyDown={(e) => {\n if (e.target !== e.currentTarget) return; // children (menu, checkbox, links) handle their own keys\n if (e.key === 'Enter' || e.key === ' ') {\n e.preventDefault();\n onSelect?.(agent);\n }\n }}\n >\n <div className=\"flex items-start justify-between gap-2\">\n <div className=\"flex items-start gap-2 sm:gap-3 min-w-0 flex-1\">\n {/* Selection Checkbox */}\n {onToggleSelect && (\n <span\n className=\"flex flex-shrink-0 items-center justify-center max-md:min-h-11 max-md:min-w-11 max-md:-m-2 md:mt-0.5\"\n onClick={(e) => {\n e.stopPropagation();\n if (e.target === e.currentTarget) onToggleSelect();\n }}\n >\n <Checkbox\n checked={selected}\n onCheckedChange={() => onToggleSelect()}\n onClick={(e) => e.stopPropagation()}\n aria-label={tr('agentsPage.selectAgent', 'Select {{name}}', { name: agent.name })}\n />\n </span>\n )}\n <div className=\"flex-shrink-0 size-8 sm:size-10 rounded-lg bg-bg-sunken flex items-center justify-center\">\n <Server className=\"size-4 sm:size-5 text-text-secondary\" aria-hidden=\"true\" />\n </div>\n <div className=\"min-w-0 flex-1\">\n <div className=\"flex items-center gap-1.5 sm:gap-2 min-w-0\">\n <h3 className=\"font-semibold text-text-primary truncate min-w-0 text-sm sm:text-base\">\n {agent.name}\n </h3>\n <PlatformIcon\n platform={agent.platform}\n className=\"size-4 flex-shrink-0 text-text-secondary\"\n />\n </div>\n <p className=\"text-xs sm:text-sm text-text-secondary truncate\">\n {agent.hostname || `ID: ${agent.id}`}\n </p>\n {(() => {\n // Always-visible (not just hover) guidance when degraded, so the\n // user knows whether it's self-healing or needs their action.\n const health = classifyAgentHealth(status, healthErrorMessage ?? null);\n if (health.phase === 'none') return null;\n return (\n <p\n className={`mt-1 flex items-start gap-1 text-xs ${\n health.phase === 'attention'\n ? 'text-status-warning-text'\n : 'text-content-tertiary'\n }`}\n >\n {health.inProgress && (\n <RefreshCw\n className=\"size-3 mt-0.5 flex-shrink-0 animate-spin\"\n aria-hidden=\"true\"\n />\n )}\n <span>\n <span className=\"font-medium\">{health.label}.</span> {health.actionHint}\n </span>\n </p>\n );\n })()}\n </div>\n </div>\n {/* Status and Actions - always visible */}\n <div className=\"flex items-center gap-1 sm:gap-2 flex-shrink-0\">\n <AgentStatusBadge\n status={status}\n reason={healthErrorMessage ?? null}\n linkTo={`/vibecontrols/agents/${agent.id}?tab=connection`}\n onClick={(event) => {\n // The card itself has a `onSelect` click target that\n // routes to the agent detail. Stop propagation so a\n // badge click doesn't *also* fire that handler — the\n // anchor's href already routes to the right tab and\n // React Router picks it up via the surrounding shell.\n event.stopPropagation();\n }}\n />\n {(handleConfigure || onStart || onStop) && menuItems.length > 0 && (\n <ActionMenu\n ariaLabel={tr('agentsPage.agentActionsMenu', 'Agent actions menu')}\n title={tr('agentsPage.agentActions', 'Agent actions')}\n align=\"end\"\n items={menuItems}\n />\n )}\n </div>\n </div>\n\n <div className=\"mt-4 grid grid-cols-2 gap-3 text-sm\">\n <div className=\"flex items-center gap-2 text-text-secondary\">\n <Clock className=\"size-4\" aria-hidden=\"true\" />\n <span>{formatLastHeartbeat(agent.lastHeartbeat)}</span>\n </div>\n {agent.architecture && (\n <div className=\"flex items-center gap-2 text-text-secondary\">\n <Cpu className=\"size-4\" aria-hidden=\"true\" />\n <span>{agent.architecture}</span>\n </div>\n )}\n </div>\n\n {agent.tunnelUrl && (\n // Plain \"Tunnel: <link>\" row. The colored status chip that used\n // to sit here was a redundant second status surface — the\n // canonical state lives in the <AgentStatusBadge> top-right.\n // The Globe glyph stays as a neutral icon (no status color).\n <div className=\"mt-3 flex items-center gap-2 text-sm\">\n <Globe className=\"size-4 flex-shrink-0 text-text-secondary\" aria-hidden=\"true\" />\n <span className=\"text-text-secondary flex-shrink-0\">Tunnel:</span>\n <a\n href={agent.tunnelUrl}\n target=\"_blank\"\n rel=\"noopener noreferrer\"\n onClick={(e) => e.stopPropagation()}\n aria-label={tr('agentsPage.tunnelOpensInNewTab', '{{url}} (opens in new tab)', {\n url: agent.tunnelUrl,\n })}\n className=\"min-w-0 truncate text-text-link hover:text-text-linkHover hover:underline\"\n >\n {agent.tunnelUrl}\n </a>\n </div>\n )}\n\n {agent.capabilities && agent.capabilities.length > 0 && (\n <div className=\"mt-3 flex flex-wrap gap-1\">\n {agent.capabilities.slice(0, 4).map((cap) => (\n <span\n key={cap}\n className=\"px-2 py-0.5 text-xs bg-bg-sunken text-text-secondary rounded-pill\"\n >\n {cap}\n </span>\n ))}\n {agent.capabilities.length > 4 && (\n <span className=\"px-2 py-0.5 text-xs bg-bg-sunken text-text-secondary rounded-pill\">\n +{agent.capabilities.length - 4}\n </span>\n )}\n </div>\n )}\n </div>\n );\n};\n"],"mappings":"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"}
|
|
@@ -5,7 +5,7 @@ import { useState as i } from "react";
|
|
|
5
5
|
import { AlertTriangle as a, Eye as o, EyeOff as s, KeyRound as c, Link2 as l, Loader2 as u, ShieldCheck as d } from "lucide-react";
|
|
6
6
|
import { jsx as f, jsxs as p } from "react/jsx-runtime";
|
|
7
7
|
//#region src/components/agents/AgentConnectionRecovery.tsx
|
|
8
|
-
var m = "rounded-
|
|
8
|
+
var m = "rounded-card border border-border-default bg-bg-surface p-4", h = "inline-flex items-center gap-2 px-3 py-2 text-sm font-medium rounded-button bg-action-primary-bg text-action-primary-text hover:bg-action-primary-bgHover transition-colors disabled:opacity-50 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-border-focus", g = "w-full px-3 py-2 text-base md:text-sm rounded-input bg-bg-sunken border border-border-default text-text-primary focus:outline-none focus:ring-2 focus:ring-border-focus font-mono", _ = "mb-1 block text-xs font-medium text-text-muted";
|
|
9
9
|
function v(e) {
|
|
10
10
|
return e instanceof Error ? e.message : String(e);
|
|
11
11
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"AgentConnectionRecovery.js","names":[],"sources":["../../../src/components/agents/AgentConnectionRecovery.tsx"],"sourcesContent":["import { useState } from 'react';\nimport { Eye, EyeOff, KeyRound, Link2, Loader2, ShieldCheck, AlertTriangle } from 'lucide-react';\n\nimport {\n useSetAgentApiKeyMutation,\n useUpdateAgentTunnelUrlMutation,\n} from '@/generated/wspace-operations';\nimport { useTr } from '../../shared/hooks/useTr';\nimport { useField } from '../../shared/hooks/useField';\n\ninterface AgentConnectionRecoveryProps {\n agentId: string;\n currentTunnelUrl?: string | null;\n /** Refetch the agent after a successful in-place update. */\n onUpdated?: () => void;\n}\n\nconst CARD = 'rounded-lg border border-border-default bg-bg-surface p-4 sm:p-5';\nconst BTN =\n 'inline-flex items-center gap-2 px-3 py-2 text-sm font-medium rounded-md bg-action-primary-bg text-action-primary-text hover:bg-action-primary-bgHover transition-colors disabled:opacity-50 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-border-focus';\nconst INPUT =\n 'w-full px-3 py-2 text-base md:text-sm rounded-md bg-bg-sunken border border-border-default text-text-primary focus:outline-none focus:ring-2 focus:ring-border-focus font-mono';\nconst LABEL = 'mb-1 block text-xs font-medium text-text-muted';\n\nfunction errMsg(e: unknown): string {\n return e instanceof Error ? e.message : String(e);\n}\n\n/**\n * Emergency connection recovery for an existing agent — used when gateway\n * auto-report is broken (so the agent can't refresh its own key/tunnel) and\n * the operator must fix it in place WITHOUT delete + re-add (which loses\n * sessions / notes / bookmarks).\n * - \"Update API key\": paste the value from `vibe key`; the backend probes the\n * agent with it and only saves if the agent accepts it.\n * - \"Update tunnel URL\": paste the new quick-tunnel URL; the backend verifies\n * it's the same machine (hostname) before saving.\n */\nexport function AgentConnectionRecovery({\n agentId,\n currentTunnelUrl,\n onUpdated,\n}: AgentConnectionRecoveryProps) {\n const tr = useTr();\n\n // ── API key ─────────────────────────────────────────────────────────────\n const [keyInput, setKeyInput] = useState('');\n const [showKey, setShowKey] = useState(false);\n const [setKey, { loading: settingKey }] = useSetAgentApiKeyMutation();\n const [keyResult, setKeyResult] = useState<{ ok: boolean; msg: string } | null>(null);\n const keyField = useField();\n\n const handleSetKey = async () => {\n setKeyResult(null);\n const key = keyInput.trim();\n if (!key) return;\n try {\n await setKey({ variables: { agentId, apiKey: key } });\n setKeyResult({\n ok: true,\n msg: tr(\n 'agentDetails.recovery.keySaved',\n 'API key validated and saved. The agent is reachable again.'\n ),\n });\n setKeyInput('');\n onUpdated?.();\n } catch (e) {\n setKeyResult({ ok: false, msg: errMsg(e) });\n }\n };\n\n // ── Tunnel URL ──────────────────────────────────────────────────────────\n const [urlInput, setUrlInput] = useState(currentTunnelUrl ?? '');\n const [updateUrl, { loading: updatingUrl }] = useUpdateAgentTunnelUrlMutation();\n const [urlResult, setUrlResult] = useState<{ ok: boolean; msg: string } | null>(null);\n const urlField = useField();\n\n const handleUpdateUrl = async () => {\n setUrlResult(null);\n const url = urlInput.trim();\n if (!url) return;\n try {\n await updateUrl({ variables: { agentId, tunnelUrl: url } });\n setUrlResult({\n ok: true,\n msg: tr(\n 'agentDetails.recovery.urlSaved',\n 'Tunnel URL verified and updated. The agent is back online.'\n ),\n });\n onUpdated?.();\n } catch (e) {\n setUrlResult({ ok: false, msg: errMsg(e) });\n }\n };\n\n const result = (r: { ok: boolean; msg: string } | null) =>\n r ? (\n <p\n role=\"status\"\n className={`mt-2 flex items-start gap-2 text-xs ${\n r.ok ? 'text-status-success-text' : 'text-status-error-text'\n }`}\n >\n {r.ok ? (\n <ShieldCheck className=\"size-4 flex-shrink-0 mt-0.5\" aria-hidden=\"true\" />\n ) : (\n <AlertTriangle className=\"size-4 flex-shrink-0 mt-0.5\" aria-hidden=\"true\" />\n )}\n <span>{r.msg}</span>\n </p>\n ) : null;\n\n return (\n <div className=\"space-y-4\">\n <section className={CARD}>\n <div className=\"flex items-center gap-2 mb-1\">\n <KeyRound className=\"size-5 text-text-secondary\" aria-hidden=\"true\" />\n <h3 className=\"text-base font-semibold text-text-primary\">\n {tr('agentDetails.recovery.keyTitle', 'Update API key (no re-add)')}\n </h3>\n </div>\n <p className=\"text-xs text-text-muted mb-3\">\n {tr(\n 'agentDetails.recovery.keyHint',\n 'If the agent’s API key changed and it can’t auto-report (gateway auth down), paste the value from `vibe key` on the host. It’s validated against the agent before saving — your sessions and data stay intact.'\n )}\n </p>\n <div className=\"flex flex-col sm:flex-row sm:items-end gap-2\">\n <div className=\"flex-1 min-w-0\">\n <label htmlFor={keyField.id} className={LABEL}>\n {tr('agentDetails.recovery.keyLabel', 'Agent API key')}\n </label>\n <div className=\"relative\">\n <input\n {...keyField.inputProps}\n type={showKey ? 'text' : 'password'}\n autoComplete=\"off\"\n value={keyInput}\n onChange={(e) => setKeyInput(e.target.value)}\n placeholder=\"vcak_…\"\n className={`${INPUT} pr-10`}\n />\n <button\n type=\"button\"\n onClick={() => setShowKey((v) => !v)}\n aria-label={\n showKey\n ? tr('agentDetails.recovery.hideKey', 'Hide API key')\n : tr('agentDetails.recovery.showKey', 'Show API key')\n }\n className=\"absolute right-1 top-1/2 -translate-y-1/2 inline-flex size-8 items-center justify-center rounded-button text-text-muted transition-colors hover:text-text-primary focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-border-focus\"\n >\n {showKey ? (\n <EyeOff className=\"size-4\" aria-hidden=\"true\" />\n ) : (\n <Eye className=\"size-4\" aria-hidden=\"true\" />\n )}\n </button>\n </div>\n </div>\n <button\n type=\"button\"\n onClick={handleSetKey}\n disabled={settingKey || keyInput.trim().length === 0}\n aria-busy={settingKey}\n className={`${BTN} shrink-0`}\n >\n {settingKey ? (\n <Loader2 className=\"size-4 animate-spin\" aria-hidden=\"true\" />\n ) : (\n <ShieldCheck className=\"size-4\" aria-hidden=\"true\" />\n )}\n {tr('agentDetails.recovery.validateSave', 'Validate & save')}\n </button>\n </div>\n {result(keyResult)}\n </section>\n\n <section className={CARD}>\n <div className=\"flex items-center gap-2 mb-1\">\n <Link2 className=\"size-5 text-text-secondary\" aria-hidden=\"true\" />\n <h3 className=\"text-base font-semibold text-text-primary\">\n {tr('agentDetails.recovery.urlTitle', 'Update tunnel URL (re-pair)')}\n </h3>\n </div>\n <p className=\"text-xs text-text-muted mb-3\">\n {tr(\n 'agentDetails.recovery.urlHint',\n 'After a quick-tunnel rotation, paste the new URL here. The backend verifies it points at the same machine before saving — no re-registration, no data loss.'\n )}\n </p>\n <div className=\"flex flex-col sm:flex-row sm:items-end gap-2\">\n <div className=\"flex-1 min-w-0\">\n <label htmlFor={urlField.id} className={LABEL}>\n {tr('agentDetails.recovery.urlLabel', 'Tunnel URL')}\n </label>\n <input\n {...urlField.inputProps}\n type=\"url\"\n value={urlInput}\n onChange={(e) => setUrlInput(e.target.value)}\n placeholder=\"https://….trycloudflare.com\"\n className={INPUT}\n />\n </div>\n <button\n type=\"button\"\n onClick={handleUpdateUrl}\n disabled={updatingUrl || urlInput.trim().length === 0}\n aria-busy={updatingUrl}\n className={`${BTN} shrink-0`}\n >\n {updatingUrl ? (\n <Loader2 className=\"size-4 animate-spin\" aria-hidden=\"true\" />\n ) : (\n <Link2 className=\"size-4\" aria-hidden=\"true\" />\n )}\n {tr('agentDetails.recovery.verifyUpdate', 'Verify & update')}\n </button>\n </div>\n {result(urlResult)}\n </section>\n </div>\n );\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"AgentConnectionRecovery.js","names":[],"sources":["../../../src/components/agents/AgentConnectionRecovery.tsx"],"sourcesContent":["import { useState } from 'react';\nimport { Eye, EyeOff, KeyRound, Link2, Loader2, ShieldCheck, AlertTriangle } from 'lucide-react';\n\nimport {\n useSetAgentApiKeyMutation,\n useUpdateAgentTunnelUrlMutation,\n} from '@/generated/wspace-operations';\nimport { useTr } from '../../shared/hooks/useTr';\nimport { useField } from '../../shared/hooks/useField';\n\ninterface AgentConnectionRecoveryProps {\n agentId: string;\n currentTunnelUrl?: string | null;\n /** Refetch the agent after a successful in-place update. */\n onUpdated?: () => void;\n}\n\nconst CARD = 'rounded-card border border-border-default bg-bg-surface p-4';\nconst BTN =\n 'inline-flex items-center gap-2 px-3 py-2 text-sm font-medium rounded-button bg-action-primary-bg text-action-primary-text hover:bg-action-primary-bgHover transition-colors disabled:opacity-50 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-border-focus';\nconst INPUT =\n 'w-full px-3 py-2 text-base md:text-sm rounded-input bg-bg-sunken border border-border-default text-text-primary focus:outline-none focus:ring-2 focus:ring-border-focus font-mono';\nconst LABEL = 'mb-1 block text-xs font-medium text-text-muted';\n\nfunction errMsg(e: unknown): string {\n return e instanceof Error ? e.message : String(e);\n}\n\n/**\n * Emergency connection recovery for an existing agent — used when gateway\n * auto-report is broken (so the agent can't refresh its own key/tunnel) and\n * the operator must fix it in place WITHOUT delete + re-add (which loses\n * sessions / notes / bookmarks).\n * - \"Update API key\": paste the value from `vibe key`; the backend probes the\n * agent with it and only saves if the agent accepts it.\n * - \"Update tunnel URL\": paste the new quick-tunnel URL; the backend verifies\n * it's the same machine (hostname) before saving.\n */\nexport function AgentConnectionRecovery({\n agentId,\n currentTunnelUrl,\n onUpdated,\n}: AgentConnectionRecoveryProps) {\n const tr = useTr();\n\n // ── API key ─────────────────────────────────────────────────────────────\n const [keyInput, setKeyInput] = useState('');\n const [showKey, setShowKey] = useState(false);\n const [setKey, { loading: settingKey }] = useSetAgentApiKeyMutation();\n const [keyResult, setKeyResult] = useState<{ ok: boolean; msg: string } | null>(null);\n const keyField = useField();\n\n const handleSetKey = async () => {\n setKeyResult(null);\n const key = keyInput.trim();\n if (!key) return;\n try {\n await setKey({ variables: { agentId, apiKey: key } });\n setKeyResult({\n ok: true,\n msg: tr(\n 'agentDetails.recovery.keySaved',\n 'API key validated and saved. The agent is reachable again.'\n ),\n });\n setKeyInput('');\n onUpdated?.();\n } catch (e) {\n setKeyResult({ ok: false, msg: errMsg(e) });\n }\n };\n\n // ── Tunnel URL ──────────────────────────────────────────────────────────\n const [urlInput, setUrlInput] = useState(currentTunnelUrl ?? '');\n const [updateUrl, { loading: updatingUrl }] = useUpdateAgentTunnelUrlMutation();\n const [urlResult, setUrlResult] = useState<{ ok: boolean; msg: string } | null>(null);\n const urlField = useField();\n\n const handleUpdateUrl = async () => {\n setUrlResult(null);\n const url = urlInput.trim();\n if (!url) return;\n try {\n await updateUrl({ variables: { agentId, tunnelUrl: url } });\n setUrlResult({\n ok: true,\n msg: tr(\n 'agentDetails.recovery.urlSaved',\n 'Tunnel URL verified and updated. The agent is back online.'\n ),\n });\n onUpdated?.();\n } catch (e) {\n setUrlResult({ ok: false, msg: errMsg(e) });\n }\n };\n\n const result = (r: { ok: boolean; msg: string } | null) =>\n r ? (\n <p\n role=\"status\"\n className={`mt-2 flex items-start gap-2 text-xs ${\n r.ok ? 'text-status-success-text' : 'text-status-error-text'\n }`}\n >\n {r.ok ? (\n <ShieldCheck className=\"size-4 flex-shrink-0 mt-0.5\" aria-hidden=\"true\" />\n ) : (\n <AlertTriangle className=\"size-4 flex-shrink-0 mt-0.5\" aria-hidden=\"true\" />\n )}\n <span>{r.msg}</span>\n </p>\n ) : null;\n\n return (\n <div className=\"space-y-4\">\n <section className={CARD}>\n <div className=\"flex items-center gap-2 mb-1\">\n <KeyRound className=\"size-5 text-text-secondary\" aria-hidden=\"true\" />\n <h3 className=\"text-base font-semibold text-text-primary\">\n {tr('agentDetails.recovery.keyTitle', 'Update API key (no re-add)')}\n </h3>\n </div>\n <p className=\"text-xs text-text-muted mb-3\">\n {tr(\n 'agentDetails.recovery.keyHint',\n 'If the agent’s API key changed and it can’t auto-report (gateway auth down), paste the value from `vibe key` on the host. It’s validated against the agent before saving — your sessions and data stay intact.'\n )}\n </p>\n <div className=\"flex flex-col sm:flex-row sm:items-end gap-2\">\n <div className=\"flex-1 min-w-0\">\n <label htmlFor={keyField.id} className={LABEL}>\n {tr('agentDetails.recovery.keyLabel', 'Agent API key')}\n </label>\n <div className=\"relative\">\n <input\n {...keyField.inputProps}\n type={showKey ? 'text' : 'password'}\n autoComplete=\"off\"\n value={keyInput}\n onChange={(e) => setKeyInput(e.target.value)}\n placeholder=\"vcak_…\"\n className={`${INPUT} pr-10`}\n />\n <button\n type=\"button\"\n onClick={() => setShowKey((v) => !v)}\n aria-label={\n showKey\n ? tr('agentDetails.recovery.hideKey', 'Hide API key')\n : tr('agentDetails.recovery.showKey', 'Show API key')\n }\n className=\"absolute right-1 top-1/2 -translate-y-1/2 inline-flex size-8 items-center justify-center rounded-button text-text-muted transition-colors hover:text-text-primary focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-border-focus\"\n >\n {showKey ? (\n <EyeOff className=\"size-4\" aria-hidden=\"true\" />\n ) : (\n <Eye className=\"size-4\" aria-hidden=\"true\" />\n )}\n </button>\n </div>\n </div>\n <button\n type=\"button\"\n onClick={handleSetKey}\n disabled={settingKey || keyInput.trim().length === 0}\n aria-busy={settingKey}\n className={`${BTN} shrink-0`}\n >\n {settingKey ? (\n <Loader2 className=\"size-4 animate-spin\" aria-hidden=\"true\" />\n ) : (\n <ShieldCheck className=\"size-4\" aria-hidden=\"true\" />\n )}\n {tr('agentDetails.recovery.validateSave', 'Validate & save')}\n </button>\n </div>\n {result(keyResult)}\n </section>\n\n <section className={CARD}>\n <div className=\"flex items-center gap-2 mb-1\">\n <Link2 className=\"size-5 text-text-secondary\" aria-hidden=\"true\" />\n <h3 className=\"text-base font-semibold text-text-primary\">\n {tr('agentDetails.recovery.urlTitle', 'Update tunnel URL (re-pair)')}\n </h3>\n </div>\n <p className=\"text-xs text-text-muted mb-3\">\n {tr(\n 'agentDetails.recovery.urlHint',\n 'After a quick-tunnel rotation, paste the new URL here. The backend verifies it points at the same machine before saving — no re-registration, no data loss.'\n )}\n </p>\n <div className=\"flex flex-col sm:flex-row sm:items-end gap-2\">\n <div className=\"flex-1 min-w-0\">\n <label htmlFor={urlField.id} className={LABEL}>\n {tr('agentDetails.recovery.urlLabel', 'Tunnel URL')}\n </label>\n <input\n {...urlField.inputProps}\n type=\"url\"\n value={urlInput}\n onChange={(e) => setUrlInput(e.target.value)}\n placeholder=\"https://….trycloudflare.com\"\n className={INPUT}\n />\n </div>\n <button\n type=\"button\"\n onClick={handleUpdateUrl}\n disabled={updatingUrl || urlInput.trim().length === 0}\n aria-busy={updatingUrl}\n className={`${BTN} shrink-0`}\n >\n {updatingUrl ? (\n <Loader2 className=\"size-4 animate-spin\" aria-hidden=\"true\" />\n ) : (\n <Link2 className=\"size-4\" aria-hidden=\"true\" />\n )}\n {tr('agentDetails.recovery.verifyUpdate', 'Verify & update')}\n </button>\n </div>\n {result(urlResult)}\n </section>\n </div>\n );\n}\n"],"mappings":"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"}
|
|
@@ -181,7 +181,7 @@ function Oe({ onSubmit: Oe, onCancel: h, isLoading: g, onNavigate: ke, targets:
|
|
|
181
181
|
children: [/* @__PURE__ */ d("div", {
|
|
182
182
|
className: "flex items-center gap-2",
|
|
183
183
|
children: [/* @__PURE__ */ u(ue, {
|
|
184
|
-
className: "size-4 text-text-
|
|
184
|
+
className: "size-4 text-text-secondary",
|
|
185
185
|
"aria-hidden": "true"
|
|
186
186
|
}), /* @__PURE__ */ u("span", {
|
|
187
187
|
className: "text-sm font-medium text-text-primary",
|
|
@@ -508,7 +508,7 @@ function Oe({ onSubmit: Oe, onCancel: h, isLoading: g, onNavigate: ke, targets:
|
|
|
508
508
|
className: "space-y-3",
|
|
509
509
|
children: [/* @__PURE__ */ d("div", {
|
|
510
510
|
role: "status",
|
|
511
|
-
className: `flex items-center gap-3 p-3 rounded-lg border ${R === null ? "border-border-default bg-bg-sunken" : R.ok ? "border-status-success-border bg-status-success-
|
|
511
|
+
className: `flex items-center gap-3 p-3 rounded-lg border ${R === null ? "border-border-default bg-bg-sunken" : R.ok ? "border-status-success-border bg-status-success-bg-subtle" : "border-status-error-border bg-status-error-bg-subtle"}`,
|
|
512
512
|
children: [L && !R ? /* @__PURE__ */ u(s, {
|
|
513
513
|
className: "size-5 text-text-secondary animate-spin",
|
|
514
514
|
"aria-hidden": "true"
|
|
@@ -548,7 +548,7 @@ function Oe({ onSubmit: Oe, onCancel: h, isLoading: g, onNavigate: ke, targets:
|
|
|
548
548
|
})]
|
|
549
549
|
}), /* @__PURE__ */ d("div", {
|
|
550
550
|
role: "status",
|
|
551
|
-
className: `flex items-center gap-3 p-3 rounded-lg border ${B === null ? "border-border-default bg-bg-sunken" : B.ok ? "border-status-success-border bg-status-success-
|
|
551
|
+
className: `flex items-center gap-3 p-3 rounded-lg border ${B === null ? "border-border-default bg-bg-sunken" : B.ok ? "border-status-success-border bg-status-success-bg-subtle" : "border-status-error-border bg-status-error-bg-subtle"}`,
|
|
552
552
|
children: [L && !B ? /* @__PURE__ */ u(s, {
|
|
553
553
|
className: "size-5 text-text-secondary animate-spin",
|
|
554
554
|
"aria-hidden": "true"
|