@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
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
import { h } from "https://esm.sh/preact";
|
|
2
|
+
import htm from "https://esm.sh/htm";
|
|
3
|
+
import {
|
|
4
|
+
kNoDestinationSessionValue,
|
|
5
|
+
useDestinationSessionSelection,
|
|
6
|
+
} from "../../../hooks/use-destination-session-selection.js";
|
|
7
|
+
import { ActionButton } from "../../action-button.js";
|
|
8
|
+
import { CloseIcon } from "../../icons.js";
|
|
9
|
+
import { ModalShell } from "../../modal-shell.js";
|
|
10
|
+
import { PageHeader } from "../../page-header.js";
|
|
11
|
+
import { SessionSelectField } from "../../session-select-field.js";
|
|
12
|
+
|
|
13
|
+
const html = htm.bind(h);
|
|
14
|
+
|
|
15
|
+
export const CreateWebhookModal = ({
|
|
16
|
+
visible,
|
|
17
|
+
name,
|
|
18
|
+
mode = "webhook",
|
|
19
|
+
onModeChange = () => {},
|
|
20
|
+
onNameChange = () => {},
|
|
21
|
+
canCreate = false,
|
|
22
|
+
creating = false,
|
|
23
|
+
onCreate = () => {},
|
|
24
|
+
onClose = () => {},
|
|
25
|
+
}) => {
|
|
26
|
+
const {
|
|
27
|
+
sessions: selectableSessions,
|
|
28
|
+
loading: loadingSessions,
|
|
29
|
+
error: destinationLoadError,
|
|
30
|
+
destinationSessionKey,
|
|
31
|
+
setDestinationSessionKey,
|
|
32
|
+
selectedDestination,
|
|
33
|
+
} = useDestinationSessionSelection({
|
|
34
|
+
enabled: visible,
|
|
35
|
+
resetKey: String(visible),
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
const normalized = String(name || "")
|
|
39
|
+
.trim()
|
|
40
|
+
.toLowerCase();
|
|
41
|
+
const previewName = normalized || "{name}";
|
|
42
|
+
const previewPath = `/hooks/${previewName}`;
|
|
43
|
+
const previewUrl =
|
|
44
|
+
mode === "oauth"
|
|
45
|
+
? `${window.location.origin}/oauth/{id}`
|
|
46
|
+
: `${window.location.origin}${previewPath}`;
|
|
47
|
+
if (!visible) return null;
|
|
48
|
+
|
|
49
|
+
return html`
|
|
50
|
+
<${ModalShell}
|
|
51
|
+
visible=${visible}
|
|
52
|
+
onClose=${onClose}
|
|
53
|
+
panelClassName="bg-modal border border-border rounded-xl p-5 max-w-lg w-full space-y-4"
|
|
54
|
+
>
|
|
55
|
+
<${PageHeader}
|
|
56
|
+
title="Create Webhook"
|
|
57
|
+
actions=${html`
|
|
58
|
+
<button
|
|
59
|
+
type="button"
|
|
60
|
+
onclick=${onClose}
|
|
61
|
+
class="h-8 w-8 inline-flex items-center justify-center rounded-lg ac-btn-secondary"
|
|
62
|
+
aria-label="Close modal"
|
|
63
|
+
>
|
|
64
|
+
<${CloseIcon} className="w-3.5 h-3.5 text-gray-300" />
|
|
65
|
+
</button>
|
|
66
|
+
`}
|
|
67
|
+
/>
|
|
68
|
+
<div class="space-y-2">
|
|
69
|
+
<p class="text-xs text-gray-500">Endpoint mode</p>
|
|
70
|
+
<div class="flex items-center gap-2">
|
|
71
|
+
<button
|
|
72
|
+
class="text-xs px-2 py-1 rounded border transition-colors ${mode ===
|
|
73
|
+
"webhook"
|
|
74
|
+
? "border-cyan-400 text-cyan-200 bg-cyan-400/10"
|
|
75
|
+
: "border-border text-gray-400 hover:text-gray-200"}"
|
|
76
|
+
onclick=${() => onModeChange("webhook")}
|
|
77
|
+
>
|
|
78
|
+
Webhook
|
|
79
|
+
</button>
|
|
80
|
+
<button
|
|
81
|
+
class="text-xs px-2 py-1 rounded border transition-colors ${mode ===
|
|
82
|
+
"oauth"
|
|
83
|
+
? "border-cyan-400 text-cyan-200 bg-cyan-400/10"
|
|
84
|
+
: "border-border text-gray-400 hover:text-gray-200"}"
|
|
85
|
+
onclick=${() => onModeChange("oauth")}
|
|
86
|
+
>
|
|
87
|
+
OAuth Callback
|
|
88
|
+
</button>
|
|
89
|
+
</div>
|
|
90
|
+
</div>
|
|
91
|
+
<div class="space-y-2">
|
|
92
|
+
<p class="text-xs text-gray-500">Name</p>
|
|
93
|
+
<input
|
|
94
|
+
type="text"
|
|
95
|
+
value=${name}
|
|
96
|
+
placeholder="fathom"
|
|
97
|
+
onInput=${(e) => onNameChange(e.target.value)}
|
|
98
|
+
onKeyDown=${(e) => {
|
|
99
|
+
if (e.key === "Enter" && canCreate && !creating) {
|
|
100
|
+
onCreate(selectedDestination, mode);
|
|
101
|
+
}
|
|
102
|
+
if (e.key === "Escape") onClose();
|
|
103
|
+
}}
|
|
104
|
+
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"
|
|
105
|
+
/>
|
|
106
|
+
</div>
|
|
107
|
+
<${SessionSelectField}
|
|
108
|
+
label="Deliver to"
|
|
109
|
+
sessions=${selectableSessions}
|
|
110
|
+
selectedSessionKey=${destinationSessionKey}
|
|
111
|
+
onChangeSessionKey=${setDestinationSessionKey}
|
|
112
|
+
disabled=${loadingSessions || creating}
|
|
113
|
+
loading=${loadingSessions}
|
|
114
|
+
error=${destinationLoadError}
|
|
115
|
+
allowNone=${true}
|
|
116
|
+
noneValue=${kNoDestinationSessionValue}
|
|
117
|
+
noneLabel="Default"
|
|
118
|
+
emptyStateText="No paired chat sessions found yet. You can still create the webhook without a default destination."
|
|
119
|
+
loadingLabel="Loading destinations..."
|
|
120
|
+
/>
|
|
121
|
+
<div class="border border-border rounded-lg overflow-hidden">
|
|
122
|
+
<table class="w-full text-xs">
|
|
123
|
+
<tbody>
|
|
124
|
+
<tr class="border-b border-border">
|
|
125
|
+
<td class="w-24 px-3 py-2 text-gray-500">Path</td>
|
|
126
|
+
<td class="px-3 py-2 text-gray-300 font-mono">
|
|
127
|
+
<code>${previewPath}</code>
|
|
128
|
+
</td>
|
|
129
|
+
</tr>
|
|
130
|
+
<tr class="border-b border-border">
|
|
131
|
+
<td class="w-24 px-3 py-2 text-gray-500">URL</td>
|
|
132
|
+
<td class="px-3 py-2 text-gray-300 font-mono break-all">
|
|
133
|
+
<code>${previewUrl}</code>
|
|
134
|
+
</td>
|
|
135
|
+
</tr>
|
|
136
|
+
<tr>
|
|
137
|
+
<td class="w-24 px-3 py-2 text-gray-500">Transform</td>
|
|
138
|
+
<td class="px-3 py-2 text-gray-300 font-mono">
|
|
139
|
+
<code>hooks/transforms/${previewName}/${previewName}-transform.mjs</code>
|
|
140
|
+
</td>
|
|
141
|
+
</tr>
|
|
142
|
+
</tbody>
|
|
143
|
+
</table>
|
|
144
|
+
</div>
|
|
145
|
+
${mode === "oauth"
|
|
146
|
+
? html`
|
|
147
|
+
<div class="space-y-1">
|
|
148
|
+
<p class="text-xs text-gray-500">
|
|
149
|
+
For OAuth providers that can't send auth headers. AlphaClaw
|
|
150
|
+
injects webhook auth before forwarding to /hooks/{name}.
|
|
151
|
+
</p>
|
|
152
|
+
</div>
|
|
153
|
+
`
|
|
154
|
+
: null}
|
|
155
|
+
<div class="pt-1 flex items-center justify-end gap-2">
|
|
156
|
+
<${ActionButton}
|
|
157
|
+
onClick=${onClose}
|
|
158
|
+
tone="secondary"
|
|
159
|
+
size="md"
|
|
160
|
+
idleLabel="Cancel"
|
|
161
|
+
className="px-4 py-2 rounded-lg text-sm"
|
|
162
|
+
/>
|
|
163
|
+
<${ActionButton}
|
|
164
|
+
onClick=${() => onCreate(selectedDestination, mode)}
|
|
165
|
+
disabled=${!canCreate || creating}
|
|
166
|
+
loading=${creating}
|
|
167
|
+
tone="primary"
|
|
168
|
+
size="md"
|
|
169
|
+
idleLabel="Create"
|
|
170
|
+
loadingLabel="Creating..."
|
|
171
|
+
className="px-4 py-2 rounded-lg text-sm"
|
|
172
|
+
/>
|
|
173
|
+
</div>
|
|
174
|
+
</${ModalShell}>
|
|
175
|
+
`;
|
|
176
|
+
};
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import {
|
|
2
|
+
formatLocaleDateTime,
|
|
3
|
+
formatLocaleDateTimeWithTodayTime,
|
|
4
|
+
} from "../../lib/format.js";
|
|
5
|
+
|
|
6
|
+
export const kNamePattern = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
|
|
7
|
+
export const kStatusFilters = ["all", "success", "error"];
|
|
8
|
+
|
|
9
|
+
export const formatDateTime = (value) => {
|
|
10
|
+
return formatLocaleDateTime(value, { fallback: "—" });
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
export const formatLastReceived = (value) => {
|
|
14
|
+
return formatLocaleDateTimeWithTodayTime(value, { fallback: "—" });
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
export const formatBytes = (size) => {
|
|
18
|
+
const bytes = Number(size || 0);
|
|
19
|
+
if (!Number.isFinite(bytes) || bytes <= 0) return "0B";
|
|
20
|
+
if (bytes < 1024) return `${bytes}B`;
|
|
21
|
+
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)}KB`;
|
|
22
|
+
return `${(bytes / (1024 * 1024)).toFixed(1)}MB`;
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
export const healthClassName = (health) => {
|
|
26
|
+
if (health === "red") return "bg-red-500";
|
|
27
|
+
if (health === "yellow") return "bg-yellow-500";
|
|
28
|
+
return "bg-green-500";
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
export const getRequestStatusTone = (status) => {
|
|
32
|
+
if (status === "success") {
|
|
33
|
+
return {
|
|
34
|
+
dotClass: "bg-green-500/90",
|
|
35
|
+
textClass: "text-green-500/90",
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
if (status === "error") {
|
|
39
|
+
return {
|
|
40
|
+
dotClass: "bg-red-500/90",
|
|
41
|
+
textClass: "text-red-500/90",
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
return {
|
|
45
|
+
dotClass: "bg-gray-500/70",
|
|
46
|
+
textClass: "text-gray-400",
|
|
47
|
+
};
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
export const formatAgentFallbackName = (agentId = "") =>
|
|
51
|
+
String(agentId || "")
|
|
52
|
+
.trim()
|
|
53
|
+
.split(/[-_\s]+/)
|
|
54
|
+
.filter(Boolean)
|
|
55
|
+
.map((part) => part.charAt(0).toUpperCase() + part.slice(1))
|
|
56
|
+
.join(" ") || "Main Agent";
|
|
57
|
+
|
|
58
|
+
export const jsonPretty = (value) => {
|
|
59
|
+
if (typeof value === "string") {
|
|
60
|
+
try {
|
|
61
|
+
const parsed = JSON.parse(value);
|
|
62
|
+
return JSON.stringify(parsed, null, 2);
|
|
63
|
+
} catch {
|
|
64
|
+
return value;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
try {
|
|
68
|
+
return JSON.stringify(value || {}, null, 2);
|
|
69
|
+
} catch {
|
|
70
|
+
return String(value || "");
|
|
71
|
+
}
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
export const buildWebhookDebugMessage = ({
|
|
75
|
+
hookName = "",
|
|
76
|
+
webhook = null,
|
|
77
|
+
request = null,
|
|
78
|
+
}) => {
|
|
79
|
+
const hookPath =
|
|
80
|
+
String(webhook?.path || "").trim() ||
|
|
81
|
+
(hookName ? `/hooks/${hookName}` : "/hooks/unknown");
|
|
82
|
+
const gatewayStatus =
|
|
83
|
+
request?.gatewayStatus == null ? "n/a" : String(request.gatewayStatus);
|
|
84
|
+
return [
|
|
85
|
+
"Investigate this failed webhook request and share findings before fixing anything.",
|
|
86
|
+
"Reply with your diagnosis first, including the likely root cause, any relevant risks, and what you would change if I approve a fix.",
|
|
87
|
+
"",
|
|
88
|
+
`Webhook: ${hookPath}`,
|
|
89
|
+
`Request ID: ${String(request?.id || "unknown")}`,
|
|
90
|
+
`Time: ${String(request?.createdAt || "unknown")}`,
|
|
91
|
+
`Method: ${String(request?.method || "unknown")}`,
|
|
92
|
+
`Source IP: ${String(request?.sourceIp || "unknown")}`,
|
|
93
|
+
`Gateway status: ${gatewayStatus}`,
|
|
94
|
+
`Transform path: ${String(webhook?.transformPath || "unknown")}`,
|
|
95
|
+
`Payload truncated: ${request?.payloadTruncated ? "yes" : "no"}`,
|
|
96
|
+
"",
|
|
97
|
+
"Headers:",
|
|
98
|
+
jsonPretty(request?.headers),
|
|
99
|
+
"",
|
|
100
|
+
"Payload:",
|
|
101
|
+
jsonPretty(request?.payload),
|
|
102
|
+
"",
|
|
103
|
+
"Gateway response:",
|
|
104
|
+
jsonPretty(request?.gatewayBody),
|
|
105
|
+
].join("\n");
|
|
106
|
+
};
|
|
@@ -0,0 +1,148 @@
|
|
|
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 { createWebhook } from "../../lib/api.js";
|
|
5
|
+
import { ActionButton } from "../action-button.js";
|
|
6
|
+
import { PageHeader } from "../page-header.js";
|
|
7
|
+
import { showToast } from "../toast.js";
|
|
8
|
+
import { CreateWebhookModal } from "./create-webhook-modal/index.js";
|
|
9
|
+
import { kNamePattern } from "./helpers.js";
|
|
10
|
+
import { WebhookDetail } from "./webhook-detail/index.js";
|
|
11
|
+
import { WebhookList } from "./webhook-list/index.js";
|
|
12
|
+
|
|
13
|
+
const html = htm.bind(h);
|
|
14
|
+
|
|
15
|
+
export const Webhooks = ({
|
|
16
|
+
selectedHookName = "",
|
|
17
|
+
onSelectHook = () => {},
|
|
18
|
+
onBackToList = () => {},
|
|
19
|
+
onRestartRequired = () => {},
|
|
20
|
+
onOpenFile = () => {},
|
|
21
|
+
}) => {
|
|
22
|
+
const [isCreating, setIsCreating] = useState(false);
|
|
23
|
+
const [newName, setNewName] = useState("");
|
|
24
|
+
const [createMode, setCreateMode] = useState("webhook");
|
|
25
|
+
const [creating, setCreating] = useState(false);
|
|
26
|
+
|
|
27
|
+
const canCreate = useMemo(() => {
|
|
28
|
+
const name = String(newName || "")
|
|
29
|
+
.trim()
|
|
30
|
+
.toLowerCase();
|
|
31
|
+
return kNamePattern.test(name);
|
|
32
|
+
}, [newName]);
|
|
33
|
+
|
|
34
|
+
const handleCreate = useCallback(
|
|
35
|
+
async (destination = null, mode = "webhook") => {
|
|
36
|
+
const candidateName = String(newName || "")
|
|
37
|
+
.trim()
|
|
38
|
+
.toLowerCase();
|
|
39
|
+
if (!kNamePattern.test(candidateName)) {
|
|
40
|
+
showToast(
|
|
41
|
+
"Name must be lowercase letters, numbers, and hyphens",
|
|
42
|
+
"error",
|
|
43
|
+
);
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
if (creating) return;
|
|
47
|
+
setCreating(true);
|
|
48
|
+
try {
|
|
49
|
+
const data = await createWebhook(candidateName, {
|
|
50
|
+
destination,
|
|
51
|
+
oauthCallback: mode === "oauth",
|
|
52
|
+
});
|
|
53
|
+
setIsCreating(false);
|
|
54
|
+
setNewName("");
|
|
55
|
+
setCreateMode("webhook");
|
|
56
|
+
onSelectHook(candidateName);
|
|
57
|
+
if (data.restartRequired) onRestartRequired(true);
|
|
58
|
+
if (mode === "oauth" && data?.webhook?.oauthCallbackUrl) {
|
|
59
|
+
showToast("Webhook + OAuth callback created", "success");
|
|
60
|
+
} else {
|
|
61
|
+
showToast("Webhook created", "success");
|
|
62
|
+
}
|
|
63
|
+
if (data.syncWarning) {
|
|
64
|
+
showToast(`Created, but git-sync failed: ${data.syncWarning}`, "warning");
|
|
65
|
+
}
|
|
66
|
+
} catch (err) {
|
|
67
|
+
showToast(err.message || "Could not create webhook", "error");
|
|
68
|
+
} finally {
|
|
69
|
+
setCreating(false);
|
|
70
|
+
}
|
|
71
|
+
},
|
|
72
|
+
[creating, newName, onRestartRequired, onSelectHook],
|
|
73
|
+
);
|
|
74
|
+
|
|
75
|
+
return html`
|
|
76
|
+
<div class="space-y-4">
|
|
77
|
+
<${PageHeader}
|
|
78
|
+
title="Webhooks"
|
|
79
|
+
leading=${selectedHookName
|
|
80
|
+
? html`
|
|
81
|
+
<button
|
|
82
|
+
class="flex items-center gap-1.5 text-sm text-gray-500 hover:text-gray-300 transition-colors"
|
|
83
|
+
onclick=${onBackToList}
|
|
84
|
+
>
|
|
85
|
+
<svg
|
|
86
|
+
width="16"
|
|
87
|
+
height="16"
|
|
88
|
+
viewBox="0 0 16 16"
|
|
89
|
+
fill="currentColor"
|
|
90
|
+
>
|
|
91
|
+
<path
|
|
92
|
+
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"
|
|
93
|
+
/>
|
|
94
|
+
</svg>
|
|
95
|
+
Back
|
|
96
|
+
</button>
|
|
97
|
+
`
|
|
98
|
+
: null}
|
|
99
|
+
actions=${selectedHookName
|
|
100
|
+
? null
|
|
101
|
+
: html`
|
|
102
|
+
<${ActionButton}
|
|
103
|
+
onClick=${() => {
|
|
104
|
+
setCreateMode("webhook");
|
|
105
|
+
setIsCreating((open) => !open);
|
|
106
|
+
}}
|
|
107
|
+
tone="secondary"
|
|
108
|
+
size="sm"
|
|
109
|
+
idleLabel="Create new"
|
|
110
|
+
className="px-3 py-1.5"
|
|
111
|
+
/>
|
|
112
|
+
`}
|
|
113
|
+
/>
|
|
114
|
+
|
|
115
|
+
${selectedHookName
|
|
116
|
+
? html`
|
|
117
|
+
<${WebhookDetail}
|
|
118
|
+
selectedHookName=${selectedHookName}
|
|
119
|
+
onBackToList=${onBackToList}
|
|
120
|
+
onRestartRequired=${onRestartRequired}
|
|
121
|
+
onOpenFile=${onOpenFile}
|
|
122
|
+
/>
|
|
123
|
+
`
|
|
124
|
+
: html`
|
|
125
|
+
<${WebhookList}
|
|
126
|
+
onSelectHook=${(name) => {
|
|
127
|
+
onSelectHook(name);
|
|
128
|
+
}}
|
|
129
|
+
/>
|
|
130
|
+
`}
|
|
131
|
+
|
|
132
|
+
<${CreateWebhookModal}
|
|
133
|
+
visible=${isCreating && !selectedHookName}
|
|
134
|
+
name=${newName}
|
|
135
|
+
mode=${createMode}
|
|
136
|
+
onModeChange=${setCreateMode}
|
|
137
|
+
onNameChange=${setNewName}
|
|
138
|
+
canCreate=${canCreate}
|
|
139
|
+
creating=${creating}
|
|
140
|
+
onCreate=${handleCreate}
|
|
141
|
+
onClose=${() => {
|
|
142
|
+
setIsCreating(false);
|
|
143
|
+
setCreateMode("webhook");
|
|
144
|
+
}}
|
|
145
|
+
/>
|
|
146
|
+
</div>
|
|
147
|
+
`;
|
|
148
|
+
};
|
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
import { h } from "https://esm.sh/preact";
|
|
2
|
+
import { useMemo } from "https://esm.sh/preact/hooks";
|
|
3
|
+
import htm from "https://esm.sh/htm";
|
|
4
|
+
import { sendAgentMessage } from "../../../lib/api.js";
|
|
5
|
+
import { ActionButton } from "../../action-button.js";
|
|
6
|
+
import { AgentSendModal } from "../../agent-send-modal.js";
|
|
7
|
+
import { showToast } from "../../toast.js";
|
|
8
|
+
import {
|
|
9
|
+
buildWebhookDebugMessage,
|
|
10
|
+
formatBytes,
|
|
11
|
+
formatLastReceived,
|
|
12
|
+
getRequestStatusTone,
|
|
13
|
+
jsonPretty,
|
|
14
|
+
kStatusFilters,
|
|
15
|
+
} from "../helpers.js";
|
|
16
|
+
import { useRequestHistory } from "./use-request-history.js";
|
|
17
|
+
|
|
18
|
+
const html = htm.bind(h);
|
|
19
|
+
|
|
20
|
+
export const RequestHistory = ({
|
|
21
|
+
selectedHookName = "",
|
|
22
|
+
effectiveAuthMode = "headers",
|
|
23
|
+
webhookUrl = "",
|
|
24
|
+
webhookUrlWithQueryToken = "",
|
|
25
|
+
bearerTokenValue = "",
|
|
26
|
+
selectedWebhook = null,
|
|
27
|
+
}) => {
|
|
28
|
+
const { state, actions } = useRequestHistory({
|
|
29
|
+
selectedHookName,
|
|
30
|
+
effectiveAuthMode,
|
|
31
|
+
webhookUrl,
|
|
32
|
+
webhookUrlWithQueryToken,
|
|
33
|
+
bearerTokenValue,
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
const {
|
|
37
|
+
requests,
|
|
38
|
+
statusFilter,
|
|
39
|
+
expandedRows,
|
|
40
|
+
replayingRequestId,
|
|
41
|
+
debugLoadingRequestId,
|
|
42
|
+
debugRequest,
|
|
43
|
+
} = state;
|
|
44
|
+
|
|
45
|
+
const debugAgentMessage = useMemo(
|
|
46
|
+
() =>
|
|
47
|
+
buildWebhookDebugMessage({
|
|
48
|
+
hookName: selectedHookName,
|
|
49
|
+
webhook: selectedWebhook,
|
|
50
|
+
request: debugRequest,
|
|
51
|
+
}),
|
|
52
|
+
[debugRequest, selectedHookName, selectedWebhook],
|
|
53
|
+
);
|
|
54
|
+
|
|
55
|
+
return html`
|
|
56
|
+
<div class="bg-surface border border-border rounded-xl p-4 space-y-3">
|
|
57
|
+
<div class="flex items-center justify-between gap-3">
|
|
58
|
+
<h3 class="card-label">Request history</h3>
|
|
59
|
+
<div class="flex items-center gap-2">
|
|
60
|
+
${kStatusFilters.map(
|
|
61
|
+
(filter) => html`
|
|
62
|
+
<button
|
|
63
|
+
class="text-xs px-2 py-1 rounded border ${statusFilter === filter
|
|
64
|
+
? "border-cyan-400 text-cyan-200 bg-cyan-400/10"
|
|
65
|
+
: "border-border text-gray-400 hover:text-gray-200"}"
|
|
66
|
+
onclick=${() => actions.handleSetStatusFilter(filter)}
|
|
67
|
+
>
|
|
68
|
+
${filter}
|
|
69
|
+
</button>
|
|
70
|
+
`,
|
|
71
|
+
)}
|
|
72
|
+
</div>
|
|
73
|
+
</div>
|
|
74
|
+
|
|
75
|
+
${requests.length === 0
|
|
76
|
+
? html`<p class="text-sm text-gray-500">No requests logged yet.</p>`
|
|
77
|
+
: html`
|
|
78
|
+
<div class="ac-history-list">
|
|
79
|
+
${requests.map((item) => {
|
|
80
|
+
const statusTone = getRequestStatusTone(item.status);
|
|
81
|
+
return html`
|
|
82
|
+
<details
|
|
83
|
+
class="ac-history-item"
|
|
84
|
+
open=${expandedRows.has(item.id)}
|
|
85
|
+
ontoggle=${(e) =>
|
|
86
|
+
actions.handleRequestRowToggle(item.id, !!e.currentTarget?.open)}
|
|
87
|
+
>
|
|
88
|
+
<summary class="ac-history-summary">
|
|
89
|
+
<div class="ac-history-summary-row">
|
|
90
|
+
<span class="inline-flex items-center gap-2 min-w-0">
|
|
91
|
+
<span class="ac-history-toggle shrink-0" aria-hidden="true"
|
|
92
|
+
>▸</span
|
|
93
|
+
>
|
|
94
|
+
<span class="truncate text-xs text-gray-300">
|
|
95
|
+
${formatLastReceived(item.createdAt)}
|
|
96
|
+
</span>
|
|
97
|
+
</span>
|
|
98
|
+
<span class="inline-flex items-center gap-2 shrink-0">
|
|
99
|
+
<span class="text-xs text-gray-500"
|
|
100
|
+
>${formatBytes(item.payloadSize)}</span
|
|
101
|
+
>
|
|
102
|
+
<span class=${`text-xs font-medium ${statusTone.textClass}`}
|
|
103
|
+
>${item.gatewayStatus || "n/a"}</span
|
|
104
|
+
>
|
|
105
|
+
<span class="inline-flex items-center">
|
|
106
|
+
<span
|
|
107
|
+
class=${`h-2.5 w-2.5 rounded-full ${statusTone.dotClass}`}
|
|
108
|
+
title=${item.status || "unknown"}
|
|
109
|
+
aria-label=${item.status || "unknown"}
|
|
110
|
+
></span>
|
|
111
|
+
</span>
|
|
112
|
+
</span>
|
|
113
|
+
</div>
|
|
114
|
+
</summary>
|
|
115
|
+
${expandedRows.has(item.id)
|
|
116
|
+
? html`
|
|
117
|
+
<div class="ac-history-body space-y-3">
|
|
118
|
+
<div>
|
|
119
|
+
<p class="text-[11px] text-gray-500 mb-1">Headers</p>
|
|
120
|
+
<pre
|
|
121
|
+
class="text-xs bg-black/30 border border-border rounded p-2 overflow-auto"
|
|
122
|
+
>
|
|
123
|
+
${jsonPretty(item.headers)}</pre
|
|
124
|
+
>
|
|
125
|
+
<div class="mt-2 flex justify-start gap-2">
|
|
126
|
+
<button
|
|
127
|
+
class="h-7 text-xs px-2.5 rounded-lg ac-btn-secondary"
|
|
128
|
+
onclick=${() =>
|
|
129
|
+
actions.handleCopyRequestField(
|
|
130
|
+
jsonPretty(item.headers),
|
|
131
|
+
"Headers",
|
|
132
|
+
)}
|
|
133
|
+
>
|
|
134
|
+
Copy
|
|
135
|
+
</button>
|
|
136
|
+
</div>
|
|
137
|
+
</div>
|
|
138
|
+
<div>
|
|
139
|
+
<p class="text-[11px] text-gray-500 mb-1">
|
|
140
|
+
Payload ${item.payloadTruncated ? "(truncated)" : ""}
|
|
141
|
+
</p>
|
|
142
|
+
<pre
|
|
143
|
+
class="text-xs bg-black/30 border border-border rounded p-2 overflow-auto"
|
|
144
|
+
>
|
|
145
|
+
${jsonPretty(item.payload)}</pre
|
|
146
|
+
>
|
|
147
|
+
<div class="mt-2 flex justify-start gap-2">
|
|
148
|
+
<button
|
|
149
|
+
class="h-7 text-xs px-2.5 rounded-lg ac-btn-secondary"
|
|
150
|
+
onclick=${() =>
|
|
151
|
+
actions.handleCopyRequestField(
|
|
152
|
+
item.payload,
|
|
153
|
+
"Payload",
|
|
154
|
+
)}
|
|
155
|
+
>
|
|
156
|
+
Copy
|
|
157
|
+
</button>
|
|
158
|
+
<button
|
|
159
|
+
class="h-7 text-xs px-2.5 rounded-lg ac-btn-secondary disabled:opacity-60"
|
|
160
|
+
onclick=${() => actions.handleReplayRequest(item)}
|
|
161
|
+
disabled=${item.payloadTruncated ||
|
|
162
|
+
replayingRequestId === item.id}
|
|
163
|
+
title=${item.payloadTruncated
|
|
164
|
+
? "Cannot replay truncated payload"
|
|
165
|
+
: "Replay this payload"}
|
|
166
|
+
>
|
|
167
|
+
${replayingRequestId === item.id
|
|
168
|
+
? "Replaying..."
|
|
169
|
+
: "Replay"}
|
|
170
|
+
</button>
|
|
171
|
+
</div>
|
|
172
|
+
</div>
|
|
173
|
+
<div>
|
|
174
|
+
<p class="text-[11px] text-gray-500 mb-1">
|
|
175
|
+
Gateway response (${item.gatewayStatus || "n/a"})
|
|
176
|
+
</p>
|
|
177
|
+
<pre
|
|
178
|
+
class="text-xs bg-black/30 border border-border rounded p-2 overflow-auto"
|
|
179
|
+
>
|
|
180
|
+
${jsonPretty(item.gatewayBody)}</pre
|
|
181
|
+
>
|
|
182
|
+
<div class="mt-2 flex justify-start gap-2">
|
|
183
|
+
<button
|
|
184
|
+
class="h-7 text-xs px-2.5 rounded-lg ac-btn-secondary"
|
|
185
|
+
onclick=${() =>
|
|
186
|
+
actions.handleCopyRequestField(
|
|
187
|
+
item.gatewayBody,
|
|
188
|
+
"Gateway response",
|
|
189
|
+
)}
|
|
190
|
+
>
|
|
191
|
+
Copy
|
|
192
|
+
</button>
|
|
193
|
+
${item.status === "error"
|
|
194
|
+
? html`<${ActionButton}
|
|
195
|
+
onClick=${() =>
|
|
196
|
+
actions.handleAskAgentToDebug(item)}
|
|
197
|
+
loading=${debugLoadingRequestId === item.id}
|
|
198
|
+
tone="primary"
|
|
199
|
+
size="sm"
|
|
200
|
+
idleLabel="Ask agent to debug"
|
|
201
|
+
loadingLabel="Loading..."
|
|
202
|
+
className="h-7 px-2.5"
|
|
203
|
+
/>`
|
|
204
|
+
: null}
|
|
205
|
+
</div>
|
|
206
|
+
</div>
|
|
207
|
+
</div>
|
|
208
|
+
`
|
|
209
|
+
: null}
|
|
210
|
+
</details>
|
|
211
|
+
`;
|
|
212
|
+
})}
|
|
213
|
+
</div>
|
|
214
|
+
`}
|
|
215
|
+
<${AgentSendModal}
|
|
216
|
+
visible=${!!debugRequest}
|
|
217
|
+
title="Ask agent to debug"
|
|
218
|
+
messageLabel="Debug request"
|
|
219
|
+
messageRows=${12}
|
|
220
|
+
initialMessage=${debugAgentMessage}
|
|
221
|
+
resetKey=${String(debugRequest?.id || "")}
|
|
222
|
+
submitLabel="Send debug request"
|
|
223
|
+
loadingLabel="Sending..."
|
|
224
|
+
onClose=${() => actions.setDebugRequest(null)}
|
|
225
|
+
onSubmit=${async ({ selectedSessionKey, message }) => {
|
|
226
|
+
try {
|
|
227
|
+
await sendAgentMessage({
|
|
228
|
+
message,
|
|
229
|
+
sessionKey: selectedSessionKey,
|
|
230
|
+
});
|
|
231
|
+
showToast("Debug request sent to agent", "success");
|
|
232
|
+
return true;
|
|
233
|
+
} catch (err) {
|
|
234
|
+
showToast(err.message || "Could not send debug request", "error");
|
|
235
|
+
return false;
|
|
236
|
+
}
|
|
237
|
+
}}
|
|
238
|
+
/>
|
|
239
|
+
</div>
|
|
240
|
+
`;
|
|
241
|
+
};
|