tide-commander 1.91.0 → 1.92.0
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/assets/{BossLogsModal-XsTxfWM8.js → BossLogsModal-BTTK3SDM.js} +1 -1
- package/dist/assets/{BossSpawnModal-DqQMPxHu.js → BossSpawnModal-BNbFVewB.js} +1 -1
- package/dist/assets/{ControlsModal-5mzDDdS5.js → ControlsModal-DnKdyYiv.js} +1 -1
- package/dist/assets/{DockerLogsModal-2eHlxyKa.js → DockerLogsModal-BRFhRQka.js} +1 -1
- package/dist/assets/{EmbeddedEditor-Bi9Ysd99.js → EmbeddedEditor-BFnSppyy.js} +1 -1
- package/dist/assets/{GmailOAuthSetup-5u85N8Br.js → GmailOAuthSetup-9uMrVRsz.js} +1 -1
- package/dist/assets/{GoogleOAuthSetup-OxT_QwZL.js → GoogleOAuthSetup-CLAy54pj.js} +1 -1
- package/dist/assets/{IframeModal-Bn1kdP1S.js → IframeModal-Cy5KHwqs.js} +1 -1
- package/dist/assets/{IntegrationsPanel-BehHkKJu.js → IntegrationsPanel-C_a9pmU1.js} +2 -2
- package/dist/assets/{LogViewerModal-JuUpWFPL.js → LogViewerModal-D68ppO94.js} +1 -1
- package/dist/assets/{MonitoringModal-CLk3uqDa.js → MonitoringModal-CoZ0uDPq.js} +1 -1
- package/dist/assets/{PM2LogsModal-C_NpOsos.js → PM2LogsModal-Bbwdouj-.js} +1 -1
- package/dist/assets/{RestoreArchivedAreaModal-Cbcg2Fm8.js → RestoreArchivedAreaModal-Dqi_ofXl.js} +1 -1
- package/dist/assets/{Scene2DCanvas-4C-jHERv.js → Scene2DCanvas-CnqwS4QZ.js} +1 -1
- package/dist/assets/{SceneManager-BoRV8xt3.js → SceneManager-W7pJrfEW.js} +1 -1
- package/dist/assets/{SkillsPanel-Bwk3UEY_.js → SkillsPanel-Cw-tVu6j.js} +1 -1
- package/dist/assets/{SlackMultiInstanceSetup-t-g3hdbr.js → SlackMultiInstanceSetup-DddkR8Zr.js} +1 -1
- package/dist/assets/{SpawnModal-BOXkPtaJ.js → SpawnModal-DZODgZFD.js} +1 -1
- package/dist/assets/{SubordinateAssignmentModal-CLHq5a9b.js → SubordinateAssignmentModal-BkhktU8i.js} +1 -1
- package/dist/assets/TriggerManagerPanel-YcbVJ2Rc.js +9 -0
- package/dist/assets/{WorkflowEditorPanel-Bevs1fpc.js → WorkflowEditorPanel-ZadWONKw.js} +1 -1
- package/dist/assets/{index-CiXA-Zp-.js → index-Bi8u2UlO.js} +1 -1
- package/dist/assets/{index-DxHwQ6CI.js → index-CF1hOKKa.js} +3 -3
- package/dist/assets/{index-CdKOXIM2.js → index-CN-XkbYd.js} +1 -1
- package/dist/assets/{index-CJuTMFz9.js → index-Ckn27_lZ.js} +1 -1
- package/dist/assets/{index-DBt10C9K.js → index-CtEyTmv-.js} +1 -1
- package/dist/assets/{index-H8kj1tuO.js → index-CyPV_nq1.js} +1 -1
- package/dist/assets/{index-Dd063aRs.js → index-DRsSsabz.js} +1 -1
- package/dist/assets/{index-B4JdUiAe.js → index-cK048gKM.js} +2 -2
- package/dist/assets/{index-vFrHpR5s.js → index-pqOlU6XT.js} +1 -1
- package/dist/assets/{main-5eyR3isL.js → main-jN7LzP4f.js} +95 -96
- package/dist/assets/{web-Cx_ySRHK.js → web-CfwxlY8x.js} +1 -1
- package/dist/assets/{web-DMjkVCWy.js → web-DZfyqisM.js} +1 -1
- package/dist/assets/{web-DGO1VHbi.js → web-DbhP1mtF.js} +1 -1
- package/dist/index.html +1 -1
- package/dist/src/packages/server/index.js +4 -0
- package/dist/src/packages/server/integrations/slack/slack-instance.js +89 -0
- package/dist/src/packages/server/integrations/slack/slack-polling-client.js +150 -42
- package/dist/src/packages/server/integrations/slack/slack-skill.js +16 -0
- package/dist/src/packages/server/integrations/slack/slack-trigger-handler.js +19 -0
- package/dist/src/packages/server/integrations/whatsapp/whatsapp-skill.js +12 -0
- package/dist/src/packages/server/integrations/whatsapp/whatsapp-trigger-handler.js +226 -16
- package/dist/src/packages/server/routes/files.js +4 -2
- package/dist/src/packages/server/services/attachment-downloader.js +317 -0
- package/dist/src/packages/server/services/attachment-janitor.js +110 -0
- package/package.json +1 -1
- package/dist/assets/TriggerManagerPanel-DuWagsLi.js +0 -3
|
@@ -0,0 +1,317 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Attachment Downloader Service
|
|
3
|
+
*
|
|
4
|
+
* Downloads files referenced by inbound integration events (Slack file_share,
|
|
5
|
+
* WhatsApp media messages, etc.) to a local temp directory so agents can read
|
|
6
|
+
* them with the `Read` tool. Files are saved under
|
|
7
|
+
* `<TEMP_DIR>/triggers/<source>/<messageId>/<sanitized-filename>` where
|
|
8
|
+
* `TEMP_DIR` is the shared `/tmp/tide-commander-uploads` mount (already served
|
|
9
|
+
* statically at `/uploads/` by the Express app).
|
|
10
|
+
*
|
|
11
|
+
* Design notes:
|
|
12
|
+
* - Never throws. Returns `null` on fetch error / size-cap violation.
|
|
13
|
+
* - Hard cap: 25 MB per file. Larger files are aborted and the partial file
|
|
14
|
+
* is unlinked.
|
|
15
|
+
* - Idempotent: if the target path already exists with the expected size, the
|
|
16
|
+
* download is skipped and the existing record is returned.
|
|
17
|
+
* - Auth headers are caller-controlled. The caller is responsible for not
|
|
18
|
+
* sending tokens to cross-origin URLs.
|
|
19
|
+
*/
|
|
20
|
+
import * as fs from 'fs/promises';
|
|
21
|
+
import * as path from 'path';
|
|
22
|
+
import { randomUUID } from 'crypto';
|
|
23
|
+
import { createLogger } from '../utils/logger.js';
|
|
24
|
+
import { TEMP_DIR } from '../routes/files.js';
|
|
25
|
+
const log = createLogger('AttachmentDownloader');
|
|
26
|
+
/** Hard cap per file. Anything above this is aborted. */
|
|
27
|
+
export const MAX_ATTACHMENT_BYTES = 25 * 1024 * 1024; // 25 MB
|
|
28
|
+
/**
|
|
29
|
+
* Render the agent-facing one-liner for a downloaded attachment, omitting
|
|
30
|
+
* `key=` segments whose value is missing/blank. The path is always emitted —
|
|
31
|
+
* even if mimetype/filename came back undefined from the upstream the agent
|
|
32
|
+
* can still `Read` the file.
|
|
33
|
+
*
|
|
34
|
+
* formatAttachmentLine({ path: '/tmp/foo.pdf' })
|
|
35
|
+
* → "[attachment: /tmp/foo.pdf]"
|
|
36
|
+
*
|
|
37
|
+
* formatAttachmentLine({ path: '/tmp/foo.png', mimetype: 'image/png', filename: 'foo.png', size: 1024 })
|
|
38
|
+
* → "[attachment: /tmp/foo.png mimetype=image/png name=foo.png size=1024]"
|
|
39
|
+
*/
|
|
40
|
+
export function formatAttachmentLine(att) {
|
|
41
|
+
const parts = [`[attachment: ${att.path}`];
|
|
42
|
+
if (att.mimetype && att.mimetype !== 'application/octet-stream') {
|
|
43
|
+
parts.push(`mimetype=${att.mimetype}`);
|
|
44
|
+
}
|
|
45
|
+
if (att.filename && att.filename.trim()) {
|
|
46
|
+
parts.push(`name=${att.filename}`);
|
|
47
|
+
}
|
|
48
|
+
if (typeof att.size === 'number' && att.size > 0) {
|
|
49
|
+
parts.push(`size=${att.size}`);
|
|
50
|
+
}
|
|
51
|
+
return parts.join(' ') + ']';
|
|
52
|
+
}
|
|
53
|
+
/** Root for all trigger attachments. Lives under the shared uploads root so
|
|
54
|
+
* the static `/uploads/triggers/...` URL also works if a consumer wants it. */
|
|
55
|
+
export const TRIGGER_ATTACHMENT_ROOT = path.join(TEMP_DIR, 'triggers');
|
|
56
|
+
const MIME_TO_EXT = {
|
|
57
|
+
'image/png': '.png',
|
|
58
|
+
'image/jpeg': '.jpg',
|
|
59
|
+
'image/jpg': '.jpg',
|
|
60
|
+
'image/gif': '.gif',
|
|
61
|
+
'image/webp': '.webp',
|
|
62
|
+
'image/heic': '.heic',
|
|
63
|
+
'image/heif': '.heif',
|
|
64
|
+
'image/svg+xml': '.svg',
|
|
65
|
+
'video/mp4': '.mp4',
|
|
66
|
+
'video/quicktime': '.mov',
|
|
67
|
+
'video/3gpp': '.3gp',
|
|
68
|
+
'video/webm': '.webm',
|
|
69
|
+
'audio/ogg': '.ogg',
|
|
70
|
+
'audio/mpeg': '.mp3',
|
|
71
|
+
'audio/mp4': '.m4a',
|
|
72
|
+
'audio/aac': '.aac',
|
|
73
|
+
'audio/wav': '.wav',
|
|
74
|
+
'audio/x-wav': '.wav',
|
|
75
|
+
'application/pdf': '.pdf',
|
|
76
|
+
'application/zip': '.zip',
|
|
77
|
+
'application/json': '.json',
|
|
78
|
+
'application/xml': '.xml',
|
|
79
|
+
'application/msword': '.doc',
|
|
80
|
+
'application/vnd.openxmlformats-officedocument.wordprocessingml.document': '.docx',
|
|
81
|
+
'application/vnd.ms-excel': '.xls',
|
|
82
|
+
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet': '.xlsx',
|
|
83
|
+
'text/plain': '.txt',
|
|
84
|
+
'text/csv': '.csv',
|
|
85
|
+
'text/html': '.html',
|
|
86
|
+
'text/markdown': '.md',
|
|
87
|
+
};
|
|
88
|
+
/** Sanitize a filename: strip path separators, `..` segments, control chars.
|
|
89
|
+
* Returns a non-empty string with at most 200 chars; falls back to `file<ext>`
|
|
90
|
+
* when the input is hostile or empty. */
|
|
91
|
+
function sanitizeFilename(raw, fallbackExt) {
|
|
92
|
+
const trimmed = (raw ?? '').trim();
|
|
93
|
+
if (!trimmed)
|
|
94
|
+
return `file${fallbackExt}`;
|
|
95
|
+
// Strip path separators and control chars; collapse `..` segments.
|
|
96
|
+
const cleaned = trimmed
|
|
97
|
+
.replace(/[\\/\x00-\x1f\x7f]/g, '_')
|
|
98
|
+
.replace(/\.{2,}/g, '_')
|
|
99
|
+
.replace(/^\.+/, '_');
|
|
100
|
+
const limited = cleaned.length > 200 ? cleaned.slice(0, 200) : cleaned;
|
|
101
|
+
return limited || `file${fallbackExt}`;
|
|
102
|
+
}
|
|
103
|
+
/** Sanitize a path segment (the source/messageId pieces of the target dir). */
|
|
104
|
+
function sanitizeSegment(raw) {
|
|
105
|
+
const cleaned = raw.replace(/[\\/\x00-\x1f\x7f]/g, '_').replace(/\.{2,}/g, '_');
|
|
106
|
+
return cleaned.slice(0, 120) || randomUUID();
|
|
107
|
+
}
|
|
108
|
+
function extFromMime(mimetype) {
|
|
109
|
+
if (!mimetype)
|
|
110
|
+
return '';
|
|
111
|
+
return MIME_TO_EXT[mimetype.toLowerCase()] ?? '';
|
|
112
|
+
}
|
|
113
|
+
/** Ensure the filename ends with an extension. If the input has none, append
|
|
114
|
+
* the mime-derived ext (or no-op if the mime is unknown). */
|
|
115
|
+
function ensureExtension(filename, mimetype) {
|
|
116
|
+
if (filename.includes('.') && !filename.startsWith('.'))
|
|
117
|
+
return filename;
|
|
118
|
+
const ext = extFromMime(mimetype);
|
|
119
|
+
return ext ? `${filename}${ext}` : filename;
|
|
120
|
+
}
|
|
121
|
+
/** Probe existing file: if size matches expected, treat as a cache hit. */
|
|
122
|
+
async function existingSizeMatches(targetPath, expected) {
|
|
123
|
+
try {
|
|
124
|
+
const stat = await fs.stat(targetPath);
|
|
125
|
+
if (!stat.isFile())
|
|
126
|
+
return null;
|
|
127
|
+
if (typeof expected === 'number' && expected > 0 && stat.size === expected)
|
|
128
|
+
return stat.size;
|
|
129
|
+
// Size hint absent — accept any existing file as a cache hit, otherwise
|
|
130
|
+
// we'd re-fetch on every event.
|
|
131
|
+
if (typeof expected !== 'number')
|
|
132
|
+
return stat.size;
|
|
133
|
+
return null;
|
|
134
|
+
}
|
|
135
|
+
catch {
|
|
136
|
+
return null;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
/** Pull `filename` out of a `Content-Disposition` header. Supports both
|
|
140
|
+
* RFC 5987 (`filename*=UTF-8''…`) and the legacy quoted/bare forms. Returns
|
|
141
|
+
* undefined when there's nothing usable. */
|
|
142
|
+
function parseContentDispositionFilename(header) {
|
|
143
|
+
if (!header)
|
|
144
|
+
return undefined;
|
|
145
|
+
// RFC 5987 form first (preferred — supports non-ASCII).
|
|
146
|
+
const star = /filename\*=\s*([^']+)''([^;]+)/i.exec(header);
|
|
147
|
+
if (star) {
|
|
148
|
+
try {
|
|
149
|
+
return decodeURIComponent(star[2].trim());
|
|
150
|
+
}
|
|
151
|
+
catch {
|
|
152
|
+
return star[2].trim();
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
// Quoted: filename="foo bar.pdf"
|
|
156
|
+
const quoted = /filename\s*=\s*"([^"]+)"/i.exec(header);
|
|
157
|
+
if (quoted)
|
|
158
|
+
return quoted[1].trim();
|
|
159
|
+
// Bare: filename=foo.pdf
|
|
160
|
+
const bare = /filename\s*=\s*([^;]+)/i.exec(header);
|
|
161
|
+
if (bare)
|
|
162
|
+
return bare[1].trim();
|
|
163
|
+
return undefined;
|
|
164
|
+
}
|
|
165
|
+
/** Strip any `; charset=...` suffix from a Content-Type header. */
|
|
166
|
+
function parseContentTypeMime(header) {
|
|
167
|
+
if (!header)
|
|
168
|
+
return undefined;
|
|
169
|
+
const idx = header.indexOf(';');
|
|
170
|
+
const raw = (idx >= 0 ? header.slice(0, idx) : header).trim();
|
|
171
|
+
return raw || undefined;
|
|
172
|
+
}
|
|
173
|
+
/**
|
|
174
|
+
* Download one attachment. Returns the persisted record or `null` if the file
|
|
175
|
+
* was skipped (cap exceeded, fetch failure, missing URL).
|
|
176
|
+
*/
|
|
177
|
+
export async function downloadAttachment(opts) {
|
|
178
|
+
if (!opts.url) {
|
|
179
|
+
log.warn(`download skipped — empty URL (source=${opts.source} messageId=${opts.messageId})`);
|
|
180
|
+
return null;
|
|
181
|
+
}
|
|
182
|
+
if (typeof opts.sizeHintBytes === 'number' && opts.sizeHintBytes > MAX_ATTACHMENT_BYTES) {
|
|
183
|
+
log.log(`download skipped — sizeHint ${opts.sizeHintBytes}B exceeds cap (source=${opts.source} name=${opts.suggestedFilename ?? 'unknown'})`);
|
|
184
|
+
return null;
|
|
185
|
+
}
|
|
186
|
+
const sourceDir = sanitizeSegment(opts.source);
|
|
187
|
+
const messageDir = sanitizeSegment(opts.messageId || randomUUID());
|
|
188
|
+
const targetDir = path.join(TRIGGER_ATTACHMENT_ROOT, sourceDir, messageDir);
|
|
189
|
+
try {
|
|
190
|
+
await fs.mkdir(targetDir, { recursive: true });
|
|
191
|
+
}
|
|
192
|
+
catch (err) {
|
|
193
|
+
log.warn(`mkdir failed for ${targetDir}: ${err}`);
|
|
194
|
+
return null;
|
|
195
|
+
}
|
|
196
|
+
// Early idempotency check — only viable when the caller gave us enough
|
|
197
|
+
// info to predict the final filename without touching the network. The
|
|
198
|
+
// WhatsApp flat-event case has neither suggestedFilename nor mimetype, so
|
|
199
|
+
// we fall through to the post-fetch path below.
|
|
200
|
+
if (opts.suggestedFilename && opts.mimetype) {
|
|
201
|
+
const earlyExt = extFromMime(opts.mimetype);
|
|
202
|
+
const earlyName = ensureExtension(sanitizeFilename(opts.suggestedFilename, earlyExt || ''), opts.mimetype);
|
|
203
|
+
const earlyPath = path.join(targetDir, earlyName);
|
|
204
|
+
const cachedSize = await existingSizeMatches(earlyPath, opts.sizeHintBytes);
|
|
205
|
+
if (cachedSize !== null) {
|
|
206
|
+
log.debug(`download cache-hit ${earlyPath} (${cachedSize}B)`);
|
|
207
|
+
return {
|
|
208
|
+
path: earlyPath,
|
|
209
|
+
filename: earlyName,
|
|
210
|
+
mimetype: opts.mimetype,
|
|
211
|
+
size: cachedSize,
|
|
212
|
+
sourceUrl: opts.url,
|
|
213
|
+
};
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
let response;
|
|
217
|
+
try {
|
|
218
|
+
response = await fetch(opts.url, {
|
|
219
|
+
method: 'GET',
|
|
220
|
+
headers: opts.headers ?? {},
|
|
221
|
+
});
|
|
222
|
+
}
|
|
223
|
+
catch (err) {
|
|
224
|
+
log.warn(`fetch failed for ${opts.url}: ${err}`);
|
|
225
|
+
return null;
|
|
226
|
+
}
|
|
227
|
+
if (!response.ok) {
|
|
228
|
+
log.warn(`fetch non-OK ${response.status} for ${opts.url}`);
|
|
229
|
+
return null;
|
|
230
|
+
}
|
|
231
|
+
// Pre-flight check against Content-Length header.
|
|
232
|
+
const contentLengthHeader = response.headers.get('content-length');
|
|
233
|
+
if (contentLengthHeader) {
|
|
234
|
+
const declared = Number(contentLengthHeader);
|
|
235
|
+
if (Number.isFinite(declared) && declared > MAX_ATTACHMENT_BYTES) {
|
|
236
|
+
log.log(`download aborted — Content-Length ${declared}B exceeds cap`);
|
|
237
|
+
return null;
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
// Derive final filename + mimetype, preferring caller-supplied values and
|
|
241
|
+
// falling back to response headers (Content-Type, Content-Disposition).
|
|
242
|
+
const headerMime = parseContentTypeMime(response.headers.get('content-type'));
|
|
243
|
+
const headerFilename = parseContentDispositionFilename(response.headers.get('content-disposition'));
|
|
244
|
+
const effectiveMime = opts.mimetype ?? headerMime;
|
|
245
|
+
const effectiveExt = extFromMime(effectiveMime);
|
|
246
|
+
const effectiveNameRaw = opts.suggestedFilename ?? headerFilename;
|
|
247
|
+
const effectiveName = ensureExtension(sanitizeFilename(effectiveNameRaw, effectiveExt || ''), effectiveMime);
|
|
248
|
+
const finalName = effectiveName || `${randomUUID()}${effectiveExt}`;
|
|
249
|
+
const targetPath = path.join(targetDir, finalName);
|
|
250
|
+
// Late idempotency check (when we couldn't do it pre-fetch).
|
|
251
|
+
if (!opts.suggestedFilename || !opts.mimetype) {
|
|
252
|
+
const cachedSize = await existingSizeMatches(targetPath, opts.sizeHintBytes);
|
|
253
|
+
if (cachedSize !== null) {
|
|
254
|
+
try {
|
|
255
|
+
await response.body?.cancel();
|
|
256
|
+
}
|
|
257
|
+
catch { /* ignore */ }
|
|
258
|
+
log.debug(`download cache-hit (post-fetch) ${targetPath} (${cachedSize}B)`);
|
|
259
|
+
return {
|
|
260
|
+
path: targetPath,
|
|
261
|
+
filename: finalName,
|
|
262
|
+
mimetype: effectiveMime ?? 'application/octet-stream',
|
|
263
|
+
size: cachedSize,
|
|
264
|
+
sourceUrl: opts.url,
|
|
265
|
+
};
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
if (!response.body) {
|
|
269
|
+
log.warn(`response has no body for ${opts.url}`);
|
|
270
|
+
return null;
|
|
271
|
+
}
|
|
272
|
+
// Stream chunks while enforcing the cap. Accumulate into memory — 25 MB
|
|
273
|
+
// ceiling makes this safe and avoids a half-written file on cap overflow.
|
|
274
|
+
const reader = response.body.getReader();
|
|
275
|
+
const chunks = [];
|
|
276
|
+
let total = 0;
|
|
277
|
+
try {
|
|
278
|
+
while (true) {
|
|
279
|
+
const { done, value } = await reader.read();
|
|
280
|
+
if (done)
|
|
281
|
+
break;
|
|
282
|
+
if (!value || value.byteLength === 0)
|
|
283
|
+
continue;
|
|
284
|
+
total += value.byteLength;
|
|
285
|
+
if (total > MAX_ATTACHMENT_BYTES) {
|
|
286
|
+
log.log(`download aborted — streamed bytes exceeded cap (${total}B > ${MAX_ATTACHMENT_BYTES}B) url=${opts.url}`);
|
|
287
|
+
try {
|
|
288
|
+
await reader.cancel();
|
|
289
|
+
}
|
|
290
|
+
catch { /* ignore */ }
|
|
291
|
+
return null;
|
|
292
|
+
}
|
|
293
|
+
chunks.push(value);
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
catch (err) {
|
|
297
|
+
log.warn(`stream read failed for ${opts.url}: ${err}`);
|
|
298
|
+
return null;
|
|
299
|
+
}
|
|
300
|
+
// Concatenate into a single Buffer and persist.
|
|
301
|
+
const buf = Buffer.concat(chunks.map((c) => Buffer.from(c.buffer, c.byteOffset, c.byteLength)));
|
|
302
|
+
try {
|
|
303
|
+
await fs.writeFile(targetPath, buf);
|
|
304
|
+
}
|
|
305
|
+
catch (err) {
|
|
306
|
+
log.warn(`write failed for ${targetPath}: ${err}`);
|
|
307
|
+
return null;
|
|
308
|
+
}
|
|
309
|
+
log.log(`downloaded ${total}B → ${targetPath} (source=${opts.source} mime=${effectiveMime ?? 'unknown'})`);
|
|
310
|
+
return {
|
|
311
|
+
path: targetPath,
|
|
312
|
+
filename: finalName,
|
|
313
|
+
mimetype: effectiveMime ?? 'application/octet-stream',
|
|
314
|
+
size: total,
|
|
315
|
+
sourceUrl: opts.url,
|
|
316
|
+
};
|
|
317
|
+
}
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Attachment Janitor
|
|
3
|
+
*
|
|
4
|
+
* Sweeps the trigger-attachment root (`/tmp/tide-commander-uploads/triggers/`)
|
|
5
|
+
* and deletes files whose mtime is older than the TTL. Runs once on boot, then
|
|
6
|
+
* every hour. Empty `<source>/<messageId>/` directories are removed after
|
|
7
|
+
* their files are gone. NEVER touches anything outside the triggers/ subtree —
|
|
8
|
+
* user-uploaded files at the top of `/tmp/tide-commander-uploads/` are left
|
|
9
|
+
* alone.
|
|
10
|
+
*/
|
|
11
|
+
import * as fs from 'fs/promises';
|
|
12
|
+
import * as path from 'path';
|
|
13
|
+
import { createLogger } from '../utils/logger.js';
|
|
14
|
+
import { TRIGGER_ATTACHMENT_ROOT } from './attachment-downloader.js';
|
|
15
|
+
const log = createLogger('AttachmentJanitor');
|
|
16
|
+
/** Files older than this are unlinked. */
|
|
17
|
+
const TTL_MS = 24 * 60 * 60 * 1000; // 24 hours
|
|
18
|
+
/** Sweep cadence after the initial boot pass. */
|
|
19
|
+
const SWEEP_INTERVAL_MS = 60 * 60 * 1000; // 1 hour
|
|
20
|
+
let sweepTimer = null;
|
|
21
|
+
async function sweepFile(filePath, stats) {
|
|
22
|
+
let st;
|
|
23
|
+
try {
|
|
24
|
+
st = await fs.stat(filePath);
|
|
25
|
+
}
|
|
26
|
+
catch {
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
if (!st.isFile())
|
|
30
|
+
return;
|
|
31
|
+
if (Date.now() - st.mtimeMs < TTL_MS)
|
|
32
|
+
return;
|
|
33
|
+
try {
|
|
34
|
+
await fs.unlink(filePath);
|
|
35
|
+
stats.files += 1;
|
|
36
|
+
stats.freedBytes += st.size;
|
|
37
|
+
}
|
|
38
|
+
catch (err) {
|
|
39
|
+
log.warn(`failed to unlink ${filePath}: ${err}`);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
async function sweepDir(dirPath, stats, depth) {
|
|
43
|
+
let entries;
|
|
44
|
+
try {
|
|
45
|
+
entries = await fs.readdir(dirPath, { withFileTypes: true });
|
|
46
|
+
}
|
|
47
|
+
catch (err) {
|
|
48
|
+
// Missing dir is fine — first boot before any download.
|
|
49
|
+
const code = err?.code;
|
|
50
|
+
if (code !== 'ENOENT')
|
|
51
|
+
log.warn(`readdir failed for ${dirPath}: ${err}`);
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
for (const entry of entries) {
|
|
55
|
+
const child = path.join(dirPath, entry.name);
|
|
56
|
+
if (entry.isDirectory()) {
|
|
57
|
+
await sweepDir(child, stats, depth + 1);
|
|
58
|
+
}
|
|
59
|
+
else if (entry.isFile()) {
|
|
60
|
+
await sweepFile(child, stats);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
// Try to clean up empty intermediate dirs (source/, messageId/).
|
|
64
|
+
// NEVER remove the root itself (depth === 0). The downloader re-creates
|
|
65
|
+
// <source>/<messageId>/ on each call so removing them is safe.
|
|
66
|
+
if (depth === 0)
|
|
67
|
+
return;
|
|
68
|
+
try {
|
|
69
|
+
const remaining = await fs.readdir(dirPath);
|
|
70
|
+
if (remaining.length === 0) {
|
|
71
|
+
await fs.rmdir(dirPath);
|
|
72
|
+
stats.dirs += 1;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
catch (err) {
|
|
76
|
+
const code = err?.code;
|
|
77
|
+
if (code !== 'ENOTEMPTY' && code !== 'ENOENT') {
|
|
78
|
+
log.warn(`rmdir failed for ${dirPath}: ${err}`);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
/** Run one sweep pass over the trigger-attachment root. */
|
|
83
|
+
export async function sweepOnce() {
|
|
84
|
+
const stats = { files: 0, dirs: 0, freedBytes: 0 };
|
|
85
|
+
await sweepDir(TRIGGER_ATTACHMENT_ROOT, stats, 0);
|
|
86
|
+
const freedMb = stats.freedBytes / (1024 * 1024);
|
|
87
|
+
log.log(`[attachment-janitor] swept ${stats.files} files, ${stats.dirs} dirs, freed ${freedMb.toFixed(2)} MB`);
|
|
88
|
+
return stats;
|
|
89
|
+
}
|
|
90
|
+
/** Initialize the janitor: run once now, then every hour. */
|
|
91
|
+
export function initAttachmentJanitor() {
|
|
92
|
+
if (sweepTimer)
|
|
93
|
+
return; // idempotent
|
|
94
|
+
// Fire-and-forget the initial sweep — never block startup on it.
|
|
95
|
+
void sweepOnce().catch((err) => log.warn(`initial sweep failed: ${err}`));
|
|
96
|
+
sweepTimer = setInterval(() => {
|
|
97
|
+
void sweepOnce().catch((err) => log.warn(`scheduled sweep failed: ${err}`));
|
|
98
|
+
}, SWEEP_INTERVAL_MS);
|
|
99
|
+
// Don't keep the event loop alive for the janitor — let the process exit
|
|
100
|
+
// cleanly when the rest of the server shuts down.
|
|
101
|
+
if (typeof sweepTimer.unref === 'function')
|
|
102
|
+
sweepTimer.unref();
|
|
103
|
+
}
|
|
104
|
+
/** Stop the janitor (test/teardown helper). */
|
|
105
|
+
export function shutdownAttachmentJanitor() {
|
|
106
|
+
if (sweepTimer) {
|
|
107
|
+
clearInterval(sweepTimer);
|
|
108
|
+
sweepTimer = null;
|
|
109
|
+
}
|
|
110
|
+
}
|
package/package.json
CHANGED
|
@@ -1,3 +0,0 @@
|
|
|
1
|
-
import{ah as ue,r as i,aJ as f,l as h,j as e}from"./main-5eyR3isL.js";import"./vendor-react--Eh9ivFN.js";import"./vendor-three-Chj50gSY.js";const he=[{value:"webhook",label:"Webhook"},{value:"cron",label:"Cron"},{value:"slack",label:"Slack"},{value:"email",label:"Email"},{value:"jira",label:"Jira"},{value:"whatsapp",label:"WhatsApp"}],ye=[{value:"structural",label:"Structural",desc:"Field-based matching (fast, free)"},{value:"llm",label:"LLM",desc:"Semantic matching via LLM (flexible, costs tokens)"},{value:"hybrid",label:"Hybrid",desc:"Structural pre-filter + LLM (recommended)"}],ge=[{value:"structural",label:"Structural"},{value:"llm",label:"LLM"}];function me(v){if(!v)return"";const d=new Date(v);if(Number.isNaN(d.getTime()))return"";const y=k=>String(k).padStart(2,"0");return`${d.getFullYear()}-${y(d.getMonth()+1)}-${y(d.getDate())}T${y(d.getHours())}:${y(d.getMinutes())}`}const z=()=>({name:"",description:"",type:"webhook",agentId:"",promptTemplate:"",enabled:!0,matchMode:"structural",extractionMode:"structural",config:{method:"POST"}});function ve({isOpen:v,onClose:d}){var _,$,J,V,G,X,Y,q,K,Q,Z,ee;const y=ue(),[k,te]=i.useState([]),[C,M]=i.useState("list"),[n,b]=i.useState(z()),[c,E]=i.useState(!1),[A,L]=i.useState(!1),[B,p]=i.useState(null),[D,le]=i.useState([]),[T,se]=i.useState(null),[F,ae]=i.useState(`{
|
|
2
|
-
|
|
3
|
-
}`),[r,S]=i.useState(null),[I,N]=i.useState(!1),[P,H]=i.useState([]),g=i.useCallback(async()=>{try{const t=await f(h("/api/triggers"));if(t.ok){const s=await t.json();te(s)}}catch(t){console.error("Failed to load triggers:",t)}},[]),w=i.useCallback(async t=>{try{const s=await f(h(`/api/triggers/${t}/events?limit=20`));if(s.ok){const a=await s.json();le(a)}}catch(s){console.error("Failed to load trigger history:",s)}},[]);i.useEffect(()=>{v&&g()},[v,g]),i.useEffect(()=>{T&&w(T)},[T,w]);const ne=i.useCallback(async()=>{L(!0),p(null);try{const t=c?h(`/api/triggers/${n.id}`):h("/api/triggers"),m=await f(t,{method:c?"PATCH":"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(n)});if(!m.ok){const j=await m.json();throw new Error(j.error||"Failed to save trigger")}await g(),M("list"),b(z()),E(!1)}catch(t){p(t instanceof Error?t.message:"Save failed")}finally{L(!1)}},[n,c,g]),ie=i.useCallback(async t=>{try{await f(h(`/api/triggers/${t}`),{method:"DELETE"}),await g()}catch(s){console.error("Failed to delete trigger:",s)}},[g]),re=i.useCallback(async t=>{try{await f(h(`/api/triggers/${t.id}`),{method:"PATCH",headers:{"Content-Type":"application/json"},body:JSON.stringify({enabled:!t.enabled})}),await g()}catch(s){console.error("Failed to toggle trigger:",s)}},[g]),oe=i.useCallback(async t=>{try{await f(h(`/api/triggers/${t}/fire`),{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({variables:{}})}),T===t&&await w(t)}catch(s){console.error("Failed to test fire trigger:",s)}},[T,w]),de=i.useCallback(async()=>{if(n.id){N(!0),S(null);try{let t;try{t=JSON.parse(F)}catch{S({structuralMatch:void 0,extractedVariables:{},wouldFire:!1}),p("Invalid JSON payload");return}const s=await f(h(`/api/triggers/${n.id}/test-match`),{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({payload:t})});if(s.ok){const a=await s.json();S(a),p(null)}else{const a=await s.json();p(a.error||"Test match failed")}}catch(t){p(t instanceof Error?t.message:"Test match failed")}finally{N(!1)}}},[n.id,F]),ce=i.useCallback(async t=>{var s;try{const a=await f(h("/api/triggers/validate-cron"),{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({expression:t,timezone:((s=n.config)==null?void 0:s.timezone)||"UTC"})});if(a.ok){const m=await a.json();H(m.valid?m.nextFires:[])}}catch{H([])}},[n]),U=i.useCallback(t=>{t?(b({...t}),E(!0)):(b(z()),E(!1)),S(null),p(null),M("edit")},[]),o=i.useCallback((t,s)=>{b(a=>({...a,[t]:s}))},[]),x=i.useCallback((t,s)=>{b(a=>({...a,config:{...a.config,[t]:s}}))},[]),O=i.useCallback((t,s)=>{b(a=>({...a,llmMatch:{...a.llmMatch||{prompt:""},[t]:s}}))},[]),W=i.useCallback((t,s)=>{b(a=>({...a,llmExtract:{...a.llmExtract||{prompt:"",variables:[]},[t]:s}}))},[]),pe=i.useMemo(()=>Array.from(y.values()).map(t=>({id:t.id,name:t.name})),[y]);return v?e.jsx("div",{style:l.overlay,onClick:d,children:e.jsxs("div",{style:l.panel,onClick:t=>t.stopPropagation(),children:[e.jsxs("div",{style:l.header,children:[e.jsx("h2",{style:l.title,children:C==="list"?"Triggers":c?"Edit Trigger":"New Trigger"}),e.jsxs("div",{style:l.headerActions,children:[C==="list"&&e.jsx("button",{style:l.addBtn,onClick:()=>U(),children:"+ New Trigger"}),C==="edit"&&e.jsx("button",{style:l.backBtn,onClick:()=>{M("list"),S(null),p(null)},children:"Back"}),e.jsx("button",{style:l.closeBtn,onClick:d,children:"X"})]})]}),B&&e.jsx("div",{style:l.error,children:B}),C==="list"&&e.jsx("div",{style:l.content,children:k.length===0?e.jsx("div",{style:l.empty,children:"No triggers configured. Create one to get started."}):k.map(t=>{var s;return e.jsxs("div",{style:{...l.triggerCard,borderLeftColor:t.status==="error"?"#e74c3c":t.enabled?"#2ecc71":"#95a5a6"},onClick:()=>{se(t.id),U(t)},children:[e.jsxs("div",{style:l.cardHeader,children:[e.jsxs("div",{style:l.cardTitle,children:[e.jsx("span",{style:l.typeTag,children:t.type}),e.jsx("span",{children:t.name}),t.type==="cron"&&(()=>{const a=t.config||{};return a.runOnce?a.completedAt?e.jsx("span",{style:{...l.matchTag,color:"#a6e3a1"},children:"Once · completed"}):a.missedAt?e.jsx("span",{style:{...l.matchTag,color:"#f38ba8"},children:"Once · missed"}):e.jsx("span",{style:{...l.matchTag,color:"#f9e2af"},children:"Once · pending"}):e.jsx("span",{style:l.matchTag,children:"Recurring"})})(),t.matchMode!=="structural"&&e.jsx("span",{style:l.matchTag,children:t.matchMode})]}),e.jsxs("div",{style:l.cardActions,children:[e.jsx("button",{style:l.smallBtn,onClick:a=>{a.stopPropagation(),re(t)},children:t.enabled?"Disable":"Enable"}),e.jsx("button",{style:l.smallBtn,onClick:a=>{a.stopPropagation(),oe(t.id)},children:"Test Fire"}),e.jsx("button",{style:{...l.smallBtn,color:"#e74c3c"},onClick:a=>{a.stopPropagation(),ie(t.id)},children:"Delete"})]})]}),e.jsxs("div",{style:l.cardMeta,children:[e.jsxs("span",{children:["Agent: ",((s=y.get(t.agentId))==null?void 0:s.name)||t.agentId]}),e.jsxs("span",{children:["Fires: ",t.fireCount]}),t.lastFiredAt&&e.jsxs("span",{children:["Last: ",new Date(t.lastFiredAt).toLocaleString()]}),t.status==="error"&&t.lastError&&e.jsxs("span",{style:{color:"#e74c3c"},children:["Error: ",t.lastError]})]})]},t.id)})}),C==="edit"&&e.jsx("div",{style:l.content,children:e.jsxs("div",{style:l.form,children:[e.jsxs("div",{style:l.field,children:[e.jsx("label",{style:l.label,children:"Name"}),e.jsx("input",{style:l.input,value:n.name||"",onChange:t=>o("name",t.target.value),placeholder:"My Trigger"})]}),e.jsxs("div",{style:l.field,children:[e.jsx("label",{style:l.label,children:"Description"}),e.jsx("input",{style:l.input,value:n.description||"",onChange:t=>o("description",t.target.value),placeholder:"Optional description"})]}),e.jsxs("div",{style:l.row,children:[e.jsxs("div",{style:l.field,children:[e.jsx("label",{style:l.label,children:"Type"}),e.jsx("select",{style:l.select,value:n.type||"webhook",onChange:t=>{const s=t.target.value;o("type",s),s==="webhook"?o("config",{method:"POST"}):s==="cron"?o("config",{expression:"0 9 * * MON-FRI",timezone:"UTC"}):o("config",{})},children:he.map(t=>e.jsx("option",{value:t.value,children:t.label},t.value))})]}),e.jsxs("div",{style:l.field,children:[e.jsx("label",{style:l.label,children:"Agent"}),e.jsxs("select",{style:l.select,value:n.agentId||"",onChange:t=>o("agentId",t.target.value),children:[e.jsx("option",{value:"",children:"Select agent..."}),pe.map(t=>e.jsx("option",{value:t.id,children:t.name},t.id))]})]})]}),e.jsxs("div",{style:l.field,children:[e.jsx("label",{style:l.label,children:"Prompt Template"}),e.jsx("textarea",{style:l.textarea,value:n.promptTemplate||"",onChange:t=>o("promptTemplate",t.target.value),placeholder:"Message sent to agent. Use {{variable}} for interpolation.",rows:4})]}),n.type==="webhook"&&e.jsxs("div",{style:l.section,children:[e.jsx("h4",{style:l.sectionTitle,children:"Webhook Config"}),e.jsxs("div",{style:l.row,children:[e.jsxs("div",{style:l.field,children:[e.jsx("label",{style:l.label,children:"Method"}),e.jsxs("select",{style:l.select,value:((_=n.config)==null?void 0:_.method)||"POST",onChange:t=>x("method",t.target.value),children:[e.jsx("option",{value:"POST",children:"POST"}),e.jsx("option",{value:"PUT",children:"PUT"})]})]}),e.jsxs("div",{style:l.field,children:[e.jsx("label",{style:l.label,children:"Secret (HMAC)"}),e.jsx("input",{style:l.input,type:"password",value:(($=n.config)==null?void 0:$.secret)||"",onChange:t=>x("secret",t.target.value||void 0),placeholder:"Optional HMAC secret"})]})]}),e.jsxs("div",{style:l.field,children:[e.jsx("label",{style:l.label,children:"Extract Fields (comma-separated JSON paths)"}),e.jsx("input",{style:l.input,value:((V=(J=n.config)==null?void 0:J.extractFields)==null?void 0:V.join(", "))||"",onChange:t=>x("extractFields",t.target.value.split(",").map(s=>s.trim()).filter(Boolean)),placeholder:"body.release_name, body.version"})]}),c&&n.id&&e.jsxs("div",{style:l.webhookUrl,children:[e.jsx("label",{style:l.label,children:"Webhook URL"}),e.jsx("code",{style:l.code,children:`${window.location.origin}/api/triggers/webhook/${n.id}`})]})]}),n.type==="cron"&&(()=>{const t=n.config||{},s=!!t.runOnce,a=t.completedAt,m=t.missedAt,j=!!a;return e.jsxs("div",{style:l.section,children:[e.jsx("h4",{style:l.sectionTitle,children:"Cron Config"}),e.jsxs("div",{style:l.modeSelector,children:[e.jsxs("button",{type:"button",style:{...l.modeBtn,...s?{}:l.modeBtnActive},onClick:()=>x("runOnce",!1),disabled:j,children:[e.jsx("strong",{children:"Repeats"}),e.jsx("span",{style:l.modeDesc,children:"Fires on a cron schedule"})]}),e.jsxs("button",{type:"button",style:{...l.modeBtn,...s?l.modeBtnActive:{}},onClick:()=>x("runOnce",!0),disabled:j,children:[e.jsx("strong",{children:"Run once"}),e.jsx("span",{style:l.modeDesc,children:"Fires exactly once at a specific time"})]})]}),j&&e.jsxs("div",{style:{...l.testResult,borderLeft:"3px solid #a6e3a1"},children:[e.jsx("strong",{style:{color:"#a6e3a1"},children:"Completed"}),e.jsxs("span",{style:l.llmMeta,children:["Fired at ",new Date(a).toLocaleString()]})]}),!j&&m&&e.jsxs("div",{style:{...l.testResult,borderLeft:"3px solid #f38ba8"},children:[e.jsx("strong",{style:{color:"#f38ba8"},children:"Missed"}),e.jsxs("span",{style:l.llmMeta,children:["Marked missed at ",new Date(m).toLocaleString()," (server was down at runAt)"]})]}),s?e.jsx("div",{style:l.row,children:e.jsxs("div",{style:l.field,children:[e.jsx("label",{style:l.label,children:"Run at"}),e.jsx("input",{style:l.input,type:"datetime-local",value:me(t.runAt),onChange:u=>{const R=u.target.value;if(!R){x("runAt",void 0);return}const xe=new Date(R).toISOString();x("runAt",xe)},disabled:j}),e.jsx("span",{style:l.llmMeta,children:"Interpreted in your browser's local timezone"})]})}):e.jsxs("div",{style:l.row,children:[e.jsxs("div",{style:l.field,children:[e.jsx("label",{style:l.label,children:"Expression"}),e.jsx("input",{style:l.input,value:t.expression||"",onChange:u=>{x("expression",u.target.value),u.target.value&&ce(u.target.value)},placeholder:"0 9 * * MON-FRI"})]}),e.jsxs("div",{style:l.field,children:[e.jsx("label",{style:l.label,children:"Timezone"}),e.jsx("input",{style:l.input,value:t.timezone||"UTC",onChange:u=>x("timezone",u.target.value),placeholder:"America/Mexico_City"})]})]}),!s&&P.length>0&&e.jsxs("div",{style:l.nextFires,children:[e.jsx("label",{style:l.label,children:"Next fires:"}),P.map((u,R)=>e.jsx("div",{style:l.nextFireItem,children:new Date(u).toLocaleString()},R))]})]})})(),e.jsxs("div",{style:l.section,children:[e.jsx("h4",{style:l.sectionTitle,children:"Match Mode"}),e.jsx("div",{style:l.modeSelector,children:ye.map(t=>e.jsxs("button",{style:{...l.modeBtn,...n.matchMode===t.value?l.modeBtnActive:{}},onClick:()=>o("matchMode",t.value),children:[e.jsx("strong",{children:t.label}),e.jsx("span",{style:l.modeDesc,children:t.desc})]},t.value))}),(n.matchMode==="llm"||n.matchMode==="hybrid")&&e.jsxs("div",{style:l.llmConfig,children:[e.jsxs("div",{style:l.field,children:[e.jsx("label",{style:l.label,children:"LLM Match Prompt"}),e.jsx("textarea",{style:l.textarea,value:((G=n.llmMatch)==null?void 0:G.prompt)||"",onChange:t=>O("prompt",t.target.value),placeholder:"Does this message request a release deployment?",rows:3})]}),e.jsxs("div",{style:l.row,children:[e.jsxs("div",{style:l.field,children:[e.jsx("label",{style:l.label,children:"Model"}),e.jsxs("select",{style:l.select,value:((X=n.llmMatch)==null?void 0:X.model)||"haiku",onChange:t=>O("model",t.target.value),children:[e.jsx("option",{value:"haiku",children:"Haiku (fast, cheap)"}),e.jsx("option",{value:"sonnet",children:"Sonnet (balanced)"}),e.jsx("option",{value:"opus",children:"Opus (powerful)"})]})]}),e.jsxs("div",{style:l.field,children:[e.jsx("label",{style:l.label,children:"Min Confidence"}),e.jsx("input",{style:l.input,type:"number",min:"0",max:"1",step:"0.1",value:((Y=n.llmMatch)==null?void 0:Y.minConfidence)??0,onChange:t=>O("minConfidence",parseFloat(t.target.value))})]})]})]})]}),e.jsxs("div",{style:l.section,children:[e.jsx("h4",{style:l.sectionTitle,children:"Extraction Mode"}),e.jsx("div",{style:l.row,children:ge.map(t=>e.jsx("button",{style:{...l.modeBtn,...n.extractionMode===t.value?l.modeBtnActive:{}},onClick:()=>o("extractionMode",t.value),children:t.label},t.value))}),n.extractionMode==="llm"&&e.jsxs("div",{style:l.llmConfig,children:[e.jsxs("div",{style:l.field,children:[e.jsx("label",{style:l.label,children:"Extraction Instructions"}),e.jsx("textarea",{style:l.textarea,value:((q=n.llmExtract)==null?void 0:q.prompt)||"",onChange:t=>W("prompt",t.target.value),placeholder:"Extract: release version, affected systems, urgency",rows:3})]}),e.jsxs("div",{style:l.field,children:[e.jsx("label",{style:l.label,children:"Variable Names (comma-separated)"}),e.jsx("input",{style:l.input,value:((Q=(K=n.llmExtract)==null?void 0:K.variables)==null?void 0:Q.join(", "))||"",onChange:t=>W("variables",t.target.value.split(",").map(s=>s.trim()).filter(Boolean)),placeholder:"release_name, affected_systems, urgency"})]})]})]}),c&&e.jsxs("div",{style:l.section,children:[e.jsx("h4",{style:l.sectionTitle,children:"Test Match"}),e.jsxs("div",{style:l.field,children:[e.jsx("label",{style:l.label,children:"Sample Event Payload (JSON)"}),e.jsx("textarea",{style:l.textarea,value:F,onChange:t=>ae(t.target.value),rows:4})]}),e.jsx("button",{style:l.testBtn,onClick:de,disabled:I,children:I?"Testing...":"Test Match"}),r&&e.jsxs("div",{style:l.testResult,children:[e.jsx("div",{style:{...l.testResultHeader,color:r.wouldFire?"#2ecc71":"#e74c3c"},children:r.wouldFire?"WOULD FIRE":"WOULD NOT FIRE"}),r.structuralMatch!==void 0&&e.jsxs("div",{children:["Structural: ",r.structuralMatch?"Match":"No match"]}),r.llmMatch&&e.jsxs("div",{style:l.llmResult,children:[e.jsxs("div",{children:["LLM: ",r.llmMatch.match?"Match":"No match"]}),e.jsxs("div",{children:["Confidence: ",(r.llmMatch.confidence*100).toFixed(0),"%"]}),e.jsxs("div",{children:["Reason: ",r.llmMatch.reason]}),e.jsxs("div",{style:l.llmMeta,children:["Model: ",r.llmMatch.model," | Tokens: ",r.llmMatch.tokensUsed," | Latency: ",r.llmMatch.durationMs,"ms"]})]}),Object.keys(r.extractedVariables).length>0&&e.jsxs("div",{children:[e.jsx("div",{style:l.label,children:"Extracted Variables:"}),Object.entries(r.extractedVariables).map(([t,s])=>e.jsxs("div",{style:l.varRow,children:[e.jsx("code",{children:t}),": ",s]},t))]})]})]}),c&&D.length>0&&e.jsxs("div",{style:l.section,children:[e.jsx("h4",{style:l.sectionTitle,children:"Recent Fires"}),e.jsx("div",{style:l.historyList,children:D.map((t,s)=>{const a=t.llm_match_result?JSON.parse(t.llm_match_result):null;return e.jsxs("div",{style:l.historyItem,children:[e.jsxs("div",{style:l.historyHeader,children:[e.jsx("span",{style:{color:t.status==="delivered"?"#2ecc71":t.status==="failed"?"#e74c3c":"#f39c12"},children:t.status}),e.jsx("span",{children:new Date(t.fired_at).toLocaleString()}),t.duration_ms&&e.jsxs("span",{children:[t.duration_ms,"ms"]})]}),t.error&&e.jsx("div",{style:{color:"#e74c3c",fontSize:"12px"},children:t.error}),a&&e.jsxs("div",{style:l.llmMeta,children:["LLM: ",a.match?"match":"no match"," (",(a.confidence*100).toFixed(0),"%) - ",a.reason]})]},t.id||s)})})]}),e.jsxs("div",{style:l.actions,children:[e.jsx("button",{style:l.cancelBtn,onClick:()=>{M("list"),p(null)},children:"Cancel"}),e.jsx("button",{style:l.saveBtn,onClick:ne,disabled:A||!n.name||!n.agentId||n.type==="cron"&&!!((Z=n.config)!=null&&Z.runOnce)&&!((ee=n.config)!=null&&ee.runAt),children:A?"Saving...":c?"Update":"Create"})]})]})})]})}):null}const l={overlay:{position:"fixed",top:0,left:0,right:0,bottom:0,background:"rgba(0,0,0,0.6)",display:"flex",alignItems:"center",justifyContent:"center",zIndex:1e3},panel:{background:"#1e1e2e",borderRadius:"12px",width:"680px",maxHeight:"85vh",display:"flex",flexDirection:"column",color:"#cdd6f4",boxShadow:"0 8px 32px rgba(0,0,0,0.4)"},header:{display:"flex",justifyContent:"space-between",alignItems:"center",padding:"16px 20px",borderBottom:"1px solid #313244"},title:{margin:0,fontSize:"18px",fontWeight:600},headerActions:{display:"flex",gap:"8px"},content:{overflow:"auto",padding:"16px 20px",flex:1},empty:{textAlign:"center",padding:"40px",color:"#6c7086"},error:{background:"#45252a",color:"#f38ba8",padding:"8px 20px",fontSize:"13px"},triggerCard:{background:"#181825",borderRadius:"8px",padding:"12px 16px",marginBottom:"8px",borderLeft:"3px solid",cursor:"pointer",transition:"background 0.15s"},cardHeader:{display:"flex",justifyContent:"space-between",alignItems:"center"},cardTitle:{display:"flex",alignItems:"center",gap:"8px",fontWeight:500},typeTag:{background:"#313244",padding:"2px 6px",borderRadius:"4px",fontSize:"11px",textTransform:"uppercase"},matchTag:{background:"#45475a",padding:"2px 6px",borderRadius:"4px",fontSize:"10px",color:"#a6e3a1"},cardActions:{display:"flex",gap:"6px"},cardMeta:{display:"flex",gap:"16px",marginTop:"8px",fontSize:"12px",color:"#6c7086"},form:{display:"flex",flexDirection:"column",gap:"12px"},field:{display:"flex",flexDirection:"column",gap:"4px",flex:1},row:{display:"flex",gap:"12px"},label:{fontSize:"12px",color:"#a6adc8",fontWeight:500},input:{background:"#313244",border:"1px solid #45475a",borderRadius:"6px",padding:"8px 10px",color:"#cdd6f4",fontSize:"13px",outline:"none"},select:{background:"#313244",border:"1px solid #45475a",borderRadius:"6px",padding:"8px 10px",color:"#cdd6f4",fontSize:"13px",outline:"none"},textarea:{background:"#313244",border:"1px solid #45475a",borderRadius:"6px",padding:"8px 10px",color:"#cdd6f4",fontSize:"13px",outline:"none",fontFamily:"monospace",resize:"vertical"},section:{borderTop:"1px solid #313244",paddingTop:"12px"},sectionTitle:{margin:"0 0 8px",fontSize:"14px",fontWeight:600,color:"#a6adc8"},modeSelector:{display:"flex",gap:"8px",marginBottom:"8px"},modeBtn:{flex:1,background:"#313244",border:"1px solid #45475a",borderRadius:"6px",padding:"8px",color:"#cdd6f4",cursor:"pointer",textAlign:"left",display:"flex",flexDirection:"column",gap:"2px",fontSize:"12px"},modeBtnActive:{borderColor:"#89b4fa",background:"#1e3a5f"},modeDesc:{fontSize:"10px",color:"#6c7086"},llmConfig:{background:"#181825",borderRadius:"6px",padding:"12px",display:"flex",flexDirection:"column",gap:"8px"},webhookUrl:{marginTop:"8px"},code:{background:"#313244",padding:"6px 10px",borderRadius:"4px",fontSize:"12px",display:"block",wordBreak:"break-all",color:"#89b4fa"},nextFires:{marginTop:"8px"},nextFireItem:{fontSize:"12px",color:"#a6e3a1",padding:"2px 0"},testBtn:{background:"#89b4fa",border:"none",borderRadius:"6px",padding:"8px 16px",color:"#1e1e2e",fontWeight:600,cursor:"pointer",fontSize:"13px"},testResult:{background:"#181825",borderRadius:"6px",padding:"12px",marginTop:"8px",fontSize:"13px",display:"flex",flexDirection:"column",gap:"6px"},testResultHeader:{fontWeight:700,fontSize:"14px"},llmResult:{display:"flex",flexDirection:"column",gap:"2px"},llmMeta:{fontSize:"11px",color:"#6c7086",marginTop:"2px"},varRow:{fontSize:"12px",padding:"2px 0"},historyList:{display:"flex",flexDirection:"column",gap:"6px",maxHeight:"200px",overflow:"auto"},historyItem:{background:"#181825",borderRadius:"4px",padding:"8px 10px",fontSize:"12px"},historyHeader:{display:"flex",justifyContent:"space-between",alignItems:"center"},actions:{display:"flex",justifyContent:"flex-end",gap:"8px",marginTop:"16px",paddingTop:"12px",borderTop:"1px solid #313244"},addBtn:{background:"#89b4fa",border:"none",borderRadius:"6px",padding:"6px 14px",color:"#1e1e2e",fontWeight:600,cursor:"pointer",fontSize:"13px"},backBtn:{background:"transparent",border:"1px solid #45475a",borderRadius:"6px",padding:"6px 14px",color:"#cdd6f4",cursor:"pointer",fontSize:"13px"},closeBtn:{background:"transparent",border:"none",color:"#6c7086",cursor:"pointer",fontSize:"16px",padding:"4px 8px"},smallBtn:{background:"transparent",border:"1px solid #45475a",borderRadius:"4px",padding:"3px 8px",color:"#cdd6f4",cursor:"pointer",fontSize:"11px"},saveBtn:{background:"#a6e3a1",border:"none",borderRadius:"6px",padding:"8px 20px",color:"#1e1e2e",fontWeight:600,cursor:"pointer",fontSize:"13px"},cancelBtn:{background:"transparent",border:"1px solid #45475a",borderRadius:"6px",padding:"8px 20px",color:"#cdd6f4",cursor:"pointer",fontSize:"13px"}};export{ve as TriggerManagerPanel,ve as default};
|