webspresso 0.0.98 → 0.0.100

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.
Files changed (88) hide show
  1. package/.agents/ADMIN.md +11 -1
  2. package/.agents/AGENTS.md +2 -2
  3. package/.agents/AUTH.md +415 -0
  4. package/.agents/CLI.md +47 -0
  5. package/.agents/ERRORS.md +59 -0
  6. package/.agents/ORM.md +178 -4
  7. package/.agents/PLUGINS.md +113 -8
  8. package/.agents/QUEUE.md +92 -0
  9. package/.agents/ROUTING.md +160 -7
  10. package/.agents/STREAMING.md +35 -0
  11. package/AGENTS.md +2 -0
  12. package/README.md +145 -93
  13. package/bin/commands/add-docker.js +467 -0
  14. package/bin/commands/deploy-check.js +339 -0
  15. package/bin/webspresso.js +4 -0
  16. package/core/auth/index.js +123 -1
  17. package/core/auth/magic-link.js +170 -0
  18. package/core/auth/manager.js +477 -3
  19. package/core/auth/middleware.js +118 -5
  20. package/core/auth/oauth.js +517 -0
  21. package/core/auth/qrcode.js +499 -0
  22. package/core/auth/session-manager.js +255 -0
  23. package/core/auth/sessions.js +351 -0
  24. package/core/auth/tokens.js +1 -0
  25. package/core/auth/totp.js +507 -0
  26. package/core/auth/user-agent.js +161 -0
  27. package/core/orm/eager-loader.js +144 -10
  28. package/core/orm/index.js +10 -1
  29. package/core/orm/model.js +55 -1
  30. package/core/orm/query-builder.js +90 -4
  31. package/core/orm/repository.js +327 -16
  32. package/core/orm/scopes.js +3 -2
  33. package/core/orm/search.js +213 -0
  34. package/core/orm/types.js +6 -1
  35. package/core/queue/cron.js +544 -0
  36. package/core/queue/index.js +8 -1
  37. package/core/queue/manager.js +100 -1
  38. package/core/queue/scheduler.js +415 -0
  39. package/core/sse/index.js +19 -0
  40. package/core/sse/stream.js +430 -0
  41. package/core/ssr/stream.js +62 -2
  42. package/index.d.ts +794 -6
  43. package/index.js +48 -0
  44. package/package.json +7 -5
  45. package/plugins/admin-panel/client/parts/05-rich-text-file-helpers.js +323 -31
  46. package/plugins/admin-panel/client/parts/08-record-list.js +5 -0
  47. package/plugins/admin-panel/client/verify-spa-parts.js +2 -0
  48. package/plugins/admin-panel/field-renderers/file-upload.js +187 -104
  49. package/plugins/admin-panel/field-renderers/index.js +2 -0
  50. package/plugins/admin-panel/modules/admin-users.js +1 -1
  51. package/plugins/api-key/crypto.js +101 -0
  52. package/plugins/api-key/index.js +288 -0
  53. package/plugins/api-key/middleware.js +461 -0
  54. package/plugins/api-key/migration-template.js +48 -0
  55. package/plugins/api-key/model.js +50 -0
  56. package/plugins/api-key/scopes.js +125 -0
  57. package/plugins/audit-log/admin-component.js +487 -70
  58. package/plugins/audit-log/api-handlers.js +114 -0
  59. package/plugins/audit-log/index.js +24 -0
  60. package/plugins/content/client/inline-edit.js +2 -46
  61. package/plugins/cors.js +13 -2
  62. package/plugins/email/auth-bridge.js +66 -0
  63. package/plugins/email/bundled-templates.js +15 -0
  64. package/plugins/email/templates/auth-magic-link.mjml +13 -0
  65. package/plugins/file-manager/admin-component.js +69 -53
  66. package/plugins/file-manager/index.js +25 -8
  67. package/plugins/index.js +8 -0
  68. package/plugins/oauth/index.js +338 -0
  69. package/plugins/queue/index.js +4 -0
  70. package/plugins/webhooks/admin-component.js +608 -0
  71. package/plugins/webhooks/api-handlers.js +237 -0
  72. package/plugins/webhooks/crypto.js +144 -0
  73. package/plugins/webhooks/dispatcher.js +708 -0
  74. package/plugins/webhooks/index.js +263 -0
  75. package/plugins/webhooks/matcher.js +113 -0
  76. package/plugins/webhooks/migration-template.js +75 -0
  77. package/plugins/webhooks/models.js +98 -0
  78. package/plugins/webhooks/sender.js +176 -0
  79. package/src/api/api-loader.js +1 -0
  80. package/src/bundle/index.js +2 -1
  81. package/src/discovery/file-route-parser.js +44 -1
  82. package/src/discovery/scan-routes.js +2 -2
  83. package/src/file-router.js +310 -104
  84. package/src/helpers.js +16 -1
  85. package/src/njk-frontmatter.js +3 -0
  86. package/src/routing/cascading.js +490 -0
  87. package/src/routing/middleware-resolver.js +92 -0
  88. package/src/server.js +6 -0
@@ -0,0 +1,608 @@
1
+ /**
2
+ * Webhooks Admin Panel Component — Mithril SPA Component Generator
3
+ * Features endpoints management, real-time ping testing, delivery log inspector, and retry
4
+ * @module plugins/webhooks/admin-component
5
+ */
6
+
7
+ 'use strict';
8
+
9
+ /**
10
+ * Generate client-side Mithril SPA component script string for Webhooks
11
+ *
12
+ * @param {Object} [options]
13
+ * @param {string} [options.apiPrefix='/webhooks']
14
+ * @returns {string}
15
+ */
16
+ function generateWebhooksComponent(options = {}) {
17
+ const apiPrefix = options.apiPrefix || '/webhooks';
18
+
19
+ return `
20
+ (function() {
21
+ var API = '${apiPrefix}';
22
+
23
+ function formatRelativeTime(dateStr) {
24
+ if (!dateStr) return '—';
25
+ try {
26
+ var d = new Date(dateStr);
27
+ var now = new Date();
28
+ var diffMs = now - d;
29
+ var diffSec = Math.floor(diffMs / 1000);
30
+ var diffMin = Math.floor(diffSec / 60);
31
+ var diffHour = Math.floor(diffMin / 60);
32
+ var diffDay = Math.floor(diffHour / 24);
33
+
34
+ if (diffSec < 60) return 'Just now';
35
+ if (diffMin < 60) return diffMin + 'm ago';
36
+ if (diffHour < 24) return diffHour + 'h ago';
37
+ if (diffDay === 1) return 'Yesterday';
38
+ if (diffDay < 30) return diffDay + 'd ago';
39
+ return d.toLocaleDateString();
40
+ } catch (e) {
41
+ return dateStr;
42
+ }
43
+ }
44
+
45
+ function parseEvents(events) {
46
+ if (!events) return ['*'];
47
+ if (Array.isArray(events)) return events;
48
+ if (typeof events === 'string') {
49
+ try {
50
+ var parsed = JSON.parse(events);
51
+ if (Array.isArray(parsed)) return parsed;
52
+ } catch (e) {}
53
+ return events.split(',').map(function(s) { return s.trim(); }).filter(Boolean);
54
+ }
55
+ return ['*'];
56
+ }
57
+
58
+ function WebhooksPage() {
59
+ var activeTab = 'endpoints'; // 'endpoints' | 'deliveries'
60
+ var loading = false;
61
+ var error = null;
62
+ var successToast = null;
63
+
64
+ // Endpoints state
65
+ var endpoints = [];
66
+ var showEndpointModal = false;
67
+ var editingEndpoint = null;
68
+ var endpointForm = {
69
+ name: '',
70
+ url: '',
71
+ secret: '',
72
+ events: '*',
73
+ is_active: true,
74
+ description: '',
75
+ };
76
+
77
+ // Deliveries state
78
+ var deliveries = [];
79
+ var deliveryTotal = 0;
80
+ var deliveryPage = 1;
81
+ var deliveryLimit = 25;
82
+ var deliveryPages = 1;
83
+ var filterEvent = '';
84
+ var filterSuccess = '';
85
+ var activeDeliveryModal = null;
86
+
87
+ function setToast(msg) {
88
+ successToast = msg;
89
+ setTimeout(function() {
90
+ successToast = null;
91
+ m.redraw();
92
+ }, 3500);
93
+ }
94
+
95
+ function generateRandomSecret() {
96
+ var chars = '0123456789abcdef';
97
+ var res = 'whsec_';
98
+ for (var i = 0; i < 48; i++) {
99
+ res += chars[Math.floor(Math.random() * chars.length)];
100
+ }
101
+ return res;
102
+ }
103
+
104
+ function loadEndpoints() {
105
+ loading = true;
106
+ error = null;
107
+ return m.request({
108
+ method: 'GET',
109
+ url: API + '/endpoints',
110
+ }).then(function(res) {
111
+ endpoints = res.items || [];
112
+ loading = false;
113
+ m.redraw();
114
+ }).catch(function(err) {
115
+ loading = false;
116
+ error = (err.response && err.response.error) || err.message || 'Failed to load endpoints.';
117
+ m.redraw();
118
+ });
119
+ }
120
+
121
+ function loadDeliveries() {
122
+ loading = true;
123
+ error = null;
124
+ var q = '?page=' + deliveryPage + '&limit=' + deliveryLimit;
125
+ if (filterEvent) q += '&event=' + encodeURIComponent(filterEvent);
126
+ if (filterSuccess !== '') q += '&success=' + encodeURIComponent(filterSuccess);
127
+
128
+ return m.request({
129
+ method: 'GET',
130
+ url: API + '/deliveries' + q,
131
+ }).then(function(res) {
132
+ deliveries = res.items || [];
133
+ deliveryTotal = res.total || 0;
134
+ deliveryPage = res.page || 1;
135
+ deliveryPages = res.pages || 1;
136
+ loading = false;
137
+ m.redraw();
138
+ }).catch(function(err) {
139
+ loading = false;
140
+ error = (err.response && err.response.error) || err.message || 'Failed to load delivery records.';
141
+ m.redraw();
142
+ });
143
+ }
144
+
145
+ function saveEndpoint() {
146
+ var isEdit = !!editingEndpoint;
147
+ var method = isEdit ? 'PUT' : 'POST';
148
+ var url = isEdit ? API + '/endpoints/' + editingEndpoint.id : API + '/endpoints';
149
+
150
+ var payload = {
151
+ name: endpointForm.name,
152
+ url: endpointForm.url,
153
+ secret: endpointForm.secret || generateRandomSecret(),
154
+ events: endpointForm.events,
155
+ is_active: endpointForm.is_active,
156
+ description: endpointForm.description,
157
+ };
158
+
159
+ return m.request({
160
+ method: method,
161
+ url: url,
162
+ body: payload,
163
+ }).then(function() {
164
+ showEndpointModal = false;
165
+ editingEndpoint = null;
166
+ setToast(isEdit ? 'Endpoint updated.' : 'New endpoint created.');
167
+ loadEndpoints();
168
+ }).catch(function(err) {
169
+ alert((err.response && err.response.error) || err.message || 'Save failed.');
170
+ });
171
+ }
172
+
173
+ function deleteEndpoint(ep) {
174
+ if (!confirm('Are you sure you want to delete endpoint "' + ep.name + '"?')) {
175
+ return;
176
+ }
177
+ m.request({
178
+ method: 'DELETE',
179
+ url: API + '/endpoints/' + ep.id,
180
+ }).then(function() {
181
+ setToast('Endpoint deleted.');
182
+ loadEndpoints();
183
+ }).catch(function(err) {
184
+ alert((err.response && err.response.error) || err.message || 'Delete failed.');
185
+ });
186
+ }
187
+
188
+ function pingEndpoint(ep) {
189
+ setToast('Sending ping to "' + ep.name + '"...');
190
+ m.request({
191
+ method: 'POST',
192
+ url: API + '/endpoints/' + ep.id + '/ping',
193
+ }).then(function(res) {
194
+ if (res.success) {
195
+ setToast('Ping successful! Response code: ' + res.statusCode + ' (' + (res.durationMs || 0) + 'ms)');
196
+ } else {
197
+ alert('Ping failed: ' + (res.error || ('HTTP ' + res.statusCode)));
198
+ }
199
+ if (activeTab === 'deliveries') loadDeliveries();
200
+ }).catch(function(err) {
201
+ alert('Failed to send ping request: ' + (err.message || 'Network error'));
202
+ });
203
+ }
204
+
205
+ function retryDelivery(dlv) {
206
+ setToast('Retrying delivery #' + dlv.id + '...');
207
+ m.request({
208
+ method: 'POST',
209
+ url: API + '/deliveries/' + dlv.id + '/retry',
210
+ }).then(function(res) {
211
+ if (res.success) {
212
+ setToast('Retry successful! (' + res.statusCode + ')');
213
+ } else {
214
+ alert('Retry failed: ' + (res.error || ('HTTP ' + res.statusCode)));
215
+ }
216
+ loadDeliveries();
217
+ }).catch(function(err) {
218
+ alert('Failed to send retry request: ' + err.message);
219
+ });
220
+ }
221
+
222
+ function openNewModal() {
223
+ editingEndpoint = null;
224
+ endpointForm = {
225
+ name: '',
226
+ url: '',
227
+ secret: generateRandomSecret(),
228
+ events: '*',
229
+ is_active: true,
230
+ description: '',
231
+ };
232
+ showEndpointModal = true;
233
+ }
234
+
235
+ function openEditModal(ep) {
236
+ editingEndpoint = ep;
237
+ var evts = parseEvents(ep.events).join(', ');
238
+ endpointForm = {
239
+ name: ep.name || '',
240
+ url: ep.url || '',
241
+ secret: ep.secret || '',
242
+ events: evts,
243
+ is_active: ep.is_active !== false,
244
+ description: ep.description || '',
245
+ };
246
+ showEndpointModal = true;
247
+ }
248
+
249
+ return {
250
+ oninit: function() {
251
+ loadEndpoints();
252
+ },
253
+
254
+ view: function() {
255
+ var totalEndpoints = endpoints.length;
256
+ var activeEndpoints = endpoints.filter(function(e) { return e.is_active; }).length;
257
+
258
+ return m('div.space-y-6', [
259
+ // Toast Notification
260
+ successToast ? m('div.fixed.bottom-5.right-5.z-50.px-4.py-2.5.rounded-xl.bg-emerald-600.text-white.text-sm.font-medium.shadow-lg.flex.items-center.gap-2.animate-fade-in', [
261
+ m('span', '✓'),
262
+ m('span', successToast),
263
+ ]) : null,
264
+
265
+ // Page Header
266
+ m('div.flex.flex-col.sm:flex-row.sm:items-center.sm:justify-between.gap-4.border-b.border-gray-200.dark:border-slate-800.pb-5', [
267
+ m('div', [
268
+ m('h1.text-2xl.font-bold.text-gray-900.dark:text-white', 'Webhooks'),
269
+ m('p.text-sm.text-gray-500.dark:text-slate-400.mt-1',
270
+ 'Secure HMAC-SHA256 event notifications and delivery management for external systems'
271
+ ),
272
+ ]),
273
+ m('div.flex.items-center.gap-3', [
274
+ m('button.inline-flex.items-center.gap-2.px-4.py-2.text-sm.font-semibold.rounded-xl.bg-indigo-600.text-white.hover:bg-indigo-500.shadow-sm.transition-all', {
275
+ onclick: openNewModal,
276
+ }, [
277
+ m('span', '+'),
278
+ m('span', 'Add New Endpoint'),
279
+ ]),
280
+ ]),
281
+ ]),
282
+
283
+ // Stats Bar
284
+ m('div.grid.grid-cols-2.sm:grid-cols-4.gap-4', [
285
+ m('div.p-4.rounded-2xl.bg-white.dark:bg-slate-800.border.border-gray-200.dark:border-slate-700.shadow-sm', [
286
+ m('div.text-xs.font-medium.text-gray-500.dark:text-slate-400', 'Registered Endpoints'),
287
+ m('div.text-2xl.font-bold.text-gray-900.dark:text-white.mt-1', totalEndpoints),
288
+ ]),
289
+ m('div.p-4.rounded-2xl.bg-white.dark:bg-slate-800.border.border-gray-200.dark:border-slate-700.shadow-sm', [
290
+ m('div.text-xs.font-medium.text-gray-500.dark:text-slate-400', 'Active Endpoints'),
291
+ m('div.text-2xl.font-bold.text-emerald-600.dark:text-emerald-400.mt-1', activeEndpoints),
292
+ ]),
293
+ m('div.p-4.rounded-2xl.bg-white.dark:bg-slate-800.border.border-gray-200.dark:border-slate-700.shadow-sm', [
294
+ m('div.text-xs.font-medium.text-gray-500.dark:text-slate-400', 'Deliveries Count'),
295
+ m('div.text-2xl.font-bold.text-indigo-600.dark:text-indigo-400.mt-1', deliveryTotal || '—'),
296
+ ]),
297
+ m('div.p-4.rounded-2xl.bg-white.dark:bg-slate-800.border.border-gray-200.dark:border-slate-700.shadow-sm', [
298
+ m('div.text-xs.font-medium.text-gray-500.dark:text-slate-400', 'Signature Standard'),
299
+ m('div.text-sm.font-semibold.text-gray-700.dark:text-slate-200.mt-2.font-mono', 'HMAC SHA-256'),
300
+ ]),
301
+ ]),
302
+
303
+ // Tabs Switcher
304
+ m('div.flex.border-b.border-gray-200.dark:border-slate-700', [
305
+ m('button.px-5.py-2.5.text-sm.font-medium.border-b-2.transition-colors', {
306
+ class: activeTab === 'endpoints'
307
+ ? 'border-indigo-600.text-indigo-600.dark:text-indigo-400.dark:border-indigo-400'
308
+ : 'border-transparent.text-gray-500.hover:text-gray-700.dark:text-slate-400.dark:hover:text-slate-200',
309
+ onclick: function() { activeTab = 'endpoints'; loadEndpoints(); },
310
+ }, 'Endpoints (' + totalEndpoints + ')'),
311
+ m('button.px-5.py-2.5.text-sm.font-medium.border-b-2.transition-colors', {
312
+ class: activeTab === 'deliveries'
313
+ ? 'border-indigo-600.text-indigo-600.dark:text-indigo-400.dark:border-indigo-400'
314
+ : 'border-transparent.text-gray-500.hover:text-gray-700.dark:text-slate-400.dark:hover:text-slate-200',
315
+ onclick: function() { activeTab = 'deliveries'; loadDeliveries(); },
316
+ }, 'Delivery History'),
317
+ ]),
318
+
319
+ // TAB 1: ENDPOINTS LIST
320
+ activeTab === 'endpoints' ? m('div.bg-white.dark:bg-slate-800.border.border-gray-200.dark:border-slate-700.rounded-2xl.overflow-hidden.shadow-sm', [
321
+ endpoints.length === 0 ? m('div.p-12.text-center.text-gray-500.dark:text-slate-400', [
322
+ m('p.text-base.font-medium', 'No webhook endpoints registered yet.'),
323
+ m('p.text-xs.mt-1', 'Add a new endpoint above to forward system events to Zapier, Slack, or your custom server.'),
324
+ ]) : m('div.overflow-x-auto', [
325
+ m('table.w-full.text-left.text-sm', [
326
+ m('thead.bg-gray-50.dark:bg-slate-750.text-xs.uppercase.text-gray-500.dark:text-slate-400.border-b.border-gray-200.dark:border-slate-700', [
327
+ m('tr', [
328
+ m('th.p-4', 'Name & Target URL'),
329
+ m('th.p-4', 'Subscribed Events'),
330
+ m('th.p-4', 'Status'),
331
+ m('th.p-4', 'Success Rate'),
332
+ m('th.p-4.text-right', 'Actions'),
333
+ ]),
334
+ ]),
335
+ m('tbody.divide-y.divide-gray-100.dark:divide-slate-700', endpoints.map(function(ep) {
336
+ var evts = parseEvents(ep.events);
337
+ var stats = ep.stats || { total: 0, successRate: 100 };
338
+
339
+ return m('tr.hover:bg-gray-50/60.dark:hover:bg-slate-750/50.transition-colors', [
340
+ m('td.p-4', [
341
+ m('div.font-semibold.text-gray-900.dark:text-white', ep.name),
342
+ m('div.text-xs.font-mono.text-gray-500.dark:text-slate-400.truncate.max-w-md.mt-0.5', { title: ep.url }, ep.url),
343
+ ep.description ? m('div.text-xs.text-gray-400.mt-1', ep.description) : null,
344
+ ]),
345
+ m('td.p-4', [
346
+ m('div.flex.flex-wrap.gap-1', evts.map(function(evt) {
347
+ return m('span.px-2.py-0.5.text-xs.font-mono.rounded-md.bg-gray-100.dark:bg-slate-700.text-gray-700.dark:text-slate-300', evt);
348
+ })),
349
+ ]),
350
+ m('td.p-4', [
351
+ ep.is_active
352
+ ? m('span.inline-flex.items-center.gap-1.5.px-2.5.py-1.rounded-full.text-xs.font-medium.bg-emerald-50.text-emerald-700.dark:bg-emerald-950/40.dark:text-emerald-300', [
353
+ m('span.w-1.5.h-1.5.rounded-full.bg-emerald-500'),
354
+ 'Active'
355
+ ])
356
+ : m('span.inline-flex.items-center.gap-1.5.px-2.5.py-1.rounded-full.text-xs.font-medium.bg-gray-100.text-gray-600.dark:bg-slate-700.dark:text-slate-400', [
357
+ m('span.w-1.5.h-1.5.rounded-full.bg-gray-400'),
358
+ 'Inactive'
359
+ ]),
360
+ ]),
361
+ m('td.p-4', [
362
+ stats.total > 0
363
+ ? m('div.text-xs', [
364
+ m('span.font-medium', stats.successRate + '% '),
365
+ m('span.text-gray-400', '(' + stats.successful + '/' + stats.total + ')'),
366
+ ])
367
+ : m('span.text-xs.text-gray-400', 'No deliveries'),
368
+ ]),
369
+ m('td.p-4.text-right', [
370
+ m('div.inline-flex.items-center.gap-2', [
371
+ m('button.px-2.5.py-1.text-xs.font-medium.rounded-lg.border.border-gray-200.dark:border-slate-700.text-gray-700.dark:text-slate-200.hover:bg-gray-100.dark:hover:bg-slate-700.transition-colors', {
372
+ onclick: function() { pingEndpoint(ep); },
373
+ }, '⚡ Ping'),
374
+ m('button.px-2.5.py-1.text-xs.font-medium.rounded-lg.border.border-gray-200.dark:border-slate-700.text-indigo-600.dark:text-indigo-400.hover:bg-indigo-50.dark:hover:bg-slate-700.transition-colors', {
375
+ onclick: function() { openEditModal(ep); },
376
+ }, 'Edit'),
377
+ m('button.px-2.5.py-1.text-xs.font-medium.rounded-lg.border.border-gray-200.dark:border-slate-700.text-red-600.hover:bg-red-50.dark:hover:bg-slate-700.transition-colors', {
378
+ onclick: function() { deleteEndpoint(ep); },
379
+ }, 'Delete'),
380
+ ]),
381
+ ]),
382
+ ]);
383
+ })),
384
+ ]),
385
+ ]),
386
+ ]) : null,
387
+
388
+ // TAB 2: DELIVERIES LIST
389
+ activeTab === 'deliveries' ? m('div.space-y-4', [
390
+ // Filter Bar
391
+ m('div.flex.flex-col.sm:flex-row.gap-3.items-center.justify-between.bg-white.dark:bg-slate-800.p-3.rounded-xl.border.border-gray-200.dark:border-slate-700', [
392
+ m('div.flex.items-center.gap-3.w-full.sm:w-auto', [
393
+ m('input.px-3.py-1.5.text-xs.border.border-gray-200.dark:border-slate-700.rounded-lg.bg-transparent.text-gray-900.dark:text-white', {
394
+ placeholder: 'Filter event (e.g. user.*)',
395
+ value: filterEvent,
396
+ oninput: function(e) { filterEvent = e.target.value; },
397
+ }),
398
+ m('select.px-3.py-1.5.text-xs.border.border-gray-200.dark:border-slate-700.rounded-lg.bg-transparent.text-gray-900.dark:text-white', {
399
+ value: filterSuccess,
400
+ onchange: function(e) { filterSuccess = e.target.value; },
401
+ }, [
402
+ m('option', { value: '' }, 'All Statuses'),
403
+ m('option', { value: 'true' }, 'Success Only'),
404
+ m('option', { value: 'false' }, 'Failed Only'),
405
+ ]),
406
+ m('button.px-3.py-1.5.text-xs.font-medium.rounded-lg.bg-gray-100.dark:bg-slate-700.text-gray-700.dark:text-slate-200.hover:bg-gray-200', {
407
+ onclick: function() { deliveryPage = 1; loadDeliveries(); },
408
+ }, 'Filter'),
409
+ ]),
410
+ m('div.text-xs.text-gray-500.dark:text-slate-400', 'Total: ' + deliveryTotal + ' records'),
411
+ ]),
412
+
413
+ // Deliveries Table
414
+ m('div.bg-white.dark:bg-slate-800.border.border-gray-200.dark:border-slate-700.rounded-2xl.overflow-hidden.shadow-sm', [
415
+ deliveries.length === 0 ? m('div.p-12.text-center.text-gray-500.dark:text-slate-400', [
416
+ m('p.text-base.font-medium', 'No delivery records found yet.'),
417
+ ]) : m('div.overflow-x-auto', [
418
+ m('table.w-full.text-left.text-sm', [
419
+ m('thead.bg-gray-50.dark:bg-slate-750.text-xs.uppercase.text-gray-500.dark:text-slate-400.border-b.border-gray-200.dark:border-slate-700', [
420
+ m('tr', [
421
+ m('th.p-4', 'Status'),
422
+ m('th.p-4', 'Event'),
423
+ m('th.p-4', 'Duration'),
424
+ m('th.p-4', 'Attempt'),
425
+ m('th.p-4', 'Time'),
426
+ m('th.p-4.text-right', 'Actions'),
427
+ ]),
428
+ ]),
429
+ m('tbody.divide-y.divide-gray-100.dark:divide-slate-700', deliveries.map(function(dlv) {
430
+ var isSuccess = dlv.success;
431
+ var badgeClass = isSuccess
432
+ ? 'bg-emerald-50 text-emerald-700 border-emerald-200 dark:bg-emerald-950/40 dark:text-emerald-300'
433
+ : 'bg-rose-50 text-rose-700 border-rose-200 dark:bg-rose-950/40 dark:text-rose-300';
434
+ var statusLabel = dlv.status_code ? 'HTTP ' + dlv.status_code : (isSuccess ? '200 OK' : 'Error');
435
+
436
+ return m('tr.hover:bg-gray-50/60.dark:hover:bg-slate-750/50.transition-colors', [
437
+ m('td.p-4', [
438
+ m('span.inline-flex.items-center.px-2.5.py-0.5.rounded-full.text-xs.font-semibold.border', {
439
+ class: badgeClass,
440
+ }, statusLabel),
441
+ ]),
442
+ m('td.p-4', [
443
+ m('span.font-mono.font-semibold.text-gray-900.dark:text-white', dlv.event),
444
+ m('div.text-xs.text-gray-400', 'Endpoint #' + dlv.endpoint_id),
445
+ ]),
446
+ m('td.p-4.text-xs.text-gray-600.dark:text-slate-300', (dlv.duration_ms != null ? dlv.duration_ms + ' ms' : '—')),
447
+ m('td.p-4.text-xs.text-gray-600.dark:text-slate-300', dlv.attempt + ' / ' + dlv.max_attempts),
448
+ m('td.p-4.text-xs.text-gray-500.dark:text-slate-400', formatRelativeTime(dlv.created_at)),
449
+ m('td.p-4.text-right', [
450
+ m('div.inline-flex.items-center.gap-2', [
451
+ m('button.px-2.5.py-1.text-xs.font-medium.rounded-lg.border.border-gray-200.dark:border-slate-700.text-indigo-600.hover:bg-indigo-50.dark:hover:bg-slate-700', {
452
+ onclick: function() { activeDeliveryModal = dlv; },
453
+ }, 'Details'),
454
+ !isSuccess ? m('button.px-2.5.py-1.text-xs.font-medium.rounded-lg.bg-indigo-50.text-indigo-600.dark:bg-indigo-950/50.dark:text-indigo-300.hover:bg-indigo-100', {
455
+ onclick: function() { retryDelivery(dlv); },
456
+ }, 'Retry') : null,
457
+ ]),
458
+ ]),
459
+ ]);
460
+ })),
461
+ ]),
462
+ ]),
463
+
464
+ // Pagination Bar
465
+ m('div.flex.items-center.justify-between.p-4.border-t.border-gray-100.dark:border-slate-700', [
466
+ m('div.text-xs.text-gray-500', deliveryTotal + ' records listed'),
467
+ m('div.flex.items-center.gap-2', [
468
+ m('button.px-3.py-1.text-xs.border.rounded-lg.disabled:opacity-40', {
469
+ disabled: deliveryPage <= 1,
470
+ onclick: function() { if (deliveryPage > 1) { deliveryPage--; loadDeliveries(); } },
471
+ }, 'Previous'),
472
+ m('span.text-xs.text-gray-600.dark:text-slate-300', deliveryPage + ' / ' + deliveryPages),
473
+ m('button.px-3.py-1.text-xs.border.rounded-lg.disabled:opacity-40', {
474
+ disabled: deliveryPage >= deliveryPages,
475
+ onclick: function() { if (deliveryPage < deliveryPages) { deliveryPage++; loadDeliveries(); } },
476
+ }, 'Next'),
477
+ ]),
478
+ ]),
479
+ ]),
480
+ ]) : null,
481
+
482
+ // MODAL: CREATE / EDIT ENDPOINT
483
+ showEndpointModal ? m('div.fixed.inset-0.z-50.flex.items-center.justify-center.p-4.bg-black/50.backdrop-blur-sm', [
484
+ m('div.bg-white.dark:bg-slate-800.rounded-2xl.border.border-gray-200.dark:border-slate-700.shadow-2xl.w-full.max-w-lg.p-6.space-y-4', [
485
+ m('div.flex.items-center.justify-between', [
486
+ m('h3.text-lg.font-bold.text-gray-900.dark:text-white', editingEndpoint ? 'Edit Endpoint' : 'New Webhook Endpoint'),
487
+ m('button.text-gray-400.hover:text-gray-600.dark:hover:text-white', {
488
+ onclick: function() { showEndpointModal = false; },
489
+ }, '✕'),
490
+ ]),
491
+ m('div.space-y-3', [
492
+ m('div', [
493
+ m('label.block.text-xs.font-semibold.text-gray-700.dark:text-slate-300.mb-1', 'Name / Description *'),
494
+ m('input.w-full.px-3.py-2.text-sm.border.border-gray-300.dark:border-slate-600.rounded-xl.bg-transparent.text-gray-900.dark:text-white', {
495
+ placeholder: 'e.g. Zapier Order Sync',
496
+ value: endpointForm.name,
497
+ oninput: function(e) { endpointForm.name = e.target.value; },
498
+ }),
499
+ ]),
500
+ m('div', [
501
+ m('label.block.text-xs.font-semibold.text-gray-700.dark:text-slate-300.mb-1', 'Target URL *'),
502
+ m('input.w-full.px-3.py-2.text-sm.border.border-gray-300.dark:border-slate-600.rounded-xl.bg-transparent.text-gray-900.dark:text-white', {
503
+ placeholder: 'https://example.com/api/webhook',
504
+ value: endpointForm.url,
505
+ oninput: function(e) { endpointForm.url = e.target.value; },
506
+ }),
507
+ ]),
508
+ m('div', [
509
+ m('div.flex.items-center.justify-between.mb-1', [
510
+ m('label.text-xs.font-semibold.text-gray-700.dark:text-slate-300', 'Signing Secret'),
511
+ m('button.text-xs.text-indigo-600.dark:text-indigo-400.hover:underline', {
512
+ type: 'button',
513
+ onclick: function() { endpointForm.secret = generateRandomSecret(); },
514
+ }, 'Generate New'),
515
+ ]),
516
+ m('input.w-full.px-3.py-2.text-xs.font-mono.border.border-gray-300.dark:border-slate-600.rounded-xl.bg-transparent.text-gray-900.dark:text-white', {
517
+ value: endpointForm.secret,
518
+ oninput: function(e) { endpointForm.secret = e.target.value; },
519
+ }),
520
+ ]),
521
+ m('div', [
522
+ m('label.block.text-xs.font-semibold.text-gray-700.dark:text-slate-300.mb-1', 'Subscribed Events (Comma-separated, * for all events)'),
523
+ m('input.w-full.px-3.py-2.text-sm.border.border-gray-300.dark:border-slate-600.rounded-xl.bg-transparent.text-gray-900.dark:text-white', {
524
+ placeholder: 'user.*, order.created, payment.success',
525
+ value: endpointForm.events,
526
+ oninput: function(e) { endpointForm.events = e.target.value; },
527
+ }),
528
+ ]),
529
+ m('div.flex.items-center.gap-2.pt-1', [
530
+ m('input[type=checkbox]', {
531
+ id: 'active_check',
532
+ checked: endpointForm.is_active,
533
+ onchange: function(e) { endpointForm.is_active = e.target.checked; },
534
+ }),
535
+ m('label.text-xs.font-medium.text-gray-700.dark:text-slate-300', { for: 'active_check' }, 'Enable this endpoint to receive events'),
536
+ ]),
537
+ ]),
538
+ m('div.flex.items-center.justify-end.gap-3.pt-3.border-t.border-gray-100.dark:border-slate-700', [
539
+ m('button.px-4.py-2.text-xs.font-medium.rounded-xl.border.border-gray-300.dark:border-slate-600.text-gray-700.dark:text-slate-200', {
540
+ onclick: function() { showEndpointModal = false; },
541
+ }, 'Cancel'),
542
+ m('button.px-4.py-2.text-xs.font-semibold.rounded-xl.bg-indigo-600.text-white.hover:bg-indigo-500', {
543
+ onclick: saveEndpoint,
544
+ }, editingEndpoint ? 'Update' : 'Save'),
545
+ ]),
546
+ ]),
547
+ ]) : null,
548
+
549
+ // MODAL: DELIVERY DETAILS
550
+ activeDeliveryModal ? m('div.fixed.inset-0.z-50.flex.items-center.justify-center.p-4.bg-black/50.backdrop-blur-sm', [
551
+ m('div.bg-white.dark:bg-slate-800.rounded-2xl.border.border-gray-200.dark:border-slate-700.shadow-2xl.w-full.max-w-2xl.max-h-[90vh].flex.flex-col.overflow-hidden', [
552
+ m('div.p-5.border-b.border-gray-200.dark:border-slate-700.flex.items-center.justify-between', [
553
+ m('div', [
554
+ m('h3.text-base.font-bold.text-gray-900.dark:text-white', 'Delivery Details #' + activeDeliveryModal.id),
555
+ m('span.text-xs.text-gray-500.dark:text-slate-400', activeDeliveryModal.event + ' — ' + formatRelativeTime(activeDeliveryModal.created_at)),
556
+ ]),
557
+ m('button.text-gray-400.hover:text-gray-600.dark:hover:text-white', {
558
+ onclick: function() { activeDeliveryModal = null; },
559
+ }, '✕'),
560
+ ]),
561
+ m('div.p-5.space-y-4.overflow-y-auto.flex-1', [
562
+ m('div.grid.grid-cols-2.sm:grid-cols-3.gap-2.text-xs.p-3.bg-gray-50.dark:bg-slate-750.rounded-xl', [
563
+ m('div', [m('span.text-gray-400.block', 'Status'), m('span.font-semibold', activeDeliveryModal.success ? 'Success' : 'Failed')]),
564
+ m('div', [m('span.text-gray-400.block', 'HTTP Code'), m('span.font-semibold', activeDeliveryModal.status_code || '—')]),
565
+ m('div', [m('span.text-gray-400.block', 'Response Time'), m('span.font-semibold', (activeDeliveryModal.duration_ms || 0) + ' ms')]),
566
+ m('div', [m('span.text-gray-400.block', 'Attempt'), m('span.font-semibold', activeDeliveryModal.attempt + ' / ' + activeDeliveryModal.max_attempts)]),
567
+ m('div.col-span-2', [m('span.text-gray-400.block', 'Error'), m('span.text-red-500', activeDeliveryModal.error || 'None')]),
568
+ ]),
569
+ m('div', [
570
+ m('label.block.text-xs.font-semibold.text-gray-700.dark:text-slate-300.mb-1', 'Request Payload (JSON)'),
571
+ m('pre.p-3.rounded-xl.bg-slate-900.text-slate-100.text-xs.font-mono.overflow-x-auto.max-h-60',
572
+ (function() {
573
+ try {
574
+ return JSON.stringify(JSON.parse(activeDeliveryModal.payload), null, 2);
575
+ } catch (e) {
576
+ return activeDeliveryModal.payload || '';
577
+ }
578
+ })()
579
+ ),
580
+ ]),
581
+ activeDeliveryModal.response_body ? m('div', [
582
+ m('label.block.text-xs.font-semibold.text-gray-700.dark:text-slate-300.mb-1', 'Target Response Body'),
583
+ m('pre.p-3.rounded-xl.bg-slate-900.text-slate-100.text-xs.font-mono.overflow-x-auto.max-h-40',
584
+ activeDeliveryModal.response_body
585
+ ),
586
+ ]) : null,
587
+ ]),
588
+ m('div.p-4.border-t.border-gray-200.dark:border-slate-700.flex.justify-end', [
589
+ m('button.px-4.py-2.text-xs.font-semibold.rounded-xl.bg-indigo-600.text-white.hover:bg-indigo-500', {
590
+ onclick: function() { activeDeliveryModal = null; },
591
+ }, 'Close'),
592
+ ]),
593
+ ]),
594
+ ]) : null,
595
+ ]);
596
+ },
597
+ };
598
+ }
599
+
600
+ window.__customPages = window.__customPages || {};
601
+ window.__customPages['webhooks'] = WebhooksPage;
602
+ })();
603
+ `;
604
+ }
605
+
606
+ module.exports = {
607
+ generateWebhooksComponent,
608
+ };