@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.
- package/LICENSE +201 -0
- package/README.md +49 -0
- package/dist/api.d.ts +30 -0
- package/dist/api.d.ts.map +1 -0
- package/dist/api.js +805 -0
- package/dist/api.js.map +1 -0
- package/dist/chat-assistant.d.ts +68 -0
- package/dist/chat-assistant.d.ts.map +1 -0
- package/dist/chat-assistant.js +663 -0
- package/dist/chat-assistant.js.map +1 -0
- package/dist/chat-memory.d.ts +171 -0
- package/dist/chat-memory.d.ts.map +1 -0
- package/dist/chat-memory.js +985 -0
- package/dist/chat-memory.js.map +1 -0
- package/dist/chat-persistence-queue.d.ts +67 -0
- package/dist/chat-persistence-queue.d.ts.map +1 -0
- package/dist/chat-persistence-queue.js +245 -0
- package/dist/chat-persistence-queue.js.map +1 -0
- package/dist/db.d.ts +402 -0
- package/dist/db.d.ts.map +1 -0
- package/dist/db.js +887 -0
- package/dist/db.js.map +1 -0
- package/dist/gelf-push-worker.d.ts +67 -0
- package/dist/gelf-push-worker.d.ts.map +1 -0
- package/dist/gelf-push-worker.js +147 -0
- package/dist/gelf-push-worker.js.map +1 -0
- package/dist/index.d.ts +20 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +12 -0
- package/dist/index.js.map +1 -0
- package/dist/llm/capability-resolver.d.ts +3 -0
- package/dist/llm/capability-resolver.d.ts.map +1 -0
- package/dist/llm/capability-resolver.js +21 -0
- package/dist/llm/capability-resolver.js.map +1 -0
- package/dist/llm/client.d.ts +23 -0
- package/dist/llm/client.d.ts.map +1 -0
- package/dist/llm/client.js +91 -0
- package/dist/llm/client.js.map +1 -0
- package/dist/llm/provider-adapter.d.ts +16 -0
- package/dist/llm/provider-adapter.d.ts.map +1 -0
- package/dist/llm/provider-adapter.js +199 -0
- package/dist/llm/provider-adapter.js.map +1 -0
- package/dist/llm/types.d.ts +64 -0
- package/dist/llm/types.d.ts.map +1 -0
- package/dist/llm/types.js +2 -0
- package/dist/llm/types.js.map +1 -0
- package/dist/metrics-collector.d.ts +36 -0
- package/dist/metrics-collector.d.ts.map +1 -0
- package/dist/metrics-collector.js +155 -0
- package/dist/metrics-collector.js.map +1 -0
- package/dist/operation-tracker.d.ts +43 -0
- package/dist/operation-tracker.d.ts.map +1 -0
- package/dist/operation-tracker.js +195 -0
- package/dist/operation-tracker.js.map +1 -0
- package/dist/structured-logger.d.ts +16 -0
- package/dist/structured-logger.d.ts.map +1 -0
- package/dist/structured-logger.js +41 -0
- package/dist/structured-logger.js.map +1 -0
- package/dist/telemetry.d.ts +35 -0
- package/dist/telemetry.d.ts.map +1 -0
- package/dist/telemetry.js +45 -0
- package/dist/telemetry.js.map +1 -0
- package/dist-ui/assets/3d-force-graph-nMUNmvtB.js +964 -0
- package/dist-ui/assets/AgentHub-XKCM9uYQ.js +65 -0
- package/dist-ui/assets/AppHost-DoLIi89g.js +1 -0
- package/dist-ui/assets/Apps-Cc8HfqfD.js +1 -0
- package/dist-ui/assets/Dashboard-D5q6MK78.js +2 -0
- package/dist-ui/assets/Explorer-B80RVksc.js +64 -0
- package/dist-ui/assets/N3Parser-Q_-1ZY5E.js +7 -0
- package/dist-ui/assets/Settings-CG7-7GM-.js +71 -0
- package/dist-ui/assets/hooks-BLTFNmyP.js +1 -0
- package/dist-ui/assets/index-8_35CUX2.js +192 -0
- package/dist-ui/assets/index-CKZq_ZB-.css +1 -0
- package/dist-ui/assets/index-DH-l6lM0.js +76 -0
- package/dist-ui/assets/jsonld-32FQRO67-DhbO8O6B.js +2 -0
- package/dist-ui/assets/jsonld-BFI4wECl.js +62 -0
- package/dist-ui/assets/ntriples-ZWBY2WET-nIpilpjf.js +1 -0
- package/dist-ui/assets/ordinal-DIohFSkg.js +1 -0
- package/dist-ui/assets/renderer-3d-2EVDZII7-DsxBsJvs.js +2 -0
- package/dist-ui/assets/three.module-uCjFke6H.js +4019 -0
- package/dist-ui/assets/turtle-JJPK7LJ5-zezDJZEp.js +1 -0
- package/dist-ui/favicon.png +0 -0
- package/dist-ui/index.html +14 -0
- 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
|