@chrysb/alphaclaw 0.8.1-beta.0 → 0.8.1-beta.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/lib/public/js/components/routes/webhooks-route.js +1 -1
- package/lib/public/js/components/webhooks/create-webhook-modal/index.js +176 -0
- package/lib/public/js/components/webhooks/helpers.js +106 -0
- package/lib/public/js/components/webhooks/index.js +148 -0
- package/lib/public/js/components/webhooks/request-history/index.js +241 -0
- package/lib/public/js/components/webhooks/request-history/use-request-history.js +167 -0
- package/lib/public/js/components/webhooks/webhook-detail/index.js +386 -0
- package/lib/public/js/components/webhooks/webhook-detail/use-webhook-detail.js +277 -0
- package/lib/public/js/components/webhooks/webhook-list/index.js +96 -0
- package/lib/public/js/components/webhooks/webhook-list/use-webhook-list.js +30 -0
- package/lib/public/js/lib/api.js +35 -1
- package/lib/server/db/webhooks/index.js +144 -0
- package/lib/server/db/webhooks/schema.js +13 -0
- package/lib/server/init/register-server-routes.js +19 -0
- package/lib/server/oauth-callback-middleware.js +34 -0
- package/lib/server/routes/proxy.js +2 -0
- package/lib/server/routes/webhooks.js +126 -18
- package/lib/server/webhook-middleware.js +6 -1
- package/lib/server.js +12 -0
- package/package.json +1 -1
- package/lib/public/js/components/webhooks.js +0 -1259
|
@@ -1,1259 +0,0 @@
|
|
|
1
|
-
import { h } from "https://esm.sh/preact";
|
|
2
|
-
import { useCallback, useMemo, useState } from "https://esm.sh/preact/hooks";
|
|
3
|
-
import htm from "https://esm.sh/htm";
|
|
4
|
-
import { usePolling } from "../hooks/usePolling.js";
|
|
5
|
-
import {
|
|
6
|
-
kNoDestinationSessionValue,
|
|
7
|
-
useDestinationSessionSelection,
|
|
8
|
-
} from "../hooks/use-destination-session-selection.js";
|
|
9
|
-
import {
|
|
10
|
-
createWebhook,
|
|
11
|
-
deleteWebhook,
|
|
12
|
-
fetchAgents,
|
|
13
|
-
fetchWebhookDetail,
|
|
14
|
-
fetchWebhookRequest,
|
|
15
|
-
fetchWebhookRequests,
|
|
16
|
-
fetchWebhooks,
|
|
17
|
-
sendAgentMessage,
|
|
18
|
-
} from "../lib/api.js";
|
|
19
|
-
import {
|
|
20
|
-
formatLocaleDateTime,
|
|
21
|
-
formatLocaleDateTimeWithTodayTime,
|
|
22
|
-
} from "../lib/format.js";
|
|
23
|
-
import { showToast } from "./toast.js";
|
|
24
|
-
import { PageHeader } from "./page-header.js";
|
|
25
|
-
import { ConfirmDialog } from "./confirm-dialog.js";
|
|
26
|
-
import { ActionButton } from "./action-button.js";
|
|
27
|
-
import { AgentSendModal } from "./agent-send-modal.js";
|
|
28
|
-
import { ModalShell } from "./modal-shell.js";
|
|
29
|
-
import { Badge } from "./badge.js";
|
|
30
|
-
import { CloseIcon } from "./icons.js";
|
|
31
|
-
import { SessionSelectField } from "./session-select-field.js";
|
|
32
|
-
|
|
33
|
-
const html = htm.bind(h);
|
|
34
|
-
const kNamePattern = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
|
|
35
|
-
const kStatusFilters = ["all", "success", "error"];
|
|
36
|
-
|
|
37
|
-
const formatDateTime = (value) => {
|
|
38
|
-
return formatLocaleDateTime(value, { fallback: "—" });
|
|
39
|
-
};
|
|
40
|
-
|
|
41
|
-
const formatLastReceived = (value) => {
|
|
42
|
-
return formatLocaleDateTimeWithTodayTime(value, { fallback: "—" });
|
|
43
|
-
};
|
|
44
|
-
|
|
45
|
-
const formatBytes = (size) => {
|
|
46
|
-
const bytes = Number(size || 0);
|
|
47
|
-
if (!Number.isFinite(bytes) || bytes <= 0) return "0B";
|
|
48
|
-
if (bytes < 1024) return `${bytes}B`;
|
|
49
|
-
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)}KB`;
|
|
50
|
-
return `${(bytes / (1024 * 1024)).toFixed(1)}MB`;
|
|
51
|
-
};
|
|
52
|
-
|
|
53
|
-
const healthClassName = (health) => {
|
|
54
|
-
if (health === "red") return "bg-red-500";
|
|
55
|
-
if (health === "yellow") return "bg-yellow-500";
|
|
56
|
-
return "bg-green-500";
|
|
57
|
-
};
|
|
58
|
-
|
|
59
|
-
const getRequestStatusTone = (status) => {
|
|
60
|
-
if (status === "success") {
|
|
61
|
-
return {
|
|
62
|
-
dotClass: "bg-green-500/90",
|
|
63
|
-
textClass: "text-green-500/90",
|
|
64
|
-
};
|
|
65
|
-
}
|
|
66
|
-
if (status === "error") {
|
|
67
|
-
return {
|
|
68
|
-
dotClass: "bg-red-500/90",
|
|
69
|
-
textClass: "text-red-500/90",
|
|
70
|
-
};
|
|
71
|
-
}
|
|
72
|
-
return {
|
|
73
|
-
dotClass: "bg-gray-500/70",
|
|
74
|
-
textClass: "text-gray-400",
|
|
75
|
-
};
|
|
76
|
-
};
|
|
77
|
-
|
|
78
|
-
const formatAgentFallbackName = (agentId = "") =>
|
|
79
|
-
String(agentId || "")
|
|
80
|
-
.trim()
|
|
81
|
-
.split(/[-_\s]+/)
|
|
82
|
-
.filter(Boolean)
|
|
83
|
-
.map((part) => part.charAt(0).toUpperCase() + part.slice(1))
|
|
84
|
-
.join(" ") || "Main Agent";
|
|
85
|
-
|
|
86
|
-
const jsonPretty = (value) => {
|
|
87
|
-
if (typeof value === "string") {
|
|
88
|
-
try {
|
|
89
|
-
const parsed = JSON.parse(value);
|
|
90
|
-
return JSON.stringify(parsed, null, 2);
|
|
91
|
-
} catch {
|
|
92
|
-
return value;
|
|
93
|
-
}
|
|
94
|
-
}
|
|
95
|
-
try {
|
|
96
|
-
return JSON.stringify(value || {}, null, 2);
|
|
97
|
-
} catch {
|
|
98
|
-
return String(value || "");
|
|
99
|
-
}
|
|
100
|
-
};
|
|
101
|
-
|
|
102
|
-
const buildWebhookDebugMessage = ({
|
|
103
|
-
hookName = "",
|
|
104
|
-
webhook = null,
|
|
105
|
-
request = null,
|
|
106
|
-
}) => {
|
|
107
|
-
const hookPath =
|
|
108
|
-
String(webhook?.path || "").trim() ||
|
|
109
|
-
(hookName ? `/hooks/${hookName}` : "/hooks/unknown");
|
|
110
|
-
const gatewayStatus =
|
|
111
|
-
request?.gatewayStatus == null ? "n/a" : String(request.gatewayStatus);
|
|
112
|
-
return [
|
|
113
|
-
"Investigate this failed webhook request and share findings before fixing anything.",
|
|
114
|
-
"Reply with your diagnosis first, including the likely root cause, any relevant risks, and what you would change if I approve a fix.",
|
|
115
|
-
"",
|
|
116
|
-
`Webhook: ${hookPath}`,
|
|
117
|
-
`Request ID: ${String(request?.id || "unknown")}`,
|
|
118
|
-
`Time: ${String(request?.createdAt || "unknown")}`,
|
|
119
|
-
`Method: ${String(request?.method || "unknown")}`,
|
|
120
|
-
`Source IP: ${String(request?.sourceIp || "unknown")}`,
|
|
121
|
-
`Gateway status: ${gatewayStatus}`,
|
|
122
|
-
`Transform path: ${String(webhook?.transformPath || "unknown")}`,
|
|
123
|
-
`Payload truncated: ${request?.payloadTruncated ? "yes" : "no"}`,
|
|
124
|
-
"",
|
|
125
|
-
"Headers:",
|
|
126
|
-
jsonPretty(request?.headers),
|
|
127
|
-
"",
|
|
128
|
-
"Payload:",
|
|
129
|
-
jsonPretty(request?.payload),
|
|
130
|
-
"",
|
|
131
|
-
"Gateway response:",
|
|
132
|
-
jsonPretty(request?.gatewayBody),
|
|
133
|
-
].join("\n");
|
|
134
|
-
};
|
|
135
|
-
|
|
136
|
-
const CreateWebhookModal = ({
|
|
137
|
-
visible,
|
|
138
|
-
name,
|
|
139
|
-
onNameChange,
|
|
140
|
-
canCreate,
|
|
141
|
-
creating,
|
|
142
|
-
onCreate = () => {},
|
|
143
|
-
onClose,
|
|
144
|
-
}) => {
|
|
145
|
-
const {
|
|
146
|
-
sessions: selectableSessions,
|
|
147
|
-
loading: loadingSessions,
|
|
148
|
-
error: destinationLoadError,
|
|
149
|
-
destinationSessionKey,
|
|
150
|
-
setDestinationSessionKey,
|
|
151
|
-
selectedDestination,
|
|
152
|
-
} = useDestinationSessionSelection({
|
|
153
|
-
enabled: visible,
|
|
154
|
-
resetKey: String(visible),
|
|
155
|
-
});
|
|
156
|
-
|
|
157
|
-
const normalized = String(name || "")
|
|
158
|
-
.trim()
|
|
159
|
-
.toLowerCase();
|
|
160
|
-
const previewName = normalized || "{name}";
|
|
161
|
-
const previewUrl = `${window.location.origin}/hooks/${previewName}`;
|
|
162
|
-
if (!visible) return null;
|
|
163
|
-
return html`
|
|
164
|
-
<${ModalShell}
|
|
165
|
-
visible=${visible}
|
|
166
|
-
onClose=${onClose}
|
|
167
|
-
panelClassName="bg-modal border border-border rounded-xl p-5 max-w-lg w-full space-y-4"
|
|
168
|
-
>
|
|
169
|
-
<${PageHeader}
|
|
170
|
-
title="Create Webhook"
|
|
171
|
-
actions=${html`
|
|
172
|
-
<button
|
|
173
|
-
type="button"
|
|
174
|
-
onclick=${onClose}
|
|
175
|
-
class="h-8 w-8 inline-flex items-center justify-center rounded-lg ac-btn-secondary"
|
|
176
|
-
aria-label="Close modal"
|
|
177
|
-
>
|
|
178
|
-
<${CloseIcon} className="w-3.5 h-3.5 text-gray-300" />
|
|
179
|
-
</button>
|
|
180
|
-
`}
|
|
181
|
-
/>
|
|
182
|
-
<div class="space-y-2">
|
|
183
|
-
<p class="text-xs text-gray-500">Name</p>
|
|
184
|
-
<input
|
|
185
|
-
type="text"
|
|
186
|
-
value=${name}
|
|
187
|
-
placeholder="fathom"
|
|
188
|
-
onInput=${(e) => onNameChange(e.target.value)}
|
|
189
|
-
onKeyDown=${(e) => {
|
|
190
|
-
if (e.key === "Enter" && canCreate && !creating) {
|
|
191
|
-
onCreate(selectedDestination);
|
|
192
|
-
}
|
|
193
|
-
if (e.key === "Escape") onClose();
|
|
194
|
-
}}
|
|
195
|
-
class="w-full bg-black/30 border border-border rounded-lg px-3 py-1.5 text-sm text-gray-200 outline-none focus:border-gray-500 font-mono"
|
|
196
|
-
/>
|
|
197
|
-
</div>
|
|
198
|
-
<${SessionSelectField}
|
|
199
|
-
label="Deliver to"
|
|
200
|
-
sessions=${selectableSessions}
|
|
201
|
-
selectedSessionKey=${destinationSessionKey}
|
|
202
|
-
onChangeSessionKey=${setDestinationSessionKey}
|
|
203
|
-
disabled=${loadingSessions || creating}
|
|
204
|
-
loading=${loadingSessions}
|
|
205
|
-
error=${destinationLoadError}
|
|
206
|
-
allowNone=${true}
|
|
207
|
-
noneValue=${kNoDestinationSessionValue}
|
|
208
|
-
noneLabel="Default"
|
|
209
|
-
emptyStateText="No paired chat sessions found yet. You can still create the webhook without a default destination."
|
|
210
|
-
loadingLabel="Loading destinations..."
|
|
211
|
-
/>
|
|
212
|
-
<div class="border border-border rounded-lg overflow-hidden">
|
|
213
|
-
<table class="w-full text-xs">
|
|
214
|
-
<tbody>
|
|
215
|
-
<tr class="border-b border-border">
|
|
216
|
-
<td class="w-24 px-3 py-2 text-gray-500">Path</td>
|
|
217
|
-
<td class="px-3 py-2 text-gray-300 font-mono">
|
|
218
|
-
<code>/hooks/${previewName}</code>
|
|
219
|
-
</td>
|
|
220
|
-
</tr>
|
|
221
|
-
<tr class="border-b border-border">
|
|
222
|
-
<td class="w-24 px-3 py-2 text-gray-500">URL</td>
|
|
223
|
-
<td class="px-3 py-2 text-gray-300 font-mono break-all">
|
|
224
|
-
<code>${previewUrl}</code>
|
|
225
|
-
</td>
|
|
226
|
-
</tr>
|
|
227
|
-
<tr>
|
|
228
|
-
<td class="w-24 px-3 py-2 text-gray-500">Transform</td>
|
|
229
|
-
<td class="px-3 py-2 text-gray-300 font-mono">
|
|
230
|
-
<code
|
|
231
|
-
>hooks/transforms/${previewName}/${previewName}-transform.mjs</code
|
|
232
|
-
>
|
|
233
|
-
</td>
|
|
234
|
-
</tr>
|
|
235
|
-
</tbody>
|
|
236
|
-
</table>
|
|
237
|
-
</div>
|
|
238
|
-
<div class="pt-1 flex items-center justify-end gap-2">
|
|
239
|
-
<${ActionButton}
|
|
240
|
-
onClick=${onClose}
|
|
241
|
-
tone="secondary"
|
|
242
|
-
size="md"
|
|
243
|
-
idleLabel="Cancel"
|
|
244
|
-
className="px-4 py-2 rounded-lg text-sm"
|
|
245
|
-
/>
|
|
246
|
-
<${ActionButton}
|
|
247
|
-
onClick=${() => onCreate(selectedDestination)}
|
|
248
|
-
disabled=${!canCreate || creating}
|
|
249
|
-
loading=${creating}
|
|
250
|
-
tone="primary"
|
|
251
|
-
size="md"
|
|
252
|
-
idleLabel="Create"
|
|
253
|
-
loadingLabel="Creating..."
|
|
254
|
-
className="px-4 py-2 rounded-lg text-sm"
|
|
255
|
-
/>
|
|
256
|
-
</div>
|
|
257
|
-
</${ModalShell}>
|
|
258
|
-
`;
|
|
259
|
-
};
|
|
260
|
-
|
|
261
|
-
export const Webhooks = ({
|
|
262
|
-
selectedHookName = "",
|
|
263
|
-
onSelectHook = () => {},
|
|
264
|
-
onBackToList = () => {},
|
|
265
|
-
onRestartRequired = () => {},
|
|
266
|
-
onOpenFile = () => {},
|
|
267
|
-
}) => {
|
|
268
|
-
const [isCreating, setIsCreating] = useState(false);
|
|
269
|
-
const [newName, setNewName] = useState("");
|
|
270
|
-
const [creating, setCreating] = useState(false);
|
|
271
|
-
const [deleting, setDeleting] = useState(false);
|
|
272
|
-
const [showDeleteConfirm, setShowDeleteConfirm] = useState(false);
|
|
273
|
-
const [deleteTransformDir, setDeleteTransformDir] = useState(true);
|
|
274
|
-
const [authMode, setAuthMode] = useState("headers");
|
|
275
|
-
const [statusFilter, setStatusFilter] = useState("all");
|
|
276
|
-
const [expandedRows, setExpandedRows] = useState(() => new Set());
|
|
277
|
-
const [sendingTestWebhook, setSendingTestWebhook] = useState(false);
|
|
278
|
-
const [replayingRequestId, setReplayingRequestId] = useState(null);
|
|
279
|
-
const [debugLoadingRequestId, setDebugLoadingRequestId] = useState(null);
|
|
280
|
-
const [debugRequest, setDebugRequest] = useState(null);
|
|
281
|
-
|
|
282
|
-
const listPoll = usePolling(fetchWebhooks, 15000);
|
|
283
|
-
const webhooks = listPoll.data?.webhooks || [];
|
|
284
|
-
const agentsPoll = usePolling(fetchAgents, 20000);
|
|
285
|
-
const agents = Array.isArray(agentsPoll.data?.agents)
|
|
286
|
-
? agentsPoll.data.agents
|
|
287
|
-
: [];
|
|
288
|
-
const agentNameById = useMemo(
|
|
289
|
-
() =>
|
|
290
|
-
new Map(
|
|
291
|
-
agents.map((agent) => [
|
|
292
|
-
String(agent?.id || "").trim(),
|
|
293
|
-
String(agent?.name || "").trim() ||
|
|
294
|
-
formatAgentFallbackName(agent?.id),
|
|
295
|
-
]),
|
|
296
|
-
),
|
|
297
|
-
[agents],
|
|
298
|
-
);
|
|
299
|
-
|
|
300
|
-
const detailPoll = usePolling(
|
|
301
|
-
async () => {
|
|
302
|
-
if (!selectedHookName) return null;
|
|
303
|
-
const data = await fetchWebhookDetail(selectedHookName);
|
|
304
|
-
return data.webhook || null;
|
|
305
|
-
},
|
|
306
|
-
10000,
|
|
307
|
-
{ enabled: !!selectedHookName },
|
|
308
|
-
);
|
|
309
|
-
|
|
310
|
-
const requestsPoll = usePolling(
|
|
311
|
-
async () => {
|
|
312
|
-
if (!selectedHookName) return { requests: [] };
|
|
313
|
-
const data = await fetchWebhookRequests(selectedHookName, {
|
|
314
|
-
limit: 25,
|
|
315
|
-
offset: 0,
|
|
316
|
-
status: statusFilter,
|
|
317
|
-
});
|
|
318
|
-
return data;
|
|
319
|
-
},
|
|
320
|
-
5000,
|
|
321
|
-
{ enabled: !!selectedHookName },
|
|
322
|
-
);
|
|
323
|
-
|
|
324
|
-
const selectedWebhook = detailPoll.data;
|
|
325
|
-
const selectedWebhookManaged = Boolean(selectedWebhook?.managed);
|
|
326
|
-
const selectedDeliveryAgentId =
|
|
327
|
-
String(selectedWebhook?.agentId || "main").trim() || "main";
|
|
328
|
-
const selectedDeliveryAgentName =
|
|
329
|
-
agentNameById.get(selectedDeliveryAgentId) ||
|
|
330
|
-
formatAgentFallbackName(selectedDeliveryAgentId);
|
|
331
|
-
const selectedDeliveryChannel =
|
|
332
|
-
String(selectedWebhook?.channel || "last").trim() || "last";
|
|
333
|
-
const requests = requestsPoll.data?.requests || [];
|
|
334
|
-
const webhookUrl =
|
|
335
|
-
selectedWebhook?.fullUrl || `.../hooks/${selectedHookName}`;
|
|
336
|
-
const webhookUrlWithQueryToken =
|
|
337
|
-
selectedWebhook?.queryStringUrl ||
|
|
338
|
-
`${webhookUrl}${webhookUrl.includes("?") ? "&" : "?"}token=<WEBHOOK_TOKEN>`;
|
|
339
|
-
const derivedTokenFromQuery = (() => {
|
|
340
|
-
try {
|
|
341
|
-
const parsed = new URL(webhookUrlWithQueryToken);
|
|
342
|
-
return String(parsed.searchParams.get("token") || "").trim();
|
|
343
|
-
} catch {
|
|
344
|
-
return "";
|
|
345
|
-
}
|
|
346
|
-
})();
|
|
347
|
-
const authHeaderValue =
|
|
348
|
-
selectedWebhook?.authHeaderValue ||
|
|
349
|
-
(derivedTokenFromQuery
|
|
350
|
-
? `Authorization: Bearer ${derivedTokenFromQuery}`
|
|
351
|
-
: "Authorization: Bearer <WEBHOOK_TOKEN>");
|
|
352
|
-
const bearerTokenValue = authHeaderValue.startsWith("Authorization: ")
|
|
353
|
-
? authHeaderValue.slice("Authorization: ".length)
|
|
354
|
-
: authHeaderValue;
|
|
355
|
-
const webhookTestPayload = useMemo(() => {
|
|
356
|
-
if (
|
|
357
|
-
String(selectedHookName || "")
|
|
358
|
-
.trim()
|
|
359
|
-
.toLowerCase() === "gmail"
|
|
360
|
-
) {
|
|
361
|
-
return {
|
|
362
|
-
payload: {
|
|
363
|
-
account: "test@gmail.com",
|
|
364
|
-
messages: [
|
|
365
|
-
{
|
|
366
|
-
id: "test-message-1",
|
|
367
|
-
from: "alerts@example.com",
|
|
368
|
-
to: ["test@gmail.com"],
|
|
369
|
-
subject: "Test Gmail webhook event",
|
|
370
|
-
snippet:
|
|
371
|
-
"This is a simulated Gmail message payload for webhook testing.",
|
|
372
|
-
receivedAt: new Date().toISOString(),
|
|
373
|
-
},
|
|
374
|
-
],
|
|
375
|
-
},
|
|
376
|
-
};
|
|
377
|
-
}
|
|
378
|
-
return {
|
|
379
|
-
source: "manual-test",
|
|
380
|
-
message: `This is a test of the ${selectedHookName || "webhook"} webhook.`,
|
|
381
|
-
};
|
|
382
|
-
}, [selectedHookName]);
|
|
383
|
-
const webhookTestPayloadJson = JSON.stringify(webhookTestPayload);
|
|
384
|
-
const curlCommandHeaders =
|
|
385
|
-
`curl -X POST "${webhookUrl}" ` +
|
|
386
|
-
`-H "Content-Type: application/json" ` +
|
|
387
|
-
`-H "${authHeaderValue}" ` +
|
|
388
|
-
`-d '${webhookTestPayloadJson}'`;
|
|
389
|
-
const curlCommandQuery =
|
|
390
|
-
`curl -X POST "${webhookUrlWithQueryToken}" ` +
|
|
391
|
-
`-H "Content-Type: application/json" ` +
|
|
392
|
-
`-d '${webhookTestPayloadJson}'`;
|
|
393
|
-
const effectiveAuthMode = selectedWebhookManaged ? "headers" : authMode;
|
|
394
|
-
const activeCurlCommand =
|
|
395
|
-
effectiveAuthMode === "query" ? curlCommandQuery : curlCommandHeaders;
|
|
396
|
-
|
|
397
|
-
const canCreate = useMemo(() => {
|
|
398
|
-
const name = String(newName || "")
|
|
399
|
-
.trim()
|
|
400
|
-
.toLowerCase();
|
|
401
|
-
return kNamePattern.test(name);
|
|
402
|
-
}, [newName]);
|
|
403
|
-
|
|
404
|
-
const refreshAll = useCallback(() => {
|
|
405
|
-
listPoll.refresh();
|
|
406
|
-
detailPoll.refresh();
|
|
407
|
-
requestsPoll.refresh();
|
|
408
|
-
}, [listPoll.refresh, detailPoll.refresh, requestsPoll.refresh]);
|
|
409
|
-
|
|
410
|
-
const handleCreate = useCallback(
|
|
411
|
-
async (destination = null) => {
|
|
412
|
-
const candidateName = String(newName || "")
|
|
413
|
-
.trim()
|
|
414
|
-
.toLowerCase();
|
|
415
|
-
if (!kNamePattern.test(candidateName)) {
|
|
416
|
-
showToast(
|
|
417
|
-
"Name must be lowercase letters, numbers, and hyphens",
|
|
418
|
-
"error",
|
|
419
|
-
);
|
|
420
|
-
return;
|
|
421
|
-
}
|
|
422
|
-
if (creating) return;
|
|
423
|
-
setCreating(true);
|
|
424
|
-
try {
|
|
425
|
-
const data = await createWebhook(candidateName, { destination });
|
|
426
|
-
setIsCreating(false);
|
|
427
|
-
setNewName("");
|
|
428
|
-
onSelectHook(candidateName);
|
|
429
|
-
if (data.restartRequired) onRestartRequired(true);
|
|
430
|
-
showToast("Webhook created", "success");
|
|
431
|
-
if (data.syncWarning) {
|
|
432
|
-
showToast(
|
|
433
|
-
`Created, but git-sync failed: ${data.syncWarning}`,
|
|
434
|
-
"warning",
|
|
435
|
-
);
|
|
436
|
-
}
|
|
437
|
-
refreshAll();
|
|
438
|
-
} catch (err) {
|
|
439
|
-
showToast(err.message || "Could not create webhook", "error");
|
|
440
|
-
} finally {
|
|
441
|
-
setCreating(false);
|
|
442
|
-
}
|
|
443
|
-
},
|
|
444
|
-
[newName, creating, refreshAll, onSelectHook, onRestartRequired],
|
|
445
|
-
);
|
|
446
|
-
|
|
447
|
-
const handleDeleteConfirmed = useCallback(async () => {
|
|
448
|
-
if (!selectedHookName || deleting) return;
|
|
449
|
-
setDeleting(true);
|
|
450
|
-
try {
|
|
451
|
-
const data = await deleteWebhook(selectedHookName, {
|
|
452
|
-
deleteTransformDir,
|
|
453
|
-
});
|
|
454
|
-
if (data.restartRequired) onRestartRequired(true);
|
|
455
|
-
onBackToList();
|
|
456
|
-
setShowDeleteConfirm(false);
|
|
457
|
-
setDeleteTransformDir(true);
|
|
458
|
-
showToast("Webhook removed", "success");
|
|
459
|
-
if (data.deletedTransformDir) {
|
|
460
|
-
showToast("Transform directory deleted", "success");
|
|
461
|
-
}
|
|
462
|
-
if (data.syncWarning) {
|
|
463
|
-
showToast(
|
|
464
|
-
`Deleted, but git-sync failed: ${data.syncWarning}`,
|
|
465
|
-
"warning",
|
|
466
|
-
);
|
|
467
|
-
}
|
|
468
|
-
refreshAll();
|
|
469
|
-
} catch (err) {
|
|
470
|
-
showToast(err.message || "Could not delete webhook", "error");
|
|
471
|
-
} finally {
|
|
472
|
-
setDeleting(false);
|
|
473
|
-
}
|
|
474
|
-
}, [
|
|
475
|
-
selectedHookName,
|
|
476
|
-
deleting,
|
|
477
|
-
deleteTransformDir,
|
|
478
|
-
refreshAll,
|
|
479
|
-
onBackToList,
|
|
480
|
-
onRestartRequired,
|
|
481
|
-
]);
|
|
482
|
-
|
|
483
|
-
const handleRequestRowToggle = useCallback((id, isOpen) => {
|
|
484
|
-
setExpandedRows((prev) => {
|
|
485
|
-
const next = new Set(prev);
|
|
486
|
-
if (isOpen) next.add(id);
|
|
487
|
-
else next.delete(id);
|
|
488
|
-
return next;
|
|
489
|
-
});
|
|
490
|
-
}, []);
|
|
491
|
-
|
|
492
|
-
const handleSendTestWebhook = useCallback(async () => {
|
|
493
|
-
if (!selectedHookName || sendingTestWebhook) return;
|
|
494
|
-
setSendingTestWebhook(true);
|
|
495
|
-
const requestUrl =
|
|
496
|
-
effectiveAuthMode === "query" ? webhookUrlWithQueryToken : webhookUrl;
|
|
497
|
-
const headers = { "Content-Type": "application/json" };
|
|
498
|
-
if (effectiveAuthMode === "headers") {
|
|
499
|
-
headers.Authorization = bearerTokenValue;
|
|
500
|
-
}
|
|
501
|
-
try {
|
|
502
|
-
const response = await fetch(requestUrl, {
|
|
503
|
-
method: "POST",
|
|
504
|
-
headers,
|
|
505
|
-
body: webhookTestPayloadJson,
|
|
506
|
-
});
|
|
507
|
-
const bodyText = await response.text();
|
|
508
|
-
let body = null;
|
|
509
|
-
try {
|
|
510
|
-
body = bodyText ? JSON.parse(bodyText) : null;
|
|
511
|
-
} catch {
|
|
512
|
-
body = null;
|
|
513
|
-
}
|
|
514
|
-
const errorMessage =
|
|
515
|
-
body?.ok === false
|
|
516
|
-
? body?.error || "Webhook rejected"
|
|
517
|
-
: !response.ok
|
|
518
|
-
? body?.error || bodyText || `HTTP ${response.status}`
|
|
519
|
-
: "";
|
|
520
|
-
if (errorMessage) {
|
|
521
|
-
showToast(`Test webhook failed: ${errorMessage}`, "error");
|
|
522
|
-
return;
|
|
523
|
-
}
|
|
524
|
-
showToast("Test webhook sent", "success");
|
|
525
|
-
setTimeout(() => requestsPoll.refresh(), 0);
|
|
526
|
-
} catch (err) {
|
|
527
|
-
showToast(err.message || "Could not send test webhook", "error");
|
|
528
|
-
} finally {
|
|
529
|
-
setSendingTestWebhook(false);
|
|
530
|
-
}
|
|
531
|
-
}, [
|
|
532
|
-
bearerTokenValue,
|
|
533
|
-
effectiveAuthMode,
|
|
534
|
-
requestsPoll.refresh,
|
|
535
|
-
selectedHookName,
|
|
536
|
-
sendingTestWebhook,
|
|
537
|
-
webhookTestPayloadJson,
|
|
538
|
-
webhookUrl,
|
|
539
|
-
webhookUrlWithQueryToken,
|
|
540
|
-
]);
|
|
541
|
-
|
|
542
|
-
const handleReplayRequest = useCallback(
|
|
543
|
-
async (item) => {
|
|
544
|
-
if (!item || replayingRequestId === item.id) return;
|
|
545
|
-
if (item.payloadTruncated) {
|
|
546
|
-
showToast("Cannot replay a truncated payload", "warning");
|
|
547
|
-
return;
|
|
548
|
-
}
|
|
549
|
-
const requestUrl =
|
|
550
|
-
effectiveAuthMode === "query" ? webhookUrlWithQueryToken : webhookUrl;
|
|
551
|
-
const headers = { "Content-Type": "application/json" };
|
|
552
|
-
if (effectiveAuthMode === "headers") {
|
|
553
|
-
headers.Authorization = bearerTokenValue;
|
|
554
|
-
}
|
|
555
|
-
setReplayingRequestId(item.id);
|
|
556
|
-
try {
|
|
557
|
-
const response = await fetch(requestUrl, {
|
|
558
|
-
method: "POST",
|
|
559
|
-
headers,
|
|
560
|
-
body: String(item.payload || ""),
|
|
561
|
-
});
|
|
562
|
-
const bodyText = await response.text();
|
|
563
|
-
let body = null;
|
|
564
|
-
try {
|
|
565
|
-
body = bodyText ? JSON.parse(bodyText) : null;
|
|
566
|
-
} catch {
|
|
567
|
-
body = null;
|
|
568
|
-
}
|
|
569
|
-
const errorMessage =
|
|
570
|
-
body?.ok === false
|
|
571
|
-
? body?.error || "Webhook rejected"
|
|
572
|
-
: !response.ok
|
|
573
|
-
? body?.error || bodyText || `HTTP ${response.status}`
|
|
574
|
-
: "";
|
|
575
|
-
if (errorMessage) {
|
|
576
|
-
showToast(`Replay failed: ${errorMessage}`, "error");
|
|
577
|
-
return;
|
|
578
|
-
}
|
|
579
|
-
showToast("Request replayed", "success");
|
|
580
|
-
setTimeout(() => requestsPoll.refresh(), 0);
|
|
581
|
-
} catch (err) {
|
|
582
|
-
showToast(err.message || "Could not replay request", "error");
|
|
583
|
-
} finally {
|
|
584
|
-
setReplayingRequestId(null);
|
|
585
|
-
}
|
|
586
|
-
},
|
|
587
|
-
[
|
|
588
|
-
bearerTokenValue,
|
|
589
|
-
effectiveAuthMode,
|
|
590
|
-
replayingRequestId,
|
|
591
|
-
requestsPoll.refresh,
|
|
592
|
-
webhookUrl,
|
|
593
|
-
webhookUrlWithQueryToken,
|
|
594
|
-
],
|
|
595
|
-
);
|
|
596
|
-
|
|
597
|
-
const handleCopyRequestField = useCallback(async (value, label) => {
|
|
598
|
-
try {
|
|
599
|
-
await navigator.clipboard.writeText(String(value || ""));
|
|
600
|
-
showToast(`${label} copied`, "success");
|
|
601
|
-
} catch {
|
|
602
|
-
showToast(
|
|
603
|
-
`Could not copy ${String(label || "value").toLowerCase()}`,
|
|
604
|
-
"error",
|
|
605
|
-
);
|
|
606
|
-
}
|
|
607
|
-
}, []);
|
|
608
|
-
|
|
609
|
-
const isListLoading = !listPoll.data && !listPoll.error;
|
|
610
|
-
const debugAgentMessage = useMemo(
|
|
611
|
-
() =>
|
|
612
|
-
buildWebhookDebugMessage({
|
|
613
|
-
hookName: selectedHookName,
|
|
614
|
-
webhook: selectedWebhook,
|
|
615
|
-
request: debugRequest,
|
|
616
|
-
}),
|
|
617
|
-
[debugRequest, selectedHookName, selectedWebhook],
|
|
618
|
-
);
|
|
619
|
-
|
|
620
|
-
const handleAskAgentToDebug = useCallback(
|
|
621
|
-
async (item) => {
|
|
622
|
-
if (!selectedHookName || !item?.id || debugLoadingRequestId === item.id)
|
|
623
|
-
return;
|
|
624
|
-
try {
|
|
625
|
-
setDebugLoadingRequestId(item.id);
|
|
626
|
-
const data = await fetchWebhookRequest(selectedHookName, item.id);
|
|
627
|
-
setDebugRequest(data?.request || item);
|
|
628
|
-
} catch (err) {
|
|
629
|
-
showToast(
|
|
630
|
-
err.message || "Could not load webhook request details",
|
|
631
|
-
"error",
|
|
632
|
-
);
|
|
633
|
-
} finally {
|
|
634
|
-
setDebugLoadingRequestId(null);
|
|
635
|
-
}
|
|
636
|
-
},
|
|
637
|
-
[debugLoadingRequestId, selectedHookName],
|
|
638
|
-
);
|
|
639
|
-
|
|
640
|
-
return html`
|
|
641
|
-
<div class="space-y-4">
|
|
642
|
-
<${PageHeader}
|
|
643
|
-
title="Webhooks"
|
|
644
|
-
leading=${selectedHookName
|
|
645
|
-
? html`
|
|
646
|
-
<button
|
|
647
|
-
class="flex items-center gap-1.5 text-sm text-gray-500 hover:text-gray-300 transition-colors"
|
|
648
|
-
onclick=${onBackToList}
|
|
649
|
-
>
|
|
650
|
-
<svg
|
|
651
|
-
width="16"
|
|
652
|
-
height="16"
|
|
653
|
-
viewBox="0 0 16 16"
|
|
654
|
-
fill="currentColor"
|
|
655
|
-
>
|
|
656
|
-
<path
|
|
657
|
-
d="M10.354 3.354a.5.5 0 00-.708-.708l-5 5a.5.5 0 000 .708l5 5a.5.5 0 00.708-.708L5.707 8l4.647-4.646z"
|
|
658
|
-
/>
|
|
659
|
-
</svg>
|
|
660
|
-
Back
|
|
661
|
-
</button>
|
|
662
|
-
`
|
|
663
|
-
: null}
|
|
664
|
-
actions=${selectedHookName
|
|
665
|
-
? null
|
|
666
|
-
: html`
|
|
667
|
-
<button
|
|
668
|
-
class="text-xs px-3 py-1.5 rounded-lg ac-btn-secondary"
|
|
669
|
-
onclick=${() => setIsCreating((open) => !open)}
|
|
670
|
-
>
|
|
671
|
-
Create new
|
|
672
|
-
</button>
|
|
673
|
-
`}
|
|
674
|
-
/>
|
|
675
|
-
|
|
676
|
-
${selectedHookName
|
|
677
|
-
? html`
|
|
678
|
-
<div
|
|
679
|
-
class="bg-surface border border-border rounded-xl p-4 space-y-4"
|
|
680
|
-
>
|
|
681
|
-
<div>
|
|
682
|
-
<h2 class="font-semibold text-sm">
|
|
683
|
-
${selectedWebhook?.path || `/hooks/${selectedHookName}`}
|
|
684
|
-
</h2>
|
|
685
|
-
</div>
|
|
686
|
-
|
|
687
|
-
<div
|
|
688
|
-
class="bg-black/20 border border-border rounded-lg p-3 space-y-4"
|
|
689
|
-
>
|
|
690
|
-
${selectedWebhookManaged
|
|
691
|
-
? null
|
|
692
|
-
: html`
|
|
693
|
-
<div class="space-y-2">
|
|
694
|
-
<p class="text-xs text-gray-500">Auth mode</p>
|
|
695
|
-
<div class="flex items-center gap-2">
|
|
696
|
-
<button
|
|
697
|
-
class="text-xs px-2 py-1 rounded border transition-colors ${authMode ===
|
|
698
|
-
"headers"
|
|
699
|
-
? "border-cyan-400 text-cyan-200 bg-cyan-400/10"
|
|
700
|
-
: "border-border text-gray-400 hover:text-gray-200"}"
|
|
701
|
-
onclick=${() => setAuthMode("headers")}
|
|
702
|
-
>
|
|
703
|
-
Headers
|
|
704
|
-
</button>
|
|
705
|
-
<button
|
|
706
|
-
class="text-xs px-2 py-1 rounded border transition-colors ${authMode ===
|
|
707
|
-
"query"
|
|
708
|
-
? "border-cyan-400 text-cyan-200 bg-cyan-400/10"
|
|
709
|
-
: "border-border text-gray-400 hover:text-gray-200"}"
|
|
710
|
-
onclick=${() => setAuthMode("query")}
|
|
711
|
-
>
|
|
712
|
-
Query string
|
|
713
|
-
</button>
|
|
714
|
-
</div>
|
|
715
|
-
</div>
|
|
716
|
-
`}
|
|
717
|
-
<div class="space-y-2">
|
|
718
|
-
<p class="text-xs text-gray-500">Webhook URL</p>
|
|
719
|
-
<div class="flex items-center gap-2">
|
|
720
|
-
<input
|
|
721
|
-
type="text"
|
|
722
|
-
readonly
|
|
723
|
-
value=${effectiveAuthMode === "query"
|
|
724
|
-
? webhookUrlWithQueryToken
|
|
725
|
-
: webhookUrl}
|
|
726
|
-
class="h-8 flex-1 bg-black/30 border border-border rounded-lg px-3 text-xs text-gray-200 outline-none font-mono"
|
|
727
|
-
/>
|
|
728
|
-
<button
|
|
729
|
-
class="h-8 text-xs px-2.5 rounded-lg ac-btn-secondary shrink-0"
|
|
730
|
-
onclick=${async () => {
|
|
731
|
-
try {
|
|
732
|
-
await navigator.clipboard.writeText(
|
|
733
|
-
effectiveAuthMode === "query"
|
|
734
|
-
? webhookUrlWithQueryToken
|
|
735
|
-
: webhookUrl,
|
|
736
|
-
);
|
|
737
|
-
showToast("Webhook URL copied", "success");
|
|
738
|
-
} catch {
|
|
739
|
-
showToast("Could not copy URL", "error");
|
|
740
|
-
}
|
|
741
|
-
}}
|
|
742
|
-
>
|
|
743
|
-
Copy
|
|
744
|
-
</button>
|
|
745
|
-
</div>
|
|
746
|
-
</div>
|
|
747
|
-
${selectedWebhookManaged
|
|
748
|
-
? null
|
|
749
|
-
: effectiveAuthMode === "headers"
|
|
750
|
-
? html`
|
|
751
|
-
<div class="space-y-2">
|
|
752
|
-
<p class="text-xs text-gray-500">Auth headers</p>
|
|
753
|
-
<div class="flex items-center gap-2">
|
|
754
|
-
<input
|
|
755
|
-
type="text"
|
|
756
|
-
readonly
|
|
757
|
-
value=${authHeaderValue}
|
|
758
|
-
class="h-8 flex-1 bg-black/30 border border-border rounded-lg px-3 text-xs text-gray-200 outline-none font-mono"
|
|
759
|
-
/>
|
|
760
|
-
<button
|
|
761
|
-
class="h-8 text-xs px-2.5 rounded-lg ac-btn-secondary shrink-0"
|
|
762
|
-
onclick=${async () => {
|
|
763
|
-
try {
|
|
764
|
-
await navigator.clipboard.writeText(
|
|
765
|
-
bearerTokenValue,
|
|
766
|
-
);
|
|
767
|
-
showToast("Bearer token copied", "success");
|
|
768
|
-
} catch {
|
|
769
|
-
showToast(
|
|
770
|
-
"Could not copy bearer token",
|
|
771
|
-
"error",
|
|
772
|
-
);
|
|
773
|
-
}
|
|
774
|
-
}}
|
|
775
|
-
>
|
|
776
|
-
Copy
|
|
777
|
-
</button>
|
|
778
|
-
</div>
|
|
779
|
-
</div>
|
|
780
|
-
`
|
|
781
|
-
: html`
|
|
782
|
-
<p class="text-xs text-yellow-300">
|
|
783
|
-
Always use auth headers when possible. Query string is
|
|
784
|
-
less secure.
|
|
785
|
-
</p>
|
|
786
|
-
`}
|
|
787
|
-
</div>
|
|
788
|
-
|
|
789
|
-
<div
|
|
790
|
-
class="bg-black/20 border border-border rounded-lg p-3 space-y-2"
|
|
791
|
-
>
|
|
792
|
-
<p class="text-xs text-gray-500">Deliver to</p>
|
|
793
|
-
<p class="text-xs text-gray-200 font-mono ">
|
|
794
|
-
${selectedDeliveryAgentName}${" "}
|
|
795
|
-
<span class="text-xs text-gray-500 font-mono">
|
|
796
|
-
(${selectedDeliveryChannel})</span
|
|
797
|
-
>
|
|
798
|
-
</p>
|
|
799
|
-
</div>
|
|
800
|
-
|
|
801
|
-
<div
|
|
802
|
-
class="bg-black/20 border border-border rounded-lg p-3 space-y-2"
|
|
803
|
-
>
|
|
804
|
-
<p class="text-xs text-gray-500">Test webhook</p>
|
|
805
|
-
<div class="flex flex-col gap-2 sm:flex-row sm:items-center">
|
|
806
|
-
<input
|
|
807
|
-
type="text"
|
|
808
|
-
readonly
|
|
809
|
-
value=${activeCurlCommand}
|
|
810
|
-
class="h-8 w-full sm:flex-1 sm:min-w-0 bg-black/30 border border-border rounded-lg px-3 text-xs text-gray-200 outline-none font-mono overflow-x-auto scrollbar-hidden"
|
|
811
|
-
/>
|
|
812
|
-
<div
|
|
813
|
-
class="grid grid-cols-2 gap-2 w-full sm:w-auto sm:flex sm:items-center"
|
|
814
|
-
>
|
|
815
|
-
<button
|
|
816
|
-
class="h-8 text-xs px-2.5 rounded-lg ac-btn-secondary w-full sm:w-auto sm:shrink-0"
|
|
817
|
-
onclick=${async () => {
|
|
818
|
-
try {
|
|
819
|
-
await navigator.clipboard.writeText(
|
|
820
|
-
activeCurlCommand,
|
|
821
|
-
);
|
|
822
|
-
showToast("curl command copied", "success");
|
|
823
|
-
} catch {
|
|
824
|
-
showToast("Could not copy curl command", "error");
|
|
825
|
-
}
|
|
826
|
-
}}
|
|
827
|
-
>
|
|
828
|
-
Copy
|
|
829
|
-
</button>
|
|
830
|
-
<button
|
|
831
|
-
class="h-8 text-xs px-2.5 rounded-lg ac-btn-secondary w-full sm:w-auto sm:shrink-0 disabled:opacity-60"
|
|
832
|
-
onclick=${handleSendTestWebhook}
|
|
833
|
-
disabled=${sendingTestWebhook}
|
|
834
|
-
>
|
|
835
|
-
${sendingTestWebhook ? "Sending..." : "Send"}
|
|
836
|
-
</button>
|
|
837
|
-
</div>
|
|
838
|
-
</div>
|
|
839
|
-
</div>
|
|
840
|
-
|
|
841
|
-
<div class="bg-black/20 border border-border rounded-lg p-3">
|
|
842
|
-
<div class="flex items-center gap-2 text-xs text-gray-300">
|
|
843
|
-
<span class="text-gray-500">Transform:</span>
|
|
844
|
-
${selectedWebhook?.transformPath
|
|
845
|
-
? html`<button
|
|
846
|
-
type="button"
|
|
847
|
-
class="ac-tip-link flex-1 min-w-0 truncate block text-left font-mono"
|
|
848
|
-
title=${selectedWebhook.transformPath}
|
|
849
|
-
onclick=${() =>
|
|
850
|
-
onOpenFile(selectedWebhook.transformPath)}
|
|
851
|
-
>
|
|
852
|
-
${selectedWebhook.transformPath}
|
|
853
|
-
</button>`
|
|
854
|
-
: html`<code class="flex-1 min-w-0 truncate block"
|
|
855
|
-
>—</code
|
|
856
|
-
>`}
|
|
857
|
-
<span
|
|
858
|
-
class=${`ml-auto inline-flex items-center gap-1 px-1.5 py-0.5 rounded border font-sans ${
|
|
859
|
-
selectedWebhook?.transformExists
|
|
860
|
-
? "border-green-500/30 text-green-300 bg-green-500/10"
|
|
861
|
-
: "border-yellow-500/30 text-yellow-300 bg-yellow-500/10"
|
|
862
|
-
}`}
|
|
863
|
-
>
|
|
864
|
-
<span class="font-sans text-sm leading-none">
|
|
865
|
-
${selectedWebhook?.transformExists ? "✓" : "!"}
|
|
866
|
-
</span>
|
|
867
|
-
${selectedWebhook?.transformExists
|
|
868
|
-
? null
|
|
869
|
-
: html`<span>missing</span>`}
|
|
870
|
-
</span>
|
|
871
|
-
</div>
|
|
872
|
-
</div>
|
|
873
|
-
|
|
874
|
-
<div class="flex items-center justify-between gap-3">
|
|
875
|
-
<p class="text-xs text-gray-600">
|
|
876
|
-
Created: ${formatDateTime(selectedWebhook?.createdAt)}
|
|
877
|
-
</p>
|
|
878
|
-
${selectedWebhookManaged
|
|
879
|
-
? null
|
|
880
|
-
: html`<${ActionButton}
|
|
881
|
-
onClick=${() => {
|
|
882
|
-
if (deleting) return;
|
|
883
|
-
setDeleteTransformDir(true);
|
|
884
|
-
setShowDeleteConfirm(true);
|
|
885
|
-
}}
|
|
886
|
-
disabled=${deleting}
|
|
887
|
-
loading=${deleting}
|
|
888
|
-
tone="danger"
|
|
889
|
-
size="sm"
|
|
890
|
-
idleLabel="Delete"
|
|
891
|
-
loadingLabel="Deleting..."
|
|
892
|
-
className="shrink-0 px-2.5 py-1"
|
|
893
|
-
/>`}
|
|
894
|
-
</div>
|
|
895
|
-
</div>
|
|
896
|
-
|
|
897
|
-
${selectedWebhookManaged
|
|
898
|
-
? html`
|
|
899
|
-
<div
|
|
900
|
-
class="rounded-lg border border-yellow-500/30 bg-yellow-500/10 p-3"
|
|
901
|
-
>
|
|
902
|
-
<p class="text-xs text-yellow-200">
|
|
903
|
-
This webhook is managed by Gmail Watch setup and cannot be
|
|
904
|
-
deleted or edited from this page.
|
|
905
|
-
</p>
|
|
906
|
-
</div>
|
|
907
|
-
`
|
|
908
|
-
: null}
|
|
909
|
-
<div
|
|
910
|
-
class="bg-surface border border-border rounded-xl p-4 space-y-3"
|
|
911
|
-
>
|
|
912
|
-
<div class="flex items-center justify-between gap-3">
|
|
913
|
-
<h3 class="card-label">Request history</h3>
|
|
914
|
-
<div class="flex items-center gap-2">
|
|
915
|
-
${kStatusFilters.map(
|
|
916
|
-
(filter) => html`
|
|
917
|
-
<button
|
|
918
|
-
class="text-xs px-2 py-1 rounded border ${statusFilter ===
|
|
919
|
-
filter
|
|
920
|
-
? "border-cyan-400 text-cyan-200 bg-cyan-400/10"
|
|
921
|
-
: "border-border text-gray-400 hover:text-gray-200"}"
|
|
922
|
-
onclick=${() => {
|
|
923
|
-
setStatusFilter(filter);
|
|
924
|
-
setExpandedRows(new Set());
|
|
925
|
-
setTimeout(() => requestsPoll.refresh(), 0);
|
|
926
|
-
}}
|
|
927
|
-
>
|
|
928
|
-
${filter}
|
|
929
|
-
</button>
|
|
930
|
-
`,
|
|
931
|
-
)}
|
|
932
|
-
</div>
|
|
933
|
-
</div>
|
|
934
|
-
|
|
935
|
-
${requests.length === 0
|
|
936
|
-
? html`<p class="text-sm text-gray-500">
|
|
937
|
-
No requests logged yet.
|
|
938
|
-
</p>`
|
|
939
|
-
: html`
|
|
940
|
-
<div class="ac-history-list">
|
|
941
|
-
${requests.map((item) => {
|
|
942
|
-
const statusTone = getRequestStatusTone(item.status);
|
|
943
|
-
return html`
|
|
944
|
-
<details
|
|
945
|
-
class="ac-history-item"
|
|
946
|
-
open=${expandedRows.has(item.id)}
|
|
947
|
-
ontoggle=${(e) =>
|
|
948
|
-
handleRequestRowToggle(
|
|
949
|
-
item.id,
|
|
950
|
-
!!e.currentTarget?.open,
|
|
951
|
-
)}
|
|
952
|
-
>
|
|
953
|
-
<summary class="ac-history-summary">
|
|
954
|
-
<div class="ac-history-summary-row">
|
|
955
|
-
<span
|
|
956
|
-
class="inline-flex items-center gap-2 min-w-0"
|
|
957
|
-
>
|
|
958
|
-
<span
|
|
959
|
-
class="ac-history-toggle shrink-0"
|
|
960
|
-
aria-hidden="true"
|
|
961
|
-
>▸</span
|
|
962
|
-
>
|
|
963
|
-
<span class="truncate text-xs text-gray-300">
|
|
964
|
-
${formatLastReceived(item.createdAt)}
|
|
965
|
-
</span>
|
|
966
|
-
</span>
|
|
967
|
-
<span
|
|
968
|
-
class="inline-flex items-center gap-2 shrink-0"
|
|
969
|
-
>
|
|
970
|
-
<span class="text-xs text-gray-500"
|
|
971
|
-
>${formatBytes(item.payloadSize)}</span
|
|
972
|
-
>
|
|
973
|
-
<span
|
|
974
|
-
class=${`text-xs font-medium ${statusTone.textClass}`}
|
|
975
|
-
>${item.gatewayStatus || "n/a"}</span
|
|
976
|
-
>
|
|
977
|
-
<span class="inline-flex items-center">
|
|
978
|
-
<span
|
|
979
|
-
class=${`h-2.5 w-2.5 rounded-full ${statusTone.dotClass}`}
|
|
980
|
-
title=${item.status || "unknown"}
|
|
981
|
-
aria-label=${item.status || "unknown"}
|
|
982
|
-
></span>
|
|
983
|
-
</span>
|
|
984
|
-
</span>
|
|
985
|
-
</div>
|
|
986
|
-
</summary>
|
|
987
|
-
${expandedRows.has(item.id)
|
|
988
|
-
? html`
|
|
989
|
-
<div class="ac-history-body space-y-3">
|
|
990
|
-
<div>
|
|
991
|
-
<p class="text-[11px] text-gray-500 mb-1">
|
|
992
|
-
Headers
|
|
993
|
-
</p>
|
|
994
|
-
<pre
|
|
995
|
-
class="text-xs bg-black/30 border border-border rounded p-2 overflow-auto"
|
|
996
|
-
>
|
|
997
|
-
${jsonPretty(item.headers)}</pre
|
|
998
|
-
>
|
|
999
|
-
<div
|
|
1000
|
-
class="mt-2 flex justify-start gap-2"
|
|
1001
|
-
>
|
|
1002
|
-
<button
|
|
1003
|
-
class="h-7 text-xs px-2.5 rounded-lg ac-btn-secondary"
|
|
1004
|
-
onclick=${() =>
|
|
1005
|
-
handleCopyRequestField(
|
|
1006
|
-
jsonPretty(item.headers),
|
|
1007
|
-
"Headers",
|
|
1008
|
-
)}
|
|
1009
|
-
>
|
|
1010
|
-
Copy
|
|
1011
|
-
</button>
|
|
1012
|
-
</div>
|
|
1013
|
-
</div>
|
|
1014
|
-
<div>
|
|
1015
|
-
<p class="text-[11px] text-gray-500 mb-1">
|
|
1016
|
-
Payload
|
|
1017
|
-
${item.payloadTruncated
|
|
1018
|
-
? "(truncated)"
|
|
1019
|
-
: ""}
|
|
1020
|
-
</p>
|
|
1021
|
-
<pre
|
|
1022
|
-
class="text-xs bg-black/30 border border-border rounded p-2 overflow-auto"
|
|
1023
|
-
>
|
|
1024
|
-
${jsonPretty(item.payload)}</pre
|
|
1025
|
-
>
|
|
1026
|
-
<div
|
|
1027
|
-
class="mt-2 flex justify-start gap-2"
|
|
1028
|
-
>
|
|
1029
|
-
<button
|
|
1030
|
-
class="h-7 text-xs px-2.5 rounded-lg ac-btn-secondary"
|
|
1031
|
-
onclick=${() =>
|
|
1032
|
-
handleCopyRequestField(
|
|
1033
|
-
item.payload,
|
|
1034
|
-
"Payload",
|
|
1035
|
-
)}
|
|
1036
|
-
>
|
|
1037
|
-
Copy
|
|
1038
|
-
</button>
|
|
1039
|
-
<button
|
|
1040
|
-
class="h-7 text-xs px-2.5 rounded-lg ac-btn-secondary disabled:opacity-60"
|
|
1041
|
-
onclick=${() =>
|
|
1042
|
-
handleReplayRequest(item)}
|
|
1043
|
-
disabled=${item.payloadTruncated ||
|
|
1044
|
-
replayingRequestId === item.id}
|
|
1045
|
-
title=${item.payloadTruncated
|
|
1046
|
-
? "Cannot replay truncated payload"
|
|
1047
|
-
: "Replay this payload"}
|
|
1048
|
-
>
|
|
1049
|
-
${replayingRequestId === item.id
|
|
1050
|
-
? "Replaying..."
|
|
1051
|
-
: "Replay"}
|
|
1052
|
-
</button>
|
|
1053
|
-
</div>
|
|
1054
|
-
</div>
|
|
1055
|
-
<div>
|
|
1056
|
-
<p class="text-[11px] text-gray-500 mb-1">
|
|
1057
|
-
Gateway response
|
|
1058
|
-
(${item.gatewayStatus || "n/a"})
|
|
1059
|
-
</p>
|
|
1060
|
-
<pre
|
|
1061
|
-
class="text-xs bg-black/30 border border-border rounded p-2 overflow-auto"
|
|
1062
|
-
>
|
|
1063
|
-
${jsonPretty(item.gatewayBody)}</pre
|
|
1064
|
-
>
|
|
1065
|
-
<div
|
|
1066
|
-
class="mt-2 flex justify-start gap-2"
|
|
1067
|
-
>
|
|
1068
|
-
<button
|
|
1069
|
-
class="h-7 text-xs px-2.5 rounded-lg ac-btn-secondary"
|
|
1070
|
-
onclick=${() =>
|
|
1071
|
-
handleCopyRequestField(
|
|
1072
|
-
item.gatewayBody,
|
|
1073
|
-
"Gateway response",
|
|
1074
|
-
)}
|
|
1075
|
-
>
|
|
1076
|
-
Copy
|
|
1077
|
-
</button>
|
|
1078
|
-
${item.status === "error"
|
|
1079
|
-
? html`<${ActionButton}
|
|
1080
|
-
onClick=${() =>
|
|
1081
|
-
handleAskAgentToDebug(item)}
|
|
1082
|
-
loading=${debugLoadingRequestId ===
|
|
1083
|
-
item.id}
|
|
1084
|
-
tone="primary"
|
|
1085
|
-
size="sm"
|
|
1086
|
-
idleLabel="Ask agent to debug"
|
|
1087
|
-
loadingLabel="Loading..."
|
|
1088
|
-
className="h-7 px-2.5"
|
|
1089
|
-
/>`
|
|
1090
|
-
: null}
|
|
1091
|
-
</div>
|
|
1092
|
-
</div>
|
|
1093
|
-
</div>
|
|
1094
|
-
`
|
|
1095
|
-
: null}
|
|
1096
|
-
</details>
|
|
1097
|
-
`;
|
|
1098
|
-
})}
|
|
1099
|
-
</div>
|
|
1100
|
-
`}
|
|
1101
|
-
</div>
|
|
1102
|
-
`
|
|
1103
|
-
: html`
|
|
1104
|
-
<div
|
|
1105
|
-
class="bg-surface border border-border rounded-xl p-4 space-y-4"
|
|
1106
|
-
>
|
|
1107
|
-
${isListLoading
|
|
1108
|
-
? html`<p class="text-xs text-gray-500">Loading webhooks...</p>`
|
|
1109
|
-
: null}
|
|
1110
|
-
${!isListLoading && webhooks.length === 0
|
|
1111
|
-
? html`<p class="text-sm text-gray-500">
|
|
1112
|
-
No webhooks configured yet. Create one to get started.
|
|
1113
|
-
</p>`
|
|
1114
|
-
: null}
|
|
1115
|
-
${webhooks.length > 0
|
|
1116
|
-
? html`
|
|
1117
|
-
<div class="overflow-auto">
|
|
1118
|
-
<table class="w-full text-sm">
|
|
1119
|
-
<thead>
|
|
1120
|
-
<tr
|
|
1121
|
-
class="text-left text-xs text-gray-500 border-b border-border"
|
|
1122
|
-
>
|
|
1123
|
-
<th class="pb-2 pr-3">Path</th>
|
|
1124
|
-
<th class="pb-2 pr-3">Last received</th>
|
|
1125
|
-
<th class="pb-2 pr-3">Errors</th>
|
|
1126
|
-
<th class="pb-2 pr-3">Health</th>
|
|
1127
|
-
<th class="pb-2 pr-3">Type</th>
|
|
1128
|
-
</tr>
|
|
1129
|
-
</thead>
|
|
1130
|
-
<tbody>
|
|
1131
|
-
<tr aria-hidden="true">
|
|
1132
|
-
<td class="h-2 p-0" colspan="5"></td>
|
|
1133
|
-
</tr>
|
|
1134
|
-
${webhooks.map(
|
|
1135
|
-
(item) => html`
|
|
1136
|
-
<tr
|
|
1137
|
-
class="group cursor-pointer"
|
|
1138
|
-
onclick=${() => {
|
|
1139
|
-
onSelectHook(item.name);
|
|
1140
|
-
setStatusFilter("all");
|
|
1141
|
-
setExpandedRows(new Set());
|
|
1142
|
-
}}
|
|
1143
|
-
>
|
|
1144
|
-
<td
|
|
1145
|
-
class="px-3 py-2.5 group-hover:bg-white/5 first:rounded-l-lg transition-colors"
|
|
1146
|
-
>
|
|
1147
|
-
<code
|
|
1148
|
-
>${item.path || `/hooks/${item.name}`}</code
|
|
1149
|
-
>
|
|
1150
|
-
</td>
|
|
1151
|
-
<td
|
|
1152
|
-
class="px-3 py-2.5 text-xs text-gray-400 group-hover:bg-white/5 transition-colors"
|
|
1153
|
-
>
|
|
1154
|
-
${formatLastReceived(item.lastReceived)}
|
|
1155
|
-
</td>
|
|
1156
|
-
<td
|
|
1157
|
-
class="px-3 py-2.5 text-xs group-hover:bg-white/5 transition-colors"
|
|
1158
|
-
>
|
|
1159
|
-
${item.errorCount || 0}
|
|
1160
|
-
</td>
|
|
1161
|
-
<td
|
|
1162
|
-
class="px-3 py-2.5 group-hover:bg-white/5 last:rounded-r-lg transition-colors"
|
|
1163
|
-
>
|
|
1164
|
-
<span
|
|
1165
|
-
class="inline-block w-2.5 h-2.5 rounded-full ${healthClassName(
|
|
1166
|
-
item.health,
|
|
1167
|
-
)}"
|
|
1168
|
-
title=${item.health}
|
|
1169
|
-
/>
|
|
1170
|
-
</td>
|
|
1171
|
-
<td
|
|
1172
|
-
class="px-3 py-2.5 text-xs text-gray-400 group-hover:bg-white/5 transition-colors"
|
|
1173
|
-
>
|
|
1174
|
-
${item.managed
|
|
1175
|
-
? html`<span
|
|
1176
|
-
class="inline-flex items-center rounded-full px-2 py-0.5 text-[11px] bg-cyan-500/10 text-cyan-200"
|
|
1177
|
-
>Managed</span
|
|
1178
|
-
>`
|
|
1179
|
-
: html`<${Badge} tone="neutral">Custom</${Badge}>`}
|
|
1180
|
-
</td>
|
|
1181
|
-
</tr>
|
|
1182
|
-
`,
|
|
1183
|
-
)}
|
|
1184
|
-
</tbody>
|
|
1185
|
-
</table>
|
|
1186
|
-
</div>
|
|
1187
|
-
`
|
|
1188
|
-
: null}
|
|
1189
|
-
</div>
|
|
1190
|
-
`}
|
|
1191
|
-
|
|
1192
|
-
<${CreateWebhookModal}
|
|
1193
|
-
visible=${isCreating && !selectedHookName}
|
|
1194
|
-
name=${newName}
|
|
1195
|
-
onNameChange=${setNewName}
|
|
1196
|
-
canCreate=${canCreate}
|
|
1197
|
-
creating=${creating}
|
|
1198
|
-
onCreate=${handleCreate}
|
|
1199
|
-
onClose=${() => setIsCreating(false)}
|
|
1200
|
-
/>
|
|
1201
|
-
<${ConfirmDialog}
|
|
1202
|
-
visible=${showDeleteConfirm &&
|
|
1203
|
-
!!selectedHookName &&
|
|
1204
|
-
!selectedWebhookManaged}
|
|
1205
|
-
title="Delete webhook?"
|
|
1206
|
-
message=${`This removes "/hooks/${selectedHookName}" from openclaw.json.`}
|
|
1207
|
-
details=${html`
|
|
1208
|
-
<div class="rounded-lg border border-border bg-black/20 p-3">
|
|
1209
|
-
<label
|
|
1210
|
-
class="flex items-center gap-2 text-xs text-gray-300 select-none"
|
|
1211
|
-
>
|
|
1212
|
-
<input
|
|
1213
|
-
type="checkbox"
|
|
1214
|
-
checked=${deleteTransformDir}
|
|
1215
|
-
onInput=${(event) =>
|
|
1216
|
-
setDeleteTransformDir(!!event.target.checked)}
|
|
1217
|
-
/>
|
|
1218
|
-
Also delete <code>hooks/transforms/${selectedHookName}</code>
|
|
1219
|
-
</label>
|
|
1220
|
-
</div>
|
|
1221
|
-
`}
|
|
1222
|
-
confirmLabel="Delete webhook"
|
|
1223
|
-
confirmLoadingLabel="Deleting..."
|
|
1224
|
-
confirmLoading=${deleting}
|
|
1225
|
-
cancelLabel="Cancel"
|
|
1226
|
-
onCancel=${() => {
|
|
1227
|
-
if (deleting) return;
|
|
1228
|
-
setDeleteTransformDir(true);
|
|
1229
|
-
setShowDeleteConfirm(false);
|
|
1230
|
-
}}
|
|
1231
|
-
onConfirm=${handleDeleteConfirmed}
|
|
1232
|
-
/>
|
|
1233
|
-
<${AgentSendModal}
|
|
1234
|
-
visible=${!!debugRequest}
|
|
1235
|
-
title="Ask agent to debug"
|
|
1236
|
-
messageLabel="Debug request"
|
|
1237
|
-
messageRows=${12}
|
|
1238
|
-
initialMessage=${debugAgentMessage}
|
|
1239
|
-
resetKey=${String(debugRequest?.id || "")}
|
|
1240
|
-
submitLabel="Send debug request"
|
|
1241
|
-
loadingLabel="Sending..."
|
|
1242
|
-
onClose=${() => setDebugRequest(null)}
|
|
1243
|
-
onSubmit=${async ({ selectedSessionKey, message }) => {
|
|
1244
|
-
try {
|
|
1245
|
-
await sendAgentMessage({
|
|
1246
|
-
message,
|
|
1247
|
-
sessionKey: selectedSessionKey,
|
|
1248
|
-
});
|
|
1249
|
-
showToast("Debug request sent to agent", "success");
|
|
1250
|
-
return true;
|
|
1251
|
-
} catch (err) {
|
|
1252
|
-
showToast(err.message || "Could not send debug request", "error");
|
|
1253
|
-
return false;
|
|
1254
|
-
}
|
|
1255
|
-
}}
|
|
1256
|
-
/>
|
|
1257
|
-
</div>
|
|
1258
|
-
`;
|
|
1259
|
-
};
|