@origintrail-official/dkg-node-ui 0.0.1-dev.1773506972.23bf9c0

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 (84) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +49 -0
  3. package/dist/api.d.ts +30 -0
  4. package/dist/api.d.ts.map +1 -0
  5. package/dist/api.js +805 -0
  6. package/dist/api.js.map +1 -0
  7. package/dist/chat-assistant.d.ts +68 -0
  8. package/dist/chat-assistant.d.ts.map +1 -0
  9. package/dist/chat-assistant.js +663 -0
  10. package/dist/chat-assistant.js.map +1 -0
  11. package/dist/chat-memory.d.ts +171 -0
  12. package/dist/chat-memory.d.ts.map +1 -0
  13. package/dist/chat-memory.js +985 -0
  14. package/dist/chat-memory.js.map +1 -0
  15. package/dist/chat-persistence-queue.d.ts +67 -0
  16. package/dist/chat-persistence-queue.d.ts.map +1 -0
  17. package/dist/chat-persistence-queue.js +245 -0
  18. package/dist/chat-persistence-queue.js.map +1 -0
  19. package/dist/db.d.ts +402 -0
  20. package/dist/db.d.ts.map +1 -0
  21. package/dist/db.js +887 -0
  22. package/dist/db.js.map +1 -0
  23. package/dist/gelf-push-worker.d.ts +67 -0
  24. package/dist/gelf-push-worker.d.ts.map +1 -0
  25. package/dist/gelf-push-worker.js +147 -0
  26. package/dist/gelf-push-worker.js.map +1 -0
  27. package/dist/index.d.ts +20 -0
  28. package/dist/index.d.ts.map +1 -0
  29. package/dist/index.js +12 -0
  30. package/dist/index.js.map +1 -0
  31. package/dist/llm/capability-resolver.d.ts +3 -0
  32. package/dist/llm/capability-resolver.d.ts.map +1 -0
  33. package/dist/llm/capability-resolver.js +21 -0
  34. package/dist/llm/capability-resolver.js.map +1 -0
  35. package/dist/llm/client.d.ts +23 -0
  36. package/dist/llm/client.d.ts.map +1 -0
  37. package/dist/llm/client.js +91 -0
  38. package/dist/llm/client.js.map +1 -0
  39. package/dist/llm/provider-adapter.d.ts +16 -0
  40. package/dist/llm/provider-adapter.d.ts.map +1 -0
  41. package/dist/llm/provider-adapter.js +199 -0
  42. package/dist/llm/provider-adapter.js.map +1 -0
  43. package/dist/llm/types.d.ts +64 -0
  44. package/dist/llm/types.d.ts.map +1 -0
  45. package/dist/llm/types.js +2 -0
  46. package/dist/llm/types.js.map +1 -0
  47. package/dist/metrics-collector.d.ts +36 -0
  48. package/dist/metrics-collector.d.ts.map +1 -0
  49. package/dist/metrics-collector.js +155 -0
  50. package/dist/metrics-collector.js.map +1 -0
  51. package/dist/operation-tracker.d.ts +43 -0
  52. package/dist/operation-tracker.d.ts.map +1 -0
  53. package/dist/operation-tracker.js +195 -0
  54. package/dist/operation-tracker.js.map +1 -0
  55. package/dist/structured-logger.d.ts +16 -0
  56. package/dist/structured-logger.d.ts.map +1 -0
  57. package/dist/structured-logger.js +41 -0
  58. package/dist/structured-logger.js.map +1 -0
  59. package/dist/telemetry.d.ts +35 -0
  60. package/dist/telemetry.d.ts.map +1 -0
  61. package/dist/telemetry.js +45 -0
  62. package/dist/telemetry.js.map +1 -0
  63. package/dist-ui/assets/3d-force-graph-nMUNmvtB.js +964 -0
  64. package/dist-ui/assets/AgentHub-XKCM9uYQ.js +65 -0
  65. package/dist-ui/assets/AppHost-DoLIi89g.js +1 -0
  66. package/dist-ui/assets/Apps-Cc8HfqfD.js +1 -0
  67. package/dist-ui/assets/Dashboard-D5q6MK78.js +2 -0
  68. package/dist-ui/assets/Explorer-B80RVksc.js +64 -0
  69. package/dist-ui/assets/N3Parser-Q_-1ZY5E.js +7 -0
  70. package/dist-ui/assets/Settings-CG7-7GM-.js +71 -0
  71. package/dist-ui/assets/hooks-BLTFNmyP.js +1 -0
  72. package/dist-ui/assets/index-8_35CUX2.js +192 -0
  73. package/dist-ui/assets/index-CKZq_ZB-.css +1 -0
  74. package/dist-ui/assets/index-DH-l6lM0.js +76 -0
  75. package/dist-ui/assets/jsonld-32FQRO67-DhbO8O6B.js +2 -0
  76. package/dist-ui/assets/jsonld-BFI4wECl.js +62 -0
  77. package/dist-ui/assets/ntriples-ZWBY2WET-nIpilpjf.js +1 -0
  78. package/dist-ui/assets/ordinal-DIohFSkg.js +1 -0
  79. package/dist-ui/assets/renderer-3d-2EVDZII7-DsxBsJvs.js +2 -0
  80. package/dist-ui/assets/three.module-uCjFke6H.js +4019 -0
  81. package/dist-ui/assets/turtle-JJPK7LJ5-zezDJZEp.js +1 -0
  82. package/dist-ui/favicon.png +0 -0
  83. package/dist-ui/index.html +14 -0
  84. package/package.json +58 -0
package/dist/db.js ADDED
@@ -0,0 +1,887 @@
1
+ import Database from 'better-sqlite3';
2
+ import { join } from 'node:path';
3
+ const SCHEMA_VERSION = 6;
4
+ const DEFAULT_RETENTION_DAYS = 90;
5
+ export class DashboardDB {
6
+ db;
7
+ dataDir;
8
+ retentionDays;
9
+ constructor(opts) {
10
+ this.dataDir = opts.dataDir;
11
+ this.retentionDays = opts.retentionDays ?? DEFAULT_RETENTION_DAYS;
12
+ const dbPath = join(opts.dataDir, 'node-ui.db');
13
+ this.db = new Database(dbPath);
14
+ this.db.pragma('journal_mode = WAL');
15
+ this.db.pragma('synchronous = NORMAL');
16
+ this.migrate();
17
+ this.prune();
18
+ }
19
+ getRetentionDays() { return this.retentionDays; }
20
+ setRetentionDays(days) {
21
+ this.retentionDays = Math.max(1, Math.min(365, days));
22
+ this.db.prepare("INSERT OR REPLACE INTO settings (key, value) VALUES ('retentionDays', ?)").run(String(this.retentionDays));
23
+ }
24
+ migrate() {
25
+ const version = this.db.pragma('user_version', { simple: true });
26
+ if (version >= SCHEMA_VERSION)
27
+ return;
28
+ if (version < 1) {
29
+ this.db.exec(`
30
+ CREATE TABLE IF NOT EXISTS metric_snapshots (
31
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
32
+ ts INTEGER NOT NULL,
33
+ cpu_percent REAL,
34
+ mem_used_bytes INTEGER,
35
+ mem_total_bytes INTEGER,
36
+ disk_used_bytes INTEGER,
37
+ disk_total_bytes INTEGER,
38
+ heap_used_bytes INTEGER,
39
+ uptime_seconds INTEGER,
40
+ peer_count INTEGER,
41
+ direct_peers INTEGER,
42
+ relayed_peers INTEGER,
43
+ mesh_peers INTEGER,
44
+ paranet_count INTEGER,
45
+ total_triples INTEGER,
46
+ total_kcs INTEGER,
47
+ total_kas INTEGER,
48
+ store_bytes INTEGER,
49
+ confirmed_kcs INTEGER,
50
+ tentative_kcs INTEGER,
51
+ rpc_latency_ms INTEGER,
52
+ rpc_healthy INTEGER
53
+ );
54
+ CREATE INDEX IF NOT EXISTS idx_snapshots_ts ON metric_snapshots(ts);
55
+
56
+ CREATE TABLE IF NOT EXISTS operations (
57
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
58
+ operation_id TEXT NOT NULL,
59
+ operation_name TEXT NOT NULL,
60
+ started_at INTEGER NOT NULL,
61
+ duration_ms INTEGER,
62
+ status TEXT DEFAULT 'in_progress',
63
+ peer_id TEXT,
64
+ paranet_id TEXT,
65
+ triple_count INTEGER,
66
+ error_message TEXT,
67
+ details TEXT
68
+ );
69
+ CREATE INDEX IF NOT EXISTS idx_ops_operation_id ON operations(operation_id);
70
+ CREATE INDEX IF NOT EXISTS idx_ops_started_at ON operations(started_at);
71
+ CREATE INDEX IF NOT EXISTS idx_ops_name ON operations(operation_name);
72
+
73
+ CREATE TABLE IF NOT EXISTS logs (
74
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
75
+ ts INTEGER NOT NULL,
76
+ level TEXT NOT NULL,
77
+ operation_name TEXT,
78
+ operation_id TEXT,
79
+ module TEXT,
80
+ message TEXT NOT NULL
81
+ );
82
+ CREATE INDEX IF NOT EXISTS idx_logs_ts ON logs(ts);
83
+ CREATE INDEX IF NOT EXISTS idx_logs_operation_id ON logs(operation_id);
84
+ CREATE INDEX IF NOT EXISTS idx_logs_level ON logs(level);
85
+
86
+ CREATE VIRTUAL TABLE IF NOT EXISTS logs_fts USING fts5(
87
+ message, content=logs, content_rowid=id
88
+ );
89
+
90
+ CREATE TRIGGER IF NOT EXISTS logs_ai AFTER INSERT ON logs BEGIN
91
+ INSERT INTO logs_fts(rowid, message) VALUES (new.id, new.message);
92
+ END;
93
+ CREATE TRIGGER IF NOT EXISTS logs_ad AFTER DELETE ON logs BEGIN
94
+ INSERT INTO logs_fts(logs_fts, rowid, message) VALUES('delete', old.id, old.message);
95
+ END;
96
+
97
+ CREATE TABLE IF NOT EXISTS query_history (
98
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
99
+ ts INTEGER NOT NULL,
100
+ sparql TEXT NOT NULL,
101
+ duration_ms INTEGER,
102
+ result_count INTEGER,
103
+ error TEXT
104
+ );
105
+ CREATE INDEX IF NOT EXISTS idx_qhist_ts ON query_history(ts);
106
+
107
+ CREATE TABLE IF NOT EXISTS saved_queries (
108
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
109
+ name TEXT NOT NULL,
110
+ description TEXT,
111
+ sparql TEXT NOT NULL,
112
+ created_at INTEGER NOT NULL,
113
+ updated_at INTEGER NOT NULL
114
+ );
115
+ `);
116
+ }
117
+ if (version < 2) {
118
+ this.db.exec(`
119
+ CREATE TABLE IF NOT EXISTS operation_phases (
120
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
121
+ operation_id TEXT NOT NULL,
122
+ phase TEXT NOT NULL,
123
+ started_at INTEGER NOT NULL,
124
+ duration_ms INTEGER,
125
+ status TEXT DEFAULT 'in_progress',
126
+ details TEXT
127
+ );
128
+ CREATE INDEX IF NOT EXISTS idx_phases_op ON operation_phases(operation_id);
129
+
130
+ ALTER TABLE operations ADD COLUMN gas_used INTEGER;
131
+ ALTER TABLE operations ADD COLUMN gas_price_gwei REAL;
132
+ ALTER TABLE operations ADD COLUMN gas_cost_eth REAL;
133
+ ALTER TABLE operations ADD COLUMN trac_cost REAL;
134
+ ALTER TABLE operations ADD COLUMN tx_hash TEXT;
135
+ ALTER TABLE operations ADD COLUMN chain_id INTEGER;
136
+ `);
137
+ }
138
+ if (version < 3) {
139
+ this.db.exec(`
140
+ CREATE TABLE IF NOT EXISTS chat_messages (
141
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
142
+ ts INTEGER NOT NULL,
143
+ direction TEXT NOT NULL,
144
+ peer TEXT NOT NULL,
145
+ peer_name TEXT,
146
+ text TEXT NOT NULL,
147
+ delivered INTEGER
148
+ );
149
+ CREATE INDEX IF NOT EXISTS idx_chat_ts ON chat_messages(ts);
150
+ CREATE INDEX IF NOT EXISTS idx_chat_peer ON chat_messages(peer);
151
+ `);
152
+ }
153
+ if (version < 4) {
154
+ this.db.exec(`
155
+ CREATE TABLE IF NOT EXISTS chat_persistence_jobs (
156
+ turn_id TEXT PRIMARY KEY,
157
+ session_id TEXT NOT NULL,
158
+ user_message TEXT NOT NULL,
159
+ assistant_reply TEXT NOT NULL,
160
+ tool_calls_json TEXT,
161
+ status TEXT NOT NULL,
162
+ attempts INTEGER NOT NULL DEFAULT 0,
163
+ max_attempts INTEGER NOT NULL DEFAULT 3,
164
+ next_attempt_at INTEGER NOT NULL,
165
+ queued_at INTEGER NOT NULL,
166
+ updated_at INTEGER NOT NULL,
167
+ store_ms INTEGER,
168
+ error_message TEXT
169
+ );
170
+ CREATE INDEX IF NOT EXISTS idx_chat_persist_status_next
171
+ ON chat_persistence_jobs(status, next_attempt_at);
172
+ CREATE INDEX IF NOT EXISTS idx_chat_persist_session
173
+ ON chat_persistence_jobs(session_id);
174
+ `);
175
+ }
176
+ if (version < 5) {
177
+ this.db.exec(`
178
+ CREATE TABLE IF NOT EXISTS notifications (
179
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
180
+ ts INTEGER NOT NULL,
181
+ type TEXT NOT NULL,
182
+ title TEXT NOT NULL,
183
+ message TEXT NOT NULL,
184
+ source TEXT,
185
+ peer TEXT,
186
+ read INTEGER NOT NULL DEFAULT 0,
187
+ meta TEXT
188
+ );
189
+ CREATE INDEX IF NOT EXISTS idx_notif_ts ON notifications(ts);
190
+ CREATE INDEX IF NOT EXISTS idx_notif_read ON notifications(read);
191
+ `);
192
+ }
193
+ if (version < 6) {
194
+ this.db.exec(`
195
+ CREATE TABLE IF NOT EXISTS settings (
196
+ key TEXT PRIMARY KEY,
197
+ value TEXT NOT NULL
198
+ );
199
+ `);
200
+ }
201
+ this.db.pragma(`user_version = ${SCHEMA_VERSION}`);
202
+ const savedRetention = this.db.prepare("SELECT value FROM settings WHERE key = 'retentionDays'").get();
203
+ if (savedRetention) {
204
+ const days = Number(savedRetention.value);
205
+ if (Number.isFinite(days) && days >= 1 && days <= 365) {
206
+ this.retentionDays = days;
207
+ }
208
+ }
209
+ }
210
+ prune() {
211
+ const cutoff = Date.now() - this.retentionDays * 86_400_000;
212
+ this.db.exec(`DELETE FROM metric_snapshots WHERE ts < ${cutoff}`);
213
+ this.db.exec(`DELETE FROM operation_phases WHERE started_at < ${cutoff}`);
214
+ this.db.exec(`DELETE FROM operations WHERE started_at < ${cutoff}`);
215
+ this.db.exec(`DELETE FROM logs WHERE ts < ${cutoff}`);
216
+ this.db.exec(`DELETE FROM query_history WHERE ts < ${cutoff}`);
217
+ this.db.exec(`DELETE FROM chat_messages WHERE ts < ${cutoff}`);
218
+ this.db.exec(`DELETE FROM chat_persistence_jobs WHERE updated_at < ${cutoff} AND status IN ('stored', 'failed')`);
219
+ this.db.exec(`DELETE FROM notifications WHERE ts < ${cutoff}`);
220
+ }
221
+ // --- Prepared statements (lazy-initialized) ---
222
+ _stmts = {};
223
+ stmt(key, sql) {
224
+ if (!this._stmts[key])
225
+ this._stmts[key] = this.db.prepare(sql);
226
+ return this._stmts[key];
227
+ }
228
+ // --- Metric snapshots ---
229
+ insertSnapshot(snap) {
230
+ this.stmt('insertSnapshot', `
231
+ INSERT INTO metric_snapshots (
232
+ ts, cpu_percent, mem_used_bytes, mem_total_bytes,
233
+ disk_used_bytes, disk_total_bytes, heap_used_bytes, uptime_seconds,
234
+ peer_count, direct_peers, relayed_peers, mesh_peers, paranet_count,
235
+ total_triples, total_kcs, total_kas, store_bytes,
236
+ confirmed_kcs, tentative_kcs, rpc_latency_ms, rpc_healthy
237
+ ) VALUES (
238
+ @ts, @cpu_percent, @mem_used_bytes, @mem_total_bytes,
239
+ @disk_used_bytes, @disk_total_bytes, @heap_used_bytes, @uptime_seconds,
240
+ @peer_count, @direct_peers, @relayed_peers, @mesh_peers, @paranet_count,
241
+ @total_triples, @total_kcs, @total_kas, @store_bytes,
242
+ @confirmed_kcs, @tentative_kcs, @rpc_latency_ms, @rpc_healthy
243
+ )
244
+ `).run(snap);
245
+ }
246
+ getLatestSnapshot() {
247
+ return this.db.prepare('SELECT * FROM metric_snapshots ORDER BY ts DESC LIMIT 1').get();
248
+ }
249
+ getSnapshotHistory(from, to, maxPoints = 500) {
250
+ const total = this.db.prepare('SELECT COUNT(*) as c FROM metric_snapshots WHERE ts >= ? AND ts <= ?').get(from, to);
251
+ if (total.c <= maxPoints) {
252
+ return this.db.prepare('SELECT * FROM metric_snapshots WHERE ts >= ? AND ts <= ? ORDER BY ts').all(from, to);
253
+ }
254
+ const step = Math.ceil(total.c / maxPoints);
255
+ return this.db.prepare(`
256
+ SELECT * FROM (
257
+ SELECT *, ROW_NUMBER() OVER (ORDER BY ts) as rn
258
+ FROM metric_snapshots WHERE ts >= ? AND ts <= ?
259
+ ) WHERE rn % ? = 0 ORDER BY ts
260
+ `).all(from, to, step);
261
+ }
262
+ // --- Operations ---
263
+ insertOperation(op) {
264
+ this.stmt('insertOp', `
265
+ INSERT INTO operations (operation_id, operation_name, started_at, status, peer_id, paranet_id, details)
266
+ VALUES (@operation_id, @operation_name, @started_at, 'in_progress', @peer_id, @paranet_id, @details)
267
+ `).run({
268
+ operation_id: op.operation_id,
269
+ operation_name: op.operation_name,
270
+ started_at: op.started_at,
271
+ peer_id: op.peer_id ?? null,
272
+ paranet_id: op.paranet_id ?? null,
273
+ details: op.details ?? null,
274
+ });
275
+ }
276
+ completeOperation(op) {
277
+ this.stmt('completeOp', `
278
+ UPDATE operations SET status = 'success', duration_ms = @duration_ms,
279
+ triple_count = @triple_count, details = COALESCE(@details, details)
280
+ WHERE operation_id = @operation_id AND status = 'in_progress'
281
+ `).run({
282
+ operation_id: op.operation_id,
283
+ duration_ms: op.duration_ms,
284
+ triple_count: op.triple_count ?? null,
285
+ details: op.details ?? null,
286
+ });
287
+ }
288
+ failOperation(op) {
289
+ this.stmt('failOp', `
290
+ UPDATE operations SET status = 'error', duration_ms = @duration_ms,
291
+ error_message = @error_message
292
+ WHERE operation_id = @operation_id AND status = 'in_progress'
293
+ `).run(op);
294
+ }
295
+ getOperations(opts = {}) {
296
+ const wheres = [];
297
+ const params = [];
298
+ if (opts.name) {
299
+ wheres.push('operation_name = ?');
300
+ params.push(opts.name);
301
+ }
302
+ if (opts.status) {
303
+ wheres.push('status = ?');
304
+ params.push(opts.status);
305
+ }
306
+ if (opts.operationId) {
307
+ wheres.push('operation_id = ?');
308
+ params.push(opts.operationId);
309
+ }
310
+ if (opts.from) {
311
+ wheres.push('started_at >= ?');
312
+ params.push(opts.from);
313
+ }
314
+ if (opts.to) {
315
+ wheres.push('started_at <= ?');
316
+ params.push(opts.to);
317
+ }
318
+ const where = wheres.length ? `WHERE ${wheres.join(' AND ')}` : '';
319
+ const limit = opts.limit ?? 100;
320
+ const offset = opts.offset ?? 0;
321
+ const total = this.db.prepare(`SELECT COUNT(*) as c FROM operations ${where}`).get(...params).c;
322
+ const operations = this.db.prepare(`SELECT * FROM operations ${where} ORDER BY started_at DESC LIMIT ? OFFSET ?`).all(...params, limit, offset);
323
+ return { operations, total };
324
+ }
325
+ getOperationsWithPhases(opts = {}) {
326
+ const { operations, total } = this.getOperations(opts);
327
+ if (operations.length === 0)
328
+ return { operations: [], total };
329
+ const ids = operations.map(o => o.operation_id);
330
+ const placeholders = ids.map(() => '?').join(',');
331
+ const allPhases = this.db.prepare(`SELECT * FROM operation_phases WHERE operation_id IN (${placeholders}) ORDER BY started_at`).all(...ids);
332
+ const phaseMap = new Map();
333
+ for (const p of allPhases) {
334
+ const arr = phaseMap.get(p.operation_id) ?? [];
335
+ arr.push(p);
336
+ phaseMap.set(p.operation_id, arr);
337
+ }
338
+ return {
339
+ operations: operations.map(o => ({ ...o, phases: phaseMap.get(o.operation_id) ?? [] })),
340
+ total,
341
+ };
342
+ }
343
+ getErrorHotspots(periodMs = 7 * 86_400_000) {
344
+ const cutoff = Date.now() - periodMs;
345
+ return this.db.prepare(`
346
+ SELECT
347
+ p.phase,
348
+ o.operation_name,
349
+ COUNT(*) as error_count,
350
+ (SELECT p2.details FROM operation_phases p2
351
+ JOIN operations o2 ON o2.operation_id = p2.operation_id
352
+ WHERE p2.phase = p.phase AND o2.operation_name = o.operation_name
353
+ AND p2.status = 'error' AND p2.started_at >= ?
354
+ ORDER BY p2.started_at DESC LIMIT 1) as last_error,
355
+ MAX(p.started_at) as last_occurred
356
+ FROM operation_phases p
357
+ JOIN operations o ON o.operation_id = p.operation_id
358
+ WHERE p.status = 'error' AND p.started_at >= ?
359
+ GROUP BY p.phase, o.operation_name
360
+ ORDER BY error_count DESC
361
+ `).all(cutoff, cutoff);
362
+ }
363
+ getFailedOperations(opts = {}) {
364
+ const cutoff = Date.now() - (opts.periodMs ?? 7 * 86_400_000);
365
+ const limit = opts.limit ?? 50;
366
+ let where = 'p.status = ? AND p.started_at >= ?';
367
+ const params = ['error', cutoff];
368
+ if (opts.phase) {
369
+ where += ' AND p.phase = ?';
370
+ params.push(opts.phase);
371
+ }
372
+ if (opts.operationName) {
373
+ where += ' AND o.operation_name = ?';
374
+ params.push(opts.operationName);
375
+ }
376
+ if (opts.q) {
377
+ where += ' AND (p.details LIKE ? OR o.operation_id LIKE ? OR o.error_message LIKE ?)';
378
+ const like = `%${opts.q}%`;
379
+ params.push(like, like, like);
380
+ }
381
+ params.push(limit);
382
+ const rows = this.db.prepare(`
383
+ SELECT
384
+ o.*,
385
+ p.phase AS phase,
386
+ p.details AS phase_error,
387
+ p.started_at AS phase_started_at
388
+ FROM operation_phases p
389
+ JOIN operations o ON o.operation_id = p.operation_id
390
+ WHERE ${where}
391
+ ORDER BY p.started_at DESC
392
+ LIMIT ?
393
+ `).all(...params);
394
+ const operations = rows.map(row => {
395
+ const logs = this.db.prepare('SELECT * FROM logs WHERE operation_id = ? ORDER BY ts DESC LIMIT 20').all(row.operation_id);
396
+ logs.reverse();
397
+ return { ...row, logs };
398
+ });
399
+ return { operations };
400
+ }
401
+ getOperation(operationId) {
402
+ const operation = this.db.prepare('SELECT * FROM operations WHERE operation_id = ?').get(operationId);
403
+ const logs = this.db.prepare('SELECT * FROM logs WHERE operation_id = ? ORDER BY ts').all(operationId);
404
+ const phases = this.db.prepare('SELECT * FROM operation_phases WHERE operation_id = ? ORDER BY started_at').all(operationId);
405
+ return { operation, logs, phases };
406
+ }
407
+ // --- Operation phases ---
408
+ insertPhase(op) {
409
+ this.stmt('insertPhase', `
410
+ INSERT INTO operation_phases (operation_id, phase, started_at, status)
411
+ VALUES (@operation_id, @phase, @started_at, 'in_progress')
412
+ `).run(op);
413
+ }
414
+ completePhase(op) {
415
+ this.stmt('completePhase', `
416
+ UPDATE operation_phases SET status = 'success', duration_ms = @duration_ms
417
+ WHERE operation_id = @operation_id AND phase = @phase AND status = 'in_progress'
418
+ `).run(op);
419
+ }
420
+ failPhase(op) {
421
+ this.stmt('failPhase', `
422
+ UPDATE operation_phases SET status = 'error', duration_ms = @duration_ms,
423
+ details = @error_message
424
+ WHERE operation_id = @operation_id AND phase = @phase AND status = 'in_progress'
425
+ `).run(op);
426
+ }
427
+ failAllPhases(op) {
428
+ this.stmt('failAllPhases', `
429
+ UPDATE operation_phases SET status = 'error', duration_ms = @duration_ms,
430
+ details = @error_message
431
+ WHERE operation_id = @operation_id AND status = 'in_progress'
432
+ `).run(op);
433
+ }
434
+ // --- Operation cost & tx ---
435
+ setOperationCost(op) {
436
+ this.stmt('setCost', `
437
+ UPDATE operations SET
438
+ gas_used = COALESCE(@gas_used, gas_used),
439
+ gas_price_gwei = COALESCE(@gas_price_gwei, gas_price_gwei),
440
+ gas_cost_eth = COALESCE(@gas_cost_eth, gas_cost_eth),
441
+ trac_cost = COALESCE(@trac_cost, trac_cost),
442
+ tx_hash = COALESCE(@tx_hash, tx_hash),
443
+ chain_id = COALESCE(@chain_id, chain_id)
444
+ WHERE operation_id = @operation_id
445
+ `).run({
446
+ operation_id: op.operation_id,
447
+ gas_used: op.gas_used ?? null,
448
+ gas_price_gwei: op.gas_price_gwei ?? null,
449
+ gas_cost_eth: op.gas_cost_eth ?? null,
450
+ trac_cost: op.trac_cost ?? null,
451
+ tx_hash: op.tx_hash ?? null,
452
+ chain_id: op.chain_id ?? null,
453
+ });
454
+ }
455
+ // --- Operation stats ---
456
+ getOperationStats(opts) {
457
+ const cutoff = Date.now() - opts.periodMs;
458
+ const nameFilter = opts.name ? 'AND operation_name = ?' : '';
459
+ const params = [cutoff];
460
+ if (opts.name)
461
+ params.push(opts.name);
462
+ const summaryRow = this.db.prepare(`
463
+ SELECT
464
+ COUNT(*) as totalCount,
465
+ SUM(CASE WHEN status = 'success' THEN 1 ELSE 0 END) as successCount,
466
+ SUM(CASE WHEN status = 'error' THEN 1 ELSE 0 END) as errorCount,
467
+ AVG(CASE WHEN status = 'success' THEN duration_ms END) as avgDurationMs,
468
+ AVG(gas_cost_eth) as avgGasCostEth,
469
+ SUM(gas_cost_eth) as totalGasCostEth,
470
+ AVG(trac_cost) as avgTracCost,
471
+ SUM(trac_cost) as totalTracCost
472
+ FROM operations WHERE started_at >= ? ${nameFilter}
473
+ `).get(...params);
474
+ const summary = {
475
+ totalCount: summaryRow.totalCount ?? 0,
476
+ successCount: summaryRow.successCount ?? 0,
477
+ errorCount: summaryRow.errorCount ?? 0,
478
+ successRate: summaryRow.totalCount > 0 ? (summaryRow.successCount ?? 0) / summaryRow.totalCount : 0,
479
+ avgDurationMs: summaryRow.avgDurationMs ?? 0,
480
+ avgGasCostEth: summaryRow.avgGasCostEth ?? 0,
481
+ totalGasCostEth: summaryRow.totalGasCostEth ?? 0,
482
+ avgTracCost: summaryRow.avgTracCost ?? 0,
483
+ totalTracCost: summaryRow.totalTracCost ?? 0,
484
+ };
485
+ const bucketSize = opts.bucketMs;
486
+ const tsParams = [bucketSize, cutoff];
487
+ if (opts.name)
488
+ tsParams.push(opts.name);
489
+ const timeSeries = this.db.prepare(`
490
+ SELECT
491
+ (CAST(started_at / ? AS INTEGER) * ?) as bucket,
492
+ COUNT(*) as count,
493
+ SUM(CASE WHEN status = 'success' THEN 1 ELSE 0 END) as successCount,
494
+ AVG(CASE WHEN status = 'success' THEN duration_ms END) as avgDurationMs,
495
+ AVG(gas_cost_eth) as avgGasCostEth,
496
+ SUM(gas_cost_eth) as totalGasCostEth
497
+ FROM operations
498
+ WHERE started_at >= ? ${nameFilter}
499
+ GROUP BY bucket ORDER BY bucket
500
+ `).all(bucketSize, bucketSize, ...params);
501
+ return {
502
+ summary,
503
+ timeSeries: timeSeries.map((r) => ({
504
+ bucket: r.bucket,
505
+ count: r.count,
506
+ successRate: r.count > 0 ? r.successCount / r.count : 0,
507
+ avgDurationMs: r.avgDurationMs ?? 0,
508
+ avgGasCostEth: r.avgGasCostEth ?? 0,
509
+ totalGasCostEth: r.totalGasCostEth ?? 0,
510
+ })),
511
+ };
512
+ }
513
+ // --- Per-type time series ---
514
+ getPerTypeTimeSeries(opts) {
515
+ const cutoff = Date.now() - opts.periodMs;
516
+ const rows = this.db.prepare(`
517
+ SELECT
518
+ (CAST(started_at / ? AS INTEGER) * ?) as bucket,
519
+ operation_name as type,
520
+ COUNT(*) as count,
521
+ AVG(CASE WHEN status = 'success' THEN duration_ms END) as avgMs,
522
+ SUM(CASE WHEN status = 'success' THEN 1 ELSE 0 END) as successCount,
523
+ SUM(gas_cost_eth) as gasCostEth
524
+ FROM operations WHERE started_at >= ?
525
+ GROUP BY bucket, operation_name ORDER BY bucket
526
+ `).all(opts.bucketMs, opts.bucketMs, cutoff);
527
+ const bucketSet = new Set();
528
+ const typeSet = new Set();
529
+ for (const r of rows) {
530
+ bucketSet.add(r.bucket);
531
+ typeSet.add(r.type);
532
+ }
533
+ const buckets = [...bucketSet].sort((a, b) => a - b);
534
+ const types = [...typeSet].sort();
535
+ const byBucketType = new Map();
536
+ for (const r of rows)
537
+ byBucketType.set(`${r.bucket}:${r.type}`, r);
538
+ const series = {};
539
+ for (const t of types) {
540
+ series[t] = buckets.map(b => {
541
+ const r = byBucketType.get(`${b}:${t}`);
542
+ return {
543
+ count: r?.count ?? 0,
544
+ avgMs: r?.avgMs ?? 0,
545
+ successRate: r ? (r.count > 0 ? r.successCount / r.count : 0) : 0,
546
+ gasCostEth: r?.gasCostEth ?? 0,
547
+ };
548
+ });
549
+ }
550
+ return { buckets, types, series };
551
+ }
552
+ // --- Success rates by operation type ---
553
+ getSuccessRatesByType(periodMs) {
554
+ const cutoff = Date.now() - periodMs;
555
+ return this.db.prepare(`
556
+ SELECT
557
+ operation_name as type,
558
+ COUNT(*) as total,
559
+ SUM(CASE WHEN status = 'success' THEN 1 ELSE 0 END) as success,
560
+ SUM(CASE WHEN status = 'error' THEN 1 ELSE 0 END) as error,
561
+ AVG(CASE WHEN status = 'success' THEN duration_ms END) as avgMs
562
+ FROM operations WHERE started_at >= ?
563
+ GROUP BY operation_name ORDER BY total DESC
564
+ `).all(cutoff).map(r => ({
565
+ ...r,
566
+ rate: r.total > 0 ? r.success / r.total : 0,
567
+ avgMs: r.avgMs ?? 0,
568
+ }));
569
+ }
570
+ // --- Spending summary ---
571
+ getSpendingSummary() {
572
+ const periods = [
573
+ { label: '24h', ms: 86_400_000 },
574
+ { label: '7d', ms: 7 * 86_400_000 },
575
+ { label: '30d', ms: 30 * 86_400_000 },
576
+ { label: 'all', ms: Date.now() },
577
+ ];
578
+ const now = Date.now();
579
+ const results = { periods: [] };
580
+ for (const p of periods) {
581
+ const cutoff = now - p.ms;
582
+ const row = this.db.prepare(`
583
+ SELECT
584
+ COUNT(*) as publishCount,
585
+ SUM(CASE WHEN status = 'success' THEN 1 ELSE 0 END) as successCount,
586
+ COALESCE(SUM(gas_cost_eth), 0) as totalGasEth,
587
+ COALESCE(SUM(trac_cost), 0) as totalTrac,
588
+ COALESCE(AVG(gas_cost_eth), 0) as avgGasEth,
589
+ COALESCE(AVG(trac_cost), 0) as avgTrac
590
+ FROM operations
591
+ WHERE operation_name = 'publish' AND started_at >= ?
592
+ `).get(cutoff);
593
+ results.periods.push({
594
+ label: p.label,
595
+ publishCount: row.publishCount ?? 0,
596
+ successCount: row.successCount ?? 0,
597
+ totalGasEth: row.totalGasEth,
598
+ totalTrac: row.totalTrac,
599
+ avgGasEth: row.avgGasEth,
600
+ avgTrac: row.avgTrac,
601
+ });
602
+ }
603
+ return results;
604
+ }
605
+ // --- Chat messages ---
606
+ insertChatMessage(msg) {
607
+ this.stmt('insertChat', `
608
+ INSERT INTO chat_messages (ts, direction, peer, peer_name, text, delivered)
609
+ VALUES (@ts, @direction, @peer, @peer_name, @text, @delivered)
610
+ `).run({
611
+ ts: msg.ts,
612
+ direction: msg.direction,
613
+ peer: msg.peer,
614
+ peer_name: msg.peerName ?? null,
615
+ text: msg.text,
616
+ delivered: msg.delivered == null ? null : msg.delivered ? 1 : 0,
617
+ });
618
+ }
619
+ getChatMessages(opts = {}) {
620
+ let sql = 'SELECT * FROM chat_messages WHERE 1=1';
621
+ const params = [];
622
+ if (opts.since) {
623
+ sql += ' AND ts > ?';
624
+ params.push(opts.since);
625
+ }
626
+ if (opts.peer) {
627
+ sql += ' AND peer = ?';
628
+ params.push(opts.peer);
629
+ }
630
+ sql += ' ORDER BY ts DESC LIMIT ?';
631
+ params.push(opts.limit ?? 200);
632
+ return this.db.prepare(sql).all(...params).reverse();
633
+ }
634
+ // --- Chat persistence jobs ---
635
+ getChatPersistenceJob(turnId) {
636
+ return this.db.prepare('SELECT * FROM chat_persistence_jobs WHERE turn_id = ?').get(turnId);
637
+ }
638
+ insertChatPersistenceJob(job) {
639
+ this.stmt('insertChatPersistenceJob', `
640
+ INSERT INTO chat_persistence_jobs (
641
+ turn_id, session_id, user_message, assistant_reply, tool_calls_json,
642
+ status, attempts, max_attempts, next_attempt_at, queued_at, updated_at,
643
+ store_ms, error_message
644
+ ) VALUES (
645
+ @turn_id, @session_id, @user_message, @assistant_reply, @tool_calls_json,
646
+ @status, @attempts, @max_attempts, @next_attempt_at, @queued_at, @updated_at,
647
+ @store_ms, @error_message
648
+ )
649
+ `).run({
650
+ ...job,
651
+ tool_calls_json: job.tool_calls_json ?? null,
652
+ store_ms: job.store_ms ?? null,
653
+ error_message: job.error_message ?? null,
654
+ });
655
+ }
656
+ markChatPersistenceInProgress(turnId, attempts, updatedAt) {
657
+ this.stmt('markChatPersistenceInProgress', `
658
+ UPDATE chat_persistence_jobs
659
+ SET status = 'in_progress', attempts = ?, updated_at = ?, error_message = NULL
660
+ WHERE turn_id = ?
661
+ `).run(attempts, updatedAt, turnId);
662
+ }
663
+ markChatPersistenceStored(turnId, storeMs, updatedAt) {
664
+ this.stmt('markChatPersistenceStored', `
665
+ UPDATE chat_persistence_jobs
666
+ SET status = 'stored', store_ms = ?, updated_at = ?, error_message = NULL
667
+ WHERE turn_id = ?
668
+ `).run(storeMs, updatedAt, turnId);
669
+ }
670
+ markChatPersistencePendingRetry(turnId, attempts, nextAttemptAt, updatedAt, errorMessage) {
671
+ this.stmt('markChatPersistencePendingRetry', `
672
+ UPDATE chat_persistence_jobs
673
+ SET status = 'pending', attempts = ?, next_attempt_at = ?, updated_at = ?, error_message = ?
674
+ WHERE turn_id = ?
675
+ `).run(attempts, nextAttemptAt, updatedAt, errorMessage, turnId);
676
+ }
677
+ markChatPersistenceFailed(turnId, attempts, updatedAt, errorMessage) {
678
+ this.stmt('markChatPersistenceFailed', `
679
+ UPDATE chat_persistence_jobs
680
+ SET status = 'failed', attempts = ?, updated_at = ?, error_message = ?
681
+ WHERE turn_id = ?
682
+ `).run(attempts, updatedAt, errorMessage, turnId);
683
+ }
684
+ recoverInProgressChatPersistenceJobs(now) {
685
+ this.stmt('recoverInProgressChatPersistenceJobs', `
686
+ UPDATE chat_persistence_jobs
687
+ SET status = 'pending', next_attempt_at = ?, updated_at = ?
688
+ WHERE status = 'in_progress'
689
+ `).run(now, now);
690
+ }
691
+ getRunnableChatPersistenceJobs(now, limit = 10) {
692
+ return this.db.prepare(`
693
+ SELECT * FROM chat_persistence_jobs
694
+ WHERE status = 'pending' AND next_attempt_at <= ?
695
+ ORDER BY next_attempt_at ASC, queued_at ASC
696
+ LIMIT ?
697
+ `).all(now, limit);
698
+ }
699
+ getNextPendingChatPersistenceAt() {
700
+ const row = this.db.prepare(`SELECT MIN(next_attempt_at) AS next_at FROM chat_persistence_jobs WHERE status = 'pending'`).get();
701
+ return row?.next_at ?? null;
702
+ }
703
+ getChatPersistenceHealth(now) {
704
+ const counts = this.db.prepare(`
705
+ SELECT
706
+ SUM(CASE WHEN status = 'pending' THEN 1 ELSE 0 END) AS pending_count,
707
+ SUM(CASE WHEN status = 'in_progress' THEN 1 ELSE 0 END) AS in_progress_count,
708
+ SUM(CASE WHEN status = 'stored' THEN 1 ELSE 0 END) AS stored_count,
709
+ SUM(CASE WHEN status = 'failed' THEN 1 ELSE 0 END) AS failed_count,
710
+ SUM(CASE WHEN status = 'pending' AND next_attempt_at < ? THEN 1 ELSE 0 END) AS overdue_pending_count
711
+ FROM chat_persistence_jobs
712
+ `).get(now);
713
+ const oldest = this.db.prepare(`
714
+ SELECT MIN(queued_at) AS oldest_pending_queued_at
715
+ FROM chat_persistence_jobs
716
+ WHERE status = 'pending'
717
+ `).get();
718
+ return {
719
+ pending_count: counts?.pending_count ?? 0,
720
+ in_progress_count: counts?.in_progress_count ?? 0,
721
+ stored_count: counts?.stored_count ?? 0,
722
+ failed_count: counts?.failed_count ?? 0,
723
+ overdue_pending_count: counts?.overdue_pending_count ?? 0,
724
+ oldest_pending_queued_at: oldest?.oldest_pending_queued_at ?? null,
725
+ };
726
+ }
727
+ // --- Logs ---
728
+ insertLog(entry) {
729
+ this.stmt('insertLog', `
730
+ INSERT INTO logs (ts, level, operation_name, operation_id, module, message)
731
+ VALUES (@ts, @level, @operation_name, @operation_id, @module, @message)
732
+ `).run({
733
+ ts: entry.ts,
734
+ level: entry.level,
735
+ operation_name: entry.operation_name ?? null,
736
+ operation_id: entry.operation_id ?? null,
737
+ module: entry.module,
738
+ message: entry.message,
739
+ });
740
+ }
741
+ searchLogs(opts = {}) {
742
+ if (opts.q) {
743
+ return this.searchLogsFts(opts);
744
+ }
745
+ const wheres = [];
746
+ const params = [];
747
+ if (opts.operationId) {
748
+ wheres.push('operation_id = ?');
749
+ params.push(opts.operationId);
750
+ }
751
+ if (opts.level) {
752
+ wheres.push('level = ?');
753
+ params.push(opts.level);
754
+ }
755
+ if (opts.module) {
756
+ wheres.push('module = ?');
757
+ params.push(opts.module);
758
+ }
759
+ if (opts.from) {
760
+ wheres.push('ts >= ?');
761
+ params.push(opts.from);
762
+ }
763
+ if (opts.to) {
764
+ wheres.push('ts <= ?');
765
+ params.push(opts.to);
766
+ }
767
+ const where = wheres.length ? `WHERE ${wheres.join(' AND ')}` : '';
768
+ const limit = opts.limit ?? 200;
769
+ const offset = opts.offset ?? 0;
770
+ const total = this.db.prepare(`SELECT COUNT(*) as c FROM logs ${where}`).get(...params).c;
771
+ const logs = this.db.prepare(`SELECT * FROM logs ${where} ORDER BY ts DESC LIMIT ? OFFSET ?`).all(...params, limit, offset);
772
+ return { logs, total };
773
+ }
774
+ searchLogsFts(opts) {
775
+ const wheres = ['logs_fts MATCH ?'];
776
+ const params = [opts.q];
777
+ if (opts.operationId) {
778
+ wheres.push('l.operation_id = ?');
779
+ params.push(opts.operationId);
780
+ }
781
+ if (opts.level) {
782
+ wheres.push('l.level = ?');
783
+ params.push(opts.level);
784
+ }
785
+ if (opts.module) {
786
+ wheres.push('l.module = ?');
787
+ params.push(opts.module);
788
+ }
789
+ if (opts.from) {
790
+ wheres.push('l.ts >= ?');
791
+ params.push(opts.from);
792
+ }
793
+ if (opts.to) {
794
+ wheres.push('l.ts <= ?');
795
+ params.push(opts.to);
796
+ }
797
+ const where = wheres.join(' AND ');
798
+ const limit = opts.limit ?? 200;
799
+ const offset = opts.offset ?? 0;
800
+ const total = this.db.prepare(`SELECT COUNT(*) as c FROM logs l JOIN logs_fts ON l.id = logs_fts.rowid WHERE ${where}`).get(...params).c;
801
+ const logs = this.db.prepare(`SELECT l.* FROM logs l JOIN logs_fts ON l.id = logs_fts.rowid WHERE ${where} ORDER BY l.ts DESC LIMIT ? OFFSET ?`).all(...params, limit, offset);
802
+ return { logs, total };
803
+ }
804
+ // --- Query history ---
805
+ insertQueryHistory(entry) {
806
+ this.stmt('insertQueryHistory', `
807
+ INSERT INTO query_history (ts, sparql, duration_ms, result_count, error)
808
+ VALUES (@ts, @sparql, @duration_ms, @result_count, @error)
809
+ `).run({
810
+ ts: Date.now(),
811
+ sparql: entry.sparql,
812
+ duration_ms: entry.duration_ms,
813
+ result_count: entry.result_count ?? null,
814
+ error: entry.error ?? null,
815
+ });
816
+ }
817
+ getQueryHistory(limit = 50, offset = 0) {
818
+ return this.db.prepare('SELECT * FROM query_history ORDER BY ts DESC LIMIT ? OFFSET ?').all(limit, offset);
819
+ }
820
+ // --- Saved queries ---
821
+ getSavedQueries() {
822
+ return this.db.prepare('SELECT * FROM saved_queries ORDER BY updated_at DESC').all();
823
+ }
824
+ insertSavedQuery(entry) {
825
+ const now = Date.now();
826
+ const result = this.db.prepare('INSERT INTO saved_queries (name, description, sparql, created_at, updated_at) VALUES (?, ?, ?, ?, ?)').run(entry.name, entry.description ?? null, entry.sparql, now, now);
827
+ return result.lastInsertRowid;
828
+ }
829
+ updateSavedQuery(id, entry) {
830
+ const sets = ['updated_at = ?'];
831
+ const params = [Date.now()];
832
+ if (entry.name !== undefined) {
833
+ sets.push('name = ?');
834
+ params.push(entry.name);
835
+ }
836
+ if (entry.description !== undefined) {
837
+ sets.push('description = ?');
838
+ params.push(entry.description);
839
+ }
840
+ if (entry.sparql !== undefined) {
841
+ sets.push('sparql = ?');
842
+ params.push(entry.sparql);
843
+ }
844
+ params.push(id);
845
+ this.db.prepare(`UPDATE saved_queries SET ${sets.join(', ')} WHERE id = ?`).run(...params);
846
+ }
847
+ deleteSavedQuery(id) {
848
+ this.db.prepare('DELETE FROM saved_queries WHERE id = ?').run(id);
849
+ }
850
+ // --- Notifications ---
851
+ insertNotification(n) {
852
+ const result = this.stmt('insertNotif', `
853
+ INSERT INTO notifications (ts, type, title, message, source, peer, read, meta)
854
+ VALUES (@ts, @type, @title, @message, @source, @peer, 0, @meta)
855
+ `).run({
856
+ ts: n.ts,
857
+ type: n.type,
858
+ title: n.title,
859
+ message: n.message,
860
+ source: n.source ?? null,
861
+ peer: n.peer ?? null,
862
+ meta: n.meta ?? null,
863
+ });
864
+ return result.lastInsertRowid;
865
+ }
866
+ getNotifications(opts = {}) {
867
+ const limit = opts.limit ?? 100;
868
+ const sinceClause = opts.since ? 'WHERE ts > ?' : '';
869
+ const params = opts.since ? [opts.since] : [];
870
+ const notifications = this.db.prepare(`SELECT * FROM notifications ${sinceClause} ORDER BY ts DESC LIMIT ?`).all(...params, limit);
871
+ const unread = this.db.prepare('SELECT COUNT(*) as c FROM notifications WHERE read = 0').get();
872
+ return { notifications, unreadCount: unread.c };
873
+ }
874
+ markNotificationsRead(ids) {
875
+ if (ids && ids.length > 0) {
876
+ const placeholders = ids.map(() => '?').join(',');
877
+ const result = this.db.prepare(`UPDATE notifications SET read = 1 WHERE id IN (${placeholders}) AND read = 0`).run(...ids);
878
+ return result.changes;
879
+ }
880
+ const result = this.db.prepare('UPDATE notifications SET read = 1 WHERE read = 0').run();
881
+ return result.changes;
882
+ }
883
+ close() {
884
+ this.db.close();
885
+ }
886
+ }
887
+ //# sourceMappingURL=db.js.map