@omnixal/openclaw-nats-plugin 0.2.3 → 0.2.5

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 (31) hide show
  1. package/PLUGIN.md +9 -6
  2. package/README.md +12 -1
  3. package/dashboard/src/lib/CronPanel.svelte +206 -27
  4. package/dashboard/src/lib/LogsPanel.svelte +211 -0
  5. package/dashboard/src/lib/RoutesPanel.svelte +157 -13
  6. package/dashboard/src/lib/api.ts +77 -0
  7. package/dashboard/src/lib/components/ui/modal/index.ts +1 -0
  8. package/dashboard/src/lib/components/ui/modal/modal.svelte +49 -0
  9. package/dashboard/src/lib/utils.ts +8 -0
  10. package/package.json +1 -1
  11. package/sidecar/bun.lock +2 -2
  12. package/sidecar/package.json +1 -1
  13. package/sidecar/src/app.module.ts +2 -0
  14. package/sidecar/src/consumer/consumer.controller.ts +20 -12
  15. package/sidecar/src/consumer/consumer.module.ts +2 -1
  16. package/sidecar/src/db/migrations/0005_strong_supernaut.sql +13 -0
  17. package/sidecar/src/db/migrations/meta/0005_snapshot.json +389 -0
  18. package/sidecar/src/db/migrations/meta/_journal.json +7 -0
  19. package/sidecar/src/db/schema.ts +17 -0
  20. package/sidecar/src/logs/log.controller.ts +50 -0
  21. package/sidecar/src/logs/log.module.ts +11 -0
  22. package/sidecar/src/logs/log.repository.ts +78 -0
  23. package/sidecar/src/logs/log.service.ts +116 -0
  24. package/sidecar/src/router/router.controller.ts +28 -6
  25. package/sidecar/src/router/router.repository.ts +8 -0
  26. package/sidecar/src/router/router.service.ts +4 -0
  27. package/sidecar/src/scheduler/scheduler.controller.ts +38 -4
  28. package/sidecar/src/scheduler/scheduler.module.ts +2 -1
  29. package/sidecar/src/scheduler/scheduler.repository.ts +8 -0
  30. package/sidecar/src/scheduler/scheduler.service.ts +94 -28
  31. package/sidecar/src/validation/schemas.ts +27 -0
@@ -0,0 +1,389 @@
1
+ {
2
+ "version": "6",
3
+ "dialect": "sqlite",
4
+ "id": "c4dc18d6-4faf-4c65-98fc-5d5d25e45cab",
5
+ "prevId": "e31f4610-cdfc-459e-b947-8363975599a0",
6
+ "tables": {
7
+ "cron_jobs": {
8
+ "name": "cron_jobs",
9
+ "columns": {
10
+ "id": {
11
+ "name": "id",
12
+ "type": "text",
13
+ "primaryKey": true,
14
+ "notNull": true,
15
+ "autoincrement": false
16
+ },
17
+ "name": {
18
+ "name": "name",
19
+ "type": "text",
20
+ "primaryKey": false,
21
+ "notNull": true,
22
+ "autoincrement": false
23
+ },
24
+ "expr": {
25
+ "name": "expr",
26
+ "type": "text",
27
+ "primaryKey": false,
28
+ "notNull": true,
29
+ "autoincrement": false
30
+ },
31
+ "subject": {
32
+ "name": "subject",
33
+ "type": "text",
34
+ "primaryKey": false,
35
+ "notNull": true,
36
+ "autoincrement": false
37
+ },
38
+ "payload": {
39
+ "name": "payload",
40
+ "type": "text",
41
+ "primaryKey": false,
42
+ "notNull": false,
43
+ "autoincrement": false
44
+ },
45
+ "timezone": {
46
+ "name": "timezone",
47
+ "type": "text",
48
+ "primaryKey": false,
49
+ "notNull": true,
50
+ "autoincrement": false,
51
+ "default": "'UTC'"
52
+ },
53
+ "enabled": {
54
+ "name": "enabled",
55
+ "type": "integer",
56
+ "primaryKey": false,
57
+ "notNull": true,
58
+ "autoincrement": false,
59
+ "default": true
60
+ },
61
+ "last_run_at": {
62
+ "name": "last_run_at",
63
+ "type": "integer",
64
+ "primaryKey": false,
65
+ "notNull": false,
66
+ "autoincrement": false
67
+ },
68
+ "created_at": {
69
+ "name": "created_at",
70
+ "type": "integer",
71
+ "primaryKey": false,
72
+ "notNull": true,
73
+ "autoincrement": false
74
+ }
75
+ },
76
+ "indexes": {
77
+ "cron_jobs_name_unique": {
78
+ "name": "cron_jobs_name_unique",
79
+ "columns": [
80
+ "name"
81
+ ],
82
+ "isUnique": true
83
+ },
84
+ "cron_jobs_name_idx": {
85
+ "name": "cron_jobs_name_idx",
86
+ "columns": [
87
+ "name"
88
+ ],
89
+ "isUnique": false
90
+ }
91
+ },
92
+ "foreignKeys": {},
93
+ "compositePrimaryKeys": {},
94
+ "uniqueConstraints": {},
95
+ "checkConstraints": {}
96
+ },
97
+ "dedup_events": {
98
+ "name": "dedup_events",
99
+ "columns": {
100
+ "event_id": {
101
+ "name": "event_id",
102
+ "type": "text",
103
+ "primaryKey": true,
104
+ "notNull": true,
105
+ "autoincrement": false
106
+ },
107
+ "subject": {
108
+ "name": "subject",
109
+ "type": "text",
110
+ "primaryKey": false,
111
+ "notNull": true,
112
+ "autoincrement": false
113
+ },
114
+ "seen_at": {
115
+ "name": "seen_at",
116
+ "type": "integer",
117
+ "primaryKey": false,
118
+ "notNull": true,
119
+ "autoincrement": false
120
+ }
121
+ },
122
+ "indexes": {
123
+ "dedup_events_seen_at_idx": {
124
+ "name": "dedup_events_seen_at_idx",
125
+ "columns": [
126
+ "seen_at"
127
+ ],
128
+ "isUnique": false
129
+ }
130
+ },
131
+ "foreignKeys": {},
132
+ "compositePrimaryKeys": {},
133
+ "uniqueConstraints": {},
134
+ "checkConstraints": {}
135
+ },
136
+ "event_routes": {
137
+ "name": "event_routes",
138
+ "columns": {
139
+ "id": {
140
+ "name": "id",
141
+ "type": "text",
142
+ "primaryKey": true,
143
+ "notNull": true,
144
+ "autoincrement": false
145
+ },
146
+ "pattern": {
147
+ "name": "pattern",
148
+ "type": "text",
149
+ "primaryKey": false,
150
+ "notNull": true,
151
+ "autoincrement": false
152
+ },
153
+ "target": {
154
+ "name": "target",
155
+ "type": "text",
156
+ "primaryKey": false,
157
+ "notNull": true,
158
+ "autoincrement": false,
159
+ "default": "'main'"
160
+ },
161
+ "enabled": {
162
+ "name": "enabled",
163
+ "type": "integer",
164
+ "primaryKey": false,
165
+ "notNull": true,
166
+ "autoincrement": false,
167
+ "default": true
168
+ },
169
+ "priority": {
170
+ "name": "priority",
171
+ "type": "integer",
172
+ "primaryKey": false,
173
+ "notNull": true,
174
+ "autoincrement": false,
175
+ "default": 5
176
+ },
177
+ "created_at": {
178
+ "name": "created_at",
179
+ "type": "integer",
180
+ "primaryKey": false,
181
+ "notNull": true,
182
+ "autoincrement": false
183
+ },
184
+ "last_delivered_at": {
185
+ "name": "last_delivered_at",
186
+ "type": "integer",
187
+ "primaryKey": false,
188
+ "notNull": false,
189
+ "autoincrement": false
190
+ },
191
+ "last_event_subject": {
192
+ "name": "last_event_subject",
193
+ "type": "text",
194
+ "primaryKey": false,
195
+ "notNull": false,
196
+ "autoincrement": false
197
+ },
198
+ "delivery_count": {
199
+ "name": "delivery_count",
200
+ "type": "integer",
201
+ "primaryKey": false,
202
+ "notNull": true,
203
+ "autoincrement": false,
204
+ "default": 0
205
+ }
206
+ },
207
+ "indexes": {
208
+ "event_routes_pattern_unique": {
209
+ "name": "event_routes_pattern_unique",
210
+ "columns": [
211
+ "pattern"
212
+ ],
213
+ "isUnique": true
214
+ },
215
+ "event_routes_pattern_idx": {
216
+ "name": "event_routes_pattern_idx",
217
+ "columns": [
218
+ "pattern"
219
+ ],
220
+ "isUnique": false
221
+ },
222
+ "event_routes_target_idx": {
223
+ "name": "event_routes_target_idx",
224
+ "columns": [
225
+ "target"
226
+ ],
227
+ "isUnique": false
228
+ }
229
+ },
230
+ "foreignKeys": {},
231
+ "compositePrimaryKeys": {},
232
+ "uniqueConstraints": {},
233
+ "checkConstraints": {}
234
+ },
235
+ "execution_logs": {
236
+ "name": "execution_logs",
237
+ "columns": {
238
+ "id": {
239
+ "name": "id",
240
+ "type": "text",
241
+ "primaryKey": true,
242
+ "notNull": true,
243
+ "autoincrement": false
244
+ },
245
+ "entity_type": {
246
+ "name": "entity_type",
247
+ "type": "text",
248
+ "primaryKey": false,
249
+ "notNull": true,
250
+ "autoincrement": false
251
+ },
252
+ "entity_id": {
253
+ "name": "entity_id",
254
+ "type": "text",
255
+ "primaryKey": false,
256
+ "notNull": true,
257
+ "autoincrement": false
258
+ },
259
+ "action": {
260
+ "name": "action",
261
+ "type": "text",
262
+ "primaryKey": false,
263
+ "notNull": true,
264
+ "autoincrement": false
265
+ },
266
+ "subject": {
267
+ "name": "subject",
268
+ "type": "text",
269
+ "primaryKey": false,
270
+ "notNull": true,
271
+ "autoincrement": false
272
+ },
273
+ "detail": {
274
+ "name": "detail",
275
+ "type": "text",
276
+ "primaryKey": false,
277
+ "notNull": false,
278
+ "autoincrement": false
279
+ },
280
+ "success": {
281
+ "name": "success",
282
+ "type": "integer",
283
+ "primaryKey": false,
284
+ "notNull": true,
285
+ "autoincrement": false,
286
+ "default": true
287
+ },
288
+ "created_at": {
289
+ "name": "created_at",
290
+ "type": "integer",
291
+ "primaryKey": false,
292
+ "notNull": true,
293
+ "autoincrement": false
294
+ }
295
+ },
296
+ "indexes": {
297
+ "execution_logs_entity_idx": {
298
+ "name": "execution_logs_entity_idx",
299
+ "columns": [
300
+ "entity_type",
301
+ "entity_id"
302
+ ],
303
+ "isUnique": false
304
+ },
305
+ "execution_logs_created_at_idx": {
306
+ "name": "execution_logs_created_at_idx",
307
+ "columns": [
308
+ "created_at"
309
+ ],
310
+ "isUnique": false
311
+ }
312
+ },
313
+ "foreignKeys": {},
314
+ "compositePrimaryKeys": {},
315
+ "uniqueConstraints": {},
316
+ "checkConstraints": {}
317
+ },
318
+ "pending_events": {
319
+ "name": "pending_events",
320
+ "columns": {
321
+ "id": {
322
+ "name": "id",
323
+ "type": "text",
324
+ "primaryKey": true,
325
+ "notNull": true,
326
+ "autoincrement": false
327
+ },
328
+ "session_key": {
329
+ "name": "session_key",
330
+ "type": "text",
331
+ "primaryKey": false,
332
+ "notNull": true,
333
+ "autoincrement": false
334
+ },
335
+ "subject": {
336
+ "name": "subject",
337
+ "type": "text",
338
+ "primaryKey": false,
339
+ "notNull": true,
340
+ "autoincrement": false
341
+ },
342
+ "payload": {
343
+ "name": "payload",
344
+ "type": "text",
345
+ "primaryKey": false,
346
+ "notNull": false,
347
+ "autoincrement": false
348
+ },
349
+ "priority": {
350
+ "name": "priority",
351
+ "type": "integer",
352
+ "primaryKey": false,
353
+ "notNull": true,
354
+ "autoincrement": false,
355
+ "default": 5
356
+ },
357
+ "created_at": {
358
+ "name": "created_at",
359
+ "type": "integer",
360
+ "primaryKey": false,
361
+ "notNull": true,
362
+ "autoincrement": false
363
+ },
364
+ "delivered_at": {
365
+ "name": "delivered_at",
366
+ "type": "integer",
367
+ "primaryKey": false,
368
+ "notNull": false,
369
+ "autoincrement": false
370
+ }
371
+ },
372
+ "indexes": {},
373
+ "foreignKeys": {},
374
+ "compositePrimaryKeys": {},
375
+ "uniqueConstraints": {},
376
+ "checkConstraints": {}
377
+ }
378
+ },
379
+ "views": {},
380
+ "enums": {},
381
+ "_meta": {
382
+ "schemas": {},
383
+ "tables": {},
384
+ "columns": {}
385
+ },
386
+ "internal": {
387
+ "indexes": {}
388
+ }
389
+ }
@@ -36,6 +36,13 @@
36
36
  "when": 1773965190659,
37
37
  "tag": "0004_familiar_zaladane",
38
38
  "breakpoints": true
39
+ },
40
+ {
41
+ "idx": 5,
42
+ "version": "6",
43
+ "when": 1774274478663,
44
+ "tag": "0005_strong_supernaut",
45
+ "breakpoints": true
39
46
  }
40
47
  ]
41
48
  }
@@ -55,3 +55,20 @@ export const cronJobs = sqliteTable('cron_jobs', {
55
55
 
56
56
  export type DbCronJob = typeof cronJobs.$inferSelect;
57
57
  export type NewCronJob = typeof cronJobs.$inferInsert;
58
+
59
+ export const executionLogs = sqliteTable('execution_logs', {
60
+ id: text('id').primaryKey(),
61
+ entityType: text('entity_type').notNull(), // 'route' | 'cron'
62
+ entityId: text('entity_id').notNull(),
63
+ action: text('action').notNull(), // 'delivery' | 'fire' | 'error' | 'skip'
64
+ subject: text('subject').notNull(),
65
+ detail: text('detail'), // nullable JSON — error or meta
66
+ success: integer('success', { mode: 'boolean' }).notNull().default(true),
67
+ createdAt: integer('created_at', { mode: 'timestamp_ms' }).notNull(),
68
+ }, (table) => [
69
+ index('execution_logs_entity_idx').on(table.entityType, table.entityId),
70
+ index('execution_logs_created_at_idx').on(table.createdAt),
71
+ ]);
72
+
73
+ export type DbExecutionLog = typeof executionLogs.$inferSelect;
74
+ export type NewExecutionLog = typeof executionLogs.$inferInsert;
@@ -0,0 +1,50 @@
1
+ import {
2
+ Controller, Get, Query,
3
+ BaseController,
4
+ type OneBunResponse,
5
+ } from '@onebun/core';
6
+ import { LogService } from './log.service';
7
+
8
+ @Controller('/api/logs')
9
+ export class LogController extends BaseController {
10
+ constructor(private logService: LogService) {
11
+ super();
12
+ }
13
+
14
+ @Get()
15
+ async getLogs(
16
+ @Query('entityType') entityType?: string,
17
+ @Query('entityId') entityId?: string,
18
+ @Query('limit') limitStr?: string,
19
+ @Query('offset') offsetStr?: string,
20
+ @Query('success') successStr?: string,
21
+ @Query('action') action?: string,
22
+ @Query('subject') subjectLike?: string,
23
+ ): Promise<OneBunResponse> {
24
+ if (!entityType || !entityId) {
25
+ return this.error('entityType and entityId are required', 400, 400);
26
+ }
27
+
28
+ const limit = Math.min(Number(limitStr) || 50, 200);
29
+ const offset = Number(offsetStr) || 0;
30
+ const success = successStr === 'true' ? true : successStr === 'false' ? false : undefined;
31
+
32
+ const result = await this.logService.getLogsForEntity(entityType, entityId, {
33
+ limit,
34
+ offset,
35
+ success,
36
+ action: action || undefined,
37
+ subjectLike: subjectLike || undefined,
38
+ });
39
+ return this.success(result);
40
+ }
41
+
42
+ @Get('/recent')
43
+ async getRecentLogs(
44
+ @Query('limit') limitStr?: string,
45
+ ): Promise<OneBunResponse> {
46
+ const limit = Math.min(Number(limitStr) || 20, 100);
47
+ const logs = await this.logService.getRecentLogs(limit);
48
+ return this.success(logs);
49
+ }
50
+ }
@@ -0,0 +1,11 @@
1
+ import { Module } from '@onebun/core';
2
+ import { LogRepository } from './log.repository';
3
+ import { LogService } from './log.service';
4
+ import { LogController } from './log.controller';
5
+
6
+ @Module({
7
+ controllers: [LogController],
8
+ providers: [LogRepository, LogService],
9
+ exports: [LogService],
10
+ })
11
+ export class LogModule {}
@@ -0,0 +1,78 @@
1
+ import { Service, BaseService } from '@onebun/core';
2
+ import { DrizzleService, eq, and, desc, sql } from '@onebun/drizzle';
3
+ import { executionLogs, type DbExecutionLog, type NewExecutionLog } from '../db/schema';
4
+
5
+ export interface LogFilters {
6
+ entityType: string;
7
+ entityId: string;
8
+ success?: boolean;
9
+ action?: string;
10
+ subjectLike?: string;
11
+ }
12
+
13
+ @Service()
14
+ export class LogRepository extends BaseService {
15
+ constructor(private db: DrizzleService) {
16
+ super();
17
+ }
18
+
19
+ async insert(log: NewExecutionLog): Promise<void> {
20
+ await this.db.insert(executionLogs).values(log);
21
+ }
22
+
23
+ private buildWhereConditions(filters: LogFilters) {
24
+ const conditions = [
25
+ eq(executionLogs.entityType, filters.entityType),
26
+ eq(executionLogs.entityId, filters.entityId),
27
+ ];
28
+ if (filters.success !== undefined) {
29
+ conditions.push(eq(executionLogs.success, filters.success));
30
+ }
31
+ if (filters.action) {
32
+ conditions.push(eq(executionLogs.action, filters.action));
33
+ }
34
+ if (filters.subjectLike) {
35
+ conditions.push(sql`${executionLogs.subject} LIKE ${'%' + filters.subjectLike + '%'}`);
36
+ }
37
+ return and(...conditions);
38
+ }
39
+
40
+ async findByEntity(
41
+ filters: LogFilters,
42
+ limit: number = 50,
43
+ offset: number = 0,
44
+ ): Promise<DbExecutionLog[]> {
45
+ return this.db
46
+ .select()
47
+ .from(executionLogs)
48
+ .where(this.buildWhereConditions(filters))
49
+ .orderBy(desc(executionLogs.createdAt))
50
+ .limit(limit)
51
+ .offset(offset) as any;
52
+ }
53
+
54
+ async countByEntity(filters: LogFilters): Promise<number> {
55
+ const result = await this.db
56
+ .select({ count: sql<number>`count(*)` })
57
+ .from(executionLogs)
58
+ .where(this.buildWhereConditions(filters));
59
+ return result[0]?.count ?? 0;
60
+ }
61
+
62
+ async findRecent(limit: number = 20): Promise<DbExecutionLog[]> {
63
+ return this.db
64
+ .select()
65
+ .from(executionLogs)
66
+ .orderBy(desc(executionLogs.createdAt))
67
+ .limit(limit) as any;
68
+ }
69
+
70
+ async deleteOlderThan(cutoffMs: number): Promise<number> {
71
+ const cutoff = new Date(Date.now() - cutoffMs);
72
+ const result = await this.db
73
+ .delete(executionLogs)
74
+ .where(sql`${executionLogs.createdAt} < ${cutoff.getTime()}`)
75
+ .returning();
76
+ return result.length;
77
+ }
78
+ }
@@ -0,0 +1,116 @@
1
+ import { Service, BaseService } from '@onebun/core';
2
+ import { LogRepository } from './log.repository';
3
+ import { ulid } from 'ulid';
4
+
5
+ const SEVEN_DAYS_MS = 7 * 24 * 60 * 60 * 1000;
6
+ const CLEANUP_INTERVAL_MS = 60 * 60 * 1000; // 1 hour
7
+
8
+ @Service()
9
+ export class LogService extends BaseService {
10
+ private cleanupTimer: ReturnType<typeof setInterval> | null = null;
11
+
12
+ constructor(private repo: LogRepository) {
13
+ super();
14
+ }
15
+
16
+ async onModuleInit(): Promise<void> {
17
+ this.cleanupTimer = setInterval(() => this.cleanup(), CLEANUP_INTERVAL_MS);
18
+ }
19
+
20
+ async onModuleDestroy(): Promise<void> {
21
+ if (this.cleanupTimer) {
22
+ clearInterval(this.cleanupTimer);
23
+ this.cleanupTimer = null;
24
+ }
25
+ }
26
+
27
+ async logDelivery(routeId: string, subject: string, detail?: string): Promise<void> {
28
+ try {
29
+ await this.repo.insert({
30
+ id: ulid(),
31
+ entityType: 'route',
32
+ entityId: routeId,
33
+ action: 'delivery',
34
+ subject,
35
+ detail: detail ?? null,
36
+ success: true,
37
+ createdAt: new Date(),
38
+ });
39
+ } catch (err) {
40
+ this.logger.error('Failed to write delivery log', err);
41
+ }
42
+ }
43
+
44
+ async logCronFire(jobId: string, subject: string, manual: boolean = false): Promise<void> {
45
+ try {
46
+ await this.repo.insert({
47
+ id: ulid(),
48
+ entityType: 'cron',
49
+ entityId: jobId,
50
+ action: 'fire',
51
+ subject,
52
+ detail: manual ? JSON.stringify({ manual: true }) : null,
53
+ success: true,
54
+ createdAt: new Date(),
55
+ });
56
+ } catch (err) {
57
+ this.logger.error('Failed to write cron fire log', err);
58
+ }
59
+ }
60
+
61
+ async logError(entityType: 'route' | 'cron', entityId: string, subject: string, error: unknown): Promise<void> {
62
+ try {
63
+ const detail = error instanceof Error
64
+ ? JSON.stringify({ message: error.message, stack: error.stack })
65
+ : JSON.stringify({ message: String(error) });
66
+
67
+ await this.repo.insert({
68
+ id: ulid(),
69
+ entityType,
70
+ entityId,
71
+ action: 'error',
72
+ subject,
73
+ detail,
74
+ success: false,
75
+ createdAt: new Date(),
76
+ });
77
+ } catch (err) {
78
+ this.logger.error('Failed to write error log', err);
79
+ }
80
+ }
81
+
82
+ async getLogsForEntity(
83
+ entityType: string,
84
+ entityId: string,
85
+ opts?: { limit?: number; offset?: number; success?: boolean; action?: string; subjectLike?: string },
86
+ ) {
87
+ const filters = { entityType, entityId, success: opts?.success, action: opts?.action, subjectLike: opts?.subjectLike };
88
+ const [logs, total] = await Promise.all([
89
+ this.repo.findByEntity(filters, opts?.limit, opts?.offset),
90
+ this.repo.countByEntity(filters),
91
+ ]);
92
+ return {
93
+ items: logs.map(l => ({ ...l, createdAt: l.createdAt.getTime() })),
94
+ total,
95
+ };
96
+ }
97
+
98
+ async getRecentLogs(limit?: number) {
99
+ const logs = await this.repo.findRecent(limit);
100
+ return logs.map(l => ({
101
+ ...l,
102
+ createdAt: l.createdAt.getTime(),
103
+ }));
104
+ }
105
+
106
+ private async cleanup(): Promise<void> {
107
+ try {
108
+ const deleted = await this.repo.deleteOlderThan(SEVEN_DAYS_MS);
109
+ if (deleted > 0) {
110
+ this.logger.info(`Cleaned up ${deleted} old execution logs`);
111
+ }
112
+ } catch (err) {
113
+ this.logger.error('Failed to cleanup execution logs', err);
114
+ }
115
+ }
116
+ }